revert achievements to bespoke master list

This commit is contained in:
Sky Johnson 2025-08-08 09:53:59 -05:00
parent c31866d080
commit 4a17075783
8 changed files with 835 additions and 1336 deletions

View File

@ -1,6 +1,7 @@
package achievements
import (
"sync"
"testing"
"eq2emu/internal/database"
@ -60,8 +61,8 @@ func TestSimpleAchievement(t *testing.T) {
}
}
// TestMasterListWithGeneric tests the master list with generic base
func TestMasterListWithGeneric(t *testing.T) {
// TestMasterList tests the bespoke master list implementation
func TestMasterList(t *testing.T) {
masterList := NewMasterList()
if masterList == nil {
@ -72,22 +73,52 @@ func TestMasterListWithGeneric(t *testing.T) {
t.Errorf("Expected size 0, got %d", masterList.Size())
}
// Create an achievement (need database for new pattern)
db, _ := database.NewSQLite("file::memory:?mode=memory&cache=shared")
// 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()
achievement := New(db)
achievement.AchievementID = 1001
achievement.Title = "Test Achievement"
achievement.Category = "Testing"
// Create achievements for testing
achievement1 := New(db)
achievement1.AchievementID = 1001
achievement1.Title = "Test Achievement 1"
achievement1.Category = "Testing"
achievement1.Expansion = "Classic"
achievement2 := New(db)
achievement2.AchievementID = 1002
achievement2.Title = "Test Achievement 2"
achievement2.Category = "Combat"
achievement2.Expansion = "Classic"
achievement3 := New(db)
achievement3.AchievementID = 1003
achievement3.Title = "Test Achievement 3"
achievement3.Category = "Testing"
achievement3.Expansion = "Expansion1"
// Test adding
if !masterList.AddAchievement(achievement) {
t.Error("Should successfully add achievement")
if !masterList.AddAchievement(achievement1) {
t.Error("Should successfully add achievement1")
}
if masterList.Size() != 1 {
t.Errorf("Expected size 1, got %d", masterList.Size())
if !masterList.AddAchievement(achievement2) {
t.Error("Should successfully add achievement2")
}
if !masterList.AddAchievement(achievement3) {
t.Error("Should successfully add achievement3")
}
if masterList.Size() != 3 {
t.Errorf("Expected size 3, got %d", masterList.Size())
}
// Test duplicate add (should fail)
if masterList.AddAchievement(achievement1) {
t.Error("Should not add duplicate achievement")
}
// Test retrieving
@ -96,13 +127,160 @@ func TestMasterListWithGeneric(t *testing.T) {
t.Error("Should retrieve added achievement")
}
if retrieved.Title != "Test Achievement" {
t.Errorf("Expected title 'Test Achievement', got '%s'", retrieved.Title)
if retrieved.Title != "Test Achievement 1" {
t.Errorf("Expected title 'Test Achievement 1', got '%s'", retrieved.Title)
}
// Test filtering
achievements := masterList.GetAchievementsByCategory("Testing")
if len(achievements) != 1 {
t.Errorf("Expected 1 achievement in Testing category, got %d", len(achievements))
// Test category filtering
testingAchievements := masterList.GetAchievementsByCategory("Testing")
if len(testingAchievements) != 2 {
t.Errorf("Expected 2 achievements in Testing category, got %d", len(testingAchievements))
}
combatAchievements := masterList.GetAchievementsByCategory("Combat")
if len(combatAchievements) != 1 {
t.Errorf("Expected 1 achievement in Combat category, got %d", len(combatAchievements))
}
// Test expansion filtering
classicAchievements := masterList.GetAchievementsByExpansion("Classic")
if len(classicAchievements) != 2 {
t.Errorf("Expected 2 achievements in Classic expansion, got %d", len(classicAchievements))
}
expansion1Achievements := masterList.GetAchievementsByExpansion("Expansion1")
if len(expansion1Achievements) != 1 {
t.Errorf("Expected 1 achievement in Expansion1, got %d", len(expansion1Achievements))
}
// Test combined filtering
combined := masterList.GetAchievementsByCategoryAndExpansion("Testing", "Classic")
if len(combined) != 1 {
t.Errorf("Expected 1 achievement matching Testing+Classic, got %d", len(combined))
}
// Test metadata caching
categories := masterList.GetCategories()
if len(categories) != 2 {
t.Errorf("Expected 2 unique categories, got %d", len(categories))
}
expansions := masterList.GetExpansions()
if len(expansions) != 2 {
t.Errorf("Expected 2 unique expansions, got %d", len(expansions))
}
// Test clone
clone := masterList.GetAchievementClone(1001)
if clone == nil {
t.Error("Should return cloned achievement")
}
if clone.Title != "Test Achievement 1" {
t.Errorf("Expected cloned title 'Test Achievement 1', got '%s'", clone.Title)
}
// Test GetAllAchievements
allAchievements := masterList.GetAllAchievements()
if len(allAchievements) != 3 {
t.Errorf("Expected 3 achievements in GetAll, got %d", len(allAchievements))
}
// Test update
updatedAchievement := New(db)
updatedAchievement.AchievementID = 1001
updatedAchievement.Title = "Updated Achievement"
updatedAchievement.Category = "Updated"
updatedAchievement.Expansion = "Updated"
if err := masterList.UpdateAchievement(updatedAchievement); err != nil {
t.Errorf("Update should succeed: %v", err)
}
// Verify update worked
retrievedUpdated := masterList.GetAchievement(1001)
if retrievedUpdated.Title != "Updated Achievement" {
t.Errorf("Expected updated title 'Updated Achievement', got '%s'", retrievedUpdated.Title)
}
// Verify category index updated
updatedCategoryAchievements := masterList.GetAchievementsByCategory("Updated")
if len(updatedCategoryAchievements) != 1 {
t.Errorf("Expected 1 achievement in Updated category, got %d", len(updatedCategoryAchievements))
}
// Test removal
if !masterList.RemoveAchievement(1001) {
t.Error("Should successfully remove achievement")
}
if masterList.Size() != 2 {
t.Errorf("Expected size 2 after removal, got %d", masterList.Size())
}
// 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 achievementsPerWorker = 100
var wg sync.WaitGroup
// Concurrently add achievements
wg.Add(numWorkers)
for i := 0; i < numWorkers; i++ {
go func(workerID int) {
defer wg.Done()
for j := 0; j < achievementsPerWorker; j++ {
achievement := New(db)
achievement.AchievementID = uint32(workerID*achievementsPerWorker + j + 1)
achievement.Title = "Concurrent Test"
achievement.Category = "Concurrency"
achievement.Expansion = "Test"
masterList.AddAchievement(achievement)
}
}(i)
}
// Concurrently read achievements
wg.Add(numWorkers)
for i := 0; i < numWorkers; i++ {
go func() {
defer wg.Done()
for j := 0; j < achievementsPerWorker; j++ {
// Random reads
_ = masterList.GetAchievement(uint32(j + 1))
_ = masterList.GetAchievementsByCategory("Concurrency")
_ = masterList.GetAchievementsByExpansion("Test")
_ = masterList.Size()
}
}()
}
wg.Wait()
// Verify final state
expectedSize := numWorkers * achievementsPerWorker
if masterList.Size() != expectedSize {
t.Errorf("Expected size %d, got %d", expectedSize, masterList.Size())
}
categories := masterList.GetCategories()
if len(categories) != 1 || categories[0] != "Concurrency" {
t.Errorf("Expected 1 category 'Concurrency', got %v", categories)
}
}

View File

@ -0,0 +1,364 @@
package achievements
import (
"fmt"
"math/rand"
"sync"
"testing"
"eq2emu/internal/database"
)
// Global shared master list for benchmarks to avoid repeated setup
var (
sharedAchievementMasterList *MasterList
sharedAchievements []*Achievement
achievementSetupOnce sync.Once
)
// setupSharedAchievementMasterList creates the shared master list once
func setupSharedAchievementMasterList(b *testing.B) {
achievementSetupOnce.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)
}
sharedAchievementMasterList = NewMasterList()
// Pre-populate with achievements for realistic testing
const numAchievements = 1000
sharedAchievements = make([]*Achievement, numAchievements)
categories := []string{"Combat", "Crafting", "Exploration", "Social", "PvP", "Quests", "Collections", "Dungeons"}
expansions := []string{"Classic", "Kingdom of Sky", "Echoes of Faydwer", "Rise of Kunark", "The Shadow Odyssey", "Sentinel's Fate"}
for i := range numAchievements {
sharedAchievements[i] = New(db)
sharedAchievements[i].AchievementID = uint32(i + 1)
sharedAchievements[i].Title = fmt.Sprintf("Achievement %d", i+1)
sharedAchievements[i].Category = categories[i%len(categories)]
sharedAchievements[i].Expansion = expansions[i%len(expansions)]
sharedAchievements[i].PointValue = uint32(rand.Intn(50) + 10)
sharedAchievements[i].QtyRequired = uint32(rand.Intn(100) + 1)
// Add some requirements and rewards
sharedAchievements[i].AddRequirement(fmt.Sprintf("task_%d", i%10), uint32(rand.Intn(10)+1))
sharedAchievements[i].AddReward(fmt.Sprintf("reward_%d", i%5))
sharedAchievementMasterList.AddAchievement(sharedAchievements[i])
}
})
}
// createTestAchievement creates an achievement for benchmarking
func createTestAchievement(b *testing.B, id uint32) *Achievement {
b.Helper()
// Use nil database for benchmarking in-memory operations
achievement := New(nil)
achievement.AchievementID = id
achievement.Title = fmt.Sprintf("Benchmark Achievement %d", id)
achievement.Category = []string{"Combat", "Crafting", "Exploration", "Social"}[id%4]
achievement.Expansion = []string{"Classic", "Expansion1", "Expansion2"}[id%3]
achievement.PointValue = uint32(rand.Intn(50) + 10)
achievement.QtyRequired = uint32(rand.Intn(100) + 1)
// Add mock requirements and rewards
achievement.AddRequirement(fmt.Sprintf("task_%d", id%10), uint32(rand.Intn(10)+1))
achievement.AddReward(fmt.Sprintf("reward_%d", id%5))
return achievement
}
// BenchmarkAchievementCreation measures achievement creation performance
func BenchmarkAchievementCreation(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++ {
achievement := New(db)
achievement.AchievementID = uint32(i)
achievement.Title = fmt.Sprintf("Achievement %d", i)
_ = achievement
}
})
b.Run("Parallel", func(b *testing.B) {
b.RunParallel(func(pb *testing.PB) {
id := uint32(0)
for pb.Next() {
achievement := New(db)
achievement.AchievementID = id
achievement.Title = fmt.Sprintf("Achievement %d", id)
id++
_ = achievement
}
})
})
}
// BenchmarkAchievementOperations measures individual achievement operations
func BenchmarkAchievementOperations(b *testing.B) {
achievement := createTestAchievement(b, 1001)
b.Run("GetID", func(b *testing.B) {
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
_ = achievement.GetID()
}
})
})
b.Run("IsNew", func(b *testing.B) {
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
_ = achievement.IsNew()
}
})
})
b.Run("Clone", func(b *testing.B) {
for b.Loop() {
_ = achievement.Clone()
}
})
}
// BenchmarkMasterListOperations measures master list performance
func BenchmarkMasterListOperations(b *testing.B) {
setupSharedAchievementMasterList(b)
ml := sharedAchievementMasterList
b.Run("GetAchievement", func(b *testing.B) {
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
id := uint32(rand.Intn(1000) + 1)
_ = ml.GetAchievement(id)
}
})
})
b.Run("AddAchievement", func(b *testing.B) {
// Create a separate master list for add operations
addML := NewMasterList()
startID := uint32(10000)
// Pre-create achievements to measure just the Add operation
achievementsToAdd := make([]*Achievement, b.N)
for i := 0; i < b.N; i++ {
achievementsToAdd[i] = createTestAchievement(b, startID+uint32(i))
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
addML.AddAchievement(achievementsToAdd[i])
}
})
b.Run("GetAchievementsByCategory", func(b *testing.B) {
categories := []string{"Combat", "Crafting", "Exploration", "Social", "PvP", "Quests", "Collections", "Dungeons"}
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
category := categories[rand.Intn(len(categories))]
_ = ml.GetAchievementsByCategory(category)
}
})
})
b.Run("GetAchievementsByExpansion", func(b *testing.B) {
expansions := []string{"Classic", "Kingdom of Sky", "Echoes of Faydwer", "Rise of Kunark", "The Shadow Odyssey", "Sentinel's Fate"}
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
expansion := expansions[rand.Intn(len(expansions))]
_ = ml.GetAchievementsByExpansion(expansion)
}
})
})
b.Run("GetAchievementsByCategoryAndExpansion", func(b *testing.B) {
categories := []string{"Combat", "Crafting", "Exploration", "Social"}
expansions := []string{"Classic", "Kingdom of Sky", "Echoes of Faydwer"}
for b.Loop() {
category := categories[rand.Intn(len(categories))]
expansion := expansions[rand.Intn(len(expansions))]
_ = ml.GetAchievementsByCategoryAndExpansion(category, expansion)
}
})
b.Run("GetCategories", func(b *testing.B) {
for b.Loop() {
_ = ml.GetCategories()
}
})
b.Run("GetExpansions", func(b *testing.B) {
for b.Loop() {
_ = ml.GetExpansions()
}
})
b.Run("Size", func(b *testing.B) {
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
_ = ml.Size()
}
})
})
}
// BenchmarkConcurrentOperations tests mixed workload performance
func BenchmarkConcurrentOperations(b *testing.B) {
setupSharedAchievementMasterList(b)
ml := sharedAchievementMasterList
b.Run("MixedOperations", func(b *testing.B) {
categories := []string{"Combat", "Crafting", "Exploration", "Social", "PvP", "Quests", "Collections", "Dungeons"}
expansions := []string{"Classic", "Kingdom of Sky", "Echoes of Faydwer", "Rise of Kunark", "The Shadow Odyssey", "Sentinel's Fate"}
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
switch rand.Intn(7) {
case 0:
id := uint32(rand.Intn(1000) + 1)
_ = ml.GetAchievement(id)
case 1:
category := categories[rand.Intn(len(categories))]
_ = ml.GetAchievementsByCategory(category)
case 2:
expansion := expansions[rand.Intn(len(expansions))]
_ = ml.GetAchievementsByExpansion(expansion)
case 3:
category := categories[rand.Intn(len(categories))]
expansion := expansions[rand.Intn(len(expansions))]
_ = ml.GetAchievementsByCategoryAndExpansion(category, expansion)
case 4:
_ = ml.GetCategories()
case 5:
_ = ml.GetExpansions()
case 6:
_ = 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("AchievementAllocation", func(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
achievement := New(db)
achievement.AchievementID = uint32(i)
achievement.Requirements = make([]Requirement, 2)
achievement.Rewards = make([]Reward, 3)
_ = achievement
}
})
b.Run("MasterListAllocation", func(b *testing.B) {
b.ReportAllocs()
for b.Loop() {
ml := NewMasterList()
_ = ml
}
})
b.Run("AddAchievement_Allocations", func(b *testing.B) {
b.ReportAllocs()
ml := NewMasterList()
for i := 0; i < b.N; i++ {
achievement := createTestAchievement(b, uint32(i+1))
ml.AddAchievement(achievement)
}
})
b.Run("GetAchievementsByCategory_Allocations", func(b *testing.B) {
setupSharedAchievementMasterList(b)
ml := sharedAchievementMasterList
b.ReportAllocs()
b.ResetTimer()
for b.Loop() {
_ = ml.GetAchievementsByCategory("Combat")
}
})
b.Run("GetCategories_Allocations", func(b *testing.B) {
setupSharedAchievementMasterList(b)
ml := sharedAchievementMasterList
b.ReportAllocs()
b.ResetTimer()
for b.Loop() {
_ = ml.GetCategories()
}
})
}
// BenchmarkUpdateOperations measures update performance
func BenchmarkUpdateOperations(b *testing.B) {
setupSharedAchievementMasterList(b)
ml := sharedAchievementMasterList
b.Run("UpdateAchievement", func(b *testing.B) {
// Create achievements to update
updateAchievements := make([]*Achievement, b.N)
for i := 0; i < b.N; i++ {
updateAchievements[i] = createTestAchievement(b, uint32((i%1000)+1))
updateAchievements[i].Title = "Updated Title"
updateAchievements[i].Category = "Updated"
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = ml.UpdateAchievement(updateAchievements[i])
}
})
b.Run("RemoveAchievement", func(b *testing.B) {
// Create a separate master list for removal testing
removeML := NewMasterList()
// Add achievements to remove
for i := 0; i < b.N; i++ {
achievement := createTestAchievement(b, uint32(i+1))
removeML.AddAchievement(achievement)
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
removeML.RemoveAchievement(uint32(i + 1))
}
})
}
// BenchmarkCloneOperations measures cloning performance
func BenchmarkCloneOperations(b *testing.B) {
setupSharedAchievementMasterList(b)
ml := sharedAchievementMasterList
b.Run("GetAchievementClone", func(b *testing.B) {
for b.Loop() {
id := uint32(rand.Intn(1000) + 1)
_ = ml.GetAchievementClone(id)
}
})
b.Run("DirectClone", func(b *testing.B) {
achievement := createTestAchievement(b, 1001)
for b.Loop() {
_ = achievement.Clone()
}
})
}

View File

@ -2,112 +2,329 @@ package achievements
import (
"fmt"
"eq2emu/internal/common"
"sync"
)
// MasterList manages the global list of all achievements
// MasterList is a specialized achievement master list optimized for:
// - Fast ID-based lookups (O(1))
// - Fast category-based lookups (O(1))
// - Fast expansion-based lookups (O(1))
// - Efficient filtering and iteration
type MasterList struct {
*common.MasterList[uint32, *Achievement]
// Core storage
achievements map[uint32]*Achievement // ID -> Achievement
mutex sync.RWMutex
// Category indices for O(1) lookups
byCategory map[string][]*Achievement // Category -> achievements
byExpansion map[string][]*Achievement // Expansion -> achievements
// Cached metadata
categories []string // Unique categories (cached)
expansions []string // Unique expansions (cached)
metaStale bool // Whether metadata cache needs refresh
}
// NewMasterList creates a new master achievement list
// NewMasterList creates a new specialized achievement master list
func NewMasterList() *MasterList {
return &MasterList{
MasterList: common.NewMasterList[uint32, *Achievement](),
achievements: make(map[uint32]*Achievement),
byCategory: make(map[string][]*Achievement),
byExpansion: make(map[string][]*Achievement),
metaStale: true,
}
}
// AddAchievement adds an achievement to the master list
// Returns false if achievement with same ID already exists
// refreshMetaCache updates the categories and expansions cache
func (m *MasterList) refreshMetaCache() {
if !m.metaStale {
return
}
categorySet := make(map[string]struct{})
expansionSet := make(map[string]struct{})
// Collect unique categories and expansions
for _, achievement := range m.achievements {
if achievement.Category != "" {
categorySet[achievement.Category] = struct{}{}
}
if achievement.Expansion != "" {
expansionSet[achievement.Expansion] = struct{}{}
}
}
// Clear existing caches and rebuild
m.categories = m.categories[:0]
for category := range categorySet {
m.categories = append(m.categories, category)
}
m.expansions = m.expansions[:0]
for expansion := range expansionSet {
m.expansions = append(m.expansions, expansion)
}
m.metaStale = false
}
// AddAchievement adds an achievement with full indexing
func (m *MasterList) AddAchievement(achievement *Achievement) bool {
if achievement == nil {
return false
}
return m.MasterList.Add(achievement)
m.mutex.Lock()
defer m.mutex.Unlock()
// Check if exists
if _, exists := m.achievements[achievement.AchievementID]; exists {
return false
}
// Add to core storage
m.achievements[achievement.AchievementID] = achievement
// Update category index
if achievement.Category != "" {
m.byCategory[achievement.Category] = append(m.byCategory[achievement.Category], achievement)
}
// Update expansion index
if achievement.Expansion != "" {
m.byExpansion[achievement.Expansion] = append(m.byExpansion[achievement.Expansion], achievement)
}
// Invalidate metadata cache
m.metaStale = true
return true
}
// GetAchievement retrieves an achievement by ID
// Returns nil if not found
// GetAchievement retrieves by ID (O(1))
func (m *MasterList) GetAchievement(id uint32) *Achievement {
return m.MasterList.Get(id)
m.mutex.RLock()
defer m.mutex.RUnlock()
return m.achievements[id]
}
// GetAchievementClone retrieves a cloned copy of an achievement by ID
// Returns nil if not found. Safe for modification without affecting master list
func (m *MasterList) GetAchievementClone(id uint32) *Achievement {
achievement := m.MasterList.Get(id)
m.mutex.RLock()
defer m.mutex.RUnlock()
achievement := m.achievements[id]
if achievement == nil {
return nil
}
return achievement.Clone()
}
// GetAllAchievements returns a map of all achievements (read-only access)
// The returned map should not be modified
// GetAllAchievements returns a copy of all achievements map
func (m *MasterList) GetAllAchievements() map[uint32]*Achievement {
return m.MasterList.GetAll()
m.mutex.RLock()
defer m.mutex.RUnlock()
// Return a copy to prevent external modification
result := make(map[uint32]*Achievement, len(m.achievements))
for id, achievement := range m.achievements {
result[id] = achievement
}
return result
}
// GetAchievementsByCategory returns achievements filtered by category
// GetAchievementsByCategory returns all achievements in a category (O(1))
func (m *MasterList) GetAchievementsByCategory(category string) []*Achievement {
return m.MasterList.Filter(func(achievement *Achievement) bool {
return achievement.Category == category
})
m.mutex.RLock()
defer m.mutex.RUnlock()
return m.byCategory[category]
}
// GetAchievementsByExpansion returns achievements filtered by expansion
// GetAchievementsByExpansion returns all achievements in an expansion (O(1))
func (m *MasterList) GetAchievementsByExpansion(expansion string) []*Achievement {
return m.MasterList.Filter(func(achievement *Achievement) bool {
return achievement.Expansion == expansion
})
m.mutex.RLock()
defer m.mutex.RUnlock()
return m.byExpansion[expansion]
}
// Removes an achievement from the master list
// Returns true if achievement was found and removed
// GetAchievementsByCategoryAndExpansion returns achievements matching both category and expansion
func (m *MasterList) GetAchievementsByCategoryAndExpansion(category, expansion string) []*Achievement {
m.mutex.RLock()
defer m.mutex.RUnlock()
categoryAchievements := m.byCategory[category]
expansionAchievements := m.byExpansion[expansion]
// Use smaller set for iteration efficiency
if len(categoryAchievements) > len(expansionAchievements) {
categoryAchievements, expansionAchievements = expansionAchievements, categoryAchievements
}
// Set intersection using map lookup
expansionSet := make(map[*Achievement]struct{}, len(expansionAchievements))
for _, achievement := range expansionAchievements {
expansionSet[achievement] = struct{}{}
}
var result []*Achievement
for _, achievement := range categoryAchievements {
if _, exists := expansionSet[achievement]; exists {
result = append(result, achievement)
}
}
return result
}
// GetCategories returns all unique categories using cached results
func (m *MasterList) GetCategories() []string {
m.mutex.Lock() // Need write lock to potentially update cache
defer m.mutex.Unlock()
m.refreshMetaCache()
// Return a copy to prevent external modification
result := make([]string, len(m.categories))
copy(result, m.categories)
return result
}
// GetExpansions returns all unique expansions using cached results
func (m *MasterList) GetExpansions() []string {
m.mutex.Lock() // Need write lock to potentially update cache
defer m.mutex.Unlock()
m.refreshMetaCache()
// Return a copy to prevent external modification
result := make([]string, len(m.expansions))
copy(result, m.expansions)
return result
}
// RemoveAchievement removes an achievement and updates all indices
func (m *MasterList) RemoveAchievement(id uint32) bool {
return m.MasterList.Remove(id)
m.mutex.Lock()
defer m.mutex.Unlock()
achievement, exists := m.achievements[id]
if !exists {
return false
}
// Remove from core storage
delete(m.achievements, id)
// Remove from category index
if achievement.Category != "" {
categoryAchievements := m.byCategory[achievement.Category]
for i, a := range categoryAchievements {
if a.AchievementID == id {
m.byCategory[achievement.Category] = append(categoryAchievements[:i], categoryAchievements[i+1:]...)
break
}
}
}
// Remove from expansion index
if achievement.Expansion != "" {
expansionAchievements := m.byExpansion[achievement.Expansion]
for i, a := range expansionAchievements {
if a.AchievementID == id {
m.byExpansion[achievement.Expansion] = append(expansionAchievements[:i], expansionAchievements[i+1:]...)
break
}
}
}
// Invalidate metadata cache
m.metaStale = true
return true
}
// UpdateAchievement updates an existing achievement
// Returns error if achievement doesn't exist
func (m *MasterList) UpdateAchievement(achievement *Achievement) error {
if achievement == nil {
return fmt.Errorf("achievement cannot be nil")
}
return m.MasterList.Update(achievement)
}
// Returns all unique categories
func (m *MasterList) GetCategories() []string {
categories := make(map[string]bool)
m.mutex.Lock()
defer m.mutex.Unlock()
m.MasterList.ForEach(func(_ uint32, achievement *Achievement) {
if achievement.Category != "" {
categories[achievement.Category] = true
}
})
result := make([]string, 0, len(categories))
for category := range categories {
result = append(result, category)
// Check if exists
old, exists := m.achievements[achievement.AchievementID]
if !exists {
return fmt.Errorf("achievement %d not found", achievement.AchievementID)
}
return result
}
// Returns all unique expansions
func (m *MasterList) GetExpansions() []string {
expansions := make(map[string]bool)
m.MasterList.ForEach(func(_ uint32, achievement *Achievement) {
if achievement.Expansion != "" {
expansions[achievement.Expansion] = true
// Remove old achievement from indices (but not core storage yet)
if old.Category != "" {
categoryAchievements := m.byCategory[old.Category]
for i, a := range categoryAchievements {
if a.AchievementID == achievement.AchievementID {
m.byCategory[old.Category] = append(categoryAchievements[:i], categoryAchievements[i+1:]...)
break
}
}
})
result := make([]string, 0, len(expansions))
for expansion := range expansions {
result = append(result, expansion)
}
return result
if old.Expansion != "" {
expansionAchievements := m.byExpansion[old.Expansion]
for i, a := range expansionAchievements {
if a.AchievementID == achievement.AchievementID {
m.byExpansion[old.Expansion] = append(expansionAchievements[:i], expansionAchievements[i+1:]...)
break
}
}
}
// Update core storage
m.achievements[achievement.AchievementID] = achievement
// Add new achievement to indices
if achievement.Category != "" {
m.byCategory[achievement.Category] = append(m.byCategory[achievement.Category], achievement)
}
if achievement.Expansion != "" {
m.byExpansion[achievement.Expansion] = append(m.byExpansion[achievement.Expansion], achievement)
}
// Invalidate metadata cache
m.metaStale = true
return nil
}
// Size returns the total number of achievements
func (m *MasterList) Size() int {
m.mutex.RLock()
defer m.mutex.RUnlock()
return len(m.achievements)
}
// Clear removes all achievements from the master list
func (m *MasterList) Clear() {
m.mutex.Lock()
defer m.mutex.Unlock()
// Clear all maps
m.achievements = make(map[uint32]*Achievement)
m.byCategory = make(map[string][]*Achievement)
m.byExpansion = make(map[string][]*Achievement)
// Clear cached metadata
m.categories = m.categories[:0]
m.expansions = m.expansions[:0]
m.metaStale = true
}
// ForEach executes a function for each achievement
func (m *MasterList) ForEach(fn func(uint32, *Achievement)) {
m.mutex.RLock()
defer m.mutex.RUnlock()
for id, achievement := range m.achievements {
fn(id, achievement)
}
}

View File

@ -1,229 +0,0 @@
# Common Package
The common package provides shared utilities and patterns used across multiple EQ2Go game systems.
## Generic Master List
### Overview
The generic `MasterList[K, V]` type provides a thread-safe, reusable collection management pattern that eliminates code duplication across the EQ2Go codebase. It implements the master list pattern used by 15+ game systems including achievements, items, spells, factions, skills, etc.
### Key Features
- **Generic Type Safety**: Full compile-time type checking with `MasterList[KeyType, ValueType]`
- **Thread Safety**: All operations use `sync.RWMutex` for concurrent access
- **Consistent API**: Standardized CRUD operations across all master lists
- **Performance Optimized**: Efficient filtering, searching, and bulk operations
- **Extension Support**: Compose with specialized interfaces for domain-specific features
### Basic Usage
```go
// Any type implementing Identifiable can be stored
type Achievement struct {
ID uint32 `json:"id"`
Title string `json:"title"`
// ... other fields
}
func (a *Achievement) GetID() uint32 {
return a.ID
}
// Create a master list
masterList := common.NewMasterList[uint32, *Achievement]()
// Add items
achievement := &Achievement{ID: 1, Title: "Dragon Slayer"}
added := masterList.Add(achievement)
// Retrieve items
retrieved := masterList.Get(1)
item, exists := masterList.GetSafe(1)
// Check existence
if masterList.Exists(1) {
// Item exists
}
// Update items
achievement.Title = "Master Dragon Slayer"
masterList.Update(achievement) // Returns error if not found
masterList.AddOrUpdate(achievement) // Always succeeds
// Remove items
removed := masterList.Remove(1)
// Bulk operations
allItems := masterList.GetAll() // Map copy
allSlice := masterList.GetAllSlice() // Slice copy
allIDs := masterList.GetAllIDs() // ID slice
// Query operations
filtered := masterList.Filter(func(a *Achievement) bool {
return strings.Contains(a.Title, "Dragon")
})
found, exists := masterList.Find(func(a *Achievement) bool {
return a.Title == "Dragon Slayer"
})
count := masterList.Count(func(a *Achievement) bool {
return strings.HasPrefix(a.Title, "Master")
})
// Iteration
masterList.ForEach(func(id uint32, achievement *Achievement) {
fmt.Printf("Achievement %d: %s\n", id, achievement.Title)
})
```
### Migration from Existing Master Lists
#### Before (Manual Implementation)
```go
type MasterList struct {
achievements map[uint32]*Achievement
mutex sync.RWMutex
}
func (m *MasterList) AddAchievement(achievement *Achievement) bool {
m.mutex.Lock()
defer m.mutex.Unlock()
if _, exists := m.achievements[achievement.ID]; exists {
return false
}
m.achievements[achievement.ID] = achievement
return true
}
func (m *MasterList) GetAchievement(id uint32) *Achievement {
m.mutex.RLock()
defer m.mutex.RUnlock()
return m.achievements[id]
}
// ... 15+ more methods with manual mutex handling
```
#### After (Generic Implementation)
```go
type MasterList struct {
*common.MasterList[uint32, *Achievement]
}
func NewMasterList() *MasterList {
return &MasterList{
MasterList: common.NewMasterList[uint32, *Achievement](),
}
}
func (m *MasterList) AddAchievement(achievement *Achievement) bool {
if achievement == nil {
return false
}
return m.MasterList.Add(achievement)
}
func (m *MasterList) GetAchievement(id uint32) *Achievement {
return m.MasterList.Get(id)
}
// Domain-specific extensions
func (m *MasterList) GetAchievementsByCategory(category string) []*Achievement {
return m.MasterList.Filter(func(achievement *Achievement) bool {
return achievement.Category == category
})
}
```
### Benefits
1. **Code Reduction**: 80%+ reduction in boilerplate code per master list
2. **Consistency**: Identical behavior across all master lists
3. **Thread Safety**: Guaranteed concurrent access safety
4. **Performance**: Optimized operations with minimal overhead
5. **Type Safety**: Compile-time guarantees prevent runtime errors
6. **Extensibility**: Easy to add domain-specific functionality
7. **Testing**: Single well-tested implementation vs 15+ custom implementations
8. **Maintenance**: Changes benefit all master lists simultaneously
### Advanced Features
#### Thread-Safe Batch Operations
```go
// Complex read operation
masterList.WithReadLock(func(items map[uint32]*Achievement) {
// Direct access to internal map while holding read lock
for id, achievement := range items {
// Complex processing...
}
})
// Complex write operation
masterList.WithWriteLock(func(items map[uint32]*Achievement) {
// Direct access to internal map while holding write lock
// Atomic multi-item modifications
})
```
#### Specialized Interface Implementations
The package provides optional interfaces for advanced functionality:
- **`DatabaseIntegrated`**: Load/save from database
- **`Validatable`**: Item validation and integrity checks
- **`Searchable`**: Advanced search capabilities
- **`Cacheable`**: Cache management
- **`Statistician`**: Usage statistics tracking
- **`Indexable`**: Multiple index support
- **`Categorizable`**: Category-based organization
- **`Versioned`**: Version compatibility filtering
- **`Relationship`**: Entity relationship management
- **`Hierarchical`**: Tree structure support
- **`Observable`**: Event notifications
### Migration Steps
1. **Add Identifiable Interface**: Ensure your type implements `GetID() KeyType`
2. **Embed Generic MasterList**: Replace custom struct with embedded generic
3. **Update Constructor**: Use `common.NewMasterList[K, V]()`
4. **Replace Manual Methods**: Use generic methods or create thin wrappers
5. **Update Domain Methods**: Convert filters to use `Filter()`, `Find()`, etc.
6. **Test**: Existing API should work unchanged with thin wrapper methods
### Performance Comparison
Based on benchmarks with 10,000 items:
| Operation | Before (Manual) | After (Generic) | Improvement |
|-----------|-----------------|------------------|-------------|
| Get | 15ns | 15ns | Same |
| Add | 45ns | 45ns | Same |
| Filter | 125μs | 120μs | 4% faster |
| Memory | Various | Consistent | Predictable |
The generic implementation maintains identical performance while providing consistency and type safety.
### Compatibility
The generic master list is fully backward compatible when used with thin wrapper methods. Existing code continues to work without modification while gaining:
- Thread safety guarantees
- Performance optimizations
- Consistent behavior
- Type safety
- Reduced maintenance burden
### Future Enhancements
Planned additions to the generic master list:
- **Persistence**: Automatic database synchronization
- **Metrics**: Built-in performance monitoring
- **Events**: Change notification system
- **Indexing**: Automatic secondary index management
- **Validation**: Built-in data integrity checks
- **Sharding**: Horizontal scaling support

View File

@ -1,210 +0,0 @@
package common
import (
"fmt"
"math/rand"
"testing"
)
// testItem implements Identifiable for benchmarking
type testItem struct {
id int32
name string
value int32
flag bool
}
func (t *testItem) GetID() int32 { return t.id }
// BenchmarkMasterListOperations benchmarks the generic MasterList
func BenchmarkMasterListOperations(b *testing.B) {
// Create master list with test data
ml := NewMasterList[int32, *testItem]()
const numItems = 10000
// Pre-populate
b.StopTimer()
for i := 0; i < numItems; i++ {
item := &testItem{
id: int32(i + 1),
name: fmt.Sprintf("Item %d", i+1),
value: int32(rand.Intn(100)),
flag: rand.Intn(2) == 1,
}
ml.Add(item)
}
b.StartTimer()
b.Run("Get", func(b *testing.B) {
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
id := int32(rand.Intn(numItems) + 1)
_ = ml.Get(id)
}
})
})
b.Run("Filter_10Percent", func(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = ml.Filter(func(item *testItem) bool {
return item.value < 10 // ~10% match
})
}
})
b.Run("Filter_50Percent", func(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = ml.Filter(func(item *testItem) bool {
return item.value < 50 // ~50% match
})
}
})
b.Run("Filter_90Percent", func(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = ml.Filter(func(item *testItem) bool {
return item.value < 90 // ~90% match
})
}
})
b.Run("Count_10Percent", func(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = ml.Count(func(item *testItem) bool {
return item.value < 10
})
}
})
b.Run("Count_50Percent", func(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = ml.Count(func(item *testItem) bool {
return item.value < 50
})
}
})
b.Run("Find", func(b *testing.B) {
for i := 0; i < b.N; i++ {
targetValue := int32(rand.Intn(100))
_, _ = ml.Find(func(item *testItem) bool {
return item.value == targetValue
})
}
})
b.Run("ForEach", func(b *testing.B) {
for i := 0; i < b.N; i++ {
ml.ForEach(func(id int32, item *testItem) {
_ = item.value + 1 // Minimal work
})
}
})
b.Run("WithReadLock", func(b *testing.B) {
for i := 0; i < b.N; i++ {
ml.WithReadLock(func(items map[int32]*testItem) {
count := 0
for _, item := range items {
if item.value < 50 {
count++
}
}
_ = count
})
}
})
b.Run("FilterWithCapacity_Accurate", func(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = ml.FilterWithCapacity(func(item *testItem) bool {
return item.value < 50
}, 5000) // Accurate estimate: 50% of 10k = 5k
}
})
b.Run("FilterInto_Reuse", func(b *testing.B) {
var reusableSlice []*testItem
for i := 0; i < b.N; i++ {
reusableSlice = ml.FilterInto(func(item *testItem) bool {
return item.value < 50
}, reusableSlice)
}
})
b.Run("CountAndFilter_Combined", func(b *testing.B) {
for i := 0; i < b.N; i++ {
_, _ = ml.CountAndFilter(func(item *testItem) bool {
return item.value < 50
})
}
})
}
// BenchmarkMemoryAllocations tests allocation patterns
func BenchmarkMemoryAllocations(b *testing.B) {
ml := NewMasterList[int32, *testItem]()
const numItems = 1000
// Pre-populate
for i := 0; i < numItems; i++ {
item := &testItem{
id: int32(i + 1),
name: fmt.Sprintf("Item %d", i+1),
value: int32(rand.Intn(100)),
flag: rand.Intn(2) == 1,
}
ml.Add(item)
}
b.Run("Filter_Allocations", func(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
_ = ml.Filter(func(item *testItem) bool {
return item.value < 50
})
}
})
b.Run("GetAll_Allocations", func(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
_ = ml.GetAll()
}
})
b.Run("GetAllSlice_Allocations", func(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
_ = ml.GetAllSlice()
}
})
b.Run("FilterWithCapacity_Allocations", func(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
_ = ml.FilterWithCapacity(func(item *testItem) bool {
return item.value < 50
}, 500) // Accurate capacity estimate
}
})
b.Run("FilterInto_Allocations", func(b *testing.B) {
b.ReportAllocs()
reusableSlice := make([]*testItem, 0, 600) // Pre-sized
for i := 0; i < b.N; i++ {
reusableSlice = ml.FilterInto(func(item *testItem) bool {
return item.value < 50
}, reusableSlice)
}
})
b.Run("CountAndFilter_Allocations", func(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
_, _ = ml.CountAndFilter(func(item *testItem) bool {
return item.value < 50
})
}
})
}

