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 (
|
||||
"database/sql"
|
||||
"log"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
@ -67,20 +66,20 @@ type AAEventHandler interface {
|
||||
type AAValidator interface {
|
||||
// Purchase validation
|
||||
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
|
||||
|
||||
// Player validation
|
||||
ValidatePlayerLevel(playerState *AAPlayerState, aaData *AltAdvanceData) error
|
||||
ValidatePlayerClass(playerState *AAPlayerState, aaData *AltAdvanceData) error
|
||||
ValidateExpansionRequirements(playerState *AAPlayerState, aaData *AltAdvanceData) error
|
||||
ValidatePlayerLevel(playerState *AAPlayerState, aaData *AltAdvancement) error
|
||||
ValidatePlayerClass(playerState *AAPlayerState, aaData *AltAdvancement) error
|
||||
ValidateExpansionRequirements(playerState *AAPlayerState, aaData *AltAdvancement) error
|
||||
|
||||
// Template validation
|
||||
ValidateTemplateChange(playerState *AAPlayerState, templateID int8) error
|
||||
ValidateTemplateEntries(entries []*AAEntry) error
|
||||
|
||||
// System validation
|
||||
ValidateAAData(aaData *AltAdvanceData) error
|
||||
ValidateAAData(aaData *AltAdvancement) error
|
||||
ValidateTreeNodeData(nodeData *TreeNodeData) error
|
||||
}
|
||||
|
||||
@ -131,8 +130,8 @@ type AAStatistics interface {
|
||||
// AACache interface for caching AA data
|
||||
type AACache interface {
|
||||
// AA data caching
|
||||
GetAA(nodeID int32) (*AltAdvanceData, bool)
|
||||
SetAA(nodeID int32, aaData *AltAdvanceData)
|
||||
GetAA(nodeID int32) (*AltAdvancement, bool)
|
||||
SetAA(nodeID int32, aaData *AltAdvancement)
|
||||
InvalidateAA(nodeID int32)
|
||||
|
||||
// Player state caching
|
||||
@ -179,23 +178,7 @@ type Transaction interface {
|
||||
Rollback() error
|
||||
}
|
||||
|
||||
// DatabaseImpl provides a concrete implementation of AADatabase
|
||||
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,
|
||||
}
|
||||
}
|
||||
// Note: Database operations are now embedded in the main AltAdvancement type
|
||||
|
||||
// AAManagerInterface defines the main interface for AA management
|
||||
type AAManagerInterface interface {
|
||||
@ -216,7 +199,7 @@ type AAManagerInterface interface {
|
||||
// AA operations
|
||||
PurchaseAA(characterID int32, nodeID int32, targetRank int8) error
|
||||
RefundAA(characterID int32, nodeID int32) error
|
||||
GetAvailableAAs(characterID int32, tabID int8) ([]*AltAdvanceData, error)
|
||||
GetAvailableAAs(characterID int32, tabID int8) ([]*AltAdvancement, error)
|
||||
|
||||
// Template operations
|
||||
ChangeAATemplate(characterID int32, templateID int8) error
|
||||
@ -228,10 +211,10 @@ type AAManagerInterface interface {
|
||||
GetAAPoints(characterID int32) (int32, int32, int32, error) // total, spent, available
|
||||
|
||||
// Query operations
|
||||
GetAA(nodeID int32) (*AltAdvanceData, error)
|
||||
GetAABySpellID(spellID int32) (*AltAdvanceData, error)
|
||||
GetAAsByGroup(group int8) ([]*AltAdvanceData, error)
|
||||
GetAAsByClass(classID int8) ([]*AltAdvanceData, error)
|
||||
GetAA(nodeID int32) (*AltAdvancement, error)
|
||||
GetAABySpellID(spellID int32) (*AltAdvancement, error)
|
||||
GetAAsByGroup(group int8) ([]*AltAdvancement, error)
|
||||
GetAAsByClass(classID int8) ([]*AltAdvancement, error)
|
||||
|
||||
// Statistics
|
||||
GetSystemStats() *AAManagerStats
|
||||
@ -312,7 +295,7 @@ func (aa *AAAdapter) RefundAA(nodeID int32) error {
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
@ -433,7 +416,7 @@ func (caa *ClientAAAdapter) GetClientVersion() int16 {
|
||||
|
||||
// Simple cache implementation for testing
|
||||
type SimpleAACache struct {
|
||||
aaData map[int32]*AltAdvanceData
|
||||
aaData map[int32]*AltAdvancement
|
||||
playerStates map[int32]*AAPlayerState
|
||||
treeNodes map[int32]*TreeNodeData
|
||||
mutex sync.RWMutex
|
||||
@ -445,7 +428,7 @@ type SimpleAACache struct {
|
||||
// NewSimpleAACache creates a new simple cache
|
||||
func NewSimpleAACache(maxSize int32) *SimpleAACache {
|
||||
return &SimpleAACache{
|
||||
aaData: make(map[int32]*AltAdvanceData),
|
||||
aaData: make(map[int32]*AltAdvancement),
|
||||
playerStates: make(map[int32]*AAPlayerState),
|
||||
treeNodes: make(map[int32]*TreeNodeData),
|
||||
maxSize: maxSize,
|
||||
@ -453,13 +436,13 @@ func NewSimpleAACache(maxSize int32) *SimpleAACache {
|
||||
}
|
||||
|
||||
// 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()
|
||||
defer c.mutex.RUnlock()
|
||||
|
||||
if data, exists := c.aaData[nodeID]; exists {
|
||||
c.hits++
|
||||
return data.Copy(), true
|
||||
return data.Clone(), true
|
||||
}
|
||||
|
||||
c.misses++
|
||||
@ -467,7 +450,7 @@ func (c *SimpleAACache) GetAA(nodeID int32) (*AltAdvanceData, bool) {
|
||||
}
|
||||
|
||||
// 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()
|
||||
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
|
||||
@ -573,7 +556,7 @@ func (c *SimpleAACache) Clear() {
|
||||
c.mutex.Lock()
|
||||
defer c.mutex.Unlock()
|
||||
|
||||
c.aaData = make(map[int32]*AltAdvanceData)
|
||||
c.aaData = make(map[int32]*AltAdvancement)
|
||||
c.playerStates = make(map[int32]*AAPlayerState)
|
||||
c.treeNodes = make(map[int32]*TreeNodeData)
|
||||
}
|
||||
|
@ -8,7 +8,7 @@ import (
|
||||
// NewAAManager creates a new AA manager
|
||||
func NewAAManager(config AAManagerConfig) *AAManager {
|
||||
return &AAManager{
|
||||
masterAAList: NewMasterAAList(),
|
||||
masterAAList: NewMasterList(),
|
||||
masterNodeList: NewMasterAANodeList(),
|
||||
playerStates: make(map[int32]*AAPlayerState),
|
||||
config: config,
|
||||
@ -97,7 +97,7 @@ func (am *AAManager) LoadAAData() error {
|
||||
// ReloadAAData reloads all AA data
|
||||
func (am *AAManager) ReloadAAData() error {
|
||||
// Clear existing data
|
||||
am.masterAAList.DestroyAltAdvancements()
|
||||
am.masterAAList.Clear()
|
||||
am.masterNodeList.DestroyTreeNodes()
|
||||
|
||||
// Clear cached player states
|
||||
@ -194,7 +194,7 @@ func (am *AAManager) PurchaseAA(characterID int32, nodeID int32, targetRank int8
|
||||
}
|
||||
|
||||
// Get AA data
|
||||
aaData := am.masterAAList.GetAltAdvancementByNodeID(nodeID)
|
||||
aaData := am.masterAAList.GetAltAdvancement(nodeID)
|
||||
if aaData == nil {
|
||||
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
|
||||
aaData := am.masterAAList.GetAltAdvancementByNodeID(nodeID)
|
||||
aaData := am.masterAAList.GetAltAdvancement(nodeID)
|
||||
if aaData == nil {
|
||||
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
|
||||
func (am *AAManager) GetAvailableAAs(characterID int32, tabID int8) ([]*AltAdvanceData, error) {
|
||||
func (am *AAManager) GetAvailableAAs(characterID int32, tabID int8) ([]*AltAdvancement, error) {
|
||||
// Get player state
|
||||
playerState := am.getPlayerState(characterID)
|
||||
if playerState == nil {
|
||||
@ -286,8 +286,8 @@ func (am *AAManager) GetAvailableAAs(characterID int32, tabID int8) ([]*AltAdvan
|
||||
}
|
||||
|
||||
// Get all AAs for the tab
|
||||
allAAs := am.masterAAList.GetAAsByGroup(tabID)
|
||||
var availableAAs []*AltAdvanceData
|
||||
allAAs := am.masterAAList.GetAltAdvancementsByGroup(tabID)
|
||||
var availableAAs []*AltAdvancement
|
||||
|
||||
for _, aa := range allAAs {
|
||||
// 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
|
||||
func (am *AAManager) GetAA(nodeID int32) (*AltAdvanceData, error) {
|
||||
aaData := am.masterAAList.GetAltAdvancementByNodeID(nodeID)
|
||||
func (am *AAManager) GetAA(nodeID int32) (*AltAdvancement, error) {
|
||||
aaData := am.masterAAList.GetAltAdvancement(nodeID)
|
||||
if aaData == nil {
|
||||
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
|
||||
func (am *AAManager) GetAABySpellID(spellID int32) (*AltAdvanceData, error) {
|
||||
aaData := am.masterAAList.GetAltAdvancement(spellID)
|
||||
if aaData == nil {
|
||||
func (am *AAManager) GetAABySpellID(spellID int32) (*AltAdvancement, error) {
|
||||
// Search for AA with matching SpellID
|
||||
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 aaData, nil
|
||||
return found, nil
|
||||
}
|
||||
|
||||
// GetAAsByGroup returns AAs for a specific group/tab
|
||||
func (am *AAManager) GetAAsByGroup(group int8) ([]*AltAdvanceData, error) {
|
||||
return am.masterAAList.GetAAsByGroup(group), nil
|
||||
func (am *AAManager) GetAAsByGroup(group int8) ([]*AltAdvancement, error) {
|
||||
return am.masterAAList.GetAltAdvancementsByGroup(group), nil
|
||||
}
|
||||
|
||||
// GetAAsByClass returns AAs available for a specific class
|
||||
func (am *AAManager) GetAAsByClass(classID int8) ([]*AltAdvanceData, error) {
|
||||
return am.masterAAList.GetAAsByClass(classID), nil
|
||||
func (am *AAManager) GetAAsByClass(classID int8) ([]*AltAdvancement, error) {
|
||||
return am.masterAAList.GetAltAdvancementsByClass(classID), nil
|
||||
}
|
||||
|
||||
// GetSystemStats returns system statistics
|
||||
@ -540,7 +546,7 @@ func (am *AAManager) getPlayerState(characterID int32) *AAPlayerState {
|
||||
}
|
||||
|
||||
// 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
|
||||
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
|
||||
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
|
||||
// Note: This would normally get player level from the actual player object
|
||||
// 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"
|
||||
)
|
||||
|
||||
// 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
|
||||
type TreeNodeData struct {
|
||||
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
|
||||
|
||||
// AA nodes in this tab
|
||||
Nodes []*AltAdvanceData `json:"nodes"`
|
||||
Nodes []*AltAdvancement `json:"nodes"`
|
||||
|
||||
// Metadata
|
||||
LastUpdate time.Time `json:"last_update"`
|
||||
@ -153,41 +109,42 @@ type AAPlayerState struct {
|
||||
needsSync bool `json:"-"`
|
||||
}
|
||||
|
||||
// MasterAAList manages all AA definitions
|
||||
type MasterAAList struct {
|
||||
// AA storage
|
||||
aaList []*AltAdvanceData `json:"aa_list"`
|
||||
aaBySpellID map[int32]*AltAdvanceData `json:"-"` // Fast lookup by spell ID
|
||||
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 (kept for compatibility)
|
||||
type MasterAANodeList struct {
|
||||
nodesByClass map[int32][]*TreeNodeData
|
||||
nodesByTree map[int32]*TreeNodeData
|
||||
mutex sync.RWMutex
|
||||
}
|
||||
|
||||
// MasterAANodeList manages tree node configurations
|
||||
type MasterAANodeList struct {
|
||||
// Node storage
|
||||
nodeList []*TreeNodeData `json:"node_list"`
|
||||
nodesByClass map[int32][]*TreeNodeData `json:"-"` // Fast lookup by class ID
|
||||
nodesByTree map[int32]*TreeNodeData `json:"-"` // Fast lookup by tree ID
|
||||
// NewMasterAANodeList creates a new master AA node list
|
||||
func NewMasterAANodeList() *MasterAANodeList {
|
||||
return &MasterAANodeList{
|
||||
nodesByClass: make(map[int32][]*TreeNodeData),
|
||||
nodesByTree: make(map[int32]*TreeNodeData),
|
||||
}
|
||||
}
|
||||
|
||||
// Synchronization
|
||||
mutex sync.RWMutex `json:"-"`
|
||||
// DestroyTreeNodes clears all tree node data
|
||||
func (manl *MasterAANodeList) DestroyTreeNodes() {
|
||||
manl.mutex.Lock()
|
||||
defer manl.mutex.Unlock()
|
||||
|
||||
// Statistics
|
||||
totalLoaded int64 `json:"total_loaded"`
|
||||
lastLoadTime time.Time `json:"last_load_time"`
|
||||
manl.nodesByClass = make(map[int32][]*TreeNodeData)
|
||||
manl.nodesByTree = make(map[int32]*TreeNodeData)
|
||||
}
|
||||
|
||||
// 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
|
||||
type AAManager struct {
|
||||
// Core lists
|
||||
masterAAList *MasterAAList `json:"master_aa_list"`
|
||||
masterAAList *MasterList `json:"master_aa_list"`
|
||||
masterNodeList *MasterAANodeList `json:"master_node_list"`
|
||||
|
||||
// Player states
|
||||
@ -341,25 +298,12 @@ func NewAATab(tabID, group int8, name string) *AATab {
|
||||
ExpansionReq: EXPANSION_NONE,
|
||||
PointsSpent: 0,
|
||||
PointsAvailable: 0,
|
||||
Nodes: make([]*AltAdvanceData, 0),
|
||||
Nodes: make([]*AltAdvancement, 0),
|
||||
LastUpdate: time.Now(),
|
||||
}
|
||||
}
|
||||
|
||||
// Copy creates a deep copy of AltAdvanceData
|
||||
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
|
||||
}
|
||||
// Note: AltAdvancement methods are now in alt_advancement.go
|
||||
|
||||
// GetMaxAAForTab returns the maximum AA points for a given tab
|
||||
func GetMaxAAForTab(group int8) int32 {
|
||||
|
Loading…
x
Reference in New Issue
Block a user