diff --git a/internal/appearances/appearance.go b/internal/appearances/appearance.go new file mode 100644 index 0000000..a015fc8 --- /dev/null +++ b/internal/appearances/appearance.go @@ -0,0 +1,189 @@ +package appearances + +import ( + "fmt" + + "eq2emu/internal/database" +) + +// Appearance represents a single appearance with ID, name, and client version requirements +type Appearance struct { + ID int32 `json:"id"` // Appearance ID + Name string `json:"name"` // Appearance name + MinClient int16 `json:"min_client"` // Minimum client version required + + db *database.Database `json:"-"` // Database connection + isNew bool `json:"-"` // Whether this is a new appearance +} + +// New creates a new appearance with the given database +func New(db *database.Database) *Appearance { + return &Appearance{ + db: db, + isNew: true, + } +} + +// NewWithData creates a new appearance with data +func NewWithData(id int32, name string, minClientVersion int16, db *database.Database) *Appearance { + return &Appearance{ + ID: id, + Name: name, + MinClient: minClientVersion, + db: db, + isNew: true, + } +} + +// Load loads an appearance by ID from the database +func Load(db *database.Database, id int32) (*Appearance, error) { + appearance := &Appearance{ + db: db, + isNew: false, + } + + query := `SELECT appearance_id, name, min_client_version FROM appearances WHERE appearance_id = ?` + row := db.QueryRow(query, id) + + err := row.Scan(&appearance.ID, &appearance.Name, &appearance.MinClient) + if err != nil { + return nil, fmt.Errorf("failed to load appearance %d: %w", id, err) + } + + return appearance, nil +} + +// GetID returns the appearance ID (implements Identifiable interface) +func (a *Appearance) GetID() int32 { + return a.ID +} + +// GetName returns the appearance name +func (a *Appearance) GetName() string { + return a.Name +} + +// GetMinClientVersion returns the minimum client version required +func (a *Appearance) GetMinClientVersion() int16 { + return a.MinClient +} + +// GetNameString returns the name as a string (alias for GetName for C++ compatibility) +func (a *Appearance) GetNameString() string { + return a.Name +} + +// SetName sets the appearance name +func (a *Appearance) SetName(name string) { + a.Name = name +} + +// SetMinClientVersion sets the minimum client version +func (a *Appearance) SetMinClientVersion(version int16) { + a.MinClient = version +} + +// IsCompatibleWithClient returns true if the appearance is compatible with the given client version +func (a *Appearance) IsCompatibleWithClient(clientVersion int16) bool { + return clientVersion >= a.MinClient +} + +// IsNew returns true if this is a new appearance not yet saved to database +func (a *Appearance) IsNew() bool { + return a.isNew +} + +// Save saves the appearance to the database +func (a *Appearance) Save() error { + if a.db == nil { + return fmt.Errorf("no database connection available") + } + + if a.isNew { + return a.insert() + } + return a.update() +} + +// Delete removes the appearance from the database +func (a *Appearance) Delete() error { + if a.db == nil { + return fmt.Errorf("no database connection available") + } + + if a.isNew { + return fmt.Errorf("cannot delete unsaved appearance") + } + + query := `DELETE FROM appearances WHERE appearance_id = ?` + _, err := a.db.Exec(query, a.ID) + if err != nil { + return fmt.Errorf("failed to delete appearance %d: %w", a.ID, err) + } + + return nil +} + +// Reload reloads the appearance data from the database +func (a *Appearance) Reload() error { + if a.db == nil { + return fmt.Errorf("no database connection available") + } + + if a.isNew { + return fmt.Errorf("cannot reload unsaved appearance") + } + + query := `SELECT name, min_client_version FROM appearances WHERE appearance_id = ?` + row := a.db.QueryRow(query, a.ID) + + err := row.Scan(&a.Name, &a.MinClient) + if err != nil { + return fmt.Errorf("failed to reload appearance %d: %w", a.ID, err) + } + + return nil +} + +// Clone creates a copy of the appearance +func (a *Appearance) Clone() *Appearance { + return &Appearance{ + ID: a.ID, + Name: a.Name, + MinClient: a.MinClient, + db: a.db, + isNew: true, // Clone is always new + } +} + +// insert inserts a new appearance into the database +func (a *Appearance) insert() error { + query := `INSERT INTO appearances (appearance_id, name, min_client_version) VALUES (?, ?, ?)` + _, err := a.db.Exec(query, a.ID, a.Name, a.MinClient) + if err != nil { + return fmt.Errorf("failed to insert appearance: %w", err) + } + + a.isNew = false + return nil +} + +// update updates an existing appearance in the database +func (a *Appearance) update() error { + query := `UPDATE appearances SET name = ?, min_client_version = ? WHERE appearance_id = ?` + result, err := a.db.Exec(query, a.Name, a.MinClient, a.ID) + if err != nil { + return fmt.Errorf("failed to update appearance: %w", err) + } + + rowsAffected, err := result.RowsAffected() + if err != nil { + return fmt.Errorf("failed to get rows affected: %w", err) + } + + if rowsAffected == 0 { + return fmt.Errorf("appearance %d not found for update", a.ID) + } + + return nil +} diff --git a/internal/appearances/appearance_test.go b/internal/appearances/appearance_test.go new file mode 100644 index 0000000..ecbfa19 --- /dev/null +++ b/internal/appearances/appearance_test.go @@ -0,0 +1,221 @@ +package appearances + +import ( + "testing" + + "eq2emu/internal/database" +) + +func TestNew(t *testing.T) { + db, err := database.NewSQLite("file::memory:?mode=memory&cache=shared") + if err != nil { + t.Fatalf("Failed to create test database: %v", err) + } + defer db.Close() + + // Test creating a new appearance + appearance := New(db) + if appearance == nil { + t.Fatal("New returned nil") + } + + if !appearance.IsNew() { + t.Error("New appearance should be marked as new") + } + + // Test setting values + appearance.ID = 1001 + appearance.Name = "Test Appearance" + appearance.MinClient = 1096 + + if appearance.GetID() != 1001 { + t.Errorf("Expected GetID() to return 1001, got %d", appearance.GetID()) + } + + if appearance.GetName() != "Test Appearance" { + t.Errorf("Expected GetName() to return 'Test Appearance', got %s", appearance.GetName()) + } + + if appearance.GetMinClientVersion() != 1096 { + t.Errorf("Expected GetMinClientVersion() to return 1096, got %d", appearance.GetMinClientVersion()) + } +} + +func TestNewWithData(t *testing.T) { + db, err := database.NewSQLite("file::memory:?mode=memory&cache=shared") + if err != nil { + t.Fatalf("Failed to create test database: %v", err) + } + defer db.Close() + + appearance := NewWithData(100, "Human Male", 1096, db) + if appearance == nil { + t.Fatal("NewWithData returned nil") + } + + if appearance.GetID() != 100 { + t.Errorf("Expected ID 100, got %d", appearance.GetID()) + } + + if appearance.GetName() != "Human Male" { + t.Errorf("Expected name 'Human Male', got '%s'", appearance.GetName()) + } + + if appearance.GetMinClientVersion() != 1096 { + t.Errorf("Expected min client 1096, got %d", appearance.GetMinClientVersion()) + } + + if !appearance.IsNew() { + t.Error("NewWithData should create new appearance") + } +} + +func TestAppearanceGetters(t *testing.T) { + db, _ := database.NewSQLite("file::memory:?mode=memory&cache=shared") + defer db.Close() + + app := NewWithData(123, "Test Appearance", 1096, db) + + if id := app.GetID(); id != 123 { + t.Errorf("GetID() = %v, want 123", id) + } + + if name := app.GetName(); name != "Test Appearance" { + t.Errorf("GetName() = %v, want Test Appearance", name) + } + + if nameStr := app.GetNameString(); nameStr != "Test Appearance" { + t.Errorf("GetNameString() = %v, want Test Appearance", nameStr) + } + + if minVer := app.GetMinClientVersion(); minVer != 1096 { + t.Errorf("GetMinClientVersion() = %v, want 1096", minVer) + } +} + +func TestAppearanceSetters(t *testing.T) { + db, _ := database.NewSQLite("file::memory:?mode=memory&cache=shared") + defer db.Close() + + app := NewWithData(100, "Original", 1000, db) + + app.SetName("Modified Name") + if app.GetName() != "Modified Name" { + t.Errorf("SetName failed: got %v, want Modified Name", app.GetName()) + } + + app.SetMinClientVersion(2000) + if app.GetMinClientVersion() != 2000 { + t.Errorf("SetMinClientVersion failed: got %v, want 2000", app.GetMinClientVersion()) + } +} + +func TestIsCompatibleWithClient(t *testing.T) { + db, _ := database.NewSQLite("file::memory:?mode=memory&cache=shared") + defer db.Close() + + app := NewWithData(100, "Test", 1096, db) + + tests := []struct { + clientVersion int16 + want bool + }{ + {1095, false}, // Below minimum + {1096, true}, // Exact minimum + {1097, true}, // Above minimum + {2000, true}, // Well above minimum + {0, false}, // Zero version + } + + for _, tt := range tests { + t.Run("", func(t *testing.T) { + if got := app.IsCompatibleWithClient(tt.clientVersion); got != tt.want { + t.Errorf("IsCompatibleWithClient(%v) = %v, want %v", tt.clientVersion, got, tt.want) + } + }) + } +} + +func TestAppearanceClone(t *testing.T) { + db, _ := database.NewSQLite("file::memory:?mode=memory&cache=shared") + defer db.Close() + + original := NewWithData(500, "Original Appearance", 1200, db) + clone := original.Clone() + + if clone == nil { + t.Fatal("Clone returned nil") + } + + if clone == original { + t.Error("Clone returned same pointer as original") + } + + if clone.GetID() != original.GetID() { + t.Errorf("Clone ID = %v, want %v", clone.GetID(), original.GetID()) + } + + if clone.GetName() != original.GetName() { + t.Errorf("Clone Name = %v, want %v", clone.GetName(), original.GetName()) + } + + if clone.GetMinClientVersion() != original.GetMinClientVersion() { + t.Errorf("Clone MinClientVersion = %v, want %v", clone.GetMinClientVersion(), original.GetMinClientVersion()) + } + + if !clone.IsNew() { + t.Error("Clone should always be marked as new") + } + + // Verify modification independence + clone.SetName("Modified Clone") + if original.GetName() == "Modified Clone" { + t.Error("Modifying clone affected original") + } +} + +// Test appearance type functions +func TestGetAppearanceType(t *testing.T) { + tests := []struct { + typeName string + expected int8 + }{ + {"hair_color1", AppearanceHairColor1}, + {"soga_hair_color1", AppearanceSOGAHairColor1}, + {"skin_color", AppearanceSkinColor}, + {"eye_color", AppearanceEyeColor}, + {"unknown_type", -1}, + } + + for _, tt := range tests { + t.Run(tt.typeName, func(t *testing.T) { + result := GetAppearanceType(tt.typeName) + if result != tt.expected { + t.Errorf("GetAppearanceType(%q) = %d, want %d", tt.typeName, result, tt.expected) + } + }) + } +} + +func TestGetAppearanceTypeName(t *testing.T) { + tests := []struct { + typeConst int8 + expected string + }{ + {AppearanceHairColor1, "hair_color1"}, + {AppearanceSOGAHairColor1, "soga_hair_color1"}, + {AppearanceSkinColor, "skin_color"}, + {AppearanceEyeColor, "eye_color"}, + {-1, "unknown"}, + {100, "unknown"}, + } + + for _, tt := range tests { + t.Run("", func(t *testing.T) { + result := GetAppearanceTypeName(tt.typeConst) + if result != tt.expected { + t.Errorf("GetAppearanceTypeName(%d) = %q, want %q", tt.typeConst, result, tt.expected) + } + }) + } +} diff --git a/internal/appearances/appearances.go b/internal/appearances/appearances.go deleted file mode 100644 index b97dbdd..0000000 --- a/internal/appearances/appearances.go +++ /dev/null @@ -1,288 +0,0 @@ -package appearances - -import ( - "fmt" - "sync" -) - -// Appearances manages a collection of appearance objects with thread-safe operations -type Appearances struct { - appearanceMap map[int32]*Appearance // Map of appearance ID to appearance - mutex sync.RWMutex // Thread safety for concurrent access -} - -// NewAppearances creates a new appearances manager -func NewAppearances() *Appearances { - return &Appearances{ - appearanceMap: make(map[int32]*Appearance), - } -} - -// Reset clears all appearances from the manager -func (a *Appearances) Reset() { - a.ClearAppearances() -} - -// ClearAppearances removes all appearances from the manager -func (a *Appearances) ClearAppearances() { - a.mutex.Lock() - defer a.mutex.Unlock() - - // Clear the map - Go's garbage collector will handle cleanup - a.appearanceMap = make(map[int32]*Appearance) -} - -// InsertAppearance adds an appearance to the manager -func (a *Appearances) InsertAppearance(appearance *Appearance) error { - if appearance == nil { - return fmt.Errorf("appearance cannot be nil") - } - - a.mutex.Lock() - defer a.mutex.Unlock() - - a.appearanceMap[appearance.GetID()] = appearance - return nil -} - -// FindAppearanceByID retrieves an appearance by its ID -func (a *Appearances) FindAppearanceByID(id int32) *Appearance { - a.mutex.RLock() - defer a.mutex.RUnlock() - - if appearance, exists := a.appearanceMap[id]; exists { - return appearance - } - - return nil -} - -// HasAppearance checks if an appearance exists by ID -func (a *Appearances) HasAppearance(id int32) bool { - a.mutex.RLock() - defer a.mutex.RUnlock() - - _, exists := a.appearanceMap[id] - return exists -} - -// GetAppearanceCount returns the total number of appearances -func (a *Appearances) GetAppearanceCount() int { - a.mutex.RLock() - defer a.mutex.RUnlock() - - return len(a.appearanceMap) -} - -// GetAllAppearances returns a copy of all appearances -func (a *Appearances) GetAllAppearances() map[int32]*Appearance { - a.mutex.RLock() - defer a.mutex.RUnlock() - - // Return a copy to prevent external modification - result := make(map[int32]*Appearance) - for id, appearance := range a.appearanceMap { - result[id] = appearance - } - - return result -} - -// GetAppearanceIDs returns all appearance IDs -func (a *Appearances) GetAppearanceIDs() []int32 { - a.mutex.RLock() - defer a.mutex.RUnlock() - - ids := make([]int32, 0, len(a.appearanceMap)) - for id := range a.appearanceMap { - ids = append(ids, id) - } - - return ids -} - -// FindAppearancesByName finds all appearances with names containing the given substring -func (a *Appearances) FindAppearancesByName(nameSubstring string) []*Appearance { - a.mutex.RLock() - defer a.mutex.RUnlock() - - var results []*Appearance - - for _, appearance := range a.appearanceMap { - if contains(appearance.GetName(), nameSubstring) { - results = append(results, appearance) - } - } - - return results -} - -// FindAppearancesByMinClient finds all appearances with a specific minimum client version -func (a *Appearances) FindAppearancesByMinClient(minClient int16) []*Appearance { - a.mutex.RLock() - defer a.mutex.RUnlock() - - var results []*Appearance - - for _, appearance := range a.appearanceMap { - if appearance.GetMinClientVersion() == minClient { - results = append(results, appearance) - } - } - - return results -} - -// GetCompatibleAppearances returns all appearances compatible with the given client version -func (a *Appearances) GetCompatibleAppearances(clientVersion int16) []*Appearance { - a.mutex.RLock() - defer a.mutex.RUnlock() - - var results []*Appearance - - for _, appearance := range a.appearanceMap { - if appearance.IsCompatibleWithClient(clientVersion) { - results = append(results, appearance) - } - } - - return results -} - -// RemoveAppearance removes an appearance by ID -func (a *Appearances) RemoveAppearance(id int32) bool { - a.mutex.Lock() - defer a.mutex.Unlock() - - if _, exists := a.appearanceMap[id]; exists { - delete(a.appearanceMap, id) - return true - } - - return false -} - -// UpdateAppearance updates an existing appearance or inserts it if it doesn't exist -func (a *Appearances) UpdateAppearance(appearance *Appearance) error { - if appearance == nil { - return fmt.Errorf("appearance cannot be nil") - } - - a.mutex.Lock() - defer a.mutex.Unlock() - - a.appearanceMap[appearance.GetID()] = appearance - return nil -} - -// GetAppearancesByIDRange returns all appearances within the given ID range (inclusive) -func (a *Appearances) GetAppearancesByIDRange(minID, maxID int32) []*Appearance { - a.mutex.RLock() - defer a.mutex.RUnlock() - - var results []*Appearance - - for id, appearance := range a.appearanceMap { - if id >= minID && id <= maxID { - results = append(results, appearance) - } - } - - return results -} - -// ValidateAppearances checks all appearances for consistency -func (a *Appearances) ValidateAppearances() []string { - a.mutex.RLock() - defer a.mutex.RUnlock() - - var issues []string - - for id, appearance := range a.appearanceMap { - if appearance == nil { - issues = append(issues, fmt.Sprintf("Appearance ID %d is nil", id)) - continue - } - - if appearance.GetID() != id { - issues = append(issues, fmt.Sprintf("Appearance ID mismatch: map key %d != appearance ID %d", id, appearance.GetID())) - } - - if len(appearance.GetName()) == 0 { - issues = append(issues, fmt.Sprintf("Appearance ID %d has empty name", id)) - } - - if appearance.GetMinClientVersion() < 0 { - issues = append(issues, fmt.Sprintf("Appearance ID %d has negative min client version: %d", id, appearance.GetMinClientVersion())) - } - } - - return issues -} - -// IsValid returns true if all appearances are valid -func (a *Appearances) IsValid() bool { - issues := a.ValidateAppearances() - return len(issues) == 0 -} - -// GetStatistics returns statistics about the appearance collection -func (a *Appearances) GetStatistics() map[string]any { - a.mutex.RLock() - defer a.mutex.RUnlock() - - stats := make(map[string]any) - stats["total_appearances"] = len(a.appearanceMap) - - // Count by minimum client version - versionCounts := make(map[int16]int) - for _, appearance := range a.appearanceMap { - versionCounts[appearance.GetMinClientVersion()]++ - } - stats["appearances_by_min_client"] = versionCounts - - // Find ID range - if len(a.appearanceMap) > 0 { - var minID, maxID int32 - first := true - - for id := range a.appearanceMap { - if first { - minID = id - maxID = id - first = false - } else { - if id < minID { - minID = id - } - if id > maxID { - maxID = id - } - } - } - - stats["min_id"] = minID - stats["max_id"] = maxID - stats["id_range"] = maxID - minID - } - - return stats -} - -// contains checks if a string contains a substring (case-sensitive) -func contains(str, substr string) bool { - if len(substr) == 0 { - return true - } - if len(str) < len(substr) { - return false - } - - for i := 0; i <= len(str)-len(substr); i++ { - if str[i:i+len(substr)] == substr { - return true - } - } - - return false -} diff --git a/internal/appearances/appearances_test.go b/internal/appearances/appearances_test.go deleted file mode 100644 index dbf6e5a..0000000 --- a/internal/appearances/appearances_test.go +++ /dev/null @@ -1,1550 +0,0 @@ -package appearances - -import ( - "fmt" - "sync" - "testing" -) - -func TestNewAppearance(t *testing.T) { - tests := []struct { - name string - id int32 - appearanceName string - minClientVersion int16 - wantNil bool - }{ - { - name: "valid appearance", - id: 100, - appearanceName: "Test Appearance", - minClientVersion: 1096, - wantNil: false, - }, - { - name: "empty name returns nil", - id: 200, - appearanceName: "", - minClientVersion: 1096, - wantNil: true, - }, - { - name: "negative id allowed", - id: -50, - appearanceName: "Negative ID", - minClientVersion: 0, - wantNil: false, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - app := NewAppearance(tt.id, tt.appearanceName, tt.minClientVersion) - - if tt.wantNil { - if app != nil { - t.Errorf("expected nil appearance, got %v", app) - } - return - } - - if app == nil { - t.Fatal("expected non-nil appearance, got nil") - } - - if app.GetID() != tt.id { - t.Errorf("ID = %v, want %v", app.GetID(), tt.id) - } - - if app.GetName() != tt.appearanceName { - t.Errorf("Name = %v, want %v", app.GetName(), tt.appearanceName) - } - - if app.GetMinClientVersion() != tt.minClientVersion { - t.Errorf("MinClientVersion = %v, want %v", app.GetMinClientVersion(), tt.minClientVersion) - } - }) - } -} - -func TestAppearanceGetters(t *testing.T) { - app := NewAppearance(123, "Test Appearance", 1096) - - if id := app.GetID(); id != 123 { - t.Errorf("GetID() = %v, want 123", id) - } - - if name := app.GetName(); name != "Test Appearance" { - t.Errorf("GetName() = %v, want Test Appearance", name) - } - - if nameStr := app.GetNameString(); nameStr != "Test Appearance" { - t.Errorf("GetNameString() = %v, want Test Appearance", nameStr) - } - - if minVer := app.GetMinClientVersion(); minVer != 1096 { - t.Errorf("GetMinClientVersion() = %v, want 1096", minVer) - } -} - -func TestAppearanceSetters(t *testing.T) { - app := NewAppearance(100, "Original", 1000) - - app.SetName("Modified Name") - if app.GetName() != "Modified Name" { - t.Errorf("SetName failed: got %v, want Modified Name", app.GetName()) - } - - app.SetMinClientVersion(2000) - if app.GetMinClientVersion() != 2000 { - t.Errorf("SetMinClientVersion failed: got %v, want 2000", app.GetMinClientVersion()) - } -} - -func TestIsCompatibleWithClient(t *testing.T) { - app := NewAppearance(100, "Test", 1096) - - tests := []struct { - clientVersion int16 - want bool - }{ - {1095, false}, // Below minimum - {1096, true}, // Exact minimum - {1097, true}, // Above minimum - {2000, true}, // Well above minimum - {0, false}, // Zero version - } - - for _, tt := range tests { - t.Run("", func(t *testing.T) { - if got := app.IsCompatibleWithClient(tt.clientVersion); got != tt.want { - t.Errorf("IsCompatibleWithClient(%v) = %v, want %v", tt.clientVersion, got, tt.want) - } - }) - } -} - -func TestAppearanceClone(t *testing.T) { - original := NewAppearance(500, "Original Appearance", 1200) - clone := original.Clone() - - if clone == nil { - t.Fatal("Clone returned nil") - } - - if clone == original { - t.Error("Clone returned same pointer as original") - } - - if clone.GetID() != original.GetID() { - t.Errorf("Clone ID = %v, want %v", clone.GetID(), original.GetID()) - } - - if clone.GetName() != original.GetName() { - t.Errorf("Clone Name = %v, want %v", clone.GetName(), original.GetName()) - } - - if clone.GetMinClientVersion() != original.GetMinClientVersion() { - t.Errorf("Clone MinClientVersion = %v, want %v", clone.GetMinClientVersion(), original.GetMinClientVersion()) - } - - // Verify modification independence - clone.SetName("Modified Clone") - if original.GetName() == "Modified Clone" { - t.Error("Modifying clone affected original") - } -} - -func TestNewAppearances(t *testing.T) { - apps := NewAppearances() - - if apps == nil { - t.Fatal("NewAppearances returned nil") - } - - if count := apps.GetAppearanceCount(); count != 0 { - t.Errorf("New appearances collection should be empty, got count %v", count) - } -} - -func TestAppearancesInsertAndFind(t *testing.T) { - apps := NewAppearances() - - // Test nil insertion - err := apps.InsertAppearance(nil) - if err == nil { - t.Error("InsertAppearance(nil) should return error") - } - - // Insert valid appearances - app1 := NewAppearance(100, "Appearance 1", 1000) - app2 := NewAppearance(200, "Appearance 2", 1100) - - if err := apps.InsertAppearance(app1); err != nil { - t.Errorf("InsertAppearance failed: %v", err) - } - - if err := apps.InsertAppearance(app2); err != nil { - t.Errorf("InsertAppearance failed: %v", err) - } - - // Test finding by ID - found := apps.FindAppearanceByID(100) - if found == nil { - t.Error("FindAppearanceByID(100) returned nil") - } else if found.GetName() != "Appearance 1" { - t.Errorf("FindAppearanceByID(100) returned wrong appearance: %v", found.GetName()) - } - - // Test finding non-existent ID - notFound := apps.FindAppearanceByID(999) - if notFound != nil { - t.Errorf("FindAppearanceByID(999) should return nil, got %v", notFound) - } -} - -func TestAppearancesHasAppearance(t *testing.T) { - apps := NewAppearances() - app := NewAppearance(300, "Test", 1000) - apps.InsertAppearance(app) - - if !apps.HasAppearance(300) { - t.Error("HasAppearance(300) should return true") - } - - if apps.HasAppearance(999) { - t.Error("HasAppearance(999) should return false") - } -} - -func TestAppearancesGetAllAndCount(t *testing.T) { - apps := NewAppearances() - - // Add multiple appearances - for i := int32(1); i <= 5; i++ { - app := NewAppearance(i*100, "Appearance", 1000) - apps.InsertAppearance(app) - } - - if count := apps.GetAppearanceCount(); count != 5 { - t.Errorf("GetAppearanceCount() = %v, want 5", count) - } - - all := apps.GetAllAppearances() - if len(all) != 5 { - t.Errorf("GetAllAppearances() returned %v items, want 5", len(all)) - } - - // Verify it's a copy by modifying returned map - delete(all, 100) - if apps.GetAppearanceCount() != 5 { - t.Error("Modifying returned map affected internal state") - } -} - -func TestAppearancesGetIDs(t *testing.T) { - apps := NewAppearances() - expectedIDs := []int32{100, 200, 300} - - for _, id := range expectedIDs { - app := NewAppearance(id, "Test", 1000) - apps.InsertAppearance(app) - } - - ids := apps.GetAppearanceIDs() - if len(ids) != len(expectedIDs) { - t.Errorf("GetAppearanceIDs() returned %v IDs, want %v", len(ids), len(expectedIDs)) - } - - // Check all expected IDs are present - idMap := make(map[int32]bool) - for _, id := range ids { - idMap[id] = true - } - - for _, expected := range expectedIDs { - if !idMap[expected] { - t.Errorf("Expected ID %v not found in returned IDs", expected) - } - } -} - -func TestAppearancesFindByName(t *testing.T) { - apps := NewAppearances() - - apps.InsertAppearance(NewAppearance(1, "Human Male", 1000)) - apps.InsertAppearance(NewAppearance(2, "Human Female", 1000)) - apps.InsertAppearance(NewAppearance(3, "Elf Male", 1000)) - apps.InsertAppearance(NewAppearance(4, "Dwarf Female", 1000)) - - // Test partial matching - humanApps := apps.FindAppearancesByName("Human") - if len(humanApps) != 2 { - t.Errorf("FindAppearancesByName('Human') returned %v results, want 2", len(humanApps)) - } - - // Test case sensitivity - maleApps := apps.FindAppearancesByName("Male") - if len(maleApps) != 2 { - t.Errorf("FindAppearancesByName('Male') returned %v results, want 2", len(maleApps)) - } - - // Test empty substring - allApps := apps.FindAppearancesByName("") - if len(allApps) != 4 { - t.Errorf("FindAppearancesByName('') returned %v results, want 4", len(allApps)) - } - - // Test no matches - noMatches := apps.FindAppearancesByName("Orc") - if len(noMatches) != 0 { - t.Errorf("FindAppearancesByName('Orc') returned %v results, want 0", len(noMatches)) - } -} - -func TestAppearancesFindByMinClient(t *testing.T) { - apps := NewAppearances() - - apps.InsertAppearance(NewAppearance(1, "Old", 1000)) - apps.InsertAppearance(NewAppearance(2, "Medium1", 1096)) - apps.InsertAppearance(NewAppearance(3, "Medium2", 1096)) - apps.InsertAppearance(NewAppearance(4, "New", 1200)) - - results := apps.FindAppearancesByMinClient(1096) - if len(results) != 2 { - t.Errorf("FindAppearancesByMinClient(1096) returned %v results, want 2", len(results)) - } - - results = apps.FindAppearancesByMinClient(999) - if len(results) != 0 { - t.Errorf("FindAppearancesByMinClient(999) returned %v results, want 0", len(results)) - } -} - -func TestAppearancesGetCompatible(t *testing.T) { - apps := NewAppearances() - - apps.InsertAppearance(NewAppearance(1, "Old", 1000)) - apps.InsertAppearance(NewAppearance(2, "Medium", 1096)) - apps.InsertAppearance(NewAppearance(3, "New", 1200)) - apps.InsertAppearance(NewAppearance(4, "Newer", 1300)) - - // Client version 1100 should get Old and Medium - compatible := apps.GetCompatibleAppearances(1100) - if len(compatible) != 2 { - t.Errorf("GetCompatibleAppearances(1100) returned %v results, want 2", len(compatible)) - } - - // Client version 1500 should get all - compatible = apps.GetCompatibleAppearances(1500) - if len(compatible) != 4 { - t.Errorf("GetCompatibleAppearances(1500) returned %v results, want 4", len(compatible)) - } - - // Client version 500 should get none - compatible = apps.GetCompatibleAppearances(500) - if len(compatible) != 0 { - t.Errorf("GetCompatibleAppearances(500) returned %v results, want 0", len(compatible)) - } -} - -func TestAppearancesRemove(t *testing.T) { - apps := NewAppearances() - app := NewAppearance(100, "Test", 1000) - apps.InsertAppearance(app) - - // Remove existing - if !apps.RemoveAppearance(100) { - t.Error("RemoveAppearance(100) should return true") - } - - if apps.HasAppearance(100) { - t.Error("Appearance 100 should have been removed") - } - - // Remove non-existent - if apps.RemoveAppearance(100) { - t.Error("RemoveAppearance(100) should return false for non-existent ID") - } -} - -func TestAppearancesUpdate(t *testing.T) { - apps := NewAppearances() - - // Test updating nil - err := apps.UpdateAppearance(nil) - if err == nil { - t.Error("UpdateAppearance(nil) should return error") - } - - // Insert and update - original := NewAppearance(100, "Original", 1000) - apps.InsertAppearance(original) - - updated := NewAppearance(100, "Updated", 1100) - err = apps.UpdateAppearance(updated) - if err != nil { - t.Errorf("UpdateAppearance failed: %v", err) - } - - found := apps.FindAppearanceByID(100) - if found.GetName() != "Updated" { - t.Errorf("Updated appearance name = %v, want Updated", found.GetName()) - } - - // Update non-existent (should insert) - new := NewAppearance(200, "New", 1200) - err = apps.UpdateAppearance(new) - if err != nil { - t.Errorf("UpdateAppearance failed: %v", err) - } - - if !apps.HasAppearance(200) { - t.Error("UpdateAppearance should insert non-existent appearance") - } -} - -func TestAppearancesGetByIDRange(t *testing.T) { - apps := NewAppearances() - - // Insert appearances with various IDs - for _, id := range []int32{5, 10, 15, 20, 25, 30} { - apps.InsertAppearance(NewAppearance(id, "Test", 1000)) - } - - results := apps.GetAppearancesByIDRange(10, 20) - if len(results) != 3 { // Should get 10, 15, 20 - t.Errorf("GetAppearancesByIDRange(10, 20) returned %v results, want 3", len(results)) - } - - // Verify correct IDs - idMap := make(map[int32]bool) - for _, app := range results { - idMap[app.GetID()] = true - } - - for _, expectedID := range []int32{10, 15, 20} { - if !idMap[expectedID] { - t.Errorf("Expected ID %v not found in range results", expectedID) - } - } - - // Test empty range - results = apps.GetAppearancesByIDRange(100, 200) - if len(results) != 0 { - t.Errorf("GetAppearancesByIDRange(100, 200) returned %v results, want 0", len(results)) - } -} - -func TestAppearancesValidate(t *testing.T) { - apps := NewAppearances() - - // Valid appearances - apps.InsertAppearance(NewAppearance(100, "Valid", 1000)) - - issues := apps.ValidateAppearances() - if len(issues) != 0 { - t.Errorf("ValidateAppearances() returned issues for valid data: %v", issues) - } - - if !apps.IsValid() { - t.Error("IsValid() should return true for valid data") - } - - // Force invalid state by directly modifying map - apps.mutex.Lock() - apps.appearanceMap[200] = nil - apps.appearanceMap[300] = NewAppearance(301, "", 1000) // ID mismatch and empty name - apps.appearanceMap[400] = NewAppearance(400, "Negative", -100) - apps.mutex.Unlock() - - issues = apps.ValidateAppearances() - if len(issues) < 3 { - t.Errorf("ValidateAppearances() should return at least 3 issues, got %v", len(issues)) - } - - if apps.IsValid() { - t.Error("IsValid() should return false for invalid data") - } -} - -func TestAppearancesStatistics(t *testing.T) { - apps := NewAppearances() - - // Add appearances with different client versions - apps.InsertAppearance(NewAppearance(10, "A", 1000)) - apps.InsertAppearance(NewAppearance(20, "B", 1000)) - apps.InsertAppearance(NewAppearance(30, "C", 1096)) - apps.InsertAppearance(NewAppearance(40, "D", 1096)) - apps.InsertAppearance(NewAppearance(50, "E", 1096)) - - stats := apps.GetStatistics() - - if total, ok := stats["total_appearances"].(int); !ok || total != 5 { - t.Errorf("total_appearances = %v, want 5", stats["total_appearances"]) - } - - if minID, ok := stats["min_id"].(int32); !ok || minID != 10 { - t.Errorf("min_id = %v, want 10", stats["min_id"]) - } - - if maxID, ok := stats["max_id"].(int32); !ok || maxID != 50 { - t.Errorf("max_id = %v, want 50", stats["max_id"]) - } - - if idRange, ok := stats["id_range"].(int32); !ok || idRange != 40 { - t.Errorf("id_range = %v, want 40", stats["id_range"]) - } - - if versionCounts, ok := stats["appearances_by_min_client"].(map[int16]int); ok { - if versionCounts[1000] != 2 { - t.Errorf("appearances with min client 1000 = %v, want 2", versionCounts[1000]) - } - if versionCounts[1096] != 3 { - t.Errorf("appearances with min client 1096 = %v, want 3", versionCounts[1096]) - } - } else { - t.Error("appearances_by_min_client not found in statistics") - } -} - -func TestAppearancesClearAndReset(t *testing.T) { - apps := NewAppearances() - - // Add some appearances - for i := int32(1); i <= 3; i++ { - apps.InsertAppearance(NewAppearance(i*100, "Test", 1000)) - } - - if apps.GetAppearanceCount() != 3 { - t.Error("Setup failed: should have 3 appearances") - } - - // Test ClearAppearances - apps.ClearAppearances() - if apps.GetAppearanceCount() != 0 { - t.Errorf("ClearAppearances() failed: count = %v, want 0", apps.GetAppearanceCount()) - } - - // Add again and test Reset - for i := int32(1); i <= 3; i++ { - apps.InsertAppearance(NewAppearance(i*100, "Test", 1000)) - } - - apps.Reset() - if apps.GetAppearanceCount() != 0 { - t.Errorf("Reset() failed: count = %v, want 0", apps.GetAppearanceCount()) - } -} - -func TestAppearancesConcurrency(t *testing.T) { - apps := NewAppearances() - var wg sync.WaitGroup - - // Concurrent insertions - for i := 0; i < 100; i++ { - wg.Add(1) - go func(id int32) { - defer wg.Done() - app := NewAppearance(id, "Concurrent", 1000) - apps.InsertAppearance(app) - }(int32(i)) - } - - // Concurrent reads - for i := 0; i < 50; i++ { - wg.Add(1) - go func() { - defer wg.Done() - _ = apps.GetAppearanceCount() - _ = apps.GetAllAppearances() - _ = apps.FindAppearanceByID(25) - }() - } - - // Concurrent searches - for i := 0; i < 20; i++ { - wg.Add(1) - go func() { - defer wg.Done() - _ = apps.FindAppearancesByName("Concurrent") - _ = apps.GetCompatibleAppearances(1100) - }() - } - - wg.Wait() - - // Verify all insertions succeeded - if count := apps.GetAppearanceCount(); count != 100 { - t.Errorf("After concurrent operations, count = %v, want 100", count) - } -} - -func TestContainsFunction(t *testing.T) { - tests := []struct { - str string - substr string - want bool - }{ - {"hello world", "world", true}, - {"hello world", "World", false}, // Case sensitive - {"hello", "hello world", false}, - {"hello", "", true}, - {"", "hello", false}, - {"", "", true}, - {"abcdef", "cde", true}, - {"abcdef", "xyz", false}, - } - - for _, tt := range tests { - t.Run("", func(t *testing.T) { - if got := contains(tt.str, tt.substr); got != tt.want { - t.Errorf("contains(%q, %q) = %v, want %v", tt.str, tt.substr, got, tt.want) - } - }) - } -} - -// Benchmarks -func BenchmarkAppearanceInsert(b *testing.B) { - apps := NewAppearances() - - b.ResetTimer() - for i := 0; i < b.N; i++ { - app := NewAppearance(int32(i), "Benchmark", 1000) - apps.InsertAppearance(app) - } -} - -func BenchmarkAppearanceFindByID(b *testing.B) { - apps := NewAppearances() - - // Pre-populate - for i := 0; i < 10000; i++ { - app := NewAppearance(int32(i), "Benchmark", 1000) - apps.InsertAppearance(app) - } - - b.ResetTimer() - for i := 0; i < b.N; i++ { - apps.FindAppearanceByID(int32(i % 10000)) - } -} - -func BenchmarkAppearanceFindByName(b *testing.B) { - apps := NewAppearances() - - // Pre-populate with varied names - names := []string{"Human Male", "Human Female", "Elf Male", "Elf Female", "Dwarf Male"} - for i := 0; i < 1000; i++ { - app := NewAppearance(int32(i), names[i%len(names)], 1000) - apps.InsertAppearance(app) - } - - b.ResetTimer() - for i := 0; i < b.N; i++ { - apps.FindAppearancesByName("Male") - } -} - -// Mock implementations -type MockDatabase struct { - appearances []*Appearance - saveError error - loadError error - deleteError error -} - -func (m *MockDatabase) LoadAllAppearances() ([]*Appearance, error) { - if m.loadError != nil { - return nil, m.loadError - } - return m.appearances, nil -} - -func (m *MockDatabase) SaveAppearance(appearance *Appearance) error { - return m.saveError -} - -func (m *MockDatabase) DeleteAppearance(id int32) error { - return m.deleteError -} - -func (m *MockDatabase) LoadAppearancesByClientVersion(minClientVersion int16) ([]*Appearance, error) { - var results []*Appearance - for _, app := range m.appearances { - if app.GetMinClientVersion() == minClientVersion { - results = append(results, app) - } - } - return results, nil -} - -type MockLogger struct { - logs []string -} - -func (m *MockLogger) LogInfo(message string, args ...any) { - m.logs = append(m.logs, fmt.Sprintf("INFO: "+message, args...)) -} - -func (m *MockLogger) LogError(message string, args ...any) { - m.logs = append(m.logs, fmt.Sprintf("ERROR: "+message, args...)) -} - -func (m *MockLogger) LogDebug(message string, args ...any) { - m.logs = append(m.logs, fmt.Sprintf("DEBUG: "+message, args...)) -} - -func (m *MockLogger) LogWarning(message string, args ...any) { - m.logs = append(m.logs, fmt.Sprintf("WARNING: "+message, args...)) -} - -type MockEntity struct { - id int32 - name string - databaseID int32 -} - -func (m *MockEntity) GetID() int32 { return m.id } -func (m *MockEntity) GetName() string { return m.name } -func (m *MockEntity) GetDatabaseID() int32 { return m.databaseID } - -type MockClient struct { - version int16 - sendError error -} - -func (m *MockClient) GetVersion() int16 { return m.version } -func (m *MockClient) SendAppearanceUpdate(appearanceID int32) error { return m.sendError } - -// Manager tests -func TestNewManager(t *testing.T) { - db := &MockDatabase{} - logger := &MockLogger{} - - manager := NewManager(db, logger) - - if manager == nil { - t.Fatal("NewManager returned nil") - } - - if manager.database != db { - t.Error("Manager database not set correctly") - } - - if manager.logger != logger { - t.Error("Manager logger not set correctly") - } - - if manager.appearances == nil { - t.Error("Manager appearances not initialized") - } -} - -func TestManagerInitialize(t *testing.T) { - tests := []struct { - name string - database *MockDatabase - wantError bool - wantCount int - wantLogInfo bool - }{ - { - name: "successful initialization", - database: &MockDatabase{ - appearances: []*Appearance{ - NewAppearance(1, "Test1", 1000), - NewAppearance(2, "Test2", 1096), - }, - }, - wantError: false, - wantCount: 2, - wantLogInfo: true, - }, - { - name: "nil database", - database: nil, - wantError: false, - wantCount: 0, - wantLogInfo: true, - }, - { - name: "database load error", - database: &MockDatabase{ - loadError: fmt.Errorf("database error"), - }, - wantError: true, - wantCount: 0, - wantLogInfo: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - logger := &MockLogger{} - var manager *Manager - if tt.database != nil { - manager = NewManager(tt.database, logger) - } else { - manager = &Manager{ - appearances: NewAppearances(), - database: nil, - logger: logger, - } - } - - err := manager.Initialize() - - if (err != nil) != tt.wantError { - t.Errorf("Initialize() error = %v, wantError %v", err, tt.wantError) - } - - if count := manager.GetAppearanceCount(); count != tt.wantCount { - t.Errorf("GetAppearanceCount() = %v, want %v", count, tt.wantCount) - } - - if tt.wantLogInfo && len(logger.logs) == 0 { - t.Error("Expected log messages, got none") - } - }) - } -} - -func TestManagerFindAppearanceByID(t *testing.T) { - db := &MockDatabase{ - appearances: []*Appearance{ - NewAppearance(100, "Test", 1000), - }, - } - logger := &MockLogger{} - manager := NewManager(db, logger) - manager.Initialize() - - // Test successful lookup - appearance := manager.FindAppearanceByID(100) - if appearance == nil { - t.Error("FindAppearanceByID(100) returned nil") - } - - // Test failed lookup - appearance = manager.FindAppearanceByID(999) - if appearance != nil { - t.Error("FindAppearanceByID(999) should return nil") - } - - // Check statistics - stats := manager.GetStatistics() - if totalLookups, ok := stats["total_lookups"].(int64); !ok || totalLookups != 2 { - t.Errorf("total_lookups = %v, want 2", stats["total_lookups"]) - } - - if successfulLookups, ok := stats["successful_lookups"].(int64); !ok || successfulLookups != 1 { - t.Errorf("successful_lookups = %v, want 1", stats["successful_lookups"]) - } - - if failedLookups, ok := stats["failed_lookups"].(int64); !ok || failedLookups != 1 { - t.Errorf("failed_lookups = %v, want 1", stats["failed_lookups"]) - } -} - -func TestManagerAddAppearance(t *testing.T) { - tests := []struct { - name string - appearance *Appearance - saveError error - existingID int32 - wantError bool - errorContains string - }{ - { - name: "successful add", - appearance: NewAppearance(100, "Test", 1000), - wantError: false, - }, - { - name: "nil appearance", - appearance: nil, - wantError: true, - errorContains: "cannot be nil", - }, - { - name: "empty name", - appearance: &Appearance{id: 100, name: "", minClient: 1000}, - wantError: true, - errorContains: "name cannot be empty", - }, - { - name: "invalid ID", - appearance: NewAppearance(0, "Test", 1000), - wantError: true, - errorContains: "must be positive", - }, - { - name: "duplicate ID", - appearance: NewAppearance(100, "Test", 1000), - existingID: 100, - wantError: true, - errorContains: "already exists", - }, - { - name: "database save error", - appearance: NewAppearance(200, "Test", 1000), - saveError: fmt.Errorf("save failed"), - wantError: true, - errorContains: "database", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - db := &MockDatabase{saveError: tt.saveError} - logger := &MockLogger{} - manager := NewManager(db, logger) - - if tt.existingID > 0 { - manager.appearances.InsertAppearance(NewAppearance(tt.existingID, "Existing", 1000)) - } - - err := manager.AddAppearance(tt.appearance) - - if (err != nil) != tt.wantError { - t.Errorf("AddAppearance() error = %v, wantError %v", err, tt.wantError) - } - - if err != nil && tt.errorContains != "" && !contains(err.Error(), tt.errorContains) { - t.Errorf("Error message %q doesn't contain %q", err.Error(), tt.errorContains) - } - - // Verify appearance was added/not added - if !tt.wantError && tt.appearance != nil { - if !manager.appearances.HasAppearance(tt.appearance.GetID()) { - t.Error("Appearance was not added to collection") - } - } - }) - } -} - -func TestManagerUpdateAppearance(t *testing.T) { - db := &MockDatabase{} - logger := &MockLogger{} - manager := NewManager(db, logger) - - // Add initial appearance - original := NewAppearance(100, "Original", 1000) - manager.AddAppearance(original) - - // Test successful update - updated := NewAppearance(100, "Updated", 1100) - err := manager.UpdateAppearance(updated) - if err != nil { - t.Errorf("UpdateAppearance failed: %v", err) - } - - found := manager.FindAppearanceByID(100) - if found.GetName() != "Updated" { - t.Error("Appearance was not updated") - } - - // Test update non-existent - notExist := NewAppearance(999, "NotExist", 1000) - err = manager.UpdateAppearance(notExist) - if err == nil { - t.Error("UpdateAppearance should fail for non-existent appearance") - } - - // Test nil appearance - err = manager.UpdateAppearance(nil) - if err == nil { - t.Error("UpdateAppearance should fail for nil appearance") - } -} - -func TestManagerRemoveAppearance(t *testing.T) { - db := &MockDatabase{} - logger := &MockLogger{} - manager := NewManager(db, logger) - - // Add appearance - app := NewAppearance(100, "Test", 1000) - manager.AddAppearance(app) - - // Test successful removal - err := manager.RemoveAppearance(100) - if err != nil { - t.Errorf("RemoveAppearance failed: %v", err) - } - - if manager.appearances.HasAppearance(100) { - t.Error("Appearance was not removed") - } - - // Test removing non-existent - err = manager.RemoveAppearance(999) - if err == nil { - t.Error("RemoveAppearance should fail for non-existent appearance") - } - - // Test database delete error - db.deleteError = fmt.Errorf("delete failed") - manager.AddAppearance(NewAppearance(200, "Test2", 1000)) - err = manager.RemoveAppearance(200) - if err == nil { - t.Error("RemoveAppearance should fail when database delete fails") - } -} - -func TestManagerCommands(t *testing.T) { - db := &MockDatabase{ - appearances: []*Appearance{ - NewAppearance(100, "Human Male", 1000), - NewAppearance(200, "Human Female", 1096), - }, - } - logger := &MockLogger{} - manager := NewManager(db, logger) - manager.Initialize() - - // Test stats command - result, err := manager.ProcessCommand("stats", nil) - if err != nil { - t.Errorf("ProcessCommand(stats) failed: %v", err) - } - if !contains(result, "Appearance System Statistics") { - t.Error("Stats command output incorrect") - } - - // Test validate command - result, err = manager.ProcessCommand("validate", nil) - if err != nil { - t.Errorf("ProcessCommand(validate) failed: %v", err) - } - if !contains(result, "valid") { - t.Error("Validate command output incorrect") - } - - // Test search command - result, err = manager.ProcessCommand("search", []string{"Human"}) - if err != nil { - t.Errorf("ProcessCommand(search) failed: %v", err) - } - if !contains(result, "Found 2 appearances") { - t.Error("Search command output incorrect") - } - - // Test search without args - _, err = manager.ProcessCommand("search", nil) - if err == nil { - t.Error("Search command should fail without arguments") - } - - // Test info command - result, err = manager.ProcessCommand("info", []string{"100"}) - if err != nil { - t.Errorf("ProcessCommand(info) failed: %v", err) - } - if !contains(result, "Human Male") { - t.Error("Info command output incorrect") - } - - // Test info without args - _, err = manager.ProcessCommand("info", nil) - if err == nil { - t.Error("Info command should fail without arguments") - } - - // Test unknown command - _, err = manager.ProcessCommand("unknown", nil) - if err == nil { - t.Error("Unknown command should return error") - } -} - -func TestManagerResetStatistics(t *testing.T) { - manager := NewManager(nil, nil) - - // Perform some lookups - manager.FindAppearanceByID(100) - manager.FindAppearanceByID(200) - - stats := manager.GetStatistics() - if totalLookups, ok := stats["total_lookups"].(int64); !ok || totalLookups != 2 { - t.Error("Statistics not tracked correctly") - } - - // Reset statistics - manager.ResetStatistics() - - stats = manager.GetStatistics() - if totalLookups, ok := stats["total_lookups"].(int64); !ok || totalLookups != 0 { - t.Error("Statistics not reset correctly") - } -} - -func TestManagerShutdown(t *testing.T) { - logger := &MockLogger{} - manager := NewManager(nil, logger) - - manager.appearances.InsertAppearance(NewAppearance(100, "Test", 1000)) - - manager.Shutdown() - - if manager.GetAppearanceCount() != 0 { - t.Error("Shutdown did not clear appearances") - } - - // Check for shutdown log - found := false - for _, log := range logger.logs { - if contains(log, "Shutting down") { - found = true - break - } - } - if !found { - t.Error("Shutdown did not log message") - } -} - -// EntityAppearanceAdapter tests -func TestEntityAppearanceAdapter(t *testing.T) { - entity := &MockEntity{id: 1, name: "TestEntity", databaseID: 100} - logger := &MockLogger{} - manager := NewManager(nil, logger) - - // Add test appearance - app := NewAppearance(500, "TestAppearance", 1000) - manager.AddAppearance(app) - - adapter := NewEntityAppearanceAdapter(entity, manager, logger) - - // Test initial state - if adapter.GetAppearanceID() != 0 { - t.Error("Initial appearance ID should be 0") - } - - if adapter.GetAppearance() != nil { - t.Error("Initial appearance should be nil") - } - - // Test setting appearance ID - adapter.SetAppearanceID(500) - if adapter.GetAppearanceID() != 500 { - t.Error("SetAppearanceID failed") - } - - // Test getting appearance - appearance := adapter.GetAppearance() - if appearance == nil || appearance.GetID() != 500 { - t.Error("GetAppearance failed") - } - - // Test appearance name - if name := adapter.GetAppearanceName(); name != "TestAppearance" { - t.Errorf("GetAppearanceName() = %v, want TestAppearance", name) - } - - // Test validation - if err := adapter.ValidateAppearance(); err != nil { - t.Errorf("ValidateAppearance() failed: %v", err) - } - - // Test invalid appearance - adapter.SetAppearanceID(999) - if err := adapter.ValidateAppearance(); err == nil { - t.Error("ValidateAppearance() should fail for invalid ID") - } -} - -func TestEntityAppearanceAdapterClientCompatibility(t *testing.T) { - entity := &MockEntity{id: 1, name: "TestEntity"} - manager := NewManager(nil, nil) - - // Add appearance with version requirement - app := NewAppearance(100, "Test", 1096) - manager.AddAppearance(app) - - adapter := NewEntityAppearanceAdapter(entity, manager, nil) - adapter.SetAppearanceID(100) - - // Test compatible client - if !adapter.IsCompatibleWithClient(1100) { - t.Error("Should be compatible with client version 1100") - } - - // Test incompatible client - if adapter.IsCompatibleWithClient(1000) { - t.Error("Should not be compatible with client version 1000") - } - - // Test no appearance (always compatible) - adapter.SetAppearanceID(0) - if !adapter.IsCompatibleWithClient(500) { - t.Error("No appearance should be compatible with all clients") - } -} - -func TestEntityAppearanceAdapterSendToClient(t *testing.T) { - entity := &MockEntity{id: 1, name: "TestEntity"} - logger := &MockLogger{} - manager := NewManager(nil, logger) - - app := NewAppearance(100, "Test", 1096) - manager.AddAppearance(app) - - adapter := NewEntityAppearanceAdapter(entity, manager, logger) - adapter.SetAppearanceID(100) - - // Test successful send - client := &MockClient{version: 1100} - err := adapter.SendAppearanceToClient(client) - if err != nil { - t.Errorf("SendAppearanceToClient failed: %v", err) - } - - // Test incompatible client - lowClient := &MockClient{version: 1000} - err = adapter.SendAppearanceToClient(lowClient) - if err == nil { - t.Error("SendAppearanceToClient should fail for incompatible client") - } - - // Test client send error - errorClient := &MockClient{version: 1100, sendError: fmt.Errorf("send failed")} - err = adapter.SendAppearanceToClient(errorClient) - if err == nil { - t.Error("SendAppearanceToClient should propagate client error") - } - - // Test nil client - err = adapter.SendAppearanceToClient(nil) - if err == nil { - t.Error("SendAppearanceToClient should fail for nil client") - } -} - -// Cache tests -func TestSimpleAppearanceCache(t *testing.T) { - cache := NewSimpleAppearanceCache() - - app1 := NewAppearance(100, "Test1", 1000) - app2 := NewAppearance(200, "Test2", 1096) - - // Test Set and Get - cache.Set(100, app1) - cache.Set(200, app2) - - if got := cache.Get(100); got != app1 { - t.Error("Cache Get(100) failed") - } - - if got := cache.Get(999); got != nil { - t.Error("Cache Get(999) should return nil") - } - - // Test GetSize - if size := cache.GetSize(); size != 2 { - t.Errorf("GetSize() = %v, want 2", size) - } - - // Test Remove - cache.Remove(100) - if cache.Get(100) != nil { - t.Error("Remove(100) failed") - } - - if size := cache.GetSize(); size != 1 { - t.Errorf("GetSize() after remove = %v, want 1", size) - } - - // Test Clear - cache.Clear() - if size := cache.GetSize(); size != 0 { - t.Errorf("GetSize() after clear = %v, want 0", size) - } -} - -func TestCachedAppearanceManager(t *testing.T) { - db := &MockDatabase{} - logger := &MockLogger{} - baseManager := NewManager(db, logger) - - // Add test appearances - app1 := NewAppearance(100, "Test1", 1000) - app2 := NewAppearance(200, "Test2", 1096) - baseManager.AddAppearance(app1) - baseManager.AddAppearance(app2) - - cache := NewSimpleAppearanceCache() - cachedManager := NewCachedAppearanceManager(baseManager, cache) - - // Test FindAppearanceByID with caching - found := cachedManager.FindAppearanceByID(100) - if found == nil || found.GetID() != 100 { - t.Error("FindAppearanceByID(100) failed") - } - - // Verify it was cached - if cache.GetSize() != 1 { - t.Error("Appearance was not cached") - } - - // Test cache hit - found2 := cachedManager.FindAppearanceByID(100) - if found2 != found { - t.Error("Should have returned cached appearance") - } - - // Test AddAppearance updates cache - app3 := NewAppearance(300, "Test3", 1200) - err := cachedManager.AddAppearance(app3) - if err != nil { - t.Errorf("AddAppearance failed: %v", err) - } - - if cache.Get(300) == nil { - t.Error("Added appearance was not cached") - } - - // Test UpdateAppearance updates cache - updated := NewAppearance(100, "Updated", 1100) - err = cachedManager.UpdateAppearance(updated) - if err != nil { - t.Errorf("UpdateAppearance failed: %v", err) - } - - cached := cache.Get(100) - if cached == nil || cached.GetName() != "Updated" { - t.Error("Cache was not updated") - } - - // Test RemoveAppearance updates cache - err = cachedManager.RemoveAppearance(200) - if err != nil { - t.Errorf("RemoveAppearance failed: %v", err) - } - - if cache.Get(200) != nil { - t.Error("Removed appearance still in cache") - } - - // Test ClearCache - cachedManager.ClearCache() - if cache.GetSize() != 0 { - t.Error("ClearCache failed") - } -} - -func TestCacheConcurrency(t *testing.T) { - cache := NewSimpleAppearanceCache() - var wg sync.WaitGroup - - // Concurrent operations - for i := 0; i < 100; i++ { - wg.Add(3) - - // Writer - go func(id int32) { - defer wg.Done() - app := NewAppearance(id, "Test", 1000) - cache.Set(id, app) - }(int32(i)) - - // Reader - go func(id int32) { - defer wg.Done() - _ = cache.Get(id) - }(int32(i)) - - // Size checker - go func() { - defer wg.Done() - _ = cache.GetSize() - }() - } - - wg.Wait() - - // Final size should be predictable - if size := cache.GetSize(); size > 100 { - t.Errorf("Cache size %v is too large", size) - } -} - -// Additional tests for uncovered code paths -func TestEntityAppearanceAdapterErrorCases(t *testing.T) { - entity := &MockEntity{id: 1, name: "TestEntity"} - logger := &MockLogger{} - - // Test with nil manager - adapter := NewEntityAppearanceAdapter(entity, nil, logger) - adapter.SetAppearanceID(100) - - // GetAppearance should return nil and log error - appearance := adapter.GetAppearance() - if appearance != nil { - t.Error("GetAppearance should return nil with nil manager") - } - - // Test UpdateAppearance with nil manager - err := adapter.UpdateAppearance(100) - if err == nil { - t.Error("UpdateAppearance should fail with nil manager") - } - - // Test UpdateAppearance with non-existent appearance - manager := NewManager(nil, logger) - adapter = NewEntityAppearanceAdapter(entity, manager, logger) - err = adapter.UpdateAppearance(999) - if err == nil { - t.Error("UpdateAppearance should fail for non-existent appearance") - } - - // Test SendAppearanceToClient with no appearance set - client := &MockClient{version: 1100} - adapter.SetAppearanceID(0) - err = adapter.SendAppearanceToClient(client) - if err != nil { - t.Errorf("SendAppearanceToClient should succeed with no appearance: %v", err) - } -} - -func TestManagerGetAppearances(t *testing.T) { - manager := NewManager(nil, nil) - - appearances := manager.GetAppearances() - if appearances == nil { - t.Error("GetAppearances should not return nil") - } - - if appearances != manager.appearances { - t.Error("GetAppearances should return internal appearances collection") - } -} - -func TestManagerCompatibleAndSearch(t *testing.T) { - manager := NewManager(nil, nil) - - // Add test appearances - app1 := NewAppearance(100, "Human Male", 1000) - app2 := NewAppearance(200, "Human Female", 1096) - manager.AddAppearance(app1) - manager.AddAppearance(app2) - - // Test GetCompatibleAppearances - compatible := manager.GetCompatibleAppearances(1050) - if len(compatible) != 1 { - t.Errorf("GetCompatibleAppearances(1050) returned %v results, want 1", len(compatible)) - } - - // Test SearchAppearancesByName - results := manager.SearchAppearancesByName("Human") - if len(results) != 2 { - t.Errorf("SearchAppearancesByName('Human') returned %v results, want 2", len(results)) - } -} - -func TestManagerReloadFromDatabase(t *testing.T) { - // Test with nil database - manager := NewManager(nil, nil) - err := manager.ReloadFromDatabase() - if err == nil { - t.Error("ReloadFromDatabase should fail with nil database") - } - - // Test successful reload - db := &MockDatabase{ - appearances: []*Appearance{ - NewAppearance(100, "Test", 1000), - }, - } - manager = NewManager(db, nil) - - // Add some appearances first - manager.AddAppearance(NewAppearance(200, "Existing", 1000)) - - err = manager.ReloadFromDatabase() - if err != nil { - t.Errorf("ReloadFromDatabase failed: %v", err) - } - - // Should only have the database appearance now - if count := manager.GetAppearanceCount(); count != 1 { - t.Errorf("After reload, count = %v, want 1", count) - } - - if !manager.appearances.HasAppearance(100) { - t.Error("Reloaded appearance not found") - } - - if manager.appearances.HasAppearance(200) { - t.Error("Previous appearance should be cleared") - } -} - -func TestManagerCommandEdgeCases(t *testing.T) { - manager := NewManager(nil, nil) - - // Test reload command without database - result, err := manager.ProcessCommand("reload", nil) - if err == nil { - t.Error("Reload command should fail without database") - } - - // Test info command with invalid ID - _, err = manager.ProcessCommand("info", []string{"invalid"}) - if err == nil { - t.Error("Info command should fail with invalid ID") - } - - // Test info command with non-existent ID - result, err = manager.ProcessCommand("info", []string{"999"}) - if err != nil { - t.Errorf("Info command failed: %v", err) - } - if !contains(result, "not found") { - t.Error("Info command should indicate appearance not found") - } - - // Test search with no results - result, err = manager.ProcessCommand("search", []string{"nonexistent"}) - if err != nil { - t.Errorf("Search command failed: %v", err) - } - if !contains(result, "No appearances found") { - t.Error("Search command should indicate no results") - } -} - -func TestManagerValidateAllAppearances(t *testing.T) { - manager := NewManager(nil, nil) - - // Add valid appearance - manager.AddAppearance(NewAppearance(100, "Valid", 1000)) - - issues := manager.ValidateAllAppearances() - if len(issues) != 0 { - t.Errorf("ValidateAllAppearances returned issues for valid data: %v", issues) - } -} - -func TestEntityAppearanceAdapterGetAppearanceName(t *testing.T) { - entity := &MockEntity{id: 1, name: "TestEntity"} - manager := NewManager(nil, nil) - adapter := NewEntityAppearanceAdapter(entity, manager, nil) - - // Test with no appearance - if name := adapter.GetAppearanceName(); name != "" { - t.Errorf("GetAppearanceName() with no appearance = %v, want empty string", name) - } - - // Test with appearance - app := NewAppearance(100, "TestName", 1000) - manager.AddAppearance(app) - adapter.SetAppearanceID(100) - - if name := adapter.GetAppearanceName(); name != "TestName" { - t.Errorf("GetAppearanceName() = %v, want TestName", name) - } -} diff --git a/internal/appearances/constants.go b/internal/appearances/constants.go index 36d6418..6087475 100644 --- a/internal/appearances/constants.go +++ b/internal/appearances/constants.go @@ -11,3 +11,134 @@ const ( MinimumClientVersion = 0 DefaultClientVersion = 283 ) + +// Appearance type constants from the original EQ2EMu implementation +// These correspond to different appearance features like hair, skin, clothing, etc. +const ( + AppearanceSOGAHairFaceHighlightColor = 0 // APPEARANCE_SOGA_HFHC + AppearanceSOGAHairTypeHighlightColor = 1 // APPEARANCE_SOGA_HTHC + AppearanceSOGAHairFaceColor = 2 // APPEARANCE_SOGA_HFC + AppearanceSOGAHairTypeColor = 3 // APPEARANCE_SOGA_HTC + AppearanceSOGAHairHighlight = 4 // APPEARANCE_SOGA_HH + AppearanceSOGAHairColor1 = 5 // APPEARANCE_SOGA_HC1 + AppearanceSOGAHairColor2 = 6 // APPEARANCE_SOGA_HC2 + AppearanceSOGASkinColor = 7 // APPEARANCE_SOGA_SC + AppearanceSOGAEyeColor = 8 // APPEARANCE_SOGA_EC + AppearanceHairTypeHighlightColor = 9 // APPEARANCE_HTHC + AppearanceHairFaceHighlightColor = 10 // APPEARANCE_HFHC + AppearanceHairTypeColor = 11 // APPEARANCE_HTC + AppearanceHairFaceColor = 12 // APPEARANCE_HFC + AppearanceHairHighlight = 13 // APPEARANCE_HH + AppearanceHairColor1 = 14 // APPEARANCE_HC1 + AppearanceHairColor2 = 15 // APPEARANCE_HC2 + AppearanceWingColor1 = 16 // APPEARANCE_WC1 + AppearanceWingColor2 = 17 // APPEARANCE_WC2 + AppearanceSkinColor = 18 // APPEARANCE_SC + AppearanceEyeColor = 19 // APPEARANCE_EC + AppearanceShirt = 20 // APPEARANCE_SHIRT + AppearanceUnknownClothingColor = 21 // APPEARANCE_UCC + AppearancePants = 22 // APPEARANCE_PANTS + AppearanceUnknownLegColor = 23 // APPEARANCE_ULC + AppearanceUnknown9 = 24 // APPEARANCE_U9 + AppearanceBodySize = 25 // APPEARANCE_BODY_SIZE + AppearanceSOGAWingColor1 = 26 // APPEARANCE_SOGA_WC1 + AppearanceSOGAWingColor2 = 27 // APPEARANCE_SOGA_WC2 + AppearanceSOGAShirt = 28 // APPEARANCE_SOGA_SHIRT + AppearanceSOGAUnknownClothingColor = 29 // APPEARANCE_SOGA_UCC + AppearanceSOGAPants = 30 // APPEARANCE_SOGA_PANTS + AppearanceSOGAUnknownLegColor = 31 // APPEARANCE_SOGA_ULC + AppearanceSOGAUnknown13 = 32 // APPEARANCE_SOGA_U13 + AppearanceSOGAEyebrowType = 33 // APPEARANCE_SOGA_EBT + AppearanceSOGACheekType = 34 // APPEARANCE_SOGA_CHEEKT + AppearanceSOGANoseType = 35 // APPEARANCE_SOGA_NT + AppearanceSOGAChinType = 36 // APPEARANCE_SOGA_CHINT + AppearanceSOGALipType = 37 // APPEARANCE_SOGA_LT + AppearanceSOGAEarType = 38 // APPEARANCE_SOGA_EART + AppearanceSOGAEyeType = 39 // APPEARANCE_SOGA_EYET + AppearanceEyebrowType = 40 // APPEARANCE_EBT + AppearanceCheekType = 41 // APPEARANCE_CHEEKT + AppearanceNoseType = 42 // APPEARANCE_NT + AppearanceChinType = 43 // APPEARANCE_CHINT + AppearanceEarType = 44 // APPEARANCE_EART + AppearanceEyeType = 45 // APPEARANCE_EYET + AppearanceLipType = 46 // APPEARANCE_LT + AppearanceBodyAge = 47 // APPEARANCE_BODY_AGE + AppearanceModelColor = 48 // APPEARANCE_MC + AppearanceSOGAModelColor = 49 // APPEARANCE_SMC + AppearanceSOGABodySize = 50 // APPEARANCE_SBS + AppearanceSOGABodyAge = 51 // APPEARANCE_SBA +) + +// AppearanceTypeNames maps appearance type constants to human-readable names +var AppearanceTypeNames = map[int8]string{ + AppearanceSOGAHairFaceHighlightColor: "soga_hair_face_highlight_color", + AppearanceSOGAHairTypeHighlightColor: "soga_hair_type_highlight_color", + AppearanceSOGAHairFaceColor: "soga_hair_face_color", + AppearanceSOGAHairTypeColor: "soga_hair_type_color", + AppearanceSOGAHairHighlight: "soga_hair_highlight", + AppearanceSOGAHairColor1: "soga_hair_color1", + AppearanceSOGAHairColor2: "soga_hair_color2", + AppearanceSOGASkinColor: "soga_skin_color", + AppearanceSOGAEyeColor: "soga_eye_color", + AppearanceHairTypeHighlightColor: "hair_type_highlight_color", + AppearanceHairFaceHighlightColor: "hair_face_highlight_color", + AppearanceHairTypeColor: "hair_type_color", + AppearanceHairFaceColor: "hair_face_color", + AppearanceHairHighlight: "hair_highlight", + AppearanceHairColor1: "hair_color1", + AppearanceHairColor2: "hair_color2", + AppearanceWingColor1: "wing_color1", + AppearanceWingColor2: "wing_color2", + AppearanceSkinColor: "skin_color", + AppearanceEyeColor: "eye_color", + AppearanceShirt: "shirt_color", + AppearanceUnknownClothingColor: "unknown_chest_color", + AppearancePants: "pants_color", + AppearanceUnknownLegColor: "unknown_leg_color", + AppearanceUnknown9: "unknown9", + AppearanceBodySize: "body_size", + AppearanceSOGAWingColor1: "soga_wing_color1", + AppearanceSOGAWingColor2: "soga_wing_color2", + AppearanceSOGAShirt: "soga_shirt_color", + AppearanceSOGAUnknownClothingColor: "soga_unknown_chest_color", + AppearanceSOGAPants: "soga_pants_color", + AppearanceSOGAUnknownLegColor: "soga_unknown_leg_color", + AppearanceSOGAUnknown13: "soga_unknown13", + AppearanceSOGAEyebrowType: "soga_eye_brow_type", + AppearanceSOGACheekType: "soga_cheek_type", + AppearanceSOGANoseType: "soga_nose_type", + AppearanceSOGAChinType: "soga_chin_type", + AppearanceSOGALipType: "soga_lip_type", + AppearanceSOGAEarType: "soga_ear_type", + AppearanceSOGAEyeType: "soga_eye_type", + AppearanceEyebrowType: "eye_brow_type", + AppearanceCheekType: "cheek_type", + AppearanceNoseType: "nose_type", + AppearanceChinType: "chin_type", + AppearanceEarType: "ear_type", + AppearanceEyeType: "eye_type", + AppearanceLipType: "lip_type", + AppearanceBodyAge: "body_age", + AppearanceModelColor: "model_color", + AppearanceSOGAModelColor: "soga_model_color", + AppearanceSOGABodySize: "soga_body_size", + AppearanceSOGABodyAge: "soga_body_age", +} + +// GetAppearanceType returns the appearance type constant for a given type name +func GetAppearanceType(typeName string) int8 { + for typeConst, name := range AppearanceTypeNames { + if name == typeName { + return typeConst + } + } + return -1 // Unknown type +} + +// GetAppearanceTypeName returns the type name for a given appearance type constant +func GetAppearanceTypeName(typeConst int8) string { + if name, exists := AppearanceTypeNames[typeConst]; exists { + return name + } + return "unknown" +} diff --git a/internal/appearances/doc.go b/internal/appearances/doc.go new file mode 100644 index 0000000..e2ea6b4 --- /dev/null +++ b/internal/appearances/doc.go @@ -0,0 +1,45 @@ +// Package appearances provides appearance management for the EverQuest II server emulator. +// +// Appearances define the visual characteristics of entities in the game world, including +// player character customization options, NPC appearances, and visual variations. +// The system supports client version compatibility to ensure proper rendering across +// different EQ2 client versions. +// +// Basic Usage: +// +// db, _ := database.NewSQLite("appearances.db") +// +// // Create new appearance +// appearance := appearances.New(db) +// appearance.ID = 1001 +// appearance.Name = "Human Male" +// appearance.MinClient = 1096 +// appearance.Save() +// +// // Load existing appearance +// loaded, _ := appearances.Load(db, 1001) +// loaded.Delete() +// +// Master List: +// +// masterList := appearances.NewMasterList() +// masterList.LoadAllAppearances(db) +// masterList.AddAppearance(appearance) +// +// // Find appearances +// found := masterList.GetAppearance(1001) +// compatible := masterList.GetCompatibleAppearances(1096) +// humans := masterList.FindAppearancesByName("Human") +// +// Appearance Types: +// +// // Get appearance type from name +// hairType := appearances.GetAppearanceType("hair_color1") +// +// // Get name from type constant +// typeName := appearances.GetAppearanceTypeName(appearances.AppearanceHairColor1) +// +// The package includes all appearance type constants from the original EQ2EMu +// implementation, supporting hair colors, skin tones, facial features, clothing, +// and body customization options for both standard and SOGA character models. +package appearances diff --git a/internal/appearances/interfaces.go b/internal/appearances/interfaces.go deleted file mode 100644 index ad7700f..0000000 --- a/internal/appearances/interfaces.go +++ /dev/null @@ -1,308 +0,0 @@ -package appearances - -import ( - "fmt" - "sync" -) - -// Database interface for appearance persistence -type Database interface { - LoadAllAppearances() ([]*Appearance, error) - SaveAppearance(appearance *Appearance) error - DeleteAppearance(id int32) error - LoadAppearancesByClientVersion(minClientVersion int16) ([]*Appearance, error) -} - -// Logger interface for appearance logging -type Logger interface { - LogInfo(message string, args ...any) - LogError(message string, args ...any) - LogDebug(message string, args ...any) - LogWarning(message string, args ...any) -} - -// AppearanceProvider interface for entities that provide appearances -type AppearanceProvider interface { - GetAppearanceID() int32 - SetAppearanceID(id int32) - GetAppearance() *Appearance - IsCompatibleWithClient(clientVersion int16) bool -} - -// AppearanceAware interface for entities that use appearances -type AppearanceAware interface { - GetAppearanceManager() *Manager - FindAppearanceByID(id int32) *Appearance - GetCompatibleAppearances(clientVersion int16) []*Appearance -} - -// Client interface for appearance-related client operations -type Client interface { - GetVersion() int16 - SendAppearanceUpdate(appearanceID int32) error -} - -// AppearanceCache interface for caching appearance data -type AppearanceCache interface { - Get(id int32) *Appearance - Set(id int32, appearance *Appearance) - Remove(id int32) - Clear() - GetSize() int -} - -// EntityAppearanceAdapter provides appearance functionality for entities -type EntityAppearanceAdapter struct { - entity Entity - appearanceID int32 - manager *Manager - logger Logger -} - -// Entity interface for things that can have appearances -type Entity interface { - GetID() int32 - GetName() string - GetDatabaseID() int32 -} - -// NewEntityAppearanceAdapter creates a new entity appearance adapter -func NewEntityAppearanceAdapter(entity Entity, manager *Manager, logger Logger) *EntityAppearanceAdapter { - return &EntityAppearanceAdapter{ - entity: entity, - appearanceID: 0, - manager: manager, - logger: logger, - } -} - -// GetAppearanceID returns the entity's appearance ID -func (eaa *EntityAppearanceAdapter) GetAppearanceID() int32 { - return eaa.appearanceID -} - -// SetAppearanceID sets the entity's appearance ID -func (eaa *EntityAppearanceAdapter) SetAppearanceID(id int32) { - eaa.appearanceID = id - - if eaa.logger != nil { - eaa.logger.LogDebug("Entity %d (%s): Set appearance ID to %d", - eaa.entity.GetID(), eaa.entity.GetName(), id) - } -} - -// GetAppearance returns the entity's appearance object -func (eaa *EntityAppearanceAdapter) GetAppearance() *Appearance { - if eaa.appearanceID == 0 { - return nil - } - - if eaa.manager == nil { - if eaa.logger != nil { - eaa.logger.LogError("Entity %d (%s): No appearance manager available", - eaa.entity.GetID(), eaa.entity.GetName()) - } - return nil - } - - return eaa.manager.FindAppearanceByID(eaa.appearanceID) -} - -// IsCompatibleWithClient checks if the entity's appearance is compatible with client version -func (eaa *EntityAppearanceAdapter) IsCompatibleWithClient(clientVersion int16) bool { - appearance := eaa.GetAppearance() - if appearance == nil { - return true // No appearance means compatible with all clients - } - - return appearance.IsCompatibleWithClient(clientVersion) -} - -// GetAppearanceName returns the name of the entity's appearance -func (eaa *EntityAppearanceAdapter) GetAppearanceName() string { - appearance := eaa.GetAppearance() - if appearance == nil { - return "" - } - - return appearance.GetName() -} - -// ValidateAppearance validates that the entity's appearance exists and is valid -func (eaa *EntityAppearanceAdapter) ValidateAppearance() error { - if eaa.appearanceID == 0 { - return nil // No appearance is valid - } - - appearance := eaa.GetAppearance() - if appearance == nil { - return fmt.Errorf("appearance ID %d not found", eaa.appearanceID) - } - - return nil -} - -// UpdateAppearance updates the entity's appearance from the manager -func (eaa *EntityAppearanceAdapter) UpdateAppearance(id int32) error { - if eaa.manager == nil { - return fmt.Errorf("no appearance manager available") - } - - appearance := eaa.manager.FindAppearanceByID(id) - if appearance == nil { - return fmt.Errorf("appearance ID %d not found", id) - } - - eaa.SetAppearanceID(id) - - if eaa.logger != nil { - eaa.logger.LogInfo("Entity %d (%s): Updated appearance to %d (%s)", - eaa.entity.GetID(), eaa.entity.GetName(), id, appearance.GetName()) - } - - return nil -} - -// SendAppearanceToClient sends the appearance to a client -func (eaa *EntityAppearanceAdapter) SendAppearanceToClient(client Client) error { - if client == nil { - return fmt.Errorf("client is nil") - } - - if eaa.appearanceID == 0 { - return nil // No appearance to send - } - - // Check client compatibility - if !eaa.IsCompatibleWithClient(client.GetVersion()) { - if eaa.logger != nil { - eaa.logger.LogWarning("Entity %d (%s): Appearance %d not compatible with client version %d", - eaa.entity.GetID(), eaa.entity.GetName(), eaa.appearanceID, client.GetVersion()) - } - return fmt.Errorf("appearance not compatible with client version %d", client.GetVersion()) - } - - return client.SendAppearanceUpdate(eaa.appearanceID) -} - -// SimpleAppearanceCache is a basic in-memory appearance cache -type SimpleAppearanceCache struct { - cache map[int32]*Appearance - mutex sync.RWMutex -} - -// NewSimpleAppearanceCache creates a new simple appearance cache -func NewSimpleAppearanceCache() *SimpleAppearanceCache { - return &SimpleAppearanceCache{ - cache: make(map[int32]*Appearance), - } -} - -// Get retrieves an appearance from cache -func (sac *SimpleAppearanceCache) Get(id int32) *Appearance { - sac.mutex.RLock() - defer sac.mutex.RUnlock() - - return sac.cache[id] -} - -// Set stores an appearance in cache -func (sac *SimpleAppearanceCache) Set(id int32, appearance *Appearance) { - sac.mutex.Lock() - defer sac.mutex.Unlock() - - sac.cache[id] = appearance -} - -// Remove removes an appearance from cache -func (sac *SimpleAppearanceCache) Remove(id int32) { - sac.mutex.Lock() - defer sac.mutex.Unlock() - - delete(sac.cache, id) -} - -// Clear removes all appearances from cache -func (sac *SimpleAppearanceCache) Clear() { - sac.mutex.Lock() - defer sac.mutex.Unlock() - - sac.cache = make(map[int32]*Appearance) -} - -// GetSize returns the number of cached appearances -func (sac *SimpleAppearanceCache) GetSize() int { - sac.mutex.RLock() - defer sac.mutex.RUnlock() - - return len(sac.cache) -} - -// CachedAppearanceManager wraps a Manager with caching functionality -type CachedAppearanceManager struct { - *Manager - cache AppearanceCache -} - -// NewCachedAppearanceManager creates a new cached appearance manager -func NewCachedAppearanceManager(manager *Manager, cache AppearanceCache) *CachedAppearanceManager { - return &CachedAppearanceManager{ - Manager: manager, - cache: cache, - } -} - -// FindAppearanceByID finds an appearance with caching -func (cam *CachedAppearanceManager) FindAppearanceByID(id int32) *Appearance { - // Check cache first - if appearance := cam.cache.Get(id); appearance != nil { - return appearance - } - - // Load from manager - appearance := cam.Manager.FindAppearanceByID(id) - if appearance != nil { - // Cache the result - cam.cache.Set(id, appearance) - } - - return appearance -} - -// AddAppearance adds an appearance and updates cache -func (cam *CachedAppearanceManager) AddAppearance(appearance *Appearance) error { - err := cam.Manager.AddAppearance(appearance) - if err == nil { - // Update cache - cam.cache.Set(appearance.GetID(), appearance) - } - - return err -} - -// UpdateAppearance updates an appearance and cache -func (cam *CachedAppearanceManager) UpdateAppearance(appearance *Appearance) error { - err := cam.Manager.UpdateAppearance(appearance) - if err == nil { - // Update cache - cam.cache.Set(appearance.GetID(), appearance) - } - - return err -} - -// RemoveAppearance removes an appearance and updates cache -func (cam *CachedAppearanceManager) RemoveAppearance(id int32) error { - err := cam.Manager.RemoveAppearance(id) - if err == nil { - // Remove from cache - cam.cache.Remove(id) - } - - return err -} - -// ClearCache clears the appearance cache -func (cam *CachedAppearanceManager) ClearCache() { - cam.cache.Clear() -} diff --git a/internal/appearances/manager.go b/internal/appearances/manager.go deleted file mode 100644 index f692b1a..0000000 --- a/internal/appearances/manager.go +++ /dev/null @@ -1,392 +0,0 @@ -package appearances - -import ( - "fmt" - "sync" -) - -// Manager provides high-level management of the appearance system -type Manager struct { - appearances *Appearances - database Database - logger Logger - mutex sync.RWMutex - - // Statistics - totalLookups int64 - successfulLookups int64 - failedLookups int64 - cacheHits int64 - cacheMisses int64 -} - -// NewManager creates a new appearance manager -func NewManager(database Database, logger Logger) *Manager { - return &Manager{ - appearances: NewAppearances(), - database: database, - logger: logger, - } -} - -// Initialize loads appearances from database -func (m *Manager) Initialize() error { - if m.logger != nil { - m.logger.LogInfo("Initializing appearance manager...") - } - - if m.database == nil { - if m.logger != nil { - m.logger.LogWarning("No database provided, starting with empty appearance list") - } - return nil - } - - appearances, err := m.database.LoadAllAppearances() - if err != nil { - return fmt.Errorf("failed to load appearances from database: %w", err) - } - - for _, appearance := range appearances { - if err := m.appearances.InsertAppearance(appearance); err != nil { - if m.logger != nil { - m.logger.LogError("Failed to insert appearance %d: %v", appearance.GetID(), err) - } - } - } - - if m.logger != nil { - m.logger.LogInfo("Loaded %d appearances from database", len(appearances)) - } - - return nil -} - -// GetAppearances returns the appearances collection -func (m *Manager) GetAppearances() *Appearances { - return m.appearances -} - -// FindAppearanceByID finds an appearance by ID with statistics tracking -func (m *Manager) FindAppearanceByID(id int32) *Appearance { - m.mutex.Lock() - m.totalLookups++ - m.mutex.Unlock() - - appearance := m.appearances.FindAppearanceByID(id) - - m.mutex.Lock() - if appearance != nil { - m.successfulLookups++ - m.cacheHits++ - } else { - m.failedLookups++ - m.cacheMisses++ - } - m.mutex.Unlock() - - if m.logger != nil && appearance == nil { - m.logger.LogDebug("Appearance lookup failed for ID: %d", id) - } - - return appearance -} - -// AddAppearance adds a new appearance -func (m *Manager) AddAppearance(appearance *Appearance) error { - if appearance == nil { - return fmt.Errorf("appearance cannot be nil") - } - - // Validate the appearance - if len(appearance.GetName()) == 0 { - return fmt.Errorf("appearance name cannot be empty") - } - - if appearance.GetID() <= 0 { - return fmt.Errorf("appearance ID must be positive") - } - - // Check if appearance already exists - if m.appearances.HasAppearance(appearance.GetID()) { - return fmt.Errorf("appearance with ID %d already exists", appearance.GetID()) - } - - // Add to collection - if err := m.appearances.InsertAppearance(appearance); err != nil { - return fmt.Errorf("failed to insert appearance: %w", err) - } - - // Save to database if available - if m.database != nil { - if err := m.database.SaveAppearance(appearance); err != nil { - // Remove from collection if database save failed - m.appearances.RemoveAppearance(appearance.GetID()) - return fmt.Errorf("failed to save appearance to database: %w", err) - } - } - - if m.logger != nil { - m.logger.LogInfo("Added appearance %d: %s (min client: %d)", - appearance.GetID(), appearance.GetName(), appearance.GetMinClientVersion()) - } - - return nil -} - -// UpdateAppearance updates an existing appearance -func (m *Manager) UpdateAppearance(appearance *Appearance) error { - if appearance == nil { - return fmt.Errorf("appearance cannot be nil") - } - - // Check if appearance exists - if !m.appearances.HasAppearance(appearance.GetID()) { - return fmt.Errorf("appearance with ID %d does not exist", appearance.GetID()) - } - - // Update in collection - if err := m.appearances.UpdateAppearance(appearance); err != nil { - return fmt.Errorf("failed to update appearance: %w", err) - } - - // Save to database if available - if m.database != nil { - if err := m.database.SaveAppearance(appearance); err != nil { - return fmt.Errorf("failed to save appearance to database: %w", err) - } - } - - if m.logger != nil { - m.logger.LogInfo("Updated appearance %d: %s", appearance.GetID(), appearance.GetName()) - } - - return nil -} - -// RemoveAppearance removes an appearance -func (m *Manager) RemoveAppearance(id int32) error { - // Check if appearance exists - if !m.appearances.HasAppearance(id) { - return fmt.Errorf("appearance with ID %d does not exist", id) - } - - // Remove from database first if available - if m.database != nil { - if err := m.database.DeleteAppearance(id); err != nil { - return fmt.Errorf("failed to delete appearance from database: %w", err) - } - } - - // Remove from collection - if !m.appearances.RemoveAppearance(id) { - return fmt.Errorf("failed to remove appearance from collection") - } - - if m.logger != nil { - m.logger.LogInfo("Removed appearance %d", id) - } - - return nil -} - -// GetCompatibleAppearances returns appearances compatible with client version -func (m *Manager) GetCompatibleAppearances(clientVersion int16) []*Appearance { - return m.appearances.GetCompatibleAppearances(clientVersion) -} - -// SearchAppearancesByName searches for appearances by name substring -func (m *Manager) SearchAppearancesByName(nameSubstring string) []*Appearance { - return m.appearances.FindAppearancesByName(nameSubstring) -} - -// GetStatistics returns appearance system statistics -func (m *Manager) GetStatistics() map[string]any { - m.mutex.RLock() - defer m.mutex.RUnlock() - - // Get basic appearance statistics - stats := m.appearances.GetStatistics() - - // Add manager statistics - stats["total_lookups"] = m.totalLookups - stats["successful_lookups"] = m.successfulLookups - stats["failed_lookups"] = m.failedLookups - stats["cache_hits"] = m.cacheHits - stats["cache_misses"] = m.cacheMisses - - if m.totalLookups > 0 { - stats["success_rate"] = float64(m.successfulLookups) / float64(m.totalLookups) * 100 - stats["cache_hit_rate"] = float64(m.cacheHits) / float64(m.totalLookups) * 100 - } - - return stats -} - -// ResetStatistics resets all statistics -func (m *Manager) ResetStatistics() { - m.mutex.Lock() - defer m.mutex.Unlock() - - m.totalLookups = 0 - m.successfulLookups = 0 - m.failedLookups = 0 - m.cacheHits = 0 - m.cacheMisses = 0 -} - -// ValidateAllAppearances validates all appearances in the system -func (m *Manager) ValidateAllAppearances() []string { - return m.appearances.ValidateAppearances() -} - -// ReloadFromDatabase reloads all appearances from database -func (m *Manager) ReloadFromDatabase() error { - if m.database == nil { - return fmt.Errorf("no database available") - } - - // Clear current appearances - m.appearances.ClearAppearances() - - // Reload from database - return m.Initialize() -} - -// GetAppearanceCount returns the total number of appearances -func (m *Manager) GetAppearanceCount() int { - return m.appearances.GetAppearanceCount() -} - -// ProcessCommand handles appearance-related commands -func (m *Manager) ProcessCommand(command string, args []string) (string, error) { - switch command { - case "stats": - return m.handleStatsCommand(args) - case "validate": - return m.handleValidateCommand(args) - case "search": - return m.handleSearchCommand(args) - case "info": - return m.handleInfoCommand(args) - case "reload": - return m.handleReloadCommand(args) - default: - return "", fmt.Errorf("unknown appearance command: %s", command) - } -} - -// handleStatsCommand shows appearance system statistics -func (m *Manager) handleStatsCommand(args []string) (string, error) { - stats := m.GetStatistics() - - result := "Appearance System Statistics:\n" - result += fmt.Sprintf("Total Appearances: %d\n", stats["total_appearances"]) - result += fmt.Sprintf("Total Lookups: %d\n", stats["total_lookups"]) - result += fmt.Sprintf("Successful Lookups: %d\n", stats["successful_lookups"]) - result += fmt.Sprintf("Failed Lookups: %d\n", stats["failed_lookups"]) - - if successRate, exists := stats["success_rate"]; exists { - result += fmt.Sprintf("Success Rate: %.1f%%\n", successRate) - } - - if cacheHitRate, exists := stats["cache_hit_rate"]; exists { - result += fmt.Sprintf("Cache Hit Rate: %.1f%%\n", cacheHitRate) - } - - if minID, exists := stats["min_id"]; exists { - result += fmt.Sprintf("ID Range: %d - %d\n", minID, stats["max_id"]) - } - - return result, nil -} - -// handleValidateCommand validates all appearances -func (m *Manager) handleValidateCommand(_ []string) (string, error) { - issues := m.ValidateAllAppearances() - - if len(issues) == 0 { - return "All appearances are valid.", nil - } - - result := fmt.Sprintf("Found %d issues with appearances:\n", len(issues)) - for i, issue := range issues { - if i >= 10 { // Limit output - result += "... (and more)\n" - break - } - result += fmt.Sprintf("%d. %s\n", i+1, issue) - } - - return result, nil -} - -// handleSearchCommand searches for appearances by name -func (m *Manager) handleSearchCommand(args []string) (string, error) { - if len(args) == 0 { - return "", fmt.Errorf("search term required") - } - - searchTerm := args[0] - results := m.SearchAppearancesByName(searchTerm) - - if len(results) == 0 { - return fmt.Sprintf("No appearances found matching '%s'.", searchTerm), nil - } - - result := fmt.Sprintf("Found %d appearances matching '%s':\n", len(results), searchTerm) - for i, appearance := range results { - if i >= 20 { // Limit output - result += "... (and more)\n" - break - } - result += fmt.Sprintf(" %d: %s (min client: %d)\n", - appearance.GetID(), appearance.GetName(), appearance.GetMinClientVersion()) - } - - return result, nil -} - -// handleInfoCommand shows information about a specific appearance -func (m *Manager) handleInfoCommand(args []string) (string, error) { - if len(args) == 0 { - return "", fmt.Errorf("appearance ID required") - } - - var appearanceID int32 - if _, err := fmt.Sscanf(args[0], "%d", &appearanceID); err != nil { - return "", fmt.Errorf("invalid appearance ID: %s", args[0]) - } - - appearance := m.FindAppearanceByID(appearanceID) - if appearance == nil { - return fmt.Sprintf("Appearance %d not found.", appearanceID), nil - } - - result := "Appearance Information:\n" - result += fmt.Sprintf("ID: %d\n", appearance.GetID()) - result += fmt.Sprintf("Name: %s\n", appearance.GetName()) - result += fmt.Sprintf("Min Client Version: %d\n", appearance.GetMinClientVersion()) - - return result, nil -} - -// handleReloadCommand reloads appearances from database -func (m *Manager) handleReloadCommand(_ []string) (string, error) { - if err := m.ReloadFromDatabase(); err != nil { - return "", fmt.Errorf("failed to reload appearances: %w", err) - } - - count := m.GetAppearanceCount() - return fmt.Sprintf("Successfully reloaded %d appearances from database.", count), nil -} - -// Shutdown gracefully shuts down the manager -func (m *Manager) Shutdown() { - if m.logger != nil { - m.logger.LogInfo("Shutting down appearance manager...") - } - - // Clear appearances - m.appearances.ClearAppearances() -} diff --git a/internal/appearances/master.go b/internal/appearances/master.go new file mode 100644 index 0000000..187b3ba --- /dev/null +++ b/internal/appearances/master.go @@ -0,0 +1,235 @@ +package appearances + +import ( + "fmt" + + "eq2emu/internal/common" + "eq2emu/internal/database" +) + +// MasterList manages a collection of appearances using the generic MasterList base +type MasterList struct { + *common.MasterList[int32, *Appearance] +} + +// NewMasterList creates a new appearance master list +func NewMasterList() *MasterList { + return &MasterList{ + MasterList: common.NewMasterList[int32, *Appearance](), + } +} + +// AddAppearance adds an appearance to the master list +func (ml *MasterList) AddAppearance(appearance *Appearance) bool { + return ml.Add(appearance) +} + +// GetAppearance retrieves an appearance by ID +func (ml *MasterList) GetAppearance(id int32) *Appearance { + return ml.Get(id) +} + +// GetAppearanceSafe retrieves an appearance by ID with existence check +func (ml *MasterList) GetAppearanceSafe(id int32) (*Appearance, bool) { + return ml.GetSafe(id) +} + +// HasAppearance checks if an appearance exists by ID +func (ml *MasterList) HasAppearance(id int32) bool { + return ml.Exists(id) +} + +// RemoveAppearance removes an appearance by ID +func (ml *MasterList) RemoveAppearance(id int32) bool { + return ml.Remove(id) +} + +// GetAllAppearances returns all appearances as a map +func (ml *MasterList) GetAllAppearances() map[int32]*Appearance { + return ml.GetAll() +} + +// GetAllAppearancesList returns all appearances as a slice +func (ml *MasterList) GetAllAppearancesList() []*Appearance { + return ml.GetAllSlice() +} + +// GetAppearanceCount returns the number of appearances +func (ml *MasterList) GetAppearanceCount() int { + return ml.Size() +} + +// ClearAppearances removes all appearances from the list +func (ml *MasterList) ClearAppearances() { + ml.Clear() +} + +// FindAppearancesByName finds appearances containing the given name substring +func (ml *MasterList) FindAppearancesByName(nameSubstring string) []*Appearance { + return ml.Filter(func(appearance *Appearance) bool { + return contains(appearance.GetName(), nameSubstring) + }) +} + +// FindAppearancesByMinClient finds appearances with specific minimum client version +func (ml *MasterList) FindAppearancesByMinClient(minClient int16) []*Appearance { + return ml.Filter(func(appearance *Appearance) bool { + return appearance.GetMinClientVersion() == minClient + }) +} + +// GetCompatibleAppearances returns appearances compatible with the given client version +func (ml *MasterList) GetCompatibleAppearances(clientVersion int16) []*Appearance { + return ml.Filter(func(appearance *Appearance) bool { + return appearance.IsCompatibleWithClient(clientVersion) + }) +} + +// GetAppearancesByIDRange returns appearances within the given ID range (inclusive) +func (ml *MasterList) GetAppearancesByIDRange(minID, maxID int32) []*Appearance { + return ml.Filter(func(appearance *Appearance) bool { + id := appearance.GetID() + return id >= minID && id <= maxID + }) +} + +// ValidateAppearances checks all appearances for consistency +func (ml *MasterList) ValidateAppearances() []string { + var issues []string + + ml.ForEach(func(id int32, appearance *Appearance) { + if appearance == nil { + issues = append(issues, fmt.Sprintf("Appearance ID %d is nil", id)) + return + } + + if appearance.GetID() != id { + issues = append(issues, fmt.Sprintf("Appearance ID mismatch: map key %d != appearance ID %d", id, appearance.GetID())) + } + + if len(appearance.GetName()) == 0 { + issues = append(issues, fmt.Sprintf("Appearance ID %d has empty name", id)) + } + + if appearance.GetMinClientVersion() < 0 { + issues = append(issues, fmt.Sprintf("Appearance ID %d has negative min client version: %d", id, appearance.GetMinClientVersion())) + } + }) + + return issues +} + +// IsValid returns true if all appearances are valid +func (ml *MasterList) IsValid() bool { + issues := ml.ValidateAppearances() + return len(issues) == 0 +} + +// GetStatistics returns statistics about the appearance collection +func (ml *MasterList) GetStatistics() map[string]any { + stats := make(map[string]any) + stats["total_appearances"] = ml.Size() + + if ml.IsEmpty() { + return stats + } + + // Count by minimum client version + versionCounts := make(map[int16]int) + var minID, maxID int32 + first := true + + ml.ForEach(func(id int32, appearance *Appearance) { + versionCounts[appearance.GetMinClientVersion()]++ + + if first { + minID = id + maxID = id + first = false + } else { + if id < minID { + minID = id + } + if id > maxID { + maxID = id + } + } + }) + + stats["appearances_by_min_client"] = versionCounts + stats["min_id"] = minID + stats["max_id"] = maxID + stats["id_range"] = maxID - minID + + return stats +} + +// LoadAllAppearances loads all appearances from the database into the master list +func (ml *MasterList) LoadAllAppearances(db *database.Database) error { + if db == nil { + return fmt.Errorf("database connection is nil") + } + + // Clear existing appearances + ml.Clear() + + query := `SELECT appearance_id, name, min_client_version FROM appearances ORDER BY appearance_id` + rows, err := db.Query(query) + if err != nil { + return fmt.Errorf("failed to query appearances: %w", err) + } + defer rows.Close() + + count := 0 + for rows.Next() { + appearance := &Appearance{ + db: db, + isNew: false, + } + + err := rows.Scan(&appearance.ID, &appearance.Name, &appearance.MinClient) + if err != nil { + return fmt.Errorf("failed to scan appearance: %w", err) + } + + if !ml.AddAppearance(appearance) { + return fmt.Errorf("failed to add appearance %d to master list", appearance.ID) + } + + count++ + } + + if err := rows.Err(); err != nil { + return fmt.Errorf("error iterating appearance rows: %w", err) + } + + return nil +} + +// LoadAllAppearancesFromDatabase is a convenience function that creates a master list and loads all appearances +func LoadAllAppearancesFromDatabase(db *database.Database) (*MasterList, error) { + masterList := NewMasterList() + err := masterList.LoadAllAppearances(db) + if err != nil { + return nil, err + } + return masterList, nil +} + +// contains checks if a string contains a substring (case-sensitive) +func contains(str, substr string) bool { + if len(substr) == 0 { + return true + } + if len(str) < len(substr) { + return false + } + + for i := 0; i <= len(str)-len(substr); i++ { + if str[i:i+len(substr)] == substr { + return true + } + } + + return false +} diff --git a/internal/appearances/types.go b/internal/appearances/types.go deleted file mode 100644 index e001ae9..0000000 --- a/internal/appearances/types.go +++ /dev/null @@ -1,65 +0,0 @@ -package appearances - -// Appearance represents a single appearance with ID, name, and client version requirements -type Appearance struct { - id int32 // Appearance ID - name string // Appearance name - minClient int16 // Minimum client version required -} - -// NewAppearance creates a new appearance with the given parameters -func NewAppearance(id int32, name string, minClientVersion int16) *Appearance { - if len(name) == 0 { - return nil - } - - return &Appearance{ - id: id, - name: name, - minClient: minClientVersion, - } -} - -// GetID returns the appearance ID -func (a *Appearance) GetID() int32 { - return a.id -} - -// GetName returns the appearance name -func (a *Appearance) GetName() string { - return a.name -} - -// GetMinClientVersion returns the minimum client version required -func (a *Appearance) GetMinClientVersion() int16 { - return a.minClient -} - -// GetNameString returns the name as a string (alias for GetName for C++ compatibility) -func (a *Appearance) GetNameString() string { - return a.name -} - -// SetName sets the appearance name -func (a *Appearance) SetName(name string) { - a.name = name -} - -// SetMinClientVersion sets the minimum client version -func (a *Appearance) SetMinClientVersion(version int16) { - a.minClient = version -} - -// IsCompatibleWithClient returns true if the appearance is compatible with the given client version -func (a *Appearance) IsCompatibleWithClient(clientVersion int16) bool { - return clientVersion >= a.minClient -} - -// Clone creates a copy of the appearance -func (a *Appearance) Clone() *Appearance { - return &Appearance{ - id: a.id, - name: a.name, - minClient: a.minClient, - } -} diff --git a/test_empty_packets.go b/test_empty_packets.go deleted file mode 100644 index cb9dc34..0000000 --- a/test_empty_packets.go +++ /dev/null @@ -1,33 +0,0 @@ -package main - -import ( - "fmt" - "log" - "eq2emu/internal/packets" -) - -func main() { - // Enable all logging to see warnings - log.SetFlags(log.LstdFlags | log.Lshortfile) - - // This will trigger the init() function in the packets loader - count := packets.GetPacketCount() - fmt.Printf("Loaded %d packet definitions\n", count) - - // Get all packet names to look for any patterns - names := packets.GetPacketNames() - if len(names) == 0 { - fmt.Println("No packets loaded!") - return - } - - // Look for packets that might be empty - fmt.Println("Checking for potentially problematic packets...") - for _, name := range names { - if packet, exists := packets.GetPacket(name); exists { - if len(packet.Fields) == 0 { - fmt.Printf("Empty packet found: %s\n", name) - } - } - } -} \ No newline at end of file diff --git a/test_specific_empty.go b/test_specific_empty.go deleted file mode 100644 index c0022cd..0000000 --- a/test_specific_empty.go +++ /dev/null @@ -1,39 +0,0 @@ -package main - -import ( - "fmt" - "eq2emu/internal/packets/parser" -) - -func main() { - // Test parsing an empty packet that might fail - emptyPacketXML := ` - - -` - - fmt.Println("Testing empty packet parsing...") - packets, err := parser.Parse(emptyPacketXML) - if err != nil { - fmt.Printf("ERROR parsing empty packet: %v\n", err) - } else { - fmt.Printf("SUCCESS: Parsed %d packets\n", len(packets)) - if packet, exists := packets["EmptyTest"]; exists { - fmt.Printf("EmptyTest packet has %d fields\n", len(packet.Fields)) - } - } - - // Test a completely self-closing packet - selfClosingXML := `` - - fmt.Println("\nTesting self-closing packet parsing...") - packets2, err2 := parser.Parse(selfClosingXML) - if err2 != nil { - fmt.Printf("ERROR parsing self-closing packet: %v\n", err2) - } else { - fmt.Printf("SUCCESS: Parsed %d packets\n", len(packets2)) - if packet, exists := packets2["SelfClosingTest"]; exists { - fmt.Printf("SelfClosingTest packet has %d fields\n", len(packet.Fields)) - } - } -} \ No newline at end of file