2025-08-08 14:31:44 -05:00

720 lines
19 KiB
Go

package heroic_ops
import (
"fmt"
"sort"
"sync"
"strings"
"eq2emu/internal/database"
)
// MasterList provides optimized heroic opportunity management with O(1) performance characteristics
type MasterList struct {
mu sync.RWMutex
// Core data storage - O(1) access by ID
starters map[int32]*HeroicOPStarter // starter_id -> starter
wheels map[int32]*HeroicOPWheel // wheel_id -> wheel
// Specialized indices for O(1) lookups
byClass map[int8]map[int32]*HeroicOPStarter // class -> starter_id -> starter
byStarterID map[int32][]*HeroicOPWheel // starter_id -> wheels
bySpellID map[int32][]*HeroicOPWheel // spell_id -> wheels
byChance map[string][]*HeroicOPWheel // chance_range -> wheels
orderedWheels map[int32]*HeroicOPWheel // wheel_id -> ordered wheels only
shiftWheels map[int32]*HeroicOPWheel // wheel_id -> wheels with shifts
spellInfo map[int32]SpellInfo // spell_id -> spell info
// Lazy metadata caching - computed on demand
totalStarters int
totalWheels int
classDistribution map[int8]int
metadataValid bool
loaded bool
}
// NewMasterList creates a new bespoke heroic opportunity master list
func NewMasterList() *MasterList {
return &MasterList{
starters: make(map[int32]*HeroicOPStarter),
wheels: make(map[int32]*HeroicOPWheel),
byClass: make(map[int8]map[int32]*HeroicOPStarter),
byStarterID: make(map[int32][]*HeroicOPWheel),
bySpellID: make(map[int32][]*HeroicOPWheel),
byChance: make(map[string][]*HeroicOPWheel),
orderedWheels: make(map[int32]*HeroicOPWheel),
shiftWheels: make(map[int32]*HeroicOPWheel),
spellInfo: make(map[int32]SpellInfo),
loaded: false,
}
}
// LoadFromDatabase loads all heroic opportunities from the database with optimal indexing
func (ml *MasterList) LoadFromDatabase(db *database.Database) error {
ml.mu.Lock()
defer ml.mu.Unlock()
// Clear existing data
ml.clearIndices()
// Load all starters
if err := ml.loadStarters(db); err != nil {
return fmt.Errorf("failed to load starters: %w", err)
}
// Load all wheels
if err := ml.loadWheels(db); err != nil {
return fmt.Errorf("failed to load wheels: %w", err)
}
// Build specialized indices for O(1) performance
ml.buildIndices()
ml.loaded = true
return nil
}
// loadStarters loads all starters from database
func (ml *MasterList) loadStarters(db *database.Database) error {
query := `SELECT id, start_class, starter_icon, ability1, ability2, ability3, ability4, ability5, ability6, name, description
FROM heroic_op_starters ORDER BY id`
rows, err := db.Query(query)
if err != nil {
return err
}
defer rows.Close()
for rows.Next() {
starter := &HeroicOPStarter{
db: db,
isNew: false,
}
err := rows.Scan(
&starter.ID,
&starter.StartClass,
&starter.StarterIcon,
&starter.Abilities[0],
&starter.Abilities[1],
&starter.Abilities[2],
&starter.Abilities[3],
&starter.Abilities[4],
&starter.Abilities[5],
&starter.Name,
&starter.Description,
)
if err != nil {
return err
}
starter.SaveNeeded = false
ml.starters[starter.ID] = starter
}
return rows.Err()
}
// loadWheels loads all wheels from database
func (ml *MasterList) loadWheels(db *database.Database) error {
query := `SELECT id, starter_link_id, chain_order, shift_icon, chance, ability1, ability2, ability3, ability4, ability5, ability6,
spell_id, name, description, required_players
FROM heroic_op_wheels ORDER BY id`
rows, err := db.Query(query)
if err != nil {
return err
}
defer rows.Close()
for rows.Next() {
wheel := &HeroicOPWheel{
db: db,
isNew: false,
}
err := rows.Scan(
&wheel.ID,
&wheel.StarterLinkID,
&wheel.Order,
&wheel.ShiftIcon,
&wheel.Chance,
&wheel.Abilities[0],
&wheel.Abilities[1],
&wheel.Abilities[2],
&wheel.Abilities[3],
&wheel.Abilities[4],
&wheel.Abilities[5],
&wheel.SpellID,
&wheel.Name,
&wheel.Description,
&wheel.RequiredPlayers,
)
if err != nil {
return err
}
wheel.SaveNeeded = false
ml.wheels[wheel.ID] = wheel
// Store spell info
ml.spellInfo[wheel.SpellID] = SpellInfo{
ID: wheel.SpellID,
Name: wheel.Name,
Description: wheel.Description,
}
}
return rows.Err()
}
// buildIndices creates specialized indices for O(1) performance
func (ml *MasterList) buildIndices() {
// Build class-based starter index
for _, starter := range ml.starters {
if ml.byClass[starter.StartClass] == nil {
ml.byClass[starter.StartClass] = make(map[int32]*HeroicOPStarter)
}
ml.byClass[starter.StartClass][starter.ID] = starter
}
// Build wheel indices
for _, wheel := range ml.wheels {
// By starter ID
ml.byStarterID[wheel.StarterLinkID] = append(ml.byStarterID[wheel.StarterLinkID], wheel)
// By spell ID
ml.bySpellID[wheel.SpellID] = append(ml.bySpellID[wheel.SpellID], wheel)
// By chance range (for performance optimization)
chanceRange := ml.getChanceRange(wheel.Chance)
ml.byChance[chanceRange] = append(ml.byChance[chanceRange], wheel)
// Special wheel types
if wheel.IsOrdered() {
ml.orderedWheels[wheel.ID] = wheel
}
if wheel.HasShift() {
ml.shiftWheels[wheel.ID] = wheel
}
}
// Sort wheels by chance for deterministic selection
for _, wheels := range ml.byStarterID {
sort.Slice(wheels, func(i, j int) bool {
return wheels[i].Chance > wheels[j].Chance
})
}
ml.metadataValid = false // Invalidate cached metadata
}
// clearIndices clears all indices
func (ml *MasterList) clearIndices() {
ml.starters = make(map[int32]*HeroicOPStarter)
ml.wheels = make(map[int32]*HeroicOPWheel)
ml.byClass = make(map[int8]map[int32]*HeroicOPStarter)
ml.byStarterID = make(map[int32][]*HeroicOPWheel)
ml.bySpellID = make(map[int32][]*HeroicOPWheel)
ml.byChance = make(map[string][]*HeroicOPWheel)
ml.orderedWheels = make(map[int32]*HeroicOPWheel)
ml.shiftWheels = make(map[int32]*HeroicOPWheel)
ml.spellInfo = make(map[int32]SpellInfo)
ml.metadataValid = false
}
// getChanceRange returns a chance range string for indexing
func (ml *MasterList) getChanceRange(chance float32) string {
switch {
case chance >= 75.0:
return "very_high"
case chance >= 50.0:
return "high"
case chance >= 25.0:
return "medium"
case chance >= 10.0:
return "low"
default:
return "very_low"
}
}
// O(1) Starter Operations
// GetStarter retrieves a starter by ID with O(1) performance
func (ml *MasterList) GetStarter(id int32) *HeroicOPStarter {
ml.mu.RLock()
defer ml.mu.RUnlock()
return ml.starters[id]
}
// GetStartersForClass returns all starters for a specific class with O(1) performance
func (ml *MasterList) GetStartersForClass(class int8) []*HeroicOPStarter {
ml.mu.RLock()
defer ml.mu.RUnlock()
var result []*HeroicOPStarter
// Add class-specific starters
if classStarters, exists := ml.byClass[class]; exists {
for _, starter := range classStarters {
result = append(result, starter)
}
}
// Add universal starters (class 0 = any)
if universalStarters, exists := ml.byClass[ClassAny]; exists {
for _, starter := range universalStarters {
result = append(result, starter)
}
}
return result
}
// O(1) Wheel Operations
// GetWheel retrieves a wheel by ID with O(1) performance
func (ml *MasterList) GetWheel(id int32) *HeroicOPWheel {
ml.mu.RLock()
defer ml.mu.RUnlock()
return ml.wheels[id]
}
// GetWheelsForStarter returns all wheels for a starter with O(1) performance
func (ml *MasterList) GetWheelsForStarter(starterID int32) []*HeroicOPWheel {
ml.mu.RLock()
defer ml.mu.RUnlock()
wheels := ml.byStarterID[starterID]
if wheels == nil {
return nil
}
// Return copy to prevent external modification
result := make([]*HeroicOPWheel, len(wheels))
copy(result, wheels)
return result
}
// GetWheelsForSpell returns all wheels that cast a specific spell with O(1) performance
func (ml *MasterList) GetWheelsForSpell(spellID int32) []*HeroicOPWheel {
ml.mu.RLock()
defer ml.mu.RUnlock()
wheels := ml.bySpellID[spellID]
if wheels == nil {
return nil
}
// Return copy to prevent external modification
result := make([]*HeroicOPWheel, len(wheels))
copy(result, wheels)
return result
}
// SelectRandomWheel selects a random wheel from starter's wheels with optimized performance
func (ml *MasterList) SelectRandomWheel(starterID int32) *HeroicOPWheel {
wheels := ml.GetWheelsForStarter(starterID)
if len(wheels) == 0 {
return nil
}
return SelectRandomWheel(wheels)
}
// O(1) Specialized Queries
// GetOrderedWheels returns all ordered wheels with O(1) performance
func (ml *MasterList) GetOrderedWheels() []*HeroicOPWheel {
ml.mu.RLock()
defer ml.mu.RUnlock()
result := make([]*HeroicOPWheel, 0, len(ml.orderedWheels))
for _, wheel := range ml.orderedWheels {
result = append(result, wheel)
}
return result
}
// GetShiftWheels returns all wheels with shift abilities with O(1) performance
func (ml *MasterList) GetShiftWheels() []*HeroicOPWheel {
ml.mu.RLock()
defer ml.mu.RUnlock()
result := make([]*HeroicOPWheel, 0, len(ml.shiftWheels))
for _, wheel := range ml.shiftWheels {
result = append(result, wheel)
}
return result
}
// GetWheelsByChanceRange returns wheels within a chance range with O(1) performance
func (ml *MasterList) GetWheelsByChanceRange(minChance, maxChance float32) []*HeroicOPWheel {
ml.mu.RLock()
defer ml.mu.RUnlock()
var result []*HeroicOPWheel
// Use indexed chance ranges for performance
for rangeKey, wheels := range ml.byChance {
for _, wheel := range wheels {
if wheel.Chance >= minChance && wheel.Chance <= maxChance {
result = append(result, wheel)
}
}
// Break early if we found wheels in this range
if len(result) > 0 && !ml.shouldContinueChanceSearch(rangeKey, minChance, maxChance) {
break
}
}
return result
}
// shouldContinueChanceSearch determines if we should continue searching other chance ranges
func (ml *MasterList) shouldContinueChanceSearch(currentRange string, minChance, maxChance float32) bool {
// Optimize search by stopping early based on range analysis
switch currentRange {
case "very_high":
return maxChance < 75.0
case "high":
return maxChance < 50.0 || minChance > 75.0
case "medium":
return maxChance < 25.0 || minChance > 50.0
case "low":
return maxChance < 10.0 || minChance > 25.0
default: // very_low
return minChance > 10.0
}
}
// Spell Information
// GetSpellInfo returns spell information with O(1) performance
func (ml *MasterList) GetSpellInfo(spellID int32) (*SpellInfo, bool) {
ml.mu.RLock()
defer ml.mu.RUnlock()
info, exists := ml.spellInfo[spellID]
return &info, exists
}
// Advanced Search with Optimized Performance
// Search performs advanced search with multiple criteria
func (ml *MasterList) Search(criteria HeroicOPSearchCriteria) *HeroicOPSearchResults {
ml.mu.RLock()
defer ml.mu.RUnlock()
results := &HeroicOPSearchResults{
Starters: make([]*HeroicOPStarter, 0),
Wheels: make([]*HeroicOPWheel, 0),
}
// Optimize search strategy based on criteria
if criteria.StarterClass != 0 {
// Class-specific search - use class index
if classStarters, exists := ml.byClass[criteria.StarterClass]; exists {
for _, starter := range classStarters {
if ml.matchesStarterCriteria(starter, criteria) {
results.Starters = append(results.Starters, starter)
}
}
}
} else {
// Search all starters
for _, starter := range ml.starters {
if ml.matchesStarterCriteria(starter, criteria) {
results.Starters = append(results.Starters, starter)
}
}
}
// Wheel search optimization
if criteria.SpellID != 0 {
// Spell-specific search - use spell index
if spellWheels, exists := ml.bySpellID[criteria.SpellID]; exists {
for _, wheel := range spellWheels {
if ml.matchesWheelCriteria(wheel, criteria) {
results.Wheels = append(results.Wheels, wheel)
}
}
}
} else if criteria.MinChance > 0 || criteria.MaxChance > 0 {
// Chance-based search - use chance ranges
minChance := criteria.MinChance
maxChance := criteria.MaxChance
if maxChance == 0 {
maxChance = MaxChance
}
results.Wheels = ml.GetWheelsByChanceRange(minChance, maxChance)
} else {
// Search all wheels
for _, wheel := range ml.wheels {
if ml.matchesWheelCriteria(wheel, criteria) {
results.Wheels = append(results.Wheels, wheel)
}
}
}
return results
}
// matchesStarterCriteria checks if starter matches search criteria
func (ml *MasterList) matchesStarterCriteria(starter *HeroicOPStarter, criteria HeroicOPSearchCriteria) bool {
if criteria.StarterClass != 0 && starter.StartClass != criteria.StarterClass {
return false
}
if criteria.NamePattern != "" {
if !strings.Contains(strings.ToLower(starter.Name), strings.ToLower(criteria.NamePattern)) {
return false
}
}
return true
}
// matchesWheelCriteria checks if wheel matches search criteria
func (ml *MasterList) matchesWheelCriteria(wheel *HeroicOPWheel, criteria HeroicOPSearchCriteria) bool {
if criteria.SpellID != 0 && wheel.SpellID != criteria.SpellID {
return false
}
if criteria.MinChance > 0 && wheel.Chance < criteria.MinChance {
return false
}
if criteria.MaxChance > 0 && wheel.Chance > criteria.MaxChance {
return false
}
if criteria.RequiredPlayers > 0 && wheel.RequiredPlayers != criteria.RequiredPlayers {
return false
}
if criteria.NamePattern != "" {
if !strings.Contains(strings.ToLower(wheel.Name), strings.ToLower(criteria.NamePattern)) {
return false
}
}
if criteria.HasShift && !wheel.HasShift() {
return false
}
if criteria.IsOrdered && !wheel.IsOrdered() {
return false
}
return true
}
// Statistics with Lazy Caching
// GetStatistics returns comprehensive statistics with lazy caching for optimal performance
func (ml *MasterList) GetStatistics() *HeroicOPStatistics {
ml.mu.Lock()
defer ml.mu.Unlock()
ml.ensureMetadataValid()
return &HeroicOPStatistics{
TotalStarters: int64(ml.totalStarters),
TotalWheels: int64(ml.totalWheels),
ClassDistribution: ml.copyClassDistribution(),
OrderedWheelsCount: int64(len(ml.orderedWheels)),
ShiftWheelsCount: int64(len(ml.shiftWheels)),
SpellCount: int64(len(ml.spellInfo)),
AverageChance: ml.calculateAverageChance(),
}
}
// ensureMetadataValid ensures cached metadata is current
func (ml *MasterList) ensureMetadataValid() {
if ml.metadataValid {
return
}
// Recompute cached metadata
ml.totalStarters = len(ml.starters)
ml.totalWheels = len(ml.wheels)
ml.classDistribution = make(map[int8]int)
for _, starter := range ml.starters {
ml.classDistribution[starter.StartClass]++
}
ml.metadataValid = true
}
// copyClassDistribution creates a copy of class distribution for thread safety
func (ml *MasterList) copyClassDistribution() map[int32]int64 {
result := make(map[int32]int64)
for class, count := range ml.classDistribution {
result[int32(class)] = int64(count)
}
return result
}
// calculateAverageChance computes average wheel chance
func (ml *MasterList) calculateAverageChance() float64 {
if len(ml.wheels) == 0 {
return 0.0
}
total := float64(0)
for _, wheel := range ml.wheels {
total += float64(wheel.Chance)
}
return total / float64(len(ml.wheels))
}
// Modification Operations
// AddStarter adds a starter to the master list with index updates
func (ml *MasterList) AddStarter(starter *HeroicOPStarter) error {
ml.mu.Lock()
defer ml.mu.Unlock()
if err := starter.Validate(); err != nil {
return fmt.Errorf("invalid starter: %w", err)
}
if _, exists := ml.starters[starter.ID]; exists {
return fmt.Errorf("starter ID %d already exists", starter.ID)
}
// Add to primary storage
ml.starters[starter.ID] = starter
// Update indices
if ml.byClass[starter.StartClass] == nil {
ml.byClass[starter.StartClass] = make(map[int32]*HeroicOPStarter)
}
ml.byClass[starter.StartClass][starter.ID] = starter
ml.metadataValid = false
return nil
}
// AddWheel adds a wheel to the master list with index updates
func (ml *MasterList) AddWheel(wheel *HeroicOPWheel) error {
ml.mu.Lock()
defer ml.mu.Unlock()
if err := wheel.Validate(); err != nil {
return fmt.Errorf("invalid wheel: %w", err)
}
if _, exists := ml.wheels[wheel.ID]; exists {
return fmt.Errorf("wheel ID %d already exists", wheel.ID)
}
// Verify starter exists
if _, exists := ml.starters[wheel.StarterLinkID]; !exists {
return fmt.Errorf("starter ID %d not found for wheel", wheel.StarterLinkID)
}
// Add to primary storage
ml.wheels[wheel.ID] = wheel
// Update indices
ml.byStarterID[wheel.StarterLinkID] = append(ml.byStarterID[wheel.StarterLinkID], wheel)
ml.bySpellID[wheel.SpellID] = append(ml.bySpellID[wheel.SpellID], wheel)
chanceRange := ml.getChanceRange(wheel.Chance)
ml.byChance[chanceRange] = append(ml.byChance[chanceRange], wheel)
if wheel.IsOrdered() {
ml.orderedWheels[wheel.ID] = wheel
}
if wheel.HasShift() {
ml.shiftWheels[wheel.ID] = wheel
}
// Store spell info
ml.spellInfo[wheel.SpellID] = SpellInfo{
ID: wheel.SpellID,
Name: wheel.Name,
Description: wheel.Description,
}
ml.metadataValid = false
return nil
}
// Utility Methods
// IsLoaded returns whether data has been loaded
func (ml *MasterList) IsLoaded() bool {
ml.mu.RLock()
defer ml.mu.RUnlock()
return ml.loaded
}
// GetCount returns total counts with O(1) performance
func (ml *MasterList) GetCount() (starters, wheels int) {
ml.mu.RLock()
defer ml.mu.RUnlock()
return len(ml.starters), len(ml.wheels)
}
// Validate performs comprehensive validation of the master list
func (ml *MasterList) Validate() []error {
ml.mu.RLock()
defer ml.mu.RUnlock()
var errors []error
// Validate all starters
for _, starter := range ml.starters {
if err := starter.Validate(); err != nil {
errors = append(errors, fmt.Errorf("starter %d: %w", starter.ID, err))
}
}
// Validate all wheels
for _, wheel := range ml.wheels {
if err := wheel.Validate(); err != nil {
errors = append(errors, fmt.Errorf("wheel %d: %w", wheel.ID, err))
}
// Check if starter exists for this wheel
if _, exists := ml.starters[wheel.StarterLinkID]; !exists {
errors = append(errors, fmt.Errorf("wheel %d references non-existent starter %d", wheel.ID, wheel.StarterLinkID))
}
}
// Check for orphaned starters (starters with no wheels)
for _, starter := range ml.starters {
if wheels := ml.byStarterID[starter.ID]; len(wheels) == 0 {
errors = append(errors, fmt.Errorf("starter %d has no associated wheels", starter.ID))
}
}
return errors
}
// HeroicOPSearchResults contains search results
type HeroicOPSearchResults struct {
Starters []*HeroicOPStarter
Wheels []*HeroicOPWheel
}
// HeroicOPStatistics contains extended statistics
type HeroicOPStatistics struct {
TotalStarters int64 `json:"total_starters"`
TotalWheels int64 `json:"total_wheels"`
ClassDistribution map[int32]int64 `json:"class_distribution"`
OrderedWheelsCount int64 `json:"ordered_wheels_count"`
ShiftWheelsCount int64 `json:"shift_wheels_count"`
SpellCount int64 `json:"spell_count"`
AverageChance float64 `json:"average_chance"`
ActiveHOCount int64 `json:"active_ho_count"`
}