View File

@ -1,205 +0,0 @@
package common
import (
"context"
"eq2emu/internal/database"
)
// DatabaseIntegrated defines the interface for master lists that can load from database
type DatabaseIntegrated interface {
// LoadFromDatabase loads all items from the database
LoadFromDatabase(db *database.Database) error
// SaveToDatabase saves all items to the database (if supported)
SaveToDatabase(db *database.Database) error
}
// ContextAware defines the interface for master lists that need context for initialization
type ContextAware interface {
// Initialize performs setup operations that may require external dependencies
Initialize(ctx context.Context) error
}
// Validatable defines the interface for master lists that support validation
type Validatable interface {
// Validate checks the integrity of all items in the list
Validate() []error
// ValidateItem checks the integrity of a specific item
ValidateItem(item interface{}) error
}
// Searchable defines the interface for master lists that support advanced search
type Searchable[V any] interface {
// Search finds items matching the given criteria
Search(criteria SearchCriteria) []V
// SearchByName finds items by name (case-insensitive)
SearchByName(name string) []V
}
// SearchCriteria defines search parameters for advanced search operations
type SearchCriteria struct {
Name string // Name-based search (case-insensitive)
Category string // Category-based search
Filters map[string]interface{} // Custom filters
Limit int // Maximum results to return (0 = no limit)
}
// Cacheable defines the interface for master lists that support caching
type Cacheable interface {
// ClearCache clears any cached data
ClearCache()
// RefreshCache rebuilds cached data
RefreshCache() error
// IsCacheValid returns true if cache is valid
IsCacheValid() bool
}
// Statistician defines the interface for master lists that track statistics
type Statistician interface {
// GetStatistics returns usage statistics for the list
GetStatistics() Statistics
// ResetStatistics resets all tracked statistics
ResetStatistics()
}
// Statistics represents usage statistics for a master list
type Statistics struct {
TotalItems int `json:"total_items"`
AccessCount int64 `json:"access_count"`
HitRate float64 `json:"hit_rate"`
MissCount int64 `json:"miss_count"`
LastAccessed int64 `json:"last_accessed"`
MemoryUsage int64 `json:"memory_usage"`
}
// Indexable defines the interface for master lists that support multiple indexes
type Indexable[K comparable, V any] interface {
// GetByIndex retrieves items using an alternate index
GetByIndex(indexName string, key interface{}) []V
// GetIndexes returns the names of all available indexes
GetIndexes() []string
// RebuildIndex rebuilds a specific index
RebuildIndex(indexName string) error
// RebuildAllIndexes rebuilds all indexes
RebuildAllIndexes() error
}
// Categorizable defines the interface for master lists that support categorization
type Categorizable[V any] interface {
// GetByCategory returns all items in a specific category
GetByCategory(category string) []V
// GetCategories returns all available categories
GetCategories() []string
// GetCategoryCount returns the number of items in a category
GetCategoryCount(category string) int
}
// Versioned defines the interface for master lists that support version filtering
type Versioned[V any] interface {
// GetByVersion returns items compatible with a specific version
GetByVersion(version uint32) []V
// GetByVersionRange returns items compatible within a version range
GetByVersionRange(minVersion, maxVersion uint32) []V
}
// Relationship defines the interface for master lists that manage entity relationships
type Relationship[K comparable, V any] interface {
// GetRelated returns items related to the given item
GetRelated(id K, relationshipType string) []V
// AddRelationship adds a relationship between two items
AddRelationship(fromID K, toID K, relationshipType string) error
// RemoveRelationship removes a relationship between two items
RemoveRelationship(fromID K, toID K, relationshipType string) error
// GetRelationshipTypes returns all supported relationship types
GetRelationshipTypes() []string
}
// Grouped defines the interface for master lists that support grouping
type Grouped[K comparable, V any] interface {
// GetByGroup returns all items in a specific group
GetByGroup(groupID K) []V
// GetGroups returns all available groups
GetGroups() []K
// GetGroupSize returns the number of items in a group
GetGroupSize(groupID K) int
}
// Hierarchical defines the interface for master lists that support tree structures
type Hierarchical[K comparable, V any] interface {
// GetChildren returns direct children of an item
GetChildren(parentID K) []V
// GetDescendants returns all descendants of an item
GetDescendants(parentID K) []V
// GetParent returns the parent of an item
GetParent(childID K) (V, bool)
// GetRoot returns the root item(s)
GetRoot() []V
// IsAncestor checks if one item is an ancestor of another
IsAncestor(ancestorID K, descendantID K) bool
}
// Observable defines the interface for master lists that support event notifications
type Observable[K comparable, V any] interface {
// Subscribe adds a listener for list events
Subscribe(listener EventListener[K, V])
// Unsubscribe removes a listener
Unsubscribe(listener EventListener[K, V])
// NotifyEvent sends an event to all listeners
NotifyEvent(event Event[K, V])
}
// EventListener receives notifications about list changes
type EventListener[K comparable, V any] interface {
// OnItemAdded is called when an item is added
OnItemAdded(id K, item V)
// OnItemRemoved is called when an item is removed
OnItemRemoved(id K, item V)
// OnItemUpdated is called when an item is updated
OnItemUpdated(id K, oldItem V, newItem V)
// OnListCleared is called when the list is cleared
OnListCleared()
}
// Event represents a change event in a master list
type Event[K comparable, V any] struct {
Type EventType `json:"type"`
ItemID K `json:"item_id"`
Item V `json:"item,omitempty"`
OldItem V `json:"old_item,omitempty"`
}
// EventType represents the type of event that occurred
type EventType int
const (
EventItemAdded EventType = iota
EventItemRemoved
EventItemUpdated
EventListCleared
)

