401 lines
10 KiB
Go
401 lines
10 KiB
Go
package alt_advancement
|
|
|
|
import (
|
|
"fmt"
|
|
"sync"
|
|
)
|
|
|
|
// MasterList is a specialized alternate advancement master list optimized for:
|
|
// - Fast ID-based lookups (O(1))
|
|
// - Fast group-based lookups (O(1))
|
|
// - Fast class-based lookups (O(1))
|
|
// - Fast level-based lookups (O(1))
|
|
// - Efficient filtering and prerequisite validation
|
|
type MasterList struct {
|
|
// Core storage
|
|
altAdvancements map[int32]*AltAdvancement // NodeID -> AltAdvancement
|
|
mutex sync.RWMutex
|
|
|
|
// Group indices for O(1) lookups
|
|
byGroup map[int8][]*AltAdvancement // Group -> AAs
|
|
byClass map[int8][]*AltAdvancement // ClassReq -> AAs
|
|
byLevel map[int8][]*AltAdvancement // MinLevel -> AAs
|
|
|
|
// Cached metadata
|
|
groups []int8 // Unique groups (cached)
|
|
classes []int8 // Unique classes (cached)
|
|
metaStale bool // Whether metadata cache needs refresh
|
|
}
|
|
|
|
// NewMasterList creates a new specialized alternate advancement master list
|
|
func NewMasterList() *MasterList {
|
|
return &MasterList{
|
|
altAdvancements: make(map[int32]*AltAdvancement),
|
|
byGroup: make(map[int8][]*AltAdvancement),
|
|
byClass: make(map[int8][]*AltAdvancement),
|
|
byLevel: make(map[int8][]*AltAdvancement),
|
|
metaStale: true,
|
|
}
|
|
}
|
|
|
|
// refreshMetaCache updates the groups and classes cache
|
|
func (m *MasterList) refreshMetaCache() {
|
|
if !m.metaStale {
|
|
return
|
|
}
|
|
|
|
groupSet := make(map[int8]struct{})
|
|
classSet := make(map[int8]struct{})
|
|
|
|
// Collect unique groups and classes
|
|
for _, aa := range m.altAdvancements {
|
|
groupSet[aa.Group] = struct{}{}
|
|
if aa.ClassReq > 0 {
|
|
classSet[aa.ClassReq] = struct{}{}
|
|
}
|
|
}
|
|
|
|
// Clear existing caches and rebuild
|
|
m.groups = m.groups[:0]
|
|
for group := range groupSet {
|
|
m.groups = append(m.groups, group)
|
|
}
|
|
|
|
m.classes = m.classes[:0]
|
|
for class := range classSet {
|
|
m.classes = append(m.classes, class)
|
|
}
|
|
|
|
m.metaStale = false
|
|
}
|
|
|
|
// AddAltAdvancement adds an alternate advancement with full indexing
|
|
func (m *MasterList) AddAltAdvancement(aa *AltAdvancement) bool {
|
|
if aa == nil {
|
|
return false
|
|
}
|
|
|
|
m.mutex.Lock()
|
|
defer m.mutex.Unlock()
|
|
|
|
// Check if exists
|
|
if _, exists := m.altAdvancements[aa.NodeID]; exists {
|
|
return false
|
|
}
|
|
|
|
// Add to core storage
|
|
m.altAdvancements[aa.NodeID] = aa
|
|
|
|
// Update group index
|
|
m.byGroup[aa.Group] = append(m.byGroup[aa.Group], aa)
|
|
|
|
// Update class index
|
|
m.byClass[aa.ClassReq] = append(m.byClass[aa.ClassReq], aa)
|
|
|
|
// Update level index
|
|
m.byLevel[aa.MinLevel] = append(m.byLevel[aa.MinLevel], aa)
|
|
|
|
// Invalidate metadata cache
|
|
m.metaStale = true
|
|
|
|
return true
|
|
}
|
|
|
|
// GetAltAdvancement retrieves by node ID (O(1))
|
|
func (m *MasterList) GetAltAdvancement(nodeID int32) *AltAdvancement {
|
|
m.mutex.RLock()
|
|
defer m.mutex.RUnlock()
|
|
return m.altAdvancements[nodeID]
|
|
}
|
|
|
|
// GetAltAdvancementClone retrieves a cloned copy of an alternate advancement by node ID
|
|
func (m *MasterList) GetAltAdvancementClone(nodeID int32) *AltAdvancement {
|
|
m.mutex.RLock()
|
|
defer m.mutex.RUnlock()
|
|
aa := m.altAdvancements[nodeID]
|
|
if aa == nil {
|
|
return nil
|
|
}
|
|
return aa.Clone()
|
|
}
|
|
|
|
// GetAllAltAdvancements returns a copy of all alternate advancements map
|
|
func (m *MasterList) GetAllAltAdvancements() map[int32]*AltAdvancement {
|
|
m.mutex.RLock()
|
|
defer m.mutex.RUnlock()
|
|
|
|
// Return a copy to prevent external modification
|
|
result := make(map[int32]*AltAdvancement, len(m.altAdvancements))
|
|
for id, aa := range m.altAdvancements {
|
|
result[id] = aa
|
|
}
|
|
return result
|
|
}
|
|
|
|
// GetAltAdvancementsByGroup returns all alternate advancements in a group (O(1))
|
|
func (m *MasterList) GetAltAdvancementsByGroup(group int8) []*AltAdvancement {
|
|
m.mutex.RLock()
|
|
defer m.mutex.RUnlock()
|
|
return m.byGroup[group]
|
|
}
|
|
|
|
// GetAltAdvancementsByClass returns all alternate advancements for a class (O(1))
|
|
func (m *MasterList) GetAltAdvancementsByClass(classID int8) []*AltAdvancement {
|
|
m.mutex.RLock()
|
|
defer m.mutex.RUnlock()
|
|
|
|
// Return class-specific AAs plus universal AAs (ClassReq == 0)
|
|
var result []*AltAdvancement
|
|
|
|
// Add class-specific AAs
|
|
if classAAs := m.byClass[classID]; classAAs != nil {
|
|
result = append(result, classAAs...)
|
|
}
|
|
|
|
// Add universal AAs (ClassReq == 0)
|
|
if universalAAs := m.byClass[0]; universalAAs != nil {
|
|
result = append(result, universalAAs...)
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
// GetAltAdvancementsByLevel returns all alternate advancements available at a specific level
|
|
func (m *MasterList) GetAltAdvancementsByLevel(level int8) []*AltAdvancement {
|
|
m.mutex.RLock()
|
|
defer m.mutex.RUnlock()
|
|
|
|
var result []*AltAdvancement
|
|
|
|
// Collect all AAs with MinLevel <= level
|
|
for minLevel, aas := range m.byLevel {
|
|
if minLevel <= level {
|
|
result = append(result, aas...)
|
|
}
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
// GetAltAdvancementsByGroupAndClass returns AAs matching both group and class
|
|
func (m *MasterList) GetAltAdvancementsByGroupAndClass(group, classID int8) []*AltAdvancement {
|
|
m.mutex.RLock()
|
|
defer m.mutex.RUnlock()
|
|
|
|
groupAAs := m.byGroup[group]
|
|
if groupAAs == nil {
|
|
return nil
|
|
}
|
|
|
|
var result []*AltAdvancement
|
|
for _, aa := range groupAAs {
|
|
if aa.ClassReq == 0 || aa.ClassReq == classID {
|
|
result = append(result, aa)
|
|
}
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
// GetGroups returns all unique groups using cached results
|
|
func (m *MasterList) GetGroups() []int8 {
|
|
m.mutex.Lock() // Need write lock to potentially update cache
|
|
defer m.mutex.Unlock()
|
|
|
|
m.refreshMetaCache()
|
|
|
|
// Return a copy to prevent external modification
|
|
result := make([]int8, len(m.groups))
|
|
copy(result, m.groups)
|
|
return result
|
|
}
|
|
|
|
// GetClasses returns all unique classes using cached results
|
|
func (m *MasterList) GetClasses() []int8 {
|
|
m.mutex.Lock() // Need write lock to potentially update cache
|
|
defer m.mutex.Unlock()
|
|
|
|
m.refreshMetaCache()
|
|
|
|
// Return a copy to prevent external modification
|
|
result := make([]int8, len(m.classes))
|
|
copy(result, m.classes)
|
|
return result
|
|
}
|
|
|
|
// RemoveAltAdvancement removes an alternate advancement and updates all indices
|
|
func (m *MasterList) RemoveAltAdvancement(nodeID int32) bool {
|
|
m.mutex.Lock()
|
|
defer m.mutex.Unlock()
|
|
|
|
aa, exists := m.altAdvancements[nodeID]
|
|
if !exists {
|
|
return false
|
|
}
|
|
|
|
// Remove from core storage
|
|
delete(m.altAdvancements, nodeID)
|
|
|
|
// Remove from group index
|
|
groupAAs := m.byGroup[aa.Group]
|
|
for i, a := range groupAAs {
|
|
if a.NodeID == nodeID {
|
|
m.byGroup[aa.Group] = append(groupAAs[:i], groupAAs[i+1:]...)
|
|
break
|
|
}
|
|
}
|
|
|
|
// Remove from class index
|
|
classAAs := m.byClass[aa.ClassReq]
|
|
for i, a := range classAAs {
|
|
if a.NodeID == nodeID {
|
|
m.byClass[aa.ClassReq] = append(classAAs[:i], classAAs[i+1:]...)
|
|
break
|
|
}
|
|
}
|
|
|
|
// Remove from level index
|
|
levelAAs := m.byLevel[aa.MinLevel]
|
|
for i, a := range levelAAs {
|
|
if a.NodeID == nodeID {
|
|
m.byLevel[aa.MinLevel] = append(levelAAs[:i], levelAAs[i+1:]...)
|
|
break
|
|
}
|
|
}
|
|
|
|
// Invalidate metadata cache
|
|
m.metaStale = true
|
|
|
|
return true
|
|
}
|
|
|
|
// UpdateAltAdvancement updates an existing alternate advancement
|
|
func (m *MasterList) UpdateAltAdvancement(aa *AltAdvancement) error {
|
|
if aa == nil {
|
|
return fmt.Errorf("alternate advancement cannot be nil")
|
|
}
|
|
|
|
m.mutex.Lock()
|
|
defer m.mutex.Unlock()
|
|
|
|
// Check if exists
|
|
old, exists := m.altAdvancements[aa.NodeID]
|
|
if !exists {
|
|
return fmt.Errorf("alternate advancement %d not found", aa.NodeID)
|
|
}
|
|
|
|
// Remove old AA from indices (but not core storage yet)
|
|
groupAAs := m.byGroup[old.Group]
|
|
for i, a := range groupAAs {
|
|
if a.NodeID == aa.NodeID {
|
|
m.byGroup[old.Group] = append(groupAAs[:i], groupAAs[i+1:]...)
|
|
break
|
|
}
|
|
}
|
|
|
|
classAAs := m.byClass[old.ClassReq]
|
|
for i, a := range classAAs {
|
|
if a.NodeID == aa.NodeID {
|
|
m.byClass[old.ClassReq] = append(classAAs[:i], classAAs[i+1:]...)
|
|
break
|
|
}
|
|
}
|
|
|
|
levelAAs := m.byLevel[old.MinLevel]
|
|
for i, a := range levelAAs {
|
|
if a.NodeID == aa.NodeID {
|
|
m.byLevel[old.MinLevel] = append(levelAAs[:i], levelAAs[i+1:]...)
|
|
break
|
|
}
|
|
}
|
|
|
|
// Update core storage
|
|
m.altAdvancements[aa.NodeID] = aa
|
|
|
|
// Add new AA to indices
|
|
m.byGroup[aa.Group] = append(m.byGroup[aa.Group], aa)
|
|
m.byClass[aa.ClassReq] = append(m.byClass[aa.ClassReq], aa)
|
|
m.byLevel[aa.MinLevel] = append(m.byLevel[aa.MinLevel], aa)
|
|
|
|
// Invalidate metadata cache
|
|
m.metaStale = true
|
|
|
|
return nil
|
|
}
|
|
|
|
// Size returns the total number of alternate advancements
|
|
func (m *MasterList) Size() int {
|
|
m.mutex.RLock()
|
|
defer m.mutex.RUnlock()
|
|
return len(m.altAdvancements)
|
|
}
|
|
|
|
// Clear removes all alternate advancements from the master list
|
|
func (m *MasterList) Clear() {
|
|
m.mutex.Lock()
|
|
defer m.mutex.Unlock()
|
|
|
|
// Clear all maps
|
|
m.altAdvancements = make(map[int32]*AltAdvancement)
|
|
m.byGroup = make(map[int8][]*AltAdvancement)
|
|
m.byClass = make(map[int8][]*AltAdvancement)
|
|
m.byLevel = make(map[int8][]*AltAdvancement)
|
|
|
|
// Clear cached metadata
|
|
m.groups = m.groups[:0]
|
|
m.classes = m.classes[:0]
|
|
m.metaStale = true
|
|
}
|
|
|
|
// ForEach executes a function for each alternate advancement
|
|
func (m *MasterList) ForEach(fn func(int32, *AltAdvancement)) {
|
|
m.mutex.RLock()
|
|
defer m.mutex.RUnlock()
|
|
|
|
for id, aa := range m.altAdvancements {
|
|
fn(id, aa)
|
|
}
|
|
}
|
|
|
|
// ValidateAll validates all alternate advancements in the master list
|
|
func (m *MasterList) ValidateAll() []error {
|
|
m.mutex.RLock()
|
|
defer m.mutex.RUnlock()
|
|
|
|
var errors []error
|
|
|
|
for nodeID, aa := range m.altAdvancements {
|
|
if !aa.IsValid() {
|
|
errors = append(errors, fmt.Errorf("invalid AA data: node_id=%d", nodeID))
|
|
}
|
|
|
|
// Validate prerequisites
|
|
if aa.RankPrereqID > 0 {
|
|
prereq := m.altAdvancements[aa.RankPrereqID]
|
|
if prereq == nil {
|
|
errors = append(errors, fmt.Errorf("AA %d has invalid prerequisite node ID %d", nodeID, aa.RankPrereqID))
|
|
}
|
|
}
|
|
|
|
// Validate positioning
|
|
if aa.Col < MIN_AA_COL || aa.Col > MAX_AA_COL {
|
|
errors = append(errors, fmt.Errorf("AA %d has invalid column %d", nodeID, aa.Col))
|
|
}
|
|
|
|
if aa.Row < MIN_AA_ROW || aa.Row > MAX_AA_ROW {
|
|
errors = append(errors, fmt.Errorf("AA %d has invalid row %d", nodeID, aa.Row))
|
|
}
|
|
|
|
// Validate costs and ranks
|
|
if aa.RankCost < MIN_RANK_COST || aa.RankCost > MAX_RANK_COST {
|
|
errors = append(errors, fmt.Errorf("AA %d has invalid rank cost %d", nodeID, aa.RankCost))
|
|
}
|
|
|
|
if aa.MaxRank < MIN_MAX_RANK || aa.MaxRank > MAX_MAX_RANK {
|
|
errors = append(errors, fmt.Errorf("AA %d has invalid max rank %d", nodeID, aa.MaxRank))
|
|
}
|
|
}
|
|
|
|
return errors
|
|
}
|