modernize aa package

This commit is contained in:
Sky Johnson 2025-08-07 12:47:52 -05:00
parent 104a039bc0
commit c3a64dd96c
13 changed files with 991 additions and 5255 deletions

192
MODERNIZE.md Normal file
View 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 ./...
```

View File

@ -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

View 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
}

View 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")
}
}

View File

@ -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)
}

View File

@ -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
}

View 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

View File

@ -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)
}

View File

@ -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

View 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
}

View File

@ -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
}

View File

@ -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 &copy
}
// 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 {