View File

@ -1,311 +0,0 @@
// Package common provides shared utilities and patterns used across multiple game systems.
//
// The MasterList type provides a generic, thread-safe collection management pattern
// that is used extensively throughout the EQ2Go server implementation for managing
// game entities like items, spells, spawns, achievements, etc.
package common
import (
"fmt"
"maps"
"sync"
)
// Identifiable represents any type that can be identified by a key
type Identifiable[K comparable] interface {
GetID() K
}
// MasterList provides a generic, thread-safe collection for managing game entities.
// It implements the common pattern used across all EQ2Go master lists with consistent
// CRUD operations, bulk operations, and thread safety.
//
// K is the key type (typically int32, uint32, or string)
// V is the value type (must implement Identifiable[K])
type MasterList[K comparable, V Identifiable[K]] struct {
items map[K]V
mutex sync.RWMutex
}
// NewMasterList creates a new master list instance
func NewMasterList[K comparable, V Identifiable[K]]() *MasterList[K, V] {
return &MasterList[K, V]{
items: make(map[K]V),
}
}
// Add adds an item to the master list. Returns true if added, false if it already exists.
// Thread-safe for concurrent access.
func (ml *MasterList[K, V]) Add(item V) bool {
ml.mutex.Lock()
defer ml.mutex.Unlock()
id := item.GetID()
if _, exists := ml.items[id]; exists {
return false
}
ml.items[id] = item
return true
}
// AddOrUpdate adds an item to the master list or updates it if it already exists.
// Always returns true. Thread-safe for concurrent access.
func (ml *MasterList[K, V]) AddOrUpdate(item V) bool {
ml.mutex.Lock()
defer ml.mutex.Unlock()
id := item.GetID()
ml.items[id] = item
return true
}
// Get retrieves an item by its ID. Returns the zero value of V if not found.
// Thread-safe for concurrent access.
func (ml *MasterList[K, V]) Get(id K) V {
ml.mutex.RLock()
defer ml.mutex.RUnlock()
return ml.items[id]
}
// GetSafe retrieves an item by its ID with existence check.
// Returns the item and true if found, zero value and false if not found.
// Thread-safe for concurrent access.
func (ml *MasterList[K, V]) GetSafe(id K) (V, bool) {
ml.mutex.RLock()
defer ml.mutex.RUnlock()
item, exists := ml.items[id]
return item, exists
}
// Exists checks if an item with the given ID exists in the list.
// Thread-safe for concurrent access.
func (ml *MasterList[K, V]) Exists(id K) bool {
ml.mutex.RLock()
defer ml.mutex.RUnlock()
_, exists := ml.items[id]
return exists
}
// Remove removes an item by its ID. Returns true if removed, false if not found.
// Thread-safe for concurrent access.
func (ml *MasterList[K, V]) Remove(id K) bool {
ml.mutex.Lock()
defer ml.mutex.Unlock()
if _, exists := ml.items[id]; !exists {
return false
}
delete(ml.items, id)
return true
}
// Update updates an existing item. Returns error if the item doesn't exist.
// Thread-safe for concurrent access.
func (ml *MasterList[K, V]) Update(item V) error {
ml.mutex.Lock()
defer ml.mutex.Unlock()
id := item.GetID()
if _, exists := ml.items[id]; !exists {
return fmt.Errorf("item with ID %v not found", id)
}
ml.items[id] = item
return nil
}
// Size returns the number of items in the list.
// Thread-safe for concurrent access.
func (ml *MasterList[K, V]) Size() int {
ml.mutex.RLock()
defer ml.mutex.RUnlock()
return len(ml.items)
}
// IsEmpty returns true if the list contains no items.
// Thread-safe for concurrent access.
func (ml *MasterList[K, V]) IsEmpty() bool {
ml.mutex.RLock()
defer ml.mutex.RUnlock()
return len(ml.items) == 0
}
// Clear removes all items from the list.
// Thread-safe for concurrent access.
func (ml *MasterList[K, V]) Clear() {
ml.mutex.Lock()
defer ml.mutex.Unlock()
// Create new map to ensure memory is freed
ml.items = make(map[K]V)
}
// GetAll returns a copy of all items in the list.
// The returned map is safe to modify without affecting the master list.
// Thread-safe for concurrent access.
func (ml *MasterList[K, V]) GetAll() map[K]V {
ml.mutex.RLock()
defer ml.mutex.RUnlock()
result := make(map[K]V, len(ml.items))
maps.Copy(result, ml.items)
return result
}
// GetAllSlice returns a slice containing all items in the list.
// The returned slice is safe to modify without affecting the master list.
// Thread-safe for concurrent access.
func (ml *MasterList[K, V]) GetAllSlice() []V {
ml.mutex.RLock()
defer ml.mutex.RUnlock()
result := make([]V, 0, len(ml.items))
for _, v := range ml.items {
result = append(result, v)
}
return result
}
// GetAllIDs returns a slice containing all IDs in the list.
// Thread-safe for concurrent access.
func (ml *MasterList[K, V]) GetAllIDs() []K {
ml.mutex.RLock()
defer ml.mutex.RUnlock()
result := make([]K, 0, len(ml.items))
for k := range ml.items {
result = append(result, k)
}
return result
}
// ForEach executes a function for each item in the list.
// The function receives a copy of each item, so modifications won't affect the list.
// Thread-safe for concurrent access.
func (ml *MasterList[K, V]) ForEach(fn func(K, V)) {
ml.mutex.RLock()
defer ml.mutex.RUnlock()
for k, v := range ml.items {
fn(k, v)
}
}
// Filter returns a new slice containing items that match the predicate function.
// Thread-safe for concurrent access.
func (ml *MasterList[K, V]) Filter(predicate func(V) bool) []V {
ml.mutex.RLock()
defer ml.mutex.RUnlock()
// Pre-allocate with estimated capacity to reduce allocations
result := make([]V, 0, len(ml.items)/4) // Assume ~25% match rate
for _, v := range ml.items {
if predicate(v) {
result = append(result, v)
}
}
return result
}
// Find returns the first item that matches the predicate function.
// Returns zero value and false if no match is found.
// Thread-safe for concurrent access.
func (ml *MasterList[K, V]) Find(predicate func(V) bool) (V, bool) {
ml.mutex.RLock()
defer ml.mutex.RUnlock()
for _, v := range ml.items {
if predicate(v) {
return v, true
}
}
var zero V
return zero, false
}
// Count returns the number of items that match the predicate function.
// Thread-safe for concurrent access.
func (ml *MasterList[K, V]) Count(predicate func(V) bool) int {
ml.mutex.RLock()
defer ml.mutex.RUnlock()
count := 0
for _, v := range ml.items {
if predicate(v) {
count++
}
}
return count
}
// WithReadLock executes a function while holding a read lock on the list.
// Use this for complex operations that need consistent read access to multiple items.
func (ml *MasterList[K, V]) WithReadLock(fn func(map[K]V)) {
ml.mutex.RLock()
defer ml.mutex.RUnlock()
fn(ml.items)
}
// WithWriteLock executes a function while holding a write lock on the list.
// Use this for complex operations that need to modify multiple items atomically.
func (ml *MasterList[K, V]) WithWriteLock(fn func(map[K]V)) {
ml.mutex.Lock()
defer ml.mutex.Unlock()
fn(ml.items)
}
// FilterWithCapacity returns items matching predicate with pre-allocated capacity.
// Use when you have a good estimate of result size to optimize allocations.
func (ml *MasterList[K, V]) FilterWithCapacity(predicate func(V) bool, expectedSize int) []V {
ml.mutex.RLock()
defer ml.mutex.RUnlock()
result := make([]V, 0, expectedSize)
for _, v := range ml.items {
if predicate(v) {
result = append(result, v)
}
}
return result
}
// FilterInto appends matching items to the provided slice, avoiding new allocations.
// Returns the updated slice. Use this for repeated filtering to reuse memory.
func (ml *MasterList[K, V]) FilterInto(predicate func(V) bool, result []V) []V {
ml.mutex.RLock()
defer ml.mutex.RUnlock()
// Clear the slice but keep capacity
result = result[:0]
for _, v := range ml.items {
if predicate(v) {
result = append(result, v)
}
}
return result
}
// CountAndFilter performs both count and filter in a single pass.
// More efficient than calling Count() and Filter() separately.
func (ml *MasterList[K, V]) CountAndFilter(predicate func(V) bool) (int, []V) {
ml.mutex.RLock()
defer ml.mutex.RUnlock()
count := 0
result := make([]V, 0, len(ml.items)/4)
for _, v := range ml.items {
if predicate(v) {
count++
result = append(result, v)
}
}
return count, result
}

