package ground_spawn import ( "context" "fmt" "math/rand" "strings" "sync" "time" "eq2emu/internal/database" ) // GroundSpawn represents a harvestable resource node type GroundSpawn struct { // Database fields ID int32 `json:"id" db:"id"` GroundSpawnID int32 `json:"groundspawn_id" db:"groundspawn_id"` Name string `json:"name" db:"name"` CollectionSkill string `json:"collection_skill" db:"collection_skill"` NumberHarvests int8 `json:"number_harvests" db:"number_harvests"` AttemptsPerHarvest int8 `json:"attempts_per_harvest" db:"attempts_per_harvest"` RandomizeHeading bool `json:"randomize_heading" db:"randomize_heading"` RespawnTime int32 `json:"respawn_time" db:"respawn_time"` // Position data X float32 `json:"x" db:"x"` Y float32 `json:"y" db:"y"` Z float32 `json:"z" db:"z"` Heading float32 `json:"heading" db:"heading"` ZoneID int32 `json:"zone_id" db:"zone_id"` GridID int32 `json:"grid_id" db:"grid_id"` // State data IsAlive bool `json:"is_alive"` CurrentHarvests int8 `json:"current_harvests"` LastHarvested time.Time `json:"last_harvested"` NextRespawn time.Time `json:"next_respawn"` // Associated data (loaded separately) HarvestEntries []*HarvestEntry `json:"harvest_entries,omitempty"` HarvestItems []*HarvestEntryItem `json:"harvest_items,omitempty"` } // HarvestEntry represents harvest table data from database type HarvestEntry struct { GroundSpawnID int32 `json:"groundspawn_id" db:"groundspawn_id"` MinSkillLevel int16 `json:"min_skill_level" db:"min_skill_level"` MinAdventureLevel int16 `json:"min_adventure_level" db:"min_adventure_level"` BonusTable bool `json:"bonus_table" db:"bonus_table"` Harvest1 float32 `json:"harvest1" db:"harvest1"` Harvest3 float32 `json:"harvest3" db:"harvest3"` Harvest5 float32 `json:"harvest5" db:"harvest5"` HarvestImbue float32 `json:"harvest_imbue" db:"harvest_imbue"` HarvestRare float32 `json:"harvest_rare" db:"harvest_rare"` Harvest10 float32 `json:"harvest10" db:"harvest10"` HarvestCoin float32 `json:"harvest_coin" db:"harvest_coin"` } // HarvestEntryItem represents items that can be harvested type HarvestEntryItem struct { GroundSpawnID int32 `json:"groundspawn_id" db:"groundspawn_id"` ItemID int32 `json:"item_id" db:"item_id"` IsRare int8 `json:"is_rare" db:"is_rare"` GridID int32 `json:"grid_id" db:"grid_id"` Quantity int16 `json:"quantity" db:"quantity"` } // HarvestResult represents the outcome of a harvest attempt type HarvestResult struct { Success bool `json:"success"` HarvestType int8 `json:"harvest_type"` ItemsAwarded []*HarvestedItem `json:"items_awarded"` MessageText string `json:"message_text"` SkillGained bool `json:"skill_gained"` Error error `json:"error,omitempty"` } // HarvestedItem represents an item awarded from harvesting type HarvestedItem struct { ItemID int32 `json:"item_id"` Quantity int16 `json:"quantity"` IsRare bool `json:"is_rare"` Name string `json:"name"` } // Player interface for harvest operations type Player interface { GetLevel() int16 GetLocation() int32 GetName() string } // Skill interface for harvest operations type Skill interface { GetCurrentValue() int16 GetMaxValue() int16 } // Logger interface for logging operations type Logger interface { LogInfo(system, format string, args ...interface{}) LogError(system, format string, args ...interface{}) LogDebug(system, format string, args ...interface{}) } // Statistics holds ground spawn system statistics type Statistics struct { TotalHarvests int64 `json:"total_harvests"` SuccessfulHarvests int64 `json:"successful_harvests"` RareItemsHarvested int64 `json:"rare_items_harvested"` SkillUpsGenerated int64 `json:"skill_ups_generated"` HarvestsBySkill map[string]int64 `json:"harvests_by_skill"` ActiveGroundSpawns int `json:"active_ground_spawns"` GroundSpawnsByZone map[int32]int `json:"ground_spawns_by_zone"` } // GetID returns the ground spawn ID func (gs *GroundSpawn) GetID() int32 { return gs.GroundSpawnID } // IsGroundSpawn returns true (compatibility method) func (gs *GroundSpawn) IsGroundSpawn() bool { return true } // IsDepleted returns true if the ground spawn has no harvests remaining func (gs *GroundSpawn) IsDepleted() bool { return gs.CurrentHarvests <= 0 } // IsAvailable returns true if the ground spawn can be harvested func (gs *GroundSpawn) IsAvailable() bool { return gs.CurrentHarvests > 0 && gs.IsAlive } // GetHarvestMessageName returns the appropriate harvest verb based on skill func (gs *GroundSpawn) GetHarvestMessageName(presentTense bool, failure bool) string { skill := strings.ToLower(gs.CollectionSkill) switch skill { case "gathering", "collecting": if presentTense { return "gather" } return "gathered" case "mining": if presentTense { return "mine" } return "mined" case "fishing": if presentTense { return "fish" } return "fished" case "trapping": if failure { return "trap" } if presentTense { return "acquire" } return "acquired" case "foresting": if presentTense { return "forest" } return "forested" default: if presentTense { return "collect" } return "collected" } } // GetHarvestSpellType returns the spell type for harvesting func (gs *GroundSpawn) GetHarvestSpellType() string { skill := strings.ToLower(gs.CollectionSkill) switch skill { case "gathering", "collecting": return SpellTypeGather case "mining": return SpellTypeMine case "trapping": return SpellTypeTrap case "foresting": return SpellTypeChop case "fishing": return SpellTypeFish default: return SpellTypeGather } } // GetHarvestSpellName returns the spell name for harvesting func (gs *GroundSpawn) GetHarvestSpellName() string { skill := gs.CollectionSkill if skill == SkillCollecting { return SkillGathering } return skill } // ProcessHarvest handles the complex harvesting logic (preserves C++ algorithm) func (gs *GroundSpawn) ProcessHarvest(player Player, skill Skill, totalSkill int16) (*HarvestResult, error) { // Check if ground spawn is depleted if gs.CurrentHarvests <= 0 { return &HarvestResult{ Success: false, MessageText: "This spawn has nothing more to harvest!", }, nil } // Validate harvest data if len(gs.HarvestEntries) == 0 { return &HarvestResult{ Success: false, MessageText: fmt.Sprintf("Error: No groundspawn entries assigned to groundspawn id: %d", gs.GroundSpawnID), }, nil } if len(gs.HarvestItems) == 0 { return &HarvestResult{ Success: false, MessageText: fmt.Sprintf("Error: No groundspawn items assigned to groundspawn id: %d", gs.GroundSpawnID), }, nil } // Check for collection skill isCollection := gs.CollectionSkill == "Collecting" result := &HarvestResult{ Success: true, ItemsAwarded: make([]*HarvestedItem, 0), } // Process each harvest attempt (preserving C++ logic) for attempt := int8(0); attempt < gs.AttemptsPerHarvest; attempt++ { attemptResult := gs.processHarvestAttempt(player, skill, totalSkill, isCollection) if attemptResult != nil { result.ItemsAwarded = append(result.ItemsAwarded, attemptResult.ItemsAwarded...) if attemptResult.SkillGained { result.SkillGained = true } } } // Decrement harvest count and update state gs.CurrentHarvests-- gs.LastHarvested = time.Now() return result, nil } // processHarvestAttempt handles a single harvest attempt (preserves C++ algorithm) func (gs *GroundSpawn) processHarvestAttempt(player Player, skill Skill, totalSkill int16, isCollection bool) *HarvestResult { // Filter available harvest tables based on player skill and level availableTables := gs.filterHarvestTables(player, totalSkill) if len(availableTables) == 0 { return &HarvestResult{ Success: false, MessageText: "You lack the skills to harvest this node!", } } // Select harvest table based on skill roll (matching C++ algorithm) selectedTable := gs.selectHarvestTable(availableTables, totalSkill) if selectedTable == nil { return &HarvestResult{ Success: false, MessageText: "Failed to determine harvest table", } } // Determine harvest type based on table percentages harvestType := gs.determineHarvestType(selectedTable, isCollection) if harvestType == HarvestTypeNone { return &HarvestResult{ Success: false, MessageText: fmt.Sprintf("You failed to %s anything from %s.", gs.GetHarvestMessageName(true, true), gs.Name), } } // Award items based on harvest type items := gs.awardHarvestItems(harvestType, player) // Handle skill progression (simplified for now) skillGained := false // TODO: Implement skill progression return &HarvestResult{ Success: len(items) > 0, HarvestType: harvestType, ItemsAwarded: items, SkillGained: skillGained, } } // filterHarvestTables filters tables based on player capabilities (preserves C++ logic) func (gs *GroundSpawn) filterHarvestTables(player Player, totalSkill int16) []*HarvestEntry { var filtered []*HarvestEntry for _, entry := range gs.HarvestEntries { // Check skill requirement if entry.MinSkillLevel > totalSkill { continue } // Check level requirement for bonus tables if entry.BonusTable && player.GetLevel() < entry.MinAdventureLevel { continue } filtered = append(filtered, entry) } return filtered } // selectHarvestTable selects a harvest table based on skill level (preserves C++ algorithm) func (gs *GroundSpawn) selectHarvestTable(tables []*HarvestEntry, totalSkill int16) *HarvestEntry { if len(tables) == 0 { return nil } // Find lowest skill requirement (matching C++ logic) lowestSkill := int16(32767) for _, table := range tables { if table.MinSkillLevel < lowestSkill { lowestSkill = table.MinSkillLevel } } // Roll for table selection (matching C++ MakeRandomInt) tableChoice := int16(rand.Intn(int(totalSkill-lowestSkill+1))) + lowestSkill // Find best matching table (matching C++ logic) var bestTable *HarvestEntry bestScore := int16(0) for _, table := range tables { if tableChoice >= table.MinSkillLevel && table.MinSkillLevel > bestScore { bestTable = table bestScore = table.MinSkillLevel } } // If multiple tables match, pick randomly (matching C++ logic) var matches []*HarvestEntry for _, table := range tables { if table.MinSkillLevel == bestScore { matches = append(matches, table) } } if len(matches) > 1 { return matches[rand.Intn(len(matches))] } return bestTable } // determineHarvestType determines what type of harvest occurs (preserves C++ algorithm) func (gs *GroundSpawn) determineHarvestType(table *HarvestEntry, isCollection bool) int8 { chance := rand.Float32() * 100.0 // Collection items always get 1 item (matching C++ logic) if isCollection { return HarvestType1Item } // Check harvest types in order of rarity (matching C++ order) if chance <= table.Harvest10 { return HarvestType10AndRare } if chance <= table.HarvestRare { return HarvestTypeRare } if chance <= table.HarvestImbue { return HarvestTypeImbue } if chance <= table.Harvest5 { return HarvestType5Items } if chance <= table.Harvest3 { return HarvestType3Items } if chance <= table.Harvest1 { return HarvestType1Item } return HarvestTypeNone } // awardHarvestItems awards items based on harvest type (preserves C++ algorithm) func (gs *GroundSpawn) awardHarvestItems(harvestType int8, player Player) []*HarvestedItem { var items []*HarvestedItem // Filter items based on harvest type and player location (matching C++ logic) normalItems := gs.filterItems(gs.HarvestItems, ItemRarityNormal, player.GetLocation()) rareItems := gs.filterItems(gs.HarvestItems, ItemRarityRare, player.GetLocation()) imbueItems := gs.filterItems(gs.HarvestItems, ItemRarityImbue, player.GetLocation()) switch harvestType { case HarvestType1Item: items = gs.selectRandomItems(normalItems, 1) case HarvestType3Items: items = gs.selectRandomItems(normalItems, 3) case HarvestType5Items: items = gs.selectRandomItems(normalItems, 5) case HarvestTypeImbue: items = gs.selectRandomItems(imbueItems, 1) case HarvestTypeRare: items = gs.selectRandomItems(rareItems, 1) case HarvestType10AndRare: normal := gs.selectRandomItems(normalItems, 10) rare := gs.selectRandomItems(rareItems, 1) items = append(normal, rare...) } return items } // filterItems filters items by rarity and grid restriction (preserves C++ logic) func (gs *GroundSpawn) filterItems(items []*HarvestEntryItem, rarity int8, playerGrid int32) []*HarvestEntryItem { var filtered []*HarvestEntryItem for _, item := range items { if item.IsRare != rarity { continue } // Check grid restriction (matching C++ logic) if item.GridID != 0 && item.GridID != playerGrid { continue } filtered = append(filtered, item) } return filtered } // selectRandomItems randomly selects items from available list (preserves C++ logic) func (gs *GroundSpawn) selectRandomItems(items []*HarvestEntryItem, quantity int16) []*HarvestedItem { if len(items) == 0 { return nil } var result []*HarvestedItem for i := int16(0); i < quantity; i++ { selectedItem := items[rand.Intn(len(items))] harvestedItem := &HarvestedItem{ ItemID: selectedItem.ItemID, Quantity: selectedItem.Quantity, IsRare: selectedItem.IsRare == ItemRarityRare, Name: fmt.Sprintf("Item_%d", selectedItem.ItemID), // Placeholder for item name lookup } result = append(result, harvestedItem) } return result } // Respawn resets the ground spawn to harvestable state func (gs *GroundSpawn) Respawn() { // Reset harvest count to default gs.CurrentHarvests = gs.NumberHarvests // Randomize heading if configured if gs.RandomizeHeading { gs.Heading = rand.Float32() * 360.0 } // Mark as alive and update times gs.IsAlive = true gs.NextRespawn = time.Time{} // Clear next respawn time } // GroundSpawnManager provides unified management of the ground spawn system type GroundSpawnManager struct { // Core storage with specialized indices for O(1) lookups spawns map[int32]*GroundSpawn // ID -> GroundSpawn byZone map[int32][]*GroundSpawn // Zone ID -> spawns bySkill map[string][]*GroundSpawn // Skill -> spawns // External dependencies database *database.Database logger Logger mutex sync.RWMutex // Statistics totalHarvests int64 successfulHarvests int64 rareItemsHarvested int64 skillUpsGenerated int64 harvestsBySkill map[string]int64 } // NewGroundSpawnManager creates a new unified ground spawn manager func NewGroundSpawnManager(db *database.Database, logger Logger) *GroundSpawnManager { return &GroundSpawnManager{ spawns: make(map[int32]*GroundSpawn), byZone: make(map[int32][]*GroundSpawn), bySkill: make(map[string][]*GroundSpawn), database: db, logger: logger, harvestsBySkill: make(map[string]int64), } } // Initialize loads ground spawns from database func (gsm *GroundSpawnManager) Initialize(ctx context.Context) error { if gsm.logger != nil { gsm.logger.LogInfo("ground_spawn", "Initializing ground spawn manager...") } if gsm.database == nil { if gsm.logger != nil { gsm.logger.LogInfo("ground_spawn", "No database provided, starting with empty spawn list") } return nil } // Load all ground spawns if err := gsm.loadGroundSpawnsFromDB(); err != nil { return fmt.Errorf("failed to load ground spawns from database: %w", err) } if gsm.logger != nil { gsm.logger.LogInfo("ground_spawn", "Loaded %d ground spawns from database", len(gsm.spawns)) } return nil } // loadGroundSpawnsFromDB loads all ground spawns from database (internal method) func (gsm *GroundSpawnManager) loadGroundSpawnsFromDB() error { // Create ground_spawns table if it doesn't exist _, err := gsm.database.Exec(` CREATE TABLE IF NOT EXISTS ground_spawns ( id INTEGER PRIMARY KEY, groundspawn_id INTEGER NOT NULL, name TEXT NOT NULL, collection_skill TEXT, number_harvests INTEGER DEFAULT 1, attempts_per_harvest INTEGER DEFAULT 1, randomize_heading BOOLEAN DEFAULT 1, respawn_time INTEGER DEFAULT 300, x REAL NOT NULL, y REAL NOT NULL, z REAL NOT NULL, heading REAL DEFAULT 0, zone_id INTEGER NOT NULL, grid_id INTEGER DEFAULT 0 ) `) if err != nil { return fmt.Errorf("failed to create ground_spawns table: %w", err) } rows, err := gsm.database.Query(` SELECT id, groundspawn_id, name, collection_skill, number_harvests, attempts_per_harvest, randomize_heading, respawn_time, x, y, z, heading, zone_id, grid_id FROM ground_spawns ORDER BY id `) if err != nil { return fmt.Errorf("failed to query ground spawns: %w", err) } defer rows.Close() count := 0 for rows.Next() { gs := &GroundSpawn{ HarvestEntries: make([]*HarvestEntry, 0), HarvestItems: make([]*HarvestEntryItem, 0), IsAlive: true, } err := rows.Scan(&gs.ID, &gs.GroundSpawnID, &gs.Name, &gs.CollectionSkill, &gs.NumberHarvests, &gs.AttemptsPerHarvest, &gs.RandomizeHeading, &gs.RespawnTime, &gs.X, &gs.Y, &gs.Z, &gs.Heading, &gs.ZoneID, &gs.GridID) if err != nil { return fmt.Errorf("failed to scan ground spawn: %w", err) } // Initialize state gs.CurrentHarvests = gs.NumberHarvests // Load harvest entries and items if err := gsm.loadHarvestData(gs); err != nil { if gsm.logger != nil { gsm.logger.LogError("ground_spawn", "Failed to load harvest data for spawn %d: %v", gs.GroundSpawnID, err) } } if err := gsm.addGroundSpawnToIndices(gs); err != nil { if gsm.logger != nil { gsm.logger.LogError("ground_spawn", "Failed to add ground spawn %d: %v", gs.GroundSpawnID, err) } continue } count++ } return rows.Err() } // loadHarvestData loads harvest entries and items for a ground spawn (internal method) func (gsm *GroundSpawnManager) loadHarvestData(gs *GroundSpawn) error { // Load harvest entries if err := gsm.loadHarvestEntries(gs); err != nil { return err } // Load harvest items if err := gsm.loadHarvestItems(gs); err != nil { return err } return nil } // loadHarvestEntries loads harvest entries for a ground spawn (internal method) func (gsm *GroundSpawnManager) loadHarvestEntries(gs *GroundSpawn) error { rows, err := gsm.database.Query(` SELECT groundspawn_id, min_skill_level, min_adventure_level, bonus_table, harvest1, harvest3, harvest5, harvest_imbue, harvest_rare, harvest10, harvest_coin FROM groundspawn_entries WHERE groundspawn_id = ? `, gs.GroundSpawnID) if err != nil { return err } defer rows.Close() for rows.Next() { entry := &HarvestEntry{} err := rows.Scan(&entry.GroundSpawnID, &entry.MinSkillLevel, &entry.MinAdventureLevel, &entry.BonusTable, &entry.Harvest1, &entry.Harvest3, &entry.Harvest5, &entry.HarvestImbue, &entry.HarvestRare, &entry.Harvest10, &entry.HarvestCoin) if err != nil { return err } gs.HarvestEntries = append(gs.HarvestEntries, entry) } return rows.Err() } // loadHarvestItems loads harvest items for a ground spawn (internal method) func (gsm *GroundSpawnManager) loadHarvestItems(gs *GroundSpawn) error { rows, err := gsm.database.Query(` SELECT groundspawn_id, item_id, is_rare, grid_id, quantity FROM groundspawn_items WHERE groundspawn_id = ? `, gs.GroundSpawnID) if err != nil { return err } defer rows.Close() for rows.Next() { item := &HarvestEntryItem{} err := rows.Scan(&item.GroundSpawnID, &item.ItemID, &item.IsRare, &item.GridID, &item.Quantity) if err != nil { return err } gs.HarvestItems = append(gs.HarvestItems, item) } return rows.Err() } // addGroundSpawnToIndices adds a ground spawn to all internal indices (internal method) func (gsm *GroundSpawnManager) addGroundSpawnToIndices(gs *GroundSpawn) error { if gs == nil { return fmt.Errorf("ground spawn cannot be nil") } // Check if exists if _, exists := gsm.spawns[gs.GroundSpawnID]; exists { return fmt.Errorf("ground spawn with ID %d already exists", gs.GroundSpawnID) } // Add to core storage gsm.spawns[gs.GroundSpawnID] = gs // Add to zone index gsm.byZone[gs.ZoneID] = append(gsm.byZone[gs.ZoneID], gs) // Add to skill index gsm.bySkill[gs.CollectionSkill] = append(gsm.bySkill[gs.CollectionSkill], gs) return nil } // GetGroundSpawn returns a ground spawn by ID func (gsm *GroundSpawnManager) GetGroundSpawn(groundSpawnID int32) *GroundSpawn { gsm.mutex.RLock() defer gsm.mutex.RUnlock() return gsm.spawns[groundSpawnID] } // GetGroundSpawnsByZone returns all ground spawns in a zone (O(1)) func (gsm *GroundSpawnManager) GetGroundSpawnsByZone(zoneID int32) []*GroundSpawn { gsm.mutex.RLock() defer gsm.mutex.RUnlock() spawns, exists := gsm.byZone[zoneID] if !exists { return nil } // Return a copy to prevent external modification result := make([]*GroundSpawn, len(spawns)) copy(result, spawns) return result } // GetGroundSpawnsBySkill returns all ground spawns for a skill (O(1)) func (gsm *GroundSpawnManager) GetGroundSpawnsBySkill(skill string) []*GroundSpawn { gsm.mutex.RLock() defer gsm.mutex.RUnlock() spawns, exists := gsm.bySkill[skill] if !exists { return nil } // Return a copy to prevent external modification result := make([]*GroundSpawn, len(spawns)) copy(result, spawns) return result } // GetGroundSpawnsByZoneAndSkill returns spawns matching both zone and skill func (gsm *GroundSpawnManager) GetGroundSpawnsByZoneAndSkill(zoneID int32, skill string) []*GroundSpawn { gsm.mutex.RLock() defer gsm.mutex.RUnlock() zoneSpawns := gsm.byZone[zoneID] skillSpawns := gsm.bySkill[skill] // Use smaller set for iteration efficiency if len(zoneSpawns) > len(skillSpawns) { zoneSpawns, skillSpawns = skillSpawns, zoneSpawns } // Set intersection using map lookup skillSet := make(map[*GroundSpawn]struct{}, len(skillSpawns)) for _, gs := range skillSpawns { skillSet[gs] = struct{}{} } var result []*GroundSpawn for _, gs := range zoneSpawns { if _, exists := skillSet[gs]; exists { result = append(result, gs) } } return result } // GetAvailableGroundSpawns returns all harvestable ground spawns func (gsm *GroundSpawnManager) GetAvailableGroundSpawns() []*GroundSpawn { gsm.mutex.RLock() defer gsm.mutex.RUnlock() var available []*GroundSpawn for _, gs := range gsm.spawns { if gs.IsAvailable() { available = append(available, gs) } } return available } // GetAvailableGroundSpawnsByZone returns harvestable ground spawns in a zone func (gsm *GroundSpawnManager) GetAvailableGroundSpawnsByZone(zoneID int32) []*GroundSpawn { gsm.mutex.RLock() defer gsm.mutex.RUnlock() zoneSpawns := gsm.byZone[zoneID] var available []*GroundSpawn for _, gs := range zoneSpawns { if gs.IsAvailable() { available = append(available, gs) } } return available } // ProcessHarvest processes a harvest attempt and updates statistics func (gsm *GroundSpawnManager) ProcessHarvest(groundSpawnID int32, player Player, skill Skill, totalSkill int16) (*HarvestResult, error) { gsm.mutex.Lock() defer gsm.mutex.Unlock() gs := gsm.spawns[groundSpawnID] if gs == nil { return nil, fmt.Errorf("ground spawn %d not found", groundSpawnID) } // Process harvest result, err := gs.ProcessHarvest(player, skill, totalSkill) if err != nil { return nil, err } // Update statistics gsm.totalHarvests++ if result.Success { gsm.successfulHarvests++ gsm.harvestsBySkill[gs.CollectionSkill]++ // Count rare items for _, item := range result.ItemsAwarded { if item.IsRare { gsm.rareItemsHarvested++ } } if result.SkillGained { gsm.skillUpsGenerated++ } } // Log harvest if logger available if gsm.logger != nil { if result.Success { gsm.logger.LogInfo("ground_spawn", "Player %s harvested %d items from spawn %d", player.GetName(), len(result.ItemsAwarded), groundSpawnID) } else { gsm.logger.LogDebug("ground_spawn", "Player %s failed to harvest spawn %d: %s", player.GetName(), groundSpawnID, result.MessageText) } } return result, nil } // RespawnGroundSpawn respawns a specific ground spawn func (gsm *GroundSpawnManager) RespawnGroundSpawn(groundSpawnID int32) bool { gsm.mutex.Lock() defer gsm.mutex.Unlock() gs := gsm.spawns[groundSpawnID] if gs == nil { return false } gs.Respawn() if gsm.logger != nil { gsm.logger.LogDebug("ground_spawn", "Respawned ground spawn %d", groundSpawnID) } return true } // GetStatistics returns ground spawn system statistics func (gsm *GroundSpawnManager) GetStatistics() *Statistics { gsm.mutex.RLock() defer gsm.mutex.RUnlock() // Count active spawns and spawns by zone activeSpawns := 0 spawnsByZone := make(map[int32]int) for _, gs := range gsm.spawns { if gs.IsAvailable() { activeSpawns++ } spawnsByZone[gs.ZoneID]++ } // Copy harvests by skill to prevent external modification harvestsBySkill := make(map[string]int64) for skill, count := range gsm.harvestsBySkill { harvestsBySkill[skill] = count } return &Statistics{ TotalHarvests: gsm.totalHarvests, SuccessfulHarvests: gsm.successfulHarvests, RareItemsHarvested: gsm.rareItemsHarvested, SkillUpsGenerated: gsm.skillUpsGenerated, HarvestsBySkill: harvestsBySkill, ActiveGroundSpawns: activeSpawns, GroundSpawnsByZone: spawnsByZone, } } // GetGroundSpawnCount returns the total number of ground spawns func (gsm *GroundSpawnManager) GetGroundSpawnCount() int32 { gsm.mutex.RLock() defer gsm.mutex.RUnlock() return int32(len(gsm.spawns)) } // AddGroundSpawn adds a new ground spawn with database persistence func (gsm *GroundSpawnManager) AddGroundSpawn(gs *GroundSpawn) error { if gs == nil { return fmt.Errorf("ground spawn cannot be nil") } gsm.mutex.Lock() defer gsm.mutex.Unlock() // Add to indices if err := gsm.addGroundSpawnToIndices(gs); err != nil { return fmt.Errorf("failed to add ground spawn to indices: %w", err) } // Save to database if available if gsm.database != nil { if err := gsm.saveGroundSpawnToDBInternal(gs); err != nil { // Remove from indices if save failed gsm.removeGroundSpawnFromIndicesInternal(gs.GroundSpawnID) return fmt.Errorf("failed to save ground spawn to database: %w", err) } } if gsm.logger != nil { gsm.logger.LogInfo("ground_spawn", "Added ground spawn %d: %s in zone %d", gs.GroundSpawnID, gs.Name, gs.ZoneID) } return nil } // saveGroundSpawnToDBInternal saves a ground spawn to database (internal method, assumes lock held) func (gsm *GroundSpawnManager) saveGroundSpawnToDBInternal(gs *GroundSpawn) error { query := `INSERT OR REPLACE INTO ground_spawns (groundspawn_id, name, collection_skill, number_harvests, attempts_per_harvest, randomize_heading, respawn_time, x, y, z, heading, zone_id, grid_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)` _, err := gsm.database.Exec(query, gs.GroundSpawnID, gs.Name, gs.CollectionSkill, gs.NumberHarvests, gs.AttemptsPerHarvest, gs.RandomizeHeading, gs.RespawnTime, gs.X, gs.Y, gs.Z, gs.Heading, gs.ZoneID, gs.GridID) return err } // removeGroundSpawnFromIndicesInternal removes ground spawn from all indices (internal method, assumes lock held) func (gsm *GroundSpawnManager) removeGroundSpawnFromIndicesInternal(groundSpawnID int32) { gs, exists := gsm.spawns[groundSpawnID] if !exists { return } // Remove from core storage delete(gsm.spawns, groundSpawnID) // Remove from zone index zoneSpawns := gsm.byZone[gs.ZoneID] for i, spawn := range zoneSpawns { if spawn.GroundSpawnID == groundSpawnID { gsm.byZone[gs.ZoneID] = append(zoneSpawns[:i], zoneSpawns[i+1:]...) break } } // Remove from skill index skillSpawns := gsm.bySkill[gs.CollectionSkill] for i, spawn := range skillSpawns { if spawn.GroundSpawnID == groundSpawnID { gsm.bySkill[gs.CollectionSkill] = append(skillSpawns[:i], skillSpawns[i+1:]...) break } } } // Shutdown gracefully shuts down the manager func (gsm *GroundSpawnManager) Shutdown() { if gsm.logger != nil { gsm.logger.LogInfo("ground_spawn", "Shutting down ground spawn manager...") } gsm.mutex.Lock() defer gsm.mutex.Unlock() // Clear all data gsm.spawns = make(map[int32]*GroundSpawn) gsm.byZone = make(map[int32][]*GroundSpawn) gsm.bySkill = make(map[string][]*GroundSpawn) gsm.harvestsBySkill = make(map[string]int64) }