720 lines
19 KiB
Go
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"`
|
|
} |