rework appearances

again
This commit is contained in:
Sky Johnson 2025-08-08 10:44:31 -05:00
parent bc81445482
commit 4595c392d1
3 changed files with 1179 additions and 54 deletions

View File

@ -1,6 +1,7 @@
package appearances
import (
"sync"
"testing"
"eq2emu/internal/database"
@ -219,3 +220,260 @@ func TestGetAppearanceTypeName(t *testing.T) {
})
}
}
// TestMasterList tests the bespoke master list implementation
func TestMasterList(t *testing.T) {
masterList := NewMasterList()
if masterList == nil {
t.Fatal("NewMasterList returned nil")
}
if masterList.Size() != 0 {
t.Errorf("Expected size 0, got %d", masterList.Size())
}
// Create test database
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()
// Create appearances for testing
app1 := NewWithData(1001, "Human Male", 1096, db)
app2 := NewWithData(1002, "Elf Female", 1200, db)
app3 := NewWithData(1003, "Dwarf Warrior", 1096, db)
// Test adding
if !masterList.AddAppearance(app1) {
t.Error("Should successfully add app1")
}
if !masterList.AddAppearance(app2) {
t.Error("Should successfully add app2")
}
if !masterList.AddAppearance(app3) {
t.Error("Should successfully add app3")
}
if masterList.Size() != 3 {
t.Errorf("Expected size 3, got %d", masterList.Size())
}
// Test duplicate add (should fail)
if masterList.AddAppearance(app1) {
t.Error("Should not add duplicate appearance")
}
// Test retrieving
retrieved := masterList.GetAppearance(1001)
if retrieved == nil {
t.Error("Should retrieve added appearance")
}
if retrieved.Name != "Human Male" {
t.Errorf("Expected name 'Human Male', got '%s'", retrieved.Name)
}
// Test safe retrieval
retrievedSafe, exists := masterList.GetAppearanceSafe(1001)
if !exists {
t.Error("Should find existing appearance")
}
if retrievedSafe.Name != "Human Male" {
t.Errorf("Expected safe name 'Human Male', got '%s'", retrievedSafe.Name)
}
_, notExists := masterList.GetAppearanceSafe(9999)
if notExists {
t.Error("Should not find non-existent appearance")
}
// Test client version filtering
version1096 := masterList.FindAppearancesByMinClient(1096)
if len(version1096) != 2 {
t.Errorf("Expected 2 appearances with min client 1096, got %d", len(version1096))
}
version1200 := masterList.FindAppearancesByMinClient(1200)
if len(version1200) != 1 {
t.Errorf("Expected 1 appearance with min client 1200, got %d", len(version1200))
}
// Test compatible appearances
compatible1200 := masterList.GetCompatibleAppearances(1200)
if len(compatible1200) != 3 {
t.Errorf("Expected 3 appearances compatible with client 1200, got %d", len(compatible1200))
}
compatible1100 := masterList.GetCompatibleAppearances(1100)
if len(compatible1100) != 2 {
t.Errorf("Expected 2 appearances compatible with client 1100, got %d", len(compatible1100))
}
// Test name searching (case insensitive)
// Names: "Human Male", "Elf Female", "Dwarf Warrior"
humanApps := masterList.FindAppearancesByName("human")
if len(humanApps) != 1 {
t.Errorf("Expected 1 appearance with 'human' in name, got %d", len(humanApps))
}
maleApps := masterList.FindAppearancesByName("male")
if len(maleApps) != 1 { // Only "Human Male" contains "male"
t.Errorf("Expected 1 appearance with 'male' in name, got %d", len(maleApps))
}
// Test exact name match (indexed lookup)
humanMaleApps := masterList.FindAppearancesByName("human male")
if len(humanMaleApps) != 1 {
t.Errorf("Expected 1 appearance with exact name 'human male', got %d", len(humanMaleApps))
}
// Test ID range filtering
rangeApps := masterList.GetAppearancesByIDRange(1001, 1002)
if len(rangeApps) != 2 {
t.Errorf("Expected 2 appearances in range 1001-1002, got %d", len(rangeApps))
}
// Test client version range filtering
clientRangeApps := masterList.GetAppearancesByClientRange(1096, 1096)
if len(clientRangeApps) != 2 {
t.Errorf("Expected 2 appearances in client range 1096-1096, got %d", len(clientRangeApps))
}
// Test metadata caching
clientVersions := masterList.GetClientVersions()
if len(clientVersions) != 2 {
t.Errorf("Expected 2 unique client versions, got %d", len(clientVersions))
}
// Test clone
clone := masterList.GetAppearanceClone(1001)
if clone == nil {
t.Error("Should return cloned appearance")
}
if clone.Name != "Human Male" {
t.Errorf("Expected cloned name 'Human Male', got '%s'", clone.Name)
}
// Test GetAllAppearances
allApps := masterList.GetAllAppearances()
if len(allApps) != 3 {
t.Errorf("Expected 3 appearances in GetAll, got %d", len(allApps))
}
// Test GetAllAppearancesList
allAppsList := masterList.GetAllAppearancesList()
if len(allAppsList) != 3 {
t.Errorf("Expected 3 appearances in GetAllList, got %d", len(allAppsList))
}
// Test update
updatedApp := NewWithData(1001, "Updated Human", 1500, db)
if err := masterList.UpdateAppearance(updatedApp); err != nil {
t.Errorf("Update should succeed: %v", err)
}
// Verify update worked
retrievedUpdated := masterList.GetAppearance(1001)
if retrievedUpdated.Name != "Updated Human" {
t.Errorf("Expected updated name 'Updated Human', got '%s'", retrievedUpdated.Name)
}
// Verify client version index updated
version1500 := masterList.FindAppearancesByMinClient(1500)
if len(version1500) != 1 {
t.Errorf("Expected 1 appearance with min client 1500, got %d", len(version1500))
}
// Test removal
if !masterList.RemoveAppearance(1001) {
t.Error("Should successfully remove appearance")
}
if masterList.Size() != 2 {
t.Errorf("Expected size 2 after removal, got %d", masterList.Size())
}
// Test validation
issues := masterList.ValidateAppearances()
if len(issues) != 0 {
t.Errorf("Expected no validation issues, got %d", len(issues))
}
// Test statistics
stats := masterList.GetStatistics()
if stats["total_appearances"] != 2 {
t.Errorf("Expected statistics total 2, got %v", stats["total_appearances"])
}
// Test clear
masterList.Clear()
if masterList.Size() != 0 {
t.Errorf("Expected size 0 after clear, got %d", masterList.Size())
}
}
// TestMasterListConcurrency tests thread safety of the master list
func TestMasterListConcurrency(t *testing.T) {
masterList := NewMasterList()
// Create test database
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()
const numWorkers = 10
const appsPerWorker = 100
var wg sync.WaitGroup
// Concurrently add appearances
wg.Add(numWorkers)
for i := 0; i < numWorkers; i++ {
go func(workerID int) {
defer wg.Done()
for j := 0; j < appsPerWorker; j++ {
app := NewWithData(
int32(workerID*appsPerWorker+j+1),
"Concurrent Test",
int16(1096+(workerID%3)*100),
db,
)
masterList.AddAppearance(app)
}
}(i)
}
// Concurrently read appearances
wg.Add(numWorkers)
for i := 0; i < numWorkers; i++ {
go func() {
defer wg.Done()
for j := 0; j < appsPerWorker; j++ {
// Random reads
_ = masterList.GetAppearance(int32(j + 1))
_ = masterList.FindAppearancesByMinClient(1096)
_ = masterList.GetCompatibleAppearances(1200)
_ = masterList.Size()
}
}()
}
wg.Wait()
// Verify final state
expectedSize := numWorkers * appsPerWorker
if masterList.Size() != expectedSize {
t.Errorf("Expected size %d, got %d", expectedSize, masterList.Size())
}
clientVersions := masterList.GetClientVersions()
if len(clientVersions) != 3 {
t.Errorf("Expected 3 client versions, got %d", len(clientVersions))
}
}

