modernize aa package
This commit is contained in:
parent
104a039bc0
commit
c3a64dd96c
192
MODERNIZE.md
Normal file
192
MODERNIZE.md
Normal file
@ -0,0 +1,192 @@
|
|||||||
|
# Package Modernization Instructions
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
Transform legacy packages to use generic MasterList pattern with simplified database operations.
|
||||||
|
|
||||||
|
## Steps
|
||||||
|
|
||||||
|
### 1. Implement Generic MasterList Base
|
||||||
|
Create `internal/common/master_list.go` with generic collection management:
|
||||||
|
```go
|
||||||
|
type MasterList[K comparable, V Identifiable[K]] struct {
|
||||||
|
items map[K]V
|
||||||
|
mutex sync.RWMutex
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Consolidate Package Structure
|
||||||
|
|
||||||
|
**Remove:**
|
||||||
|
- Legacy wrapper functions (LoadAll, SaveAll, etc.)
|
||||||
|
- Duplicate database files (database.go, database_legacy.go)
|
||||||
|
- README.md (use doc.go instead)
|
||||||
|
- Separate "active_record.go" files
|
||||||
|
|
||||||
|
**Consolidate into:**
|
||||||
|
- `{type}.go` - Main type with embedded database operations
|
||||||
|
- `types.go` - Supporting types only (no main type)
|
||||||
|
- `master.go` - MasterList using generic base
|
||||||
|
- `player.go` - Player-specific logic (if applicable)
|
||||||
|
- `doc.go` - Primary documentation
|
||||||
|
- `{type}_test.go` - Focused tests
|
||||||
|
|
||||||
|
### 3. Refactor Main Type
|
||||||
|
|
||||||
|
Transform the main type to include database operations:
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Before: Separate type and database operations
|
||||||
|
type Achievement struct {
|
||||||
|
ID uint32
|
||||||
|
Title string
|
||||||
|
}
|
||||||
|
|
||||||
|
func LoadAchievement(db *database.Database, id uint32) (*Achievement, error)
|
||||||
|
func SaveAchievement(db *database.Database, a *Achievement) error
|
||||||
|
|
||||||
|
// After: Embedded database operations
|
||||||
|
type Achievement struct {
|
||||||
|
ID uint32
|
||||||
|
Title string
|
||||||
|
|
||||||
|
db *database.Database
|
||||||
|
isNew bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(db *database.Database) *Achievement
|
||||||
|
func Load(db *database.Database, id uint32) (*Achievement, error)
|
||||||
|
func (a *Achievement) Save() error
|
||||||
|
func (a *Achievement) Delete() error
|
||||||
|
func (a *Achievement) Reload() error
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Update MasterList
|
||||||
|
|
||||||
|
Replace manual implementation with generic base:
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Before: Manual thread-safety
|
||||||
|
type MasterList struct {
|
||||||
|
items map[uint32]*Achievement
|
||||||
|
mutex sync.RWMutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MasterList) AddAchievement(a *Achievement) bool {
|
||||||
|
m.mutex.Lock()
|
||||||
|
defer m.mutex.Unlock()
|
||||||
|
// manual implementation
|
||||||
|
}
|
||||||
|
|
||||||
|
// After: Generic base
|
||||||
|
type MasterList struct {
|
||||||
|
*common.MasterList[uint32, *Achievement]
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewMasterList() *MasterList {
|
||||||
|
return &MasterList{
|
||||||
|
MasterList: common.NewMasterList[uint32, *Achievement](),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MasterList) AddAchievement(a *Achievement) bool {
|
||||||
|
return m.MasterList.Add(a)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Implement Identifiable Interface
|
||||||
|
|
||||||
|
Ensure main type implements `GetID()`:
|
||||||
|
|
||||||
|
```go
|
||||||
|
func (a *Achievement) GetID() uint32 {
|
||||||
|
return a.AchievementID // or appropriate ID field
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6. Simplify API
|
||||||
|
|
||||||
|
**Remove:**
|
||||||
|
- Legacy type variants (Achievement vs AchievementRecord)
|
||||||
|
- Conversion methods (ToLegacy, FromLegacy)
|
||||||
|
- Duplicate CRUD operations
|
||||||
|
- Complex wrapper functions
|
||||||
|
|
||||||
|
**Keep:**
|
||||||
|
- Single type definition
|
||||||
|
- Direct database methods on type
|
||||||
|
- Domain-specific extensions only
|
||||||
|
|
||||||
|
### 7. Update Documentation
|
||||||
|
|
||||||
|
Create concise `doc.go`:
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Package achievements provides [brief description].
|
||||||
|
//
|
||||||
|
// Basic Usage:
|
||||||
|
//
|
||||||
|
// achievement := achievements.New(db)
|
||||||
|
// achievement.Title = "Dragon Slayer"
|
||||||
|
// achievement.Save()
|
||||||
|
//
|
||||||
|
// loaded, _ := achievements.Load(db, 1001)
|
||||||
|
// loaded.Delete()
|
||||||
|
//
|
||||||
|
// Master List:
|
||||||
|
//
|
||||||
|
// masterList := achievements.NewMasterList()
|
||||||
|
// masterList.Add(achievement)
|
||||||
|
package achievements
|
||||||
|
```
|
||||||
|
|
||||||
|
### 8. Testing
|
||||||
|
|
||||||
|
Create focused tests:
|
||||||
|
- Test core type operations (New, Save, Load, Delete)
|
||||||
|
- Test MasterList basic operations
|
||||||
|
- Remove legacy compatibility tests
|
||||||
|
- Keep tests simple and direct
|
||||||
|
|
||||||
|
## Key Principles
|
||||||
|
|
||||||
|
1. **Single Source of Truth**: One type definition, not multiple variants
|
||||||
|
2. **Embedded Operations**: Database methods on the type itself
|
||||||
|
3. **Generic Base**: Use common.MasterList for thread-safety
|
||||||
|
4. **No Legacy Baggage**: Remove all "Legacy" types and converters
|
||||||
|
5. **Documentation in Code**: Use doc.go, not README.md
|
||||||
|
|
||||||
|
## Migration Checklist
|
||||||
|
|
||||||
|
- [ ] Create/verify generic MasterList in common package
|
||||||
|
- [ ] Identify main type and supporting types
|
||||||
|
- [ ] Consolidate database operations into main type
|
||||||
|
- [ ] Add db field and methods to main type
|
||||||
|
- [ ] Replace manual MasterList with generic base
|
||||||
|
- [ ] Implement GetID() for Identifiable interface
|
||||||
|
- [ ] Remove all legacy types and converters
|
||||||
|
- [ ] Update doc.go with concise examples
|
||||||
|
- [ ] Simplify tests to cover core functionality
|
||||||
|
- [ ] Run `go fmt` and `go test`
|
||||||
|
|
||||||
|
## Expected Results
|
||||||
|
|
||||||
|
- **80% less code** in most packages
|
||||||
|
- **Single type** instead of multiple variants
|
||||||
|
- **Thread-safe** operations via generic base
|
||||||
|
- **Consistent API** across all packages
|
||||||
|
- **Better maintainability** with less duplication
|
||||||
|
|
||||||
|
## Example Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Remove legacy files
|
||||||
|
rm README.md database_legacy.go active_record.go
|
||||||
|
|
||||||
|
# Rename if needed
|
||||||
|
mv active_record.go achievement.go
|
||||||
|
|
||||||
|
# Test the changes
|
||||||
|
go fmt ./...
|
||||||
|
go test ./...
|
||||||
|
go build ./...
|
||||||
|
```
|
@ -1,499 +0,0 @@
|
|||||||
# Alternate Advancement System
|
|
||||||
|
|
||||||
The alternate advancement system (`internal/alt_advancement`) provides comprehensive character progression beyond normal leveling for the EQ2Go server emulator. This system is converted from the original C++ EQ2EMu AltAdvancement implementation with modern Go concurrency patterns and clean architecture principles.
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
The alternate advancement (AA) system manages character progression through specialized skill trees including:
|
|
||||||
|
|
||||||
- **Class Trees**: Class-specific advancement paths
|
|
||||||
- **Subclass Trees**: Subclass-specific specializations
|
|
||||||
- **Heroic Trees**: Heroic advancement from Destiny of Velious
|
|
||||||
- **Shadows Trees**: Shadow-based abilities from Shadows of Luclin
|
|
||||||
- **Tradeskill Trees**: Tradeskill-focused advancement
|
|
||||||
- **Prestige Trees**: Prestigious advancement paths
|
|
||||||
- **Dragon Trees**: Dragon-themed advancement
|
|
||||||
- **Far Seas Trees**: Far Seas trading company advancement
|
|
||||||
|
|
||||||
## Architecture
|
|
||||||
|
|
||||||
### Core Components
|
|
||||||
|
|
||||||
**MasterAAList** - Global repository of all AA definitions with fast lookup capabilities
|
|
||||||
**MasterAANodeList** - Tree node configurations mapping classes to AA trees
|
|
||||||
**AAManager** - Central management system for all AA operations
|
|
||||||
**AAPlayerState** - Individual player AA progression and template management
|
|
||||||
**DatabaseImpl** - Database operations for persistent AA data
|
|
||||||
|
|
||||||
### Key Files
|
|
||||||
|
|
||||||
- `types.go` - Core data structures and type definitions
|
|
||||||
- `constants.go` - All AA system constants, tab definitions, and limits
|
|
||||||
- `master_list.go` - MasterAAList and MasterAANodeList implementations
|
|
||||||
- `manager.go` - Central AAManager with player state management
|
|
||||||
- `database.go` - Database operations and persistence
|
|
||||||
- `interfaces.go` - System integration interfaces and adapters
|
|
||||||
- `README.md` - This documentation
|
|
||||||
|
|
||||||
## System Initialization
|
|
||||||
|
|
||||||
```go
|
|
||||||
// Create AA manager with configuration
|
|
||||||
config := alt_advancement.DefaultAAManagerConfig()
|
|
||||||
config.EnableCaching = true
|
|
||||||
config.DatabaseEnabled = true
|
|
||||||
|
|
||||||
aaManager := alt_advancement.NewAAManager(config)
|
|
||||||
|
|
||||||
// Set up database integration
|
|
||||||
database := alt_advancement.NewDatabaseImpl(db, masterAAList, masterNodeList, logger)
|
|
||||||
aaManager.SetDatabase(database)
|
|
||||||
|
|
||||||
// Start the system
|
|
||||||
err := aaManager.Start()
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("Failed to start AA system: %v", err)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## AA Data Management
|
|
||||||
|
|
||||||
```go
|
|
||||||
// Load all AA data from database
|
|
||||||
err := aaManager.LoadAAData()
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("Failed to load AA data: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get specific AA by node ID
|
|
||||||
aaData, err := aaManager.GetAA(nodeID)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("AA not found: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get AAs for a specific class
|
|
||||||
classAAs, err := aaManager.GetAAsByClass(classID)
|
|
||||||
fmt.Printf("Found %d AAs for class %d\n", len(classAAs), classID)
|
|
||||||
|
|
||||||
// Get AAs for a specific tab/group
|
|
||||||
tabAAs, err := aaManager.GetAAsByGroup(alt_advancement.AA_CLASS)
|
|
||||||
fmt.Printf("Class tab has %d AAs\n", len(tabAAs))
|
|
||||||
```
|
|
||||||
|
|
||||||
## Player AA Management
|
|
||||||
|
|
||||||
```go
|
|
||||||
// Load player's AA data
|
|
||||||
characterID := int32(12345)
|
|
||||||
playerState, err := aaManager.LoadPlayerAA(characterID)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("Failed to load player AA: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get player's AA point totals
|
|
||||||
totalPoints, spentPoints, availablePoints, err := aaManager.GetAAPoints(characterID)
|
|
||||||
fmt.Printf("Player has %d total, %d spent, %d available AA points\n",
|
|
||||||
totalPoints, spentPoints, availablePoints)
|
|
||||||
|
|
||||||
// Award AA points to player
|
|
||||||
err = aaManager.AwardAAPoints(characterID, 10, "Level up bonus")
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("Failed to award AA points: %v", err)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## AA Purchasing System
|
|
||||||
|
|
||||||
```go
|
|
||||||
// Purchase an AA for a player
|
|
||||||
nodeID := int32(1001)
|
|
||||||
targetRank := int8(1)
|
|
||||||
|
|
||||||
err := aaManager.PurchaseAA(characterID, nodeID, targetRank)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("AA purchase failed: %v", err)
|
|
||||||
} else {
|
|
||||||
fmt.Println("AA purchased successfully!")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Refund an AA
|
|
||||||
err = aaManager.RefundAA(characterID, nodeID)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("AA refund failed: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check available AAs for a tab
|
|
||||||
availableAAs, err := aaManager.GetAvailableAAs(characterID, alt_advancement.AA_CLASS)
|
|
||||||
fmt.Printf("Player can purchase %d AAs in class tab\n", len(availableAAs))
|
|
||||||
```
|
|
||||||
|
|
||||||
## AA Templates System
|
|
||||||
|
|
||||||
```go
|
|
||||||
// Change active AA template
|
|
||||||
templateID := int8(alt_advancement.AA_TEMPLATE_PERSONAL_1)
|
|
||||||
err := aaManager.ChangeAATemplate(characterID, templateID)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("Template change failed: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save custom AA template
|
|
||||||
err = aaManager.SaveAATemplate(characterID, templateID, "My Build")
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("Template save failed: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get all templates for player
|
|
||||||
templates, err := aaManager.GetAATemplates(characterID)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("Failed to get templates: %v", err)
|
|
||||||
} else {
|
|
||||||
for id, template := range templates {
|
|
||||||
fmt.Printf("Template %d: %s (%d entries)\n",
|
|
||||||
id, template.Name, len(template.Entries))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## AA Data Structures
|
|
||||||
|
|
||||||
### AltAdvanceData - Individual AA Definition
|
|
||||||
|
|
||||||
```go
|
|
||||||
type AltAdvanceData struct {
|
|
||||||
SpellID int32 // Associated spell ID
|
|
||||||
NodeID int32 // Unique node identifier
|
|
||||||
Name string // Display name
|
|
||||||
Description string // AA description
|
|
||||||
Group int8 // Tab/group (AA_CLASS, AA_SUBCLASS, etc.)
|
|
||||||
Col int8 // Column position in tree
|
|
||||||
Row int8 // Row position in tree
|
|
||||||
Icon int16 // Display icon ID
|
|
||||||
RankCost int8 // Cost per rank
|
|
||||||
MaxRank int8 // Maximum achievable rank
|
|
||||||
MinLevel int8 // Minimum character level
|
|
||||||
RankPrereqID int32 // Prerequisite AA node ID
|
|
||||||
RankPrereq int8 // Required rank in prerequisite
|
|
||||||
ClassReq int8 // Required class (0 = all classes)
|
|
||||||
// ... additional fields
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### AAPlayerState - Player AA Progression
|
|
||||||
|
|
||||||
```go
|
|
||||||
type AAPlayerState struct {
|
|
||||||
CharacterID int32 // Character identifier
|
|
||||||
TotalPoints int32 // Total AA points earned
|
|
||||||
SpentPoints int32 // Total AA points spent
|
|
||||||
AvailablePoints int32 // Available AA points for spending
|
|
||||||
BankedPoints int32 // Banked AA points
|
|
||||||
ActiveTemplate int8 // Currently active template
|
|
||||||
Templates map[int8]*AATemplate // All templates
|
|
||||||
Tabs map[int8]*AATab // Tab states
|
|
||||||
AAProgress map[int32]*PlayerAAData // AA progression by node ID
|
|
||||||
// ... synchronization and metadata
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## AA Tab System
|
|
||||||
|
|
||||||
The system supports 10 different AA tabs:
|
|
||||||
|
|
||||||
```go
|
|
||||||
// AA tab constants
|
|
||||||
const (
|
|
||||||
AA_CLASS = 0 // Class-specific abilities
|
|
||||||
AA_SUBCLASS = 1 // Subclass specializations
|
|
||||||
AA_SHADOW = 2 // Shadow abilities
|
|
||||||
AA_HEROIC = 3 // Heroic advancement
|
|
||||||
AA_TRADESKILL = 4 // Tradeskill abilities
|
|
||||||
AA_PRESTIGE = 5 // Prestige advancement
|
|
||||||
AA_TRADESKILL_PRESTIGE = 6 // Tradeskill prestige
|
|
||||||
AA_DRAGON = 7 // Dragon abilities
|
|
||||||
AA_DRAGONCLASS = 8 // Dragon class abilities
|
|
||||||
AA_FARSEAS = 9 // Far Seas abilities
|
|
||||||
)
|
|
||||||
|
|
||||||
// Get maximum AA points for each tab
|
|
||||||
maxClassAA := alt_advancement.GetMaxAAForTab(alt_advancement.AA_CLASS) // 100
|
|
||||||
maxHeroicAA := alt_advancement.GetMaxAAForTab(alt_advancement.AA_HEROIC) // 50
|
|
||||||
```
|
|
||||||
|
|
||||||
## Database Integration
|
|
||||||
|
|
||||||
```go
|
|
||||||
// Custom database implementation
|
|
||||||
type MyAADatabase struct {
|
|
||||||
db *sql.DB
|
|
||||||
}
|
|
||||||
|
|
||||||
func (db *MyAADatabase) LoadAltAdvancements() error {
|
|
||||||
// Load AA definitions from database
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (db *MyAADatabase) LoadPlayerAA(characterID int32) (*AAPlayerState, error) {
|
|
||||||
// Load player AA data from database
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (db *MyAADatabase) SavePlayerAA(playerState *AAPlayerState) error {
|
|
||||||
// Save player AA data to database
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set database implementation
|
|
||||||
aaManager.SetDatabase(&MyAADatabase{db: myDB})
|
|
||||||
```
|
|
||||||
|
|
||||||
## Event Handling
|
|
||||||
|
|
||||||
```go
|
|
||||||
// Custom event handler
|
|
||||||
type MyAAEventHandler struct{}
|
|
||||||
|
|
||||||
func (h *MyAAEventHandler) OnAAPurchased(characterID int32, nodeID int32, newRank int8, pointsSpent int32) error {
|
|
||||||
fmt.Printf("Player %d purchased AA %d rank %d for %d points\n",
|
|
||||||
characterID, nodeID, newRank, pointsSpent)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *MyAAEventHandler) OnAATemplateChanged(characterID int32, oldTemplate, newTemplate int8) error {
|
|
||||||
fmt.Printf("Player %d changed template from %d to %d\n",
|
|
||||||
characterID, oldTemplate, newTemplate)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *MyAAEventHandler) OnPlayerAAPointsChanged(characterID int32, oldPoints, newPoints int32) error {
|
|
||||||
fmt.Printf("Player %d AA points changed from %d to %d\n",
|
|
||||||
characterID, oldPoints, newPoints)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Register event handler
|
|
||||||
aaManager.SetEventHandler(&MyAAEventHandler{})
|
|
||||||
```
|
|
||||||
|
|
||||||
## Validation System
|
|
||||||
|
|
||||||
```go
|
|
||||||
// Custom validator
|
|
||||||
type MyAAValidator struct{}
|
|
||||||
|
|
||||||
func (v *MyAAValidator) ValidateAAPurchase(playerState *AAPlayerState, nodeID int32, targetRank int8) error {
|
|
||||||
// Check if player has enough points
|
|
||||||
if playerState.AvailablePoints < int32(targetRank) {
|
|
||||||
return fmt.Errorf("insufficient AA points")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check prerequisites
|
|
||||||
// ... validation logic
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *MyAAValidator) ValidatePlayerLevel(playerState *AAPlayerState, aaData *AltAdvanceData) error {
|
|
||||||
// Check minimum level requirement
|
|
||||||
// ... validation logic
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set validator
|
|
||||||
aaManager.SetValidator(&MyAAValidator{})
|
|
||||||
```
|
|
||||||
|
|
||||||
## Statistics and Monitoring
|
|
||||||
|
|
||||||
```go
|
|
||||||
// Get system statistics
|
|
||||||
stats := aaManager.GetSystemStats()
|
|
||||||
fmt.Printf("Total AAs loaded: %d\n", stats.TotalAAsLoaded)
|
|
||||||
fmt.Printf("Active players: %d\n", stats.ActivePlayers)
|
|
||||||
fmt.Printf("Total purchases: %d\n", stats.TotalAAPurchases)
|
|
||||||
fmt.Printf("Average points spent: %.1f\n", stats.AveragePointsSpent)
|
|
||||||
|
|
||||||
// Get player-specific statistics
|
|
||||||
playerStats := aaManager.GetPlayerStats(characterID)
|
|
||||||
fmt.Printf("Player stats: %+v\n", playerStats)
|
|
||||||
|
|
||||||
// Get database statistics (if database supports it)
|
|
||||||
if db, ok := aaManager.database.(*DatabaseImpl); ok {
|
|
||||||
dbStats, err := db.GetAAStatistics()
|
|
||||||
if err == nil {
|
|
||||||
fmt.Printf("Database stats: %+v\n", dbStats)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Configuration Options
|
|
||||||
|
|
||||||
```go
|
|
||||||
// Configure the AA system
|
|
||||||
config := alt_advancement.AAManagerConfig{
|
|
||||||
EnableAASystem: true,
|
|
||||||
EnableCaching: true,
|
|
||||||
EnableValidation: true,
|
|
||||||
EnableLogging: false,
|
|
||||||
AAPointsPerLevel: 2,
|
|
||||||
MaxBankedPoints: 30,
|
|
||||||
EnableAABanking: true,
|
|
||||||
CacheSize: 10000,
|
|
||||||
UpdateInterval: 1 * time.Second,
|
|
||||||
BatchSize: 100,
|
|
||||||
DatabaseEnabled: true,
|
|
||||||
AutoSave: true,
|
|
||||||
SaveInterval: 5 * time.Minute,
|
|
||||||
}
|
|
||||||
|
|
||||||
aaManager.SetConfig(config)
|
|
||||||
```
|
|
||||||
|
|
||||||
## Caching System
|
|
||||||
|
|
||||||
```go
|
|
||||||
// Enable caching for better performance
|
|
||||||
cache := alt_advancement.NewSimpleAACache(1000)
|
|
||||||
aaManager.SetCache(cache)
|
|
||||||
|
|
||||||
// Get cache statistics
|
|
||||||
cacheStats := cache.GetStats()
|
|
||||||
fmt.Printf("Cache hits: %d, misses: %d\n",
|
|
||||||
cacheStats["hits"], cacheStats["misses"])
|
|
||||||
```
|
|
||||||
|
|
||||||
## Packet Handling Integration
|
|
||||||
|
|
||||||
```go
|
|
||||||
// Custom packet handler
|
|
||||||
type MyAAPacketHandler struct{}
|
|
||||||
|
|
||||||
func (ph *MyAAPacketHandler) GetAAListPacket(client any) ([]byte, error) {
|
|
||||||
// Build AA list packet for client
|
|
||||||
return []byte{}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ph *MyAAPacketHandler) SendAAUpdate(client any, playerState *AAPlayerState) error {
|
|
||||||
// Send AA update to client
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ph *MyAAPacketHandler) HandleAAPurchase(client any, nodeID int32, rank int8) error {
|
|
||||||
// Handle AA purchase from client
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set packet handler
|
|
||||||
aaManager.SetPacketHandler(&MyAAPacketHandler{})
|
|
||||||
```
|
|
||||||
|
|
||||||
## Advanced Usage
|
|
||||||
|
|
||||||
### Custom AA Trees
|
|
||||||
|
|
||||||
```go
|
|
||||||
// Create custom AA data
|
|
||||||
customAA := &alt_advancement.AltAdvanceData{
|
|
||||||
SpellID: 2001,
|
|
||||||
NodeID: 2001,
|
|
||||||
Name: "Custom Ability",
|
|
||||||
Description: "A custom AA ability",
|
|
||||||
Group: alt_advancement.AA_CLASS,
|
|
||||||
Col: 1,
|
|
||||||
Row: 1,
|
|
||||||
Icon: 100,
|
|
||||||
RankCost: 1,
|
|
||||||
MaxRank: 5,
|
|
||||||
MinLevel: 20,
|
|
||||||
ClassReq: 1, // Fighter only
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add to master list
|
|
||||||
err := masterAAList.AddAltAdvancement(customAA)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("Failed to add custom AA: %v", err)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Bulk Operations
|
|
||||||
|
|
||||||
```go
|
|
||||||
// Award AA points to multiple players
|
|
||||||
playerIDs := []int32{1001, 1002, 1003}
|
|
||||||
for _, playerID := range playerIDs {
|
|
||||||
err := aaManager.AwardAAPoints(playerID, 5, "Server event bonus")
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("Failed to award points to player %d: %v", playerID, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Batch save player states
|
|
||||||
for _, playerID := range playerIDs {
|
|
||||||
err := aaManager.SavePlayerAA(playerID)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("Failed to save player %d AA data: %v", playerID, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Thread Safety
|
|
||||||
|
|
||||||
All AA operations are thread-safe using appropriate synchronization:
|
|
||||||
|
|
||||||
- **RWMutex** for read-heavy operations (AA lookups, player state access)
|
|
||||||
- **Atomic operations** for simple counters and flags
|
|
||||||
- **Proper lock ordering** to prevent deadlocks
|
|
||||||
- **Background goroutines** for periodic processing and auto-save
|
|
||||||
- **Channel-based communication** for event handling
|
|
||||||
|
|
||||||
## Performance Considerations
|
|
||||||
|
|
||||||
- **Efficient data structures** with hash maps for O(1) lookups
|
|
||||||
- **Caching system** to reduce database queries
|
|
||||||
- **Batch processing** for bulk operations
|
|
||||||
- **Background processing** to avoid blocking gameplay
|
|
||||||
- **Statistics collection** with minimal overhead
|
|
||||||
- **Memory-efficient storage** with proper cleanup
|
|
||||||
|
|
||||||
## Integration with Other Systems
|
|
||||||
|
|
||||||
The AA system integrates with:
|
|
||||||
|
|
||||||
- **Player System** - Player-specific AA progression and point management
|
|
||||||
- **Spell System** - AA abilities are linked to spells
|
|
||||||
- **Class System** - Class-specific AA trees and requirements
|
|
||||||
- **Level System** - Level-based AA point awards and prerequisites
|
|
||||||
- **Database System** - Persistent storage of AA data and player progress
|
|
||||||
- **Client System** - AA UI updates and purchase handling
|
|
||||||
- **Achievement System** - AA milestones and progression tracking
|
|
||||||
|
|
||||||
## Migration from C++
|
|
||||||
|
|
||||||
This Go implementation maintains compatibility with the original C++ EQ2EMu AA system while providing:
|
|
||||||
|
|
||||||
- **Modern concurrency** with goroutines and channels
|
|
||||||
- **Better error handling** with Go's error interface
|
|
||||||
- **Cleaner architecture** with interface-based design
|
|
||||||
- **Improved maintainability** with package organization
|
|
||||||
- **Enhanced testing** capabilities
|
|
||||||
- **Type safety** with Go's type system
|
|
||||||
- **Memory management** with Go's garbage collector
|
|
||||||
|
|
||||||
## TODO Items
|
|
||||||
|
|
||||||
The conversion includes areas for future implementation:
|
|
||||||
|
|
||||||
- **Complete packet handling** for all client communication
|
|
||||||
- **Advanced validation** for complex AA prerequisites
|
|
||||||
- **Lua scripting integration** for custom AA behaviors
|
|
||||||
- **Web administration interface** for AA management
|
|
||||||
- **Performance optimizations** for large-scale deployments
|
|
||||||
- **Advanced caching strategies** with TTL and eviction policies
|
|
||||||
- **Metrics and monitoring** integration with external systems
|
|
||||||
- **AA import/export** functionality for configuration management
|
|
||||||
|
|
||||||
## Usage Examples
|
|
||||||
|
|
||||||
See the code examples throughout this documentation for detailed usage patterns. The system is designed to be used alongside the existing EQ2Go server infrastructure with proper initialization and configuration.
|
|
||||||
|
|
||||||
The AA system provides a solid foundation for character progression mechanics while maintaining the flexibility to extend and customize behavior through the comprehensive interface system.
|
|
File diff suppressed because it is too large
Load Diff
344
internal/alt_advancement/alt_advancement.go
Normal file
344
internal/alt_advancement/alt_advancement.go
Normal file
@ -0,0 +1,344 @@
|
|||||||
|
package alt_advancement
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"eq2emu/internal/database"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AltAdvancement represents an Alternate Advancement node with database operations
|
||||||
|
type AltAdvancement struct {
|
||||||
|
// Core identification
|
||||||
|
SpellID int32 `json:"spell_id" db:"spell_id"`
|
||||||
|
NodeID int32 `json:"node_id" db:"node_id"`
|
||||||
|
SpellCRC int32 `json:"spell_crc" db:"spell_crc"`
|
||||||
|
|
||||||
|
// Display information
|
||||||
|
Name string `json:"name" db:"name"`
|
||||||
|
Description string `json:"description" db:"description"`
|
||||||
|
|
||||||
|
// Tree organization
|
||||||
|
Group int8 `json:"group" db:"group"` // AA tab (AA_CLASS, AA_SUBCLASS, etc.)
|
||||||
|
Col int8 `json:"col" db:"col"` // Column position in tree
|
||||||
|
Row int8 `json:"row" db:"row"` // Row position in tree
|
||||||
|
|
||||||
|
// Visual representation
|
||||||
|
Icon int16 `json:"icon" db:"icon"` // Primary icon ID
|
||||||
|
Icon2 int16 `json:"icon2" db:"icon2"` // Secondary icon ID
|
||||||
|
|
||||||
|
// Ranking system
|
||||||
|
RankCost int8 `json:"rank_cost" db:"rank_cost"` // Cost per rank
|
||||||
|
MaxRank int8 `json:"max_rank" db:"max_rank"` // Maximum achievable rank
|
||||||
|
|
||||||
|
// Prerequisites
|
||||||
|
MinLevel int8 `json:"min_level" db:"min_level"` // Minimum character level
|
||||||
|
RankPrereqID int32 `json:"rank_prereq_id" db:"rank_prereq_id"` // Prerequisite AA node ID
|
||||||
|
RankPrereq int8 `json:"rank_prereq" db:"rank_prereq"` // Required rank in prerequisite
|
||||||
|
ClassReq int8 `json:"class_req" db:"class_req"` // Required class
|
||||||
|
Tier int8 `json:"tier" db:"tier"` // AA tier
|
||||||
|
ReqPoints int8 `json:"req_points" db:"req_points"` // Required points in classification
|
||||||
|
ReqTreePoints int16 `json:"req_tree_points" db:"req_tree_points"` // Required points in entire tree
|
||||||
|
|
||||||
|
// Display classification
|
||||||
|
ClassName string `json:"class_name" db:"class_name"` // Class name for display
|
||||||
|
SubclassName string `json:"subclass_name" db:"subclass_name"` // Subclass name for display
|
||||||
|
LineTitle string `json:"line_title" db:"line_title"` // AA line title
|
||||||
|
TitleLevel int8 `json:"title_level" db:"title_level"` // Title level requirement
|
||||||
|
|
||||||
|
// Metadata
|
||||||
|
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
|
||||||
|
|
||||||
|
// Database connection
|
||||||
|
db *database.Database
|
||||||
|
isNew bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// New creates a new alternate advancement with database connection
|
||||||
|
func New(db *database.Database) *AltAdvancement {
|
||||||
|
return &AltAdvancement{
|
||||||
|
CreatedAt: time.Now(),
|
||||||
|
UpdatedAt: time.Now(),
|
||||||
|
db: db,
|
||||||
|
isNew: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load loads an alternate advancement by node ID
|
||||||
|
func Load(db *database.Database, nodeID int32) (*AltAdvancement, error) {
|
||||||
|
aa := &AltAdvancement{
|
||||||
|
db: db,
|
||||||
|
isNew: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
query := `SELECT nodeid, minlevel, spellcrc, name, description, aa_list_fk,
|
||||||
|
icon_id, icon_backdrop, xcoord, ycoord, pointspertier, maxtier,
|
||||||
|
firstparentid, firstparentrequiredtier, displayedclassification,
|
||||||
|
requiredclassification, classificationpointsrequired,
|
||||||
|
pointsspentintreetounlock, title, titlelevel
|
||||||
|
FROM spell_aa_nodelist WHERE nodeid = ?`
|
||||||
|
|
||||||
|
err := db.QueryRow(query, nodeID).Scan(
|
||||||
|
&aa.NodeID,
|
||||||
|
&aa.MinLevel,
|
||||||
|
&aa.SpellCRC,
|
||||||
|
&aa.Name,
|
||||||
|
&aa.Description,
|
||||||
|
&aa.Group,
|
||||||
|
&aa.Icon,
|
||||||
|
&aa.Icon2,
|
||||||
|
&aa.Col,
|
||||||
|
&aa.Row,
|
||||||
|
&aa.RankCost,
|
||||||
|
&aa.MaxRank,
|
||||||
|
&aa.RankPrereqID,
|
||||||
|
&aa.RankPrereq,
|
||||||
|
&aa.ClassReq,
|
||||||
|
&aa.Tier,
|
||||||
|
&aa.ReqPoints,
|
||||||
|
&aa.ReqTreePoints,
|
||||||
|
&aa.LineTitle,
|
||||||
|
&aa.TitleLevel,
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
return nil, fmt.Errorf("alternate advancement not found: %d", nodeID)
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("failed to load alternate advancement: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set spell ID to node ID if not provided separately
|
||||||
|
aa.SpellID = aa.NodeID
|
||||||
|
|
||||||
|
return aa, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadAll loads all alternate advancements from database
|
||||||
|
func LoadAll(db *database.Database) ([]*AltAdvancement, error) {
|
||||||
|
query := `SELECT nodeid, minlevel, spellcrc, name, description, aa_list_fk,
|
||||||
|
icon_id, icon_backdrop, xcoord, ycoord, pointspertier, maxtier,
|
||||||
|
firstparentid, firstparentrequiredtier, displayedclassification,
|
||||||
|
requiredclassification, classificationpointsrequired,
|
||||||
|
pointsspentintreetounlock, title, titlelevel
|
||||||
|
FROM spell_aa_nodelist
|
||||||
|
ORDER BY aa_list_fk, ycoord, xcoord`
|
||||||
|
|
||||||
|
rows, err := db.Query(query)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to query alternate advancements: %w", err)
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
var aas []*AltAdvancement
|
||||||
|
|
||||||
|
for rows.Next() {
|
||||||
|
aa := &AltAdvancement{
|
||||||
|
db: db,
|
||||||
|
isNew: false,
|
||||||
|
CreatedAt: time.Now(),
|
||||||
|
UpdatedAt: time.Now(),
|
||||||
|
}
|
||||||
|
|
||||||
|
err := rows.Scan(
|
||||||
|
&aa.NodeID,
|
||||||
|
&aa.MinLevel,
|
||||||
|
&aa.SpellCRC,
|
||||||
|
&aa.Name,
|
||||||
|
&aa.Description,
|
||||||
|
&aa.Group,
|
||||||
|
&aa.Icon,
|
||||||
|
&aa.Icon2,
|
||||||
|
&aa.Col,
|
||||||
|
&aa.Row,
|
||||||
|
&aa.RankCost,
|
||||||
|
&aa.MaxRank,
|
||||||
|
&aa.RankPrereqID,
|
||||||
|
&aa.RankPrereq,
|
||||||
|
&aa.ClassReq,
|
||||||
|
&aa.Tier,
|
||||||
|
&aa.ReqPoints,
|
||||||
|
&aa.ReqTreePoints,
|
||||||
|
&aa.LineTitle,
|
||||||
|
&aa.TitleLevel,
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to scan alternate advancement: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set spell ID to node ID if not provided separately
|
||||||
|
aa.SpellID = aa.NodeID
|
||||||
|
|
||||||
|
aas = append(aas, aa)
|
||||||
|
}
|
||||||
|
|
||||||
|
return aas, rows.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save saves the alternate advancement to the database (insert if new, update if existing)
|
||||||
|
func (aa *AltAdvancement) Save() error {
|
||||||
|
if aa.db == nil {
|
||||||
|
return fmt.Errorf("no database connection")
|
||||||
|
}
|
||||||
|
|
||||||
|
tx, err := aa.db.Begin()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to begin transaction: %w", err)
|
||||||
|
}
|
||||||
|
defer tx.Rollback()
|
||||||
|
|
||||||
|
if aa.isNew {
|
||||||
|
err = aa.insert(tx)
|
||||||
|
} else {
|
||||||
|
err = aa.update(tx)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return tx.Commit()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete removes the alternate advancement from the database
|
||||||
|
func (aa *AltAdvancement) Delete() error {
|
||||||
|
if aa.db == nil {
|
||||||
|
return fmt.Errorf("no database connection")
|
||||||
|
}
|
||||||
|
|
||||||
|
if aa.isNew {
|
||||||
|
return fmt.Errorf("cannot delete unsaved alternate advancement")
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := aa.db.Exec("DELETE FROM spell_aa_nodelist WHERE nodeid = ?", aa.NodeID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to delete alternate advancement: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reload reloads the alternate advancement from the database
|
||||||
|
func (aa *AltAdvancement) Reload() error {
|
||||||
|
if aa.db == nil {
|
||||||
|
return fmt.Errorf("no database connection")
|
||||||
|
}
|
||||||
|
|
||||||
|
if aa.isNew {
|
||||||
|
return fmt.Errorf("cannot reload unsaved alternate advancement")
|
||||||
|
}
|
||||||
|
|
||||||
|
reloaded, err := Load(aa.db, aa.NodeID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy all fields from reloaded AA
|
||||||
|
*aa = *reloaded
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsNew returns true if this is a new (unsaved) alternate advancement
|
||||||
|
func (aa *AltAdvancement) IsNew() bool {
|
||||||
|
return aa.isNew
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetID returns the node ID (implements common.Identifiable interface)
|
||||||
|
func (aa *AltAdvancement) GetID() int32 {
|
||||||
|
return aa.NodeID
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsValid validates the alternate advancement data
|
||||||
|
func (aa *AltAdvancement) IsValid() bool {
|
||||||
|
return aa.SpellID > 0 &&
|
||||||
|
aa.NodeID > 0 &&
|
||||||
|
len(aa.Name) > 0 &&
|
||||||
|
aa.MaxRank > 0 &&
|
||||||
|
aa.RankCost > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clone creates a deep copy of the alternate advancement
|
||||||
|
func (aa *AltAdvancement) Clone() *AltAdvancement {
|
||||||
|
clone := &AltAdvancement{
|
||||||
|
SpellID: aa.SpellID,
|
||||||
|
NodeID: aa.NodeID,
|
||||||
|
SpellCRC: aa.SpellCRC,
|
||||||
|
Name: aa.Name,
|
||||||
|
Description: aa.Description,
|
||||||
|
Group: aa.Group,
|
||||||
|
Col: aa.Col,
|
||||||
|
Row: aa.Row,
|
||||||
|
Icon: aa.Icon,
|
||||||
|
Icon2: aa.Icon2,
|
||||||
|
RankCost: aa.RankCost,
|
||||||
|
MaxRank: aa.MaxRank,
|
||||||
|
MinLevel: aa.MinLevel,
|
||||||
|
RankPrereqID: aa.RankPrereqID,
|
||||||
|
RankPrereq: aa.RankPrereq,
|
||||||
|
ClassReq: aa.ClassReq,
|
||||||
|
Tier: aa.Tier,
|
||||||
|
ReqPoints: aa.ReqPoints,
|
||||||
|
ReqTreePoints: aa.ReqTreePoints,
|
||||||
|
ClassName: aa.ClassName,
|
||||||
|
SubclassName: aa.SubclassName,
|
||||||
|
LineTitle: aa.LineTitle,
|
||||||
|
TitleLevel: aa.TitleLevel,
|
||||||
|
CreatedAt: aa.CreatedAt,
|
||||||
|
UpdatedAt: aa.UpdatedAt,
|
||||||
|
db: aa.db,
|
||||||
|
isNew: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
return clone
|
||||||
|
}
|
||||||
|
|
||||||
|
// Private helper methods
|
||||||
|
|
||||||
|
func (aa *AltAdvancement) insert(tx *sql.Tx) error {
|
||||||
|
query := `INSERT INTO spell_aa_nodelist
|
||||||
|
(nodeid, minlevel, spellcrc, name, description, aa_list_fk,
|
||||||
|
icon_id, icon_backdrop, xcoord, ycoord, pointspertier, maxtier,
|
||||||
|
firstparentid, firstparentrequiredtier, displayedclassification,
|
||||||
|
requiredclassification, classificationpointsrequired,
|
||||||
|
pointsspentintreetounlock, title, titlelevel)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
||||||
|
|
||||||
|
_, err := tx.Exec(query,
|
||||||
|
aa.NodeID, aa.MinLevel, aa.SpellCRC, aa.Name, aa.Description, aa.Group,
|
||||||
|
aa.Icon, aa.Icon2, aa.Col, aa.Row, aa.RankCost, aa.MaxRank,
|
||||||
|
aa.RankPrereqID, aa.RankPrereq, aa.ClassReq, aa.Tier, aa.ReqPoints,
|
||||||
|
aa.ReqTreePoints, aa.LineTitle, aa.TitleLevel)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to insert alternate advancement: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
aa.isNew = false
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (aa *AltAdvancement) update(tx *sql.Tx) error {
|
||||||
|
query := `UPDATE spell_aa_nodelist SET
|
||||||
|
minlevel = ?, spellcrc = ?, name = ?, description = ?, aa_list_fk = ?,
|
||||||
|
icon_id = ?, icon_backdrop = ?, xcoord = ?, ycoord = ?, pointspertier = ?,
|
||||||
|
maxtier = ?, firstparentid = ?, firstparentrequiredtier = ?,
|
||||||
|
displayedclassification = ?, requiredclassification = ?,
|
||||||
|
classificationpointsrequired = ?, pointsspentintreetounlock = ?,
|
||||||
|
title = ?, titlelevel = ?
|
||||||
|
WHERE nodeid = ?`
|
||||||
|
|
||||||
|
_, err := tx.Exec(query,
|
||||||
|
aa.MinLevel, aa.SpellCRC, aa.Name, aa.Description, aa.Group,
|
||||||
|
aa.Icon, aa.Icon2, aa.Col, aa.Row, aa.RankCost, aa.MaxRank,
|
||||||
|
aa.RankPrereqID, aa.RankPrereq, aa.ClassReq, aa.Tier, aa.ReqPoints,
|
||||||
|
aa.ReqTreePoints, aa.LineTitle, aa.TitleLevel, aa.NodeID)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to update alternate advancement: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
142
internal/alt_advancement/alt_advancement_test.go
Normal file
142
internal/alt_advancement/alt_advancement_test.go
Normal file
@ -0,0 +1,142 @@
|
|||||||
|
package alt_advancement
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"eq2emu/internal/database"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestSimpleAltAdvancement tests the basic new AltAdvancement functionality
|
||||||
|
func TestSimpleAltAdvancement(t *testing.T) {
|
||||||
|
db, err := database.NewSQLite("file::memory:?mode=memory&cache=shared")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create test database: %v", err)
|
||||||
|
}
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
// Test creating a new alternate advancement
|
||||||
|
aa := New(db)
|
||||||
|
if aa == nil {
|
||||||
|
t.Fatal("New returned nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !aa.IsNew() {
|
||||||
|
t.Error("New AA should be marked as new")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test setting values
|
||||||
|
aa.SpellID = 1001
|
||||||
|
aa.NodeID = 1001
|
||||||
|
aa.Name = "Dragon's Strength"
|
||||||
|
aa.Group = AA_CLASS
|
||||||
|
aa.RankCost = 1
|
||||||
|
aa.MaxRank = 5
|
||||||
|
|
||||||
|
if aa.GetID() != 1001 {
|
||||||
|
t.Errorf("Expected GetID() to return 1001, got %d", aa.GetID())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test validation
|
||||||
|
if !aa.IsValid() {
|
||||||
|
t.Error("AA should be valid after setting required fields")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test Clone
|
||||||
|
clone := aa.Clone()
|
||||||
|
if clone == nil {
|
||||||
|
t.Fatal("Clone returned nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
if clone.NodeID != aa.NodeID {
|
||||||
|
t.Errorf("Expected clone ID %d, got %d", aa.NodeID, clone.NodeID)
|
||||||
|
}
|
||||||
|
|
||||||
|
if clone.Name != aa.Name {
|
||||||
|
t.Errorf("Expected clone name %s, got %s", aa.Name, clone.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure clone is not the same instance
|
||||||
|
if clone == aa {
|
||||||
|
t.Error("Clone should return a different instance")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestMasterListWithGeneric tests the master list with generic base
|
||||||
|
func TestMasterListWithGeneric(t *testing.T) {
|
||||||
|
masterList := NewMasterList()
|
||||||
|
|
||||||
|
if masterList == nil {
|
||||||
|
t.Fatal("NewMasterList returned nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
if masterList.Size() != 0 {
|
||||||
|
t.Errorf("Expected size 0, got %d", masterList.Size())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create an AA (need database for new pattern)
|
||||||
|
db, _ := database.NewSQLite("file::memory:?mode=memory&cache=shared")
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
aa := New(db)
|
||||||
|
aa.SpellID = 1001
|
||||||
|
aa.NodeID = 1001
|
||||||
|
aa.Name = "Dragon's Strength"
|
||||||
|
aa.Group = AA_CLASS
|
||||||
|
aa.RankCost = 1
|
||||||
|
aa.MaxRank = 5
|
||||||
|
|
||||||
|
// Test adding
|
||||||
|
if !masterList.AddAltAdvancement(aa) {
|
||||||
|
t.Error("Should successfully add alternate advancement")
|
||||||
|
}
|
||||||
|
|
||||||
|
if masterList.Size() != 1 {
|
||||||
|
t.Errorf("Expected size 1, got %d", masterList.Size())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test retrieving
|
||||||
|
retrieved := masterList.GetAltAdvancement(1001)
|
||||||
|
if retrieved == nil {
|
||||||
|
t.Error("Should retrieve added alternate advancement")
|
||||||
|
}
|
||||||
|
|
||||||
|
if retrieved.Name != "Dragon's Strength" {
|
||||||
|
t.Errorf("Expected name 'Dragon's Strength', got '%s'", retrieved.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test filtering
|
||||||
|
classAAs := masterList.GetAltAdvancementsByGroup(AA_CLASS)
|
||||||
|
if len(classAAs) != 1 {
|
||||||
|
t.Errorf("Expected 1 AA in Class group, got %d", len(classAAs))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestAltAdvancementValidation tests validation functionality
|
||||||
|
func TestAltAdvancementValidation(t *testing.T) {
|
||||||
|
db, _ := database.NewSQLite("file::memory:?mode=memory&cache=shared")
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
// Test valid AA
|
||||||
|
validAA := New(db)
|
||||||
|
validAA.SpellID = 100
|
||||||
|
validAA.NodeID = 100
|
||||||
|
validAA.Name = "Test AA"
|
||||||
|
validAA.RankCost = 1
|
||||||
|
validAA.MaxRank = 5
|
||||||
|
|
||||||
|
if !validAA.IsValid() {
|
||||||
|
t.Error("Valid AA should pass validation")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test invalid AA - missing name
|
||||||
|
invalidAA := New(db)
|
||||||
|
invalidAA.SpellID = 100
|
||||||
|
invalidAA.NodeID = 100
|
||||||
|
invalidAA.RankCost = 1
|
||||||
|
invalidAA.MaxRank = 5
|
||||||
|
// Name is empty
|
||||||
|
|
||||||
|
if invalidAA.IsValid() {
|
||||||
|
t.Error("Invalid AA (missing name) should fail validation")
|
||||||
|
}
|
||||||
|
}
|
@ -1,570 +0,0 @@
|
|||||||
package alt_advancement
|
|
||||||
|
|
||||||
import (
|
|
||||||
"sync"
|
|
||||||
"sync/atomic"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
// TestAAManagerConcurrentPlayerAccess tests concurrent access to player states
|
|
||||||
func TestAAManagerConcurrentPlayerAccess(t *testing.T) {
|
|
||||||
config := DefaultAAManagerConfig()
|
|
||||||
manager := NewAAManager(config)
|
|
||||||
|
|
||||||
// Set up mock database
|
|
||||||
mockDB := &mockAADatabase{}
|
|
||||||
manager.SetDatabase(mockDB)
|
|
||||||
|
|
||||||
// Test concurrent access to the same player
|
|
||||||
const numGoroutines = 100
|
|
||||||
const characterID = int32(123)
|
|
||||||
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
var successCount int64
|
|
||||||
|
|
||||||
// Launch multiple goroutines trying to get the same player state
|
|
||||||
for i := 0; i < numGoroutines; i++ {
|
|
||||||
wg.Add(1)
|
|
||||||
go func() {
|
|
||||||
defer wg.Done()
|
|
||||||
|
|
||||||
state, err := manager.GetPlayerAAState(characterID)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Failed to get player state: %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if state == nil {
|
|
||||||
t.Error("Got nil player state")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if state.CharacterID != characterID {
|
|
||||||
t.Errorf("Wrong character ID: expected %d, got %d", characterID, state.CharacterID)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
atomic.AddInt64(&successCount, 1)
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
wg.Wait()
|
|
||||||
|
|
||||||
if atomic.LoadInt64(&successCount) != numGoroutines {
|
|
||||||
t.Errorf("Expected %d successful operations, got %d", numGoroutines, successCount)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify only one instance was created in cache
|
|
||||||
manager.statesMutex.RLock()
|
|
||||||
cachedStates := len(manager.playerStates)
|
|
||||||
manager.statesMutex.RUnlock()
|
|
||||||
|
|
||||||
if cachedStates != 1 {
|
|
||||||
t.Errorf("Expected 1 cached state, got %d", cachedStates)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestAAManagerConcurrentMultiplePlayer tests concurrent access to different players
|
|
||||||
func TestAAManagerConcurrentMultiplePlayer(t *testing.T) {
|
|
||||||
config := DefaultAAManagerConfig()
|
|
||||||
manager := NewAAManager(config)
|
|
||||||
|
|
||||||
// Set up mock database
|
|
||||||
mockDB := &mockAADatabase{}
|
|
||||||
manager.SetDatabase(mockDB)
|
|
||||||
|
|
||||||
const numPlayers = 50
|
|
||||||
const goroutinesPerPlayer = 10
|
|
||||||
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
var successCount int64
|
|
||||||
|
|
||||||
// Launch multiple goroutines for different players
|
|
||||||
for playerID := int32(1); playerID <= numPlayers; playerID++ {
|
|
||||||
for j := 0; j < goroutinesPerPlayer; j++ {
|
|
||||||
wg.Add(1)
|
|
||||||
go func(id int32) {
|
|
||||||
defer wg.Done()
|
|
||||||
|
|
||||||
state, err := manager.GetPlayerAAState(id)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Failed to get player state for %d: %v", id, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if state == nil {
|
|
||||||
t.Errorf("Got nil player state for %d", id)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if state.CharacterID != id {
|
|
||||||
t.Errorf("Wrong character ID: expected %d, got %d", id, state.CharacterID)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
atomic.AddInt64(&successCount, 1)
|
|
||||||
}(playerID)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
wg.Wait()
|
|
||||||
|
|
||||||
expectedSuccess := int64(numPlayers * goroutinesPerPlayer)
|
|
||||||
if atomic.LoadInt64(&successCount) != expectedSuccess {
|
|
||||||
t.Errorf("Expected %d successful operations, got %d", expectedSuccess, successCount)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify correct number of cached states
|
|
||||||
manager.statesMutex.RLock()
|
|
||||||
cachedStates := len(manager.playerStates)
|
|
||||||
manager.statesMutex.RUnlock()
|
|
||||||
|
|
||||||
if cachedStates != numPlayers {
|
|
||||||
t.Errorf("Expected %d cached states, got %d", numPlayers, cachedStates)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestConcurrentAAPurchases tests concurrent AA purchases
|
|
||||||
func TestConcurrentAAPurchases(t *testing.T) {
|
|
||||||
config := DefaultAAManagerConfig()
|
|
||||||
manager := NewAAManager(config)
|
|
||||||
|
|
||||||
// Set up mock database
|
|
||||||
mockDB := &mockAADatabase{}
|
|
||||||
manager.SetDatabase(mockDB)
|
|
||||||
|
|
||||||
// Add test AAs
|
|
||||||
for i := 1; i <= 10; i++ {
|
|
||||||
aa := &AltAdvanceData{
|
|
||||||
SpellID: int32(i * 100),
|
|
||||||
NodeID: int32(i * 200),
|
|
||||||
Name: "Test AA",
|
|
||||||
Group: AA_CLASS,
|
|
||||||
MaxRank: 5,
|
|
||||||
RankCost: 1, // Low cost for testing
|
|
||||||
MinLevel: 1,
|
|
||||||
}
|
|
||||||
manager.masterAAList.AddAltAdvancement(aa)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get player state and give it points
|
|
||||||
state, err := manager.GetPlayerAAState(123)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to get player state: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Give player plenty of points
|
|
||||||
state.TotalPoints = 1000
|
|
||||||
state.AvailablePoints = 1000
|
|
||||||
|
|
||||||
const numGoroutines = 20
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
var successCount, errorCount int64
|
|
||||||
|
|
||||||
// Concurrent purchases
|
|
||||||
for i := 0; i < numGoroutines; i++ {
|
|
||||||
wg.Add(1)
|
|
||||||
go func(goroutineID int) {
|
|
||||||
defer wg.Done()
|
|
||||||
|
|
||||||
// Try to purchase different AAs
|
|
||||||
aaNodeID := int32(200 + (goroutineID%10)*200) // Spread across different AAs
|
|
||||||
|
|
||||||
err := manager.PurchaseAA(123, aaNodeID, 1)
|
|
||||||
if err != nil {
|
|
||||||
atomic.AddInt64(&errorCount, 1)
|
|
||||||
// Some errors expected due to race conditions or insufficient points
|
|
||||||
} else {
|
|
||||||
atomic.AddInt64(&successCount, 1)
|
|
||||||
}
|
|
||||||
}(i)
|
|
||||||
}
|
|
||||||
|
|
||||||
wg.Wait()
|
|
||||||
|
|
||||||
t.Logf("Successful purchases: %d, Errors: %d", successCount, errorCount)
|
|
||||||
|
|
||||||
// Verify final state consistency
|
|
||||||
state.mutex.RLock()
|
|
||||||
finalAvailable := state.AvailablePoints
|
|
||||||
finalSpent := state.SpentPoints
|
|
||||||
finalTotal := state.TotalPoints
|
|
||||||
numProgress := len(state.AAProgress)
|
|
||||||
state.mutex.RUnlock()
|
|
||||||
|
|
||||||
// Basic consistency checks
|
|
||||||
if finalAvailable+finalSpent != finalTotal {
|
|
||||||
t.Errorf("Point consistency check failed: available(%d) + spent(%d) != total(%d)",
|
|
||||||
finalAvailable, finalSpent, finalTotal)
|
|
||||||
}
|
|
||||||
|
|
||||||
if numProgress > int(successCount) {
|
|
||||||
t.Errorf("More progress entries (%d) than successful purchases (%d)", numProgress, successCount)
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Logf("Final state: Total=%d, Spent=%d, Available=%d, Progress entries=%d",
|
|
||||||
finalTotal, finalSpent, finalAvailable, numProgress)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestConcurrentAAPointAwarding tests concurrent point awarding
|
|
||||||
func TestConcurrentAAPointAwarding(t *testing.T) {
|
|
||||||
config := DefaultAAManagerConfig()
|
|
||||||
manager := NewAAManager(config)
|
|
||||||
|
|
||||||
// Set up mock database
|
|
||||||
mockDB := &mockAADatabase{}
|
|
||||||
manager.SetDatabase(mockDB)
|
|
||||||
|
|
||||||
const characterID = int32(123)
|
|
||||||
const numGoroutines = 100
|
|
||||||
const pointsPerAward = int32(10)
|
|
||||||
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
var successCount int64
|
|
||||||
|
|
||||||
// Concurrent point awarding
|
|
||||||
for i := 0; i < numGoroutines; i++ {
|
|
||||||
wg.Add(1)
|
|
||||||
go func(goroutineID int) {
|
|
||||||
defer wg.Done()
|
|
||||||
|
|
||||||
err := manager.AwardAAPoints(characterID, pointsPerAward, "Concurrent test")
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Failed to award points: %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
atomic.AddInt64(&successCount, 1)
|
|
||||||
}(i)
|
|
||||||
}
|
|
||||||
|
|
||||||
wg.Wait()
|
|
||||||
|
|
||||||
if atomic.LoadInt64(&successCount) != numGoroutines {
|
|
||||||
t.Errorf("Expected %d successful awards, got %d", numGoroutines, successCount)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify final point total
|
|
||||||
total, spent, available, err := manager.GetAAPoints(characterID)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to get AA points: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
expectedTotal := pointsPerAward * numGoroutines
|
|
||||||
if total != expectedTotal {
|
|
||||||
t.Errorf("Expected total points %d, got %d", expectedTotal, total)
|
|
||||||
}
|
|
||||||
|
|
||||||
if spent != 0 {
|
|
||||||
t.Errorf("Expected 0 spent points, got %d", spent)
|
|
||||||
}
|
|
||||||
|
|
||||||
if available != expectedTotal {
|
|
||||||
t.Errorf("Expected available points %d, got %d", expectedTotal, available)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestMasterAAListConcurrentOperations tests thread safety of MasterAAList
|
|
||||||
func TestMasterAAListConcurrentOperations(t *testing.T) {
|
|
||||||
masterList := NewMasterAAList()
|
|
||||||
|
|
||||||
// Pre-populate with some AAs
|
|
||||||
for i := 1; i <= 100; i++ {
|
|
||||||
aa := &AltAdvanceData{
|
|
||||||
SpellID: int32(i * 100),
|
|
||||||
NodeID: int32(i * 200),
|
|
||||||
Name: "Test AA",
|
|
||||||
Group: AA_CLASS,
|
|
||||||
MaxRank: 5,
|
|
||||||
RankCost: 2,
|
|
||||||
}
|
|
||||||
masterList.AddAltAdvancement(aa)
|
|
||||||
}
|
|
||||||
|
|
||||||
const numReaders = 50
|
|
||||||
const numWriters = 10
|
|
||||||
const operationsPerGoroutine = 100
|
|
||||||
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
var readOps, writeOps int64
|
|
||||||
|
|
||||||
// Reader goroutines
|
|
||||||
for i := 0; i < numReaders; i++ {
|
|
||||||
wg.Add(1)
|
|
||||||
go func() {
|
|
||||||
defer wg.Done()
|
|
||||||
|
|
||||||
for j := 0; j < operationsPerGoroutine; j++ {
|
|
||||||
// Mix different read operations
|
|
||||||
switch j % 5 {
|
|
||||||
case 0:
|
|
||||||
masterList.GetAltAdvancement(100)
|
|
||||||
case 1:
|
|
||||||
masterList.GetAltAdvancementByNodeID(200)
|
|
||||||
case 2:
|
|
||||||
masterList.GetAAsByGroup(AA_CLASS)
|
|
||||||
case 3:
|
|
||||||
masterList.Size()
|
|
||||||
case 4:
|
|
||||||
masterList.GetAllAAs()
|
|
||||||
}
|
|
||||||
atomic.AddInt64(&readOps, 1)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Writer goroutines (adding new AAs)
|
|
||||||
for i := 0; i < numWriters; i++ {
|
|
||||||
wg.Add(1)
|
|
||||||
go func(writerID int) {
|
|
||||||
defer wg.Done()
|
|
||||||
|
|
||||||
for j := 0; j < operationsPerGoroutine; j++ {
|
|
||||||
// Create unique AAs for each writer
|
|
||||||
baseID := (writerID + 1000) * 1000 + j
|
|
||||||
aa := &AltAdvanceData{
|
|
||||||
SpellID: int32(baseID),
|
|
||||||
NodeID: int32(baseID + 100000),
|
|
||||||
Name: "Concurrent AA",
|
|
||||||
Group: AA_CLASS,
|
|
||||||
MaxRank: 5,
|
|
||||||
RankCost: 2,
|
|
||||||
}
|
|
||||||
|
|
||||||
err := masterList.AddAltAdvancement(aa)
|
|
||||||
if err != nil {
|
|
||||||
// Some errors expected due to potential duplicates
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
atomic.AddInt64(&writeOps, 1)
|
|
||||||
}
|
|
||||||
}(i)
|
|
||||||
}
|
|
||||||
|
|
||||||
wg.Wait()
|
|
||||||
|
|
||||||
t.Logf("Read operations: %d, Write operations: %d", readOps, writeOps)
|
|
||||||
|
|
||||||
// Verify final state
|
|
||||||
finalSize := masterList.Size()
|
|
||||||
if finalSize < 100 {
|
|
||||||
t.Errorf("Expected at least 100 AAs, got %d", finalSize)
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Logf("Final AA count: %d", finalSize)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestMasterAANodeListConcurrentOperations tests thread safety of MasterAANodeList
|
|
||||||
func TestMasterAANodeListConcurrentOperations(t *testing.T) {
|
|
||||||
nodeList := NewMasterAANodeList()
|
|
||||||
|
|
||||||
// Pre-populate with some nodes
|
|
||||||
for i := 1; i <= 50; i++ {
|
|
||||||
node := &TreeNodeData{
|
|
||||||
ClassID: int32(i % 10 + 1), // Classes 1-10
|
|
||||||
TreeID: int32(i * 100),
|
|
||||||
AATreeID: int32(i * 200),
|
|
||||||
}
|
|
||||||
nodeList.AddTreeNode(node)
|
|
||||||
}
|
|
||||||
|
|
||||||
const numReaders = 30
|
|
||||||
const numWriters = 5
|
|
||||||
const operationsPerGoroutine = 100
|
|
||||||
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
var readOps, writeOps int64
|
|
||||||
|
|
||||||
// Reader goroutines
|
|
||||||
for i := 0; i < numReaders; i++ {
|
|
||||||
wg.Add(1)
|
|
||||||
go func() {
|
|
||||||
defer wg.Done()
|
|
||||||
|
|
||||||
for j := 0; j < operationsPerGoroutine; j++ {
|
|
||||||
// Mix different read operations
|
|
||||||
switch j % 4 {
|
|
||||||
case 0:
|
|
||||||
nodeList.GetTreeNode(100)
|
|
||||||
case 1:
|
|
||||||
nodeList.GetTreeNodesByClass(1)
|
|
||||||
case 2:
|
|
||||||
nodeList.Size()
|
|
||||||
case 3:
|
|
||||||
nodeList.GetTreeNodes()
|
|
||||||
}
|
|
||||||
atomic.AddInt64(&readOps, 1)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Writer goroutines
|
|
||||||
for i := 0; i < numWriters; i++ {
|
|
||||||
wg.Add(1)
|
|
||||||
go func(writerID int) {
|
|
||||||
defer wg.Done()
|
|
||||||
|
|
||||||
for j := 0; j < operationsPerGoroutine; j++ {
|
|
||||||
// Create unique nodes for each writer
|
|
||||||
baseID := (writerID + 1000) * 1000 + j
|
|
||||||
node := &TreeNodeData{
|
|
||||||
ClassID: int32(writerID%5 + 1),
|
|
||||||
TreeID: int32(baseID),
|
|
||||||
AATreeID: int32(baseID + 100000),
|
|
||||||
}
|
|
||||||
|
|
||||||
err := nodeList.AddTreeNode(node)
|
|
||||||
if err != nil {
|
|
||||||
// Some errors expected due to potential duplicates
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
atomic.AddInt64(&writeOps, 1)
|
|
||||||
}
|
|
||||||
}(i)
|
|
||||||
}
|
|
||||||
|
|
||||||
wg.Wait()
|
|
||||||
|
|
||||||
t.Logf("Read operations: %d, Write operations: %d", readOps, writeOps)
|
|
||||||
|
|
||||||
// Verify final state
|
|
||||||
finalSize := nodeList.Size()
|
|
||||||
if finalSize < 50 {
|
|
||||||
t.Errorf("Expected at least 50 nodes, got %d", finalSize)
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Logf("Final node count: %d", finalSize)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestAAPlayerStateConcurrentAccess tests thread safety of AAPlayerState
|
|
||||||
func TestAAPlayerStateConcurrentAccess(t *testing.T) {
|
|
||||||
playerState := NewAAPlayerState(123)
|
|
||||||
|
|
||||||
// Give player some initial points
|
|
||||||
playerState.TotalPoints = 1000
|
|
||||||
playerState.AvailablePoints = 1000
|
|
||||||
|
|
||||||
const numGoroutines = 100
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
|
|
||||||
// Concurrent operations on player state
|
|
||||||
for i := 0; i < numGoroutines; i++ {
|
|
||||||
wg.Add(1)
|
|
||||||
go func(goroutineID int) {
|
|
||||||
defer wg.Done()
|
|
||||||
|
|
||||||
// Mix of different operations
|
|
||||||
switch goroutineID % 4 {
|
|
||||||
case 0:
|
|
||||||
// Add AA progress
|
|
||||||
progress := &PlayerAAData{
|
|
||||||
CharacterID: 123,
|
|
||||||
NodeID: int32(goroutineID + 1000),
|
|
||||||
CurrentRank: 1,
|
|
||||||
PointsSpent: 2,
|
|
||||||
}
|
|
||||||
playerState.AddAAProgress(progress)
|
|
||||||
|
|
||||||
case 1:
|
|
||||||
// Update points
|
|
||||||
playerState.UpdatePoints(1000, int32(goroutineID), 0)
|
|
||||||
|
|
||||||
case 2:
|
|
||||||
// Get AA progress
|
|
||||||
playerState.GetAAProgress(int32(goroutineID + 1000))
|
|
||||||
|
|
||||||
case 3:
|
|
||||||
// Calculate spent points
|
|
||||||
playerState.CalculateSpentPoints()
|
|
||||||
}
|
|
||||||
}(i)
|
|
||||||
}
|
|
||||||
|
|
||||||
wg.Wait()
|
|
||||||
|
|
||||||
// Verify state is still consistent
|
|
||||||
playerState.mutex.RLock()
|
|
||||||
totalPoints := playerState.TotalPoints
|
|
||||||
progressCount := len(playerState.AAProgress)
|
|
||||||
playerState.mutex.RUnlock()
|
|
||||||
|
|
||||||
if totalPoints != 1000 {
|
|
||||||
t.Errorf("Expected total points to remain 1000, got %d", totalPoints)
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Logf("Final progress entries: %d", progressCount)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestConcurrentSystemOperations tests mixed system operations
|
|
||||||
func TestConcurrentSystemOperations(t *testing.T) {
|
|
||||||
config := DefaultAAManagerConfig()
|
|
||||||
manager := NewAAManager(config)
|
|
||||||
|
|
||||||
// Set up mock database
|
|
||||||
mockDB := &mockAADatabase{}
|
|
||||||
manager.SetDatabase(mockDB)
|
|
||||||
|
|
||||||
// Add some test AAs
|
|
||||||
for i := 1; i <= 20; i++ {
|
|
||||||
aa := &AltAdvanceData{
|
|
||||||
SpellID: int32(i * 100),
|
|
||||||
NodeID: int32(i * 200),
|
|
||||||
Name: "Test AA",
|
|
||||||
Group: int8(i % 3), // Mix groups
|
|
||||||
MaxRank: 5,
|
|
||||||
RankCost: 2,
|
|
||||||
MinLevel: 1,
|
|
||||||
}
|
|
||||||
manager.masterAAList.AddAltAdvancement(aa)
|
|
||||||
}
|
|
||||||
|
|
||||||
const numGoroutines = 50
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
var operations int64
|
|
||||||
|
|
||||||
for i := 0; i < numGoroutines; i++ {
|
|
||||||
wg.Add(1)
|
|
||||||
go func(goroutineID int) {
|
|
||||||
defer wg.Done()
|
|
||||||
|
|
||||||
playerID := int32(goroutineID%10 + 1) // 10 different players
|
|
||||||
|
|
||||||
// Mix of operations
|
|
||||||
switch goroutineID % 6 {
|
|
||||||
case 0:
|
|
||||||
// Get player state
|
|
||||||
manager.GetPlayerAAState(playerID)
|
|
||||||
|
|
||||||
case 1:
|
|
||||||
// Award points
|
|
||||||
manager.AwardAAPoints(playerID, 50, "Test")
|
|
||||||
|
|
||||||
case 2:
|
|
||||||
// Get AA points
|
|
||||||
manager.GetAAPoints(playerID)
|
|
||||||
|
|
||||||
case 3:
|
|
||||||
// Get AAs by group
|
|
||||||
manager.GetAAsByGroup(AA_CLASS)
|
|
||||||
|
|
||||||
case 4:
|
|
||||||
// Get system stats
|
|
||||||
manager.GetSystemStats()
|
|
||||||
|
|
||||||
case 5:
|
|
||||||
// Try to purchase AA (might fail, that's ok)
|
|
||||||
manager.PurchaseAA(playerID, 200, 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
atomic.AddInt64(&operations, 1)
|
|
||||||
}(i)
|
|
||||||
}
|
|
||||||
|
|
||||||
wg.Wait()
|
|
||||||
|
|
||||||
if atomic.LoadInt64(&operations) != numGoroutines {
|
|
||||||
t.Errorf("Expected %d operations, got %d", numGoroutines, operations)
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Logf("Completed %d concurrent system operations", operations)
|
|
||||||
}
|
|
@ -1,564 +0,0 @@
|
|||||||
package alt_advancement
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
// LoadAltAdvancements loads all AA definitions from the database
|
|
||||||
func (db *DatabaseImpl) LoadAltAdvancements() error {
|
|
||||||
query := `
|
|
||||||
SELECT nodeid, minlevel, spellcrc, name, description, aa_list_fk,
|
|
||||||
icon_id, icon_backdrop, xcoord, ycoord, pointspertier, maxtier,
|
|
||||||
firstparentid, firstparentrequiredtier, displayedclassification,
|
|
||||||
requiredclassification, classificationpointsrequired,
|
|
||||||
pointsspentintreetounlock, title, titlelevel
|
|
||||||
FROM spell_aa_nodelist
|
|
||||||
ORDER BY aa_list_fk, ycoord, xcoord`
|
|
||||||
|
|
||||||
rows, err := db.db.Query(query)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to query AA data: %v", err)
|
|
||||||
}
|
|
||||||
defer rows.Close()
|
|
||||||
|
|
||||||
loadedCount := 0
|
|
||||||
for rows.Next() {
|
|
||||||
data := &AltAdvanceData{
|
|
||||||
CreatedAt: time.Now(),
|
|
||||||
UpdatedAt: time.Now(),
|
|
||||||
}
|
|
||||||
|
|
||||||
err := rows.Scan(
|
|
||||||
&data.NodeID,
|
|
||||||
&data.MinLevel,
|
|
||||||
&data.SpellCRC,
|
|
||||||
&data.Name,
|
|
||||||
&data.Description,
|
|
||||||
&data.Group,
|
|
||||||
&data.Icon,
|
|
||||||
&data.Icon2,
|
|
||||||
&data.Col,
|
|
||||||
&data.Row,
|
|
||||||
&data.RankCost,
|
|
||||||
&data.MaxRank,
|
|
||||||
&data.RankPrereqID,
|
|
||||||
&data.RankPrereq,
|
|
||||||
&data.ClassReq,
|
|
||||||
&data.Tier,
|
|
||||||
&data.ReqPoints,
|
|
||||||
&data.ReqTreePoints,
|
|
||||||
&data.LineTitle,
|
|
||||||
&data.TitleLevel,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to scan AA data: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set spell ID to node ID if not provided separately
|
|
||||||
data.SpellID = data.NodeID
|
|
||||||
|
|
||||||
// Validate and add to master list
|
|
||||||
if err := db.masterAAList.AddAltAdvancement(data); err != nil {
|
|
||||||
// Log warning but continue loading
|
|
||||||
if db.logger != nil {
|
|
||||||
db.logger.Printf("Warning: failed to add AA node %d: %v", data.NodeID, err)
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
loadedCount++
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = rows.Err(); err != nil {
|
|
||||||
return fmt.Errorf("error iterating AA rows: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sort AAs within each group for proper display order
|
|
||||||
db.masterAAList.SortAAsByGroup()
|
|
||||||
|
|
||||||
if db.logger != nil {
|
|
||||||
db.logger.Printf("Loaded %d Alternate Advancement(s)", loadedCount)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// LoadTreeNodes loads tree node configurations from the database
|
|
||||||
func (db *DatabaseImpl) LoadTreeNodes() error {
|
|
||||||
query := `
|
|
||||||
SELECT class_id, tree_node, aa_tree_id
|
|
||||||
FROM spell_aa_class_list
|
|
||||||
ORDER BY class_id, tree_node`
|
|
||||||
|
|
||||||
rows, err := db.db.Query(query)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to query tree node data: %v", err)
|
|
||||||
}
|
|
||||||
defer rows.Close()
|
|
||||||
|
|
||||||
loadedCount := 0
|
|
||||||
for rows.Next() {
|
|
||||||
data := &TreeNodeData{}
|
|
||||||
|
|
||||||
err := rows.Scan(
|
|
||||||
&data.ClassID,
|
|
||||||
&data.TreeID,
|
|
||||||
&data.AATreeID,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to scan tree node data: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add to master node list
|
|
||||||
if err := db.masterNodeList.AddTreeNode(data); err != nil {
|
|
||||||
// Log warning but continue loading
|
|
||||||
if db.logger != nil {
|
|
||||||
db.logger.Printf("Warning: failed to add tree node %d: %v", data.TreeID, err)
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
loadedCount++
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = rows.Err(); err != nil {
|
|
||||||
return fmt.Errorf("error iterating tree node rows: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if db.logger != nil {
|
|
||||||
db.logger.Printf("Loaded %d AA Tree Nodes", loadedCount)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// LoadPlayerAA loads AA data for a specific player
|
|
||||||
func (db *DatabaseImpl) LoadPlayerAA(characterID int32) (*AAPlayerState, error) {
|
|
||||||
playerState := NewAAPlayerState(characterID)
|
|
||||||
|
|
||||||
// Load player's AA entries
|
|
||||||
query := `
|
|
||||||
SELECT template_id, tab_id, aa_id, order, treeid
|
|
||||||
FROM character_aa
|
|
||||||
WHERE char_id = ?
|
|
||||||
ORDER BY template_id, tab_id, order`
|
|
||||||
|
|
||||||
rows, err := db.db.Query(query, characterID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to query player AA data: %v", err)
|
|
||||||
}
|
|
||||||
defer rows.Close()
|
|
||||||
|
|
||||||
// Group entries by template
|
|
||||||
templateEntries := make(map[int8][]*AAEntry)
|
|
||||||
|
|
||||||
for rows.Next() {
|
|
||||||
entry := &AAEntry{}
|
|
||||||
|
|
||||||
err := rows.Scan(
|
|
||||||
&entry.TemplateID,
|
|
||||||
&entry.TabID,
|
|
||||||
&entry.AAID,
|
|
||||||
&entry.Order,
|
|
||||||
&entry.TreeID,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to scan player AA entry: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if templateEntries[entry.TemplateID] == nil {
|
|
||||||
templateEntries[entry.TemplateID] = make([]*AAEntry, 0)
|
|
||||||
}
|
|
||||||
templateEntries[entry.TemplateID] = append(templateEntries[entry.TemplateID], entry)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = rows.Err(); err != nil {
|
|
||||||
return nil, fmt.Errorf("error iterating player AA rows: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create templates from loaded entries
|
|
||||||
for templateID, entries := range templateEntries {
|
|
||||||
template := NewAATemplate(templateID, GetTemplateName(templateID))
|
|
||||||
template.Entries = entries
|
|
||||||
playerState.Templates[templateID] = template
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load player's AA progression data
|
|
||||||
err = db.loadPlayerAAProgress(characterID, playerState)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to load player AA progress: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load player's AA point totals
|
|
||||||
err = db.loadPlayerAAPoints(characterID, playerState)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to load player AA points: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize tabs based on loaded data
|
|
||||||
db.initializePlayerTabs(playerState)
|
|
||||||
|
|
||||||
return playerState, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// loadPlayerAAProgress loads detailed AA progression for a player
|
|
||||||
func (db *DatabaseImpl) loadPlayerAAProgress(characterID int32, playerState *AAPlayerState) error {
|
|
||||||
query := `
|
|
||||||
SELECT node_id, current_rank, points_spent, template_id, tab_id,
|
|
||||||
purchased_at, updated_at
|
|
||||||
FROM character_aa_progress
|
|
||||||
WHERE character_id = ?`
|
|
||||||
|
|
||||||
rows, err := db.db.Query(query, characterID)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to query player AA progress: %v", err)
|
|
||||||
}
|
|
||||||
defer rows.Close()
|
|
||||||
|
|
||||||
for rows.Next() {
|
|
||||||
progress := &PlayerAAData{
|
|
||||||
CharacterID: characterID,
|
|
||||||
}
|
|
||||||
|
|
||||||
var purchasedAt, updatedAt string
|
|
||||||
err := rows.Scan(
|
|
||||||
&progress.NodeID,
|
|
||||||
&progress.CurrentRank,
|
|
||||||
&progress.PointsSpent,
|
|
||||||
&progress.TemplateID,
|
|
||||||
&progress.TabID,
|
|
||||||
&purchasedAt,
|
|
||||||
&updatedAt,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to scan player AA progress: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse timestamps
|
|
||||||
if progress.PurchasedAt, err = time.Parse("2006-01-02 15:04:05", purchasedAt); err != nil {
|
|
||||||
progress.PurchasedAt = time.Now()
|
|
||||||
}
|
|
||||||
if progress.UpdatedAt, err = time.Parse("2006-01-02 15:04:05", updatedAt); err != nil {
|
|
||||||
progress.UpdatedAt = time.Now()
|
|
||||||
}
|
|
||||||
|
|
||||||
playerState.AAProgress[progress.NodeID] = progress
|
|
||||||
}
|
|
||||||
|
|
||||||
return rows.Err()
|
|
||||||
}
|
|
||||||
|
|
||||||
// loadPlayerAAPoints loads AA point totals for a player
|
|
||||||
func (db *DatabaseImpl) loadPlayerAAPoints(characterID int32, playerState *AAPlayerState) error {
|
|
||||||
query := `
|
|
||||||
SELECT total_points, spent_points, available_points, banked_points,
|
|
||||||
active_template
|
|
||||||
FROM character_aa_points
|
|
||||||
WHERE character_id = ?`
|
|
||||||
|
|
||||||
row := db.db.QueryRow(query, characterID)
|
|
||||||
|
|
||||||
err := row.Scan(
|
|
||||||
&playerState.TotalPoints,
|
|
||||||
&playerState.SpentPoints,
|
|
||||||
&playerState.AvailablePoints,
|
|
||||||
&playerState.BankedPoints,
|
|
||||||
&playerState.ActiveTemplate,
|
|
||||||
)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
// If no record exists, initialize with defaults
|
|
||||||
if err.Error() == "sql: no rows in result set" {
|
|
||||||
playerState.TotalPoints = 0
|
|
||||||
playerState.SpentPoints = 0
|
|
||||||
playerState.AvailablePoints = 0
|
|
||||||
playerState.BankedPoints = 0
|
|
||||||
playerState.ActiveTemplate = AA_TEMPLATE_CURRENT
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return fmt.Errorf("failed to load player AA points: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// initializePlayerTabs initializes tab states based on loaded data
|
|
||||||
func (db *DatabaseImpl) initializePlayerTabs(playerState *AAPlayerState) {
|
|
||||||
// Initialize all standard tabs
|
|
||||||
for i := int8(0); i < 10; i++ {
|
|
||||||
tab := NewAATab(i, i, GetTabName(i))
|
|
||||||
tab.MaxAA = GetMaxAAForTab(i)
|
|
||||||
|
|
||||||
// Calculate points spent in this tab
|
|
||||||
pointsSpent := int32(0)
|
|
||||||
for _, progress := range playerState.AAProgress {
|
|
||||||
if progress.TabID == i {
|
|
||||||
pointsSpent += progress.PointsSpent
|
|
||||||
}
|
|
||||||
}
|
|
||||||
tab.PointsSpent = pointsSpent
|
|
||||||
tab.PointsAvailable = playerState.AvailablePoints
|
|
||||||
|
|
||||||
playerState.Tabs[i] = tab
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// SavePlayerAA saves a player's AA data to the database
|
|
||||||
func (db *DatabaseImpl) SavePlayerAA(playerState *AAPlayerState) error {
|
|
||||||
if playerState == nil {
|
|
||||||
return fmt.Errorf("player state cannot be nil")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start transaction
|
|
||||||
tx, err := db.db.Begin()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to begin transaction: %v", err)
|
|
||||||
}
|
|
||||||
defer tx.Rollback()
|
|
||||||
|
|
||||||
// Save AA point totals
|
|
||||||
err = db.savePlayerAAPoints(tx, playerState)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to save player AA points: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save AA progress
|
|
||||||
err = db.savePlayerAAProgress(tx, playerState)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to save player AA progress: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save template entries
|
|
||||||
err = db.savePlayerAATemplates(tx, playerState)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to save player AA templates: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Commit transaction
|
|
||||||
if err = tx.Commit(); err != nil {
|
|
||||||
return fmt.Errorf("failed to commit transaction: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update last save time
|
|
||||||
playerState.lastUpdate = time.Now()
|
|
||||||
playerState.needsSync = false
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// savePlayerAAPoints saves AA point totals to the database
|
|
||||||
func (db *DatabaseImpl) savePlayerAAPoints(tx Transaction, playerState *AAPlayerState) error {
|
|
||||||
query := `
|
|
||||||
INSERT OR REPLACE INTO character_aa_points
|
|
||||||
(character_id, total_points, spent_points, available_points,
|
|
||||||
banked_points, active_template, updated_at)
|
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?)`
|
|
||||||
|
|
||||||
_, err := tx.Exec(query,
|
|
||||||
playerState.CharacterID,
|
|
||||||
playerState.TotalPoints,
|
|
||||||
playerState.SpentPoints,
|
|
||||||
playerState.AvailablePoints,
|
|
||||||
playerState.BankedPoints,
|
|
||||||
playerState.ActiveTemplate,
|
|
||||||
time.Now().Format("2006-01-02 15:04:05"),
|
|
||||||
)
|
|
||||||
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// savePlayerAAProgress saves AA progression data to the database
|
|
||||||
func (db *DatabaseImpl) savePlayerAAProgress(tx Transaction, playerState *AAPlayerState) error {
|
|
||||||
// Delete existing progress
|
|
||||||
_, err := tx.Exec("DELETE FROM character_aa_progress WHERE character_id = ?", playerState.CharacterID)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Insert current progress
|
|
||||||
query := `
|
|
||||||
INSERT INTO character_aa_progress
|
|
||||||
(character_id, node_id, current_rank, points_spent, template_id,
|
|
||||||
tab_id, purchased_at, updated_at)
|
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
|
|
||||||
|
|
||||||
for _, progress := range playerState.AAProgress {
|
|
||||||
_, err = tx.Exec(query,
|
|
||||||
progress.CharacterID,
|
|
||||||
progress.NodeID,
|
|
||||||
progress.CurrentRank,
|
|
||||||
progress.PointsSpent,
|
|
||||||
progress.TemplateID,
|
|
||||||
progress.TabID,
|
|
||||||
progress.PurchasedAt.Format("2006-01-02 15:04:05"),
|
|
||||||
progress.UpdatedAt.Format("2006-01-02 15:04:05"),
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// savePlayerAATemplates saves AA template entries to the database
|
|
||||||
func (db *DatabaseImpl) savePlayerAATemplates(tx Transaction, playerState *AAPlayerState) error {
|
|
||||||
// Delete existing entries for server templates (4-6)
|
|
||||||
_, err := tx.Exec("DELETE FROM character_aa WHERE char_id = ? AND template_id BETWEEN 4 AND 6", playerState.CharacterID)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Insert current template entries for server templates only
|
|
||||||
query := `
|
|
||||||
INSERT INTO character_aa
|
|
||||||
(char_id, template_id, tab_id, aa_id, order, treeid)
|
|
||||||
VALUES (?, ?, ?, ?, ?, ?)`
|
|
||||||
|
|
||||||
for _, template := range playerState.Templates {
|
|
||||||
// Only save server templates (4-6) as personal templates (1-3) are class defaults
|
|
||||||
if template.TemplateID >= 4 && template.TemplateID <= 6 {
|
|
||||||
for _, entry := range template.Entries {
|
|
||||||
_, err = tx.Exec(query,
|
|
||||||
playerState.CharacterID,
|
|
||||||
entry.TemplateID,
|
|
||||||
entry.TabID,
|
|
||||||
entry.AAID,
|
|
||||||
entry.Order,
|
|
||||||
entry.TreeID,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// LoadPlayerAADefaults loads default AA templates for a class
|
|
||||||
func (db *DatabaseImpl) LoadPlayerAADefaults(classID int8) (map[int8][]*AAEntry, error) {
|
|
||||||
query := `
|
|
||||||
SELECT template_id, tab_id, aa_id, order, treeid
|
|
||||||
FROM character_aa_defaults
|
|
||||||
WHERE class = ?
|
|
||||||
ORDER BY template_id, tab_id, order`
|
|
||||||
|
|
||||||
rows, err := db.db.Query(query, classID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to query AA defaults: %v", err)
|
|
||||||
}
|
|
||||||
defer rows.Close()
|
|
||||||
|
|
||||||
templates := make(map[int8][]*AAEntry)
|
|
||||||
|
|
||||||
for rows.Next() {
|
|
||||||
entry := &AAEntry{}
|
|
||||||
|
|
||||||
err := rows.Scan(
|
|
||||||
&entry.TemplateID,
|
|
||||||
&entry.TabID,
|
|
||||||
&entry.AAID,
|
|
||||||
&entry.Order,
|
|
||||||
&entry.TreeID,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to scan AA default entry: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if templates[entry.TemplateID] == nil {
|
|
||||||
templates[entry.TemplateID] = make([]*AAEntry, 0)
|
|
||||||
}
|
|
||||||
templates[entry.TemplateID] = append(templates[entry.TemplateID], entry)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = rows.Err(); err != nil {
|
|
||||||
return nil, fmt.Errorf("error iterating AA default rows: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return templates, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeletePlayerAA removes all AA data for a player
|
|
||||||
func (db *DatabaseImpl) DeletePlayerAA(characterID int32) error {
|
|
||||||
// Start transaction
|
|
||||||
tx, err := db.db.Begin()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to begin transaction: %v", err)
|
|
||||||
}
|
|
||||||
defer tx.Rollback()
|
|
||||||
|
|
||||||
// Delete from all related tables
|
|
||||||
tables := []string{
|
|
||||||
"character_aa_points",
|
|
||||||
"character_aa_progress",
|
|
||||||
"character_aa",
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, table := range tables {
|
|
||||||
query := fmt.Sprintf("DELETE FROM %s WHERE char_id = ? OR character_id = ?", table)
|
|
||||||
_, err = tx.Exec(query, characterID, characterID)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to delete from %s: %v", table, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Commit transaction
|
|
||||||
if err = tx.Commit(); err != nil {
|
|
||||||
return fmt.Errorf("failed to commit transaction: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetAAStatistics returns statistics about AA usage
|
|
||||||
func (db *DatabaseImpl) GetAAStatistics() (map[string]any, error) {
|
|
||||||
stats := make(map[string]any)
|
|
||||||
|
|
||||||
// Get total players with AA data
|
|
||||||
var totalPlayers int64
|
|
||||||
err := db.db.QueryRow("SELECT COUNT(DISTINCT character_id) FROM character_aa_points").Scan(&totalPlayers)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to get total players: %v", err)
|
|
||||||
}
|
|
||||||
stats["total_players_with_aa"] = totalPlayers
|
|
||||||
|
|
||||||
// Get average points spent
|
|
||||||
var avgPointsSpent float64
|
|
||||||
err = db.db.QueryRow("SELECT AVG(spent_points) FROM character_aa_points").Scan(&avgPointsSpent)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to get average points spent: %v", err)
|
|
||||||
}
|
|
||||||
stats["average_points_spent"] = avgPointsSpent
|
|
||||||
|
|
||||||
// Get most popular AAs
|
|
||||||
query := `
|
|
||||||
SELECT node_id, COUNT(*) as usage_count
|
|
||||||
FROM character_aa_progress
|
|
||||||
WHERE current_rank > 0
|
|
||||||
GROUP BY node_id
|
|
||||||
ORDER BY usage_count DESC
|
|
||||||
LIMIT 10`
|
|
||||||
|
|
||||||
rows, err := db.db.Query(query)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to query popular AAs: %v", err)
|
|
||||||
}
|
|
||||||
defer rows.Close()
|
|
||||||
|
|
||||||
popularAAs := make(map[int32]int64)
|
|
||||||
for rows.Next() {
|
|
||||||
var nodeID int32
|
|
||||||
var count int64
|
|
||||||
err := rows.Scan(&nodeID, &count)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to scan popular AA: %v", err)
|
|
||||||
}
|
|
||||||
popularAAs[nodeID] = count
|
|
||||||
}
|
|
||||||
stats["popular_aas"] = popularAAs
|
|
||||||
|
|
||||||
return stats, nil
|
|
||||||
}
|
|
80
internal/alt_advancement/doc.go
Normal file
80
internal/alt_advancement/doc.go
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
// Package alt_advancement provides a complete alternate advancement system for EQ2Go servers.
|
||||||
|
//
|
||||||
|
// Features:
|
||||||
|
// - Alternate advancement definitions with requirements and rewards
|
||||||
|
// - Thread-safe master AA list for server-wide management
|
||||||
|
// - Player-specific AA tracking and progression
|
||||||
|
// - Database operations with internal database wrapper
|
||||||
|
//
|
||||||
|
// Basic Usage:
|
||||||
|
//
|
||||||
|
// // Create database connection
|
||||||
|
// db, _ := database.NewSQLite("world.db")
|
||||||
|
//
|
||||||
|
// // Create new alternate advancement
|
||||||
|
// aa := alt_advancement.New(db)
|
||||||
|
// aa.NodeID = 1001
|
||||||
|
// aa.Name = "Dragon's Strength"
|
||||||
|
// aa.Group = alt_advancement.AA_CLASS
|
||||||
|
// aa.RankCost = 1
|
||||||
|
// aa.MaxRank = 5
|
||||||
|
//
|
||||||
|
// // Save to database (insert or update automatically)
|
||||||
|
// aa.Save()
|
||||||
|
//
|
||||||
|
// // Load alternate advancement by node ID
|
||||||
|
// loaded, _ := alt_advancement.Load(db, 1001)
|
||||||
|
//
|
||||||
|
// // Update and save
|
||||||
|
// loaded.Name = "Improved Dragon's Strength"
|
||||||
|
// loaded.Save()
|
||||||
|
//
|
||||||
|
// // Delete alternate advancement
|
||||||
|
// loaded.Delete()
|
||||||
|
//
|
||||||
|
// Master List Management:
|
||||||
|
//
|
||||||
|
// // Create master list for server-wide AA management
|
||||||
|
// masterList := alt_advancement.NewMasterList()
|
||||||
|
//
|
||||||
|
// // Load all AAs from database
|
||||||
|
// allAAs, _ := alt_advancement.LoadAll(db)
|
||||||
|
// for _, aa := range allAAs {
|
||||||
|
// masterList.AddAltAdvancement(aa)
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// // Get AAs by group/tab
|
||||||
|
// classAAs := masterList.GetAltAdvancementsByGroup(alt_advancement.AA_CLASS)
|
||||||
|
//
|
||||||
|
// // Get AAs by class requirement
|
||||||
|
// fighterAAs := masterList.GetAltAdvancementsByClass(1) // Fighter class
|
||||||
|
//
|
||||||
|
// // Get AAs available at specific level
|
||||||
|
// level20AAs := masterList.GetAltAdvancementsByLevel(20)
|
||||||
|
//
|
||||||
|
// Player AA Management:
|
||||||
|
//
|
||||||
|
// // Create player AA state
|
||||||
|
// playerState := alt_advancement.NewAAPlayerState(characterID)
|
||||||
|
//
|
||||||
|
// // Award AA points
|
||||||
|
// playerState.TotalPoints = 50
|
||||||
|
// playerState.AvailablePoints = 25
|
||||||
|
//
|
||||||
|
// // Track AA progress
|
||||||
|
// progress := &alt_advancement.PlayerAAData{
|
||||||
|
// CharacterID: characterID,
|
||||||
|
// NodeID: 1001,
|
||||||
|
// CurrentRank: 3,
|
||||||
|
// PointsSpent: 3,
|
||||||
|
// }
|
||||||
|
// playerState.AAProgress[1001] = progress
|
||||||
|
//
|
||||||
|
// Thread Safety:
|
||||||
|
//
|
||||||
|
// All operations are thread-safe using the generic MasterList base:
|
||||||
|
// - Read-heavy operations use RWMutex for optimal performance
|
||||||
|
// - Atomic operations for simple counters and flags
|
||||||
|
// - Proper lock ordering to prevent deadlocks
|
||||||
|
// - Background processing with goroutines and channels
|
||||||
|
package alt_advancement
|
@ -2,7 +2,6 @@ package alt_advancement
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"log"
|
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
@ -67,20 +66,20 @@ type AAEventHandler interface {
|
|||||||
type AAValidator interface {
|
type AAValidator interface {
|
||||||
// Purchase validation
|
// Purchase validation
|
||||||
ValidateAAPurchase(playerState *AAPlayerState, nodeID int32, targetRank int8) error
|
ValidateAAPurchase(playerState *AAPlayerState, nodeID int32, targetRank int8) error
|
||||||
ValidateAAPrerequisites(playerState *AAPlayerState, aaData *AltAdvanceData) error
|
ValidateAAPrerequisites(playerState *AAPlayerState, aaData *AltAdvancement) error
|
||||||
ValidateAAPoints(playerState *AAPlayerState, pointsRequired int32) error
|
ValidateAAPoints(playerState *AAPlayerState, pointsRequired int32) error
|
||||||
|
|
||||||
// Player validation
|
// Player validation
|
||||||
ValidatePlayerLevel(playerState *AAPlayerState, aaData *AltAdvanceData) error
|
ValidatePlayerLevel(playerState *AAPlayerState, aaData *AltAdvancement) error
|
||||||
ValidatePlayerClass(playerState *AAPlayerState, aaData *AltAdvanceData) error
|
ValidatePlayerClass(playerState *AAPlayerState, aaData *AltAdvancement) error
|
||||||
ValidateExpansionRequirements(playerState *AAPlayerState, aaData *AltAdvanceData) error
|
ValidateExpansionRequirements(playerState *AAPlayerState, aaData *AltAdvancement) error
|
||||||
|
|
||||||
// Template validation
|
// Template validation
|
||||||
ValidateTemplateChange(playerState *AAPlayerState, templateID int8) error
|
ValidateTemplateChange(playerState *AAPlayerState, templateID int8) error
|
||||||
ValidateTemplateEntries(entries []*AAEntry) error
|
ValidateTemplateEntries(entries []*AAEntry) error
|
||||||
|
|
||||||
// System validation
|
// System validation
|
||||||
ValidateAAData(aaData *AltAdvanceData) error
|
ValidateAAData(aaData *AltAdvancement) error
|
||||||
ValidateTreeNodeData(nodeData *TreeNodeData) error
|
ValidateTreeNodeData(nodeData *TreeNodeData) error
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -131,8 +130,8 @@ type AAStatistics interface {
|
|||||||
// AACache interface for caching AA data
|
// AACache interface for caching AA data
|
||||||
type AACache interface {
|
type AACache interface {
|
||||||
// AA data caching
|
// AA data caching
|
||||||
GetAA(nodeID int32) (*AltAdvanceData, bool)
|
GetAA(nodeID int32) (*AltAdvancement, bool)
|
||||||
SetAA(nodeID int32, aaData *AltAdvanceData)
|
SetAA(nodeID int32, aaData *AltAdvancement)
|
||||||
InvalidateAA(nodeID int32)
|
InvalidateAA(nodeID int32)
|
||||||
|
|
||||||
// Player state caching
|
// Player state caching
|
||||||
@ -179,23 +178,7 @@ type Transaction interface {
|
|||||||
Rollback() error
|
Rollback() error
|
||||||
}
|
}
|
||||||
|
|
||||||
// DatabaseImpl provides a concrete implementation of AADatabase
|
// Note: Database operations are now embedded in the main AltAdvancement type
|
||||||
type DatabaseImpl struct {
|
|
||||||
db *sql.DB
|
|
||||||
masterAAList *MasterAAList
|
|
||||||
masterNodeList *MasterAANodeList
|
|
||||||
logger *log.Logger
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewDatabaseImpl creates a new database implementation
|
|
||||||
func NewDatabaseImpl(db *sql.DB, masterAAList *MasterAAList, masterNodeList *MasterAANodeList, logger *log.Logger) *DatabaseImpl {
|
|
||||||
return &DatabaseImpl{
|
|
||||||
db: db,
|
|
||||||
masterAAList: masterAAList,
|
|
||||||
masterNodeList: masterNodeList,
|
|
||||||
logger: logger,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// AAManagerInterface defines the main interface for AA management
|
// AAManagerInterface defines the main interface for AA management
|
||||||
type AAManagerInterface interface {
|
type AAManagerInterface interface {
|
||||||
@ -216,7 +199,7 @@ type AAManagerInterface interface {
|
|||||||
// AA operations
|
// AA operations
|
||||||
PurchaseAA(characterID int32, nodeID int32, targetRank int8) error
|
PurchaseAA(characterID int32, nodeID int32, targetRank int8) error
|
||||||
RefundAA(characterID int32, nodeID int32) error
|
RefundAA(characterID int32, nodeID int32) error
|
||||||
GetAvailableAAs(characterID int32, tabID int8) ([]*AltAdvanceData, error)
|
GetAvailableAAs(characterID int32, tabID int8) ([]*AltAdvancement, error)
|
||||||
|
|
||||||
// Template operations
|
// Template operations
|
||||||
ChangeAATemplate(characterID int32, templateID int8) error
|
ChangeAATemplate(characterID int32, templateID int8) error
|
||||||
@ -228,10 +211,10 @@ type AAManagerInterface interface {
|
|||||||
GetAAPoints(characterID int32) (int32, int32, int32, error) // total, spent, available
|
GetAAPoints(characterID int32) (int32, int32, int32, error) // total, spent, available
|
||||||
|
|
||||||
// Query operations
|
// Query operations
|
||||||
GetAA(nodeID int32) (*AltAdvanceData, error)
|
GetAA(nodeID int32) (*AltAdvancement, error)
|
||||||
GetAABySpellID(spellID int32) (*AltAdvanceData, error)
|
GetAABySpellID(spellID int32) (*AltAdvancement, error)
|
||||||
GetAAsByGroup(group int8) ([]*AltAdvanceData, error)
|
GetAAsByGroup(group int8) ([]*AltAdvancement, error)
|
||||||
GetAAsByClass(classID int8) ([]*AltAdvanceData, error)
|
GetAAsByClass(classID int8) ([]*AltAdvancement, error)
|
||||||
|
|
||||||
// Statistics
|
// Statistics
|
||||||
GetSystemStats() *AAManagerStats
|
GetSystemStats() *AAManagerStats
|
||||||
@ -312,7 +295,7 @@ func (aa *AAAdapter) RefundAA(nodeID int32) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetAvailableAAs returns available AAs for a tab
|
// GetAvailableAAs returns available AAs for a tab
|
||||||
func (aa *AAAdapter) GetAvailableAAs(tabID int8) ([]*AltAdvanceData, error) {
|
func (aa *AAAdapter) GetAvailableAAs(tabID int8) ([]*AltAdvancement, error) {
|
||||||
return aa.manager.GetAvailableAAs(aa.characterID, tabID)
|
return aa.manager.GetAvailableAAs(aa.characterID, tabID)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -433,7 +416,7 @@ func (caa *ClientAAAdapter) GetClientVersion() int16 {
|
|||||||
|
|
||||||
// Simple cache implementation for testing
|
// Simple cache implementation for testing
|
||||||
type SimpleAACache struct {
|
type SimpleAACache struct {
|
||||||
aaData map[int32]*AltAdvanceData
|
aaData map[int32]*AltAdvancement
|
||||||
playerStates map[int32]*AAPlayerState
|
playerStates map[int32]*AAPlayerState
|
||||||
treeNodes map[int32]*TreeNodeData
|
treeNodes map[int32]*TreeNodeData
|
||||||
mutex sync.RWMutex
|
mutex sync.RWMutex
|
||||||
@ -445,7 +428,7 @@ type SimpleAACache struct {
|
|||||||
// NewSimpleAACache creates a new simple cache
|
// NewSimpleAACache creates a new simple cache
|
||||||
func NewSimpleAACache(maxSize int32) *SimpleAACache {
|
func NewSimpleAACache(maxSize int32) *SimpleAACache {
|
||||||
return &SimpleAACache{
|
return &SimpleAACache{
|
||||||
aaData: make(map[int32]*AltAdvanceData),
|
aaData: make(map[int32]*AltAdvancement),
|
||||||
playerStates: make(map[int32]*AAPlayerState),
|
playerStates: make(map[int32]*AAPlayerState),
|
||||||
treeNodes: make(map[int32]*TreeNodeData),
|
treeNodes: make(map[int32]*TreeNodeData),
|
||||||
maxSize: maxSize,
|
maxSize: maxSize,
|
||||||
@ -453,13 +436,13 @@ func NewSimpleAACache(maxSize int32) *SimpleAACache {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetAA retrieves AA data from cache
|
// GetAA retrieves AA data from cache
|
||||||
func (c *SimpleAACache) GetAA(nodeID int32) (*AltAdvanceData, bool) {
|
func (c *SimpleAACache) GetAA(nodeID int32) (*AltAdvancement, bool) {
|
||||||
c.mutex.RLock()
|
c.mutex.RLock()
|
||||||
defer c.mutex.RUnlock()
|
defer c.mutex.RUnlock()
|
||||||
|
|
||||||
if data, exists := c.aaData[nodeID]; exists {
|
if data, exists := c.aaData[nodeID]; exists {
|
||||||
c.hits++
|
c.hits++
|
||||||
return data.Copy(), true
|
return data.Clone(), true
|
||||||
}
|
}
|
||||||
|
|
||||||
c.misses++
|
c.misses++
|
||||||
@ -467,7 +450,7 @@ func (c *SimpleAACache) GetAA(nodeID int32) (*AltAdvanceData, bool) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// SetAA stores AA data in cache
|
// SetAA stores AA data in cache
|
||||||
func (c *SimpleAACache) SetAA(nodeID int32, aaData *AltAdvanceData) {
|
func (c *SimpleAACache) SetAA(nodeID int32, aaData *AltAdvancement) {
|
||||||
c.mutex.Lock()
|
c.mutex.Lock()
|
||||||
defer c.mutex.Unlock()
|
defer c.mutex.Unlock()
|
||||||
|
|
||||||
@ -479,7 +462,7 @@ func (c *SimpleAACache) SetAA(nodeID int32, aaData *AltAdvanceData) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
c.aaData[nodeID] = aaData.Copy()
|
c.aaData[nodeID] = aaData.Clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
// InvalidateAA removes AA data from cache
|
// InvalidateAA removes AA data from cache
|
||||||
@ -573,7 +556,7 @@ func (c *SimpleAACache) Clear() {
|
|||||||
c.mutex.Lock()
|
c.mutex.Lock()
|
||||||
defer c.mutex.Unlock()
|
defer c.mutex.Unlock()
|
||||||
|
|
||||||
c.aaData = make(map[int32]*AltAdvanceData)
|
c.aaData = make(map[int32]*AltAdvancement)
|
||||||
c.playerStates = make(map[int32]*AAPlayerState)
|
c.playerStates = make(map[int32]*AAPlayerState)
|
||||||
c.treeNodes = make(map[int32]*TreeNodeData)
|
c.treeNodes = make(map[int32]*TreeNodeData)
|
||||||
}
|
}
|
||||||
|
@ -8,7 +8,7 @@ import (
|
|||||||
// NewAAManager creates a new AA manager
|
// NewAAManager creates a new AA manager
|
||||||
func NewAAManager(config AAManagerConfig) *AAManager {
|
func NewAAManager(config AAManagerConfig) *AAManager {
|
||||||
return &AAManager{
|
return &AAManager{
|
||||||
masterAAList: NewMasterAAList(),
|
masterAAList: NewMasterList(),
|
||||||
masterNodeList: NewMasterAANodeList(),
|
masterNodeList: NewMasterAANodeList(),
|
||||||
playerStates: make(map[int32]*AAPlayerState),
|
playerStates: make(map[int32]*AAPlayerState),
|
||||||
config: config,
|
config: config,
|
||||||
@ -97,7 +97,7 @@ func (am *AAManager) LoadAAData() error {
|
|||||||
// ReloadAAData reloads all AA data
|
// ReloadAAData reloads all AA data
|
||||||
func (am *AAManager) ReloadAAData() error {
|
func (am *AAManager) ReloadAAData() error {
|
||||||
// Clear existing data
|
// Clear existing data
|
||||||
am.masterAAList.DestroyAltAdvancements()
|
am.masterAAList.Clear()
|
||||||
am.masterNodeList.DestroyTreeNodes()
|
am.masterNodeList.DestroyTreeNodes()
|
||||||
|
|
||||||
// Clear cached player states
|
// Clear cached player states
|
||||||
@ -194,7 +194,7 @@ func (am *AAManager) PurchaseAA(characterID int32, nodeID int32, targetRank int8
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get AA data
|
// Get AA data
|
||||||
aaData := am.masterAAList.GetAltAdvancementByNodeID(nodeID)
|
aaData := am.masterAAList.GetAltAdvancement(nodeID)
|
||||||
if aaData == nil {
|
if aaData == nil {
|
||||||
return fmt.Errorf("AA node %d not found", nodeID)
|
return fmt.Errorf("AA node %d not found", nodeID)
|
||||||
}
|
}
|
||||||
@ -239,7 +239,7 @@ func (am *AAManager) RefundAA(characterID int32, nodeID int32) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get AA data
|
// Get AA data
|
||||||
aaData := am.masterAAList.GetAltAdvancementByNodeID(nodeID)
|
aaData := am.masterAAList.GetAltAdvancement(nodeID)
|
||||||
if aaData == nil {
|
if aaData == nil {
|
||||||
return fmt.Errorf("AA node %d not found", nodeID)
|
return fmt.Errorf("AA node %d not found", nodeID)
|
||||||
}
|
}
|
||||||
@ -278,7 +278,7 @@ func (am *AAManager) RefundAA(characterID int32, nodeID int32) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetAvailableAAs returns AAs available for a player in a specific tab
|
// GetAvailableAAs returns AAs available for a player in a specific tab
|
||||||
func (am *AAManager) GetAvailableAAs(characterID int32, tabID int8) ([]*AltAdvanceData, error) {
|
func (am *AAManager) GetAvailableAAs(characterID int32, tabID int8) ([]*AltAdvancement, error) {
|
||||||
// Get player state
|
// Get player state
|
||||||
playerState := am.getPlayerState(characterID)
|
playerState := am.getPlayerState(characterID)
|
||||||
if playerState == nil {
|
if playerState == nil {
|
||||||
@ -286,8 +286,8 @@ func (am *AAManager) GetAvailableAAs(characterID int32, tabID int8) ([]*AltAdvan
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get all AAs for the tab
|
// Get all AAs for the tab
|
||||||
allAAs := am.masterAAList.GetAAsByGroup(tabID)
|
allAAs := am.masterAAList.GetAltAdvancementsByGroup(tabID)
|
||||||
var availableAAs []*AltAdvanceData
|
var availableAAs []*AltAdvancement
|
||||||
|
|
||||||
for _, aa := range allAAs {
|
for _, aa := range allAAs {
|
||||||
// Check if AA is available for this player
|
// Check if AA is available for this player
|
||||||
@ -418,8 +418,8 @@ func (am *AAManager) GetAAPoints(characterID int32) (int32, int32, int32, error)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetAA returns an AA by node ID
|
// GetAA returns an AA by node ID
|
||||||
func (am *AAManager) GetAA(nodeID int32) (*AltAdvanceData, error) {
|
func (am *AAManager) GetAA(nodeID int32) (*AltAdvancement, error) {
|
||||||
aaData := am.masterAAList.GetAltAdvancementByNodeID(nodeID)
|
aaData := am.masterAAList.GetAltAdvancement(nodeID)
|
||||||
if aaData == nil {
|
if aaData == nil {
|
||||||
return nil, fmt.Errorf("AA node %d not found", nodeID)
|
return nil, fmt.Errorf("AA node %d not found", nodeID)
|
||||||
}
|
}
|
||||||
@ -427,22 +427,28 @@ func (am *AAManager) GetAA(nodeID int32) (*AltAdvanceData, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetAABySpellID returns an AA by spell ID
|
// GetAABySpellID returns an AA by spell ID
|
||||||
func (am *AAManager) GetAABySpellID(spellID int32) (*AltAdvanceData, error) {
|
func (am *AAManager) GetAABySpellID(spellID int32) (*AltAdvancement, error) {
|
||||||
aaData := am.masterAAList.GetAltAdvancement(spellID)
|
// Search for AA with matching SpellID
|
||||||
if aaData == nil {
|
var found *AltAdvancement
|
||||||
|
am.masterAAList.ForEach(func(_ int32, aa *AltAdvancement) {
|
||||||
|
if aa.SpellID == spellID {
|
||||||
|
found = aa
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if found == nil {
|
||||||
return nil, fmt.Errorf("AA with spell ID %d not found", spellID)
|
return nil, fmt.Errorf("AA with spell ID %d not found", spellID)
|
||||||
}
|
}
|
||||||
return aaData, nil
|
return found, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetAAsByGroup returns AAs for a specific group/tab
|
// GetAAsByGroup returns AAs for a specific group/tab
|
||||||
func (am *AAManager) GetAAsByGroup(group int8) ([]*AltAdvanceData, error) {
|
func (am *AAManager) GetAAsByGroup(group int8) ([]*AltAdvancement, error) {
|
||||||
return am.masterAAList.GetAAsByGroup(group), nil
|
return am.masterAAList.GetAltAdvancementsByGroup(group), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetAAsByClass returns AAs available for a specific class
|
// GetAAsByClass returns AAs available for a specific class
|
||||||
func (am *AAManager) GetAAsByClass(classID int8) ([]*AltAdvanceData, error) {
|
func (am *AAManager) GetAAsByClass(classID int8) ([]*AltAdvancement, error) {
|
||||||
return am.masterAAList.GetAAsByClass(classID), nil
|
return am.masterAAList.GetAltAdvancementsByClass(classID), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetSystemStats returns system statistics
|
// GetSystemStats returns system statistics
|
||||||
@ -540,7 +546,7 @@ func (am *AAManager) getPlayerState(characterID int32) *AAPlayerState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// performAAPurchase performs the actual AA purchase
|
// performAAPurchase performs the actual AA purchase
|
||||||
func (am *AAManager) performAAPurchase(playerState *AAPlayerState, aaData *AltAdvanceData, targetRank int8) error {
|
func (am *AAManager) performAAPurchase(playerState *AAPlayerState, aaData *AltAdvancement, targetRank int8) error {
|
||||||
// Calculate cost
|
// Calculate cost
|
||||||
pointsCost := int32(aaData.RankCost) * int32(targetRank)
|
pointsCost := int32(aaData.RankCost) * int32(targetRank)
|
||||||
|
|
||||||
@ -583,7 +589,7 @@ func (am *AAManager) performAAPurchase(playerState *AAPlayerState, aaData *AltAd
|
|||||||
}
|
}
|
||||||
|
|
||||||
// isAAAvailable checks if an AA is available for a player
|
// isAAAvailable checks if an AA is available for a player
|
||||||
func (am *AAManager) isAAAvailable(playerState *AAPlayerState, aaData *AltAdvanceData) bool {
|
func (am *AAManager) isAAAvailable(playerState *AAPlayerState, aaData *AltAdvancement) bool {
|
||||||
// Check if player meets minimum level requirement
|
// Check if player meets minimum level requirement
|
||||||
// Note: This would normally get player level from the actual player object
|
// Note: This would normally get player level from the actual player object
|
||||||
// For now, we'll assume level requirements are met
|
// For now, we'll assume level requirements are met
|
||||||
|
157
internal/alt_advancement/master.go
Normal file
157
internal/alt_advancement/master.go
Normal file
@ -0,0 +1,157 @@
|
|||||||
|
package alt_advancement
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"eq2emu/internal/common"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MasterList manages the global list of all alternate advancements
|
||||||
|
type MasterList struct {
|
||||||
|
*common.MasterList[int32, *AltAdvancement]
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewMasterList creates a new master alternate advancement list
|
||||||
|
func NewMasterList() *MasterList {
|
||||||
|
return &MasterList{
|
||||||
|
MasterList: common.NewMasterList[int32, *AltAdvancement](),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddAltAdvancement adds an alternate advancement to the master list
|
||||||
|
// Returns false if AA with same ID already exists
|
||||||
|
func (m *MasterList) AddAltAdvancement(aa *AltAdvancement) bool {
|
||||||
|
if aa == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return m.MasterList.Add(aa)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAltAdvancement retrieves an alternate advancement by node ID
|
||||||
|
// Returns nil if not found
|
||||||
|
func (m *MasterList) GetAltAdvancement(nodeID int32) *AltAdvancement {
|
||||||
|
return m.MasterList.Get(nodeID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAltAdvancementClone retrieves a cloned copy of an alternate advancement by node ID
|
||||||
|
// Returns nil if not found. Safe for modification without affecting master list
|
||||||
|
func (m *MasterList) GetAltAdvancementClone(nodeID int32) *AltAdvancement {
|
||||||
|
aa := m.MasterList.Get(nodeID)
|
||||||
|
if aa == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return aa.Clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAllAltAdvancements returns a map of all alternate advancements (read-only access)
|
||||||
|
// The returned map should not be modified
|
||||||
|
func (m *MasterList) GetAllAltAdvancements() map[int32]*AltAdvancement {
|
||||||
|
return m.MasterList.GetAll()
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAltAdvancementsByGroup returns alternate advancements filtered by group/tab
|
||||||
|
func (m *MasterList) GetAltAdvancementsByGroup(group int8) []*AltAdvancement {
|
||||||
|
return m.MasterList.Filter(func(aa *AltAdvancement) bool {
|
||||||
|
return aa.Group == group
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAltAdvancementsByClass returns alternate advancements filtered by class requirement
|
||||||
|
func (m *MasterList) GetAltAdvancementsByClass(classID int8) []*AltAdvancement {
|
||||||
|
return m.MasterList.Filter(func(aa *AltAdvancement) bool {
|
||||||
|
return aa.ClassReq == 0 || aa.ClassReq == classID
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAltAdvancementsByLevel returns alternate advancements available at a specific level
|
||||||
|
func (m *MasterList) GetAltAdvancementsByLevel(level int8) []*AltAdvancement {
|
||||||
|
return m.MasterList.Filter(func(aa *AltAdvancement) bool {
|
||||||
|
return aa.MinLevel <= level
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoveAltAdvancement removes an alternate advancement from the master list
|
||||||
|
// Returns true if AA was found and removed
|
||||||
|
func (m *MasterList) RemoveAltAdvancement(nodeID int32) bool {
|
||||||
|
return m.MasterList.Remove(nodeID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateAltAdvancement updates an existing alternate advancement
|
||||||
|
// Returns error if AA doesn't exist
|
||||||
|
func (m *MasterList) UpdateAltAdvancement(aa *AltAdvancement) error {
|
||||||
|
if aa == nil {
|
||||||
|
return fmt.Errorf("alternate advancement cannot be nil")
|
||||||
|
}
|
||||||
|
return m.MasterList.Update(aa)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetGroups returns all unique groups/tabs that have alternate advancements
|
||||||
|
func (m *MasterList) GetGroups() []int8 {
|
||||||
|
groups := make(map[int8]bool)
|
||||||
|
|
||||||
|
m.MasterList.ForEach(func(_ int32, aa *AltAdvancement) {
|
||||||
|
groups[aa.Group] = true
|
||||||
|
})
|
||||||
|
|
||||||
|
result := make([]int8, 0, len(groups))
|
||||||
|
for group := range groups {
|
||||||
|
result = append(result, group)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetClasses returns all unique classes that have alternate advancements
|
||||||
|
func (m *MasterList) GetClasses() []int8 {
|
||||||
|
classes := make(map[int8]bool)
|
||||||
|
|
||||||
|
m.MasterList.ForEach(func(_ int32, aa *AltAdvancement) {
|
||||||
|
if aa.ClassReq > 0 {
|
||||||
|
classes[aa.ClassReq] = true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
result := make([]int8, 0, len(classes))
|
||||||
|
for class := range classes {
|
||||||
|
result = append(result, class)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateAll validates all alternate advancements in the master list
|
||||||
|
func (m *MasterList) ValidateAll() []error {
|
||||||
|
var errors []error
|
||||||
|
|
||||||
|
m.MasterList.ForEach(func(nodeID int32, aa *AltAdvancement) {
|
||||||
|
if !aa.IsValid() {
|
||||||
|
errors = append(errors, fmt.Errorf("invalid AA data: node_id=%d", nodeID))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate prerequisites
|
||||||
|
if aa.RankPrereqID > 0 {
|
||||||
|
prereq := m.MasterList.Get(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
|
||||||
|
}
|
@ -1,475 +0,0 @@
|
|||||||
package alt_advancement
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"sort"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
// NewMasterAAList creates a new master AA list
|
|
||||||
func NewMasterAAList() *MasterAAList {
|
|
||||||
return &MasterAAList{
|
|
||||||
aaList: make([]*AltAdvanceData, 0),
|
|
||||||
aaBySpellID: make(map[int32]*AltAdvanceData),
|
|
||||||
aaByNodeID: make(map[int32]*AltAdvanceData),
|
|
||||||
aaByGroup: make(map[int8][]*AltAdvanceData),
|
|
||||||
totalLoaded: 0,
|
|
||||||
lastLoadTime: time.Now(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// AddAltAdvancement adds an AA to the master list
|
|
||||||
func (mal *MasterAAList) AddAltAdvancement(data *AltAdvanceData) error {
|
|
||||||
if data == nil {
|
|
||||||
return fmt.Errorf("data cannot be nil")
|
|
||||||
}
|
|
||||||
|
|
||||||
if !data.IsValid() {
|
|
||||||
return fmt.Errorf("invalid AA data: spell_id=%d, node_id=%d", data.SpellID, data.NodeID)
|
|
||||||
}
|
|
||||||
|
|
||||||
mal.mutex.Lock()
|
|
||||||
defer mal.mutex.Unlock()
|
|
||||||
|
|
||||||
// Check for duplicates
|
|
||||||
if _, exists := mal.aaBySpellID[data.SpellID]; exists {
|
|
||||||
return fmt.Errorf("AA with spell ID %d already exists", data.SpellID)
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, exists := mal.aaByNodeID[data.NodeID]; exists {
|
|
||||||
return fmt.Errorf("AA with node ID %d already exists", data.NodeID)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add to main list
|
|
||||||
mal.aaList = append(mal.aaList, data)
|
|
||||||
|
|
||||||
// Add to lookup maps
|
|
||||||
mal.aaBySpellID[data.SpellID] = data
|
|
||||||
mal.aaByNodeID[data.NodeID] = data
|
|
||||||
|
|
||||||
// Add to group map
|
|
||||||
if mal.aaByGroup[data.Group] == nil {
|
|
||||||
mal.aaByGroup[data.Group] = make([]*AltAdvanceData, 0)
|
|
||||||
}
|
|
||||||
mal.aaByGroup[data.Group] = append(mal.aaByGroup[data.Group], data)
|
|
||||||
|
|
||||||
mal.totalLoaded++
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetAltAdvancement returns an AA by spell ID
|
|
||||||
func (mal *MasterAAList) GetAltAdvancement(spellID int32) *AltAdvanceData {
|
|
||||||
mal.mutex.RLock()
|
|
||||||
defer mal.mutex.RUnlock()
|
|
||||||
|
|
||||||
if data, exists := mal.aaBySpellID[spellID]; exists {
|
|
||||||
return data.Copy()
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetAltAdvancementByNodeID returns an AA by node ID
|
|
||||||
func (mal *MasterAAList) GetAltAdvancementByNodeID(nodeID int32) *AltAdvanceData {
|
|
||||||
mal.mutex.RLock()
|
|
||||||
defer mal.mutex.RUnlock()
|
|
||||||
|
|
||||||
if data, exists := mal.aaByNodeID[nodeID]; exists {
|
|
||||||
return data.Copy()
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetAAsByGroup returns all AAs for a specific group/tab
|
|
||||||
func (mal *MasterAAList) GetAAsByGroup(group int8) []*AltAdvanceData {
|
|
||||||
mal.mutex.RLock()
|
|
||||||
defer mal.mutex.RUnlock()
|
|
||||||
|
|
||||||
if aaList, exists := mal.aaByGroup[group]; exists {
|
|
||||||
// Return copies to prevent external modification
|
|
||||||
result := make([]*AltAdvanceData, len(aaList))
|
|
||||||
for i, aa := range aaList {
|
|
||||||
result[i] = aa.Copy()
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
return []*AltAdvanceData{}
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetAAsByClass returns AAs available for a specific class
|
|
||||||
func (mal *MasterAAList) GetAAsByClass(classID int8) []*AltAdvanceData {
|
|
||||||
mal.mutex.RLock()
|
|
||||||
defer mal.mutex.RUnlock()
|
|
||||||
|
|
||||||
var result []*AltAdvanceData
|
|
||||||
|
|
||||||
for _, aa := range mal.aaList {
|
|
||||||
// Check if AA is available for this class (0 means all classes)
|
|
||||||
if aa.ClassReq == 0 || aa.ClassReq == classID {
|
|
||||||
result = append(result, aa.Copy())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetAAsByLevel returns AAs available at a specific level
|
|
||||||
func (mal *MasterAAList) GetAAsByLevel(level int8) []*AltAdvanceData {
|
|
||||||
mal.mutex.RLock()
|
|
||||||
defer mal.mutex.RUnlock()
|
|
||||||
|
|
||||||
var result []*AltAdvanceData
|
|
||||||
|
|
||||||
for _, aa := range mal.aaList {
|
|
||||||
if aa.MinLevel <= level {
|
|
||||||
result = append(result, aa.Copy())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
// Size returns the total number of AAs
|
|
||||||
func (mal *MasterAAList) Size() int {
|
|
||||||
mal.mutex.RLock()
|
|
||||||
defer mal.mutex.RUnlock()
|
|
||||||
|
|
||||||
return len(mal.aaList)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetAllAAs returns all AAs (copies)
|
|
||||||
func (mal *MasterAAList) GetAllAAs() []*AltAdvanceData {
|
|
||||||
mal.mutex.RLock()
|
|
||||||
defer mal.mutex.RUnlock()
|
|
||||||
|
|
||||||
result := make([]*AltAdvanceData, len(mal.aaList))
|
|
||||||
for i, aa := range mal.aaList {
|
|
||||||
result[i] = aa.Copy()
|
|
||||||
}
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
// DestroyAltAdvancements clears all AA data
|
|
||||||
func (mal *MasterAAList) DestroyAltAdvancements() {
|
|
||||||
mal.mutex.Lock()
|
|
||||||
defer mal.mutex.Unlock()
|
|
||||||
|
|
||||||
mal.aaList = make([]*AltAdvanceData, 0)
|
|
||||||
mal.aaBySpellID = make(map[int32]*AltAdvanceData)
|
|
||||||
mal.aaByNodeID = make(map[int32]*AltAdvanceData)
|
|
||||||
mal.aaByGroup = make(map[int8][]*AltAdvanceData)
|
|
||||||
mal.totalLoaded = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// SortAAsByGroup sorts AAs within each group by row and column
|
|
||||||
func (mal *MasterAAList) SortAAsByGroup() {
|
|
||||||
mal.mutex.Lock()
|
|
||||||
defer mal.mutex.Unlock()
|
|
||||||
|
|
||||||
for group := range mal.aaByGroup {
|
|
||||||
sort.Slice(mal.aaByGroup[group], func(i, j int) bool {
|
|
||||||
aaI := mal.aaByGroup[group][i]
|
|
||||||
aaJ := mal.aaByGroup[group][j]
|
|
||||||
|
|
||||||
// Sort by row first, then by column
|
|
||||||
if aaI.Row != aaJ.Row {
|
|
||||||
return aaI.Row < aaJ.Row
|
|
||||||
}
|
|
||||||
return aaI.Col < aaJ.Col
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetGroupCount returns the number of groups with AAs
|
|
||||||
func (mal *MasterAAList) GetGroupCount() int {
|
|
||||||
mal.mutex.RLock()
|
|
||||||
defer mal.mutex.RUnlock()
|
|
||||||
|
|
||||||
return len(mal.aaByGroup)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetGroups returns all group IDs that have AAs
|
|
||||||
func (mal *MasterAAList) GetGroups() []int8 {
|
|
||||||
mal.mutex.RLock()
|
|
||||||
defer mal.mutex.RUnlock()
|
|
||||||
|
|
||||||
groups := make([]int8, 0, len(mal.aaByGroup))
|
|
||||||
for group := range mal.aaByGroup {
|
|
||||||
groups = append(groups, group)
|
|
||||||
}
|
|
||||||
|
|
||||||
sort.Slice(groups, func(i, j int) bool {
|
|
||||||
return groups[i] < groups[j]
|
|
||||||
})
|
|
||||||
|
|
||||||
return groups
|
|
||||||
}
|
|
||||||
|
|
||||||
// ValidateAAData validates all AA data for consistency
|
|
||||||
func (mal *MasterAAList) ValidateAAData() []error {
|
|
||||||
mal.mutex.RLock()
|
|
||||||
defer mal.mutex.RUnlock()
|
|
||||||
|
|
||||||
var errors []error
|
|
||||||
|
|
||||||
for _, aa := range mal.aaList {
|
|
||||||
if !aa.IsValid() {
|
|
||||||
errors = append(errors, fmt.Errorf("invalid AA data: spell_id=%d, node_id=%d", aa.SpellID, aa.NodeID))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate prerequisites
|
|
||||||
if aa.RankPrereqID > 0 {
|
|
||||||
if _, exists := mal.aaByNodeID[aa.RankPrereqID]; !exists {
|
|
||||||
errors = append(errors, fmt.Errorf("AA %d has invalid prerequisite node ID %d", aa.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", aa.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", aa.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", aa.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", aa.NodeID, aa.MaxRank))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return errors
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetStats returns statistics about the master AA list
|
|
||||||
func (mal *MasterAAList) GetStats() map[string]any {
|
|
||||||
mal.mutex.RLock()
|
|
||||||
defer mal.mutex.RUnlock()
|
|
||||||
|
|
||||||
stats := make(map[string]any)
|
|
||||||
stats[STAT_TOTAL_AAS_LOADED] = mal.totalLoaded
|
|
||||||
stats["last_load_time"] = mal.lastLoadTime
|
|
||||||
stats["groups_count"] = len(mal.aaByGroup)
|
|
||||||
|
|
||||||
// Count AAs per group
|
|
||||||
groupCounts := make(map[int8]int)
|
|
||||||
for group, aaList := range mal.aaByGroup {
|
|
||||||
groupCounts[group] = len(aaList)
|
|
||||||
}
|
|
||||||
stats[STAT_AAS_PER_TAB] = groupCounts
|
|
||||||
|
|
||||||
return stats
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewMasterAANodeList creates a new master AA node list
|
|
||||||
func NewMasterAANodeList() *MasterAANodeList {
|
|
||||||
return &MasterAANodeList{
|
|
||||||
nodeList: make([]*TreeNodeData, 0),
|
|
||||||
nodesByClass: make(map[int32][]*TreeNodeData),
|
|
||||||
nodesByTree: make(map[int32]*TreeNodeData),
|
|
||||||
totalLoaded: 0,
|
|
||||||
lastLoadTime: time.Now(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// AddTreeNode adds a tree node to the master list
|
|
||||||
func (manl *MasterAANodeList) AddTreeNode(data *TreeNodeData) error {
|
|
||||||
if data == nil {
|
|
||||||
return fmt.Errorf("data cannot be nil")
|
|
||||||
}
|
|
||||||
|
|
||||||
manl.mutex.Lock()
|
|
||||||
defer manl.mutex.Unlock()
|
|
||||||
|
|
||||||
// Check for duplicates
|
|
||||||
if _, exists := manl.nodesByTree[data.TreeID]; exists {
|
|
||||||
return fmt.Errorf("tree node with tree ID %d already exists", data.TreeID)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add to main list
|
|
||||||
manl.nodeList = append(manl.nodeList, data)
|
|
||||||
|
|
||||||
// Add to lookup maps
|
|
||||||
manl.nodesByTree[data.TreeID] = data
|
|
||||||
|
|
||||||
// Add to class map
|
|
||||||
if manl.nodesByClass[data.ClassID] == nil {
|
|
||||||
manl.nodesByClass[data.ClassID] = make([]*TreeNodeData, 0)
|
|
||||||
}
|
|
||||||
manl.nodesByClass[data.ClassID] = append(manl.nodesByClass[data.ClassID], data)
|
|
||||||
|
|
||||||
manl.totalLoaded++
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetTreeNodes returns all tree nodes
|
|
||||||
func (manl *MasterAANodeList) GetTreeNodes() []*TreeNodeData {
|
|
||||||
manl.mutex.RLock()
|
|
||||||
defer manl.mutex.RUnlock()
|
|
||||||
|
|
||||||
// Return copies to prevent external modification
|
|
||||||
result := make([]*TreeNodeData, len(manl.nodeList))
|
|
||||||
for i, node := range manl.nodeList {
|
|
||||||
nodeCopy := *node
|
|
||||||
result[i] = &nodeCopy
|
|
||||||
}
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetTreeNodesByClass returns tree nodes for a specific class
|
|
||||||
func (manl *MasterAANodeList) GetTreeNodesByClass(classID int32) []*TreeNodeData {
|
|
||||||
manl.mutex.RLock()
|
|
||||||
defer manl.mutex.RUnlock()
|
|
||||||
|
|
||||||
if nodeList, exists := manl.nodesByClass[classID]; exists {
|
|
||||||
// Return copies to prevent external modification
|
|
||||||
result := make([]*TreeNodeData, len(nodeList))
|
|
||||||
for i, node := range nodeList {
|
|
||||||
nodeCopy := *node
|
|
||||||
result[i] = &nodeCopy
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
return []*TreeNodeData{}
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetTreeNode returns a specific tree node by tree ID
|
|
||||||
func (manl *MasterAANodeList) GetTreeNode(treeID int32) *TreeNodeData {
|
|
||||||
manl.mutex.RLock()
|
|
||||||
defer manl.mutex.RUnlock()
|
|
||||||
|
|
||||||
if node, exists := manl.nodesByTree[treeID]; exists {
|
|
||||||
nodeCopy := *node
|
|
||||||
return &nodeCopy
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Size returns the total number of tree nodes
|
|
||||||
func (manl *MasterAANodeList) Size() int {
|
|
||||||
manl.mutex.RLock()
|
|
||||||
defer manl.mutex.RUnlock()
|
|
||||||
|
|
||||||
return len(manl.nodeList)
|
|
||||||
}
|
|
||||||
|
|
||||||
// DestroyTreeNodes clears all tree node data
|
|
||||||
func (manl *MasterAANodeList) DestroyTreeNodes() {
|
|
||||||
manl.mutex.Lock()
|
|
||||||
defer manl.mutex.Unlock()
|
|
||||||
|
|
||||||
manl.nodeList = make([]*TreeNodeData, 0)
|
|
||||||
manl.nodesByClass = make(map[int32][]*TreeNodeData)
|
|
||||||
manl.nodesByTree = make(map[int32]*TreeNodeData)
|
|
||||||
manl.totalLoaded = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetClassCount returns the number of classes with tree nodes
|
|
||||||
func (manl *MasterAANodeList) GetClassCount() int {
|
|
||||||
manl.mutex.RLock()
|
|
||||||
defer manl.mutex.RUnlock()
|
|
||||||
|
|
||||||
return len(manl.nodesByClass)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetClasses returns all class IDs that have tree nodes
|
|
||||||
func (manl *MasterAANodeList) GetClasses() []int32 {
|
|
||||||
manl.mutex.RLock()
|
|
||||||
defer manl.mutex.RUnlock()
|
|
||||||
|
|
||||||
classes := make([]int32, 0, len(manl.nodesByClass))
|
|
||||||
for classID := range manl.nodesByClass {
|
|
||||||
classes = append(classes, classID)
|
|
||||||
}
|
|
||||||
|
|
||||||
sort.Slice(classes, func(i, j int) bool {
|
|
||||||
return classes[i] < classes[j]
|
|
||||||
})
|
|
||||||
|
|
||||||
return classes
|
|
||||||
}
|
|
||||||
|
|
||||||
// ValidateTreeNodes validates all tree node data for consistency
|
|
||||||
func (manl *MasterAANodeList) ValidateTreeNodes() []error {
|
|
||||||
manl.mutex.RLock()
|
|
||||||
defer manl.mutex.RUnlock()
|
|
||||||
|
|
||||||
var errors []error
|
|
||||||
|
|
||||||
// Check for orphaned tree IDs
|
|
||||||
treeIDMap := make(map[int32]bool)
|
|
||||||
for _, node := range manl.nodeList {
|
|
||||||
treeIDMap[node.TreeID] = true
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for duplicate class/tree combinations
|
|
||||||
classTreeMap := make(map[string]bool)
|
|
||||||
for _, node := range manl.nodeList {
|
|
||||||
key := fmt.Sprintf("%d_%d", node.ClassID, node.TreeID)
|
|
||||||
if classTreeMap[key] {
|
|
||||||
errors = append(errors, fmt.Errorf("duplicate class/tree combination: class=%d, tree=%d", node.ClassID, node.TreeID))
|
|
||||||
}
|
|
||||||
classTreeMap[key] = true
|
|
||||||
}
|
|
||||||
|
|
||||||
return errors
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetStats returns statistics about the master node list
|
|
||||||
func (manl *MasterAANodeList) GetStats() map[string]any {
|
|
||||||
manl.mutex.RLock()
|
|
||||||
defer manl.mutex.RUnlock()
|
|
||||||
|
|
||||||
stats := make(map[string]any)
|
|
||||||
stats[STAT_TOTAL_NODES_LOADED] = manl.totalLoaded
|
|
||||||
stats["last_load_time"] = manl.lastLoadTime
|
|
||||||
stats["classes_count"] = len(manl.nodesByClass)
|
|
||||||
|
|
||||||
// Count nodes per class
|
|
||||||
classCounts := make(map[int32]int)
|
|
||||||
for classID, nodeList := range manl.nodesByClass {
|
|
||||||
classCounts[classID] = len(nodeList)
|
|
||||||
}
|
|
||||||
stats["nodes_per_class"] = classCounts
|
|
||||||
|
|
||||||
return stats
|
|
||||||
}
|
|
||||||
|
|
||||||
// BuildAATreeMap builds a map of AA tree configurations for a specific class
|
|
||||||
func (manl *MasterAANodeList) BuildAATreeMap(classID int32) map[int8]int32 {
|
|
||||||
nodes := manl.GetTreeNodesByClass(classID)
|
|
||||||
treeMap := make(map[int8]int32)
|
|
||||||
|
|
||||||
// Map each tab/group to its corresponding tree ID
|
|
||||||
for i, node := range nodes {
|
|
||||||
if i < 10 { // Limit to the number of defined AA tabs
|
|
||||||
treeMap[int8(i)] = node.TreeID
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return treeMap
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetTreeIDForTab returns the tree ID for a specific tab and class
|
|
||||||
func (manl *MasterAANodeList) GetTreeIDForTab(classID int32, tab int8) int32 {
|
|
||||||
nodes := manl.GetTreeNodesByClass(classID)
|
|
||||||
|
|
||||||
if int(tab) < len(nodes) {
|
|
||||||
return nodes[tab].TreeID
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0
|
|
||||||
}
|
|
@ -5,50 +5,6 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
// AltAdvanceData represents an Alternate Advancement node
|
|
||||||
type AltAdvanceData struct {
|
|
||||||
// Core identification
|
|
||||||
SpellID int32 `json:"spell_id" db:"spell_id"`
|
|
||||||
NodeID int32 `json:"node_id" db:"node_id"`
|
|
||||||
SpellCRC int32 `json:"spell_crc" db:"spell_crc"`
|
|
||||||
|
|
||||||
// Display information
|
|
||||||
Name string `json:"name" db:"name"`
|
|
||||||
Description string `json:"description" db:"description"`
|
|
||||||
|
|
||||||
// Tree organization
|
|
||||||
Group int8 `json:"group" db:"group"` // AA tab (AA_CLASS, AA_SUBCLASS, etc.)
|
|
||||||
Col int8 `json:"col" db:"col"` // Column position in tree
|
|
||||||
Row int8 `json:"row" db:"row"` // Row position in tree
|
|
||||||
|
|
||||||
// Visual representation
|
|
||||||
Icon int16 `json:"icon" db:"icon"` // Primary icon ID
|
|
||||||
Icon2 int16 `json:"icon2" db:"icon2"` // Secondary icon ID
|
|
||||||
|
|
||||||
// Ranking system
|
|
||||||
RankCost int8 `json:"rank_cost" db:"rank_cost"` // Cost per rank
|
|
||||||
MaxRank int8 `json:"max_rank" db:"max_rank"` // Maximum achievable rank
|
|
||||||
|
|
||||||
// Prerequisites
|
|
||||||
MinLevel int8 `json:"min_level" db:"min_level"` // Minimum character level
|
|
||||||
RankPrereqID int32 `json:"rank_prereq_id" db:"rank_prereq_id"` // Prerequisite AA node ID
|
|
||||||
RankPrereq int8 `json:"rank_prereq" db:"rank_prereq"` // Required rank in prerequisite
|
|
||||||
ClassReq int8 `json:"class_req" db:"class_req"` // Required class
|
|
||||||
Tier int8 `json:"tier" db:"tier"` // AA tier
|
|
||||||
ReqPoints int8 `json:"req_points" db:"req_points"` // Required points in classification
|
|
||||||
ReqTreePoints int16 `json:"req_tree_points" db:"req_tree_points"` // Required points in entire tree
|
|
||||||
|
|
||||||
// Display classification
|
|
||||||
ClassName string `json:"class_name" db:"class_name"` // Class name for display
|
|
||||||
SubclassName string `json:"subclass_name" db:"subclass_name"` // Subclass name for display
|
|
||||||
LineTitle string `json:"line_title" db:"line_title"` // AA line title
|
|
||||||
TitleLevel int8 `json:"title_level" db:"title_level"` // Title level requirement
|
|
||||||
|
|
||||||
// Metadata
|
|
||||||
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
|
||||||
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// TreeNodeData represents class-specific AA tree node configuration
|
// TreeNodeData represents class-specific AA tree node configuration
|
||||||
type TreeNodeData struct {
|
type TreeNodeData struct {
|
||||||
ClassID int32 `json:"class_id" db:"class_id"` // Character class ID
|
ClassID int32 `json:"class_id" db:"class_id"` // Character class ID
|
||||||
@ -120,7 +76,7 @@ type AATab struct {
|
|||||||
PointsAvailable int32 `json:"points_available"` // Available points for spending
|
PointsAvailable int32 `json:"points_available"` // Available points for spending
|
||||||
|
|
||||||
// AA nodes in this tab
|
// AA nodes in this tab
|
||||||
Nodes []*AltAdvanceData `json:"nodes"`
|
Nodes []*AltAdvancement `json:"nodes"`
|
||||||
|
|
||||||
// Metadata
|
// Metadata
|
||||||
LastUpdate time.Time `json:"last_update"`
|
LastUpdate time.Time `json:"last_update"`
|
||||||
@ -153,41 +109,42 @@ type AAPlayerState struct {
|
|||||||
needsSync bool `json:"-"`
|
needsSync bool `json:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// MasterAAList manages all AA definitions
|
// MasterAANodeList manages tree node configurations (kept for compatibility)
|
||||||
type MasterAAList struct {
|
type MasterAANodeList struct {
|
||||||
// AA storage
|
nodesByClass map[int32][]*TreeNodeData
|
||||||
aaList []*AltAdvanceData `json:"aa_list"`
|
nodesByTree map[int32]*TreeNodeData
|
||||||
aaBySpellID map[int32]*AltAdvanceData `json:"-"` // Fast lookup by spell ID
|
mutex sync.RWMutex
|
||||||
aaByNodeID map[int32]*AltAdvanceData `json:"-"` // Fast lookup by node ID
|
|
||||||
aaByGroup map[int8][]*AltAdvanceData `json:"-"` // Fast lookup by group/tab
|
|
||||||
|
|
||||||
// Synchronization
|
|
||||||
mutex sync.RWMutex `json:"-"`
|
|
||||||
|
|
||||||
// Statistics
|
|
||||||
totalLoaded int64 `json:"total_loaded"`
|
|
||||||
lastLoadTime time.Time `json:"last_load_time"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MasterAANodeList manages tree node configurations
|
// NewMasterAANodeList creates a new master AA node list
|
||||||
type MasterAANodeList struct {
|
func NewMasterAANodeList() *MasterAANodeList {
|
||||||
// Node storage
|
return &MasterAANodeList{
|
||||||
nodeList []*TreeNodeData `json:"node_list"`
|
nodesByClass: make(map[int32][]*TreeNodeData),
|
||||||
nodesByClass map[int32][]*TreeNodeData `json:"-"` // Fast lookup by class ID
|
nodesByTree: make(map[int32]*TreeNodeData),
|
||||||
nodesByTree map[int32]*TreeNodeData `json:"-"` // Fast lookup by tree ID
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Synchronization
|
// DestroyTreeNodes clears all tree node data
|
||||||
mutex sync.RWMutex `json:"-"`
|
func (manl *MasterAANodeList) DestroyTreeNodes() {
|
||||||
|
manl.mutex.Lock()
|
||||||
|
defer manl.mutex.Unlock()
|
||||||
|
|
||||||
// Statistics
|
manl.nodesByClass = make(map[int32][]*TreeNodeData)
|
||||||
totalLoaded int64 `json:"total_loaded"`
|
manl.nodesByTree = make(map[int32]*TreeNodeData)
|
||||||
lastLoadTime time.Time `json:"last_load_time"`
|
}
|
||||||
|
|
||||||
|
// Size returns the total number of tree nodes
|
||||||
|
func (manl *MasterAANodeList) Size() int {
|
||||||
|
manl.mutex.RLock()
|
||||||
|
defer manl.mutex.RUnlock()
|
||||||
|
|
||||||
|
return len(manl.nodesByTree)
|
||||||
}
|
}
|
||||||
|
|
||||||
// AAManager manages the entire AA system
|
// AAManager manages the entire AA system
|
||||||
type AAManager struct {
|
type AAManager struct {
|
||||||
// Core lists
|
// Core lists
|
||||||
masterAAList *MasterAAList `json:"master_aa_list"`
|
masterAAList *MasterList `json:"master_aa_list"`
|
||||||
masterNodeList *MasterAANodeList `json:"master_node_list"`
|
masterNodeList *MasterAANodeList `json:"master_node_list"`
|
||||||
|
|
||||||
// Player states
|
// Player states
|
||||||
@ -341,25 +298,12 @@ func NewAATab(tabID, group int8, name string) *AATab {
|
|||||||
ExpansionReq: EXPANSION_NONE,
|
ExpansionReq: EXPANSION_NONE,
|
||||||
PointsSpent: 0,
|
PointsSpent: 0,
|
||||||
PointsAvailable: 0,
|
PointsAvailable: 0,
|
||||||
Nodes: make([]*AltAdvanceData, 0),
|
Nodes: make([]*AltAdvancement, 0),
|
||||||
LastUpdate: time.Now(),
|
LastUpdate: time.Now(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Copy creates a deep copy of AltAdvanceData
|
// Note: AltAdvancement methods are now in alt_advancement.go
|
||||||
func (aad *AltAdvanceData) Copy() *AltAdvanceData {
|
|
||||||
copy := *aad
|
|
||||||
return ©
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsValid validates the AltAdvanceData
|
|
||||||
func (aad *AltAdvanceData) IsValid() bool {
|
|
||||||
return aad.SpellID > 0 &&
|
|
||||||
aad.NodeID > 0 &&
|
|
||||||
len(aad.Name) > 0 &&
|
|
||||||
aad.MaxRank > 0 &&
|
|
||||||
aad.RankCost > 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetMaxAAForTab returns the maximum AA points for a given tab
|
// GetMaxAAForTab returns the maximum AA points for a given tab
|
||||||
func GetMaxAAForTab(group int8) int32 {
|
func GetMaxAAForTab(group int8) int32 {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user