rework appearances
again
This commit is contained in:
parent
bc81445482
commit
4595c392d1
@ -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))
|
||||
}
|
||||
}
|
||||
|
548
internal/appearances/benchmark_test.go
Normal file
548
internal/appearances/benchmark_test.go
Normal 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)
|
||||
}
|
||||
})
|
||||
}
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user