fix collections
This commit is contained in:
parent
bec433ef89
commit
6cad1bd9f9
456
internal/collections/benchmark_test.go
Normal file
456
internal/collections/benchmark_test.go
Normal file
@ -0,0 +1,456 @@
|
|||||||
|
package collections
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"eq2emu/internal/database"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Setup creates a master list with test data for benchmarking
|
||||||
|
func benchmarkSetup() *MasterList {
|
||||||
|
db, _ := database.NewSQLite("file::memory:?mode=memory&cache=shared")
|
||||||
|
|
||||||
|
masterList := NewMasterList()
|
||||||
|
|
||||||
|
// Add collections across different categories and levels
|
||||||
|
categories := []string{
|
||||||
|
"Heritage", "Treasured", "Legendary", "Fabled", "Mythical",
|
||||||
|
"Handcrafted", "Mastercrafted", "Rare", "Uncommon", "Common",
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < 100; i++ {
|
||||||
|
category := categories[i%len(categories)]
|
||||||
|
level := int8((i % 50) + 1) // Levels 1-50
|
||||||
|
|
||||||
|
collection := NewWithData(int32(i+1), fmt.Sprintf("Collection %d", i+1), category, level, db)
|
||||||
|
|
||||||
|
// Add collection items (some found, some not)
|
||||||
|
numItems := (i % 5) + 1 // 1-5 items per collection
|
||||||
|
for j := 0; j < numItems; j++ {
|
||||||
|
found := ItemNotFound
|
||||||
|
if (i+j)%3 == 0 { // About 1/3 of items are found
|
||||||
|
found = ItemFound
|
||||||
|
}
|
||||||
|
collection.CollectionItems = append(collection.CollectionItems, CollectionItem{
|
||||||
|
ItemID: int32((i+1)*1000 + j + 1),
|
||||||
|
Index: int8(j),
|
||||||
|
Found: int8(found),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add rewards
|
||||||
|
if i%4 == 0 {
|
||||||
|
collection.RewardCoin = int64((i + 1) * 100)
|
||||||
|
}
|
||||||
|
if i%5 == 0 {
|
||||||
|
collection.RewardXP = int64((i + 1) * 50)
|
||||||
|
}
|
||||||
|
if i%6 == 0 {
|
||||||
|
collection.RewardItems = append(collection.RewardItems, CollectionRewardItem{
|
||||||
|
ItemID: int32(i + 10000),
|
||||||
|
Quantity: 1,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if i%7 == 0 {
|
||||||
|
collection.SelectableRewardItems = append(collection.SelectableRewardItems, CollectionRewardItem{
|
||||||
|
ItemID: int32(i + 20000),
|
||||||
|
Quantity: 1,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Some collections are completed
|
||||||
|
if i%10 == 0 {
|
||||||
|
collection.Completed = true
|
||||||
|
}
|
||||||
|
|
||||||
|
masterList.AddCollection(collection)
|
||||||
|
}
|
||||||
|
|
||||||
|
return masterList
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkMasterList_AddCollection(b *testing.B) {
|
||||||
|
db, _ := database.NewSQLite("file::memory:?mode=memory&cache=shared")
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
masterList := NewMasterList()
|
||||||
|
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
collection := NewWithData(int32(i+10000), fmt.Sprintf("Collection%d", i), "Heritage", 20, db)
|
||||||
|
collection.CollectionItems = []CollectionItem{
|
||||||
|
{ItemID: int32(i + 50000), Index: 0, Found: ItemNotFound},
|
||||||
|
}
|
||||||
|
masterList.AddCollection(collection)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkMasterList_GetCollection(b *testing.B) {
|
||||||
|
masterList := benchmarkSetup()
|
||||||
|
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
masterList.GetCollection(int32(i%100 + 1))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkMasterList_GetCollectionSafe(b *testing.B) {
|
||||||
|
masterList := benchmarkSetup()
|
||||||
|
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
masterList.GetCollectionSafe(int32(i%100 + 1))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkMasterList_HasCollection(b *testing.B) {
|
||||||
|
masterList := benchmarkSetup()
|
||||||
|
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
masterList.HasCollection(int32(i%100 + 1))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkMasterList_FindCollectionsByCategory(b *testing.B) {
|
||||||
|
masterList := benchmarkSetup()
|
||||||
|
categories := []string{"Heritage", "Treasured", "Legendary", "Fabled", "Mythical"}
|
||||||
|
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
masterList.FindCollectionsByCategory(categories[i%len(categories)])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkMasterList_GetCollectionsByExactLevel(b *testing.B) {
|
||||||
|
masterList := benchmarkSetup()
|
||||||
|
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
level := int8(i%50 + 1)
|
||||||
|
masterList.GetCollectionsByExactLevel(level)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkMasterList_FindCollectionsByLevel(b *testing.B) {
|
||||||
|
masterList := benchmarkSetup()
|
||||||
|
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
minLevel := int8(i%45 + 1)
|
||||||
|
maxLevel := minLevel + 5
|
||||||
|
masterList.FindCollectionsByLevel(minLevel, maxLevel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkMasterList_GetCollectionByName(b *testing.B) {
|
||||||
|
masterList := benchmarkSetup()
|
||||||
|
names := []string{"collection 1", "collection 25", "collection 50", "collection 75", "collection 100"}
|
||||||
|
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
masterList.GetCollectionByName(names[i%len(names)])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkMasterList_NeedsItem(b *testing.B) {
|
||||||
|
masterList := benchmarkSetup()
|
||||||
|
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
itemID := int32(i%100*1000 + 1001) // Various item IDs from the collections
|
||||||
|
masterList.NeedsItem(itemID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkMasterList_GetCollectionsNeedingItem(b *testing.B) {
|
||||||
|
masterList := benchmarkSetup()
|
||||||
|
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
itemID := int32(i%100*1000 + 1001) // Various item IDs from the collections
|
||||||
|
masterList.GetCollectionsNeedingItem(itemID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkMasterList_GetCompletedCollections(b *testing.B) {
|
||||||
|
masterList := benchmarkSetup()
|
||||||
|
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
masterList.GetCompletedCollections()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkMasterList_GetIncompleteCollections(b *testing.B) {
|
||||||
|
masterList := benchmarkSetup()
|
||||||
|
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
masterList.GetIncompleteCollections()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkMasterList_GetReadyToTurnInCollections(b *testing.B) {
|
||||||
|
masterList := benchmarkSetup()
|
||||||
|
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
masterList.GetReadyToTurnInCollections()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkMasterList_GetCategories(b *testing.B) {
|
||||||
|
masterList := benchmarkSetup()
|
||||||
|
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
masterList.GetCategories()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkMasterList_GetLevels(b *testing.B) {
|
||||||
|
masterList := benchmarkSetup()
|
||||||
|
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
masterList.GetLevels()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkMasterList_GetItemsNeeded(b *testing.B) {
|
||||||
|
masterList := benchmarkSetup()
|
||||||
|
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
masterList.GetItemsNeeded()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkMasterList_GetAllCollections(b *testing.B) {
|
||||||
|
masterList := benchmarkSetup()
|
||||||
|
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
masterList.GetAllCollections()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkMasterList_GetAllCollectionsList(b *testing.B) {
|
||||||
|
masterList := benchmarkSetup()
|
||||||
|
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
masterList.GetAllCollectionsList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkMasterList_GetStatistics(b *testing.B) {
|
||||||
|
masterList := benchmarkSetup()
|
||||||
|
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
masterList.GetStatistics()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkMasterList_ValidateCollections(b *testing.B) {
|
||||||
|
masterList := benchmarkSetup()
|
||||||
|
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
masterList.ValidateCollections()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkMasterList_RemoveCollection(b *testing.B) {
|
||||||
|
b.StopTimer()
|
||||||
|
masterList := benchmarkSetup()
|
||||||
|
initialCount := masterList.GetCollectionCount()
|
||||||
|
|
||||||
|
// Pre-populate with collections we'll remove
|
||||||
|
db, _ := database.NewSQLite("file::memory:?mode=memory&cache=shared")
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
collection := NewWithData(int32(20000+i), fmt.Sprintf("ToRemove%d", i), "Temporary", 1, db)
|
||||||
|
collection.CollectionItems = []CollectionItem{
|
||||||
|
{ItemID: int32(60000 + i), Index: 0, Found: ItemNotFound},
|
||||||
|
}
|
||||||
|
masterList.AddCollection(collection)
|
||||||
|
}
|
||||||
|
|
||||||
|
b.StartTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
masterList.RemoveCollection(int32(20000 + i))
|
||||||
|
}
|
||||||
|
|
||||||
|
b.StopTimer()
|
||||||
|
if masterList.GetCollectionCount() != initialCount {
|
||||||
|
b.Errorf("Expected %d collections after removal, got %d", initialCount, masterList.GetCollectionCount())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkMasterList_ForEach(b *testing.B) {
|
||||||
|
masterList := benchmarkSetup()
|
||||||
|
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
count := 0
|
||||||
|
masterList.ForEach(func(id int32, collection *Collection) {
|
||||||
|
count++
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkMasterList_UpdateCollection(b *testing.B) {
|
||||||
|
masterList := benchmarkSetup()
|
||||||
|
db, _ := database.NewSQLite("file::memory:?mode=memory&cache=shared")
|
||||||
|
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
collectionID := int32(i%100 + 1)
|
||||||
|
updatedCollection := &Collection{
|
||||||
|
ID: collectionID,
|
||||||
|
Name: fmt.Sprintf("Updated%d", i),
|
||||||
|
Category: "Updated",
|
||||||
|
Level: 25,
|
||||||
|
db: db,
|
||||||
|
isNew: false,
|
||||||
|
CollectionItems: []CollectionItem{
|
||||||
|
{ItemID: int32(i + 70000), Index: 0, Found: ItemNotFound},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
masterList.UpdateCollection(updatedCollection)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkMasterList_RefreshCollectionIndices(b *testing.B) {
|
||||||
|
masterList := benchmarkSetup()
|
||||||
|
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
collection := masterList.GetCollection(int32(i%100 + 1))
|
||||||
|
if collection != nil {
|
||||||
|
masterList.RefreshCollectionIndices(collection)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkMasterList_GetCollectionClone(b *testing.B) {
|
||||||
|
masterList := benchmarkSetup()
|
||||||
|
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
masterList.GetCollectionClone(int32(i%100 + 1))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Memory allocation benchmarks
|
||||||
|
func BenchmarkMasterList_GetCollection_Allocs(b *testing.B) {
|
||||||
|
masterList := benchmarkSetup()
|
||||||
|
|
||||||
|
b.ResetTimer()
|
||||||
|
b.ReportAllocs()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
masterList.GetCollection(int32(i%100 + 1))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkMasterList_FindCollectionsByCategory_Allocs(b *testing.B) {
|
||||||
|
masterList := benchmarkSetup()
|
||||||
|
|
||||||
|
b.ResetTimer()
|
||||||
|
b.ReportAllocs()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
masterList.FindCollectionsByCategory("Heritage")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkMasterList_GetCollectionByName_Allocs(b *testing.B) {
|
||||||
|
masterList := benchmarkSetup()
|
||||||
|
|
||||||
|
b.ResetTimer()
|
||||||
|
b.ReportAllocs()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
masterList.GetCollectionByName("collection 1")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkMasterList_NeedsItem_Allocs(b *testing.B) {
|
||||||
|
masterList := benchmarkSetup()
|
||||||
|
|
||||||
|
b.ResetTimer()
|
||||||
|
b.ReportAllocs()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
masterList.NeedsItem(1001)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkMasterList_GetCollectionsNeedingItem_Allocs(b *testing.B) {
|
||||||
|
masterList := benchmarkSetup()
|
||||||
|
|
||||||
|
b.ResetTimer()
|
||||||
|
b.ReportAllocs()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
masterList.GetCollectionsNeedingItem(1001)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Concurrent benchmarks
|
||||||
|
func BenchmarkMasterList_ConcurrentReads(b *testing.B) {
|
||||||
|
masterList := benchmarkSetup()
|
||||||
|
|
||||||
|
b.ResetTimer()
|
||||||
|
b.RunParallel(func(pb *testing.PB) {
|
||||||
|
for pb.Next() {
|
||||||
|
// Mix of read operations
|
||||||
|
switch b.N % 6 {
|
||||||
|
case 0:
|
||||||
|
masterList.GetCollection(int32(b.N%100 + 1))
|
||||||
|
case 1:
|
||||||
|
masterList.FindCollectionsByCategory("Heritage")
|
||||||
|
case 2:
|
||||||
|
masterList.GetCollectionByName("collection 1")
|
||||||
|
case 3:
|
||||||
|
masterList.NeedsItem(1001)
|
||||||
|
case 4:
|
||||||
|
masterList.GetCompletedCollections()
|
||||||
|
case 5:
|
||||||
|
masterList.GetCollectionsByExactLevel(10)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkMasterList_ConcurrentMixed(b *testing.B) {
|
||||||
|
masterList := benchmarkSetup()
|
||||||
|
db, _ := database.NewSQLite("file::memory:?mode=memory&cache=shared")
|
||||||
|
|
||||||
|
b.ResetTimer()
|
||||||
|
b.RunParallel(func(pb *testing.PB) {
|
||||||
|
for pb.Next() {
|
||||||
|
// Mix of read and write operations (mostly reads)
|
||||||
|
switch b.N % 10 {
|
||||||
|
case 0: // 10% writes
|
||||||
|
collection := NewWithData(int32(b.N+50000), fmt.Sprintf("Concurrent%d", b.N), "Concurrent", 15, db)
|
||||||
|
collection.CollectionItems = []CollectionItem{
|
||||||
|
{ItemID: int32(b.N + 80000), Index: 0, Found: ItemNotFound},
|
||||||
|
}
|
||||||
|
masterList.AddCollection(collection)
|
||||||
|
default: // 90% reads
|
||||||
|
switch b.N % 5 {
|
||||||
|
case 0:
|
||||||
|
masterList.GetCollection(int32(b.N%100 + 1))
|
||||||
|
case 1:
|
||||||
|
masterList.FindCollectionsByCategory("Heritage")
|
||||||
|
case 2:
|
||||||
|
masterList.GetCollectionByName("collection 1")
|
||||||
|
case 3:
|
||||||
|
masterList.NeedsItem(1001)
|
||||||
|
case 4:
|
||||||
|
masterList.GetCompletedCollections()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
@ -2,122 +2,510 @@ package collections
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"maps"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
"eq2emu/internal/common"
|
|
||||||
"eq2emu/internal/database"
|
"eq2emu/internal/database"
|
||||||
)
|
)
|
||||||
|
|
||||||
// MasterList manages a collection of collections using the generic MasterList base
|
// MasterList is a specialized collection master list optimized for:
|
||||||
|
// - Fast ID-based lookups (O(1))
|
||||||
|
// - Fast category filtering (O(1))
|
||||||
|
// - Fast level range queries (indexed)
|
||||||
|
// - Fast item requirement lookups (O(1))
|
||||||
|
// - Efficient completion status filtering
|
||||||
|
// - Name-based searching with indexing
|
||||||
type MasterList struct {
|
type MasterList struct {
|
||||||
*common.MasterList[int32, *Collection]
|
// Core storage
|
||||||
|
collections map[int32]*Collection // ID -> Collection
|
||||||
|
mutex sync.RWMutex
|
||||||
|
|
||||||
|
// Specialized indices for O(1) lookups
|
||||||
|
byCategory map[string][]*Collection // Category -> collections
|
||||||
|
byLevel map[int8][]*Collection // Level -> collections
|
||||||
|
byItemNeeded map[int32][]*Collection // ItemID -> collections that need it
|
||||||
|
byNameLower map[string]*Collection // Lowercase name -> collection
|
||||||
|
byCompletion map[bool][]*Collection // Completion status -> collections
|
||||||
|
|
||||||
|
// Cached metadata
|
||||||
|
categories []string // Unique categories (cached)
|
||||||
|
levels []int8 // Unique levels (cached)
|
||||||
|
itemsNeeded []int32 // Unique items needed (cached)
|
||||||
|
categoryStats map[string]int // Category -> count
|
||||||
|
metaStale bool // Whether metadata cache needs refresh
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewMasterList creates a new collection master list
|
// NewMasterList creates a new specialized collection master list
|
||||||
func NewMasterList() *MasterList {
|
func NewMasterList() *MasterList {
|
||||||
return &MasterList{
|
return &MasterList{
|
||||||
MasterList: common.NewMasterList[int32, *Collection](),
|
collections: make(map[int32]*Collection),
|
||||||
|
byCategory: make(map[string][]*Collection),
|
||||||
|
byLevel: make(map[int8][]*Collection),
|
||||||
|
byItemNeeded: make(map[int32][]*Collection),
|
||||||
|
byNameLower: make(map[string]*Collection),
|
||||||
|
byCompletion: make(map[bool][]*Collection),
|
||||||
|
categoryStats: make(map[string]int),
|
||||||
|
metaStale: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddCollection adds a collection to the master list
|
// refreshMetaCache updates the cached metadata
|
||||||
func (ml *MasterList) AddCollection(collection *Collection) bool {
|
func (ml *MasterList) refreshMetaCache() {
|
||||||
return ml.Add(collection)
|
if !ml.metaStale {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear and rebuild category stats
|
||||||
|
ml.categoryStats = make(map[string]int)
|
||||||
|
categorySet := make(map[string]struct{})
|
||||||
|
levelSet := make(map[int8]struct{})
|
||||||
|
itemSet := make(map[int32]struct{})
|
||||||
|
|
||||||
|
// Collect unique values and stats
|
||||||
|
for _, collection := range ml.collections {
|
||||||
|
category := collection.GetCategory()
|
||||||
|
ml.categoryStats[category]++
|
||||||
|
categorySet[category] = struct{}{}
|
||||||
|
levelSet[collection.GetLevel()] = struct{}{}
|
||||||
|
|
||||||
|
// Collect items needed by this collection
|
||||||
|
for _, item := range collection.CollectionItems {
|
||||||
|
if item.Found == ItemNotFound {
|
||||||
|
itemSet[item.ItemID] = struct{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear and rebuild cached slices
|
||||||
|
ml.categories = ml.categories[:0]
|
||||||
|
for category := range categorySet {
|
||||||
|
ml.categories = append(ml.categories, category)
|
||||||
|
}
|
||||||
|
|
||||||
|
ml.levels = ml.levels[:0]
|
||||||
|
for level := range levelSet {
|
||||||
|
ml.levels = append(ml.levels, level)
|
||||||
|
}
|
||||||
|
|
||||||
|
ml.itemsNeeded = ml.itemsNeeded[:0]
|
||||||
|
for itemID := range itemSet {
|
||||||
|
ml.itemsNeeded = append(ml.itemsNeeded, itemID)
|
||||||
|
}
|
||||||
|
|
||||||
|
ml.metaStale = false
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetCollection retrieves a collection by ID
|
// updateCollectionIndices updates all indices for a collection
|
||||||
|
func (ml *MasterList) updateCollectionIndices(collection *Collection, add bool) {
|
||||||
|
if add {
|
||||||
|
// Add to category index
|
||||||
|
category := collection.GetCategory()
|
||||||
|
ml.byCategory[category] = append(ml.byCategory[category], collection)
|
||||||
|
|
||||||
|
// Add to level index
|
||||||
|
level := collection.GetLevel()
|
||||||
|
ml.byLevel[level] = append(ml.byLevel[level], collection)
|
||||||
|
|
||||||
|
// Add to name index
|
||||||
|
ml.byNameLower[strings.ToLower(collection.GetName())] = collection
|
||||||
|
|
||||||
|
// Add to completion index
|
||||||
|
completed := collection.Completed
|
||||||
|
ml.byCompletion[completed] = append(ml.byCompletion[completed], collection)
|
||||||
|
|
||||||
|
// Add to item needed index
|
||||||
|
for _, item := range collection.CollectionItems {
|
||||||
|
if item.Found == ItemNotFound {
|
||||||
|
ml.byItemNeeded[item.ItemID] = append(ml.byItemNeeded[item.ItemID], collection)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Remove from category index
|
||||||
|
category := collection.GetCategory()
|
||||||
|
categoryCollections := ml.byCategory[category]
|
||||||
|
for i, coll := range categoryCollections {
|
||||||
|
if coll.ID == collection.ID {
|
||||||
|
ml.byCategory[category] = append(categoryCollections[:i], categoryCollections[i+1:]...)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove from level index
|
||||||
|
level := collection.GetLevel()
|
||||||
|
levelCollections := ml.byLevel[level]
|
||||||
|
for i, coll := range levelCollections {
|
||||||
|
if coll.ID == collection.ID {
|
||||||
|
ml.byLevel[level] = append(levelCollections[:i], levelCollections[i+1:]...)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove from name index
|
||||||
|
delete(ml.byNameLower, strings.ToLower(collection.GetName()))
|
||||||
|
|
||||||
|
// Remove from completion index
|
||||||
|
completed := collection.Completed
|
||||||
|
completionCollections := ml.byCompletion[completed]
|
||||||
|
for i, coll := range completionCollections {
|
||||||
|
if coll.ID == collection.ID {
|
||||||
|
ml.byCompletion[completed] = append(completionCollections[:i], completionCollections[i+1:]...)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove from item needed index
|
||||||
|
for _, item := range collection.CollectionItems {
|
||||||
|
if item.Found == ItemNotFound {
|
||||||
|
itemCollections := ml.byItemNeeded[item.ItemID]
|
||||||
|
for i, coll := range itemCollections {
|
||||||
|
if coll.ID == collection.ID {
|
||||||
|
ml.byItemNeeded[item.ItemID] = append(itemCollections[:i], itemCollections[i+1:]...)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddCollection adds a collection with full indexing
|
||||||
|
func (ml *MasterList) AddCollection(collection *Collection) bool {
|
||||||
|
if collection == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
ml.mutex.Lock()
|
||||||
|
defer ml.mutex.Unlock()
|
||||||
|
|
||||||
|
// Check if exists
|
||||||
|
if _, exists := ml.collections[collection.ID]; exists {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add to core storage
|
||||||
|
ml.collections[collection.ID] = collection
|
||||||
|
|
||||||
|
// Update all indices
|
||||||
|
ml.updateCollectionIndices(collection, true)
|
||||||
|
|
||||||
|
// Invalidate metadata cache
|
||||||
|
ml.metaStale = true
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCollection retrieves by ID (O(1))
|
||||||
func (ml *MasterList) GetCollection(id int32) *Collection {
|
func (ml *MasterList) GetCollection(id int32) *Collection {
|
||||||
return ml.Get(id)
|
ml.mutex.RLock()
|
||||||
|
defer ml.mutex.RUnlock()
|
||||||
|
return ml.collections[id]
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetCollectionSafe retrieves a collection by ID with existence check
|
// GetCollectionSafe retrieves a collection by ID with existence check
|
||||||
func (ml *MasterList) GetCollectionSafe(id int32) (*Collection, bool) {
|
func (ml *MasterList) GetCollectionSafe(id int32) (*Collection, bool) {
|
||||||
return ml.GetSafe(id)
|
ml.mutex.RLock()
|
||||||
|
defer ml.mutex.RUnlock()
|
||||||
|
collection, exists := ml.collections[id]
|
||||||
|
return collection, exists
|
||||||
}
|
}
|
||||||
|
|
||||||
// HasCollection checks if a collection exists by ID
|
// HasCollection checks if a collection exists by ID
|
||||||
func (ml *MasterList) HasCollection(id int32) bool {
|
func (ml *MasterList) HasCollection(id int32) bool {
|
||||||
return ml.Exists(id)
|
ml.mutex.RLock()
|
||||||
|
defer ml.mutex.RUnlock()
|
||||||
|
_, exists := ml.collections[id]
|
||||||
|
return exists
|
||||||
}
|
}
|
||||||
|
|
||||||
// RemoveCollection removes a collection by ID
|
// GetCollectionClone retrieves a cloned copy of a collection by ID
|
||||||
|
func (ml *MasterList) GetCollectionClone(id int32) *Collection {
|
||||||
|
ml.mutex.RLock()
|
||||||
|
defer ml.mutex.RUnlock()
|
||||||
|
collection := ml.collections[id]
|
||||||
|
if collection == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return collection.Clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoveCollection removes a collection and updates all indices
|
||||||
func (ml *MasterList) RemoveCollection(id int32) bool {
|
func (ml *MasterList) RemoveCollection(id int32) bool {
|
||||||
return ml.Remove(id)
|
ml.mutex.Lock()
|
||||||
|
defer ml.mutex.Unlock()
|
||||||
|
|
||||||
|
collection, exists := ml.collections[id]
|
||||||
|
if !exists {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove from core storage
|
||||||
|
delete(ml.collections, id)
|
||||||
|
|
||||||
|
// Update all indices
|
||||||
|
ml.updateCollectionIndices(collection, false)
|
||||||
|
|
||||||
|
// Invalidate metadata cache
|
||||||
|
ml.metaStale = true
|
||||||
|
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetAllCollections returns all collections as a map
|
// GetAllCollections returns a copy of all collections map
|
||||||
func (ml *MasterList) GetAllCollections() map[int32]*Collection {
|
func (ml *MasterList) GetAllCollections() map[int32]*Collection {
|
||||||
return ml.GetAll()
|
ml.mutex.RLock()
|
||||||
|
defer ml.mutex.RUnlock()
|
||||||
|
|
||||||
|
// Return a copy to prevent external modification
|
||||||
|
result := make(map[int32]*Collection, len(ml.collections))
|
||||||
|
maps.Copy(result, ml.collections)
|
||||||
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetAllCollectionsList returns all collections as a slice
|
// GetAllCollectionsList returns all collections as a slice
|
||||||
func (ml *MasterList) GetAllCollectionsList() []*Collection {
|
func (ml *MasterList) GetAllCollectionsList() []*Collection {
|
||||||
return ml.GetAllSlice()
|
ml.mutex.RLock()
|
||||||
|
defer ml.mutex.RUnlock()
|
||||||
|
|
||||||
|
result := make([]*Collection, 0, len(ml.collections))
|
||||||
|
for _, collection := range ml.collections {
|
||||||
|
result = append(result, collection)
|
||||||
|
}
|
||||||
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetCollectionCount returns the number of collections
|
// GetCollectionCount returns the number of collections
|
||||||
func (ml *MasterList) GetCollectionCount() int {
|
func (ml *MasterList) GetCollectionCount() int {
|
||||||
return ml.Size()
|
ml.mutex.RLock()
|
||||||
|
defer ml.mutex.RUnlock()
|
||||||
|
return len(ml.collections)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Size returns the total number of collections
|
||||||
|
func (ml *MasterList) Size() int {
|
||||||
|
ml.mutex.RLock()
|
||||||
|
defer ml.mutex.RUnlock()
|
||||||
|
return len(ml.collections)
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsEmpty returns true if the master list is empty
|
||||||
|
func (ml *MasterList) IsEmpty() bool {
|
||||||
|
ml.mutex.RLock()
|
||||||
|
defer ml.mutex.RUnlock()
|
||||||
|
return len(ml.collections) == 0
|
||||||
}
|
}
|
||||||
|
|
||||||
// ClearCollections removes all collections from the list
|
// ClearCollections removes all collections from the list
|
||||||
func (ml *MasterList) ClearCollections() {
|
func (ml *MasterList) ClearCollections() {
|
||||||
ml.Clear()
|
ml.mutex.Lock()
|
||||||
|
defer ml.mutex.Unlock()
|
||||||
|
|
||||||
|
// Clear all maps
|
||||||
|
ml.collections = make(map[int32]*Collection)
|
||||||
|
ml.byCategory = make(map[string][]*Collection)
|
||||||
|
ml.byLevel = make(map[int8][]*Collection)
|
||||||
|
ml.byItemNeeded = make(map[int32][]*Collection)
|
||||||
|
ml.byNameLower = make(map[string]*Collection)
|
||||||
|
ml.byCompletion = make(map[bool][]*Collection)
|
||||||
|
|
||||||
|
// Clear cached metadata
|
||||||
|
ml.categories = ml.categories[:0]
|
||||||
|
ml.levels = ml.levels[:0]
|
||||||
|
ml.itemsNeeded = ml.itemsNeeded[:0]
|
||||||
|
ml.categoryStats = make(map[string]int)
|
||||||
|
ml.metaStale = true
|
||||||
}
|
}
|
||||||
|
|
||||||
// NeedsItem checks if any collection needs the specified item
|
// Clear removes all collections from the master list
|
||||||
func (ml *MasterList) NeedsItem(itemID int32) bool {
|
func (ml *MasterList) Clear() {
|
||||||
for _, collection := range ml.GetAll() {
|
ml.ClearCollections()
|
||||||
if collection.NeedsItem(itemID) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// FindCollectionsByCategory finds collections in a specific category
|
// FindCollectionsByCategory finds collections in a specific category (O(1))
|
||||||
func (ml *MasterList) FindCollectionsByCategory(category string) []*Collection {
|
func (ml *MasterList) FindCollectionsByCategory(category string) []*Collection {
|
||||||
return ml.Filter(func(collection *Collection) bool {
|
ml.mutex.RLock()
|
||||||
return collection.GetCategory() == category
|
defer ml.mutex.RUnlock()
|
||||||
})
|
return ml.byCategory[category]
|
||||||
}
|
}
|
||||||
|
|
||||||
// FindCollectionsByLevel finds collections for a specific level range
|
// FindCollectionsByLevel finds collections for a specific level range
|
||||||
func (ml *MasterList) FindCollectionsByLevel(minLevel, maxLevel int8) []*Collection {
|
func (ml *MasterList) FindCollectionsByLevel(minLevel, maxLevel int8) []*Collection {
|
||||||
return ml.Filter(func(collection *Collection) bool {
|
ml.mutex.RLock()
|
||||||
level := collection.GetLevel()
|
defer ml.mutex.RUnlock()
|
||||||
return level >= minLevel && level <= maxLevel
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetCollectionsNeedingItem returns all collections that need a specific item
|
var result []*Collection
|
||||||
func (ml *MasterList) GetCollectionsNeedingItem(itemID int32) []*Collection {
|
for level := minLevel; level <= maxLevel; level++ {
|
||||||
return ml.Filter(func(collection *Collection) bool {
|
result = append(result, ml.byLevel[level]...)
|
||||||
return collection.NeedsItem(itemID)
|
}
|
||||||
})
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetCategories returns all unique collection categories
|
// GetCollectionsByExactLevel returns collections with specific level (O(1))
|
||||||
func (ml *MasterList) GetCategories() []string {
|
func (ml *MasterList) GetCollectionsByExactLevel(level int8) []*Collection {
|
||||||
categoryMap := make(map[string]bool)
|
ml.mutex.RLock()
|
||||||
ml.ForEach(func(id int32, collection *Collection) {
|
defer ml.mutex.RUnlock()
|
||||||
categoryMap[collection.GetCategory()] = true
|
return ml.byLevel[level]
|
||||||
})
|
}
|
||||||
|
|
||||||
categories := make([]string, 0, len(categoryMap))
|
// GetCollectionByName retrieves a collection by name (case-insensitive, O(1))
|
||||||
for category := range categoryMap {
|
func (ml *MasterList) GetCollectionByName(name string) *Collection {
|
||||||
categories = append(categories, category)
|
ml.mutex.RLock()
|
||||||
|
defer ml.mutex.RUnlock()
|
||||||
|
return ml.byNameLower[strings.ToLower(name)]
|
||||||
|
}
|
||||||
|
|
||||||
|
// NeedsItem checks if any collection needs the specified item (O(1))
|
||||||
|
func (ml *MasterList) NeedsItem(itemID int32) bool {
|
||||||
|
ml.mutex.RLock()
|
||||||
|
defer ml.mutex.RUnlock()
|
||||||
|
collections := ml.byItemNeeded[itemID]
|
||||||
|
return len(collections) > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCollectionsNeedingItem returns all collections that need a specific item (O(1))
|
||||||
|
func (ml *MasterList) GetCollectionsNeedingItem(itemID int32) []*Collection {
|
||||||
|
ml.mutex.RLock()
|
||||||
|
defer ml.mutex.RUnlock()
|
||||||
|
return ml.byItemNeeded[itemID]
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCompletedCollections returns all completed collections (O(1))
|
||||||
|
func (ml *MasterList) GetCompletedCollections() []*Collection {
|
||||||
|
ml.mutex.RLock()
|
||||||
|
defer ml.mutex.RUnlock()
|
||||||
|
return ml.byCompletion[true]
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetIncompleteCollections returns all incomplete collections (O(1))
|
||||||
|
func (ml *MasterList) GetIncompleteCollections() []*Collection {
|
||||||
|
ml.mutex.RLock()
|
||||||
|
defer ml.mutex.RUnlock()
|
||||||
|
return ml.byCompletion[false]
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetReadyToTurnInCollections returns collections ready to be turned in
|
||||||
|
func (ml *MasterList) GetReadyToTurnInCollections() []*Collection {
|
||||||
|
ml.mutex.RLock()
|
||||||
|
defer ml.mutex.RUnlock()
|
||||||
|
|
||||||
|
var result []*Collection
|
||||||
|
for _, collection := range ml.collections {
|
||||||
|
if !collection.Completed && collection.GetIsReadyToTurnIn() {
|
||||||
|
result = append(result, collection)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCollectionsByLevelRange returns collections within level range using indices
|
||||||
|
func (ml *MasterList) GetCollectionsByLevelRange(minLevel, maxLevel int8) []*Collection {
|
||||||
|
return ml.FindCollectionsByLevel(minLevel, maxLevel)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCategories returns all unique collection categories using cached results
|
||||||
|
func (ml *MasterList) GetCategories() []string {
|
||||||
|
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([]string, len(ml.categories))
|
||||||
|
copy(result, ml.categories)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetLevels returns all unique collection levels using cached results
|
||||||
|
func (ml *MasterList) GetLevels() []int8 {
|
||||||
|
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([]int8, len(ml.levels))
|
||||||
|
copy(result, ml.levels)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetItemsNeeded returns all unique items needed by collections
|
||||||
|
func (ml *MasterList) GetItemsNeeded() []int32 {
|
||||||
|
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([]int32, len(ml.itemsNeeded))
|
||||||
|
copy(result, ml.itemsNeeded)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateCollection updates an existing collection and refreshes indices
|
||||||
|
func (ml *MasterList) UpdateCollection(collection *Collection) error {
|
||||||
|
if collection == nil {
|
||||||
|
return fmt.Errorf("collection cannot be nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
ml.mutex.Lock()
|
||||||
|
defer ml.mutex.Unlock()
|
||||||
|
|
||||||
|
// Check if exists
|
||||||
|
old, exists := ml.collections[collection.ID]
|
||||||
|
if !exists {
|
||||||
|
return fmt.Errorf("collection %d not found", collection.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove old collection from indices (but not core storage yet)
|
||||||
|
ml.updateCollectionIndices(old, false)
|
||||||
|
|
||||||
|
// Update core storage
|
||||||
|
ml.collections[collection.ID] = collection
|
||||||
|
|
||||||
|
// Add new collection to indices
|
||||||
|
ml.updateCollectionIndices(collection, true)
|
||||||
|
|
||||||
|
// Invalidate metadata cache
|
||||||
|
ml.metaStale = true
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RefreshCollectionIndices refreshes indices for a collection (used when items found/completion changes)
|
||||||
|
func (ml *MasterList) RefreshCollectionIndices(collection *Collection) {
|
||||||
|
ml.mutex.Lock()
|
||||||
|
defer ml.mutex.Unlock()
|
||||||
|
|
||||||
|
// Remove from old indices
|
||||||
|
ml.updateCollectionIndices(collection, false)
|
||||||
|
// Add to new indices
|
||||||
|
ml.updateCollectionIndices(collection, true)
|
||||||
|
|
||||||
|
// Invalidate metadata cache
|
||||||
|
ml.metaStale = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// ForEach executes a function for each collection
|
||||||
|
func (ml *MasterList) ForEach(fn func(int32, *Collection)) {
|
||||||
|
ml.mutex.RLock()
|
||||||
|
defer ml.mutex.RUnlock()
|
||||||
|
|
||||||
|
for id, collection := range ml.collections {
|
||||||
|
fn(id, collection)
|
||||||
}
|
}
|
||||||
return categories
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ValidateCollections checks all collections for consistency
|
// ValidateCollections checks all collections for consistency
|
||||||
func (ml *MasterList) ValidateCollections() []string {
|
func (ml *MasterList) ValidateCollections() []string {
|
||||||
|
ml.mutex.RLock()
|
||||||
|
defer ml.mutex.RUnlock()
|
||||||
|
|
||||||
var issues []string
|
var issues []string
|
||||||
|
|
||||||
ml.ForEach(func(id int32, collection *Collection) {
|
for id, collection := range ml.collections {
|
||||||
if collection == nil {
|
if collection == nil {
|
||||||
issues = append(issues, fmt.Sprintf("Collection ID %d is nil", id))
|
issues = append(issues, fmt.Sprintf("Collection ID %d is nil", id))
|
||||||
return
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if collection.GetID() != id {
|
if collection.GetID() != id {
|
||||||
@ -140,6 +528,14 @@ func (ml *MasterList) ValidateCollections() []string {
|
|||||||
issues = append(issues, fmt.Sprintf("Collection ID %d has no collection items", id))
|
issues = append(issues, fmt.Sprintf("Collection ID %d has no collection items", id))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(collection.GetName()) > MaxCollectionNameLength {
|
||||||
|
issues = append(issues, fmt.Sprintf("Collection ID %d name too long: %d > %d", id, len(collection.GetName()), MaxCollectionNameLength))
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(collection.GetCategory()) > MaxCollectionCategoryLength {
|
||||||
|
issues = append(issues, fmt.Sprintf("Collection ID %d category too long: %d > %d", id, len(collection.GetCategory()), MaxCollectionCategoryLength))
|
||||||
|
}
|
||||||
|
|
||||||
// Check for duplicate item indices
|
// Check for duplicate item indices
|
||||||
indexMap := make(map[int8]bool)
|
indexMap := make(map[int8]bool)
|
||||||
for _, item := range collection.CollectionItems {
|
for _, item := range collection.CollectionItems {
|
||||||
@ -148,7 +544,7 @@ func (ml *MasterList) ValidateCollections() []string {
|
|||||||
}
|
}
|
||||||
indexMap[item.Index] = true
|
indexMap[item.Index] = true
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
|
|
||||||
return issues
|
return issues
|
||||||
}
|
}
|
||||||
@ -159,27 +555,40 @@ func (ml *MasterList) IsValid() bool {
|
|||||||
return len(issues) == 0
|
return len(issues) == 0
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetStatistics returns statistics about the collection collection
|
// GetStatistics returns statistics about the collection collection using cached data
|
||||||
func (ml *MasterList) GetStatistics() map[string]any {
|
func (ml *MasterList) GetStatistics() map[string]any {
|
||||||
stats := make(map[string]any)
|
ml.mutex.Lock() // Need write lock to potentially update cache
|
||||||
stats["total_collections"] = ml.Size()
|
defer ml.mutex.Unlock()
|
||||||
|
|
||||||
if ml.IsEmpty() {
|
ml.refreshMetaCache()
|
||||||
|
|
||||||
|
stats := make(map[string]any)
|
||||||
|
stats["total_collections"] = len(ml.collections)
|
||||||
|
|
||||||
|
if len(ml.collections) == 0 {
|
||||||
return stats
|
return stats
|
||||||
}
|
}
|
||||||
|
|
||||||
// Count by category
|
// Use cached category stats
|
||||||
categoryCounts := make(map[string]int)
|
stats["collections_by_category"] = ml.categoryStats
|
||||||
|
|
||||||
|
// Calculate additional stats
|
||||||
var totalItems, totalRewards int
|
var totalItems, totalRewards int
|
||||||
|
var completedCount, readyCount int
|
||||||
var minLevel, maxLevel int8 = 127, 0
|
var minLevel, maxLevel int8 = 127, 0
|
||||||
var minID, maxID int32
|
var minID, maxID int32
|
||||||
first := true
|
first := true
|
||||||
|
|
||||||
ml.ForEach(func(id int32, collection *Collection) {
|
for id, collection := range ml.collections {
|
||||||
categoryCounts[collection.GetCategory()]++
|
|
||||||
totalItems += len(collection.CollectionItems)
|
totalItems += len(collection.CollectionItems)
|
||||||
totalRewards += len(collection.RewardItems) + len(collection.SelectableRewardItems)
|
totalRewards += len(collection.RewardItems) + len(collection.SelectableRewardItems)
|
||||||
|
|
||||||
|
if collection.Completed {
|
||||||
|
completedCount++
|
||||||
|
} else if collection.GetIsReadyToTurnIn() {
|
||||||
|
readyCount++
|
||||||
|
}
|
||||||
|
|
||||||
level := collection.GetLevel()
|
level := collection.GetLevel()
|
||||||
if level < minLevel {
|
if level < minLevel {
|
||||||
minLevel = level
|
minLevel = level
|
||||||
@ -200,17 +609,18 @@ func (ml *MasterList) GetStatistics() map[string]any {
|
|||||||
maxID = id
|
maxID = id
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
|
|
||||||
stats["collections_by_category"] = categoryCounts
|
|
||||||
stats["total_collection_items"] = totalItems
|
stats["total_collection_items"] = totalItems
|
||||||
stats["total_rewards"] = totalRewards
|
stats["total_rewards"] = totalRewards
|
||||||
|
stats["completed_collections"] = completedCount
|
||||||
|
stats["ready_to_turn_in"] = readyCount
|
||||||
stats["min_level"] = minLevel
|
stats["min_level"] = minLevel
|
||||||
stats["max_level"] = maxLevel
|
stats["max_level"] = maxLevel
|
||||||
stats["min_id"] = minID
|
stats["min_id"] = minID
|
||||||
stats["max_id"] = maxID
|
stats["max_id"] = maxID
|
||||||
stats["id_range"] = maxID - minID
|
stats["id_range"] = maxID - minID
|
||||||
stats["average_items_per_collection"] = float64(totalItems) / float64(ml.Size())
|
stats["average_items_per_collection"] = float64(totalItems) / float64(len(ml.collections))
|
||||||
|
|
||||||
return stats
|
return stats
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package collections
|
package collections
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"eq2emu/internal/database"
|
"eq2emu/internal/database"
|
||||||
@ -362,4 +363,211 @@ func TestMasterListStatistics(t *testing.T) {
|
|||||||
if avgItems, ok := stats["average_items_per_collection"].(float64); !ok || avgItems != float64(4)/3 {
|
if avgItems, ok := stats["average_items_per_collection"].(float64); !ok || avgItems != float64(4)/3 {
|
||||||
t.Errorf("average_items_per_collection = %v, want %v", avgItems, float64(4)/3)
|
t.Errorf("average_items_per_collection = %v, want %v", avgItems, float64(4)/3)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMasterListBespokeFeatures(t *testing.T) {
|
||||||
|
db, _ := database.NewSQLite("file::memory:?mode=memory&cache=shared")
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
masterList := NewMasterList()
|
||||||
|
|
||||||
|
// Create collections with different properties
|
||||||
|
col1 := NewWithData(101, "Heritage Quest", "Heritage", 10, db)
|
||||||
|
col1.CollectionItems = []CollectionItem{
|
||||||
|
{ItemID: 1001, Index: 0, Found: ItemNotFound},
|
||||||
|
{ItemID: 1002, Index: 1, Found: ItemFound},
|
||||||
|
}
|
||||||
|
col1.Completed = false
|
||||||
|
|
||||||
|
col2 := NewWithData(102, "Treasured Quest", "Treasured", 20, db)
|
||||||
|
col2.CollectionItems = []CollectionItem{
|
||||||
|
{ItemID: 1003, Index: 0, Found: ItemFound},
|
||||||
|
{ItemID: 1004, Index: 1, Found: ItemFound},
|
||||||
|
}
|
||||||
|
col2.Completed = true
|
||||||
|
|
||||||
|
col3 := NewWithData(103, "Legendary Quest", "Legendary", 10, db)
|
||||||
|
col3.CollectionItems = []CollectionItem{
|
||||||
|
{ItemID: 1001, Index: 0, Found: ItemNotFound}, // Same item as col1
|
||||||
|
}
|
||||||
|
col3.Completed = false
|
||||||
|
|
||||||
|
masterList.AddCollection(col1)
|
||||||
|
masterList.AddCollection(col2)
|
||||||
|
masterList.AddCollection(col3)
|
||||||
|
|
||||||
|
// Test GetCollectionsByExactLevel
|
||||||
|
level10Collections := masterList.GetCollectionsByExactLevel(10)
|
||||||
|
if len(level10Collections) != 2 {
|
||||||
|
t.Errorf("GetCollectionsByExactLevel(10) returned %v results, want 2", len(level10Collections))
|
||||||
|
}
|
||||||
|
|
||||||
|
level20Collections := masterList.GetCollectionsByExactLevel(20)
|
||||||
|
if len(level20Collections) != 1 {
|
||||||
|
t.Errorf("GetCollectionsByExactLevel(20) returned %v results, want 1", len(level20Collections))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test GetCollectionByName
|
||||||
|
found := masterList.GetCollectionByName("heritage quest")
|
||||||
|
if found == nil || found.ID != 101 {
|
||||||
|
t.Error("GetCollectionByName should find 'Heritage Quest' (case insensitive)")
|
||||||
|
}
|
||||||
|
|
||||||
|
found = masterList.GetCollectionByName("TREASURED QUEST")
|
||||||
|
if found == nil || found.ID != 102 {
|
||||||
|
t.Error("GetCollectionByName should find 'Treasured Quest' (uppercase)")
|
||||||
|
}
|
||||||
|
|
||||||
|
found = masterList.GetCollectionByName("NonExistent")
|
||||||
|
if found != nil {
|
||||||
|
t.Error("GetCollectionByName should return nil for non-existent collection")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test completion status filtering
|
||||||
|
completedCollections := masterList.GetCompletedCollections()
|
||||||
|
if len(completedCollections) != 1 {
|
||||||
|
t.Errorf("GetCompletedCollections() returned %v results, want 1", len(completedCollections))
|
||||||
|
}
|
||||||
|
|
||||||
|
incompleteCollections := masterList.GetIncompleteCollections()
|
||||||
|
if len(incompleteCollections) != 2 {
|
||||||
|
t.Errorf("GetIncompleteCollections() returned %v results, want 2", len(incompleteCollections))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test GetCollectionsNeedingItem (multiple collections need same item)
|
||||||
|
collectionsNeedingItem := masterList.GetCollectionsNeedingItem(1001)
|
||||||
|
if len(collectionsNeedingItem) != 2 {
|
||||||
|
t.Errorf("GetCollectionsNeedingItem(1001) returned %v results, want 2", len(collectionsNeedingItem))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test GetReadyToTurnInCollections
|
||||||
|
readyCollections := masterList.GetReadyToTurnInCollections()
|
||||||
|
if len(readyCollections) != 0 { // col1 has one item not found, col3 has one item not found
|
||||||
|
t.Errorf("GetReadyToTurnInCollections() returned %v results, want 0", len(readyCollections))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mark col1 as ready to turn in
|
||||||
|
col1.CollectionItems[0].Found = ItemFound
|
||||||
|
masterList.RefreshCollectionIndices(col1)
|
||||||
|
|
||||||
|
readyCollections = masterList.GetReadyToTurnInCollections()
|
||||||
|
if len(readyCollections) != 1 {
|
||||||
|
t.Errorf("GetReadyToTurnInCollections() returned %v results, want 1 after marking items found", len(readyCollections))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test UpdateCollection
|
||||||
|
updatedCol := &Collection{
|
||||||
|
ID: 101,
|
||||||
|
Name: "Updated Heritage Quest",
|
||||||
|
Category: "Updated",
|
||||||
|
Level: 25,
|
||||||
|
db: db,
|
||||||
|
isNew: false,
|
||||||
|
CollectionItems: []CollectionItem{
|
||||||
|
{ItemID: 2001, Index: 0, Found: ItemNotFound},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
err := masterList.UpdateCollection(updatedCol)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("UpdateCollection failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify the update worked
|
||||||
|
retrieved := masterList.GetCollection(101)
|
||||||
|
if retrieved.Name != "Updated Heritage Quest" {
|
||||||
|
t.Errorf("Expected updated name 'Updated Heritage Quest', got '%s'", retrieved.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
if retrieved.Category != "Updated" {
|
||||||
|
t.Errorf("Expected updated category 'Updated', got '%s'", retrieved.Category)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test updating non-existent collection
|
||||||
|
nonExistentCol := &Collection{ID: 9999, Name: "Non-existent", db: db}
|
||||||
|
err = masterList.UpdateCollection(nonExistentCol)
|
||||||
|
if err == nil {
|
||||||
|
t.Error("UpdateCollection should fail for non-existent collection")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test GetLevels and GetItemsNeeded
|
||||||
|
levels := masterList.GetLevels()
|
||||||
|
if len(levels) == 0 {
|
||||||
|
t.Error("GetLevels() should return levels")
|
||||||
|
}
|
||||||
|
|
||||||
|
itemsNeeded := masterList.GetItemsNeeded()
|
||||||
|
if len(itemsNeeded) == 0 {
|
||||||
|
t.Error("GetItemsNeeded() should return items needed")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test GetCollectionClone
|
||||||
|
cloned := masterList.GetCollectionClone(101)
|
||||||
|
if cloned == nil {
|
||||||
|
t.Error("GetCollectionClone should return a clone")
|
||||||
|
}
|
||||||
|
if cloned == retrieved {
|
||||||
|
t.Error("GetCollectionClone should return a different object")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMasterListConcurrency(t *testing.T) {
|
||||||
|
db, _ := database.NewSQLite("file::memory:?mode=memory&cache=shared")
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
masterList := NewMasterList()
|
||||||
|
|
||||||
|
// Add initial collections
|
||||||
|
for i := 1; i <= 100; i++ {
|
||||||
|
col := NewWithData(int32(i), fmt.Sprintf("Collection%d", i), "Heritage", 10, db)
|
||||||
|
col.CollectionItems = []CollectionItem{
|
||||||
|
{ItemID: int32(i + 1000), Index: 0, Found: ItemNotFound},
|
||||||
|
}
|
||||||
|
masterList.AddCollection(col)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test concurrent access
|
||||||
|
done := make(chan bool, 10)
|
||||||
|
|
||||||
|
// Concurrent readers
|
||||||
|
for i := 0; i < 5; i++ {
|
||||||
|
go func() {
|
||||||
|
defer func() { done <- true }()
|
||||||
|
for j := 0; j < 100; j++ {
|
||||||
|
masterList.GetCollection(int32(j%100 + 1))
|
||||||
|
masterList.FindCollectionsByCategory("Heritage")
|
||||||
|
masterList.GetCollectionByName(fmt.Sprintf("collection%d", j%100+1))
|
||||||
|
masterList.NeedsItem(int32(j + 1000))
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Concurrent writers
|
||||||
|
for i := 0; i < 5; i++ {
|
||||||
|
go func(workerID int) {
|
||||||
|
defer func() { done <- true }()
|
||||||
|
for j := 0; j < 10; j++ {
|
||||||
|
colID := int32(workerID*1000 + j + 1)
|
||||||
|
col := NewWithData(colID, fmt.Sprintf("Worker%d-Collection%d", workerID, j), "Treasured", 20, db)
|
||||||
|
col.CollectionItems = []CollectionItem{
|
||||||
|
{ItemID: colID + 10000, Index: 0, Found: ItemNotFound},
|
||||||
|
}
|
||||||
|
masterList.AddCollection(col) // Some may fail due to concurrent additions
|
||||||
|
}
|
||||||
|
}(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for all goroutines
|
||||||
|
for i := 0; i < 10; i++ {
|
||||||
|
<-done
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify final state - should have at least 100 initial collections
|
||||||
|
finalCount := masterList.GetCollectionCount()
|
||||||
|
if finalCount < 100 {
|
||||||
|
t.Errorf("Expected at least 100 collections after concurrent operations, got %d", finalCount)
|
||||||
|
}
|
||||||
|
if finalCount > 150 {
|
||||||
|
t.Errorf("Expected at most 150 collections after concurrent operations, got %d", finalCount)
|
||||||
|
}
|
||||||
}
|
}
|
Loading…
x
Reference in New Issue
Block a user