View File

@ -1,305 +0,0 @@
package common
import (
"fmt"
"testing"
)
// TestItem implements Identifiable for testing
type TestItem struct {
ID int32 `json:"id"`
Name string `json:"name"`
Category string `json:"category"`
}
func (t *TestItem) GetID() int32 {
return t.ID
}
// TestMasterList tests the basic functionality of the generic master list
func TestMasterList(t *testing.T) {
ml := NewMasterList[int32, *TestItem]()
// Test initial state
if !ml.IsEmpty() {
t.Error("New master list should be empty")
}
if ml.Size() != 0 {
t.Error("New master list should have size 0")
}
// Test adding items
item1 := &TestItem{ID: 1, Name: "Item One", Category: "A"}
item2 := &TestItem{ID: 2, Name: "Item Two", Category: "B"}
item3 := &TestItem{ID: 3, Name: "Item Three", Category: "A"}
if !ml.Add(item1) {
t.Error("Should successfully add item1")
}
if !ml.Add(item2) {
t.Error("Should successfully add item2")
}
if !ml.Add(item3) {
t.Error("Should successfully add item3")
}
// Test duplicate addition
if ml.Add(item1) {
t.Error("Should not add duplicate item")
}
// Test size
if ml.Size() != 3 {
t.Errorf("Expected size 3, got %d", ml.Size())
}
if ml.IsEmpty() {
t.Error("List should not be empty")
}
// Test retrieval
retrieved := ml.Get(1)
if retrieved == nil || retrieved.Name != "Item One" {
t.Error("Failed to retrieve item1")
}
// Test safe retrieval
retrievedSafe, exists := ml.GetSafe(1)
if !exists || retrievedSafe.Name != "Item One" {
t.Error("Failed to safely retrieve item1")
}
_, exists = ml.GetSafe(999)
if exists {
t.Error("Should not find non-existent item")
}
// Test existence
if !ml.Exists(1) {
t.Error("Item 1 should exist")
}
if ml.Exists(999) {
t.Error("Item 999 should not exist")
}
// Test update
updatedItem := &TestItem{ID: 1, Name: "Updated Item One", Category: "A"}
if err := ml.Update(updatedItem); err != nil {
t.Errorf("Should successfully update item: %v", err)
}
retrieved = ml.Get(1)
if retrieved.Name != "Updated Item One" {
t.Error("Item was not updated correctly")
}
// Test update non-existent item
nonExistent := &TestItem{ID: 999, Name: "Non Existent", Category: "Z"}
if err := ml.Update(nonExistent); err == nil {
t.Error("Should fail to update non-existent item")
}
// Test AddOrUpdate
newItem := &TestItem{ID: 4, Name: "Item Four", Category: "C"}
if !ml.AddOrUpdate(newItem) {
t.Error("Should successfully add new item with AddOrUpdate")
}
updateExisting := &TestItem{ID: 1, Name: "Double Updated Item One", Category: "A"}
if !ml.AddOrUpdate(updateExisting) {
t.Error("Should successfully update existing item with AddOrUpdate")
}
retrieved = ml.Get(1)
if retrieved.Name != "Double Updated Item One" {
t.Error("Item was not updated correctly with AddOrUpdate")
}
if ml.Size() != 4 {
t.Errorf("Expected size 4 after AddOrUpdate, got %d", ml.Size())
}
// Test removal
if !ml.Remove(2) {
t.Error("Should successfully remove item2")
}
if ml.Remove(2) {
t.Error("Should not remove already removed item")
}
if ml.Size() != 3 {
t.Errorf("Expected size 3 after removal, got %d", ml.Size())
}
// Test GetAll
all := ml.GetAll()
if len(all) != 3 {
t.Errorf("Expected 3 items in GetAll, got %d", len(all))
}
// Verify we can modify the returned map without affecting the original
all[999] = &TestItem{ID: 999, Name: "Should not affect original", Category: "Z"}
if ml.Exists(999) {
t.Error("Modifying returned map should not affect original list")
}
// Test GetAllSlice
slice := ml.GetAllSlice()
if len(slice) != 3 {
t.Errorf("Expected 3 items in GetAllSlice, got %d", len(slice))
}
// Test GetAllIDs
ids := ml.GetAllIDs()
if len(ids) != 3 {
t.Errorf("Expected 3 IDs in GetAllIDs, got %d", len(ids))
}
// Test Clear
ml.Clear()
if !ml.IsEmpty() {
t.Error("List should be empty after Clear")
}
if ml.Size() != 0 {
t.Error("List should have size 0 after Clear")
}
}
// TestMasterListSearch tests search functionality
func TestMasterListSearch(t *testing.T) {
ml := NewMasterList[int32, *TestItem]()
// Add test items
items := []*TestItem{
{ID: 1, Name: "Alpha", Category: "A"},
{ID: 2, Name: "Beta", Category: "B"},
{ID: 3, Name: "Gamma", Category: "A"},
{ID: 4, Name: "Delta", Category: "C"},
{ID: 5, Name: "Alpha Two", Category: "A"},
}
for _, item := range items {
ml.Add(item)
}
// Test Filter
categoryA := ml.Filter(func(item *TestItem) bool {
return item.Category == "A"
})
if len(categoryA) != 3 {
t.Errorf("Expected 3 items in category A, got %d", len(categoryA))
}
// Test Find
found, exists := ml.Find(func(item *TestItem) bool {
return item.Name == "Beta"
})
if !exists || found.ID != 2 {
t.Error("Should find Beta with ID 2")
}
notFound, exists := ml.Find(func(item *TestItem) bool {
return item.Name == "Nonexistent"
})
if exists || notFound != nil {
t.Error("Should not find nonexistent item")
}
// Test Count
count := ml.Count(func(item *TestItem) bool {
return item.Category == "A"
})
if count != 3 {
t.Errorf("Expected count of 3 for category A, got %d", count)
}
// Test ForEach
var visitedIDs []int32
ml.ForEach(func(id int32, item *TestItem) {
visitedIDs = append(visitedIDs, id)
})
if len(visitedIDs) != 5 {
t.Errorf("Expected to visit 5 items, visited %d", len(visitedIDs))
}
}
// TestMasterListConcurrency tests thread safety (basic test)
func TestMasterListConcurrency(t *testing.T) {
ml := NewMasterList[int32, *TestItem]()
// Test WithReadLock
ml.Add(&TestItem{ID: 1, Name: "Test", Category: "A"})
var foundItem *TestItem
ml.WithReadLock(func(items map[int32]*TestItem) {
foundItem = items[1]
})
if foundItem == nil || foundItem.Name != "Test" {
t.Error("WithReadLock should provide access to internal map")
}
// Test WithWriteLock
ml.WithWriteLock(func(items map[int32]*TestItem) {
items[2] = &TestItem{ID: 2, Name: "Added via WriteLock", Category: "B"}
})
if !ml.Exists(2) {
t.Error("Item added via WithWriteLock should exist")
}
retrieved := ml.Get(2)
if retrieved.Name != "Added via WriteLock" {
t.Error("Item added via WithWriteLock not found correctly")
}
}
// BenchmarkMasterList tests performance of basic operations
func BenchmarkMasterList(b *testing.B) {
ml := NewMasterList[int32, *TestItem]()
// Pre-populate for benchmarks
for i := int32(0); i < 1000; i++ {
ml.Add(&TestItem{
ID: i,
Name: fmt.Sprintf("Item %d", i),
Category: fmt.Sprintf("Category %d", i%10),
})
}
b.Run("Get", func(b *testing.B) {
for i := 0; i < b.N; i++ {
ml.Get(int32(i % 1000))
}
})
b.Run("Add", func(b *testing.B) {
for i := 0; i < b.N; i++ {
ml.AddOrUpdate(&TestItem{
ID: int32(1000 + i),
Name: fmt.Sprintf("Bench Item %d", i),
Category: "Bench",
})
}
})
b.Run("Filter", func(b *testing.B) {
for i := 0; i < b.N; i++ {
ml.Filter(func(item *TestItem) bool {
return item.Category == "Category 5"
})
}
})
}