bespoke master list for ground_spawn
This commit is contained in:
parent
4278cf79bf
commit
c31866d080
210
internal/common/benchmark_test.go
Normal file
210
internal/common/benchmark_test.go
Normal 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
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
@ -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
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user