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 ( import (
"fmt" "fmt"
"maps"
"sync" "sync"
) )
@ -154,9 +155,7 @@ func (ml *MasterList[K, V]) GetAll() map[K]V {
defer ml.mutex.RUnlock() defer ml.mutex.RUnlock()
result := make(map[K]V, len(ml.items)) result := make(map[K]V, len(ml.items))
for k, v := range ml.items { maps.Copy(result, ml.items)
result[k] = v
}
return result return result
} }
@ -205,7 +204,8 @@ func (ml *MasterList[K, V]) Filter(predicate func(V) bool) []V {
ml.mutex.RLock() ml.mutex.RLock()
defer ml.mutex.RUnlock() 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 { for _, v := range ml.items {
if predicate(v) { if predicate(v) {
result = append(result, v) result = append(result, v)
@ -261,3 +261,51 @@ func (ml *MasterList[K, V]) WithWriteLock(fn func(map[K]V)) {
defer ml.mutex.Unlock() defer ml.mutex.Unlock()
fn(ml.items) 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 ( import (
"fmt" "fmt"
"math/rand" "math/rand"
"sync"
"testing" "testing"
"eq2emu/internal/database" "eq2emu/internal/database"
@ -34,12 +35,9 @@ func (s *mockSkill) GetMaxValue() int16 { return s.max }
func createTestGroundSpawn(b *testing.B, id int32) *GroundSpawn { func createTestGroundSpawn(b *testing.B, id int32) *GroundSpawn {
b.Helper() b.Helper()
db, err := database.NewSQLite("file::memory:?mode=memory&cache=shared") // Don't create a database for every ground spawn - that's extremely expensive!
if err != nil { // Pass nil for the database since we're just benchmarking the in-memory operations
b.Fatalf("Failed to create test database: %v", err) gs := New(nil)
}
gs := New(db)
gs.GroundSpawnID = id gs.GroundSpawnID = id
gs.Name = fmt.Sprintf("Benchmark Node %d", id) gs.Name = fmt.Sprintf("Benchmark Node %d", id)
gs.CollectionSkill = "Mining" gs.CollectionSkill = "Mining"
@ -167,7 +165,7 @@ func BenchmarkHarvestAlgorithm(b *testing.B) {
skill := &mockSkill{current: 75, max: 100} skill := &mockSkill{current: 75, max: 100}
b.Run("ProcessHarvest", func(b *testing.B) { b.Run("ProcessHarvest", func(b *testing.B) {
for i := 0; i < b.N; i++ { for b.Loop() {
// Reset harvest count for consistent benchmarking // Reset harvest count for consistent benchmarking
gs.CurrentHarvests = gs.NumberHarvests gs.CurrentHarvests = gs.NumberHarvests
@ -179,14 +177,14 @@ func BenchmarkHarvestAlgorithm(b *testing.B) {
}) })
b.Run("FilterHarvestTables", func(b *testing.B) { b.Run("FilterHarvestTables", func(b *testing.B) {
for i := 0; i < b.N; i++ { for b.Loop() {
_ = gs.filterHarvestTables(player, 75) _ = gs.filterHarvestTables(player, 75)
} }
}) })
b.Run("SelectHarvestTable", func(b *testing.B) { b.Run("SelectHarvestTable", func(b *testing.B) {
tables := gs.filterHarvestTables(player, 75) tables := gs.filterHarvestTables(player, 75)
for i := 0; i < b.N; i++ { for b.Loop() {
_ = gs.selectHarvestTable(tables, 75) _ = gs.selectHarvestTable(tables, 75)
} }
}) })
@ -194,50 +192,77 @@ func BenchmarkHarvestAlgorithm(b *testing.B) {
b.Run("DetermineHarvestType", func(b *testing.B) { b.Run("DetermineHarvestType", func(b *testing.B) {
tables := gs.filterHarvestTables(player, 75) tables := gs.filterHarvestTables(player, 75)
table := gs.selectHarvestTable(tables, 75) table := gs.selectHarvestTable(tables, 75)
for i := 0; i < b.N; i++ { for b.Loop() {
_ = gs.determineHarvestType(table, false) _ = gs.determineHarvestType(table, false)
} }
}) })
b.Run("AwardHarvestItems", func(b *testing.B) { b.Run("AwardHarvestItems", func(b *testing.B) {
for i := 0; i < b.N; i++ { for b.Loop() {
_ = gs.awardHarvestItems(HarvestType3Items, player) _ = 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 // BenchmarkMasterListOperations measures master list performance
func BenchmarkMasterListOperations(b *testing.B) { func BenchmarkMasterListOperations(b *testing.B) {
ml := NewMasterList() setupSharedMasterList(b)
ml := sharedMasterList
// 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()
b.Run("GetGroundSpawn", func(b *testing.B) { b.Run("GetGroundSpawn", func(b *testing.B) {
b.RunParallel(func(pb *testing.PB) { b.RunParallel(func(pb *testing.PB) {
for pb.Next() { for pb.Next() {
id := int32((rand.Intn(numSpawns) + 1)) id := int32((rand.Intn(1000) + 1))
_ = ml.GetGroundSpawn(id) _ = ml.GetGroundSpawn(id)
} }
}) })
}) })
b.Run("AddGroundSpawn", func(b *testing.B) { 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() b.ResetTimer()
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
gs := createTestGroundSpawn(b, startID+int32(i)) addML.AddGroundSpawn(spawnsToAdd[i])
ml.AddGroundSpawn(gs)
} }
}) })
@ -261,19 +286,19 @@ func BenchmarkMasterListOperations(b *testing.B) {
}) })
b.Run("GetAvailableSpawns", func(b *testing.B) { b.Run("GetAvailableSpawns", func(b *testing.B) {
for i := 0; i < b.N; i++ { for b.Loop() {
_ = ml.GetAvailableSpawns() _ = ml.GetAvailableSpawns()
} }
}) })
b.Run("GetDepletedSpawns", func(b *testing.B) { b.Run("GetDepletedSpawns", func(b *testing.B) {
for i := 0; i < b.N; i++ { for b.Loop() {
_ = ml.GetDepletedSpawns() _ = ml.GetDepletedSpawns()
} }
}) })
b.Run("GetStatistics", func(b *testing.B) { b.Run("GetStatistics", func(b *testing.B) {
for i := 0; i < b.N; i++ { for b.Loop() {
_ = ml.GetStatistics() _ = ml.GetStatistics()
} }
}) })
@ -329,7 +354,7 @@ func BenchmarkMemoryAllocation(b *testing.B) {
b.Run("MasterListAllocation", func(b *testing.B) { b.Run("MasterListAllocation", func(b *testing.B) {
b.ReportAllocs() b.ReportAllocs()
for i := 0; i < b.N; i++ { for b.Loop() {
ml := NewMasterList() ml := NewMasterList()
_ = ml _ = ml
} }
@ -337,7 +362,7 @@ func BenchmarkMemoryAllocation(b *testing.B) {
b.Run("HarvestResultAllocation", func(b *testing.B) { b.Run("HarvestResultAllocation", func(b *testing.B) {
b.ReportAllocs() b.ReportAllocs()
for i := 0; i < b.N; i++ { for b.Loop() {
result := &HarvestResult{ result := &HarvestResult{
Success: true, Success: true,
HarvestType: HarvestType3Items, HarvestType: HarvestType3Items,
@ -348,6 +373,31 @@ func BenchmarkMemoryAllocation(b *testing.B) {
_ = result _ = 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 // BenchmarkRespawnOperations measures respawn performance
@ -355,7 +405,7 @@ func BenchmarkRespawnOperations(b *testing.B) {
gs := createTestGroundSpawn(b, 1001) gs := createTestGroundSpawn(b, 1001)
b.Run("Respawn", func(b *testing.B) { b.Run("Respawn", func(b *testing.B) {
for i := 0; i < b.N; i++ { for b.Loop() {
gs.CurrentHarvests = 0 // Deplete gs.CurrentHarvests = 0 // Deplete
gs.Respawn() gs.Respawn()
} }
@ -363,7 +413,7 @@ func BenchmarkRespawnOperations(b *testing.B) {
b.Run("RespawnWithRandomHeading", func(b *testing.B) { b.Run("RespawnWithRandomHeading", func(b *testing.B) {
gs.RandomizeHeading = true gs.RandomizeHeading = true
for i := 0; i < b.N; i++ { for b.Loop() {
gs.CurrentHarvests = 0 // Deplete gs.CurrentHarvests = 0 // Deplete
gs.Respawn() gs.Respawn()
} }
@ -397,34 +447,76 @@ func BenchmarkStringOperations(b *testing.B) {
}) })
} }
// BenchmarkComparisonWithOldSystem provides comparison benchmarks // BenchmarkSpatialFeatures tests features unique to spatial implementation
// These would help measure the performance improvement from modernization func BenchmarkSpatialFeatures(b *testing.B) {
func BenchmarkComparisonWithOldSystem(b *testing.B) { setupSharedMasterList(b)
ml := NewMasterList() ml := sharedMasterList
const numSpawns = 1000
// Setup b.Run("GetByZoneAndSkill", func(b *testing.B) {
b.StopTimer() skills := []string{"Mining", "Gathering", "Fishing", "Trapping"}
for i := 0; i < numSpawns; i++ { for b.Loop() {
gs := createTestGroundSpawn(b, int32(i+1)) zoneID := int32(rand.Intn(10) + 1)
ml.AddGroundSpawn(gs) skill := skills[rand.Intn(len(skills))]
} _ = ml.GetByZoneAndSkill(zoneID, skill)
b.StartTimer() }
b.Run("ModernizedLookup", func(b *testing.B) {
// Modern generic-based lookup
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
id := int32(rand.Intn(numSpawns) + 1)
_ = ml.GetGroundSpawn(id)
}
})
}) })
b.Run("ModernizedFiltering", func(b *testing.B) { b.Run("GetNearby_Small", func(b *testing.B) {
// Modern filter-based operations for b.Loop() {
for i := 0; i < b.N; i++ { x := float32(rand.Intn(10000))
_ = ml.GetAvailableSpawns() 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() {
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()
}
}
})
})
}

View File

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

View File

@ -1,174 +1,106 @@
package ground_spawn package ground_spawn
import ( import (
"fmt" "sync"
"eq2emu/internal/common"
"eq2emu/internal/database"
"zombiezen.com/go/sqlite"
) )
// 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 { 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 { func NewMasterList() *MasterList {
return &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 // getGridKey returns the grid cell for given coordinates
func (ml *MasterList) AddGroundSpawn(gs *GroundSpawn) bool { func (ml *MasterList) getGridKey(x, y float32) gridKey {
return ml.MasterList.Add(gs) return gridKey{
} x: int32(x / ml.gridSize),
y: int32(y / ml.gridSize),
// 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)
} }
return ml.loadFromMySQL(db)
} }
func (ml *MasterList) loadFromSQLite(db *database.Database) error { // refreshStateCache updates the available/depleted spawn caches
return db.ExecTransient(` func (ml *MasterList) refreshStateCache() {
SELECT id, groundspawn_id, name, collection_skill, number_harvests, if !ml.stateStale {
attempts_per_harvest, randomize_heading, respawn_time, return
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)
} }
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 // refreshStatsCache updates the statistics cache
func (ml *MasterList) GetStatistics() *Statistics { func (ml *MasterList) refreshStatsCache() {
availableSpawns := len(ml.GetAvailableSpawns()) if !ml.statsStale {
return
}
// Count by zone var availableSpawns int
zoneMap := make(map[int32]int) zoneMap := make(map[int32]int)
skillMap := make(map[string]int64) 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]++ zoneMap[gs.ZoneID]++
skillMap[gs.CollectionSkill]++ skillMap[gs.CollectionSkill]++
}) }
return &Statistics{ ml.stats = &Statistics{
TotalHarvests: 0, // Would need to be tracked separately TotalHarvests: 0, // Would need to be tracked separately
SuccessfulHarvests: 0, // Would need to be tracked separately SuccessfulHarvests: 0, // Would need to be tracked separately
RareItemsHarvested: 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, ActiveGroundSpawns: availableSpawns,
GroundSpawnsByZone: zoneMap, 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
} }