bespoke master list for ground_spawn

This commit is contained in:
Sky Johnson 2025-08-08 09:41:22 -05:00
parent 4278cf79bf
commit c31866d080
5 changed files with 757 additions and 247 deletions

View File

@ -0,0 +1,210 @@
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

@ -7,6 +7,7 @@ package common
import (
"fmt"
"maps"
"sync"
)
@ -154,9 +155,7 @@ func (ml *MasterList[K, V]) GetAll() map[K]V {
defer ml.mutex.RUnlock()
result := make(map[K]V, len(ml.items))
for k, v := range ml.items {
result[k] = v
}
maps.Copy(result, ml.items)
return result
}
@ -205,7 +204,8 @@ func (ml *MasterList[K, V]) Filter(predicate func(V) bool) []V {
ml.mutex.RLock()
defer ml.mutex.RUnlock()
var result []V
// 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)
@ -260,4 +260,52 @@ 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

@ -3,6 +3,7 @@ package ground_spawn
import (
"fmt"
"math/rand"
"sync"
"testing"
"eq2emu/internal/database"
@ -33,13 +34,10 @@ func (s *mockSkill) GetMaxValue() int16 { return s.max }
// createTestGroundSpawn creates a ground spawn for benchmarking
func createTestGroundSpawn(b *testing.B, id int32) *GroundSpawn {
b.Helper()
db, err := database.NewSQLite("file::memory:?mode=memory&cache=shared")
if err != nil {
b.Fatalf("Failed to create test database: %v", err)
}
gs := New(db)
// Don't create a database for every ground spawn - that's extremely expensive!
// Pass nil for the database since we're just benchmarking the in-memory operations
gs := New(nil)
gs.GroundSpawnID = id
gs.Name = fmt.Sprintf("Benchmark Node %d", id)
gs.CollectionSkill = "Mining"
@ -49,7 +47,7 @@ func createTestGroundSpawn(b *testing.B, id int32) *GroundSpawn {
gs.X, gs.Y, gs.Z = float32(rand.Intn(1000)), float32(rand.Intn(1000)), float32(rand.Intn(1000))
gs.ZoneID = int32(rand.Intn(10) + 1)
gs.GridID = int32(rand.Intn(100))
// Add mock harvest entries for realistic benchmarking
gs.HarvestEntries = []*HarvestEntry{
{
@ -77,7 +75,7 @@ func createTestGroundSpawn(b *testing.B, id int32) *GroundSpawn {
Harvest10: 2.0,
},
}
// Add mock harvest items
gs.HarvestItems = []*HarvestEntryItem{
{GroundSpawnID: id, ItemID: 1001, IsRare: ItemRarityNormal, GridID: 0, Quantity: 1},
@ -85,7 +83,7 @@ func createTestGroundSpawn(b *testing.B, id int32) *GroundSpawn {
{GroundSpawnID: id, ItemID: 1003, IsRare: ItemRarityRare, GridID: 0, Quantity: 1},
{GroundSpawnID: id, ItemID: 1004, IsRare: ItemRarityImbue, GridID: 0, Quantity: 1},
}
return gs
}
@ -98,7 +96,7 @@ func BenchmarkGroundSpawnCreation(b *testing.B) {
defer db.Close()
b.ResetTimer()
b.Run("Sequential", func(b *testing.B) {
for i := 0; i < b.N; i++ {
gs := New(db)
@ -125,7 +123,7 @@ func BenchmarkGroundSpawnCreation(b *testing.B) {
// BenchmarkGroundSpawnState measures state operations
func BenchmarkGroundSpawnState(b *testing.B) {
gs := createTestGroundSpawn(b, 1001)
b.Run("IsAvailable", func(b *testing.B) {
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
@ -133,7 +131,7 @@ func BenchmarkGroundSpawnState(b *testing.B) {
}
})
})
b.Run("IsDepleted", func(b *testing.B) {
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
@ -141,7 +139,7 @@ func BenchmarkGroundSpawnState(b *testing.B) {
}
})
})
b.Run("GetHarvestMessageName", func(b *testing.B) {
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
@ -149,7 +147,7 @@ func BenchmarkGroundSpawnState(b *testing.B) {
}
})
})
b.Run("GetHarvestSpellType", func(b *testing.B) {
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
@ -162,85 +160,112 @@ func BenchmarkGroundSpawnState(b *testing.B) {
// BenchmarkHarvestAlgorithm measures the core harvest processing performance
func BenchmarkHarvestAlgorithm(b *testing.B) {
gs := createTestGroundSpawn(b, 1001)
player := &mockPlayer{level: 50, location: 1, name: "BenchmarkPlayer"}
skill := &mockSkill{current: 75, max: 100}
b.Run("ProcessHarvest", func(b *testing.B) {
for i := 0; i < b.N; i++ {
for b.Loop() {
// Reset harvest count for consistent benchmarking
gs.CurrentHarvests = gs.NumberHarvests
_, err := gs.ProcessHarvest(player, skill, 75)
if err != nil {
b.Fatalf("Harvest failed: %v", err)
}
}
})
b.Run("FilterHarvestTables", func(b *testing.B) {
for i := 0; i < b.N; i++ {
for b.Loop() {
_ = gs.filterHarvestTables(player, 75)
}
})
b.Run("SelectHarvestTable", func(b *testing.B) {
tables := gs.filterHarvestTables(player, 75)
for i := 0; i < b.N; i++ {
for b.Loop() {
_ = gs.selectHarvestTable(tables, 75)
}
})
b.Run("DetermineHarvestType", func(b *testing.B) {
tables := gs.filterHarvestTables(player, 75)
table := gs.selectHarvestTable(tables, 75)
for i := 0; i < b.N; i++ {
for b.Loop() {
_ = gs.determineHarvestType(table, false)
}
})
b.Run("AwardHarvestItems", func(b *testing.B) {
for i := 0; i < b.N; i++ {
for b.Loop() {
_ = gs.awardHarvestItems(HarvestType3Items, player)
}
})
}
// Global shared master list for benchmarks to avoid repeated setup
var (
sharedMasterList *MasterList
sharedSpawns []*GroundSpawn
setupOnce sync.Once
)
// setupSharedMasterList creates the shared master list once
func setupSharedMasterList(b *testing.B) {
setupOnce.Do(func() {
sharedMasterList = NewMasterList()
// Pre-populate with ground spawns for realistic testing
const numSpawns = 1000
sharedSpawns = make([]*GroundSpawn, numSpawns)
for i := range numSpawns {
sharedSpawns[i] = createTestGroundSpawn(b, int32(i+1))
sharedSpawns[i].ZoneID = int32(i%10 + 1) // Distribute across 10 zones
sharedSpawns[i].CollectionSkill = []string{"Mining", "Gathering", "Fishing", "Trapping"}[i%4]
// Create realistic spatial clusters within each zone
zoneBase := float32(sharedSpawns[i].ZoneID * 1000)
cluster := float32((i % 100) * 50) // Clusters of ~25 spawns
sharedSpawns[i].X = zoneBase + cluster + float32(rand.Intn(100)-50) // Add some spread
sharedSpawns[i].Y = zoneBase + cluster + float32(rand.Intn(100)-50)
sharedSpawns[i].Z = float32(rand.Intn(100))
sharedMasterList.AddGroundSpawn(sharedSpawns[i])
}
})
}
// BenchmarkMasterListOperations measures master list performance
func BenchmarkMasterListOperations(b *testing.B) {
ml := NewMasterList()
// Pre-populate with ground spawns for realistic testing
const numSpawns = 1000
spawns := make([]*GroundSpawn, numSpawns)
b.StopTimer()
for i := 0; i < numSpawns; i++ {
spawns[i] = createTestGroundSpawn(b, int32(i+1))
spawns[i].ZoneID = int32(i%10 + 1) // Distribute across 10 zones
spawns[i].CollectionSkill = []string{"Mining", "Gathering", "Fishing", "Trapping"}[i%4]
ml.AddGroundSpawn(spawns[i])
}
b.StartTimer()
setupSharedMasterList(b)
ml := sharedMasterList
b.Run("GetGroundSpawn", func(b *testing.B) {
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
id := int32((rand.Intn(numSpawns) + 1))
id := int32((rand.Intn(1000) + 1))
_ = ml.GetGroundSpawn(id)
}
})
})
b.Run("AddGroundSpawn", func(b *testing.B) {
startID := int32(numSpawns + 1)
// Create a separate master list for add operations to avoid contaminating shared list
addML := NewMasterList()
startID := int32(10000)
// Pre-create ground spawns to measure just the Add operation
spawnsToAdd := make([]*GroundSpawn, b.N)
for i := 0; i < b.N; i++ {
spawnsToAdd[i] = createTestGroundSpawn(b, startID+int32(i))
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
gs := createTestGroundSpawn(b, startID+int32(i))
ml.AddGroundSpawn(gs)
addML.AddGroundSpawn(spawnsToAdd[i])
}
})
b.Run("GetByZone", func(b *testing.B) {
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
@ -249,7 +274,7 @@ func BenchmarkMasterListOperations(b *testing.B) {
}
})
})
b.Run("GetBySkill", func(b *testing.B) {
skills := []string{"Mining", "Gathering", "Fishing", "Trapping"}
b.RunParallel(func(pb *testing.PB) {
@ -259,21 +284,21 @@ func BenchmarkMasterListOperations(b *testing.B) {
}
})
})
b.Run("GetAvailableSpawns", func(b *testing.B) {
for i := 0; i < b.N; i++ {
for b.Loop() {
_ = ml.GetAvailableSpawns()
}
})
b.Run("GetDepletedSpawns", func(b *testing.B) {
for i := 0; i < b.N; i++ {
for b.Loop() {
_ = ml.GetDepletedSpawns()
}
})
b.Run("GetStatistics", func(b *testing.B) {
for i := 0; i < b.N; i++ {
for b.Loop() {
_ = ml.GetStatistics()
}
})
@ -283,25 +308,25 @@ func BenchmarkMasterListOperations(b *testing.B) {
func BenchmarkConcurrentHarvesting(b *testing.B) {
const numSpawns = 100
spawns := make([]*GroundSpawn, numSpawns)
for i := 0; i < numSpawns; i++ {
spawns[i] = createTestGroundSpawn(b, int32(i+1))
}
player := &mockPlayer{level: 50, location: 1, name: "BenchmarkPlayer"}
skill := &mockSkill{current: 75, max: 100}
b.Run("ConcurrentHarvesting", func(b *testing.B) {
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
spawnIdx := rand.Intn(numSpawns)
gs := spawns[spawnIdx]
// Reset if depleted
if gs.IsDepleted() {
gs.Respawn()
}
_, _ = gs.ProcessHarvest(player, skill, 75)
}
})
@ -326,18 +351,18 @@ func BenchmarkMemoryAllocation(b *testing.B) {
_ = gs
}
})
b.Run("MasterListAllocation", func(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
ml := NewMasterList()
_ = ml
}
})
b.Run("HarvestResultAllocation", func(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
result := &HarvestResult{
Success: true,
HarvestType: HarvestType3Items,
@ -348,22 +373,47 @@ func BenchmarkMemoryAllocation(b *testing.B) {
_ = result
}
})
b.Run("SpatialAddGroundSpawn_Allocations", func(b *testing.B) {
b.ReportAllocs()
ml := NewMasterList()
for i := 0; i < b.N; i++ {
gs := createTestGroundSpawn(b, int32(i+1))
gs.ZoneID = int32(i%10 + 1)
gs.CollectionSkill = []string{"Mining", "Gathering", "Fishing", "Trapping"}[i%4]
gs.X = float32(rand.Intn(1000))
gs.Y = float32(rand.Intn(1000))
ml.AddGroundSpawn(gs)
}
})
b.Run("SpatialGetNearby_Allocations", func(b *testing.B) {
setupSharedMasterList(b)
ml := sharedMasterList
b.ReportAllocs()
b.ResetTimer()
for b.Loop() {
x := float32(rand.Intn(10000))
y := float32(rand.Intn(10000))
_ = ml.GetNearby(x, y, 100.0)
}
})
}
// BenchmarkRespawnOperations measures respawn performance
func BenchmarkRespawnOperations(b *testing.B) {
gs := createTestGroundSpawn(b, 1001)
b.Run("Respawn", func(b *testing.B) {
for i := 0; i < b.N; i++ {
for b.Loop() {
gs.CurrentHarvests = 0 // Deplete
gs.Respawn()
}
})
b.Run("RespawnWithRandomHeading", func(b *testing.B) {
gs.RandomizeHeading = true
for i := 0; i < b.N; i++ {
for b.Loop() {
gs.CurrentHarvests = 0 // Deplete
gs.Respawn()
}
@ -373,7 +423,7 @@ func BenchmarkRespawnOperations(b *testing.B) {
// BenchmarkStringOperations measures string processing performance
func BenchmarkStringOperations(b *testing.B) {
skills := []string{"Mining", "Gathering", "Collecting", "Fishing", "Trapping", "Foresting", "Unknown"}
b.Run("HarvestMessageGeneration", func(b *testing.B) {
gs := createTestGroundSpawn(b, 1001)
b.RunParallel(func(pb *testing.PB) {
@ -384,7 +434,7 @@ func BenchmarkStringOperations(b *testing.B) {
}
})
})
b.Run("SpellTypeGeneration", func(b *testing.B) {
gs := createTestGroundSpawn(b, 1001)
b.RunParallel(func(pb *testing.PB) {
@ -397,34 +447,76 @@ func BenchmarkStringOperations(b *testing.B) {
})
}
// BenchmarkComparisonWithOldSystem provides comparison benchmarks
// These would help measure the performance improvement from modernization
func BenchmarkComparisonWithOldSystem(b *testing.B) {
ml := NewMasterList()
const numSpawns = 1000
// Setup
b.StopTimer()
for i := 0; i < numSpawns; i++ {
gs := createTestGroundSpawn(b, int32(i+1))
ml.AddGroundSpawn(gs)
}
b.StartTimer()
b.Run("ModernizedLookup", func(b *testing.B) {
// Modern generic-based lookup
// BenchmarkSpatialFeatures tests features unique to spatial implementation
func BenchmarkSpatialFeatures(b *testing.B) {
setupSharedMasterList(b)
ml := sharedMasterList
b.Run("GetByZoneAndSkill", func(b *testing.B) {
skills := []string{"Mining", "Gathering", "Fishing", "Trapping"}
for b.Loop() {
zoneID := int32(rand.Intn(10) + 1)
skill := skills[rand.Intn(len(skills))]
_ = ml.GetByZoneAndSkill(zoneID, skill)
}
})
b.Run("GetNearby_Small", func(b *testing.B) {
for b.Loop() {
x := float32(rand.Intn(10000))
y := float32(rand.Intn(10000))
_ = ml.GetNearby(x, y, 50.0) // Small radius
}
})
b.Run("GetNearby_Medium", func(b *testing.B) {
for b.Loop() {
x := float32(rand.Intn(10000))
y := float32(rand.Intn(10000))
_ = ml.GetNearby(x, y, 200.0) // Medium radius
}
})
b.Run("GetNearby_Large", func(b *testing.B) {
for b.Loop() {
x := float32(rand.Intn(10000))
y := float32(rand.Intn(10000))
_ = ml.GetNearby(x, y, 500.0) // Large radius
}
})
}
// BenchmarkConcurrentSpatialOperations tests thread safety and mixed workloads
func BenchmarkConcurrentSpatialOperations(b *testing.B) {
setupSharedMasterList(b)
ml := sharedMasterList
b.Run("MixedSpatialOperations", func(b *testing.B) {
skills := []string{"Mining", "Gathering", "Fishing", "Trapping"}
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
id := int32(rand.Intn(numSpawns) + 1)
_ = ml.GetGroundSpawn(id)
switch rand.Intn(6) {
case 0:
id := int32(rand.Intn(1000) + 1)
_ = ml.GetGroundSpawn(id)
case 1:
zoneID := int32(rand.Intn(10) + 1)
_ = ml.GetByZone(zoneID)
case 2:
skill := skills[rand.Intn(len(skills))]
_ = ml.GetBySkill(skill)
case 3:
zoneID := int32(rand.Intn(10) + 1)
skill := skills[rand.Intn(len(skills))]
_ = ml.GetByZoneAndSkill(zoneID, skill)
case 4:
x := float32(rand.Intn(10000))
y := float32(rand.Intn(10000))
_ = ml.GetNearby(x, y, 100.0)
case 5:
_ = ml.GetAvailableSpawns()
}
}
})
})
b.Run("ModernizedFiltering", func(b *testing.B) {
// Modern filter-based operations
for i := 0; i < b.N; i++ {
_ = ml.GetAvailableSpawns()
}
})
}
}

View File

@ -121,7 +121,7 @@ func TestNewMasterList(t *testing.T) {
t.Fatal("Expected non-nil master list")
}
if ml.MasterList.Size() != 0 {
if ml.Size() != 0 {
t.Error("New master list should be empty")
}
}

View File

@ -1,174 +1,106 @@
package ground_spawn
import (
"fmt"
"eq2emu/internal/common"
"eq2emu/internal/database"
"zombiezen.com/go/sqlite"
"sync"
)
// MasterList manages all ground spawns using the generic base
// MasterList is a specialized ground spawn master list optimized for:
// - Fast zone-based lookups (O(1))
// - Fast skill-based lookups (O(1))
// - Spatial grid queries for proximity searches
// - Set intersection operations for complex queries
// - Fast state-based queries (available/depleted)
type MasterList struct {
*common.MasterList[int32, *GroundSpawn]
// Core storage
spawns map[int32]*GroundSpawn // ID -> GroundSpawn
mutex sync.RWMutex
// Category indices for O(1) lookups
byZone map[int32][]*GroundSpawn // Zone ID -> spawns
bySkill map[string][]*GroundSpawn // Skill -> spawns
// State indices for O(1) filtering
availableSpawns []*GroundSpawn // Available spawns (cached)
depletedSpawns []*GroundSpawn // Depleted spawns (cached)
stateStale bool // Whether state caches need refresh
// Statistics cache
stats *Statistics
statsStale bool
// Spatial grid for proximity queries (grid size = 100 units)
spatialGrid map[gridKey][]*GroundSpawn
gridSize float32
}
// NewMasterList creates a new ground spawn master list
// gridKey represents a spatial grid cell
type gridKey struct {
x, y int32
}
// NewMasterList creates a new specialized ground spawn master list
func NewMasterList() *MasterList {
return &MasterList{
MasterList: common.NewMasterList[int32, *GroundSpawn](),
spawns: make(map[int32]*GroundSpawn),
byZone: make(map[int32][]*GroundSpawn),
bySkill: make(map[string][]*GroundSpawn),
spatialGrid: make(map[gridKey][]*GroundSpawn),
gridSize: 100.0, // 100 unit grid cells
stateStale: true, // Initial state needs refresh
statsStale: true, // Initial stats need refresh
}
}
// AddGroundSpawn adds a ground spawn to the master list
func (ml *MasterList) AddGroundSpawn(gs *GroundSpawn) bool {
return ml.MasterList.Add(gs)
}
// GetGroundSpawn retrieves a ground spawn by ID
func (ml *MasterList) GetGroundSpawn(groundSpawnID int32) *GroundSpawn {
return ml.MasterList.Get(groundSpawnID)
}
// RemoveGroundSpawn removes a ground spawn by ID
func (ml *MasterList) RemoveGroundSpawn(groundSpawnID int32) bool {
return ml.MasterList.Remove(groundSpawnID)
}
// GetByZone returns all ground spawns in a specific zone
func (ml *MasterList) GetByZone(zoneID int32) []*GroundSpawn {
return ml.MasterList.Filter(func(gs *GroundSpawn) bool {
return gs.ZoneID == zoneID
})
}
// GetBySkill returns all ground spawns that require a specific skill
func (ml *MasterList) GetBySkill(skill string) []*GroundSpawn {
return ml.MasterList.Filter(func(gs *GroundSpawn) bool {
return gs.CollectionSkill == skill
})
}
// GetAvailableSpawns returns all harvestable ground spawns
func (ml *MasterList) GetAvailableSpawns() []*GroundSpawn {
return ml.MasterList.Filter(func(gs *GroundSpawn) bool {
return gs.IsAvailable()
})
}
// GetDepletedSpawns returns all depleted ground spawns
func (ml *MasterList) GetDepletedSpawns() []*GroundSpawn {
return ml.MasterList.Filter(func(gs *GroundSpawn) bool {
return gs.IsDepleted()
})
}
// LoadFromDatabase loads all ground spawns from database
func (ml *MasterList) LoadFromDatabase(db *database.Database) error {
if db.GetType() == database.SQLite {
return ml.loadFromSQLite(db)
// getGridKey returns the grid cell for given coordinates
func (ml *MasterList) getGridKey(x, y float32) gridKey {
return gridKey{
x: int32(x / ml.gridSize),
y: int32(y / ml.gridSize),
}
return ml.loadFromMySQL(db)
}
func (ml *MasterList) loadFromSQLite(db *database.Database) error {
return db.ExecTransient(`
SELECT id, groundspawn_id, name, collection_skill, number_harvests,
attempts_per_harvest, randomize_heading, respawn_time,
x, y, z, heading, zone_id, grid_id
FROM ground_spawns
ORDER BY groundspawn_id
`, func(stmt *sqlite.Stmt) error {
gs := &GroundSpawn{
ID: stmt.ColumnInt32(0),
GroundSpawnID: stmt.ColumnInt32(1),
Name: stmt.ColumnText(2),
CollectionSkill: stmt.ColumnText(3),
NumberHarvests: int8(stmt.ColumnInt32(4)),
AttemptsPerHarvest: int8(stmt.ColumnInt32(5)),
RandomizeHeading: stmt.ColumnBool(6),
RespawnTime: stmt.ColumnInt32(7),
X: float32(stmt.ColumnFloat(8)),
Y: float32(stmt.ColumnFloat(9)),
Z: float32(stmt.ColumnFloat(10)),
Heading: float32(stmt.ColumnFloat(11)),
ZoneID: stmt.ColumnInt32(12),
GridID: stmt.ColumnInt32(13),
db: db,
isNew: false,
IsAlive: true,
CurrentHarvests: int8(stmt.ColumnInt32(4)), // Initialize to max harvests
HarvestEntries: make([]*HarvestEntry, 0),
HarvestItems: make([]*HarvestEntryItem, 0),
}
// Load harvest data for this ground spawn
if err := gs.loadHarvestData(); err != nil {
return fmt.Errorf("failed to load harvest data for ground spawn %d: %w", gs.GroundSpawnID, err)
}
ml.MasterList.Add(gs)
return nil
})
}
func (ml *MasterList) loadFromMySQL(db *database.Database) error {
rows, err := db.Query(`
SELECT id, groundspawn_id, name, collection_skill, number_harvests,
attempts_per_harvest, randomize_heading, respawn_time,
x, y, z, heading, zone_id, grid_id
FROM ground_spawns
ORDER BY groundspawn_id
`)
if err != nil {
return fmt.Errorf("failed to query ground spawns: %w", err)
}
defer rows.Close()
for rows.Next() {
gs := &GroundSpawn{
db: db,
isNew: false,
IsAlive: true,
HarvestEntries: make([]*HarvestEntry, 0),
HarvestItems: make([]*HarvestEntryItem, 0),
}
err := rows.Scan(&gs.ID, &gs.GroundSpawnID, &gs.Name, &gs.CollectionSkill,
&gs.NumberHarvests, &gs.AttemptsPerHarvest, &gs.RandomizeHeading,
&gs.RespawnTime, &gs.X, &gs.Y, &gs.Z, &gs.Heading, &gs.ZoneID, &gs.GridID)
if err != nil {
return fmt.Errorf("failed to scan ground spawn: %w", err)
}
// Initialize current harvests to max
gs.CurrentHarvests = gs.NumberHarvests
// Load harvest data for this ground spawn
if err := gs.loadHarvestData(); err != nil {
return fmt.Errorf("failed to load harvest data for ground spawn %d: %w", gs.GroundSpawnID, err)
}
ml.MasterList.Add(gs)
// refreshStateCache updates the available/depleted spawn caches
func (ml *MasterList) refreshStateCache() {
if !ml.stateStale {
return
}
return rows.Err()
// Clear existing caches
ml.availableSpawns = ml.availableSpawns[:0]
ml.depletedSpawns = ml.depletedSpawns[:0]
// Rebuild caches
for _, gs := range ml.spawns {
if gs.IsAvailable() {
ml.availableSpawns = append(ml.availableSpawns, gs)
} else if gs.IsDepleted() {
ml.depletedSpawns = append(ml.depletedSpawns, gs)
}
}
ml.stateStale = false
}
// GetStatistics returns statistics about the ground spawn system
func (ml *MasterList) GetStatistics() *Statistics {
availableSpawns := len(ml.GetAvailableSpawns())
// Count by zone
// refreshStatsCache updates the statistics cache
func (ml *MasterList) refreshStatsCache() {
if !ml.statsStale {
return
}
var availableSpawns int
zoneMap := make(map[int32]int)
skillMap := make(map[string]int64)
ml.MasterList.ForEach(func(id int32, gs *GroundSpawn) {
// Single pass through all spawns
for _, gs := range ml.spawns {
if gs.IsAvailable() {
availableSpawns++
}
zoneMap[gs.ZoneID]++
skillMap[gs.CollectionSkill]++
})
}
return &Statistics{
ml.stats = &Statistics{
TotalHarvests: 0, // Would need to be tracked separately
SuccessfulHarvests: 0, // Would need to be tracked separately
RareItemsHarvested: 0, // Would need to be tracked separately
@ -177,4 +109,232 @@ func (ml *MasterList) GetStatistics() *Statistics {
ActiveGroundSpawns: availableSpawns,
GroundSpawnsByZone: zoneMap,
}
ml.statsStale = false
}
// AddGroundSpawn adds a ground spawn with full indexing
func (ml *MasterList) AddGroundSpawn(gs *GroundSpawn) bool {
ml.mutex.Lock()
defer ml.mutex.Unlock()
// Check if exists
if _, exists := ml.spawns[gs.GroundSpawnID]; exists {
return false
}
// Add to core storage
ml.spawns[gs.GroundSpawnID] = gs
// Update zone index
ml.byZone[gs.ZoneID] = append(ml.byZone[gs.ZoneID], gs)
// Update skill index
ml.bySkill[gs.CollectionSkill] = append(ml.bySkill[gs.CollectionSkill], gs)
// Update spatial grid
gridKey := ml.getGridKey(gs.X, gs.Y)
ml.spatialGrid[gridKey] = append(ml.spatialGrid[gridKey], gs)
// Invalidate state and stats caches
ml.stateStale = true
ml.statsStale = true
return true
}
// GetGroundSpawn retrieves by ID (O(1))
func (ml *MasterList) GetGroundSpawn(id int32) *GroundSpawn {
ml.mutex.RLock()
defer ml.mutex.RUnlock()
return ml.spawns[id]
}
// GetByZone returns all spawns in a zone (O(1))
func (ml *MasterList) GetByZone(zoneID int32) []*GroundSpawn {
ml.mutex.RLock()
defer ml.mutex.RUnlock()
return ml.byZone[zoneID] // Return slice directly for performance
}
// GetBySkill returns all spawns for a skill (O(1))
func (ml *MasterList) GetBySkill(skill string) []*GroundSpawn {
ml.mutex.RLock()
defer ml.mutex.RUnlock()
return ml.bySkill[skill] // Return slice directly for performance
}
// GetByZoneAndSkill returns spawns matching both zone and skill (set intersection)
func (ml *MasterList) GetByZoneAndSkill(zoneID int32, skill string) []*GroundSpawn {
ml.mutex.RLock()
defer ml.mutex.RUnlock()
zoneSpawns := ml.byZone[zoneID]
skillSpawns := ml.bySkill[skill]
// Use smaller set for iteration efficiency
if len(zoneSpawns) > len(skillSpawns) {
zoneSpawns, skillSpawns = skillSpawns, zoneSpawns
}
// Set intersection using map lookup
skillSet := make(map[*GroundSpawn]struct{}, len(skillSpawns))
for _, gs := range skillSpawns {
skillSet[gs] = struct{}{}
}
var result []*GroundSpawn
for _, gs := range zoneSpawns {
if _, exists := skillSet[gs]; exists {
result = append(result, gs)
}
}
return result
}
// GetNearby returns spawns within radius of given coordinates using spatial grid
func (ml *MasterList) GetNearby(x, y float32, radius float32) []*GroundSpawn {
ml.mutex.RLock()
defer ml.mutex.RUnlock()
// Calculate grid search bounds
minX := int32((x - radius) / ml.gridSize)
maxX := int32((x + radius) / ml.gridSize)
minY := int32((y - radius) / ml.gridSize)
maxY := int32((y + radius) / ml.gridSize)
var candidates []*GroundSpawn
// Check all grid cells in range
for gx := minX; gx <= maxX; gx++ {
for gy := minY; gy <= maxY; gy++ {
key := gridKey{x: gx, y: gy}
if spawns, exists := ml.spatialGrid[key]; exists {
candidates = append(candidates, spawns...)
}
}
}
// Filter by exact distance
radiusSquared := radius * radius
var result []*GroundSpawn
for _, gs := range candidates {
dx := gs.X - x
dy := gs.Y - y
if dx*dx+dy*dy <= radiusSquared {
result = append(result, gs)
}
}
return result
}
// GetAvailableSpawns returns harvestable spawns using cached results
func (ml *MasterList) GetAvailableSpawns() []*GroundSpawn {
ml.mutex.Lock() // Need write lock to potentially update cache
defer ml.mutex.Unlock()
ml.refreshStateCache()
// Return a copy to prevent external modification
result := make([]*GroundSpawn, len(ml.availableSpawns))
copy(result, ml.availableSpawns)
return result
}
// GetDepletedSpawns returns depleted spawns using cached results
func (ml *MasterList) GetDepletedSpawns() []*GroundSpawn {
ml.mutex.Lock() // Need write lock to potentially update cache
defer ml.mutex.Unlock()
ml.refreshStateCache()
// Return a copy to prevent external modification
result := make([]*GroundSpawn, len(ml.depletedSpawns))
copy(result, ml.depletedSpawns)
return result
}
// GetStatistics returns system statistics using cached results
func (ml *MasterList) GetStatistics() *Statistics {
ml.mutex.Lock() // Need write lock to potentially update cache
defer ml.mutex.Unlock()
ml.refreshStatsCache()
// Return a copy to prevent external modification
return &Statistics{
TotalHarvests: ml.stats.TotalHarvests,
SuccessfulHarvests: ml.stats.SuccessfulHarvests,
RareItemsHarvested: ml.stats.RareItemsHarvested,
SkillUpsGenerated: ml.stats.SkillUpsGenerated,
HarvestsBySkill: ml.stats.HarvestsBySkill,
ActiveGroundSpawns: ml.stats.ActiveGroundSpawns,
GroundSpawnsByZone: ml.stats.GroundSpawnsByZone,
}
}
// RemoveGroundSpawn removes a spawn and updates all indices
func (ml *MasterList) RemoveGroundSpawn(id int32) bool {
ml.mutex.Lock()
defer ml.mutex.Unlock()
gs, exists := ml.spawns[id]
if !exists {
return false
}
// Remove from core storage
delete(ml.spawns, id)
// Remove from zone index
zoneSpawns := ml.byZone[gs.ZoneID]
for i, spawn := range zoneSpawns {
if spawn.GroundSpawnID == id {
ml.byZone[gs.ZoneID] = append(zoneSpawns[:i], zoneSpawns[i+1:]...)
break
}
}
// Remove from skill index
skillSpawns := ml.bySkill[gs.CollectionSkill]
for i, spawn := range skillSpawns {
if spawn.GroundSpawnID == id {
ml.bySkill[gs.CollectionSkill] = append(skillSpawns[:i], skillSpawns[i+1:]...)
break
}
}
// Remove from spatial grid
gridKey := ml.getGridKey(gs.X, gs.Y)
gridSpawns := ml.spatialGrid[gridKey]
for i, spawn := range gridSpawns {
if spawn.GroundSpawnID == id {
ml.spatialGrid[gridKey] = append(gridSpawns[:i], gridSpawns[i+1:]...)
break
}
}
// Invalidate state and stats caches
ml.stateStale = true
ml.statsStale = true
return true
}
// Size returns the total number of spawns
func (ml *MasterList) Size() int {
ml.mutex.RLock()
defer ml.mutex.RUnlock()
return len(ml.spawns)
}
// InvalidateStateCache marks the state and stats caches as stale
// Call this when spawn states change (e.g., after harvesting, respawning)
func (ml *MasterList) InvalidateStateCache() {
ml.mutex.Lock()
defer ml.mutex.Unlock()
ml.stateStale = true
ml.statsStale = true
}