View File

@ -0,0 +1,548 @@
package appearances
import (
"fmt"
"math/rand"
"sync"
"testing"
"eq2emu/internal/database"
)
// Global shared master list for benchmarks to avoid repeated setup
var (
sharedAppearanceMasterList *MasterList
sharedAppearances []*Appearance
appearanceSetupOnce sync.Once
)
// setupSharedAppearanceMasterList creates the shared master list once
func setupSharedAppearanceMasterList(b *testing.B) {
appearanceSetupOnce.Do(func() {
// Create test database
db, err := database.NewSQLite("file::memory:?mode=memory&cache=shared")
if err != nil {
b.Fatalf("Failed to create test database: %v", err)
}
sharedAppearanceMasterList = NewMasterList()
// Pre-populate with appearances for realistic testing
const numAppearances = 1000
sharedAppearances = make([]*Appearance, numAppearances)
clientVersions := []int16{1096, 1200, 1300, 1400, 1500}
nameTemplates := []string{
"Human %s",
"Elf %s",
"Dwarf %s",
"Halfling %s",
"Barbarian %s",
"Dark Elf %s",
"Wood Elf %s",
"High Elf %s",
"Gnome %s",
"Troll %s",
}
genders := []string{"Male", "Female"}
for i := range numAppearances {
sharedAppearances[i] = NewWithData(
int32(i+1),
fmt.Sprintf(nameTemplates[i%len(nameTemplates)], genders[i%len(genders)]),
clientVersions[i%len(clientVersions)],
db,
)
sharedAppearanceMasterList.AddAppearance(sharedAppearances[i])
}
})
}
// createTestAppearance creates an appearance for benchmarking
func createTestAppearance(b *testing.B, id int32) *Appearance {
b.Helper()
// Use nil database for benchmarking in-memory operations
clientVersions := []int16{1096, 1200, 1300, 1400, 1500}
nameTemplates := []string{"Human", "Elf", "Dwarf", "Halfling"}
genders := []string{"Male", "Female"}
app := NewWithData(
id,
fmt.Sprintf("Benchmark %s %s", nameTemplates[id%int32(len(nameTemplates))], genders[id%2]),
clientVersions[id%int32(len(clientVersions))],
nil,
)
return app
}
// BenchmarkAppearanceCreation measures appearance creation performance
func BenchmarkAppearanceCreation(b *testing.B) {
db, err := database.NewSQLite("file::memory:?mode=memory&cache=shared")
if err != nil {
b.Fatalf("Failed to create test database: %v", err)
}
defer db.Close()
b.ResetTimer()
b.Run("Sequential", func(b *testing.B) {
for i := 0; i < b.N; i++ {
app := New(db)
app.ID = int32(i)
app.Name = fmt.Sprintf("Appearance %d", i)
app.MinClient = 1096
_ = app
}
})
b.Run("Parallel", func(b *testing.B) {
b.RunParallel(func(pb *testing.PB) {
id := int32(0)
for pb.Next() {
app := New(db)
app.ID = id
app.Name = fmt.Sprintf("Appearance %d", id)
app.MinClient = 1096
id++
_ = app
}
})
})
b.Run("NewWithData", func(b *testing.B) {
for i := 0; i < b.N; i++ {
app := NewWithData(int32(i), fmt.Sprintf("Appearance %d", i), 1096, db)
_ = app
}
})
}
// BenchmarkAppearanceOperations measures individual appearance operations
func BenchmarkAppearanceOperations(b *testing.B) {
app := createTestAppearance(b, 1001)
b.Run("GetID", func(b *testing.B) {
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
_ = app.GetID()
}
})
})
b.Run("GetName", func(b *testing.B) {
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
_ = app.GetName()
}
})
})
b.Run("GetMinClientVersion", func(b *testing.B) {
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
_ = app.GetMinClientVersion()
}
})
})
b.Run("IsCompatibleWithClient", func(b *testing.B) {
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
_ = app.IsCompatibleWithClient(1200)
}
})
})
b.Run("Clone", func(b *testing.B) {
for b.Loop() {
_ = app.Clone()
}
})
b.Run("IsNew", func(b *testing.B) {
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
_ = app.IsNew()
}
})
})
}
// BenchmarkMasterListOperations measures master list performance
func BenchmarkMasterListOperations(b *testing.B) {
setupSharedAppearanceMasterList(b)
ml := sharedAppearanceMasterList
b.Run("GetAppearance", func(b *testing.B) {
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
id := int32(rand.Intn(1000) + 1)
_ = ml.GetAppearance(id)
}
})
})
b.Run("AddAppearance", func(b *testing.B) {
// Create a separate master list for add operations
addML := NewMasterList()
startID := int32(10000)
// Pre-create appearances to measure just the Add operation
appsToAdd := make([]*Appearance, b.N)
for i := 0; i < b.N; i++ {
appsToAdd[i] = createTestAppearance(b, startID+int32(i))
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
addML.AddAppearance(appsToAdd[i])
}
})
b.Run("GetAppearanceSafe", func(b *testing.B) {
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
id := int32(rand.Intn(1000) + 1)
_, _ = ml.GetAppearanceSafe(id)
}
})
})
b.Run("HasAppearance", func(b *testing.B) {
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
id := int32(rand.Intn(1000) + 1)
_ = ml.HasAppearance(id)
}
})
})
b.Run("FindAppearancesByMinClient", func(b *testing.B) {
clientVersions := []int16{1096, 1200, 1300, 1400, 1500}
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
version := clientVersions[rand.Intn(len(clientVersions))]
_ = ml.FindAppearancesByMinClient(version)
}
})
})
b.Run("GetCompatibleAppearances", func(b *testing.B) {
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
version := int16(1200 + rand.Intn(300))
_ = ml.GetCompatibleAppearances(version)
}
})
})
b.Run("FindAppearancesByName", func(b *testing.B) {
searchTerms := []string{"human", "male", "elf", "female", "dwarf"}
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
term := searchTerms[rand.Intn(len(searchTerms))]
_ = ml.FindAppearancesByName(term)
}
})
})
b.Run("GetAppearancesByIDRange", func(b *testing.B) {
for b.Loop() {
start := int32(rand.Intn(900) + 1)
end := start + int32(rand.Intn(100)+10)
_ = ml.GetAppearancesByIDRange(start, end)
}
})
b.Run("GetAppearancesByClientRange", func(b *testing.B) {
for b.Loop() {
minVersion := int16(1096 + rand.Intn(200))
maxVersion := minVersion + int16(rand.Intn(200))
_ = ml.GetAppearancesByClientRange(minVersion, maxVersion)
}
})
b.Run("GetClientVersions", func(b *testing.B) {
for b.Loop() {
_ = ml.GetClientVersions()
}
})
b.Run("Size", func(b *testing.B) {
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
_ = ml.Size()
}
})
})
b.Run("GetAppearanceCount", func(b *testing.B) {
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
_ = ml.GetAppearanceCount()
}
})
})
}
// BenchmarkConcurrentOperations tests mixed workload performance
func BenchmarkConcurrentOperations(b *testing.B) {
setupSharedAppearanceMasterList(b)
ml := sharedAppearanceMasterList
b.Run("MixedOperations", func(b *testing.B) {
clientVersions := []int16{1096, 1200, 1300, 1400, 1500}
searchTerms := []string{"human", "male", "elf", "female", "dwarf"}
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
switch rand.Intn(10) {
case 0:
id := int32(rand.Intn(1000) + 1)
_ = ml.GetAppearance(id)
case 1:
id := int32(rand.Intn(1000) + 1)
_, _ = ml.GetAppearanceSafe(id)
case 2:
id := int32(rand.Intn(1000) + 1)
_ = ml.HasAppearance(id)
case 3:
version := clientVersions[rand.Intn(len(clientVersions))]
_ = ml.FindAppearancesByMinClient(version)
case 4:
version := int16(1200 + rand.Intn(300))
_ = ml.GetCompatibleAppearances(version)
case 5:
term := searchTerms[rand.Intn(len(searchTerms))]
_ = ml.FindAppearancesByName(term)
case 6:
start := int32(rand.Intn(900) + 1)
end := start + int32(rand.Intn(100)+10)
_ = ml.GetAppearancesByIDRange(start, end)
case 7:
minVersion := int16(1096 + rand.Intn(200))
maxVersion := minVersion + int16(rand.Intn(200))
_ = ml.GetAppearancesByClientRange(minVersion, maxVersion)
case 8:
_ = ml.GetClientVersions()
case 9:
_ = ml.Size()
}
}
})
})
}
// BenchmarkMemoryAllocation measures memory allocation patterns
func BenchmarkMemoryAllocation(b *testing.B) {
db, err := database.NewSQLite("file::memory:?mode=memory&cache=shared")
if err != nil {
b.Fatalf("Failed to create test database: %v", err)
}
defer db.Close()
b.Run("AppearanceAllocation", func(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
app := New(db)
app.ID = int32(i)
app.Name = fmt.Sprintf("Appearance %d", i)
app.MinClient = 1096
_ = app
}
})
b.Run("NewWithDataAllocation", func(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
app := NewWithData(int32(i), fmt.Sprintf("Appearance %d", i), 1096, db)
_ = app
}
})
b.Run("MasterListAllocation", func(b *testing.B) {
b.ReportAllocs()
for b.Loop() {
ml := NewMasterList()
_ = ml
}
})
b.Run("AddAppearance_Allocations", func(b *testing.B) {
b.ReportAllocs()
ml := NewMasterList()
for i := 0; i < b.N; i++ {
app := createTestAppearance(b, int32(i+1))
ml.AddAppearance(app)
}
})
b.Run("FindAppearancesByMinClient_Allocations", func(b *testing.B) {
setupSharedAppearanceMasterList(b)
ml := sharedAppearanceMasterList
b.ReportAllocs()
b.ResetTimer()
for b.Loop() {
_ = ml.FindAppearancesByMinClient(1096)
}
})
b.Run("FindAppearancesByName_Allocations", func(b *testing.B) {
setupSharedAppearanceMasterList(b)
ml := sharedAppearanceMasterList
b.ReportAllocs()
b.ResetTimer()
for b.Loop() {
_ = ml.FindAppearancesByName("human")
}
})
b.Run("GetClientVersions_Allocations", func(b *testing.B) {
setupSharedAppearanceMasterList(b)
ml := sharedAppearanceMasterList
b.ReportAllocs()
b.ResetTimer()
for b.Loop() {
_ = ml.GetClientVersions()
}
})
}
// BenchmarkUpdateOperations measures update performance
func BenchmarkUpdateOperations(b *testing.B) {
setupSharedAppearanceMasterList(b)
ml := sharedAppearanceMasterList
b.Run("UpdateAppearance", func(b *testing.B) {
// Create appearances to update
updateApps := make([]*Appearance, b.N)
for i := 0; i < b.N; i++ {
updateApps[i] = createTestAppearance(b, int32((i%1000)+1))
updateApps[i].Name = "Updated Name"
updateApps[i].MinClient = 1600
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = ml.UpdateAppearance(updateApps[i])
}
})
b.Run("RemoveAppearance", func(b *testing.B) {
// Create a separate master list for removal testing
removeML := NewMasterList()
// Add appearances to remove
for i := 0; i < b.N; i++ {
app := createTestAppearance(b, int32(i+1))
removeML.AddAppearance(app)
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
removeML.RemoveAppearance(int32(i + 1))
}
})
}
// BenchmarkValidation measures validation performance
func BenchmarkValidation(b *testing.B) {
setupSharedAppearanceMasterList(b)
ml := sharedAppearanceMasterList
b.Run("ValidateAppearances", func(b *testing.B) {
for b.Loop() {
_ = ml.ValidateAppearances()
}
})
b.Run("IsValid", func(b *testing.B) {
for b.Loop() {
_ = ml.IsValid()
}
})
b.Run("IndividualValidation", func(b *testing.B) {
app := createTestAppearance(b, 1001)
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
_ = app.IsCompatibleWithClient(1200)
}
})
})
}
// BenchmarkCloneOperations measures cloning performance
func BenchmarkCloneOperations(b *testing.B) {
setupSharedAppearanceMasterList(b)
ml := sharedAppearanceMasterList
b.Run("GetAppearanceClone", func(b *testing.B) {
for b.Loop() {
id := int32(rand.Intn(1000) + 1)
_ = ml.GetAppearanceClone(id)
}
})
b.Run("DirectClone", func(b *testing.B) {
app := createTestAppearance(b, 1001)
for b.Loop() {
_ = app.Clone()
}
})
}
// BenchmarkStatistics measures statistics performance
func BenchmarkStatistics(b *testing.B) {
setupSharedAppearanceMasterList(b)
ml := sharedAppearanceMasterList
b.Run("GetStatistics", func(b *testing.B) {
for b.Loop() {
_ = ml.GetStatistics()
}
})
b.Run("GetAllAppearances", func(b *testing.B) {
for b.Loop() {
_ = ml.GetAllAppearances()
}
})
b.Run("GetAllAppearancesList", func(b *testing.B) {
for b.Loop() {
_ = ml.GetAllAppearancesList()
}
})
}
// BenchmarkStringOperations measures string operations performance
func BenchmarkStringOperations(b *testing.B) {
b.Run("GetAppearanceType", func(b *testing.B) {
typeNames := []string{"hair_color1", "skin_color", "eye_color", "unknown_type"}
for b.Loop() {
typeName := typeNames[rand.Intn(len(typeNames))]
_ = GetAppearanceType(typeName)
}
})
b.Run("GetAppearanceTypeName", func(b *testing.B) {
typeConstants := []int8{AppearanceHairColor1, AppearanceSkinColor, AppearanceEyeColor, -1}
for b.Loop() {
typeConst := typeConstants[rand.Intn(len(typeConstants))]
_ = GetAppearanceTypeName(typeConst)
}
})
b.Run("ContainsSubstring", func(b *testing.B) {
testStrings := []string{"Human Male Fighter", "Elf Female Mage", "Dwarf Male Warrior"}
searchTerms := []string{"human", "male", "elf", "notfound"}
for b.Loop() {
str := testStrings[rand.Intn(len(testStrings))]
term := searchTerms[rand.Intn(len(searchTerms))]
_ = contains(str, term)
}
})
}

