rework appearances
again
This commit is contained in:
parent
bc81445482
commit
4595c392d1
@ -1,6 +1,7 @@
|
|||||||
package appearances
|
package appearances
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"eq2emu/internal/database"
|
"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 (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"maps"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
"eq2emu/internal/common"
|
|
||||||
"eq2emu/internal/database"
|
"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 {
|
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 {
|
func NewMasterList() *MasterList {
|
||||||
return &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
|
// refreshMetaCache updates the client versions cache
|
||||||
func (ml *MasterList) AddAppearance(appearance *Appearance) bool {
|
func (ml *MasterList) refreshMetaCache() {
|
||||||
return ml.Add(appearance)
|
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 {
|
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
|
// GetAppearanceSafe retrieves an appearance by ID with existence check
|
||||||
func (ml *MasterList) GetAppearanceSafe(id int32) (*Appearance, bool) {
|
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
|
// HasAppearance checks if an appearance exists by ID
|
||||||
func (ml *MasterList) HasAppearance(id int32) bool {
|
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
|
// GetAppearanceClone retrieves a cloned copy of an appearance by ID
|
||||||
func (ml *MasterList) RemoveAppearance(id int32) bool {
|
func (ml *MasterList) GetAppearanceClone(id int32) *Appearance {
|
||||||
return ml.Remove(id)
|
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 {
|
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
|
// GetAllAppearancesList returns all appearances as a slice
|
||||||
func (ml *MasterList) GetAllAppearancesList() []*Appearance {
|
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
|
// FindAppearancesByMinClient finds appearances with specific minimum client version (O(1))
|
||||||
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 {
|
func (ml *MasterList) FindAppearancesByMinClient(minClient int16) []*Appearance {
|
||||||
return ml.Filter(func(appearance *Appearance) bool {
|
ml.mutex.RLock()
|
||||||
return appearance.GetMinClientVersion() == minClient
|
defer ml.mutex.RUnlock()
|
||||||
})
|
return ml.byMinClient[minClient]
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetCompatibleAppearances returns appearances compatible with the given client version
|
// GetCompatibleAppearances returns appearances compatible with the given client version
|
||||||
func (ml *MasterList) GetCompatibleAppearances(clientVersion int16) []*Appearance {
|
func (ml *MasterList) GetCompatibleAppearances(clientVersion int16) []*Appearance {
|
||||||
return ml.Filter(func(appearance *Appearance) bool {
|
ml.mutex.RLock()
|
||||||
return appearance.IsCompatibleWithClient(clientVersion)
|
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)
|
// GetAppearancesByIDRange returns appearances within the given ID range (inclusive)
|
||||||
func (ml *MasterList) GetAppearancesByIDRange(minID, maxID int32) []*Appearance {
|
func (ml *MasterList) GetAppearancesByIDRange(minID, maxID int32) []*Appearance {
|
||||||
return ml.Filter(func(appearance *Appearance) bool {
|
ml.mutex.RLock()
|
||||||
id := appearance.GetID()
|
defer ml.mutex.RUnlock()
|
||||||
return id >= minID && id <= maxID
|
|
||||||
})
|
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
|
// ValidateAppearances checks all appearances for consistency
|
||||||
func (ml *MasterList) ValidateAppearances() []string {
|
func (ml *MasterList) ValidateAppearances() []string {
|
||||||
|
ml.mutex.RLock()
|
||||||
|
defer ml.mutex.RUnlock()
|
||||||
|
|
||||||
var issues []string
|
var issues []string
|
||||||
|
|
||||||
ml.ForEach(func(id int32, appearance *Appearance) {
|
for id, appearance := range ml.appearances {
|
||||||
if appearance == nil {
|
if appearance == nil {
|
||||||
issues = append(issues, fmt.Sprintf("Appearance ID %d is nil", id))
|
issues = append(issues, fmt.Sprintf("Appearance ID %d is nil", id))
|
||||||
return
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if appearance.GetID() != id {
|
if appearance.GetID() != id {
|
||||||
@ -114,7 +430,7 @@ func (ml *MasterList) ValidateAppearances() []string {
|
|||||||
if appearance.GetMinClientVersion() < 0 {
|
if appearance.GetMinClientVersion() < 0 {
|
||||||
issues = append(issues, fmt.Sprintf("Appearance ID %d has negative min client version: %d", id, appearance.GetMinClientVersion()))
|
issues = append(issues, fmt.Sprintf("Appearance ID %d has negative min client version: %d", id, appearance.GetMinClientVersion()))
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
|
|
||||||
return issues
|
return issues
|
||||||
}
|
}
|
||||||
@ -127,10 +443,13 @@ func (ml *MasterList) IsValid() bool {
|
|||||||
|
|
||||||
// GetStatistics returns statistics about the appearance collection
|
// GetStatistics returns statistics about the appearance collection
|
||||||
func (ml *MasterList) GetStatistics() map[string]any {
|
func (ml *MasterList) GetStatistics() map[string]any {
|
||||||
stats := make(map[string]any)
|
ml.mutex.RLock()
|
||||||
stats["total_appearances"] = ml.Size()
|
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
|
return stats
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -139,7 +458,7 @@ func (ml *MasterList) GetStatistics() map[string]any {
|
|||||||
var minID, maxID int32
|
var minID, maxID int32
|
||||||
first := true
|
first := true
|
||||||
|
|
||||||
ml.ForEach(func(id int32, appearance *Appearance) {
|
for id, appearance := range ml.appearances {
|
||||||
versionCounts[appearance.GetMinClientVersion()]++
|
versionCounts[appearance.GetMinClientVersion()]++
|
||||||
|
|
||||||
if first {
|
if first {
|
||||||
@ -154,7 +473,7 @@ func (ml *MasterList) GetStatistics() map[string]any {
|
|||||||
maxID = id
|
maxID = id
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
|
|
||||||
stats["appearances_by_min_client"] = versionCounts
|
stats["appearances_by_min_client"] = versionCounts
|
||||||
stats["min_id"] = minID
|
stats["min_id"] = minID
|
||||||
|
Loading…
x
Reference in New Issue
Block a user