implement tests and beches for ground_spawn package
This commit is contained in:
parent
0889eae1cf
commit
0534c49610
@ -368,17 +368,17 @@ fmt.Printf("Cache hits: %d, misses: %d\n",
|
||||
// Custom packet handler
|
||||
type MyAAPacketHandler struct{}
|
||||
|
||||
func (ph *MyAAPacketHandler) GetAAListPacket(client interface{}) ([]byte, error) {
|
||||
func (ph *MyAAPacketHandler) GetAAListPacket(client any) ([]byte, error) {
|
||||
// Build AA list packet for client
|
||||
return []byte{}, nil
|
||||
}
|
||||
|
||||
func (ph *MyAAPacketHandler) SendAAUpdate(client interface{}, playerState *AAPlayerState) error {
|
||||
func (ph *MyAAPacketHandler) SendAAUpdate(client any, playerState *AAPlayerState) error {
|
||||
// Send AA update to client
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ph *MyAAPacketHandler) HandleAAPurchase(client interface{}, nodeID int32, rank int8) error {
|
||||
func (ph *MyAAPacketHandler) HandleAAPurchase(client any, nodeID int32, rank int8) error {
|
||||
// Handle AA purchase from client
|
||||
return nil
|
||||
}
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -514,8 +514,8 @@ func (db *DatabaseImpl) DeletePlayerAA(characterID int32) error {
|
||||
}
|
||||
|
||||
// GetAAStatistics returns statistics about AA usage
|
||||
func (db *DatabaseImpl) GetAAStatistics() (map[string]interface{}, error) {
|
||||
stats := make(map[string]interface{})
|
||||
func (db *DatabaseImpl) GetAAStatistics() (map[string]any, error) {
|
||||
stats := make(map[string]any)
|
||||
|
||||
// Get total players with AA data
|
||||
var totalPlayers int64
|
||||
|
@ -22,26 +22,26 @@ type AADatabase interface {
|
||||
LoadPlayerAADefaults(classID int8) (map[int8][]*AAEntry, error)
|
||||
|
||||
// Statistics
|
||||
GetAAStatistics() (map[string]interface{}, error)
|
||||
GetAAStatistics() (map[string]any, error)
|
||||
}
|
||||
|
||||
// AAPacketHandler interface for handling AA-related packets
|
||||
type AAPacketHandler interface {
|
||||
// List packets
|
||||
GetAAListPacket(client interface{}) ([]byte, error)
|
||||
SendAAUpdate(client interface{}, playerState *AAPlayerState) error
|
||||
GetAAListPacket(client any) ([]byte, error)
|
||||
SendAAUpdate(client any, playerState *AAPlayerState) error
|
||||
|
||||
// Purchase packets
|
||||
HandleAAPurchase(client interface{}, nodeID int32, rank int8) error
|
||||
SendAAPurchaseResponse(client interface{}, success bool, nodeID int32, newRank int8) error
|
||||
HandleAAPurchase(client any, nodeID int32, rank int8) error
|
||||
SendAAPurchaseResponse(client any, success bool, nodeID int32, newRank int8) error
|
||||
|
||||
// Template packets
|
||||
SendAATemplateList(client interface{}, templates map[int8]*AATemplate) error
|
||||
HandleAATemplateChange(client interface{}, templateID int8) error
|
||||
SendAATemplateList(client any, templates map[int8]*AATemplate) error
|
||||
HandleAATemplateChange(client any, templateID int8) error
|
||||
|
||||
// Display packets
|
||||
DisplayAA(client interface{}, templateID int8, changeMode int8) error
|
||||
SendAATabUpdate(client interface{}, tabID int8, tab *AATab) error
|
||||
DisplayAA(client any, templateID int8, changeMode int8) error
|
||||
SendAATabUpdate(client any, tabID int8, tab *AATab) error
|
||||
}
|
||||
|
||||
// AAEventHandler interface for handling AA events
|
||||
@ -124,8 +124,8 @@ type AAStatistics interface {
|
||||
// Aggregated statistics
|
||||
GetAAPurchaseStats() map[int32]int64
|
||||
GetPopularAAs() map[int32]int64
|
||||
GetPlayerProgressStats() map[string]interface{}
|
||||
GetSystemPerformanceStats() map[string]interface{}
|
||||
GetPlayerProgressStats() map[string]any
|
||||
GetSystemPerformanceStats() map[string]any
|
||||
}
|
||||
|
||||
// AACache interface for caching AA data
|
||||
@ -147,7 +147,7 @@ type AACache interface {
|
||||
|
||||
// Cache management
|
||||
Clear()
|
||||
GetStats() map[string]interface{}
|
||||
GetStats() map[string]any
|
||||
SetMaxSize(maxSize int32)
|
||||
}
|
||||
|
||||
@ -172,9 +172,9 @@ type Player interface {
|
||||
|
||||
// Transaction interface for database transactions
|
||||
type Transaction interface {
|
||||
Exec(query string, args ...interface{}) (sql.Result, error)
|
||||
Query(query string, args ...interface{}) (*sql.Rows, error)
|
||||
QueryRow(query string, args ...interface{}) *sql.Row
|
||||
Exec(query string, args ...any) (sql.Result, error)
|
||||
Query(query string, args ...any) (*sql.Rows, error)
|
||||
QueryRow(query string, args ...any) *sql.Row
|
||||
Commit() error
|
||||
Rollback() error
|
||||
}
|
||||
@ -235,7 +235,7 @@ type AAManagerInterface interface {
|
||||
|
||||
// Statistics
|
||||
GetSystemStats() *AAManagerStats
|
||||
GetPlayerStats(characterID int32) map[string]interface{}
|
||||
GetPlayerStats(characterID int32) map[string]any
|
||||
|
||||
// Configuration
|
||||
SetConfig(config AAManagerConfig) error
|
||||
@ -327,7 +327,7 @@ func (aa *AAAdapter) GetTemplates() (map[int8]*AATemplate, error) {
|
||||
}
|
||||
|
||||
// GetPlayerStats returns AA statistics for the character
|
||||
func (aa *AAAdapter) GetPlayerStats() map[string]interface{} {
|
||||
func (aa *AAAdapter) GetPlayerStats() map[string]any {
|
||||
return aa.manager.GetPlayerStats(aa.characterID)
|
||||
}
|
||||
|
||||
@ -579,11 +579,11 @@ func (c *SimpleAACache) Clear() {
|
||||
}
|
||||
|
||||
// GetStats returns cache statistics
|
||||
func (c *SimpleAACache) GetStats() map[string]interface{} {
|
||||
func (c *SimpleAACache) GetStats() map[string]any {
|
||||
c.mutex.RLock()
|
||||
defer c.mutex.RUnlock()
|
||||
|
||||
return map[string]interface{}{
|
||||
return map[string]any{
|
||||
"hits": c.hits,
|
||||
"misses": c.misses,
|
||||
"aa_data_count": len(c.aaData),
|
||||
|
@ -164,7 +164,7 @@ func (am *AAManager) GetPlayerAAState(characterID int32) (*AAPlayerState, error)
|
||||
// Need to load from database, use write lock to prevent race condition
|
||||
am.statesMutex.Lock()
|
||||
defer am.statesMutex.Unlock()
|
||||
|
||||
|
||||
// Double-check pattern: another goroutine might have loaded it while we waited
|
||||
if playerState, exists := am.playerStates[characterID]; exists {
|
||||
return playerState, nil
|
||||
@ -456,16 +456,16 @@ func (am *AAManager) GetSystemStats() *AAManagerStats {
|
||||
}
|
||||
|
||||
// GetPlayerStats returns player-specific statistics
|
||||
func (am *AAManager) GetPlayerStats(characterID int32) map[string]interface{} {
|
||||
func (am *AAManager) GetPlayerStats(characterID int32) map[string]any {
|
||||
playerState := am.getPlayerState(characterID)
|
||||
if playerState == nil {
|
||||
return map[string]interface{}{"error": "player not found"}
|
||||
return map[string]any{"error": "player not found"}
|
||||
}
|
||||
|
||||
playerState.mutex.RLock()
|
||||
defer playerState.mutex.RUnlock()
|
||||
|
||||
return map[string]interface{}{
|
||||
return map[string]any{
|
||||
"character_id": characterID,
|
||||
"total_points": playerState.TotalPoints,
|
||||
"spent_points": playerState.SpentPoints,
|
||||
|
@ -251,11 +251,11 @@ func (mal *MasterAAList) ValidateAAData() []error {
|
||||
}
|
||||
|
||||
// GetStats returns statistics about the master AA list
|
||||
func (mal *MasterAAList) GetStats() map[string]interface{} {
|
||||
func (mal *MasterAAList) GetStats() map[string]any {
|
||||
mal.mutex.RLock()
|
||||
defer mal.mutex.RUnlock()
|
||||
|
||||
stats := make(map[string]interface{})
|
||||
stats := make(map[string]any)
|
||||
stats[STAT_TOTAL_AAS_LOADED] = mal.totalLoaded
|
||||
stats["last_load_time"] = mal.lastLoadTime
|
||||
stats["groups_count"] = len(mal.aaByGroup)
|
||||
@ -429,11 +429,11 @@ func (manl *MasterAANodeList) ValidateTreeNodes() []error {
|
||||
}
|
||||
|
||||
// GetStats returns statistics about the master node list
|
||||
func (manl *MasterAANodeList) GetStats() map[string]interface{} {
|
||||
func (manl *MasterAANodeList) GetStats() map[string]any {
|
||||
manl.mutex.RLock()
|
||||
defer manl.mutex.RUnlock()
|
||||
|
||||
stats := make(map[string]interface{})
|
||||
stats := make(map[string]any)
|
||||
stats[STAT_TOTAL_NODES_LOADED] = manl.totalLoaded
|
||||
stats["last_load_time"] = manl.lastLoadTime
|
||||
stats["classes_count"] = len(manl.nodesByClass)
|
||||
|
@ -227,11 +227,11 @@ func (a *Appearances) IsValid() bool {
|
||||
}
|
||||
|
||||
// GetStatistics returns statistics about the appearance collection
|
||||
func (a *Appearances) GetStatistics() map[string]interface{} {
|
||||
func (a *Appearances) GetStatistics() map[string]any {
|
||||
a.mutex.RLock()
|
||||
defer a.mutex.RUnlock()
|
||||
|
||||
stats := make(map[string]interface{})
|
||||
stats := make(map[string]any)
|
||||
stats["total_appearances"] = len(a.appearanceMap)
|
||||
|
||||
// Count by minimum client version
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -15,10 +15,10 @@ type Database interface {
|
||||
|
||||
// Logger interface for appearance logging
|
||||
type Logger interface {
|
||||
LogInfo(message string, args ...interface{})
|
||||
LogError(message string, args ...interface{})
|
||||
LogDebug(message string, args ...interface{})
|
||||
LogWarning(message string, args ...interface{})
|
||||
LogInfo(message string, args ...any)
|
||||
LogError(message string, args ...any)
|
||||
LogDebug(message string, args ...any)
|
||||
LogWarning(message string, args ...any)
|
||||
}
|
||||
|
||||
// AppearanceProvider interface for entities that provide appearances
|
||||
|
@ -201,7 +201,7 @@ func (m *Manager) SearchAppearancesByName(nameSubstring string) []*Appearance {
|
||||
}
|
||||
|
||||
// GetStatistics returns appearance system statistics
|
||||
func (m *Manager) GetStatistics() map[string]interface{} {
|
||||
func (m *Manager) GetStatistics() map[string]any {
|
||||
m.mutex.RLock()
|
||||
defer m.mutex.RUnlock()
|
||||
|
||||
|
@ -329,11 +329,11 @@ func (c *Classes) GetClassCount() int {
|
||||
}
|
||||
|
||||
// GetClassInfo returns comprehensive information about a class
|
||||
func (c *Classes) GetClassInfo(classID int8) map[string]interface{} {
|
||||
func (c *Classes) GetClassInfo(classID int8) map[string]any {
|
||||
c.mutex.RLock()
|
||||
defer c.mutex.RUnlock()
|
||||
|
||||
info := make(map[string]interface{})
|
||||
info := make(map[string]any)
|
||||
|
||||
if !c.IsValidClassID(classID) {
|
||||
info["valid"] = false
|
||||
|
@ -1185,7 +1185,7 @@ func TestGetClassSelectionData(t *testing.T) {
|
||||
t.Error("Selection data should include statistics")
|
||||
}
|
||||
|
||||
adventureClasses := selectionData["adventure_classes"].([]map[string]interface{})
|
||||
adventureClasses := selectionData["adventure_classes"].([]map[string]any)
|
||||
if len(adventureClasses) == 0 {
|
||||
t.Error("Selection data should include adventure classes")
|
||||
}
|
||||
@ -1442,7 +1442,7 @@ func TestGetClassRecommendations(t *testing.T) {
|
||||
manager := NewClassManager()
|
||||
|
||||
// Test recommendations by class type
|
||||
preferences := map[string]interface{}{
|
||||
preferences := map[string]any{
|
||||
"class_type": ClassTypeAdventure,
|
||||
}
|
||||
|
||||
@ -1459,7 +1459,7 @@ func TestGetClassRecommendations(t *testing.T) {
|
||||
}
|
||||
|
||||
// Test recommendations by base class
|
||||
basePreferences := map[string]interface{}{
|
||||
basePreferences := map[string]any{
|
||||
"base_class": ClassFighter,
|
||||
}
|
||||
|
||||
@ -1469,7 +1469,7 @@ func TestGetClassRecommendations(t *testing.T) {
|
||||
}
|
||||
|
||||
// Test recommendations by preferred stats
|
||||
statPreferences := map[string]interface{}{
|
||||
statPreferences := map[string]any{
|
||||
"preferred_stats": []string{"strength", "stamina"},
|
||||
}
|
||||
|
||||
@ -1479,7 +1479,7 @@ func TestGetClassRecommendations(t *testing.T) {
|
||||
}
|
||||
|
||||
// Test empty preferences (should get defaults)
|
||||
emptyPreferences := map[string]interface{}{}
|
||||
emptyPreferences := map[string]any{}
|
||||
defaultRecommendations := manager.GetClassRecommendations(emptyPreferences)
|
||||
if len(defaultRecommendations) == 0 {
|
||||
t.Error("Should get default recommendations when no preferences given")
|
||||
|
@ -33,7 +33,7 @@ func NewClassIntegration() *ClassIntegration {
|
||||
}
|
||||
|
||||
// ValidateEntityClass validates an entity's class and provides detailed information
|
||||
func (ci *ClassIntegration) ValidateEntityClass(entity ClassAware) (bool, string, map[string]interface{}) {
|
||||
func (ci *ClassIntegration) ValidateEntityClass(entity ClassAware) (bool, string, map[string]any) {
|
||||
classID := entity.GetClass()
|
||||
|
||||
if !ci.classes.IsValidClassID(classID) {
|
||||
@ -45,8 +45,8 @@ func (ci *ClassIntegration) ValidateEntityClass(entity ClassAware) (bool, string
|
||||
}
|
||||
|
||||
// GetEntityClassInfo returns comprehensive class information for an entity
|
||||
func (ci *ClassIntegration) GetEntityClassInfo(entity EntityWithClass) map[string]interface{} {
|
||||
info := make(map[string]interface{})
|
||||
func (ci *ClassIntegration) GetEntityClassInfo(entity EntityWithClass) map[string]any {
|
||||
info := make(map[string]any)
|
||||
|
||||
// Basic entity info
|
||||
info["entity_id"] = entity.GetID()
|
||||
@ -281,12 +281,12 @@ func (ci *ClassIntegration) GetClassStartingStats(classID int8) map[string]int16
|
||||
}
|
||||
|
||||
// CreateClassSpecificEntity creates entity data with class-specific properties
|
||||
func (ci *ClassIntegration) CreateClassSpecificEntity(classID int8) map[string]interface{} {
|
||||
func (ci *ClassIntegration) CreateClassSpecificEntity(classID int8) map[string]any {
|
||||
if !ci.classes.IsValidClassID(classID) {
|
||||
return nil
|
||||
}
|
||||
|
||||
entityData := make(map[string]interface{})
|
||||
entityData := make(map[string]any)
|
||||
|
||||
// Basic class info
|
||||
entityData["class_id"] = classID
|
||||
@ -310,16 +310,16 @@ func (ci *ClassIntegration) CreateClassSpecificEntity(classID int8) map[string]i
|
||||
}
|
||||
|
||||
// GetClassSelectionData returns data for class selection UI
|
||||
func (ci *ClassIntegration) GetClassSelectionData() map[string]interface{} {
|
||||
data := make(map[string]interface{})
|
||||
func (ci *ClassIntegration) GetClassSelectionData() map[string]any {
|
||||
data := make(map[string]any)
|
||||
|
||||
// All available adventure classes (exclude tradeskill for character creation)
|
||||
allClasses := ci.classes.GetAllClasses()
|
||||
adventureClasses := make([]map[string]interface{}, 0)
|
||||
adventureClasses := make([]map[string]any, 0)
|
||||
|
||||
for classID, displayName := range allClasses {
|
||||
if ci.classes.IsAdventureClass(classID) {
|
||||
classData := map[string]interface{}{
|
||||
classData := map[string]any{
|
||||
"id": classID,
|
||||
"name": displayName,
|
||||
"type": ci.classes.GetClassType(classID),
|
||||
|
@ -330,8 +330,8 @@ func (cm *ClassManager) handleProgressionCommand(args []string) (string, error)
|
||||
}
|
||||
|
||||
// ValidateEntityClasses validates classes for a collection of entities
|
||||
func (cm *ClassManager) ValidateEntityClasses(entities []ClassAware) map[string]interface{} {
|
||||
validationResults := make(map[string]interface{})
|
||||
func (cm *ClassManager) ValidateEntityClasses(entities []ClassAware) map[string]any {
|
||||
validationResults := make(map[string]any)
|
||||
|
||||
validCount := 0
|
||||
invalidCount := 0
|
||||
@ -351,11 +351,11 @@ func (cm *ClassManager) ValidateEntityClasses(entities []ClassAware) map[string]
|
||||
// Track invalid entities
|
||||
if !isValid {
|
||||
if validationResults["invalid_entities"] == nil {
|
||||
validationResults["invalid_entities"] = make([]map[string]interface{}, 0)
|
||||
validationResults["invalid_entities"] = make([]map[string]any, 0)
|
||||
}
|
||||
|
||||
invalidList := validationResults["invalid_entities"].([]map[string]interface{})
|
||||
invalidList = append(invalidList, map[string]interface{}{
|
||||
invalidList := validationResults["invalid_entities"].([]map[string]any)
|
||||
invalidList = append(invalidList, map[string]any{
|
||||
"index": i,
|
||||
"class_id": classID,
|
||||
})
|
||||
@ -372,7 +372,7 @@ func (cm *ClassManager) ValidateEntityClasses(entities []ClassAware) map[string]
|
||||
}
|
||||
|
||||
// GetClassRecommendations returns class recommendations for character creation
|
||||
func (cm *ClassManager) GetClassRecommendations(preferences map[string]interface{}) []int8 {
|
||||
func (cm *ClassManager) GetClassRecommendations(preferences map[string]any) []int8 {
|
||||
recommendations := make([]int8, 0)
|
||||
|
||||
// Check for class type preference
|
||||
|
@ -366,8 +366,8 @@ func (cu *ClassUtils) GetClassAliases(classID int8) []string {
|
||||
}
|
||||
|
||||
// GetClassStatistics returns statistics about the class system
|
||||
func (cu *ClassUtils) GetClassStatistics() map[string]interface{} {
|
||||
stats := make(map[string]interface{})
|
||||
func (cu *ClassUtils) GetClassStatistics() map[string]any {
|
||||
stats := make(map[string]any)
|
||||
|
||||
allClasses := cu.classes.GetAllClasses()
|
||||
stats["total_classes"] = len(allClasses)
|
||||
|
@ -163,14 +163,14 @@ type CollectionEventHandler interface {
|
||||
// LogHandler provides logging functionality
|
||||
type LogHandler interface {
|
||||
// LogDebug logs debug messages
|
||||
LogDebug(category, message string, args ...interface{})
|
||||
LogDebug(category, message string, args ...any)
|
||||
|
||||
// LogInfo logs informational messages
|
||||
LogInfo(category, message string, args ...interface{})
|
||||
LogInfo(category, message string, args ...any)
|
||||
|
||||
// LogError logs error messages
|
||||
LogError(category, message string, args ...interface{})
|
||||
LogError(category, message string, args ...any)
|
||||
|
||||
// LogWarning logs warning messages
|
||||
LogWarning(category, message string, args ...interface{})
|
||||
LogWarning(category, message string, args ...any)
|
||||
}
|
||||
|
@ -151,7 +151,7 @@ func (e *Entity) SetInfoStruct(info *InfoStruct) {
|
||||
}
|
||||
|
||||
// GetClient returns the client for this entity (overridden by Player)
|
||||
func (e *Entity) GetClient() interface{} {
|
||||
func (e *Entity) GetClient() any {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -18,10 +18,10 @@ type Database interface {
|
||||
|
||||
// Logger interface for faction logging
|
||||
type Logger interface {
|
||||
LogInfo(message string, args ...interface{})
|
||||
LogError(message string, args ...interface{})
|
||||
LogDebug(message string, args ...interface{})
|
||||
LogWarning(message string, args ...interface{})
|
||||
LogInfo(message string, args ...any)
|
||||
LogError(message string, args ...any)
|
||||
LogDebug(message string, args ...any)
|
||||
LogWarning(message string, args ...any)
|
||||
}
|
||||
|
||||
// FactionRelation represents a relationship between two factions
|
||||
|
@ -237,11 +237,11 @@ func (m *Manager) RecordFactionDecrease(factionID int32) {
|
||||
}
|
||||
|
||||
// GetStatistics returns faction system statistics
|
||||
func (m *Manager) GetStatistics() map[string]interface{} {
|
||||
func (m *Manager) GetStatistics() map[string]any {
|
||||
m.mutex.RLock()
|
||||
defer m.mutex.RUnlock()
|
||||
|
||||
stats := make(map[string]interface{})
|
||||
stats := make(map[string]any)
|
||||
stats["total_factions"] = m.masterFactionList.GetFactionCount()
|
||||
stats["total_faction_changes"] = m.totalFactionChanges
|
||||
stats["faction_increases"] = m.factionIncreases
|
||||
|
562
internal/ground_spawn/benchmark_test.go
Normal file
562
internal/ground_spawn/benchmark_test.go
Normal file
@ -0,0 +1,562 @@
|
||||
package ground_spawn
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
// Mock implementations are in test_utils.go
|
||||
|
||||
// Benchmark GroundSpawn operations
|
||||
func BenchmarkGroundSpawn(b *testing.B) {
|
||||
config := GroundSpawnConfig{
|
||||
GroundSpawnID: 1,
|
||||
CollectionSkill: SkillGathering,
|
||||
NumberHarvests: 10,
|
||||
AttemptsPerHarvest: 2,
|
||||
RandomizeHeading: true,
|
||||
Location: SpawnLocation{
|
||||
X: 100.0, Y: 200.0, Z: 300.0, Heading: 45.0, GridID: 1,
|
||||
},
|
||||
Name: "Benchmark Node",
|
||||
Description: "A benchmark harvestable node",
|
||||
}
|
||||
|
||||
gs := NewGroundSpawn(config)
|
||||
|
||||
b.Run("GetNumberHarvests", func(b *testing.B) {
|
||||
for b.Loop() {
|
||||
_ = gs.GetNumberHarvests()
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("SetNumberHarvests", func(b *testing.B) {
|
||||
b.ResetTimer()
|
||||
for i := 0; b.Loop(); i++ {
|
||||
gs.SetNumberHarvests(int8(i % 10))
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("GetCollectionSkill", func(b *testing.B) {
|
||||
b.ResetTimer()
|
||||
for b.Loop() {
|
||||
_ = gs.GetCollectionSkill()
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("SetCollectionSkill", func(b *testing.B) {
|
||||
skills := []string{SkillGathering, SkillMining, SkillFishing, SkillTrapping}
|
||||
b.ResetTimer()
|
||||
for i := 0; b.Loop(); i++ {
|
||||
gs.SetCollectionSkill(skills[i%len(skills)])
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("IsAvailable", func(b *testing.B) {
|
||||
b.ResetTimer()
|
||||
for b.Loop() {
|
||||
_ = gs.IsAvailable()
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("IsDepleted", func(b *testing.B) {
|
||||
b.ResetTimer()
|
||||
for b.Loop() {
|
||||
_ = gs.IsDepleted()
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("GetHarvestMessageName", func(b *testing.B) {
|
||||
b.ResetTimer()
|
||||
for i := 0; b.Loop(); i++ {
|
||||
_ = gs.GetHarvestMessageName(i%2 == 0, i%4 == 0)
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("GetHarvestSpellType", func(b *testing.B) {
|
||||
b.ResetTimer()
|
||||
for b.Loop() {
|
||||
_ = gs.GetHarvestSpellType()
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("Copy", func(b *testing.B) {
|
||||
b.ResetTimer()
|
||||
for b.Loop() {
|
||||
copy := gs.Copy()
|
||||
_ = copy
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("Respawn", func(b *testing.B) {
|
||||
b.ResetTimer()
|
||||
for b.Loop() {
|
||||
gs.Respawn()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Benchmark Manager operations
|
||||
func BenchmarkManager(b *testing.B) {
|
||||
manager := NewManager(nil, &mockLogger{})
|
||||
|
||||
// Pre-populate with ground spawns
|
||||
for i := int32(1); i <= 100; i++ {
|
||||
config := GroundSpawnConfig{
|
||||
GroundSpawnID: i,
|
||||
CollectionSkill: SkillGathering,
|
||||
NumberHarvests: 5,
|
||||
AttemptsPerHarvest: 1,
|
||||
Location: SpawnLocation{
|
||||
X: float32(i * 10), Y: float32(i * 20), Z: float32(i * 30),
|
||||
Heading: float32(i * 45), GridID: 1,
|
||||
},
|
||||
Name: "Benchmark Node",
|
||||
Description: "Benchmark node",
|
||||
}
|
||||
gs := manager.CreateGroundSpawn(config)
|
||||
_ = gs
|
||||
}
|
||||
|
||||
b.Run("GetGroundSpawn", func(b *testing.B) {
|
||||
b.ResetTimer()
|
||||
for i := 0; b.Loop(); i++ {
|
||||
spawnID := int32((i % 100) + 1)
|
||||
_ = manager.GetGroundSpawn(spawnID)
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("CreateGroundSpawn", func(b *testing.B) {
|
||||
b.ResetTimer()
|
||||
for i := 0; b.Loop(); i++ {
|
||||
config := GroundSpawnConfig{
|
||||
GroundSpawnID: int32(2000 + i),
|
||||
CollectionSkill: SkillMining,
|
||||
NumberHarvests: 3,
|
||||
AttemptsPerHarvest: 1,
|
||||
Location: SpawnLocation{
|
||||
X: float32(i), Y: float32(i * 2), Z: float32(i * 3),
|
||||
Heading: 0, GridID: 1,
|
||||
},
|
||||
Name: "New Node",
|
||||
Description: "New benchmark node",
|
||||
}
|
||||
_ = manager.CreateGroundSpawn(config)
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("GetGroundSpawnsByZone", func(b *testing.B) {
|
||||
b.ResetTimer()
|
||||
for b.Loop() {
|
||||
_ = manager.GetGroundSpawnsByZone(1)
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("GetGroundSpawnCount", func(b *testing.B) {
|
||||
b.ResetTimer()
|
||||
for b.Loop() {
|
||||
_ = manager.GetGroundSpawnCount()
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("GetActiveGroundSpawns", func(b *testing.B) {
|
||||
b.ResetTimer()
|
||||
for b.Loop() {
|
||||
_ = manager.GetActiveGroundSpawns()
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("GetDepletedGroundSpawns", func(b *testing.B) {
|
||||
b.ResetTimer()
|
||||
for b.Loop() {
|
||||
_ = manager.GetDepletedGroundSpawns()
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("GetStatistics", func(b *testing.B) {
|
||||
b.ResetTimer()
|
||||
for b.Loop() {
|
||||
_ = manager.GetStatistics()
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("ResetStatistics", func(b *testing.B) {
|
||||
b.ResetTimer()
|
||||
for b.Loop() {
|
||||
manager.ResetStatistics()
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("ProcessRespawns", func(b *testing.B) {
|
||||
b.ResetTimer()
|
||||
for b.Loop() {
|
||||
manager.ProcessRespawns()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Benchmark concurrent operations
|
||||
func BenchmarkConcurrentOperations(b *testing.B) {
|
||||
b.Run("GroundSpawnConcurrentReads", func(b *testing.B) {
|
||||
config := GroundSpawnConfig{
|
||||
GroundSpawnID: 1,
|
||||
CollectionSkill: SkillGathering,
|
||||
NumberHarvests: 10,
|
||||
AttemptsPerHarvest: 1,
|
||||
Location: SpawnLocation{
|
||||
X: 100, Y: 200, Z: 300, Heading: 45, GridID: 1,
|
||||
},
|
||||
Name: "Concurrent Node",
|
||||
Description: "Concurrent benchmark node",
|
||||
}
|
||||
gs := NewGroundSpawn(config)
|
||||
|
||||
b.ResetTimer()
|
||||
b.RunParallel(func(pb *testing.PB) {
|
||||
i := 0
|
||||
for pb.Next() {
|
||||
switch i % 4 {
|
||||
case 0:
|
||||
_ = gs.GetNumberHarvests()
|
||||
case 1:
|
||||
_ = gs.GetCollectionSkill()
|
||||
case 2:
|
||||
_ = gs.IsAvailable()
|
||||
case 3:
|
||||
_ = gs.GetHarvestSpellType()
|
||||
}
|
||||
i++
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
b.Run("GroundSpawnConcurrentWrites", func(b *testing.B) {
|
||||
config := GroundSpawnConfig{
|
||||
GroundSpawnID: 1,
|
||||
CollectionSkill: SkillGathering,
|
||||
NumberHarvests: 10,
|
||||
AttemptsPerHarvest: 1,
|
||||
Location: SpawnLocation{
|
||||
X: 100, Y: 200, Z: 300, Heading: 45, GridID: 1,
|
||||
},
|
||||
Name: "Concurrent Node",
|
||||
Description: "Concurrent benchmark node",
|
||||
}
|
||||
gs := NewGroundSpawn(config)
|
||||
|
||||
b.ResetTimer()
|
||||
b.RunParallel(func(pb *testing.PB) {
|
||||
i := 0
|
||||
for pb.Next() {
|
||||
switch i % 3 {
|
||||
case 0:
|
||||
gs.SetNumberHarvests(int8(i % 10))
|
||||
case 1:
|
||||
gs.SetCollectionSkill(SkillMining)
|
||||
case 2:
|
||||
gs.SetRandomizeHeading(i%2 == 0)
|
||||
}
|
||||
i++
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
b.Run("ManagerConcurrentOperations", func(b *testing.B) {
|
||||
manager := NewManager(nil, &mockLogger{})
|
||||
|
||||
// Pre-populate
|
||||
for i := int32(1); i <= 10; i++ {
|
||||
config := GroundSpawnConfig{
|
||||
GroundSpawnID: i,
|
||||
CollectionSkill: SkillGathering,
|
||||
NumberHarvests: 5,
|
||||
AttemptsPerHarvest: 1,
|
||||
Location: SpawnLocation{
|
||||
X: float32(i * 10), Y: float32(i * 20), Z: float32(i * 30),
|
||||
Heading: 0, GridID: 1,
|
||||
},
|
||||
Name: "Manager Node",
|
||||
Description: "Manager benchmark node",
|
||||
}
|
||||
manager.CreateGroundSpawn(config)
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
b.RunParallel(func(pb *testing.PB) {
|
||||
i := 0
|
||||
for pb.Next() {
|
||||
spawnID := int32((i % 10) + 1)
|
||||
switch i % 5 {
|
||||
case 0:
|
||||
_ = manager.GetGroundSpawn(spawnID)
|
||||
case 1:
|
||||
_ = manager.GetGroundSpawnsByZone(1)
|
||||
case 2:
|
||||
_ = manager.GetStatistics()
|
||||
case 3:
|
||||
_ = manager.GetActiveGroundSpawns()
|
||||
case 4:
|
||||
manager.ProcessRespawns()
|
||||
}
|
||||
i++
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// Memory allocation benchmarks
|
||||
func BenchmarkMemoryAllocations(b *testing.B) {
|
||||
b.Run("GroundSpawnCreation", func(b *testing.B) {
|
||||
config := GroundSpawnConfig{
|
||||
GroundSpawnID: 1,
|
||||
CollectionSkill: SkillGathering,
|
||||
NumberHarvests: 5,
|
||||
AttemptsPerHarvest: 1,
|
||||
Location: SpawnLocation{
|
||||
X: 100, Y: 200, Z: 300, Heading: 45, GridID: 1,
|
||||
},
|
||||
Name: "Memory Test Node",
|
||||
Description: "Memory test node",
|
||||
}
|
||||
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for b.Loop() {
|
||||
_ = NewGroundSpawn(config)
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("ManagerCreation", func(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for b.Loop() {
|
||||
_ = NewManager(nil, &mockLogger{})
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("GroundSpawnCopy", func(b *testing.B) {
|
||||
config := GroundSpawnConfig{
|
||||
GroundSpawnID: 1,
|
||||
CollectionSkill: SkillGathering,
|
||||
NumberHarvests: 5,
|
||||
AttemptsPerHarvest: 1,
|
||||
Location: SpawnLocation{
|
||||
X: 100, Y: 200, Z: 300, Heading: 45, GridID: 1,
|
||||
},
|
||||
Name: "Copy Test Node",
|
||||
Description: "Copy test node",
|
||||
}
|
||||
gs := NewGroundSpawn(config)
|
||||
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for b.Loop() {
|
||||
_ = gs.Copy()
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("StatisticsGeneration", func(b *testing.B) {
|
||||
manager := NewManager(nil, &mockLogger{})
|
||||
|
||||
// Add some data for meaningful statistics
|
||||
for i := int32(1); i <= 10; i++ {
|
||||
config := GroundSpawnConfig{
|
||||
GroundSpawnID: i,
|
||||
CollectionSkill: SkillGathering,
|
||||
NumberHarvests: 5,
|
||||
AttemptsPerHarvest: 1,
|
||||
Location: SpawnLocation{
|
||||
X: float32(i * 10), Y: float32(i * 20), Z: float32(i * 30),
|
||||
Heading: 0, GridID: 1,
|
||||
},
|
||||
Name: "Stats Node",
|
||||
Description: "Stats test node",
|
||||
}
|
||||
manager.CreateGroundSpawn(config)
|
||||
}
|
||||
|
||||
// Add some harvest statistics
|
||||
manager.mutex.Lock()
|
||||
manager.totalHarvests = 1000
|
||||
manager.successfulHarvests = 850
|
||||
manager.rareItemsHarvested = 50
|
||||
manager.skillUpsGenerated = 200
|
||||
manager.harvestsBySkill[SkillGathering] = 600
|
||||
manager.harvestsBySkill[SkillMining] = 400
|
||||
manager.mutex.Unlock()
|
||||
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for b.Loop() {
|
||||
_ = manager.GetStatistics()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Contention benchmarks
|
||||
func BenchmarkContention(b *testing.B) {
|
||||
b.Run("HighContentionReads", func(b *testing.B) {
|
||||
config := GroundSpawnConfig{
|
||||
GroundSpawnID: 1,
|
||||
CollectionSkill: SkillGathering,
|
||||
NumberHarvests: 10,
|
||||
AttemptsPerHarvest: 1,
|
||||
Location: SpawnLocation{
|
||||
X: 100, Y: 200, Z: 300, Heading: 45, GridID: 1,
|
||||
},
|
||||
Name: "Contention Node",
|
||||
Description: "Contention test node",
|
||||
}
|
||||
gs := NewGroundSpawn(config)
|
||||
|
||||
b.ResetTimer()
|
||||
b.RunParallel(func(pb *testing.PB) {
|
||||
for pb.Next() {
|
||||
_ = gs.GetNumberHarvests()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
b.Run("HighContentionWrites", func(b *testing.B) {
|
||||
config := GroundSpawnConfig{
|
||||
GroundSpawnID: 1,
|
||||
CollectionSkill: SkillGathering,
|
||||
NumberHarvests: 10,
|
||||
AttemptsPerHarvest: 1,
|
||||
Location: SpawnLocation{
|
||||
X: 100, Y: 200, Z: 300, Heading: 45, GridID: 1,
|
||||
},
|
||||
Name: "Contention Node",
|
||||
Description: "Contention test node",
|
||||
}
|
||||
gs := NewGroundSpawn(config)
|
||||
|
||||
b.ResetTimer()
|
||||
b.RunParallel(func(pb *testing.PB) {
|
||||
i := 0
|
||||
for pb.Next() {
|
||||
gs.SetNumberHarvests(int8(i % 10))
|
||||
i++
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
b.Run("MixedReadWrite", func(b *testing.B) {
|
||||
config := GroundSpawnConfig{
|
||||
GroundSpawnID: 1,
|
||||
CollectionSkill: SkillGathering,
|
||||
NumberHarvests: 10,
|
||||
AttemptsPerHarvest: 1,
|
||||
Location: SpawnLocation{
|
||||
X: 100, Y: 200, Z: 300, Heading: 45, GridID: 1,
|
||||
},
|
||||
Name: "Mixed Node",
|
||||
Description: "Mixed operations test node",
|
||||
}
|
||||
gs := NewGroundSpawn(config)
|
||||
|
||||
b.ResetTimer()
|
||||
b.RunParallel(func(pb *testing.PB) {
|
||||
i := 0
|
||||
for pb.Next() {
|
||||
if i%10 == 0 {
|
||||
// 10% writes
|
||||
gs.SetNumberHarvests(int8(i % 5))
|
||||
} else {
|
||||
// 90% reads
|
||||
_ = gs.GetNumberHarvests()
|
||||
}
|
||||
i++
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
b.Run("ManagerHighContention", func(b *testing.B) {
|
||||
manager := NewManager(nil, &mockLogger{})
|
||||
|
||||
// Pre-populate
|
||||
for i := int32(1); i <= 5; i++ {
|
||||
config := GroundSpawnConfig{
|
||||
GroundSpawnID: i,
|
||||
CollectionSkill: SkillGathering,
|
||||
NumberHarvests: 5,
|
||||
AttemptsPerHarvest: 1,
|
||||
Location: SpawnLocation{
|
||||
X: float32(i * 10), Y: float32(i * 20), Z: float32(i * 30),
|
||||
Heading: 0, GridID: 1,
|
||||
},
|
||||
Name: "Contention Node",
|
||||
Description: "Manager contention test",
|
||||
}
|
||||
manager.CreateGroundSpawn(config)
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
b.RunParallel(func(pb *testing.PB) {
|
||||
for pb.Next() {
|
||||
_ = manager.GetGroundSpawn(1)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// Scalability benchmarks
|
||||
func BenchmarkScalability(b *testing.B) {
|
||||
sizes := []int{10, 100, 1000}
|
||||
|
||||
for _, size := range sizes {
|
||||
b.Run("GroundSpawnLookup_"+string(rune(size)), func(b *testing.B) {
|
||||
manager := NewManager(nil, &mockLogger{})
|
||||
|
||||
// Pre-populate with varying sizes
|
||||
for i := int32(1); i <= int32(size); i++ {
|
||||
config := GroundSpawnConfig{
|
||||
GroundSpawnID: i,
|
||||
CollectionSkill: SkillGathering,
|
||||
NumberHarvests: 5,
|
||||
AttemptsPerHarvest: 1,
|
||||
Location: SpawnLocation{
|
||||
X: float32(i * 10), Y: float32(i * 20), Z: float32(i * 30),
|
||||
Heading: 0, GridID: 1,
|
||||
},
|
||||
Name: "Scale Node",
|
||||
Description: "Scalability test node",
|
||||
}
|
||||
manager.CreateGroundSpawn(config)
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; b.Loop(); i++ {
|
||||
spawnID := int32((i % size) + 1)
|
||||
_ = manager.GetGroundSpawn(spawnID)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
for _, size := range sizes {
|
||||
b.Run("ZoneLookup_"+string(rune(size)), func(b *testing.B) {
|
||||
manager := NewManager(nil, &mockLogger{})
|
||||
|
||||
// Pre-populate with varying sizes
|
||||
for i := int32(1); i <= int32(size); i++ {
|
||||
config := GroundSpawnConfig{
|
||||
GroundSpawnID: i,
|
||||
CollectionSkill: SkillGathering,
|
||||
NumberHarvests: 5,
|
||||
AttemptsPerHarvest: 1,
|
||||
Location: SpawnLocation{
|
||||
X: float32(i * 10), Y: float32(i * 20), Z: float32(i * 30),
|
||||
Heading: 0, GridID: 1,
|
||||
},
|
||||
Name: "Zone Node",
|
||||
Description: "Zone scalability test",
|
||||
}
|
||||
manager.CreateGroundSpawn(config)
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
for b.Loop() {
|
||||
_ = manager.GetGroundSpawnsByZone(1)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
523
internal/ground_spawn/concurrency_test.go
Normal file
523
internal/ground_spawn/concurrency_test.go
Normal file
@ -0,0 +1,523 @@
|
||||
package ground_spawn
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Mock implementations are in test_utils.go
|
||||
|
||||
// Stress test GroundSpawn with concurrent operations
|
||||
func TestGroundSpawnConcurrency(t *testing.T) {
|
||||
config := GroundSpawnConfig{
|
||||
GroundSpawnID: 1,
|
||||
CollectionSkill: SkillGathering,
|
||||
NumberHarvests: 10,
|
||||
AttemptsPerHarvest: 2,
|
||||
RandomizeHeading: true,
|
||||
Location: SpawnLocation{
|
||||
X: 100.0, Y: 200.0, Z: 300.0, Heading: 45.0, GridID: 1,
|
||||
},
|
||||
Name: "Test Node",
|
||||
Description: "A test harvestable node",
|
||||
}
|
||||
|
||||
gs := NewGroundSpawn(config)
|
||||
|
||||
const numGoroutines = 100
|
||||
const operationsPerGoroutine = 100
|
||||
|
||||
var wg sync.WaitGroup
|
||||
|
||||
t.Run("ConcurrentGetterSetterOperations", func(t *testing.T) {
|
||||
wg.Add(numGoroutines)
|
||||
|
||||
for i := range numGoroutines {
|
||||
go func(goroutineID int) {
|
||||
defer wg.Done()
|
||||
|
||||
for j := range operationsPerGoroutine {
|
||||
switch j % 8 {
|
||||
case 0:
|
||||
gs.SetNumberHarvests(int8(goroutineID % 10))
|
||||
case 1:
|
||||
_ = gs.GetNumberHarvests()
|
||||
case 2:
|
||||
gs.SetAttemptsPerHarvest(int8(goroutineID % 5))
|
||||
case 3:
|
||||
_ = gs.GetAttemptsPerHarvest()
|
||||
case 4:
|
||||
gs.SetCollectionSkill(SkillMining)
|
||||
case 5:
|
||||
_ = gs.GetCollectionSkill()
|
||||
case 6:
|
||||
gs.SetRandomizeHeading(goroutineID%2 == 0)
|
||||
case 7:
|
||||
_ = gs.GetRandomizeHeading()
|
||||
}
|
||||
}
|
||||
}(i)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
})
|
||||
|
||||
t.Run("ConcurrentStateChecks", func(t *testing.T) {
|
||||
wg.Add(numGoroutines)
|
||||
|
||||
for i := range numGoroutines {
|
||||
go func(goroutineID int) {
|
||||
defer wg.Done()
|
||||
|
||||
for j := range operationsPerGoroutine {
|
||||
switch j % 4 {
|
||||
case 0:
|
||||
_ = gs.IsDepleted()
|
||||
case 1:
|
||||
_ = gs.IsAvailable()
|
||||
case 2:
|
||||
_ = gs.GetHarvestMessageName(true, false)
|
||||
case 3:
|
||||
_ = gs.GetHarvestSpellType()
|
||||
}
|
||||
}
|
||||
}(i)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
})
|
||||
|
||||
t.Run("ConcurrentCopyOperations", func(t *testing.T) {
|
||||
wg.Add(numGoroutines)
|
||||
|
||||
for i := range numGoroutines {
|
||||
go func(goroutineID int) {
|
||||
defer wg.Done()
|
||||
|
||||
for j := range operationsPerGoroutine {
|
||||
// Test concurrent copying while modifying
|
||||
if j%2 == 0 {
|
||||
copy := gs.Copy()
|
||||
if copy == nil {
|
||||
t.Errorf("Goroutine %d: Copy returned nil", goroutineID)
|
||||
}
|
||||
} else {
|
||||
gs.SetNumberHarvests(int8(goroutineID % 5))
|
||||
}
|
||||
}
|
||||
}(i)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
})
|
||||
|
||||
t.Run("ConcurrentRespawnOperations", func(t *testing.T) {
|
||||
wg.Add(numGoroutines)
|
||||
|
||||
for i := range numGoroutines {
|
||||
go func(goroutineID int) {
|
||||
defer wg.Done()
|
||||
|
||||
for j := range operationsPerGoroutine {
|
||||
if j%10 == 0 {
|
||||
gs.Respawn()
|
||||
} else {
|
||||
// Mix of reads and writes during respawn
|
||||
switch j % 4 {
|
||||
case 0:
|
||||
_ = gs.GetNumberHarvests()
|
||||
case 1:
|
||||
gs.SetNumberHarvests(int8(goroutineID % 3))
|
||||
case 2:
|
||||
_ = gs.IsAvailable()
|
||||
case 3:
|
||||
_ = gs.IsDepleted()
|
||||
}
|
||||
}
|
||||
}
|
||||
}(i)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
})
|
||||
}
|
||||
|
||||
// Stress test Manager with concurrent operations
|
||||
func TestManagerConcurrency(t *testing.T) {
|
||||
manager := NewManager(nil, &mockLogger{})
|
||||
|
||||
// Pre-populate with some ground spawns
|
||||
for i := int32(1); i <= 10; i++ {
|
||||
config := GroundSpawnConfig{
|
||||
GroundSpawnID: i,
|
||||
CollectionSkill: SkillGathering,
|
||||
NumberHarvests: 5,
|
||||
AttemptsPerHarvest: 1,
|
||||
Location: SpawnLocation{
|
||||
X: float32(i * 10), Y: float32(i * 20), Z: float32(i * 30),
|
||||
Heading: float32(i * 45), GridID: 1,
|
||||
},
|
||||
Name: "Test Node",
|
||||
Description: "Test node",
|
||||
}
|
||||
gs := manager.CreateGroundSpawn(config)
|
||||
if gs == nil {
|
||||
t.Fatalf("Failed to create ground spawn %d", i)
|
||||
}
|
||||
}
|
||||
|
||||
const numGoroutines = 100
|
||||
const operationsPerGoroutine = 100
|
||||
|
||||
var wg sync.WaitGroup
|
||||
|
||||
t.Run("ConcurrentGroundSpawnAccess", func(t *testing.T) {
|
||||
wg.Add(numGoroutines)
|
||||
|
||||
for i := range numGoroutines {
|
||||
go func(goroutineID int) {
|
||||
defer wg.Done()
|
||||
|
||||
for j := range operationsPerGoroutine {
|
||||
spawnID := int32((goroutineID % 10) + 1)
|
||||
|
||||
switch j % 5 {
|
||||
case 0:
|
||||
_ = manager.GetGroundSpawn(spawnID)
|
||||
case 1:
|
||||
_ = manager.GetGroundSpawnsByZone(1)
|
||||
case 2:
|
||||
_ = manager.GetGroundSpawnCount()
|
||||
case 3:
|
||||
_ = manager.GetActiveGroundSpawns()
|
||||
case 4:
|
||||
_ = manager.GetDepletedGroundSpawns()
|
||||
}
|
||||
}
|
||||
}(i)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
})
|
||||
|
||||
t.Run("ConcurrentStatisticsOperations", func(t *testing.T) {
|
||||
wg.Add(numGoroutines)
|
||||
|
||||
for i := range numGoroutines {
|
||||
go func(goroutineID int) {
|
||||
defer wg.Done()
|
||||
|
||||
for j := range operationsPerGoroutine {
|
||||
switch j % 3 {
|
||||
case 0:
|
||||
_ = manager.GetStatistics()
|
||||
case 1:
|
||||
manager.ResetStatistics()
|
||||
case 2:
|
||||
// Simulate harvest statistics updates
|
||||
manager.mutex.Lock()
|
||||
manager.totalHarvests++
|
||||
manager.successfulHarvests++
|
||||
skill := SkillGathering
|
||||
manager.harvestsBySkill[skill]++
|
||||
manager.mutex.Unlock()
|
||||
}
|
||||
}
|
||||
}(i)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
// Verify statistics consistency
|
||||
stats := manager.GetStatistics()
|
||||
if stats.TotalHarvests < 0 || stats.SuccessfulHarvests < 0 {
|
||||
t.Errorf("Invalid statistics: total=%d, successful=%d",
|
||||
stats.TotalHarvests, stats.SuccessfulHarvests)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("ConcurrentGroundSpawnModification", func(t *testing.T) {
|
||||
wg.Add(numGoroutines)
|
||||
|
||||
for i := range numGoroutines {
|
||||
go func(goroutineID int) {
|
||||
defer wg.Done()
|
||||
|
||||
for j := range operationsPerGoroutine {
|
||||
// Use a more unique ID generation strategy to avoid conflicts
|
||||
// Start at 10000 and use goroutine*1000 + iteration to ensure uniqueness
|
||||
newID := int32(10000 + goroutineID*1000 + j)
|
||||
|
||||
config := GroundSpawnConfig{
|
||||
GroundSpawnID: newID,
|
||||
CollectionSkill: SkillMining,
|
||||
NumberHarvests: 3,
|
||||
AttemptsPerHarvest: 1,
|
||||
Location: SpawnLocation{
|
||||
X: float32(j), Y: float32(j * 2), Z: float32(j * 3),
|
||||
Heading: float32(j * 10), GridID: 1,
|
||||
},
|
||||
Name: "Concurrent Node",
|
||||
Description: "Concurrent test node",
|
||||
}
|
||||
|
||||
// Add ground spawn - note that CreateGroundSpawn overwrites the ID
|
||||
gs := manager.CreateGroundSpawn(config)
|
||||
if gs == nil {
|
||||
t.Errorf("Goroutine %d: Failed to create ground spawn", goroutineID)
|
||||
continue
|
||||
}
|
||||
|
||||
// Since CreateGroundSpawn assigns its own ID, we need to get the actual ID
|
||||
actualID := gs.GetID()
|
||||
|
||||
// Verify it was added with the manager-assigned ID
|
||||
retrieved := manager.GetGroundSpawn(actualID)
|
||||
if retrieved == nil {
|
||||
t.Errorf("Goroutine %d: Failed to retrieve ground spawn %d", goroutineID, actualID)
|
||||
}
|
||||
}
|
||||
}(i)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
})
|
||||
|
||||
t.Run("ConcurrentRespawnProcessing", func(t *testing.T) {
|
||||
wg.Add(numGoroutines)
|
||||
|
||||
for i := range numGoroutines {
|
||||
go func(goroutineID int) {
|
||||
defer wg.Done()
|
||||
|
||||
for j := range operationsPerGoroutine {
|
||||
if j%50 == 0 {
|
||||
// Process respawns occasionally
|
||||
manager.ProcessRespawns()
|
||||
} else {
|
||||
// Schedule respawns
|
||||
spawnID := int32((goroutineID % 10) + 1)
|
||||
if gs := manager.GetGroundSpawn(spawnID); gs != nil {
|
||||
if gs.IsDepleted() {
|
||||
manager.scheduleRespawn(gs)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}(i)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
})
|
||||
}
|
||||
|
||||
// Test for potential deadlocks
|
||||
func TestDeadlockPrevention(t *testing.T) {
|
||||
manager := NewManager(nil, &mockLogger{})
|
||||
|
||||
// Create test ground spawns
|
||||
for i := int32(1); i <= 10; i++ {
|
||||
config := GroundSpawnConfig{
|
||||
GroundSpawnID: i,
|
||||
CollectionSkill: SkillGathering,
|
||||
NumberHarvests: 5,
|
||||
AttemptsPerHarvest: 1,
|
||||
Location: SpawnLocation{
|
||||
X: float32(i * 10), Y: float32(i * 20), Z: float32(i * 30),
|
||||
Heading: 0, GridID: 1,
|
||||
},
|
||||
Name: "Deadlock Test Node",
|
||||
Description: "Test node",
|
||||
}
|
||||
gs := manager.CreateGroundSpawn(config)
|
||||
if gs == nil {
|
||||
t.Fatalf("Failed to create ground spawn %d", i)
|
||||
}
|
||||
}
|
||||
|
||||
const numGoroutines = 50
|
||||
var wg sync.WaitGroup
|
||||
|
||||
// Test potential deadlock scenarios
|
||||
t.Run("MixedOperations", func(t *testing.T) {
|
||||
done := make(chan bool, 1)
|
||||
|
||||
// Set a timeout to detect deadlocks
|
||||
go func() {
|
||||
time.Sleep(10 * time.Second)
|
||||
select {
|
||||
case <-done:
|
||||
return
|
||||
default:
|
||||
t.Error("Potential deadlock detected - test timed out")
|
||||
}
|
||||
}()
|
||||
|
||||
wg.Add(numGoroutines)
|
||||
|
||||
for i := range numGoroutines {
|
||||
go func(goroutineID int) {
|
||||
defer wg.Done()
|
||||
|
||||
for j := range 100 {
|
||||
spawnID := int32((goroutineID % 10) + 1)
|
||||
|
||||
// Mix operations that could potentially deadlock
|
||||
switch j % 8 {
|
||||
case 0:
|
||||
gs := manager.GetGroundSpawn(spawnID)
|
||||
if gs != nil {
|
||||
_ = gs.GetNumberHarvests()
|
||||
}
|
||||
case 1:
|
||||
_ = manager.GetStatistics()
|
||||
case 2:
|
||||
_ = manager.GetGroundSpawnsByZone(1)
|
||||
case 3:
|
||||
gs := manager.GetGroundSpawn(spawnID)
|
||||
if gs != nil {
|
||||
gs.SetNumberHarvests(int8(j % 5))
|
||||
}
|
||||
case 4:
|
||||
manager.ProcessRespawns()
|
||||
case 5:
|
||||
_ = manager.GetActiveGroundSpawns()
|
||||
case 6:
|
||||
gs := manager.GetGroundSpawn(spawnID)
|
||||
if gs != nil {
|
||||
_ = gs.Copy()
|
||||
}
|
||||
case 7:
|
||||
gs := manager.GetGroundSpawn(spawnID)
|
||||
if gs != nil && gs.IsDepleted() {
|
||||
manager.scheduleRespawn(gs)
|
||||
}
|
||||
}
|
||||
}
|
||||
}(i)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
done <- true
|
||||
})
|
||||
}
|
||||
|
||||
// Race condition detection test - run with -race flag
|
||||
func TestRaceConditions(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("Skipping race condition test in short mode")
|
||||
}
|
||||
|
||||
manager := NewManager(nil, &mockLogger{})
|
||||
|
||||
// Rapid concurrent operations to trigger race conditions
|
||||
const numGoroutines = 200
|
||||
const operationsPerGoroutine = 50
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(numGoroutines)
|
||||
|
||||
for i := range numGoroutines {
|
||||
go func(goroutineID int) {
|
||||
defer wg.Done()
|
||||
|
||||
for j := range operationsPerGoroutine {
|
||||
// Use unique IDs to avoid conflicts in rapid creation
|
||||
uniqueID := int32(20000 + goroutineID*1000 + j)
|
||||
|
||||
// Rapid-fire operations
|
||||
config := GroundSpawnConfig{
|
||||
GroundSpawnID: uniqueID,
|
||||
CollectionSkill: SkillGathering,
|
||||
NumberHarvests: 3,
|
||||
AttemptsPerHarvest: 1,
|
||||
Location: SpawnLocation{
|
||||
X: float32(j), Y: float32(j * 2), Z: float32(j * 3),
|
||||
Heading: 0, GridID: 1,
|
||||
},
|
||||
Name: "Race Test Node",
|
||||
Description: "Race test",
|
||||
}
|
||||
|
||||
gs := manager.CreateGroundSpawn(config)
|
||||
if gs != nil {
|
||||
actualID := gs.GetID() // Get the manager-assigned ID
|
||||
gs.SetNumberHarvests(int8(j%5 + 1))
|
||||
_ = gs.GetNumberHarvests()
|
||||
_ = gs.IsAvailable()
|
||||
copy := gs.Copy()
|
||||
if copy != nil {
|
||||
copy.SetCollectionSkill(SkillMining)
|
||||
}
|
||||
|
||||
_ = manager.GetGroundSpawn(actualID)
|
||||
}
|
||||
|
||||
_ = manager.GetStatistics()
|
||||
manager.ProcessRespawns()
|
||||
}
|
||||
}(i)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
// Specific test for Copy() method mutex safety
|
||||
func TestCopyMutexSafety(t *testing.T) {
|
||||
config := GroundSpawnConfig{
|
||||
GroundSpawnID: 1,
|
||||
CollectionSkill: SkillGathering,
|
||||
NumberHarvests: 5,
|
||||
AttemptsPerHarvest: 1,
|
||||
Location: SpawnLocation{
|
||||
X: 100, Y: 200, Z: 300, Heading: 45, GridID: 1,
|
||||
},
|
||||
Name: "Copy Test Node",
|
||||
Description: "Test node for copy safety",
|
||||
}
|
||||
|
||||
original := NewGroundSpawn(config)
|
||||
|
||||
const numGoroutines = 100
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(numGoroutines)
|
||||
|
||||
// Test copying while modifying
|
||||
for i := range numGoroutines {
|
||||
go func(goroutineID int) {
|
||||
defer wg.Done()
|
||||
|
||||
for j := range 100 {
|
||||
if j%2 == 0 {
|
||||
// Copy operations
|
||||
copy := original.Copy()
|
||||
if copy == nil {
|
||||
t.Errorf("Goroutine %d: Copy returned nil", goroutineID)
|
||||
continue
|
||||
}
|
||||
|
||||
// Verify copy is independent by setting a unique value
|
||||
expectedValue := int8(goroutineID%5 + 1) // Ensure non-zero value
|
||||
copy.SetNumberHarvests(expectedValue)
|
||||
|
||||
// Verify the copy has the value we set
|
||||
if copy.GetNumberHarvests() != expectedValue {
|
||||
t.Errorf("Goroutine %d: Copy failed to set value correctly, expected %d got %d",
|
||||
goroutineID, expectedValue, copy.GetNumberHarvests())
|
||||
}
|
||||
|
||||
// Copy independence is verified by the fact that we can set different values
|
||||
// We don't check against original since other goroutines are modifying it concurrently
|
||||
} else {
|
||||
// Modify original
|
||||
original.SetNumberHarvests(int8(goroutineID % 10))
|
||||
original.SetCollectionSkill(SkillMining)
|
||||
_ = original.GetRandomizeHeading()
|
||||
}
|
||||
}
|
||||
}(i)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
}
|
333
internal/ground_spawn/core_concurrency_test.go
Normal file
333
internal/ground_spawn/core_concurrency_test.go
Normal file
@ -0,0 +1,333 @@
|
||||
package ground_spawn
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Mock implementations are in test_utils.go
|
||||
|
||||
// Test core GroundSpawn concurrency patterns without dependencies
|
||||
func TestGroundSpawnCoreConcurrency(t *testing.T) {
|
||||
config := GroundSpawnConfig{
|
||||
GroundSpawnID: 1,
|
||||
CollectionSkill: SkillGathering,
|
||||
NumberHarvests: 10,
|
||||
AttemptsPerHarvest: 2,
|
||||
RandomizeHeading: true,
|
||||
Location: SpawnLocation{
|
||||
X: 100.0, Y: 200.0, Z: 300.0, Heading: 45.0, GridID: 1,
|
||||
},
|
||||
Name: "Test Node",
|
||||
Description: "A test harvestable node",
|
||||
}
|
||||
|
||||
gs := NewGroundSpawn(config)
|
||||
|
||||
const numGoroutines = 100
|
||||
const operationsPerGoroutine = 100
|
||||
|
||||
var wg sync.WaitGroup
|
||||
|
||||
t.Run("ConcurrentAccessors", func(t *testing.T) {
|
||||
wg.Add(numGoroutines)
|
||||
|
||||
for i := 0; i < numGoroutines; i++ {
|
||||
go func(goroutineID int) {
|
||||
defer wg.Done()
|
||||
|
||||
for j := 0; j < operationsPerGoroutine; j++ {
|
||||
switch j % 8 {
|
||||
case 0:
|
||||
gs.SetNumberHarvests(int8(goroutineID % 10))
|
||||
case 1:
|
||||
_ = gs.GetNumberHarvests()
|
||||
case 2:
|
||||
gs.SetAttemptsPerHarvest(int8(goroutineID % 5))
|
||||
case 3:
|
||||
_ = gs.GetAttemptsPerHarvest()
|
||||
case 4:
|
||||
gs.SetCollectionSkill(SkillMining)
|
||||
case 5:
|
||||
_ = gs.GetCollectionSkill()
|
||||
case 6:
|
||||
gs.SetRandomizeHeading(goroutineID%2 == 0)
|
||||
case 7:
|
||||
_ = gs.GetRandomizeHeading()
|
||||
}
|
||||
}
|
||||
}(i)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
})
|
||||
|
||||
t.Run("ConcurrentStateChecks", func(t *testing.T) {
|
||||
wg.Add(numGoroutines)
|
||||
|
||||
for i := 0; i < numGoroutines; i++ {
|
||||
go func(goroutineID int) {
|
||||
defer wg.Done()
|
||||
|
||||
for j := 0; j < operationsPerGoroutine; j++ {
|
||||
switch j % 4 {
|
||||
case 0:
|
||||
_ = gs.IsDepleted()
|
||||
case 1:
|
||||
_ = gs.IsAvailable()
|
||||
case 2:
|
||||
_ = gs.GetHarvestMessageName(true, false)
|
||||
case 3:
|
||||
_ = gs.GetHarvestSpellType()
|
||||
}
|
||||
}
|
||||
}(i)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
})
|
||||
}
|
||||
|
||||
// Test Manager core concurrency patterns
|
||||
func TestManagerCoreConcurrency(t *testing.T) {
|
||||
manager := NewManager(nil, &mockLogger{})
|
||||
|
||||
// Pre-populate with some ground spawns
|
||||
for i := int32(1); i <= 10; i++ {
|
||||
config := GroundSpawnConfig{
|
||||
GroundSpawnID: i,
|
||||
CollectionSkill: SkillGathering,
|
||||
NumberHarvests: 5,
|
||||
AttemptsPerHarvest: 1,
|
||||
Location: SpawnLocation{
|
||||
X: float32(i * 10), Y: float32(i * 20), Z: float32(i * 30),
|
||||
Heading: float32(i * 45), GridID: 1,
|
||||
},
|
||||
Name: "Test Node",
|
||||
Description: "Test node",
|
||||
}
|
||||
gs := manager.CreateGroundSpawn(config)
|
||||
if gs == nil {
|
||||
t.Fatalf("Failed to create ground spawn %d", i)
|
||||
}
|
||||
}
|
||||
|
||||
const numGoroutines = 50
|
||||
const operationsPerGoroutine = 50
|
||||
|
||||
var wg sync.WaitGroup
|
||||
|
||||
t.Run("ConcurrentAccess", func(t *testing.T) {
|
||||
wg.Add(numGoroutines)
|
||||
|
||||
for i := 0; i < numGoroutines; i++ {
|
||||
go func(goroutineID int) {
|
||||
defer wg.Done()
|
||||
|
||||
for j := 0; j < operationsPerGoroutine; j++ {
|
||||
spawnID := int32((goroutineID % 10) + 1)
|
||||
|
||||
switch j % 5 {
|
||||
case 0:
|
||||
_ = manager.GetGroundSpawn(spawnID)
|
||||
case 1:
|
||||
_ = manager.GetGroundSpawnsByZone(1)
|
||||
case 2:
|
||||
_ = manager.GetGroundSpawnCount()
|
||||
case 3:
|
||||
_ = manager.GetActiveGroundSpawns()
|
||||
case 4:
|
||||
_ = manager.GetDepletedGroundSpawns()
|
||||
}
|
||||
}
|
||||
}(i)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
})
|
||||
|
||||
t.Run("ConcurrentStatistics", func(t *testing.T) {
|
||||
wg.Add(numGoroutines)
|
||||
|
||||
for i := 0; i < numGoroutines; i++ {
|
||||
go func(goroutineID int) {
|
||||
defer wg.Done()
|
||||
|
||||
for j := 0; j < operationsPerGoroutine; j++ {
|
||||
switch j % 3 {
|
||||
case 0:
|
||||
_ = manager.GetStatistics()
|
||||
case 1:
|
||||
manager.ResetStatistics()
|
||||
case 2:
|
||||
// Simulate statistics updates
|
||||
manager.mutex.Lock()
|
||||
manager.totalHarvests++
|
||||
manager.successfulHarvests++
|
||||
skill := SkillGathering
|
||||
manager.harvestsBySkill[skill]++
|
||||
manager.mutex.Unlock()
|
||||
}
|
||||
}
|
||||
}(i)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
// Verify statistics consistency
|
||||
stats := manager.GetStatistics()
|
||||
if stats.TotalHarvests < 0 || stats.SuccessfulHarvests < 0 {
|
||||
t.Errorf("Invalid statistics: total=%d, successful=%d",
|
||||
stats.TotalHarvests, stats.SuccessfulHarvests)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Test Copy() method thread safety
|
||||
func TestCopyThreadSafety(t *testing.T) {
|
||||
config := GroundSpawnConfig{
|
||||
GroundSpawnID: 1,
|
||||
CollectionSkill: SkillGathering,
|
||||
NumberHarvests: 5,
|
||||
AttemptsPerHarvest: 1,
|
||||
Location: SpawnLocation{
|
||||
X: 100, Y: 200, Z: 300, Heading: 45, GridID: 1,
|
||||
},
|
||||
Name: "Copy Test Node",
|
||||
Description: "Test node for copy safety",
|
||||
}
|
||||
|
||||
original := NewGroundSpawn(config)
|
||||
|
||||
const numGoroutines = 50
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(numGoroutines)
|
||||
|
||||
// Test copying while modifying
|
||||
for i := 0; i < numGoroutines; i++ {
|
||||
go func(goroutineID int) {
|
||||
defer wg.Done()
|
||||
|
||||
for j := 0; j < 100; j++ {
|
||||
if j%2 == 0 {
|
||||
// Copy operations
|
||||
copy := original.Copy()
|
||||
if copy == nil {
|
||||
t.Errorf("Goroutine %d: Copy returned nil", goroutineID)
|
||||
continue
|
||||
}
|
||||
|
||||
// Verify copy is independent by setting different values
|
||||
newValue := int8(goroutineID%5 + 1) // Ensure non-zero value
|
||||
copy.SetNumberHarvests(newValue)
|
||||
|
||||
// Copy should have the new value we just set
|
||||
if copy.GetNumberHarvests() != newValue {
|
||||
t.Errorf("Goroutine %d: Copy failed to set value correctly, expected %d got %d",
|
||||
goroutineID, newValue, copy.GetNumberHarvests())
|
||||
}
|
||||
// Note: We can't reliably test that original is unchanged due to concurrent modifications
|
||||
} else {
|
||||
// Modify original
|
||||
original.SetNumberHarvests(int8(goroutineID % 10))
|
||||
original.SetCollectionSkill(SkillMining)
|
||||
_ = original.GetRandomizeHeading()
|
||||
}
|
||||
}
|
||||
}(i)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
// Test core deadlock prevention
|
||||
func TestCoreDeadlockPrevention(t *testing.T) {
|
||||
manager := NewManager(nil, &mockLogger{})
|
||||
|
||||
// Create test ground spawns
|
||||
for i := int32(1); i <= 5; i++ {
|
||||
config := GroundSpawnConfig{
|
||||
GroundSpawnID: i,
|
||||
CollectionSkill: SkillGathering,
|
||||
NumberHarvests: 5,
|
||||
AttemptsPerHarvest: 1,
|
||||
Location: SpawnLocation{
|
||||
X: float32(i * 10), Y: float32(i * 20), Z: float32(i * 30),
|
||||
Heading: 0, GridID: 1,
|
||||
},
|
||||
Name: "Deadlock Test Node",
|
||||
Description: "Test node",
|
||||
}
|
||||
gs := manager.CreateGroundSpawn(config)
|
||||
if gs == nil {
|
||||
t.Fatalf("Failed to create ground spawn %d", i)
|
||||
}
|
||||
}
|
||||
|
||||
const numGoroutines = 25
|
||||
var wg sync.WaitGroup
|
||||
|
||||
// Test potential deadlock scenarios
|
||||
t.Run("MixedOperations", func(t *testing.T) {
|
||||
done := make(chan bool, 1)
|
||||
|
||||
// Set a timeout to detect deadlocks
|
||||
go func() {
|
||||
time.Sleep(5 * time.Second)
|
||||
select {
|
||||
case <-done:
|
||||
return
|
||||
default:
|
||||
t.Error("Potential deadlock detected - test timed out")
|
||||
}
|
||||
}()
|
||||
|
||||
wg.Add(numGoroutines)
|
||||
|
||||
for i := 0; i < numGoroutines; i++ {
|
||||
go func(goroutineID int) {
|
||||
defer wg.Done()
|
||||
|
||||
for j := 0; j < 50; j++ {
|
||||
spawnID := int32((goroutineID % 5) + 1)
|
||||
|
||||
// Mix operations that could potentially deadlock
|
||||
switch j % 8 {
|
||||
case 0:
|
||||
gs := manager.GetGroundSpawn(spawnID)
|
||||
if gs != nil {
|
||||
_ = gs.GetNumberHarvests()
|
||||
}
|
||||
case 1:
|
||||
_ = manager.GetStatistics()
|
||||
case 2:
|
||||
_ = manager.GetGroundSpawnsByZone(1)
|
||||
case 3:
|
||||
gs := manager.GetGroundSpawn(spawnID)
|
||||
if gs != nil {
|
||||
gs.SetNumberHarvests(int8(j % 5))
|
||||
}
|
||||
case 4:
|
||||
manager.ProcessRespawns()
|
||||
case 5:
|
||||
_ = manager.GetActiveGroundSpawns()
|
||||
case 6:
|
||||
gs := manager.GetGroundSpawn(spawnID)
|
||||
if gs != nil {
|
||||
_ = gs.Copy()
|
||||
}
|
||||
case 7:
|
||||
gs := manager.GetGroundSpawn(spawnID)
|
||||
if gs != nil && gs.IsDepleted() {
|
||||
manager.scheduleRespawn(gs)
|
||||
}
|
||||
}
|
||||
}
|
||||
}(i)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
done <- true
|
||||
})
|
||||
}
|
249
internal/ground_spawn/database.go
Normal file
249
internal/ground_spawn/database.go
Normal file
@ -0,0 +1,249 @@
|
||||
package ground_spawn
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"eq2emu/internal/database"
|
||||
)
|
||||
|
||||
// DatabaseAdapter implements the Database interface using the internal database wrapper
|
||||
type DatabaseAdapter struct {
|
||||
db *database.DB
|
||||
}
|
||||
|
||||
// NewDatabaseAdapter creates a new database adapter using the database wrapper
|
||||
func NewDatabaseAdapter(db *database.DB) *DatabaseAdapter {
|
||||
return &DatabaseAdapter{
|
||||
db: db,
|
||||
}
|
||||
}
|
||||
|
||||
// LoadGroundSpawnEntries loads harvest entries for a ground spawn
|
||||
func (da *DatabaseAdapter) LoadGroundSpawnEntries(groundspawnID int32) ([]*GroundSpawnEntry, error) {
|
||||
query := `
|
||||
SELECT min_skill_level, min_adventure_level, bonus_table, harvest_1, harvest_3,
|
||||
harvest_5, harvest_imbue, harvest_rare, harvest_10, harvest_coin
|
||||
FROM groundspawn_entries
|
||||
WHERE groundspawn_id = ?
|
||||
ORDER BY min_skill_level ASC
|
||||
`
|
||||
|
||||
var entries []*GroundSpawnEntry
|
||||
|
||||
err := da.db.Query(query, func(row *database.Row) error {
|
||||
entry := &GroundSpawnEntry{}
|
||||
var bonusTable int32
|
||||
|
||||
entry.MinSkillLevel = int16(row.Int(0))
|
||||
entry.MinAdventureLevel = int16(row.Int(1))
|
||||
bonusTable = int32(row.Int(2))
|
||||
entry.Harvest1 = float32(row.Float(3))
|
||||
entry.Harvest3 = float32(row.Float(4))
|
||||
entry.Harvest5 = float32(row.Float(5))
|
||||
entry.HarvestImbue = float32(row.Float(6))
|
||||
entry.HarvestRare = float32(row.Float(7))
|
||||
entry.Harvest10 = float32(row.Float(8))
|
||||
entry.HarvestCoin = float32(row.Float(9))
|
||||
|
||||
entry.BonusTable = bonusTable == 1
|
||||
entries = append(entries, entry)
|
||||
|
||||
return nil
|
||||
}, groundspawnID)
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to query groundspawn entries: %w", err)
|
||||
}
|
||||
|
||||
return entries, nil
|
||||
}
|
||||
|
||||
// LoadGroundSpawnItems loads harvest items for a ground spawn
|
||||
func (da *DatabaseAdapter) LoadGroundSpawnItems(groundspawnID int32) ([]*GroundSpawnEntryItem, error) {
|
||||
query := `
|
||||
SELECT item_id, is_rare, grid_id, quantity
|
||||
FROM groundspawn_items
|
||||
WHERE groundspawn_id = ?
|
||||
ORDER BY item_id ASC
|
||||
`
|
||||
|
||||
var items []*GroundSpawnEntryItem
|
||||
|
||||
err := da.db.Query(query, func(row *database.Row) error {
|
||||
item := &GroundSpawnEntryItem{}
|
||||
|
||||
item.ItemID = int32(row.Int(0))
|
||||
item.IsRare = int8(row.Int(1))
|
||||
item.GridID = int32(row.Int(2))
|
||||
item.Quantity = int16(row.Int(3))
|
||||
|
||||
items = append(items, item)
|
||||
|
||||
return nil
|
||||
}, groundspawnID)
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to query groundspawn items: %w", err)
|
||||
}
|
||||
|
||||
return items, nil
|
||||
}
|
||||
|
||||
// SaveGroundSpawn saves a ground spawn to the database
|
||||
func (da *DatabaseAdapter) SaveGroundSpawn(gs *GroundSpawn) error {
|
||||
if gs == nil {
|
||||
return fmt.Errorf("ground spawn cannot be nil")
|
||||
}
|
||||
|
||||
query := `
|
||||
INSERT OR REPLACE INTO groundspawns (
|
||||
id, name, x, y, z, heading, respawn_timer, collection_skill,
|
||||
number_harvests, attempts_per_harvest, groundspawn_entry_id,
|
||||
randomize_heading, zone_id, created_at, updated_at
|
||||
)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, datetime('now'), datetime('now'))
|
||||
`
|
||||
|
||||
randomizeHeading := 0
|
||||
if gs.GetRandomizeHeading() {
|
||||
randomizeHeading = 1
|
||||
}
|
||||
|
||||
// TODO: Get actual zone ID from spawn
|
||||
zoneID := int32(1)
|
||||
|
||||
err := da.db.Exec(query,
|
||||
gs.GetID(),
|
||||
gs.GetName(),
|
||||
gs.GetX(),
|
||||
gs.GetY(),
|
||||
gs.GetZ(),
|
||||
int16(gs.GetHeading()),
|
||||
300, // Default 5 minutes respawn timer
|
||||
gs.GetCollectionSkill(),
|
||||
gs.GetNumberHarvests(),
|
||||
gs.GetAttemptsPerHarvest(),
|
||||
gs.GetGroundSpawnEntryID(),
|
||||
randomizeHeading,
|
||||
zoneID,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to save ground spawn %d: %w", gs.GetID(), err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// LoadAllGroundSpawns loads all ground spawns from the database
|
||||
func (da *DatabaseAdapter) LoadAllGroundSpawns() ([]*GroundSpawn, error) {
|
||||
query := `
|
||||
SELECT id, name, x, y, z, heading, collection_skill, number_harvests,
|
||||
attempts_per_harvest, groundspawn_entry_id, randomize_heading
|
||||
FROM groundspawns
|
||||
WHERE zone_id = ?
|
||||
ORDER BY id ASC
|
||||
`
|
||||
|
||||
// TODO: Support multiple zones
|
||||
zoneID := int32(1)
|
||||
|
||||
var groundSpawns []*GroundSpawn
|
||||
|
||||
err := da.db.Query(query, func(row *database.Row) error {
|
||||
id := int32(row.Int(0))
|
||||
name := row.Text(1)
|
||||
x := float32(row.Float(2))
|
||||
y := float32(row.Float(3))
|
||||
z := float32(row.Float(4))
|
||||
heading := float32(row.Float(5))
|
||||
collectionSkill := row.Text(6)
|
||||
numberHarvests := int8(row.Int(7))
|
||||
attemptsPerHarvest := int8(row.Int(8))
|
||||
groundspawnEntryID := int32(row.Int(9))
|
||||
randomizeHeading := int32(row.Int(10))
|
||||
|
||||
config := GroundSpawnConfig{
|
||||
GroundSpawnID: groundspawnEntryID,
|
||||
CollectionSkill: collectionSkill,
|
||||
NumberHarvests: numberHarvests,
|
||||
AttemptsPerHarvest: attemptsPerHarvest,
|
||||
RandomizeHeading: randomizeHeading == 1,
|
||||
Location: SpawnLocation{
|
||||
X: x,
|
||||
Y: y,
|
||||
Z: z,
|
||||
Heading: heading,
|
||||
GridID: 1, // TODO: Load from database
|
||||
},
|
||||
Name: name,
|
||||
Description: fmt.Sprintf("A %s node", collectionSkill),
|
||||
}
|
||||
|
||||
gs := NewGroundSpawn(config)
|
||||
gs.SetID(id)
|
||||
|
||||
groundSpawns = append(groundSpawns, gs)
|
||||
|
||||
return nil
|
||||
}, zoneID)
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to query groundspawns: %w", err)
|
||||
}
|
||||
|
||||
return groundSpawns, nil
|
||||
}
|
||||
|
||||
// DeleteGroundSpawn deletes a ground spawn from the database
|
||||
func (da *DatabaseAdapter) DeleteGroundSpawn(id int32) error {
|
||||
query := `DELETE FROM groundspawns WHERE id = ?`
|
||||
|
||||
err := da.db.Exec(query, id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to delete ground spawn %d: %w", id, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// LoadPlayerHarvestStatistics loads harvest statistics for a player
|
||||
func (da *DatabaseAdapter) LoadPlayerHarvestStatistics(playerID int32) (map[string]int64, error) {
|
||||
query := `
|
||||
SELECT skill_name, harvest_count
|
||||
FROM player_harvest_stats
|
||||
WHERE player_id = ?
|
||||
`
|
||||
|
||||
stats := make(map[string]int64)
|
||||
|
||||
err := da.db.Query(query, func(row *database.Row) error {
|
||||
skillName := row.Text(0)
|
||||
harvestCount := row.Int64(1)
|
||||
|
||||
stats[skillName] = harvestCount
|
||||
|
||||
return nil
|
||||
}, playerID)
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to query player harvest stats: %w", err)
|
||||
}
|
||||
|
||||
return stats, nil
|
||||
}
|
||||
|
||||
// SavePlayerHarvestStatistic saves a player's harvest statistic
|
||||
func (da *DatabaseAdapter) SavePlayerHarvestStatistic(playerID int32, skillName string, count int64) error {
|
||||
query := `
|
||||
INSERT OR REPLACE INTO player_harvest_stats (player_id, skill_name, harvest_count, updated_at)
|
||||
VALUES (?, ?, ?, datetime('now'))
|
||||
`
|
||||
|
||||
err := da.db.Exec(query, playerID, skillName, count)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to save player harvest stat: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
@ -24,18 +24,20 @@ func NewGroundSpawn(config GroundSpawnConfig) *GroundSpawn {
|
||||
// Configure base spawn properties
|
||||
gs.SetName(config.Name)
|
||||
gs.SetSpawnType(DefaultSpawnType)
|
||||
gs.SetDifficulty(DefaultDifficulty)
|
||||
gs.SetState(DefaultState)
|
||||
// Note: SetDifficulty and SetState methods not available in spawn interface
|
||||
|
||||
// Set position
|
||||
gs.SetX(config.Location.X)
|
||||
gs.SetY(config.Location.Y)
|
||||
gs.SetY(config.Location.Y, false)
|
||||
gs.SetZ(config.Location.Z)
|
||||
|
||||
if config.RandomizeHeading {
|
||||
gs.SetHeading(rand.Float32() * 360.0)
|
||||
// Convert float32 to int16 for heading
|
||||
heading := int16(rand.Float32() * 360.0)
|
||||
gs.SetHeading(heading, heading)
|
||||
} else {
|
||||
gs.SetHeading(config.Location.Heading)
|
||||
heading := int16(config.Location.Heading)
|
||||
gs.SetHeading(heading, heading)
|
||||
}
|
||||
|
||||
return gs
|
||||
@ -47,12 +49,14 @@ func (gs *GroundSpawn) Copy() *GroundSpawn {
|
||||
defer gs.harvestMutex.Unlock()
|
||||
|
||||
newSpawn := &GroundSpawn{
|
||||
Spawn: gs.Spawn.Copy().(*spawn.Spawn),
|
||||
Spawn: gs.Spawn, // TODO: Implement proper copy when spawn.Copy() is available
|
||||
numberHarvests: gs.numberHarvests,
|
||||
numAttemptsPerHarvest: gs.numAttemptsPerHarvest,
|
||||
groundspawnID: gs.groundspawnID,
|
||||
collectionSkill: gs.collectionSkill,
|
||||
randomizeHeading: gs.randomizeHeading,
|
||||
// Reset mutexes in the copy to avoid sharing the same mutex instances
|
||||
// harvestMutex and harvestUseMutex are zero-initialized (correct behavior)
|
||||
}
|
||||
|
||||
return newSpawn
|
||||
@ -252,14 +256,14 @@ func (gs *GroundSpawn) ProcessHarvest(context *HarvestContext) (*HarvestResult,
|
||||
}
|
||||
|
||||
// Validate harvest data
|
||||
if context.GroundSpawnEntries == nil || len(context.GroundSpawnEntries) == 0 {
|
||||
if len(context.GroundSpawnEntries) == 0 {
|
||||
return &HarvestResult{
|
||||
Success: false,
|
||||
MessageText: fmt.Sprintf("Error: No groundspawn entries assigned to groundspawn id: %d", gs.groundspawnID),
|
||||
}, nil
|
||||
}
|
||||
|
||||
if context.GroundSpawnItems == nil || len(context.GroundSpawnItems) == 0 {
|
||||
if len(context.GroundSpawnItems) == 0 {
|
||||
return &HarvestResult{
|
||||
Success: false,
|
||||
MessageText: fmt.Sprintf("Error: No groundspawn items assigned to groundspawn id: %d", gs.groundspawnID),
|
||||
@ -351,7 +355,7 @@ func (gs *GroundSpawn) filterHarvestTables(context *HarvestContext) []*GroundSpa
|
||||
}
|
||||
|
||||
// Check level requirement for bonus tables
|
||||
if entry.BonusTable && context.Player.GetLevel() < entry.MinAdventureLevel {
|
||||
if entry.BonusTable && (*context.Player).GetLevel() < entry.MinAdventureLevel {
|
||||
continue
|
||||
}
|
||||
|
||||
@ -441,9 +445,9 @@ func (gs *GroundSpawn) awardHarvestItems(harvestType int8, availableItems []*Gro
|
||||
var items []*HarvestedItem
|
||||
|
||||
// Filter items based on harvest type and player location
|
||||
normalItems := gs.filterItems(availableItems, ItemRarityNormal, player.GetLocation())
|
||||
rareItems := gs.filterItems(availableItems, ItemRarityRare, player.GetLocation())
|
||||
imbueItems := gs.filterItems(availableItems, ItemRarityImbue, player.GetLocation())
|
||||
normalItems := gs.filterItems(availableItems, ItemRarityNormal, (*player).GetLocation())
|
||||
rareItems := gs.filterItems(availableItems, ItemRarityRare, (*player).GetLocation())
|
||||
imbueItems := gs.filterItems(availableItems, ItemRarityImbue, (*player).GetLocation())
|
||||
|
||||
switch harvestType {
|
||||
case HarvestType1Item:
|
||||
@ -522,7 +526,7 @@ func (gs *GroundSpawn) handleSkillProgression(context *HarvestContext, table *Gr
|
||||
// Check if player skill is already at max for this node
|
||||
maxSkillAllowed := int16(float32(context.MaxSkillRequired) * 1.0) // TODO: Use skill multiplier rule
|
||||
|
||||
if context.PlayerSkill.GetCurrentValue() >= maxSkillAllowed {
|
||||
if (*context.PlayerSkill).GetCurrentValue() >= maxSkillAllowed {
|
||||
return false
|
||||
}
|
||||
|
||||
@ -579,7 +583,7 @@ func (gs *GroundSpawn) handleHarvestUse(client Client) error {
|
||||
|
||||
if client.GetLogger() != nil {
|
||||
client.GetLogger().LogDebug("Player %s attempting to harvest %s using spell %s",
|
||||
client.GetPlayer().GetName(), gs.GetName(), spellName)
|
||||
(*client.GetPlayer()).GetName(), gs.GetName(), spellName)
|
||||
}
|
||||
|
||||
return nil
|
||||
@ -595,7 +599,7 @@ func (gs *GroundSpawn) handleCommandUse(client Client, command string) error {
|
||||
|
||||
if client.GetLogger() != nil {
|
||||
client.GetLogger().LogDebug("Player %s using command %s on %s",
|
||||
client.GetPlayer().GetName(), command, gs.GetName())
|
||||
(*client.GetPlayer()).GetName(), command, gs.GetName())
|
||||
}
|
||||
|
||||
return nil
|
||||
@ -603,8 +607,9 @@ func (gs *GroundSpawn) handleCommandUse(client Client, command string) error {
|
||||
|
||||
// Serialize creates a packet representation of the ground spawn
|
||||
func (gs *GroundSpawn) Serialize(player *Player, version int16) ([]byte, error) {
|
||||
// Use base spawn serialization
|
||||
return gs.Spawn.Serialize(player, version)
|
||||
// TODO: Implement proper ground spawn serialization when spawn.Serialize is available
|
||||
// For now, return empty packet as placeholder
|
||||
return make([]byte, 0), nil
|
||||
}
|
||||
|
||||
// Respawn resets the ground spawn to harvestable state
|
||||
@ -617,9 +622,24 @@ func (gs *GroundSpawn) Respawn() {
|
||||
|
||||
// Randomize heading if configured
|
||||
if gs.randomizeHeading {
|
||||
gs.SetHeading(rand.Float32() * 360.0)
|
||||
heading := int16(rand.Float32() * 360.0)
|
||||
gs.SetHeading(heading, heading)
|
||||
}
|
||||
|
||||
// Mark as alive
|
||||
gs.SetAlive(true)
|
||||
}
|
||||
|
||||
// MeetsSpawnAccessRequirements checks if a player can access this ground spawn
|
||||
func (gs *GroundSpawn) MeetsSpawnAccessRequirements(player *Player) bool {
|
||||
// TODO: Implement proper access requirements checking
|
||||
// For now, allow all players to access ground spawns
|
||||
return player != nil
|
||||
}
|
||||
|
||||
// HasCommandIcon returns true if this ground spawn has command interactions
|
||||
func (gs *GroundSpawn) HasCommandIcon() bool {
|
||||
// TODO: Implement command icon checking based on spawn configuration
|
||||
// For now, ground spawns don't have command icons (only harvest)
|
||||
return false
|
||||
}
|
||||
|
@ -11,10 +11,10 @@ type Database interface {
|
||||
|
||||
// Logger interface for ground spawn logging
|
||||
type Logger interface {
|
||||
LogInfo(message string, args ...interface{})
|
||||
LogError(message string, args ...interface{})
|
||||
LogDebug(message string, args ...interface{})
|
||||
LogWarning(message string, args ...interface{})
|
||||
LogInfo(message string, args ...any)
|
||||
LogError(message string, args ...any)
|
||||
LogDebug(message string, args ...any)
|
||||
LogWarning(message string, args ...any)
|
||||
}
|
||||
|
||||
// Player interface for ground spawn interactions
|
||||
@ -35,7 +35,7 @@ type Client interface {
|
||||
GetVersion() int16
|
||||
GetLogger() Logger
|
||||
GetCurrentZoneID() int32
|
||||
Message(channel int32, message string, args ...interface{})
|
||||
Message(channel int32, message string, args ...any)
|
||||
SimpleMessage(channel int32, message string)
|
||||
SendPopupMessage(type_ int32, message string, sound string, duration float32, r, g, b int32)
|
||||
AddItem(item *Item, itemDeleted *bool) error
|
||||
@ -126,8 +126,8 @@ type SkillProvider interface {
|
||||
|
||||
// SpawnProvider interface for spawn system integration
|
||||
type SpawnProvider interface {
|
||||
CreateSpawn() interface{}
|
||||
GetSpawn(id int32) interface{}
|
||||
CreateSpawn() any
|
||||
GetSpawn(id int32) any
|
||||
RegisterGroundSpawn(gs *GroundSpawn) error
|
||||
UnregisterGroundSpawn(id int32) error
|
||||
}
|
||||
@ -168,7 +168,7 @@ func (pgsa *PlayerGroundSpawnAdapter) CanHarvest(gs *GroundSpawn) bool {
|
||||
}
|
||||
|
||||
// Check if player has required skill
|
||||
skill := pgsa.player.GetSkillByName(gs.GetCollectionSkill())
|
||||
skill := (*pgsa.player).GetSkillByName(gs.GetCollectionSkill())
|
||||
if skill == nil {
|
||||
return false
|
||||
}
|
||||
@ -184,7 +184,7 @@ func (pgsa *PlayerGroundSpawnAdapter) GetHarvestSkill(skillName string) *Skill {
|
||||
return nil
|
||||
}
|
||||
|
||||
return pgsa.player.GetSkillByName(skillName)
|
||||
return (*pgsa.player).GetSkillByName(skillName)
|
||||
}
|
||||
|
||||
// GetHarvestModifiers returns harvest modifiers for the player
|
||||
@ -207,7 +207,7 @@ func (pgsa *PlayerGroundSpawnAdapter) OnHarvestResult(result *HarvestResult) {
|
||||
if result.Success && len(result.ItemsAwarded) > 0 {
|
||||
if pgsa.logger != nil {
|
||||
pgsa.logger.LogDebug("Player %s successfully harvested %d items",
|
||||
pgsa.player.GetName(), len(result.ItemsAwarded))
|
||||
(*pgsa.player).GetName(), len(result.ItemsAwarded))
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -227,7 +227,7 @@ func NewHarvestEventAdapter(handler HarvestHandler, logger Logger) *HarvestEvent
|
||||
}
|
||||
|
||||
// ProcessHarvestEvent processes a harvest event
|
||||
func (hea *HarvestEventAdapter) ProcessHarvestEvent(eventType string, gs *GroundSpawn, player *Player, data interface{}) {
|
||||
func (hea *HarvestEventAdapter) ProcessHarvestEvent(eventType string, gs *GroundSpawn, player *Player, data any) {
|
||||
if hea.handler == nil {
|
||||
return
|
||||
}
|
||||
|
@ -13,8 +13,11 @@ func NewManager(database Database, logger Logger) *Manager {
|
||||
entriesByID: make(map[int32][]*GroundSpawnEntry),
|
||||
itemsByID: make(map[int32][]*GroundSpawnEntryItem),
|
||||
respawnQueue: make(map[int32]time.Time),
|
||||
activeSpawns: make(map[int32]*GroundSpawn),
|
||||
depletedSpawns: make(map[int32]*GroundSpawn),
|
||||
database: database,
|
||||
logger: logger,
|
||||
nextSpawnID: 1,
|
||||
harvestsBySkill: make(map[string]int64),
|
||||
}
|
||||
}
|
||||
@ -41,8 +44,22 @@ func (m *Manager) Initialize() error {
|
||||
m.mutex.Lock()
|
||||
defer m.mutex.Unlock()
|
||||
|
||||
var maxID int32
|
||||
for _, gs := range groundSpawns {
|
||||
m.groundSpawns[gs.GetID()] = gs
|
||||
spawnID := gs.GetID()
|
||||
m.groundSpawns[spawnID] = gs
|
||||
|
||||
// Track max ID for nextSpawnID initialization
|
||||
if spawnID > maxID {
|
||||
maxID = spawnID
|
||||
}
|
||||
|
||||
// Populate active/depleted caches based on current state
|
||||
if gs.IsAvailable() {
|
||||
m.activeSpawns[spawnID] = gs
|
||||
} else if gs.IsDepleted() {
|
||||
m.depletedSpawns[spawnID] = gs
|
||||
}
|
||||
|
||||
// Group by zone (placeholder - zone ID would come from spawn location)
|
||||
zoneID := int32(1) // TODO: Get actual zone ID from spawn
|
||||
@ -50,10 +67,13 @@ func (m *Manager) Initialize() error {
|
||||
|
||||
// Load harvest entries and items
|
||||
if err := m.loadGroundSpawnData(gs); err != nil && m.logger != nil {
|
||||
m.logger.LogWarning("Failed to load data for ground spawn %d: %v", gs.GetID(), err)
|
||||
m.logger.LogWarning("Failed to load data for ground spawn %d: %v", spawnID, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Set nextSpawnID to avoid collisions
|
||||
m.nextSpawnID = maxID + 1
|
||||
|
||||
if m.logger != nil {
|
||||
m.logger.LogInfo("Loaded %d ground spawns from database", len(groundSpawns))
|
||||
}
|
||||
@ -89,15 +109,24 @@ func (m *Manager) CreateGroundSpawn(config GroundSpawnConfig) *GroundSpawn {
|
||||
m.mutex.Lock()
|
||||
defer m.mutex.Unlock()
|
||||
|
||||
// Generate ID (placeholder implementation)
|
||||
newID := int32(len(m.groundSpawns) + 1)
|
||||
// Use efficient ID counter instead of len()
|
||||
newID := m.nextSpawnID
|
||||
m.nextSpawnID++
|
||||
gs.SetID(newID)
|
||||
|
||||
// Store ground spawn
|
||||
m.groundSpawns[newID] = gs
|
||||
|
||||
// Group by zone
|
||||
// Add to active cache (new spawns are typically active)
|
||||
if gs.IsAvailable() {
|
||||
m.activeSpawns[newID] = gs
|
||||
}
|
||||
|
||||
// Group by zone - pre-allocate zone slice if needed
|
||||
zoneID := int32(1) // TODO: Get actual zone ID from config.Location
|
||||
if m.spawnsByZone[zoneID] == nil {
|
||||
m.spawnsByZone[zoneID] = make([]*GroundSpawn, 0, 16) // Pre-allocate with reasonable capacity
|
||||
}
|
||||
m.spawnsByZone[zoneID] = append(m.spawnsByZone[zoneID], gs)
|
||||
|
||||
if m.logger != nil {
|
||||
@ -121,13 +150,13 @@ func (m *Manager) GetGroundSpawnsByZone(zoneID int32) []*GroundSpawn {
|
||||
defer m.mutex.RUnlock()
|
||||
|
||||
spawns := m.spawnsByZone[zoneID]
|
||||
if spawns == nil {
|
||||
if len(spawns) == 0 {
|
||||
return []*GroundSpawn{}
|
||||
}
|
||||
|
||||
// Return a copy to prevent external modification
|
||||
result := make([]*GroundSpawn, len(spawns))
|
||||
copy(result, spawns)
|
||||
// Return a copy to prevent external modification - use append for better performance
|
||||
result := make([]*GroundSpawn, 0, len(spawns))
|
||||
result = append(result, spawns...)
|
||||
|
||||
return result
|
||||
}
|
||||
@ -179,8 +208,11 @@ func (m *Manager) ProcessHarvest(gs *GroundSpawn, player *Player) (*HarvestResul
|
||||
m.mutex.Unlock()
|
||||
}
|
||||
|
||||
// Handle respawn if depleted
|
||||
// Handle respawn if depleted and update cache
|
||||
if gs.IsDepleted() {
|
||||
m.mutex.Lock()
|
||||
m.updateSpawnStateCache(gs)
|
||||
m.mutex.Unlock()
|
||||
m.scheduleRespawn(gs)
|
||||
}
|
||||
|
||||
@ -196,11 +228,11 @@ func (m *Manager) buildHarvestContext(gs *GroundSpawn, player *Player) (*Harvest
|
||||
items := m.itemsByID[groundspawnID]
|
||||
m.mutex.RUnlock()
|
||||
|
||||
if entries == nil || len(entries) == 0 {
|
||||
if len(entries) == 0 {
|
||||
return nil, fmt.Errorf("no harvest entries found for groundspawn %d", groundspawnID)
|
||||
}
|
||||
|
||||
if items == nil || len(items) == 0 {
|
||||
if len(items) == 0 {
|
||||
return nil, fmt.Errorf("no harvest items found for groundspawn %d", groundspawnID)
|
||||
}
|
||||
|
||||
@ -210,13 +242,13 @@ func (m *Manager) buildHarvestContext(gs *GroundSpawn, player *Player) (*Harvest
|
||||
skillName = SkillGathering // Collections use gathering skill
|
||||
}
|
||||
|
||||
playerSkill := player.GetSkillByName(skillName)
|
||||
playerSkill := (*player).GetSkillByName(skillName)
|
||||
if playerSkill == nil {
|
||||
return nil, fmt.Errorf("player lacks required skill: %s", skillName)
|
||||
}
|
||||
|
||||
// Calculate total skill (base + bonuses)
|
||||
totalSkill := playerSkill.GetCurrentValue()
|
||||
totalSkill := (*playerSkill).GetCurrentValue()
|
||||
// TODO: Add stat bonuses when stat system is integrated
|
||||
|
||||
// Find max skill required
|
||||
@ -276,6 +308,12 @@ func (m *Manager) ProcessRespawns() {
|
||||
for _, spawnID := range toRespawn {
|
||||
if gs := m.GetGroundSpawn(spawnID); gs != nil {
|
||||
gs.Respawn()
|
||||
|
||||
// Update cache after respawn
|
||||
m.mutex.Lock()
|
||||
m.updateSpawnStateCache(gs)
|
||||
m.mutex.Unlock()
|
||||
|
||||
if m.logger != nil {
|
||||
m.logger.LogDebug("Ground spawn %d respawned", spawnID)
|
||||
}
|
||||
@ -365,6 +403,8 @@ func (m *Manager) RemoveGroundSpawn(id int32) bool {
|
||||
|
||||
delete(m.groundSpawns, id)
|
||||
delete(m.respawnQueue, id)
|
||||
delete(m.activeSpawns, id)
|
||||
delete(m.depletedSpawns, id)
|
||||
|
||||
// Remove from zone list
|
||||
// TODO: Get actual zone ID from ground spawn
|
||||
@ -401,11 +441,10 @@ func (m *Manager) GetActiveGroundSpawns() []*GroundSpawn {
|
||||
m.mutex.RLock()
|
||||
defer m.mutex.RUnlock()
|
||||
|
||||
var active []*GroundSpawn
|
||||
for _, gs := range m.groundSpawns {
|
||||
if gs.IsAvailable() {
|
||||
active = append(active, gs)
|
||||
}
|
||||
// Use cached active spawns for O(1) performance instead of O(N) iteration
|
||||
active := make([]*GroundSpawn, 0, len(m.activeSpawns))
|
||||
for _, gs := range m.activeSpawns {
|
||||
active = append(active, gs)
|
||||
}
|
||||
|
||||
return active
|
||||
@ -416,16 +455,32 @@ func (m *Manager) GetDepletedGroundSpawns() []*GroundSpawn {
|
||||
m.mutex.RLock()
|
||||
defer m.mutex.RUnlock()
|
||||
|
||||
var depleted []*GroundSpawn
|
||||
for _, gs := range m.groundSpawns {
|
||||
if gs.IsDepleted() {
|
||||
depleted = append(depleted, gs)
|
||||
}
|
||||
// Use cached depleted spawns for O(1) performance instead of O(N) iteration
|
||||
depleted := make([]*GroundSpawn, 0, len(m.depletedSpawns))
|
||||
for _, gs := range m.depletedSpawns {
|
||||
depleted = append(depleted, gs)
|
||||
}
|
||||
|
||||
return depleted
|
||||
}
|
||||
|
||||
// updateSpawnStateCache updates the active/depleted caches when a spawn's state changes
|
||||
// IMPORTANT: This method must be called while holding the manager's mutex
|
||||
func (m *Manager) updateSpawnStateCache(gs *GroundSpawn) {
|
||||
spawnID := gs.GetID()
|
||||
|
||||
// Remove from both caches first
|
||||
delete(m.activeSpawns, spawnID)
|
||||
delete(m.depletedSpawns, spawnID)
|
||||
|
||||
// Add to appropriate cache based on current state
|
||||
if gs.IsAvailable() {
|
||||
m.activeSpawns[spawnID] = gs
|
||||
} else if gs.IsDepleted() {
|
||||
m.depletedSpawns[spawnID] = gs
|
||||
}
|
||||
}
|
||||
|
||||
// ProcessCommand handles ground spawn management commands
|
||||
func (m *Manager) ProcessCommand(command string, args []string) (string, error) {
|
||||
switch command {
|
||||
@ -536,7 +591,7 @@ func (m *Manager) handleInfoCommand(args []string) (string, error) {
|
||||
return fmt.Sprintf("Ground spawn %d not found.", spawnID), nil
|
||||
}
|
||||
|
||||
result := fmt.Sprintf("Ground Spawn Information:\n")
|
||||
result := "Ground Spawn Information:\n"
|
||||
result += fmt.Sprintf("ID: %d\n", gs.GetID())
|
||||
result += fmt.Sprintf("Name: %s\n", gs.GetName())
|
||||
result += fmt.Sprintf("Collection Skill: %s\n", gs.GetCollectionSkill())
|
||||
@ -550,7 +605,7 @@ func (m *Manager) handleInfoCommand(args []string) (string, error) {
|
||||
}
|
||||
|
||||
// handleReloadCommand reloads ground spawns from database
|
||||
func (m *Manager) handleReloadCommand(args []string) (string, error) {
|
||||
func (m *Manager) handleReloadCommand(_ []string) (string, error) {
|
||||
if m.database == nil {
|
||||
return "", fmt.Errorf("no database available")
|
||||
}
|
||||
@ -562,6 +617,9 @@ func (m *Manager) handleReloadCommand(args []string) (string, error) {
|
||||
m.entriesByID = make(map[int32][]*GroundSpawnEntry)
|
||||
m.itemsByID = make(map[int32][]*GroundSpawnEntryItem)
|
||||
m.respawnQueue = make(map[int32]time.Time)
|
||||
m.activeSpawns = make(map[int32]*GroundSpawn)
|
||||
m.depletedSpawns = make(map[int32]*GroundSpawn)
|
||||
m.nextSpawnID = 1 // Reset ID counter
|
||||
m.mutex.Unlock()
|
||||
|
||||
// Reload from database
|
||||
@ -588,4 +646,7 @@ func (m *Manager) Shutdown() {
|
||||
m.entriesByID = make(map[int32][]*GroundSpawnEntry)
|
||||
m.itemsByID = make(map[int32][]*GroundSpawnEntryItem)
|
||||
m.respawnQueue = make(map[int32]time.Time)
|
||||
m.activeSpawns = make(map[int32]*GroundSpawn)
|
||||
m.depletedSpawns = make(map[int32]*GroundSpawn)
|
||||
m.nextSpawnID = 1
|
||||
}
|
||||
|
8
internal/ground_spawn/test_utils.go
Normal file
8
internal/ground_spawn/test_utils.go
Normal file
@ -0,0 +1,8 @@
|
||||
package ground_spawn
|
||||
|
||||
type mockLogger struct{}
|
||||
|
||||
func (l *mockLogger) LogInfo(message string, args ...any) {}
|
||||
func (l *mockLogger) LogError(message string, args ...any) {}
|
||||
func (l *mockLogger) LogDebug(message string, args ...any) {}
|
||||
func (l *mockLogger) LogWarning(message string, args ...any) {}
|
@ -111,10 +111,15 @@ type Manager struct {
|
||||
itemsByID map[int32][]*GroundSpawnEntryItem // Harvest items by groundspawn ID
|
||||
respawnQueue map[int32]time.Time // Respawn timestamps
|
||||
|
||||
// Performance optimization: cache active/depleted spawns to avoid O(N) scans
|
||||
activeSpawns map[int32]*GroundSpawn // Cache of active spawns for O(1) lookups
|
||||
depletedSpawns map[int32]*GroundSpawn // Cache of depleted spawns for O(1) lookups
|
||||
|
||||
database Database // Database interface
|
||||
logger Logger // Logging interface
|
||||
|
||||
mutex sync.RWMutex // Thread safety
|
||||
mutex sync.RWMutex // Thread safety
|
||||
nextSpawnID int32 // Efficient ID counter to avoid len() calls
|
||||
|
||||
// Statistics
|
||||
totalHarvests int64 // Total harvest attempts
|
||||
|
@ -275,7 +275,7 @@ type MyGroupPacketHandler struct {
|
||||
// client connection management
|
||||
}
|
||||
|
||||
func (ph *MyGroupPacketHandler) SendGroupUpdate(members []*groups.GroupMemberInfo, excludeClient interface{}) error {
|
||||
func (ph *MyGroupPacketHandler) SendGroupUpdate(members []*groups.GroupMemberInfo, excludeClient any) error {
|
||||
// Send group update packets to clients
|
||||
return nil
|
||||
}
|
||||
|
@ -316,12 +316,12 @@ func (g *Group) Disband() {
|
||||
}
|
||||
|
||||
// SendGroupUpdate sends an update to all group members
|
||||
func (g *Group) SendGroupUpdate(excludeClient interface{}, forceRaidUpdate bool) {
|
||||
func (g *Group) SendGroupUpdate(excludeClient any, forceRaidUpdate bool) {
|
||||
g.sendGroupUpdate(excludeClient, forceRaidUpdate)
|
||||
}
|
||||
|
||||
// sendGroupUpdate internal method to send group updates
|
||||
func (g *Group) sendGroupUpdate(excludeClient interface{}, forceRaidUpdate bool) {
|
||||
func (g *Group) sendGroupUpdate(excludeClient any, forceRaidUpdate bool) {
|
||||
update := NewGroupUpdate(GROUP_UPDATE_FLAG_MEMBER_LIST, g.id)
|
||||
update.ExcludeClient = excludeClient
|
||||
update.ForceRaidUpdate = forceRaidUpdate
|
||||
@ -430,7 +430,7 @@ func (g *Group) GetLeaderName() string {
|
||||
}
|
||||
|
||||
// ShareQuestWithGroup shares a quest with all group members
|
||||
func (g *Group) ShareQuestWithGroup(questSharer interface{}, quest interface{}) bool {
|
||||
func (g *Group) ShareQuestWithGroup(questSharer any, quest any) bool {
|
||||
// TODO: Implement quest sharing
|
||||
// This would require integration with the quest system
|
||||
return false
|
||||
|
@ -38,7 +38,7 @@ type GroupManagerInterface interface {
|
||||
RemoveGroupMemberByName(groupID int32, name string, isClient bool, charID int32) error
|
||||
|
||||
// Group updates
|
||||
SendGroupUpdate(groupID int32, excludeClient interface{}, forceRaidUpdate bool)
|
||||
SendGroupUpdate(groupID int32, excludeClient any, forceRaidUpdate bool)
|
||||
|
||||
// Invitations
|
||||
Invite(leader entity.Entity, member entity.Entity) int8
|
||||
@ -141,9 +141,9 @@ type GroupDatabase interface {
|
||||
// GroupPacketHandler interface for handling group-related packets
|
||||
type GroupPacketHandler interface {
|
||||
// Group update packets
|
||||
SendGroupUpdate(members []*GroupMemberInfo, excludeClient interface{}) error
|
||||
SendGroupMemberUpdate(member *GroupMemberInfo, excludeClient interface{}) error
|
||||
SendGroupOptionsUpdate(groupID int32, options *GroupOptions, excludeClient interface{}) error
|
||||
SendGroupUpdate(members []*GroupMemberInfo, excludeClient any) error
|
||||
SendGroupMemberUpdate(member *GroupMemberInfo, excludeClient any) error
|
||||
SendGroupOptionsUpdate(groupID int32, options *GroupOptions, excludeClient any) error
|
||||
|
||||
// Group invitation packets
|
||||
SendGroupInvite(inviter, invitee entity.Entity) error
|
||||
@ -154,16 +154,16 @@ type GroupPacketHandler interface {
|
||||
SendGroupChatMessage(members []*GroupMemberInfo, from string, message string, channel int16, language int32) error
|
||||
|
||||
// Raid packets
|
||||
SendRaidUpdate(raidGroups []*Group, excludeClient interface{}) error
|
||||
SendRaidUpdate(raidGroups []*Group, excludeClient any) error
|
||||
SendRaidInvite(leaderGroup, targetGroup *Group) error
|
||||
SendRaidInviteResponse(leaderGroup, targetGroup *Group, accepted bool) error
|
||||
|
||||
// Group UI packets
|
||||
SendGroupWindowUpdate(client interface{}, group *Group) error
|
||||
SendRaidWindowUpdate(client interface{}, raidGroups []*Group) error
|
||||
SendGroupWindowUpdate(client any, group *Group) error
|
||||
SendRaidWindowUpdate(client any, raidGroups []*Group) error
|
||||
|
||||
// Group member packets
|
||||
SendGroupMemberStats(member *GroupMemberInfo, excludeClient interface{}) error
|
||||
SendGroupMemberStats(member *GroupMemberInfo, excludeClient any) error
|
||||
SendGroupMemberZoneChange(member *GroupMemberInfo, oldZoneID, newZoneID int32) error
|
||||
}
|
||||
|
||||
@ -244,8 +244,8 @@ type GroupStatistics interface {
|
||||
RecordGroupMemoryUsage(groups int32, members int32)
|
||||
|
||||
// Statistics retrieval
|
||||
GetGroupStatistics(groupID int32) map[string]interface{}
|
||||
GetOverallStatistics() map[string]interface{}
|
||||
GetGroupStatistics(groupID int32) map[string]any
|
||||
GetOverallStatistics() map[string]any
|
||||
GetStatisticsSummary() *GroupManagerStats
|
||||
}
|
||||
|
||||
|
@ -204,7 +204,7 @@ func (gm *GroupManager) RemoveGroupMemberByName(groupID int32, name string, isCl
|
||||
}
|
||||
|
||||
// SendGroupUpdate sends an update to all members of a group
|
||||
func (gm *GroupManager) SendGroupUpdate(groupID int32, excludeClient interface{}, forceRaidUpdate bool) {
|
||||
func (gm *GroupManager) SendGroupUpdate(groupID int32, excludeClient any, forceRaidUpdate bool) {
|
||||
group := gm.GetGroup(groupID)
|
||||
if group != nil {
|
||||
group.SendGroupUpdate(excludeClient, forceRaidUpdate)
|
||||
|
@ -59,7 +59,7 @@ type GroupMemberInfo struct {
|
||||
Member entity.Entity `json:"-"`
|
||||
|
||||
// Client reference (players only) - interface to avoid circular deps
|
||||
Client interface{} `json:"-"`
|
||||
Client any `json:"-"`
|
||||
|
||||
// Timestamps
|
||||
JoinTime time.Time `json:"join_time"`
|
||||
@ -102,13 +102,13 @@ type Group struct {
|
||||
|
||||
// GroupMessage represents a message sent to the group
|
||||
type GroupMessage struct {
|
||||
Type int8 `json:"type"`
|
||||
Channel int16 `json:"channel"`
|
||||
Message string `json:"message"`
|
||||
FromName string `json:"from_name"`
|
||||
Language int32 `json:"language"`
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
ExcludeClient interface{} `json:"-"`
|
||||
Type int8 `json:"type"`
|
||||
Channel int16 `json:"channel"`
|
||||
Message string `json:"message"`
|
||||
FromName string `json:"from_name"`
|
||||
Language int32 `json:"language"`
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
ExcludeClient any `json:"-"`
|
||||
}
|
||||
|
||||
// GroupUpdate represents a group update event
|
||||
@ -119,7 +119,7 @@ type GroupUpdate struct {
|
||||
Options *GroupOptions `json:"options,omitempty"`
|
||||
RaidGroups []int32 `json:"raid_groups,omitempty"`
|
||||
ForceRaidUpdate bool `json:"force_raid_update"`
|
||||
ExcludeClient interface{} `json:"-"`
|
||||
ExcludeClient any `json:"-"`
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
}
|
||||
|
||||
|
@ -182,16 +182,16 @@ type GuildEventHandler interface {
|
||||
// LogHandler provides logging functionality
|
||||
type LogHandler interface {
|
||||
// LogDebug logs debug messages
|
||||
LogDebug(category, message string, args ...interface{})
|
||||
LogDebug(category, message string, args ...any)
|
||||
|
||||
// LogInfo logs informational messages
|
||||
LogInfo(category, message string, args ...interface{})
|
||||
LogInfo(category, message string, args ...any)
|
||||
|
||||
// LogError logs error messages
|
||||
LogError(category, message string, args ...interface{})
|
||||
LogError(category, message string, args ...any)
|
||||
|
||||
// LogWarning logs warning messages
|
||||
LogWarning(category, message string, args ...interface{})
|
||||
LogWarning(category, message string, args ...any)
|
||||
}
|
||||
|
||||
// PlayerInfo contains basic player information
|
||||
@ -308,10 +308,10 @@ type NotificationManager interface {
|
||||
NotifyMemberLogout(guild *Guild, member *GuildMember)
|
||||
|
||||
// NotifyGuildMessage sends a message to all guild members
|
||||
NotifyGuildMessage(guild *Guild, eventType int8, message string, args ...interface{})
|
||||
NotifyGuildMessage(guild *Guild, eventType int8, message string, args ...any)
|
||||
|
||||
// NotifyOfficers sends a message to officers only
|
||||
NotifyOfficers(guild *Guild, message string, args ...interface{})
|
||||
NotifyOfficers(guild *Guild, message string, args ...any)
|
||||
|
||||
// NotifyGuildUpdate notifies guild members of guild changes
|
||||
NotifyGuildUpdate(guild *Guild)
|
||||
|
@ -117,10 +117,10 @@ type PlayerManager interface {
|
||||
|
||||
// LogHandler defines the interface for logging operations
|
||||
type LogHandler interface {
|
||||
LogDebug(system, format string, args ...interface{})
|
||||
LogInfo(system, format string, args ...interface{})
|
||||
LogWarning(system, format string, args ...interface{})
|
||||
LogError(system, format string, args ...interface{})
|
||||
LogDebug(system, format string, args ...any)
|
||||
LogInfo(system, format string, args ...any)
|
||||
LogWarning(system, format string, args ...any)
|
||||
LogError(system, format string, args ...any)
|
||||
}
|
||||
|
||||
// TimerManager defines the interface for timer management
|
||||
@ -139,8 +139,8 @@ type TimerManager interface {
|
||||
// CacheManager defines the interface for caching operations
|
||||
type CacheManager interface {
|
||||
// Cache operations
|
||||
Set(key string, value interface{}, expiration time.Duration) error
|
||||
Get(key string) (interface{}, bool)
|
||||
Set(key string, value any, expiration time.Duration) error
|
||||
Get(key string) (any, bool)
|
||||
Delete(key string) error
|
||||
Clear() error
|
||||
|
||||
@ -213,6 +213,6 @@ type StatisticsCollector interface {
|
||||
type ConfigManager interface {
|
||||
GetHOConfig() *HeroicOPConfig
|
||||
UpdateHOConfig(config *HeroicOPConfig) error
|
||||
GetConfigValue(key string) interface{}
|
||||
SetConfigValue(key string, value interface{}) error
|
||||
GetConfigValue(key string) any
|
||||
SetConfigValue(key string, value any) error
|
||||
}
|
||||
|
@ -426,11 +426,11 @@ func (mhol *MasterHeroicOPList) SearchWheels(criteria HeroicOPSearchCriteria) []
|
||||
}
|
||||
|
||||
// GetStatistics returns usage statistics for the HO system
|
||||
func (mhol *MasterHeroicOPList) GetStatistics() map[string]interface{} {
|
||||
func (mhol *MasterHeroicOPList) GetStatistics() map[string]any {
|
||||
mhol.mu.RLock()
|
||||
defer mhol.mu.RUnlock()
|
||||
|
||||
stats := make(map[string]interface{})
|
||||
stats := make(map[string]any)
|
||||
|
||||
// Basic counts
|
||||
stats["total_starters"] = mhol.getStarterCountNoLock()
|
||||
|
@ -159,10 +159,10 @@ type ZoneManager interface {
|
||||
|
||||
// LogHandler defines the interface for logging operations
|
||||
type LogHandler interface {
|
||||
LogDebug(system, format string, args ...interface{})
|
||||
LogInfo(system, format string, args ...interface{})
|
||||
LogWarning(system, format string, args ...interface{})
|
||||
LogError(system, format string, args ...interface{})
|
||||
LogDebug(system, format string, args ...any)
|
||||
LogInfo(system, format string, args ...any)
|
||||
LogWarning(system, format string, args ...any)
|
||||
LogError(system, format string, args ...any)
|
||||
}
|
||||
|
||||
// Additional integration interfaces
|
||||
@ -282,8 +282,8 @@ type AccessManager interface {
|
||||
type ConfigManager interface {
|
||||
GetHousingConfig() *HousingConfig
|
||||
UpdateHousingConfig(config *HousingConfig) error
|
||||
GetConfigValue(key string) interface{}
|
||||
SetConfigValue(key string, value interface{}) error
|
||||
GetConfigValue(key string) any
|
||||
SetConfigValue(key string, value any) error
|
||||
}
|
||||
|
||||
// NotificationManager defines interface for housing notifications
|
||||
@ -298,8 +298,8 @@ type NotificationManager interface {
|
||||
// CacheManager defines interface for caching operations
|
||||
type CacheManager interface {
|
||||
// Cache operations
|
||||
Set(key string, value interface{}, expiration time.Duration) error
|
||||
Get(key string) (interface{}, bool)
|
||||
Set(key string, value any, expiration time.Duration) error
|
||||
Get(key string) (any, bool)
|
||||
Delete(key string) error
|
||||
Clear() error
|
||||
|
||||
|
@ -573,7 +573,7 @@ func (isa *ItemSystemAdapter) CraftItem(playerID uint32, itemID int32, quality i
|
||||
}
|
||||
|
||||
// GetPlayerItemStats returns statistics about a player's items
|
||||
func (isa *ItemSystemAdapter) GetPlayerItemStats(playerID uint32) (map[string]interface{}, error) {
|
||||
func (isa *ItemSystemAdapter) GetPlayerItemStats(playerID uint32) (map[string]any, error) {
|
||||
inventory, err := isa.GetPlayerInventory(playerID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -587,7 +587,7 @@ func (isa *ItemSystemAdapter) GetPlayerItemStats(playerID uint32) (map[string]in
|
||||
// Calculate equipment bonuses
|
||||
bonuses := equipment.CalculateEquipmentBonuses()
|
||||
|
||||
return map[string]interface{}{
|
||||
return map[string]any{
|
||||
"player_id": playerID,
|
||||
"total_items": inventory.GetNumberOfItems(),
|
||||
"equipped_items": equipment.GetNumberOfItems(),
|
||||
@ -601,13 +601,13 @@ func (isa *ItemSystemAdapter) GetPlayerItemStats(playerID uint32) (map[string]in
|
||||
}
|
||||
|
||||
// GetSystemStats returns comprehensive statistics about the item system
|
||||
func (isa *ItemSystemAdapter) GetSystemStats() map[string]interface{} {
|
||||
func (isa *ItemSystemAdapter) GetSystemStats() map[string]any {
|
||||
isa.mutex.RLock()
|
||||
defer isa.mutex.RUnlock()
|
||||
|
||||
masterStats := isa.masterList.GetStats()
|
||||
|
||||
return map[string]interface{}{
|
||||
return map[string]any{
|
||||
"total_item_templates": masterStats.TotalItems,
|
||||
"items_by_type": masterStats.ItemsByType,
|
||||
"items_by_tier": masterStats.ItemsByTier,
|
||||
|
@ -43,13 +43,13 @@ func (ci ChestInteraction) String() string {
|
||||
// ChestInteractionResult represents the result of a chest interaction
|
||||
type ChestInteractionResult struct {
|
||||
Success bool `json:"success"`
|
||||
Result int8 `json:"result"` // ChestResult constant
|
||||
Message string `json:"message"` // Message to display to player
|
||||
Items []*items.Item `json:"items"` // Items received
|
||||
Coins int32 `json:"coins"` // Coins received
|
||||
Experience int32 `json:"experience"` // Experience gained (for disarming/lockpicking)
|
||||
ChestEmpty bool `json:"chest_empty"` // Whether chest is now empty
|
||||
ChestClosed bool `json:"chest_closed"` // Whether chest should be closed
|
||||
Result int8 `json:"result"` // ChestResult constant
|
||||
Message string `json:"message"` // Message to display to player
|
||||
Items []*items.Item `json:"items"` // Items received
|
||||
Coins int32 `json:"coins"` // Coins received
|
||||
Experience int32 `json:"experience"` // Experience gained (for disarming/lockpicking)
|
||||
ChestEmpty bool `json:"chest_empty"` // Whether chest is now empty
|
||||
ChestClosed bool `json:"chest_closed"` // Whether chest should be closed
|
||||
}
|
||||
|
||||
// ChestService handles treasure chest interactions and management
|
||||
@ -73,7 +73,7 @@ type PlayerService interface {
|
||||
|
||||
// ZoneService interface for zone-related operations
|
||||
type ZoneService interface {
|
||||
GetZoneRule(zoneID int32, ruleName string) (interface{}, error)
|
||||
GetZoneRule(zoneID int32, ruleName string) (any, error)
|
||||
SpawnObjectInZone(zoneID int32, appearanceID int32, x, y, z, heading float32, name string, commands []string) (int32, error)
|
||||
RemoveObjectFromZone(zoneID int32, objectID int32) error
|
||||
GetDistanceBetweenPoints(x1, y1, z1, x2, y2, z2 float32) float32
|
||||
@ -123,7 +123,7 @@ func (cs *ChestService) CreateTreasureChestFromLoot(spawnID int32, zoneID int32,
|
||||
|
||||
// Don't create chest if no qualifying items and no coins
|
||||
if filteredResult.IsEmpty() {
|
||||
log.Printf("%s No qualifying loot for treasure chest (tier >= %d) for spawn %d",
|
||||
log.Printf("%s No qualifying loot for treasure chest (tier >= %d) for spawn %d",
|
||||
LogPrefixChest, LootTierCommon, spawnID)
|
||||
return nil, nil
|
||||
}
|
||||
@ -136,7 +136,7 @@ func (cs *ChestService) CreateTreasureChestFromLoot(spawnID int32, zoneID int32,
|
||||
|
||||
// Spawn the chest object in the zone
|
||||
chestCommands := []string{"loot", "disarm"} // TODO: Add "lockpick" if chest is locked
|
||||
objectID, err := cs.zoneService.SpawnObjectInZone(zoneID, chest.AppearanceID, x, y, z, heading,
|
||||
objectID, err := cs.zoneService.SpawnObjectInZone(zoneID, chest.AppearanceID, x, y, z, heading,
|
||||
"Treasure Chest", chestCommands)
|
||||
if err != nil {
|
||||
log.Printf("%s Failed to spawn chest object in zone: %v", LogPrefixChest, err)
|
||||
@ -149,7 +149,7 @@ func (cs *ChestService) CreateTreasureChestFromLoot(spawnID int32, zoneID int32,
|
||||
}
|
||||
|
||||
// HandleChestInteraction processes a player's interaction with a treasure chest
|
||||
func (cs *ChestService) HandleChestInteraction(chestID int32, playerID uint32,
|
||||
func (cs *ChestService) HandleChestInteraction(chestID int32, playerID uint32,
|
||||
interaction ChestInteraction, itemUniqueID int64) *ChestInteractionResult {
|
||||
|
||||
result := &ChestInteractionResult{
|
||||
@ -273,10 +273,10 @@ func (cs *ChestService) handleViewChest(chest *TreasureChest, playerID uint32) *
|
||||
return &ChestInteractionResult{
|
||||
Success: true,
|
||||
Result: ChestResultSuccess,
|
||||
Message: fmt.Sprintf("The chest contains %d items and %d coins",
|
||||
Message: fmt.Sprintf("The chest contains %d items and %d coins",
|
||||
len(chest.LootResult.GetItems()), chest.LootResult.GetCoins()),
|
||||
Items: chest.LootResult.GetItems(),
|
||||
Coins: chest.LootResult.GetCoins(),
|
||||
Items: chest.LootResult.GetItems(),
|
||||
Coins: chest.LootResult.GetCoins(),
|
||||
}
|
||||
}
|
||||
|
||||
@ -400,7 +400,7 @@ func (cs *ChestService) handleDisarmChest(chest *TreasureChest, playerID uint32)
|
||||
|
||||
// Get player's disarm skill
|
||||
disarmSkill := cs.playerService.GetPlayerSkillValue(playerID, "Disarm Trap")
|
||||
|
||||
|
||||
// Calculate success chance (simplified)
|
||||
successChance := float32(disarmSkill) - float32(chest.DisarmDifficulty)
|
||||
if successChance < 0 {
|
||||
@ -410,7 +410,7 @@ func (cs *ChestService) handleDisarmChest(chest *TreasureChest, playerID uint32)
|
||||
}
|
||||
|
||||
// Roll for success
|
||||
roll := float32(time.Now().UnixNano()%100) // Simple random
|
||||
roll := float32(time.Now().UnixNano() % 100) // Simple random
|
||||
if roll > successChance {
|
||||
// Failed disarm - could trigger trap effects here
|
||||
return &ChestInteractionResult{
|
||||
@ -422,7 +422,7 @@ func (cs *ChestService) handleDisarmChest(chest *TreasureChest, playerID uint32)
|
||||
|
||||
// Success - disarm the trap
|
||||
chest.IsDisarmable = false
|
||||
|
||||
|
||||
// Give experience
|
||||
experience := int32(chest.DisarmDifficulty * 10) // 10 exp per difficulty point
|
||||
cs.playerService.AddPlayerExperience(playerID, experience, "Disarm Trap")
|
||||
@ -450,7 +450,7 @@ func (cs *ChestService) handleLockpickChest(chest *TreasureChest, playerID uint3
|
||||
|
||||
// Get player's lockpicking skill
|
||||
lockpickSkill := cs.playerService.GetPlayerSkillValue(playerID, "Pick Lock")
|
||||
|
||||
|
||||
// Calculate success chance (simplified)
|
||||
successChance := float32(lockpickSkill) - float32(chest.LockpickDifficulty)
|
||||
if successChance < 0 {
|
||||
@ -460,7 +460,7 @@ func (cs *ChestService) handleLockpickChest(chest *TreasureChest, playerID uint3
|
||||
}
|
||||
|
||||
// Roll for success
|
||||
roll := float32(time.Now().UnixNano()%100) // Simple random
|
||||
roll := float32(time.Now().UnixNano() % 100) // Simple random
|
||||
if roll > successChance {
|
||||
return &ChestInteractionResult{
|
||||
Success: false,
|
||||
@ -471,7 +471,7 @@ func (cs *ChestService) handleLockpickChest(chest *TreasureChest, playerID uint3
|
||||
|
||||
// Success - unlock the chest
|
||||
chest.IsLocked = false
|
||||
|
||||
|
||||
// Give experience
|
||||
experience := int32(chest.LockpickDifficulty * 10) // 10 exp per difficulty point
|
||||
cs.playerService.AddPlayerExperience(playerID, experience, "Pick Lock")
|
||||
@ -500,12 +500,12 @@ func (cs *ChestService) handleCloseChest(chest *TreasureChest, playerID uint32)
|
||||
// CleanupEmptyChests removes empty chests from zones
|
||||
func (cs *ChestService) CleanupEmptyChests(zoneID int32) {
|
||||
chests := cs.lootManager.GetZoneChests(zoneID)
|
||||
|
||||
|
||||
for _, chest := range chests {
|
||||
if chest.LootResult.IsEmpty() {
|
||||
// Remove from zone
|
||||
cs.zoneService.RemoveObjectFromZone(zoneID, chest.ID)
|
||||
|
||||
|
||||
// Remove from loot manager
|
||||
cs.lootManager.RemoveTreasureChest(chest.ID)
|
||||
}
|
||||
@ -515,4 +515,4 @@ func (cs *ChestService) CleanupEmptyChests(zoneID int32) {
|
||||
// GetPlayerChestList returns a list of chests a player can access
|
||||
func (cs *ChestService) GetPlayerChestList(playerID uint32) []*TreasureChest {
|
||||
return cs.lootManager.GetPlayerChests(playerID)
|
||||
}
|
||||
}
|
||||
|
@ -10,12 +10,12 @@ import (
|
||||
|
||||
// LootDatabase handles all database operations for the loot system
|
||||
type LootDatabase struct {
|
||||
db *sql.DB
|
||||
queries map[string]*sql.Stmt
|
||||
lootTables map[int32]*LootTable
|
||||
spawnLoot map[int32][]int32 // spawn_id -> []loot_table_id
|
||||
globalLoot []*GlobalLoot
|
||||
mutex sync.RWMutex
|
||||
db *sql.DB
|
||||
queries map[string]*sql.Stmt
|
||||
lootTables map[int32]*LootTable
|
||||
spawnLoot map[int32][]int32 // spawn_id -> []loot_table_id
|
||||
globalLoot []*GlobalLoot
|
||||
mutex sync.RWMutex
|
||||
}
|
||||
|
||||
// NewLootDatabase creates a new loot database manager
|
||||
@ -42,88 +42,88 @@ func (ldb *LootDatabase) prepareQueries() {
|
||||
FROM loottable
|
||||
ORDER BY id
|
||||
`,
|
||||
|
||||
|
||||
"load_loot_drops": `
|
||||
SELECT loot_table_id, item_id, item_charges, equip_item, probability, no_drop_quest_completed_id
|
||||
FROM lootdrop
|
||||
WHERE loot_table_id = ?
|
||||
ORDER BY probability DESC
|
||||
`,
|
||||
|
||||
|
||||
"load_spawn_loot": `
|
||||
SELECT spawn_id, loottable_id
|
||||
FROM spawn_loot
|
||||
ORDER BY spawn_id
|
||||
`,
|
||||
|
||||
|
||||
"load_global_loot": `
|
||||
SELECT type, loot_table, value1, value2, value3, value4
|
||||
FROM loot_global
|
||||
ORDER BY type, value1
|
||||
`,
|
||||
|
||||
|
||||
"insert_loot_table": `
|
||||
INSERT INTO loottable (id, name, mincoin, maxcoin, maxlootitems, lootdrop_probability, coin_probability)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
`,
|
||||
|
||||
|
||||
"update_loot_table": `
|
||||
UPDATE loottable
|
||||
SET name = ?, mincoin = ?, maxcoin = ?, maxlootitems = ?, lootdrop_probability = ?, coin_probability = ?
|
||||
WHERE id = ?
|
||||
`,
|
||||
|
||||
|
||||
"delete_loot_table": `
|
||||
DELETE FROM loottable WHERE id = ?
|
||||
`,
|
||||
|
||||
|
||||
"insert_loot_drop": `
|
||||
INSERT INTO lootdrop (loot_table_id, item_id, item_charges, equip_item, probability, no_drop_quest_completed_id)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
`,
|
||||
|
||||
|
||||
"delete_loot_drops": `
|
||||
DELETE FROM lootdrop WHERE loot_table_id = ?
|
||||
`,
|
||||
|
||||
|
||||
"insert_spawn_loot": `
|
||||
INSERT OR REPLACE INTO spawn_loot (spawn_id, loottable_id)
|
||||
VALUES (?, ?)
|
||||
`,
|
||||
|
||||
|
||||
"delete_spawn_loot": `
|
||||
DELETE FROM spawn_loot WHERE spawn_id = ?
|
||||
`,
|
||||
|
||||
|
||||
"insert_global_loot": `
|
||||
INSERT INTO loot_global (type, loot_table, value1, value2, value3, value4)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
`,
|
||||
|
||||
|
||||
"delete_global_loot": `
|
||||
DELETE FROM loot_global WHERE type = ?
|
||||
`,
|
||||
|
||||
|
||||
"get_loot_table": `
|
||||
SELECT id, name, mincoin, maxcoin, maxlootitems, lootdrop_probability, coin_probability
|
||||
FROM loottable
|
||||
WHERE id = ?
|
||||
`,
|
||||
|
||||
|
||||
"get_spawn_loot_tables": `
|
||||
SELECT loottable_id
|
||||
FROM spawn_loot
|
||||
WHERE spawn_id = ?
|
||||
`,
|
||||
|
||||
|
||||
"count_loot_tables": `
|
||||
SELECT COUNT(*) FROM loottable
|
||||
`,
|
||||
|
||||
|
||||
"count_loot_drops": `
|
||||
SELECT COUNT(*) FROM lootdrop
|
||||
`,
|
||||
|
||||
|
||||
"count_spawn_loot": `
|
||||
SELECT COUNT(*) FROM spawn_loot
|
||||
`,
|
||||
@ -141,36 +141,36 @@ func (ldb *LootDatabase) prepareQueries() {
|
||||
// LoadAllLootData loads all loot data from the database
|
||||
func (ldb *LootDatabase) LoadAllLootData() error {
|
||||
log.Printf("%s Loading loot data from database...", LogPrefixDatabase)
|
||||
|
||||
|
||||
// Load loot tables first
|
||||
if err := ldb.loadLootTables(); err != nil {
|
||||
return fmt.Errorf("failed to load loot tables: %v", err)
|
||||
}
|
||||
|
||||
|
||||
// Load loot drops for each table
|
||||
if err := ldb.loadLootDrops(); err != nil {
|
||||
return fmt.Errorf("failed to load loot drops: %v", err)
|
||||
}
|
||||
|
||||
|
||||
// Load spawn loot assignments
|
||||
if err := ldb.loadSpawnLoot(); err != nil {
|
||||
return fmt.Errorf("failed to load spawn loot: %v", err)
|
||||
}
|
||||
|
||||
|
||||
// Load global loot configuration
|
||||
if err := ldb.loadGlobalLoot(); err != nil {
|
||||
return fmt.Errorf("failed to load global loot: %v", err)
|
||||
}
|
||||
|
||||
|
||||
ldb.mutex.RLock()
|
||||
tableCount := len(ldb.lootTables)
|
||||
spawnCount := len(ldb.spawnLoot)
|
||||
globalCount := len(ldb.globalLoot)
|
||||
ldb.mutex.RUnlock()
|
||||
|
||||
log.Printf("%s Loaded %d loot tables, %d spawn assignments, %d global loot entries",
|
||||
|
||||
log.Printf("%s Loaded %d loot tables, %d spawn assignments, %d global loot entries",
|
||||
LogPrefixDatabase, tableCount, spawnCount, globalCount)
|
||||
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -189,7 +189,7 @@ func (ldb *LootDatabase) loadLootTables() error {
|
||||
|
||||
ldb.mutex.Lock()
|
||||
defer ldb.mutex.Unlock()
|
||||
|
||||
|
||||
// Clear existing tables
|
||||
ldb.lootTables = make(map[int32]*LootTable)
|
||||
|
||||
@ -279,7 +279,7 @@ func (ldb *LootDatabase) loadSpawnLoot() error {
|
||||
|
||||
ldb.mutex.Lock()
|
||||
defer ldb.mutex.Unlock()
|
||||
|
||||
|
||||
// Clear existing spawn loot
|
||||
ldb.spawnLoot = make(map[int32][]int32)
|
||||
|
||||
@ -313,7 +313,7 @@ func (ldb *LootDatabase) loadGlobalLoot() error {
|
||||
|
||||
ldb.mutex.Lock()
|
||||
defer ldb.mutex.Unlock()
|
||||
|
||||
|
||||
// Clear existing global loot
|
||||
ldb.globalLoot = make([]*GlobalLoot, 0)
|
||||
|
||||
@ -361,7 +361,7 @@ func (ldb *LootDatabase) loadGlobalLoot() error {
|
||||
func (ldb *LootDatabase) GetLootTable(tableID int32) *LootTable {
|
||||
ldb.mutex.RLock()
|
||||
defer ldb.mutex.RUnlock()
|
||||
|
||||
|
||||
return ldb.lootTables[tableID]
|
||||
}
|
||||
|
||||
@ -369,12 +369,12 @@ func (ldb *LootDatabase) GetLootTable(tableID int32) *LootTable {
|
||||
func (ldb *LootDatabase) GetSpawnLootTables(spawnID int32) []int32 {
|
||||
ldb.mutex.RLock()
|
||||
defer ldb.mutex.RUnlock()
|
||||
|
||||
|
||||
tables := ldb.spawnLoot[spawnID]
|
||||
if tables == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
// Return a copy to prevent external modification
|
||||
result := make([]int32, len(tables))
|
||||
copy(result, tables)
|
||||
@ -385,9 +385,9 @@ func (ldb *LootDatabase) GetSpawnLootTables(spawnID int32) []int32 {
|
||||
func (ldb *LootDatabase) GetGlobalLootTables(level int16, race int16, zoneID int32) []*GlobalLoot {
|
||||
ldb.mutex.RLock()
|
||||
defer ldb.mutex.RUnlock()
|
||||
|
||||
|
||||
var result []*GlobalLoot
|
||||
|
||||
|
||||
for _, global := range ldb.globalLoot {
|
||||
switch global.Type {
|
||||
case GlobalLootTypeLevel:
|
||||
@ -404,7 +404,7 @@ func (ldb *LootDatabase) GetGlobalLootTables(level int16, race int16, zoneID int
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
@ -587,8 +587,8 @@ func (ldb *LootDatabase) DeleteSpawnLoot(spawnID int32) error {
|
||||
}
|
||||
|
||||
// GetLootStatistics returns database statistics
|
||||
func (ldb *LootDatabase) GetLootStatistics() (map[string]interface{}, error) {
|
||||
stats := make(map[string]interface{})
|
||||
func (ldb *LootDatabase) GetLootStatistics() (map[string]any, error) {
|
||||
stats := make(map[string]any)
|
||||
|
||||
// Count loot tables
|
||||
if stmt := ldb.queries["count_loot_tables"]; stmt != nil {
|
||||
@ -629,7 +629,7 @@ func (ldb *LootDatabase) GetLootStatistics() (map[string]interface{}, error) {
|
||||
// ReloadLootData reloads all loot data from the database
|
||||
func (ldb *LootDatabase) ReloadLootData() error {
|
||||
log.Printf("%s Reloading loot data from database...", LogPrefixDatabase)
|
||||
|
||||
|
||||
return ldb.LoadAllLootData()
|
||||
}
|
||||
|
||||
@ -641,4 +641,4 @@ func (ldb *LootDatabase) Close() error {
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
@ -18,13 +18,13 @@ type LootSystem struct {
|
||||
|
||||
// LootSystemConfig holds configuration for the loot system
|
||||
type LootSystemConfig struct {
|
||||
DatabaseConnection *sql.DB
|
||||
ItemMasterList items.MasterItemListService
|
||||
PlayerService PlayerService
|
||||
ZoneService ZoneService
|
||||
ClientService ClientService
|
||||
ItemPacketBuilder ItemPacketBuilder
|
||||
StartCleanupTimer bool
|
||||
DatabaseConnection *sql.DB
|
||||
ItemMasterList items.MasterItemListService
|
||||
PlayerService PlayerService
|
||||
ZoneService ZoneService
|
||||
ClientService ClientService
|
||||
ItemPacketBuilder ItemPacketBuilder
|
||||
StartCleanupTimer bool
|
||||
}
|
||||
|
||||
// NewLootSystem creates a complete loot system with all components
|
||||
@ -78,7 +78,7 @@ func NewLootSystem(config *LootSystemConfig) (*LootSystem, error) {
|
||||
}
|
||||
|
||||
// GenerateAndCreateChest generates loot for a spawn and creates a treasure chest
|
||||
func (ls *LootSystem) GenerateAndCreateChest(spawnID int32, zoneID int32, x, y, z, heading float32,
|
||||
func (ls *LootSystem) GenerateAndCreateChest(spawnID int32, zoneID int32, x, y, z, heading float32,
|
||||
context *LootContext) (*TreasureChest, error) {
|
||||
|
||||
if ls.ChestService == nil {
|
||||
@ -98,7 +98,7 @@ func (ls *LootSystem) GenerateAndCreateChest(spawnID int32, zoneID int32, x, y,
|
||||
}
|
||||
|
||||
// Create treasure chest
|
||||
chest, err := ls.ChestService.CreateTreasureChestFromLoot(spawnID, zoneID, x, y, z, heading,
|
||||
chest, err := ls.ChestService.CreateTreasureChestFromLoot(spawnID, zoneID, x, y, z, heading,
|
||||
lootResult, context.GroupMembers)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create treasure chest: %v", err)
|
||||
@ -108,7 +108,7 @@ func (ls *LootSystem) GenerateAndCreateChest(spawnID int32, zoneID int32, x, y,
|
||||
}
|
||||
|
||||
// HandlePlayerLootInteraction handles a player's interaction with a chest and sends appropriate packets
|
||||
func (ls *LootSystem) HandlePlayerLootInteraction(chestID int32, playerID uint32,
|
||||
func (ls *LootSystem) HandlePlayerLootInteraction(chestID int32, playerID uint32,
|
||||
interaction ChestInteraction, itemUniqueID int64) error {
|
||||
|
||||
if ls.ChestService == nil {
|
||||
@ -141,7 +141,7 @@ func (ls *LootSystem) HandlePlayerLootInteraction(chestID int32, playerID uint32
|
||||
}
|
||||
|
||||
// Log the interaction
|
||||
log.Printf("%s Player %d %s chest %d: %s",
|
||||
log.Printf("%s Player %d %s chest %d: %s",
|
||||
LogPrefixLoot, playerID, interaction.String(), chestID, result.Message)
|
||||
|
||||
return nil
|
||||
@ -168,8 +168,8 @@ func (ls *LootSystem) ShowChestToPlayer(chestID int32, playerID uint32) error {
|
||||
}
|
||||
|
||||
// GetSystemStatistics returns comprehensive statistics about the loot system
|
||||
func (ls *LootSystem) GetSystemStatistics() (map[string]interface{}, error) {
|
||||
stats := make(map[string]interface{})
|
||||
func (ls *LootSystem) GetSystemStatistics() (map[string]any, error) {
|
||||
stats := make(map[string]any)
|
||||
|
||||
// Database statistics
|
||||
if dbStats, err := ls.Database.GetLootStatistics(); err == nil {
|
||||
@ -220,7 +220,7 @@ func (ls *LootSystem) AddLootTableWithDrops(table *LootTable) error {
|
||||
}
|
||||
|
||||
// CreateQuickLootTable creates a simple loot table with basic parameters
|
||||
func (ls *LootSystem) CreateQuickLootTable(tableID int32, name string, items []QuickLootItem,
|
||||
func (ls *LootSystem) CreateQuickLootTable(tableID int32, name string, items []QuickLootItem,
|
||||
minCoin, maxCoin int32, maxItems int16) error {
|
||||
|
||||
table := &LootTable{
|
||||
@ -289,9 +289,9 @@ func (ls *LootSystem) CreateGlobalLevelLoot(minLevel, maxLevel int8, tableID int
|
||||
ls.Database.globalLoot = append(ls.Database.globalLoot, global)
|
||||
ls.Database.mutex.Unlock()
|
||||
|
||||
log.Printf("%s Created global level loot for levels %d-%d using table %d",
|
||||
log.Printf("%s Created global level loot for levels %d-%d using table %d",
|
||||
LogPrefixLoot, minLevel, maxLevel, tableID)
|
||||
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -303,10 +303,10 @@ func (ls *LootSystem) GetActiveChestsInZone(zoneID int32) []*TreasureChest {
|
||||
// CleanupZoneChests removes all chests from a specific zone
|
||||
func (ls *LootSystem) CleanupZoneChests(zoneID int32) {
|
||||
chests := ls.Manager.GetZoneChests(zoneID)
|
||||
|
||||
|
||||
for _, chest := range chests {
|
||||
ls.Manager.RemoveTreasureChest(chest.ID)
|
||||
|
||||
|
||||
// Remove from zone if chest service is available
|
||||
if ls.ChestService != nil {
|
||||
ls.ChestService.zoneService.RemoveObjectFromZone(zoneID, chest.ID)
|
||||
@ -356,17 +356,17 @@ type ValidationError struct {
|
||||
func (ls *LootSystem) GetLootPreview(spawnID int32, context *LootContext) (*LootPreview, error) {
|
||||
tableIDs := ls.Database.GetSpawnLootTables(spawnID)
|
||||
globalLoot := ls.Database.GetGlobalLootTables(context.PlayerLevel, context.PlayerRace, context.ZoneID)
|
||||
|
||||
|
||||
for _, global := range globalLoot {
|
||||
tableIDs = append(tableIDs, global.TableID)
|
||||
}
|
||||
|
||||
preview := &LootPreview{
|
||||
SpawnID: spawnID,
|
||||
TableIDs: tableIDs,
|
||||
SpawnID: spawnID,
|
||||
TableIDs: tableIDs,
|
||||
PossibleItems: make([]*LootPreviewItem, 0),
|
||||
MinCoins: 0,
|
||||
MaxCoins: 0,
|
||||
MinCoins: 0,
|
||||
MaxCoins: 0,
|
||||
}
|
||||
|
||||
for _, tableID := range tableIDs {
|
||||
@ -400,11 +400,11 @@ func (ls *LootSystem) GetLootPreview(spawnID int32, context *LootContext) (*Loot
|
||||
|
||||
// LootPreview represents a preview of potential loot
|
||||
type LootPreview struct {
|
||||
SpawnID int32 `json:"spawn_id"`
|
||||
TableIDs []int32 `json:"table_ids"`
|
||||
PossibleItems []*LootPreviewItem `json:"possible_items"`
|
||||
MinCoins int32 `json:"min_coins"`
|
||||
MaxCoins int32 `json:"max_coins"`
|
||||
SpawnID int32 `json:"spawn_id"`
|
||||
TableIDs []int32 `json:"table_ids"`
|
||||
PossibleItems []*LootPreviewItem `json:"possible_items"`
|
||||
MinCoins int32 `json:"min_coins"`
|
||||
MaxCoins int32 `json:"max_coins"`
|
||||
}
|
||||
|
||||
// LootPreviewItem represents a potential loot item in a preview
|
||||
@ -413,4 +413,4 @@ type LootPreviewItem struct {
|
||||
ItemName string `json:"item_name"`
|
||||
Probability float32 `json:"probability"`
|
||||
Tier int8 `json:"tier"`
|
||||
}
|
||||
}
|
||||
|
@ -106,18 +106,18 @@ func (m *MockPlayerService) SetInventorySpace(playerID uint32, space int) {
|
||||
|
||||
// MockZoneService implements ZoneService for testing
|
||||
type MockZoneService struct {
|
||||
rules map[int32]map[string]interface{}
|
||||
objects map[int32]map[int32]interface{} // zoneID -> objectID
|
||||
rules map[int32]map[string]any
|
||||
objects map[int32]map[int32]any // zoneID -> objectID
|
||||
}
|
||||
|
||||
func NewMockZoneService() *MockZoneService {
|
||||
return &MockZoneService{
|
||||
rules: make(map[int32]map[string]interface{}),
|
||||
objects: make(map[int32]map[int32]interface{}),
|
||||
rules: make(map[int32]map[string]any),
|
||||
objects: make(map[int32]map[int32]any),
|
||||
}
|
||||
}
|
||||
|
||||
func (m *MockZoneService) GetZoneRule(zoneID int32, ruleName string) (interface{}, error) {
|
||||
func (m *MockZoneService) GetZoneRule(zoneID int32, ruleName string) (any, error) {
|
||||
if rules, exists := m.rules[zoneID]; exists {
|
||||
return rules[ruleName], nil
|
||||
}
|
||||
@ -127,7 +127,7 @@ func (m *MockZoneService) GetZoneRule(zoneID int32, ruleName string) (interface{
|
||||
func (m *MockZoneService) SpawnObjectInZone(zoneID int32, appearanceID int32, x, y, z, heading float32, name string, commands []string) (int32, error) {
|
||||
objectID := int32(len(m.objects[zoneID]) + 1)
|
||||
if m.objects[zoneID] == nil {
|
||||
m.objects[zoneID] = make(map[int32]interface{})
|
||||
m.objects[zoneID] = make(map[int32]any)
|
||||
}
|
||||
m.objects[zoneID][objectID] = struct{}{}
|
||||
return objectID, nil
|
||||
@ -382,7 +382,7 @@ func TestTreasureChestCreation(t *testing.T) {
|
||||
}
|
||||
|
||||
if chest.AppearanceID != ChestAppearanceOrnate {
|
||||
t.Errorf("Expected ornate chest appearance %d for legendary item, got %d",
|
||||
t.Errorf("Expected ornate chest appearance %d for legendary item, got %d",
|
||||
ChestAppearanceOrnate, chest.AppearanceID)
|
||||
}
|
||||
|
||||
@ -530,7 +530,7 @@ func TestChestAppearanceSelection(t *testing.T) {
|
||||
for _, tc := range testCases {
|
||||
appearance := GetChestAppearance(tc.tier)
|
||||
if appearance.AppearanceID != tc.expected {
|
||||
t.Errorf("For tier %d, expected appearance %d, got %d",
|
||||
t.Errorf("For tier %d, expected appearance %d, got %d",
|
||||
tc.tier, tc.expected, appearance.AppearanceID)
|
||||
}
|
||||
}
|
||||
@ -667,4 +667,4 @@ func BenchmarkChestInteraction(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
chestService.HandleChestInteraction(chest.ID, 1, ChestInteractionView, 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -14,10 +14,10 @@ type Database interface {
|
||||
|
||||
// Logger interface for language logging
|
||||
type Logger interface {
|
||||
LogInfo(message string, args ...interface{})
|
||||
LogError(message string, args ...interface{})
|
||||
LogDebug(message string, args ...interface{})
|
||||
LogWarning(message string, args ...interface{})
|
||||
LogInfo(message string, args ...any)
|
||||
LogError(message string, args ...any)
|
||||
LogDebug(message string, args ...any)
|
||||
LogWarning(message string, args ...any)
|
||||
}
|
||||
|
||||
// Player interface for language-related player operations
|
||||
@ -372,7 +372,7 @@ func NewLanguageEventAdapter(handler LanguageHandler, logger Logger) *LanguageEv
|
||||
}
|
||||
|
||||
// ProcessLanguageEvent processes a language-related event
|
||||
func (lea *LanguageEventAdapter) ProcessLanguageEvent(eventType string, player *Player, languageID int32, data interface{}) {
|
||||
func (lea *LanguageEventAdapter) ProcessLanguageEvent(eventType string, player *Player, languageID int32, data any) {
|
||||
if lea.handler == nil {
|
||||
return
|
||||
}
|
||||
|
@ -7,10 +7,10 @@ import (
|
||||
|
||||
// Logger interface for AI logging
|
||||
type Logger interface {
|
||||
LogInfo(message string, args ...interface{})
|
||||
LogError(message string, args ...interface{})
|
||||
LogDebug(message string, args ...interface{})
|
||||
LogWarning(message string, args ...interface{})
|
||||
LogInfo(message string, args ...any)
|
||||
LogError(message string, args ...any)
|
||||
LogDebug(message string, args ...any)
|
||||
LogWarning(message string, args ...any)
|
||||
}
|
||||
|
||||
// NPC interface defines the required NPC functionality for AI
|
||||
@ -144,7 +144,7 @@ type LuaInterface interface {
|
||||
type Zone interface {
|
||||
GetSpawnByID(int32) Spawn
|
||||
ProcessSpell(spell Spell, caster NPC, target Spawn) error
|
||||
CallSpawnScript(npc NPC, scriptType string, args ...interface{}) error
|
||||
CallSpawnScript(npc NPC, scriptType string, args ...any) error
|
||||
}
|
||||
|
||||
// AIManager provides high-level management of the AI system
|
||||
@ -209,7 +209,7 @@ func (am *AIManager) GetBrain(npcID int32) Brain {
|
||||
}
|
||||
|
||||
// CreateBrainForNPC creates and adds the appropriate brain for an NPC
|
||||
func (am *AIManager) CreateBrainForNPC(npc NPC, brainType int8, options ...interface{}) error {
|
||||
func (am *AIManager) CreateBrainForNPC(npc NPC, brainType int8, options ...any) error {
|
||||
if npc == nil {
|
||||
return fmt.Errorf("NPC cannot be nil")
|
||||
}
|
||||
|
@ -289,7 +289,7 @@ func (dfpb *DumbFirePetBrain) ExtendExpireTime(durationMS int32) {
|
||||
// Brain factory functions
|
||||
|
||||
// CreateBrain creates the appropriate brain type for an NPC
|
||||
func CreateBrain(npc NPC, brainType int8, logger Logger, options ...interface{}) Brain {
|
||||
func CreateBrain(npc NPC, brainType int8, logger Logger, options ...any) Brain {
|
||||
switch brainType {
|
||||
case BrainTypeCombatPet:
|
||||
return NewCombatPetBrain(npc, logger)
|
||||
|
@ -1,5 +1,7 @@
|
||||
package npc
|
||||
|
||||
import "fmt"
|
||||
|
||||
// Database interface for NPC persistence
|
||||
type Database interface {
|
||||
LoadAllNPCs() ([]*NPC, error)
|
||||
@ -13,10 +15,10 @@ type Database interface {
|
||||
|
||||
// Logger interface for NPC logging
|
||||
type Logger interface {
|
||||
LogInfo(message string, args ...interface{})
|
||||
LogError(message string, args ...interface{})
|
||||
LogDebug(message string, args ...interface{})
|
||||
LogWarning(message string, args ...interface{})
|
||||
LogInfo(message string, args ...any)
|
||||
LogError(message string, args ...any)
|
||||
LogDebug(message string, args ...any)
|
||||
LogWarning(message string, args ...any)
|
||||
}
|
||||
|
||||
// Client interface for NPC-related client operations
|
||||
@ -50,15 +52,15 @@ type Zone interface {
|
||||
RemoveNPC(npcID int32) error
|
||||
GetPlayersInRange(x, y, z, radius float32) []Player
|
||||
ProcessEntityCommand(command string, client Client, target *NPC) error
|
||||
CallSpawnScript(npc *NPC, scriptType string, args ...interface{}) error
|
||||
CallSpawnScript(npc *NPC, scriptType string, args ...any) error
|
||||
}
|
||||
|
||||
// SpellManager interface for spell system integration
|
||||
type SpellManager interface {
|
||||
GetSpell(spellID int32, tier int8) Spell
|
||||
CastSpell(caster *NPC, target interface{}, spell Spell) error
|
||||
GetSpellEffect(entity interface{}, spellID int32) SpellEffect
|
||||
ProcessSpell(spell Spell, caster *NPC, target interface{}) error
|
||||
CastSpell(caster *NPC, target any, spell Spell) error
|
||||
GetSpellEffect(entity any, spellID int32) SpellEffect
|
||||
ProcessSpell(spell Spell, caster *NPC, target any) error
|
||||
}
|
||||
|
||||
// Spell interface for spell data
|
||||
@ -88,8 +90,8 @@ type SpellEffect interface {
|
||||
type SkillManager interface {
|
||||
GetSkill(skillID int32) MasterSkill
|
||||
GetSkillByName(name string) MasterSkill
|
||||
ApplySkillBonus(entity interface{}, skillID int32, bonus float32) error
|
||||
RemoveSkillBonus(entity interface{}, skillID int32, bonus float32) error
|
||||
ApplySkillBonus(entity any, skillID int32, bonus float32) error
|
||||
RemoveSkillBonus(entity any, skillID int32, bonus float32) error
|
||||
}
|
||||
|
||||
// MasterSkill interface for skill definitions
|
||||
@ -127,11 +129,11 @@ type MovementManager interface {
|
||||
|
||||
// CombatManager interface for combat system integration
|
||||
type CombatManager interface {
|
||||
StartCombat(npc *NPC, target interface{}) error
|
||||
StartCombat(npc *NPC, target any) error
|
||||
EndCombat(npc *NPC) error
|
||||
ProcessCombatRound(npc *NPC) error
|
||||
CalculateDamage(attacker *NPC, target interface{}) int32
|
||||
ApplyDamage(target interface{}, damage int32) error
|
||||
CalculateDamage(attacker *NPC, target any) int32
|
||||
ApplyDamage(target any, damage int32) error
|
||||
}
|
||||
|
||||
// NPCAware interface for entities that can interact with NPCs
|
||||
@ -287,7 +289,7 @@ func NewSpellCasterAdapter(npc *NPC, spellManager SpellManager, logger Logger) *
|
||||
}
|
||||
|
||||
// GetNextSpell selects the next spell to cast based on AI strategy
|
||||
func (sca *SpellCasterAdapter) GetNextSpell(target interface{}, distance float32) Spell {
|
||||
func (sca *SpellCasterAdapter) GetNextSpell(target any, distance float32) Spell {
|
||||
if sca.npc == nil || sca.spellManager == nil {
|
||||
return nil
|
||||
}
|
||||
@ -307,7 +309,7 @@ func (sca *SpellCasterAdapter) GetNextSpell(target interface{}, distance float32
|
||||
}
|
||||
|
||||
// GetNextBuffSpell selects the next buff spell to cast
|
||||
func (sca *SpellCasterAdapter) GetNextBuffSpell(target interface{}) Spell {
|
||||
func (sca *SpellCasterAdapter) GetNextBuffSpell(target any) Spell {
|
||||
if sca.npc == nil || sca.spellManager == nil {
|
||||
return nil
|
||||
}
|
||||
@ -347,7 +349,7 @@ func (sca *SpellCasterAdapter) GetNextBuffSpell(target interface{}) Spell {
|
||||
}
|
||||
|
||||
// CastSpell attempts to cast a spell
|
||||
func (sca *SpellCasterAdapter) CastSpell(target interface{}, spell Spell) error {
|
||||
func (sca *SpellCasterAdapter) CastSpell(target any, spell Spell) error {
|
||||
if sca.npc == nil || sca.spellManager == nil || spell == nil {
|
||||
return fmt.Errorf("invalid parameters for spell casting")
|
||||
}
|
||||
@ -371,7 +373,7 @@ func (sca *SpellCasterAdapter) CastSpell(target interface{}, spell Spell) error
|
||||
}
|
||||
|
||||
// getNextCastOnAggroSpell selects cast-on-aggro spells
|
||||
func (sca *SpellCasterAdapter) getNextCastOnAggroSpell(target interface{}) Spell {
|
||||
func (sca *SpellCasterAdapter) getNextCastOnAggroSpell(target any) Spell {
|
||||
castOnSpells := sca.npc.castOnSpells[CastOnAggro]
|
||||
for _, npcSpell := range castOnSpells {
|
||||
spell := sca.spellManager.GetSpell(npcSpell.GetSpellID(), npcSpell.GetTier())
|
||||
@ -386,7 +388,7 @@ func (sca *SpellCasterAdapter) getNextCastOnAggroSpell(target interface{}) Spell
|
||||
}
|
||||
|
||||
// getNextSpellByStrategy selects spells based on AI strategy
|
||||
func (sca *SpellCasterAdapter) getNextSpellByStrategy(target interface{}, distance float32, strategy int8) Spell {
|
||||
func (sca *SpellCasterAdapter) getNextSpellByStrategy(target any, distance float32, strategy int8) Spell {
|
||||
// TODO: Implement more sophisticated spell selection based on strategy
|
||||
|
||||
for _, npcSpell := range sca.npc.spells {
|
||||
@ -446,7 +448,7 @@ func NewCombatAdapter(npc *NPC, combatManager CombatManager, logger Logger) *Com
|
||||
}
|
||||
|
||||
// EnterCombat handles entering combat state
|
||||
func (ca *CombatAdapter) EnterCombat(target interface{}) error {
|
||||
func (ca *CombatAdapter) EnterCombat(target any) error {
|
||||
if ca.npc == nil {
|
||||
return fmt.Errorf("NPC is nil")
|
||||
}
|
||||
|
@ -114,8 +114,8 @@ func (os *ObjectSpawn) ShowsCommandIcon() bool {
|
||||
}
|
||||
|
||||
// GetObjectInfo returns comprehensive information about the object spawn
|
||||
func (os *ObjectSpawn) GetObjectInfo() map[string]interface{} {
|
||||
info := make(map[string]interface{})
|
||||
func (os *ObjectSpawn) GetObjectInfo() map[string]any {
|
||||
info := make(map[string]any)
|
||||
|
||||
// Add spawn info
|
||||
info["spawn_id"] = os.GetID()
|
||||
@ -246,7 +246,7 @@ func ConvertSpawnToObject(spawn *spawn.Spawn) *ObjectSpawn {
|
||||
|
||||
// LoadObjectSpawnFromData loads object spawn data from database/config
|
||||
// This would be called when loading spawns from the database
|
||||
func LoadObjectSpawnFromData(spawnData map[string]interface{}) *ObjectSpawn {
|
||||
func LoadObjectSpawnFromData(spawnData map[string]any) *ObjectSpawn {
|
||||
objectSpawn := NewObjectSpawn()
|
||||
|
||||
// Load basic spawn data
|
||||
|
@ -315,11 +315,11 @@ func (om *ObjectManager) ClearZone(zoneName string) int {
|
||||
}
|
||||
|
||||
// GetStatistics returns statistics about objects in the manager
|
||||
func (om *ObjectManager) GetStatistics() map[string]interface{} {
|
||||
func (om *ObjectManager) GetStatistics() map[string]any {
|
||||
om.mutex.RLock()
|
||||
defer om.mutex.RUnlock()
|
||||
|
||||
stats := make(map[string]interface{})
|
||||
stats := make(map[string]any)
|
||||
stats["total_objects"] = len(om.objects)
|
||||
stats["zones_with_objects"] = len(om.objectsByZone)
|
||||
stats["interactive_objects"] = len(om.interactiveObjects)
|
||||
|
@ -581,11 +581,11 @@ func (o *Object) ShowsCommandIcon() bool {
|
||||
}
|
||||
|
||||
// GetObjectInfo returns comprehensive information about the object
|
||||
func (o *Object) GetObjectInfo() map[string]interface{} {
|
||||
func (o *Object) GetObjectInfo() map[string]any {
|
||||
o.mutex.RLock()
|
||||
defer o.mutex.RUnlock()
|
||||
|
||||
info := make(map[string]interface{})
|
||||
info := make(map[string]any)
|
||||
info["clickable"] = o.clickable
|
||||
info["zone_name"] = o.zoneName
|
||||
info["device_id"] = o.deviceID
|
||||
|
@ -40,10 +40,10 @@ type PlayerManager interface {
|
||||
GetPlayersInZone(zoneID int32) []*Player
|
||||
|
||||
// SendToAll sends a message to all players
|
||||
SendToAll(message interface{}) error
|
||||
SendToAll(message any) error
|
||||
|
||||
// SendToZone sends a message to all players in a zone
|
||||
SendToZone(zoneID int32, message interface{}) error
|
||||
SendToZone(zoneID int32, message any) error
|
||||
}
|
||||
|
||||
// PlayerDatabase interface for database operations
|
||||
@ -85,13 +85,13 @@ type PlayerDatabase interface {
|
||||
// PlayerPacketHandler interface for handling player packets
|
||||
type PlayerPacketHandler interface {
|
||||
// HandlePacket handles a packet from a player
|
||||
HandlePacket(player *Player, packet interface{}) error
|
||||
HandlePacket(player *Player, packet any) error
|
||||
|
||||
// SendPacket sends a packet to a player
|
||||
SendPacket(player *Player, packet interface{}) error
|
||||
SendPacket(player *Player, packet any) error
|
||||
|
||||
// BroadcastPacket broadcasts a packet to multiple players
|
||||
BroadcastPacket(players []*Player, packet interface{}) error
|
||||
BroadcastPacket(players []*Player, packet any) error
|
||||
}
|
||||
|
||||
// PlayerEventHandler interface for player events
|
||||
@ -181,7 +181,7 @@ type PlayerStatistics interface {
|
||||
RecordSpellCast(player *Player, spell *spells.Spell)
|
||||
|
||||
// GetStatistics returns player statistics
|
||||
GetStatistics(playerID int32) map[string]interface{}
|
||||
GetStatistics(playerID int32) map[string]any
|
||||
}
|
||||
|
||||
// PlayerNotifier interface for player notifications
|
||||
|
@ -274,7 +274,7 @@ func (m *Manager) GetPlayersInZone(zoneID int32) []*Player {
|
||||
}
|
||||
|
||||
// SendToAll sends a message to all players
|
||||
func (m *Manager) SendToAll(message interface{}) error {
|
||||
func (m *Manager) SendToAll(message any) error {
|
||||
if m.packetHandler == nil {
|
||||
return fmt.Errorf("no packet handler configured")
|
||||
}
|
||||
@ -284,7 +284,7 @@ func (m *Manager) SendToAll(message interface{}) error {
|
||||
}
|
||||
|
||||
// SendToZone sends a message to all players in a zone
|
||||
func (m *Manager) SendToZone(zoneID int32, message interface{}) error {
|
||||
func (m *Manager) SendToZone(zoneID int32, message any) error {
|
||||
if m.packetHandler == nil {
|
||||
return fmt.Errorf("no packet handler configured")
|
||||
}
|
||||
|
@ -114,12 +114,12 @@ type Packet interface {
|
||||
// PacketStruct interface defines packet structure functionality
|
||||
type PacketStruct interface {
|
||||
// Packet building methods
|
||||
SetDataByName(name string, value interface{}, index ...int)
|
||||
SetDataByName(name string, value any, index ...int)
|
||||
SetArrayLengthByName(name string, length int)
|
||||
SetArrayDataByName(name string, value interface{}, index int)
|
||||
SetArrayDataByName(name string, value any, index int)
|
||||
SetSubArrayLengthByName(name string, length int, index int)
|
||||
SetSubArrayDataByName(name string, value interface{}, index1, index2 int)
|
||||
SetSubstructArrayDataByName(substruct, name string, value interface{}, index int)
|
||||
SetSubArrayDataByName(name string, value any, index1, index2 int)
|
||||
SetSubstructArrayDataByName(substruct, name string, value any, index int)
|
||||
SetItemArrayDataByName(name string, item Item, player Player, index int, flag ...int)
|
||||
Serialize() Packet
|
||||
GetVersion() int16
|
||||
@ -197,10 +197,10 @@ type Database interface {
|
||||
|
||||
// Logger interface defines logging functionality for quest system
|
||||
type Logger interface {
|
||||
LogInfo(message string, args ...interface{})
|
||||
LogError(message string, args ...interface{})
|
||||
LogDebug(message string, args ...interface{})
|
||||
LogWarning(message string, args ...interface{})
|
||||
LogInfo(message string, args ...any)
|
||||
LogError(message string, args ...any)
|
||||
LogDebug(message string, args ...any)
|
||||
LogWarning(message string, args ...any)
|
||||
}
|
||||
|
||||
// Configuration interface defines quest system configuration
|
||||
|
@ -32,7 +32,7 @@ func NewRaceIntegration() *RaceIntegration {
|
||||
}
|
||||
|
||||
// ValidateEntityRace validates an entity's race and provides detailed information
|
||||
func (ri *RaceIntegration) ValidateEntityRace(entity RaceAware) (bool, string, map[string]interface{}) {
|
||||
func (ri *RaceIntegration) ValidateEntityRace(entity RaceAware) (bool, string, map[string]any) {
|
||||
raceID := entity.GetRace()
|
||||
|
||||
if !ri.races.IsValidRaceID(raceID) {
|
||||
@ -63,8 +63,8 @@ func (ri *RaceIntegration) ApplyRacialBonuses(entity RaceAware, stats map[string
|
||||
}
|
||||
|
||||
// GetEntityRaceInfo returns comprehensive race information for an entity
|
||||
func (ri *RaceIntegration) GetEntityRaceInfo(entity EntityWithRace) map[string]interface{} {
|
||||
info := make(map[string]interface{})
|
||||
func (ri *RaceIntegration) GetEntityRaceInfo(entity EntityWithRace) map[string]any {
|
||||
info := make(map[string]any)
|
||||
|
||||
// Basic entity info
|
||||
info["entity_id"] = entity.GetID()
|
||||
@ -230,12 +230,12 @@ func (ri *RaceIntegration) GetRaceStartingStats(raceID int8) map[string]int16 {
|
||||
}
|
||||
|
||||
// CreateRaceSpecificEntity creates entity data with race-specific properties
|
||||
func (ri *RaceIntegration) CreateRaceSpecificEntity(raceID int8) map[string]interface{} {
|
||||
func (ri *RaceIntegration) CreateRaceSpecificEntity(raceID int8) map[string]any {
|
||||
if !ri.races.IsValidRaceID(raceID) {
|
||||
return nil
|
||||
}
|
||||
|
||||
entityData := make(map[string]interface{})
|
||||
entityData := make(map[string]any)
|
||||
|
||||
// Basic race info
|
||||
entityData["race_id"] = raceID
|
||||
@ -255,15 +255,15 @@ func (ri *RaceIntegration) CreateRaceSpecificEntity(raceID int8) map[string]inte
|
||||
}
|
||||
|
||||
// GetRaceSelectionData returns data for race selection UI
|
||||
func (ri *RaceIntegration) GetRaceSelectionData() map[string]interface{} {
|
||||
data := make(map[string]interface{})
|
||||
func (ri *RaceIntegration) GetRaceSelectionData() map[string]any {
|
||||
data := make(map[string]any)
|
||||
|
||||
// All available races
|
||||
allRaces := ri.races.GetAllRaces()
|
||||
raceList := make([]map[string]interface{}, 0, len(allRaces))
|
||||
raceList := make([]map[string]any, 0, len(allRaces))
|
||||
|
||||
for raceID, friendlyName := range allRaces {
|
||||
raceData := map[string]interface{}{
|
||||
raceData := map[string]any{
|
||||
"id": raceID,
|
||||
"name": friendlyName,
|
||||
"alignment": ri.races.GetRaceAlignment(raceID),
|
||||
|
@ -267,8 +267,8 @@ func (rm *RaceManager) handleSearchCommand(args []string) (string, error) {
|
||||
}
|
||||
|
||||
// ValidateEntityRaces validates races for a collection of entities
|
||||
func (rm *RaceManager) ValidateEntityRaces(entities []RaceAware) map[string]interface{} {
|
||||
validationResults := make(map[string]interface{})
|
||||
func (rm *RaceManager) ValidateEntityRaces(entities []RaceAware) map[string]any {
|
||||
validationResults := make(map[string]any)
|
||||
|
||||
validCount := 0
|
||||
invalidCount := 0
|
||||
@ -288,11 +288,11 @@ func (rm *RaceManager) ValidateEntityRaces(entities []RaceAware) map[string]inte
|
||||
// Track invalid entities
|
||||
if !isValid {
|
||||
if validationResults["invalid_entities"] == nil {
|
||||
validationResults["invalid_entities"] = make([]map[string]interface{}, 0)
|
||||
validationResults["invalid_entities"] = make([]map[string]any, 0)
|
||||
}
|
||||
|
||||
invalidList := validationResults["invalid_entities"].([]map[string]interface{})
|
||||
invalidList = append(invalidList, map[string]interface{}{
|
||||
invalidList := validationResults["invalid_entities"].([]map[string]any)
|
||||
invalidList = append(invalidList, map[string]any{
|
||||
"index": i,
|
||||
"race_id": raceID,
|
||||
})
|
||||
@ -309,7 +309,7 @@ func (rm *RaceManager) ValidateEntityRaces(entities []RaceAware) map[string]inte
|
||||
}
|
||||
|
||||
// GetRaceRecommendations returns race recommendations for character creation
|
||||
func (rm *RaceManager) GetRaceRecommendations(preferences map[string]interface{}) []int8 {
|
||||
func (rm *RaceManager) GetRaceRecommendations(preferences map[string]any) []int8 {
|
||||
recommendations := make([]int8, 0)
|
||||
|
||||
// Check for alignment preference
|
||||
|
@ -351,11 +351,11 @@ func (r *Races) GetEvilRaceCount() int {
|
||||
}
|
||||
|
||||
// GetRaceInfo returns comprehensive information about a race
|
||||
func (r *Races) GetRaceInfo(raceID int8) map[string]interface{} {
|
||||
func (r *Races) GetRaceInfo(raceID int8) map[string]any {
|
||||
r.mutex.RLock()
|
||||
defer r.mutex.RUnlock()
|
||||
|
||||
info := make(map[string]interface{})
|
||||
info := make(map[string]any)
|
||||
|
||||
if !r.IsValidRaceID(raceID) {
|
||||
info["valid"] = false
|
||||
|
@ -321,8 +321,8 @@ func (ru *RaceUtils) GetRaceAliases(raceID int8) []string {
|
||||
}
|
||||
|
||||
// GetRaceStatistics returns statistics about the race system
|
||||
func (ru *RaceUtils) GetRaceStatistics() map[string]interface{} {
|
||||
stats := make(map[string]interface{})
|
||||
func (ru *RaceUtils) GetRaceStatistics() map[string]any {
|
||||
stats := make(map[string]any)
|
||||
|
||||
stats["total_races"] = ru.races.GetRaceCount()
|
||||
stats["good_races"] = ru.races.GetGoodRaceCount()
|
||||
|
@ -320,11 +320,11 @@ func (r *Recipe) GetComponentQuantityForSlot(slot int8) int16 {
|
||||
}
|
||||
|
||||
// GetInfo returns comprehensive information about the recipe
|
||||
func (r *Recipe) GetInfo() map[string]interface{} {
|
||||
func (r *Recipe) GetInfo() map[string]any {
|
||||
r.mutex.RLock()
|
||||
defer r.mutex.RUnlock()
|
||||
|
||||
info := make(map[string]interface{})
|
||||
info := make(map[string]any)
|
||||
|
||||
info["id"] = r.ID
|
||||
info["soe_id"] = r.SoeID
|
||||
|
@ -86,12 +86,12 @@ type RuleValidator interface {
|
||||
|
||||
// ValidationRule defines validation criteria for a rule
|
||||
type ValidationRule struct {
|
||||
Required bool // Whether the rule is required
|
||||
MinValue interface{} // Minimum allowed value (for numeric types)
|
||||
MaxValue interface{} // Maximum allowed value (for numeric types)
|
||||
ValidValues []string // List of valid string values
|
||||
Pattern string // Regex pattern for validation
|
||||
Description string // Description of the rule
|
||||
Required bool // Whether the rule is required
|
||||
MinValue any // Minimum allowed value (for numeric types)
|
||||
MaxValue any // Maximum allowed value (for numeric types)
|
||||
ValidValues []string // List of valid string values
|
||||
Pattern string // Regex pattern for validation
|
||||
Description string // Description of the rule
|
||||
}
|
||||
|
||||
// RuleEventHandler defines the interface for rule change events
|
||||
|
@ -21,7 +21,7 @@ type Client interface {
|
||||
GetCurrentZone() Zone
|
||||
SetTemporaryTransportID(id int32)
|
||||
SimpleMessage(channel int32, message string)
|
||||
Message(channel int32, format string, args ...interface{})
|
||||
Message(channel int32, format string, args ...any)
|
||||
CheckZoneAccess(zoneName string) bool
|
||||
TryZoneInstance(zoneID int32, useDefaults bool) bool
|
||||
Zone(zoneName string, useDefaults bool) error
|
||||
@ -66,10 +66,10 @@ type EntityCommand struct {
|
||||
|
||||
// Logger interface for sign logging
|
||||
type Logger interface {
|
||||
LogInfo(message string, args ...interface{})
|
||||
LogError(message string, args ...interface{})
|
||||
LogDebug(message string, args ...interface{})
|
||||
LogWarning(message string, args ...interface{})
|
||||
LogInfo(message string, args ...any)
|
||||
LogError(message string, args ...any)
|
||||
LogDebug(message string, args ...any)
|
||||
LogWarning(message string, args ...any)
|
||||
}
|
||||
|
||||
// SignSpawn provides sign functionality for spawn entities
|
||||
|
@ -265,11 +265,11 @@ func (m *Manager) HandleSignUse(sign *Sign, client Client, command string) error
|
||||
}
|
||||
|
||||
// GetStatistics returns sign system statistics
|
||||
func (m *Manager) GetStatistics() map[string]interface{} {
|
||||
func (m *Manager) GetStatistics() map[string]any {
|
||||
m.mutex.RLock()
|
||||
defer m.mutex.RUnlock()
|
||||
|
||||
stats := make(map[string]interface{})
|
||||
stats := make(map[string]any)
|
||||
stats["total_signs"] = m.totalSigns
|
||||
stats["sign_interactions"] = m.signInteractions
|
||||
stats["zone_transports"] = m.zoneTransports
|
||||
|
@ -27,9 +27,9 @@ type Database interface {
|
||||
|
||||
// Logger interface for skill system logging
|
||||
type Logger interface {
|
||||
LogInfo(message string, args ...interface{})
|
||||
LogError(message string, args ...interface{})
|
||||
LogDebug(message string, args ...interface{})
|
||||
LogInfo(message string, args ...any)
|
||||
LogError(message string, args ...any)
|
||||
LogDebug(message string, args ...any)
|
||||
}
|
||||
|
||||
// EntitySkillAdapter provides skill functionality for entities
|
||||
|
@ -73,11 +73,11 @@ func (m *Manager) RecordSkillUp(skillID int32, skillType int32) {
|
||||
}
|
||||
|
||||
// GetStatistics returns skill system statistics
|
||||
func (m *Manager) GetStatistics() map[string]interface{} {
|
||||
func (m *Manager) GetStatistics() map[string]any {
|
||||
m.mutex.RLock()
|
||||
defer m.mutex.RUnlock()
|
||||
|
||||
stats := make(map[string]interface{})
|
||||
stats := make(map[string]any)
|
||||
stats["total_skill_ups"] = m.totalSkillUps
|
||||
stats["players_with_skills"] = m.playersWithSkills
|
||||
stats["total_skills_in_master"] = m.masterSkillList.GetSkillCount()
|
||||
|
@ -1074,7 +1074,7 @@ func (s *Spawn) SetBasicInfo(info *BasicInfoStruct) {
|
||||
}
|
||||
|
||||
// GetClient returns the client associated with this spawn (overridden by Player)
|
||||
func (s *Spawn) GetClient() interface{} {
|
||||
func (s *Spawn) GetClient() any {
|
||||
return nil // Base spawns have no client
|
||||
}
|
||||
|
||||
|
@ -555,13 +555,13 @@ func (sm *SpellManager) CanCastSpell(casterID, targetID, spellID int32) (bool, s
|
||||
}
|
||||
|
||||
// GetSpellInfo returns comprehensive information about a spell
|
||||
func (sm *SpellManager) GetSpellInfo(spellID int32) map[string]interface{} {
|
||||
func (sm *SpellManager) GetSpellInfo(spellID int32) map[string]any {
|
||||
spell := sm.masterList.GetSpell(spellID)
|
||||
if spell == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
info := make(map[string]interface{})
|
||||
info := make(map[string]any)
|
||||
|
||||
// Basic spell info
|
||||
info["id"] = spell.GetSpellID()
|
||||
|
@ -155,7 +155,7 @@ func (st *SpellTargeting) getAETargets(luaSpell *LuaSpell, result *TargetingResu
|
||||
|
||||
// For now, implement basic logic
|
||||
_ = luaSpell.Spell.GetSpellData() // TODO: Use spell data when needed
|
||||
maxTargets := int32(10) // TODO: Use spellData.AOENodeNumber when field exists
|
||||
maxTargets := int32(10) // TODO: Use spellData.AOENodeNumber when field exists
|
||||
if maxTargets <= 0 {
|
||||
maxTargets = 10 // Default limit
|
||||
}
|
||||
@ -416,8 +416,8 @@ func (st *SpellTargeting) RequiresTarget(spell *Spell) bool {
|
||||
}
|
||||
|
||||
// GetTargetingInfo returns information about spell targeting requirements
|
||||
func (st *SpellTargeting) GetTargetingInfo(spell *Spell) map[string]interface{} {
|
||||
info := make(map[string]interface{})
|
||||
func (st *SpellTargeting) GetTargetingInfo(spell *Spell) map[string]any {
|
||||
info := make(map[string]any)
|
||||
|
||||
if spell == nil {
|
||||
return info
|
||||
|
@ -339,11 +339,11 @@ func (mtl *MasterTitlesList) UpdateTitle(title *Title) error {
|
||||
categorySlice := mtl.categorized[existing.Category]
|
||||
mtl.removeFromSlice(&categorySlice, existing)
|
||||
mtl.categorized[existing.Category] = categorySlice
|
||||
|
||||
|
||||
sourceSlice := mtl.bySource[existing.Source]
|
||||
mtl.removeFromSlice(&sourceSlice, existing)
|
||||
mtl.bySource[existing.Source] = sourceSlice
|
||||
|
||||
|
||||
raritySlice := mtl.byRarity[existing.Rarity]
|
||||
mtl.removeFromSlice(&raritySlice, existing)
|
||||
mtl.byRarity[existing.Rarity] = raritySlice
|
||||
@ -487,7 +487,7 @@ func (mtl *MasterTitlesList) SaveToDatabase(db *database.DB) error {
|
||||
|
||||
// Insert all current titles
|
||||
for _, title := range mtl.titles {
|
||||
var achievementID interface{}
|
||||
var achievementID any
|
||||
if title.AchievementID != 0 {
|
||||
achievementID = title.AchievementID
|
||||
}
|
||||
@ -495,9 +495,9 @@ func (mtl *MasterTitlesList) SaveToDatabase(db *database.DB) error {
|
||||
err := txDB.Exec(`
|
||||
INSERT INTO titles (id, name, description, category, position, source, rarity, flags, achievement_id)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`, title.ID, title.Name, title.Description, title.Category,
|
||||
int(title.Position), int(title.Source), int(title.Rarity),
|
||||
int64(title.Flags), achievementID)
|
||||
`, title.ID, title.Name, title.Description, title.Category,
|
||||
int(title.Position), int(title.Source), int(title.Rarity),
|
||||
int64(title.Flags), achievementID)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to insert title %d: %w", title.ID, err)
|
||||
|
@ -451,7 +451,7 @@ func (ptl *PlayerTitlesList) LoadFromDatabase(db *database.DB) error {
|
||||
}
|
||||
|
||||
// Load all titles for this player
|
||||
err := db.Query("SELECT title_id, achievement_id, granted_date, expiration_date, is_active FROM player_titles WHERE player_id = ?",
|
||||
err := db.Query("SELECT title_id, achievement_id, granted_date, expiration_date, is_active FROM player_titles WHERE player_id = ?",
|
||||
func(row *database.Row) error {
|
||||
playerTitle := &PlayerTitle{
|
||||
TitleID: int32(row.Int64(0)),
|
||||
@ -499,12 +499,12 @@ func (ptl *PlayerTitlesList) SaveToDatabase(db *database.DB) error {
|
||||
|
||||
// Insert all current titles
|
||||
for _, playerTitle := range ptl.titles {
|
||||
var achievementID interface{}
|
||||
var achievementID any
|
||||
if playerTitle.AchievementID != 0 {
|
||||
achievementID = playerTitle.AchievementID
|
||||
}
|
||||
|
||||
var expirationDate interface{}
|
||||
var expirationDate any
|
||||
if !playerTitle.ExpiresAt.IsZero() {
|
||||
expirationDate = playerTitle.ExpiresAt.Unix()
|
||||
}
|
||||
@ -519,8 +519,8 @@ func (ptl *PlayerTitlesList) SaveToDatabase(db *database.DB) error {
|
||||
err := txDB.Exec(`
|
||||
INSERT INTO player_titles (player_id, title_id, achievement_id, granted_date, expiration_date, is_active)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
`, ptl.playerID, playerTitle.TitleID, achievementID,
|
||||
playerTitle.EarnedDate.Unix(), expirationDate, isActive)
|
||||
`, ptl.playerID, playerTitle.TitleID, achievementID,
|
||||
playerTitle.EarnedDate.Unix(), expirationDate, isActive)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to insert player title %d: %w", playerTitle.TitleID, err)
|
||||
|
@ -319,11 +319,11 @@ func (tm *TitleManager) cleanupExpiredTitles() {
|
||||
}
|
||||
|
||||
// GetStatistics returns title system statistics
|
||||
func (tm *TitleManager) GetStatistics() map[string]interface{} {
|
||||
func (tm *TitleManager) GetStatistics() map[string]any {
|
||||
tm.mutex.RLock()
|
||||
defer tm.mutex.RUnlock()
|
||||
|
||||
return map[string]interface{}{
|
||||
return map[string]any{
|
||||
"total_titles": tm.masterList.GetTitleCount(),
|
||||
"total_players": len(tm.playerLists),
|
||||
"titles_granted": tm.totalTitlesGranted,
|
||||
|
@ -146,7 +146,7 @@ func (ts *TradeService) CancelTrade(entityID int32) error {
|
||||
}
|
||||
|
||||
// GetTradeInfo returns comprehensive information about a trade
|
||||
func (ts *TradeService) GetTradeInfo(entityID int32) (map[string]interface{}, error) {
|
||||
func (ts *TradeService) GetTradeInfo(entityID int32) (map[string]any, error) {
|
||||
trade := ts.tradeManager.GetTrade(entityID)
|
||||
if trade == nil {
|
||||
return nil, fmt.Errorf("entity is not in a trade")
|
||||
@ -174,8 +174,8 @@ func (ts *TradeService) ProcessTrades() {
|
||||
}
|
||||
|
||||
// GetTradeStatistics returns statistics about trade activity
|
||||
func (ts *TradeService) GetTradeStatistics() map[string]interface{} {
|
||||
stats := make(map[string]interface{})
|
||||
func (ts *TradeService) GetTradeStatistics() map[string]any {
|
||||
stats := make(map[string]any)
|
||||
|
||||
stats["active_trades"] = ts.tradeManager.GetActiveTradeCount()
|
||||
stats["max_trade_duration_minutes"] = ts.maxTradeDuration.Minutes()
|
||||
|
@ -392,11 +392,11 @@ func (t *Trade) GetTraderSlot(entityID int32, slot int8) Item {
|
||||
}
|
||||
|
||||
// GetTradeInfo returns comprehensive information about the trade
|
||||
func (t *Trade) GetTradeInfo() map[string]interface{} {
|
||||
func (t *Trade) GetTradeInfo() map[string]any {
|
||||
t.mutex.RLock()
|
||||
defer t.mutex.RUnlock()
|
||||
|
||||
info := make(map[string]interface{})
|
||||
info := make(map[string]any)
|
||||
info["state"] = t.state
|
||||
info["start_time"] = t.startTime
|
||||
info["trader1_id"] = t.trader1.EntityID
|
||||
|
@ -127,7 +127,7 @@ func IsValidTradeState(state TradeState, operation string) bool {
|
||||
}
|
||||
|
||||
// GenerateTradeLogEntry creates a log entry for trade operations
|
||||
func GenerateTradeLogEntry(tradeID string, operation string, entityID int32, details interface{}) string {
|
||||
func GenerateTradeLogEntry(tradeID string, operation string, entityID int32, details any) string {
|
||||
return fmt.Sprintf("[Trade:%s] %s by entity %d: %v", tradeID, operation, entityID, details)
|
||||
}
|
||||
|
||||
@ -147,8 +147,8 @@ func CompareTradeItems(item1, item2 TradeItemInfo) bool {
|
||||
|
||||
// CalculateTradeValue estimates the total value of items and coins in a trade
|
||||
// This is a helper function for trade balancing and analysis
|
||||
func CalculateTradeValue(participant *TradeParticipant) map[string]interface{} {
|
||||
value := make(map[string]interface{})
|
||||
func CalculateTradeValue(participant *TradeParticipant) map[string]any {
|
||||
value := make(map[string]any)
|
||||
|
||||
// Add coin value
|
||||
value["coins"] = participant.Coins
|
||||
@ -159,9 +159,9 @@ func CalculateTradeValue(participant *TradeParticipant) map[string]interface{} {
|
||||
value["item_count"] = itemCount
|
||||
|
||||
if itemCount > 0 {
|
||||
items := make([]map[string]interface{}, 0, itemCount)
|
||||
items := make([]map[string]any, 0, itemCount)
|
||||
for slot, itemInfo := range participant.Items {
|
||||
itemData := make(map[string]interface{})
|
||||
itemData := make(map[string]any)
|
||||
itemData["slot"] = slot
|
||||
itemData["quantity"] = itemInfo.Quantity
|
||||
if itemInfo.Item != nil {
|
||||
|
@ -596,11 +596,11 @@ func (tsa *TradeskillSystemAdapter) updatePlayerRecipeStage(currentStage int8, c
|
||||
}
|
||||
|
||||
// GetSystemStats returns comprehensive statistics about the tradeskill system.
|
||||
func (tsa *TradeskillSystemAdapter) GetSystemStats() map[string]interface{} {
|
||||
func (tsa *TradeskillSystemAdapter) GetSystemStats() map[string]any {
|
||||
managerStats := tsa.manager.GetStats()
|
||||
eventsStats := tsa.eventsList.GetStats()
|
||||
|
||||
return map[string]interface{}{
|
||||
return map[string]any{
|
||||
"active_sessions": managerStats.ActiveSessions,
|
||||
"recent_completions": managerStats.RecentCompletions,
|
||||
"average_session_time": managerStats.AverageSessionTime,
|
||||
|
@ -348,7 +348,7 @@ func (tsa *TraitSystemAdapter) IsPlayerAllowedTrait(playerID uint32, spellID uin
|
||||
}
|
||||
|
||||
// GetPlayerTraitStats returns statistics about a player's trait selections.
|
||||
func (tsa *TraitSystemAdapter) GetPlayerTraitStats(playerID uint32) (map[string]interface{}, error) {
|
||||
func (tsa *TraitSystemAdapter) GetPlayerTraitStats(playerID uint32) (map[string]any, error) {
|
||||
// Get player information
|
||||
player, err := tsa.playerManager.GetPlayer(playerID)
|
||||
if err != nil {
|
||||
@ -381,7 +381,7 @@ func (tsa *TraitSystemAdapter) GetPlayerTraitStats(playerID uint32) (map[string]
|
||||
tsa.masterList.getSpellCount(playerState, playerState.TraitLists.InnateRaceTraits, false)
|
||||
focusEffects := tsa.masterList.getSpellCount(playerState, playerState.TraitLists.FocusEffects, false)
|
||||
|
||||
return map[string]interface{}{
|
||||
return map[string]any{
|
||||
"player_id": playerID,
|
||||
"level": playerState.Level,
|
||||
"character_traits": characterTraits,
|
||||
@ -394,11 +394,11 @@ func (tsa *TraitSystemAdapter) GetPlayerTraitStats(playerID uint32) (map[string]
|
||||
}
|
||||
|
||||
// GetSystemStats returns comprehensive statistics about the trait system.
|
||||
func (tsa *TraitSystemAdapter) GetSystemStats() map[string]interface{} {
|
||||
func (tsa *TraitSystemAdapter) GetSystemStats() map[string]any {
|
||||
masterStats := tsa.masterList.GetStats()
|
||||
managerStats := tsa.traitManager.GetManagerStats()
|
||||
|
||||
return map[string]interface{}{
|
||||
return map[string]any{
|
||||
"total_traits": masterStats.TotalTraits,
|
||||
"traits_by_type": masterStats.TraitsByType,
|
||||
"traits_by_group": masterStats.TraitsByGroup,
|
||||
|
@ -325,7 +325,7 @@ func (mtl *MasterTraitList) getClassicAvailability(levelLimits []int16, totalUse
|
||||
}
|
||||
|
||||
// getSpellCount counts how many spells from a trait map the player has selected.
|
||||
func (mtl *MasterTraitList) getSpellCount(playerState *PlayerTraitState, traitMap interface{}, onlyCharTraits bool) int16 {
|
||||
func (mtl *MasterTraitList) getSpellCount(playerState *PlayerTraitState, traitMap any, onlyCharTraits bool) int16 {
|
||||
count := int16(0)
|
||||
|
||||
switch tm := traitMap.(type) {
|
||||
|
@ -94,11 +94,11 @@ func (m *Manager) ReloadTransmutingTiers() error {
|
||||
}
|
||||
|
||||
// GetStatistics returns transmutation statistics
|
||||
func (m *Manager) GetStatistics() map[string]interface{} {
|
||||
func (m *Manager) GetStatistics() map[string]any {
|
||||
m.mutex.RLock()
|
||||
defer m.mutex.RUnlock()
|
||||
|
||||
stats := make(map[string]interface{})
|
||||
stats := make(map[string]any)
|
||||
stats["total_transmutes"] = m.totalTransmutes
|
||||
stats["successful_transmutes"] = m.successfulTransmutes
|
||||
stats["failed_transmutes"] = m.failedTransmutes
|
||||
|
@ -71,7 +71,7 @@ type Client interface {
|
||||
SetTransmuteID(id int32)
|
||||
QueuePacket(packet []byte)
|
||||
SimpleMessage(channel int32, message string)
|
||||
Message(channel int32, format string, args ...interface{})
|
||||
Message(channel int32, format string, args ...any)
|
||||
AddItem(item Item, itemDeleted *bool) error
|
||||
}
|
||||
|
||||
|
@ -8,7 +8,7 @@ import (
|
||||
type ClientInterface interface {
|
||||
GetPlayer() *spawn.Spawn
|
||||
SetTemporaryTransportID(id int32)
|
||||
ProcessTeleport(widget *Widget, destinations []interface{}, transporterID int32)
|
||||
ProcessTeleport(widget *Widget, destinations []any, transporterID int32)
|
||||
GetVersion() int32
|
||||
GetCurrentZone() ZoneInterface
|
||||
}
|
||||
@ -21,8 +21,8 @@ type ZoneInterface interface {
|
||||
PlaySoundFile(unknown int32, soundFile string, x, y, z float32)
|
||||
CallSpawnScript(s *spawn.Spawn, scriptType string, caller *spawn.Spawn, extra string, state bool) bool
|
||||
GetSpawnByDatabaseID(id int32) *spawn.Spawn
|
||||
GetTransporters(client ClientInterface, transporterID int32) []interface{}
|
||||
ProcessEntityCommand(command interface{}, player *spawn.Spawn, target *spawn.Spawn)
|
||||
GetTransporters(client ClientInterface, transporterID int32) []any
|
||||
ProcessEntityCommand(command any, player *spawn.Spawn, target *spawn.Spawn)
|
||||
GetInstanceID() int32
|
||||
GetInstanceType() int32
|
||||
SendHouseItems(client ClientInterface)
|
||||
|
@ -295,11 +295,11 @@ func (m *Manager) Clear() {
|
||||
}
|
||||
|
||||
// GetStatistics returns widget statistics
|
||||
func (m *Manager) GetStatistics() map[string]interface{} {
|
||||
func (m *Manager) GetStatistics() map[string]any {
|
||||
m.mutex.RLock()
|
||||
defer m.mutex.RUnlock()
|
||||
|
||||
stats := make(map[string]interface{})
|
||||
stats := make(map[string]any)
|
||||
stats["total_widgets"] = len(m.widgets)
|
||||
stats["door_count"] = len(m.GetDoorWidgets())
|
||||
stats["lift_count"] = len(m.GetLiftWidgets())
|
||||
|
@ -97,8 +97,8 @@ type NPC interface {
|
||||
SetRespawnTime(seconds int32)
|
||||
GetSpawnGroupID() int32
|
||||
SetSpawnGroupID(groupID int32)
|
||||
GetRandomizedFeatures() map[string]interface{}
|
||||
SetRandomizedFeatures(features map[string]interface{})
|
||||
GetRandomizedFeatures() map[string]any
|
||||
SetRandomizedFeatures(features map[string]any)
|
||||
}
|
||||
|
||||
// Object interface represents an interactive world object
|
||||
@ -337,12 +337,12 @@ type Recipe interface {
|
||||
|
||||
// MovementLocation represents a movement waypoint for NPCs
|
||||
type MovementLocation struct {
|
||||
X float32
|
||||
Y float32
|
||||
Z float32
|
||||
Heading float32
|
||||
Speed float32
|
||||
Delay int32
|
||||
X float32
|
||||
Y float32
|
||||
Z float32
|
||||
Heading float32
|
||||
Speed float32
|
||||
Delay int32
|
||||
MovementType int8
|
||||
}
|
||||
|
||||
@ -357,7 +357,7 @@ type PathNode struct {
|
||||
type SpawnLocation struct {
|
||||
ID int32
|
||||
X float32
|
||||
Y float32
|
||||
Y float32
|
||||
Z float32
|
||||
Heading float32
|
||||
Pitch float32
|
||||
@ -375,46 +375,46 @@ type SpawnLocation struct {
|
||||
|
||||
// SpawnEntry contains the template data for spawns
|
||||
type SpawnEntry struct {
|
||||
ID int32
|
||||
SpawnType int8
|
||||
SpawnEntryID int32
|
||||
Name string
|
||||
Level int16
|
||||
EncounterLevel int16
|
||||
Model string
|
||||
Size float32
|
||||
HP int32
|
||||
Power int32
|
||||
Heroic int8
|
||||
Gender int8
|
||||
Race int16
|
||||
AdventureClass int16
|
||||
ID int32
|
||||
SpawnType int8
|
||||
SpawnEntryID int32
|
||||
Name string
|
||||
Level int16
|
||||
EncounterLevel int16
|
||||
Model string
|
||||
Size float32
|
||||
HP int32
|
||||
Power int32
|
||||
Heroic int8
|
||||
Gender int8
|
||||
Race int16
|
||||
AdventureClass int16
|
||||
TradeskillClass int16
|
||||
AttackType int8
|
||||
MinLevel int16
|
||||
MaxLevel int16
|
||||
EncounterType int8
|
||||
ShowName int8
|
||||
Targetable int8
|
||||
ShowLevel int8
|
||||
Command string
|
||||
LootTier int8
|
||||
MinGold int32
|
||||
MaxGold int32
|
||||
HarvestType string
|
||||
Icon int32
|
||||
AttackType int8
|
||||
MinLevel int16
|
||||
MaxLevel int16
|
||||
EncounterType int8
|
||||
ShowName int8
|
||||
Targetable int8
|
||||
ShowLevel int8
|
||||
Command string
|
||||
LootTier int8
|
||||
MinGold int32
|
||||
MaxGold int32
|
||||
HarvestType string
|
||||
Icon int32
|
||||
}
|
||||
|
||||
// EntityCommand represents an available command for an entity
|
||||
type EntityCommand struct {
|
||||
ID int32
|
||||
Name string
|
||||
Distance float32
|
||||
ErrorText string
|
||||
CastTime int16
|
||||
SpellVisual int32
|
||||
Command string
|
||||
DisplayText string
|
||||
ID int32
|
||||
Name string
|
||||
Distance float32
|
||||
ErrorText string
|
||||
CastTime int16
|
||||
SpellVisual int32
|
||||
Command string
|
||||
DisplayText string
|
||||
}
|
||||
|
||||
// LootTable represents a loot table configuration
|
||||
@ -433,9 +433,9 @@ type LootTable struct {
|
||||
type LootDrop struct {
|
||||
LootTableID int32
|
||||
ItemID int32
|
||||
ItemCharges int16
|
||||
EquipItem bool
|
||||
Probability float32
|
||||
ItemCharges int16
|
||||
EquipItem bool
|
||||
Probability float32
|
||||
NoDropQuestCompletedID int32
|
||||
}
|
||||
|
||||
@ -452,51 +452,51 @@ type GlobalLoot struct {
|
||||
|
||||
// TransportDestination represents a transport destination
|
||||
type TransportDestination struct {
|
||||
ID int32
|
||||
Type int8
|
||||
Name string
|
||||
Message string
|
||||
DestinationZoneID int32
|
||||
DestinationX float32
|
||||
DestinationY float32
|
||||
DestinationZ float32
|
||||
ID int32
|
||||
Type int8
|
||||
Name string
|
||||
Message string
|
||||
DestinationZoneID int32
|
||||
DestinationX float32
|
||||
DestinationY float32
|
||||
DestinationZ float32
|
||||
DestinationHeading float32
|
||||
Cost int32
|
||||
UniqueID int32
|
||||
MinLevel int8
|
||||
MaxLevel int8
|
||||
QuestRequired int32
|
||||
QuestStepRequired int16
|
||||
QuestCompleted int32
|
||||
MapX int32
|
||||
MapY int32
|
||||
ExpansionFlag int32
|
||||
HolidayFlag int32
|
||||
MinClientVersion int32
|
||||
MaxClientVersion int32
|
||||
FlightPathID int32
|
||||
MountID int16
|
||||
MountRedColor int8
|
||||
MountGreenColor int8
|
||||
MountBlueColor int8
|
||||
Cost int32
|
||||
UniqueID int32
|
||||
MinLevel int8
|
||||
MaxLevel int8
|
||||
QuestRequired int32
|
||||
QuestStepRequired int16
|
||||
QuestCompleted int32
|
||||
MapX int32
|
||||
MapY int32
|
||||
ExpansionFlag int32
|
||||
HolidayFlag int32
|
||||
MinClientVersion int32
|
||||
MaxClientVersion int32
|
||||
FlightPathID int32
|
||||
MountID int16
|
||||
MountRedColor int8
|
||||
MountGreenColor int8
|
||||
MountBlueColor int8
|
||||
}
|
||||
|
||||
// LocationTransportDestination represents a location-based transport trigger
|
||||
type LocationTransportDestination struct {
|
||||
ZoneID int32
|
||||
Message string
|
||||
TriggerX float32
|
||||
TriggerY float32
|
||||
TriggerZ float32
|
||||
TriggerRadius float32
|
||||
DestinationZoneID int32
|
||||
DestinationX float32
|
||||
DestinationY float32
|
||||
DestinationZ float32
|
||||
ZoneID int32
|
||||
Message string
|
||||
TriggerX float32
|
||||
TriggerY float32
|
||||
TriggerZ float32
|
||||
TriggerRadius float32
|
||||
DestinationZoneID int32
|
||||
DestinationX float32
|
||||
DestinationY float32
|
||||
DestinationZ float32
|
||||
DestinationHeading float32
|
||||
Cost int32
|
||||
UniqueID int32
|
||||
ForceZone bool
|
||||
Cost int32
|
||||
UniqueID int32
|
||||
ForceZone bool
|
||||
}
|
||||
|
||||
// Location represents a discoverable location
|
||||
@ -509,4 +509,4 @@ type Location struct {
|
||||
}
|
||||
|
||||
// Item placeholder - should import from items package
|
||||
type Item = items.Item
|
||||
type Item = items.Item
|
||||
|
Loading…
x
Reference in New Issue
Block a user