View File

@ -2,105 +2,421 @@ package appearances
import (
"fmt"
"maps"
"strings"
"sync"
"eq2emu/internal/common"
"eq2emu/internal/database"
)
// MasterList manages a collection of appearances using the generic MasterList base
// MasterList is a specialized appearance master list optimized for:
// - Fast ID-based lookups (O(1))
// - Fast name-based searching (indexed)
// - Fast client version filtering (O(1))
// - Efficient range queries and statistics
type MasterList struct {
*common.MasterList[int32, *Appearance]
// Core storage
appearances map[int32]*Appearance // ID -> Appearance
mutex sync.RWMutex
// Specialized indices for O(1) lookups
byMinClient map[int16][]*Appearance // MinClient -> appearances
byNamePart map[string][]*Appearance // Name substring -> appearances (for common searches)
// Cached metadata
clientVersions []int16 // Unique client versions (cached)
metaStale bool // Whether metadata cache needs refresh
}
// NewMasterList creates a new appearance master list
// NewMasterList creates a new specialized appearance master list
func NewMasterList() *MasterList {
return &MasterList{
MasterList: common.NewMasterList[int32, *Appearance](),
appearances: make(map[int32]*Appearance),
byMinClient: make(map[int16][]*Appearance),
byNamePart: make(map[string][]*Appearance),
metaStale: true,
}
}
// AddAppearance adds an appearance to the master list
func (ml *MasterList) AddAppearance(appearance *Appearance) bool {
return ml.Add(appearance)
// refreshMetaCache updates the client versions cache
func (ml *MasterList) refreshMetaCache() {
if !ml.metaStale {
return
}
clientVersionSet := make(map[int16]struct{})
// Collect unique client versions
for _, appearance := range ml.appearances {
clientVersionSet[appearance.MinClient] = struct{}{}
}
// Clear existing cache and rebuild
ml.clientVersions = ml.clientVersions[:0]
for version := range clientVersionSet {
ml.clientVersions = append(ml.clientVersions, version)
}
ml.metaStale = false
}
// GetAppearance retrieves an appearance by ID
// updateNameIndices updates name-based indices for an appearance
func (ml *MasterList) updateNameIndices(appearance *Appearance, add bool) {
// Index common name patterns for fast searching
name := strings.ToLower(appearance.Name)
partsSet := make(map[string]struct{}) // Use set to avoid duplicates
// Add full name
partsSet[name] = struct{}{}
// Add word-based indices for multi-word names
if strings.Contains(name, " ") {
words := strings.FieldsSeq(name)
for word := range words {
partsSet[word] = struct{}{}
}
}
// Add prefix indices for common prefixes
if len(name) >= 3 {
partsSet[name[:3]] = struct{}{}
}
if len(name) >= 5 {
partsSet[name[:5]] = struct{}{}
}
// Convert set to slice
for part := range partsSet {
if add {
ml.byNamePart[part] = append(ml.byNamePart[part], appearance)
} else {
// Remove from name part index
if namePartApps := ml.byNamePart[part]; namePartApps != nil {
for i, app := range namePartApps {
if app.ID == appearance.ID {
ml.byNamePart[part] = append(namePartApps[:i], namePartApps[i+1:]...)
break
}
}
}
}
}
}
// AddAppearance adds an appearance with full indexing
func (ml *MasterList) AddAppearance(appearance *Appearance) bool {
if appearance == nil {
return false
}
ml.mutex.Lock()
defer ml.mutex.Unlock()
// Check if exists
if _, exists := ml.appearances[appearance.ID]; exists {
return false
}
// Add to core storage
ml.appearances[appearance.ID] = appearance
// Update client version index
ml.byMinClient[appearance.MinClient] = append(ml.byMinClient[appearance.MinClient], appearance)
// Update name indices
ml.updateNameIndices(appearance, true)
// Invalidate metadata cache
ml.metaStale = true
return true
}
// GetAppearance retrieves by ID (O(1))
func (ml *MasterList) GetAppearance(id int32) *Appearance {
return ml.Get(id)
ml.mutex.RLock()
defer ml.mutex.RUnlock()
return ml.appearances[id]
}
// GetAppearanceSafe retrieves an appearance by ID with existence check
func (ml *MasterList) GetAppearanceSafe(id int32) (*Appearance, bool) {
return ml.GetSafe(id)
ml.mutex.RLock()
defer ml.mutex.RUnlock()
appearance, exists := ml.appearances[id]
return appearance, exists
}
// HasAppearance checks if an appearance exists by ID
func (ml *MasterList) HasAppearance(id int32) bool {
return ml.Exists(id)
ml.mutex.RLock()
defer ml.mutex.RUnlock()
_, exists := ml.appearances[id]
return exists
}
// RemoveAppearance removes an appearance by ID
func (ml *MasterList) RemoveAppearance(id int32) bool {
return ml.Remove(id)
// GetAppearanceClone retrieves a cloned copy of an appearance by ID
func (ml *MasterList) GetAppearanceClone(id int32) *Appearance {
ml.mutex.RLock()
defer ml.mutex.RUnlock()
appearance := ml.appearances[id]
if appearance == nil {
return nil
}
return appearance.Clone()
}
// GetAllAppearances returns all appearances as a map
// GetAllAppearances returns a copy of all appearances map
func (ml *MasterList) GetAllAppearances() map[int32]*Appearance {
return ml.GetAll()
ml.mutex.RLock()
defer ml.mutex.RUnlock()
// Return a copy to prevent external modification
result := make(map[int32]*Appearance, len(ml.appearances))
maps.Copy(result, ml.appearances)
return result
}
// GetAllAppearancesList returns all appearances as a slice
func (ml *MasterList) GetAllAppearancesList() []*Appearance {
return ml.GetAllSlice()
ml.mutex.RLock()
defer ml.mutex.RUnlock()
result := make([]*Appearance, 0, len(ml.appearances))
for _, appearance := range ml.appearances {
result = append(result, appearance)
}
return result
}
// 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
// FindAppearancesByMinClient finds appearances with specific minimum client version (O(1))
func (ml *MasterList) FindAppearancesByMinClient(minClient int16) []*Appearance {
return ml.Filter(func(appearance *Appearance) bool {
return appearance.GetMinClientVersion() == minClient
})
ml.mutex.RLock()
defer ml.mutex.RUnlock()
return ml.byMinClient[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)
})
ml.mutex.RLock()
defer ml.mutex.RUnlock()
var result []*Appearance
// Collect all appearances with MinClient <= clientVersion
for minClient, appearances := range ml.byMinClient {
if minClient <= clientVersion {
result = append(result, appearances...)
}
}
return result
}
// FindAppearancesByName finds appearances containing the given name substring (optimized)
func (ml *MasterList) FindAppearancesByName(nameSubstring string) []*Appearance {
ml.mutex.RLock()
defer ml.mutex.RUnlock()
searchKey := strings.ToLower(nameSubstring)
// Try indexed lookup first for exact matches
if indexedApps := ml.byNamePart[searchKey]; indexedApps != nil {
// Return a copy to prevent external modification
result := make([]*Appearance, len(indexedApps))
copy(result, indexedApps)
return result
}
// Fallback to full scan for partial matches
var result []*Appearance
for _, appearance := range ml.appearances {
if contains(strings.ToLower(appearance.Name), searchKey) {
result = append(result, appearance)
}
}
return result
}
// 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
})
ml.mutex.RLock()
defer ml.mutex.RUnlock()
var result []*Appearance
for id, appearance := range ml.appearances {
if id >= minID && id <= maxID {
result = append(result, appearance)
}
}
return result
}
// GetAppearancesByClientRange returns appearances compatible within client version range
func (ml *MasterList) GetAppearancesByClientRange(minClientVersion, maxClientVersion int16) []*Appearance {
ml.mutex.RLock()
defer ml.mutex.RUnlock()
var result []*Appearance
// Collect all appearances with MinClient in range
for minClient, appearances := range ml.byMinClient {
if minClient >= minClientVersion && minClient <= maxClientVersion {
result = append(result, appearances...)
}
}
return result
}
// GetClientVersions returns all unique client versions using cached results
func (ml *MasterList) GetClientVersions() []int16 {
ml.mutex.Lock() // Need write lock to potentially update cache
defer ml.mutex.Unlock()
ml.refreshMetaCache()
// Return a copy to prevent external modification
result := make([]int16, len(ml.clientVersions))
copy(result, ml.clientVersions)
return result
}
// RemoveAppearance removes an appearance and updates all indices
func (ml *MasterList) RemoveAppearance(id int32) bool {
ml.mutex.Lock()
defer ml.mutex.Unlock()
appearance, exists := ml.appearances[id]
if !exists {
return false
}
// Remove from core storage
delete(ml.appearances, id)
// Remove from client version index
clientApps := ml.byMinClient[appearance.MinClient]
for i, app := range clientApps {
if app.ID == id {
ml.byMinClient[appearance.MinClient] = append(clientApps[:i], clientApps[i+1:]...)
break
}
}
// Remove from name indices
ml.updateNameIndices(appearance, false)
// Invalidate metadata cache
ml.metaStale = true
return true
}
// UpdateAppearance updates an existing appearance
func (ml *MasterList) UpdateAppearance(appearance *Appearance) error {
if appearance == nil {
return fmt.Errorf("appearance cannot be nil")
}
ml.mutex.Lock()
defer ml.mutex.Unlock()
// Check if exists
old, exists := ml.appearances[appearance.ID]
if !exists {
return fmt.Errorf("appearance %d not found", appearance.ID)
}
// Remove old appearance from indices (but not core storage yet)
clientApps := ml.byMinClient[old.MinClient]
for i, app := range clientApps {
if app.ID == appearance.ID {
ml.byMinClient[old.MinClient] = append(clientApps[:i], clientApps[i+1:]...)
break
}
}
// Remove from name indices
ml.updateNameIndices(old, false)
// Update core storage
ml.appearances[appearance.ID] = appearance
// Add new appearance to indices
ml.byMinClient[appearance.MinClient] = append(ml.byMinClient[appearance.MinClient], appearance)
ml.updateNameIndices(appearance, true)
// Invalidate metadata cache
ml.metaStale = true
return nil
}
// GetAppearanceCount returns the number of appearances
func (ml *MasterList) GetAppearanceCount() int {
ml.mutex.RLock()
defer ml.mutex.RUnlock()
return len(ml.appearances)
}
// Size returns the total number of appearances
func (ml *MasterList) Size() int {
ml.mutex.RLock()
defer ml.mutex.RUnlock()
return len(ml.appearances)
}
// IsEmpty returns true if the master list is empty
func (ml *MasterList) IsEmpty() bool {
ml.mutex.RLock()
defer ml.mutex.RUnlock()
return len(ml.appearances) == 0
}
// ClearAppearances removes all appearances from the list
func (ml *MasterList) ClearAppearances() {
ml.mutex.Lock()
defer ml.mutex.Unlock()
// Clear all maps
ml.appearances = make(map[int32]*Appearance)
ml.byMinClient = make(map[int16][]*Appearance)
ml.byNamePart = make(map[string][]*Appearance)
// Clear cached metadata
ml.clientVersions = ml.clientVersions[:0]
ml.metaStale = true
}
// Clear removes all appearances from the master list
func (ml *MasterList) Clear() {
ml.ClearAppearances()
}
// ForEach executes a function for each appearance
func (ml *MasterList) ForEach(fn func(int32, *Appearance)) {
ml.mutex.RLock()
defer ml.mutex.RUnlock()
for id, appearance := range ml.appearances {
fn(id, appearance)
}
}
// ValidateAppearances checks all appearances for consistency
func (ml *MasterList) ValidateAppearances() []string {
ml.mutex.RLock()
defer ml.mutex.RUnlock()
var issues []string
ml.ForEach(func(id int32, appearance *Appearance) {
for id, appearance := range ml.appearances {
if appearance == nil {
issues = append(issues, fmt.Sprintf("Appearance ID %d is nil", id))
return
continue
}
if appearance.GetID() != id {
@ -114,7 +430,7 @@ func (ml *MasterList) ValidateAppearances() []string {
if appearance.GetMinClientVersion() < 0 {
issues = append(issues, fmt.Sprintf("Appearance ID %d has negative min client version: %d", id, appearance.GetMinClientVersion()))
}
})
}
return issues
}
@ -127,10 +443,13 @@ func (ml *MasterList) IsValid() bool {
// GetStatistics returns statistics about the appearance collection
func (ml *MasterList) GetStatistics() map[string]any {
stats := make(map[string]any)
stats["total_appearances"] = ml.Size()
ml.mutex.RLock()
defer ml.mutex.RUnlock()
if ml.IsEmpty() {
stats := make(map[string]any)
stats["total_appearances"] = len(ml.appearances)
if len(ml.appearances) == 0 {
return stats
}
@ -139,7 +458,7 @@ func (ml *MasterList) GetStatistics() map[string]any {
var minID, maxID int32
first := true
ml.ForEach(func(id int32, appearance *Appearance) {
for id, appearance := range ml.appearances {
versionCounts[appearance.GetMinClientVersion()]++
if first {
@ -154,7 +473,7 @@ func (ml *MasterList) GetStatistics() map[string]any {
maxID = id
}
}
})
}
stats["appearances_by_min_client"] = versionCounts
stats["min_id"] = minID