convert more internals

This commit is contained in:
Sky Johnson 2025-07-31 11:22:03 -05:00
parent 47e6102af1
commit 812dd6716a
197 changed files with 26283 additions and 7557 deletions

5261
internal/Items.cpp Normal file

File diff suppressed because it is too large Load Diff

1298
internal/Items.h Normal file

File diff suppressed because it is too large Load Diff

View File

@ -2,22 +2,22 @@ package alt_advancement
// AA tab/group constants based on group # from DB
const (
AA_CLASS = 0 // Class-specific advancement trees
AA_SUBCLASS = 1 // Subclass-specific advancement trees
AA_SHADOW = 2 // Shadows advancement (from Shadows of Luclin)
AA_HEROIC = 3 // Heroic advancement (from Destiny of Velious)
AA_TRADESKILL = 4 // Tradeskill advancement trees
AA_PRESTIGE = 5 // Prestige advancement (from Destiny of Velious)
AA_TRADESKILL_PRESTIGE = 6 // Tradeskill prestige advancement
AA_DRAGON = 7 // Dragon advancement
AA_DRAGONCLASS = 8 // Dragon class-specific advancement
AA_FARSEAS = 9 // Far Seas advancement
AA_CLASS = 0 // Class-specific advancement trees
AA_SUBCLASS = 1 // Subclass-specific advancement trees
AA_SHADOW = 2 // Shadows advancement (from Shadows of Luclin)
AA_HEROIC = 3 // Heroic advancement (from Destiny of Velious)
AA_TRADESKILL = 4 // Tradeskill advancement trees
AA_PRESTIGE = 5 // Prestige advancement (from Destiny of Velious)
AA_TRADESKILL_PRESTIGE = 6 // Tradeskill prestige advancement
AA_DRAGON = 7 // Dragon advancement
AA_DRAGONCLASS = 8 // Dragon class-specific advancement
AA_FARSEAS = 9 // Far Seas advancement
)
// AA tab names for display
var AATabNames = map[int8]string{
AA_CLASS: "Class",
AA_SUBCLASS: "Subclass",
AA_SUBCLASS: "Subclass",
AA_SHADOW: "Shadows",
AA_HEROIC: "Heroic",
AA_TRADESKILL: "Tradeskill",
@ -30,35 +30,35 @@ var AATabNames = map[int8]string{
// Maximum AA values per tab (from C++ packet data)
const (
MAX_CLASS_AA = 100 // 0x64
MAX_SUBCLASS_AA = 100 // 0x64
MAX_SHADOWS_AA = 70 // 0x46
MAX_HEROIC_AA = 50 // 0x32
MAX_TRADESKILL_AA = 40 // 0x28
MAX_PRESTIGE_AA = 25 // 0x19
MAX_TRADESKILL_PRESTIGE_AA = 25 // 0x19
MAX_DRAGON_AA = 100 // Estimated
MAX_DRAGONCLASS_AA = 100 // Estimated
MAX_FARSEAS_AA = 100 // Estimated
MAX_CLASS_AA = 100 // 0x64
MAX_SUBCLASS_AA = 100 // 0x64
MAX_SHADOWS_AA = 70 // 0x46
MAX_HEROIC_AA = 50 // 0x32
MAX_TRADESKILL_AA = 40 // 0x28
MAX_PRESTIGE_AA = 25 // 0x19
MAX_TRADESKILL_PRESTIGE_AA = 25 // 0x19
MAX_DRAGON_AA = 100 // Estimated
MAX_DRAGONCLASS_AA = 100 // Estimated
MAX_FARSEAS_AA = 100 // Estimated
)
// AA template constants
const (
AA_TEMPLATE_PERSONAL_1 = 1 // Personal template 1
AA_TEMPLATE_PERSONAL_2 = 2 // Personal template 2
AA_TEMPLATE_PERSONAL_3 = 3 // Personal template 3
AA_TEMPLATE_SERVER_1 = 4 // Server template 1
AA_TEMPLATE_SERVER_2 = 5 // Server template 2
AA_TEMPLATE_SERVER_3 = 6 // Server template 3
AA_TEMPLATE_CURRENT = 7 // Current active template
MAX_AA_TEMPLATES = 8 // Maximum number of templates
AA_TEMPLATE_PERSONAL_1 = 1 // Personal template 1
AA_TEMPLATE_PERSONAL_2 = 2 // Personal template 2
AA_TEMPLATE_PERSONAL_3 = 3 // Personal template 3
AA_TEMPLATE_SERVER_1 = 4 // Server template 1
AA_TEMPLATE_SERVER_2 = 5 // Server template 2
AA_TEMPLATE_SERVER_3 = 6 // Server template 3
AA_TEMPLATE_CURRENT = 7 // Current active template
MAX_AA_TEMPLATES = 8 // Maximum number of templates
)
// AA template names
var AATemplateNames = map[int8]string{
AA_TEMPLATE_PERSONAL_1: "Personal 1",
AA_TEMPLATE_PERSONAL_2: "Personal 2",
AA_TEMPLATE_PERSONAL_3: "Personal 3",
AA_TEMPLATE_PERSONAL_3: "Personal 3",
AA_TEMPLATE_SERVER_1: "Server 1",
AA_TEMPLATE_SERVER_2: "Server 2",
AA_TEMPLATE_SERVER_3: "Server 3",
@ -67,98 +67,98 @@ var AATemplateNames = map[int8]string{
// AA prerequisite constants
const (
AA_PREREQ_NONE = 0 // No prerequisite
AA_PREREQ_EXPANSION = 1 // Requires specific expansion
AA_PREREQ_LEVEL = 2 // Requires minimum level
AA_PREREQ_CLASS = 3 // Requires specific class
AA_PREREQ_POINTS = 4 // Requires points spent in tree
AA_PREREQ_ACHIEVEMENT = 5 // Requires achievement completion
AA_PREREQ_NONE = 0 // No prerequisite
AA_PREREQ_EXPANSION = 1 // Requires specific expansion
AA_PREREQ_LEVEL = 2 // Requires minimum level
AA_PREREQ_CLASS = 3 // Requires specific class
AA_PREREQ_POINTS = 4 // Requires points spent in tree
AA_PREREQ_ACHIEVEMENT = 5 // Requires achievement completion
)
// Expansion requirement flags
const (
EXPANSION_NONE = 0x00 // No expansion required
EXPANSION_KOS = 0x01 // Kingdom of Sky required
EXPANSION_EOF = 0x02 // Echoes of Faydwer required
EXPANSION_ROK = 0x04 // Rise of Kunark required
EXPANSION_TSO = 0x08 // The Shadow Odyssey required
EXPANSION_SF = 0x10 // Sentinel's Fate required
EXPANSION_DOV = 0x20 // Destiny of Velious required
EXPANSION_COE = 0x40 // Chains of Eternity required
EXPANSION_TOV = 0x80 // Tears of Veeshan required
EXPANSION_NONE = 0x00 // No expansion required
EXPANSION_KOS = 0x01 // Kingdom of Sky required
EXPANSION_EOF = 0x02 // Echoes of Faydwer required
EXPANSION_ROK = 0x04 // Rise of Kunark required
EXPANSION_TSO = 0x08 // The Shadow Odyssey required
EXPANSION_SF = 0x10 // Sentinel's Fate required
EXPANSION_DOV = 0x20 // Destiny of Velious required
EXPANSION_COE = 0x40 // Chains of Eternity required
EXPANSION_TOV = 0x80 // Tears of Veeshan required
)
// AA node positioning constants
const (
MIN_AA_COL = 0 // Minimum column position
MAX_AA_COL = 10 // Maximum column position
MIN_AA_ROW = 0 // Minimum row position
MAX_AA_ROW = 15 // Maximum row position
MIN_AA_COL = 0 // Minimum column position
MAX_AA_COL = 10 // Maximum column position
MIN_AA_ROW = 0 // Minimum row position
MAX_AA_ROW = 15 // Maximum row position
)
// AA cost and rank constants
const (
MIN_RANK_COST = 1 // Minimum cost per rank
MAX_RANK_COST = 10 // Maximum cost per rank
MIN_MAX_RANK = 1 // Minimum maximum rank
MAX_MAX_RANK = 20 // Maximum maximum rank
MIN_TITLE_LEVEL = 1 // Minimum title level
MAX_TITLE_LEVEL = 100 // Maximum title level
MIN_RANK_COST = 1 // Minimum cost per rank
MAX_RANK_COST = 10 // Maximum cost per rank
MIN_MAX_RANK = 1 // Minimum maximum rank
MAX_MAX_RANK = 20 // Maximum maximum rank
MIN_TITLE_LEVEL = 1 // Minimum title level
MAX_TITLE_LEVEL = 100 // Maximum title level
)
// AA packet operation codes
const (
OP_ADVENTURE_LIST = 0x023B // Adventure list packet opcode
OP_AA_UPDATE = 0x024C // AA update packet opcode
OP_AA_PURCHASE = 0x024D // AA purchase packet opcode
OP_ADVENTURE_LIST = 0x023B // Adventure list packet opcode
OP_AA_UPDATE = 0x024C // AA update packet opcode
OP_AA_PURCHASE = 0x024D // AA purchase packet opcode
)
// AA display modes
const (
AA_DISPLAY_NEW = 0 // New template display
AA_DISPLAY_CHANGE = 1 // Change template display
AA_DISPLAY_UPDATE = 2 // Update existing display
AA_DISPLAY_NEW = 0 // New template display
AA_DISPLAY_CHANGE = 1 // Change template display
AA_DISPLAY_UPDATE = 2 // Update existing display
)
// AA validation constants
const (
MIN_SPELL_ID = 1 // Minimum valid spell ID
MAX_SPELL_ID = 2147483647 // Maximum valid spell ID
MIN_NODE_ID = 1 // Minimum valid node ID
MAX_NODE_ID = 2147483647 // Maximum valid node ID
MIN_SPELL_ID = 1 // Minimum valid spell ID
MAX_SPELL_ID = 2147483647 // Maximum valid spell ID
MIN_NODE_ID = 1 // Minimum valid node ID
MAX_NODE_ID = 2147483647 // Maximum valid node ID
)
// AA processing constants
const (
AA_PROCESSING_BATCH_SIZE = 100 // Batch size for processing AAs
AA_CACHE_SIZE = 10000 // Cache size for AA data
AA_UPDATE_INTERVAL = 1000 // Update interval in milliseconds
AA_PROCESSING_BATCH_SIZE = 100 // Batch size for processing AAs
AA_CACHE_SIZE = 10000 // Cache size for AA data
AA_UPDATE_INTERVAL = 1000 // Update interval in milliseconds
)
// AA error codes
const (
AA_ERROR_NONE = 0 // No error
AA_ERROR_INVALID_SPELL_ID = 1 // Invalid spell ID
AA_ERROR_INVALID_NODE_ID = 2 // Invalid node ID
AA_ERROR_INSUFFICIENT_POINTS = 3 // Insufficient AA points
AA_ERROR_PREREQ_NOT_MET = 4 // Prerequisites not met
AA_ERROR_MAX_RANK_REACHED = 5 // Maximum rank already reached
AA_ERROR_INVALID_CLASS = 6 // Invalid class for this AA
AA_ERROR_EXPANSION_REQUIRED = 7 // Required expansion not owned
AA_ERROR_LEVEL_TOO_LOW = 8 // Character level too low
AA_ERROR_TREE_LOCKED = 9 // AA tree is locked
AA_ERROR_DATABASE_ERROR = 10 // Database operation failed
AA_ERROR_NONE = 0 // No error
AA_ERROR_INVALID_SPELL_ID = 1 // Invalid spell ID
AA_ERROR_INVALID_NODE_ID = 2 // Invalid node ID
AA_ERROR_INSUFFICIENT_POINTS = 3 // Insufficient AA points
AA_ERROR_PREREQ_NOT_MET = 4 // Prerequisites not met
AA_ERROR_MAX_RANK_REACHED = 5 // Maximum rank already reached
AA_ERROR_INVALID_CLASS = 6 // Invalid class for this AA
AA_ERROR_EXPANSION_REQUIRED = 7 // Required expansion not owned
AA_ERROR_LEVEL_TOO_LOW = 8 // Character level too low
AA_ERROR_TREE_LOCKED = 9 // AA tree is locked
AA_ERROR_DATABASE_ERROR = 10 // Database operation failed
)
// AA statistic tracking constants
const (
STAT_TOTAL_AAS_LOADED = "total_aas_loaded"
STAT_TOTAL_NODES_LOADED = "total_nodes_loaded"
STAT_AAS_PER_TAB = "aas_per_tab"
STAT_PLAYER_AA_PURCHASES = "player_aa_purchases"
STAT_CACHE_HITS = "cache_hits"
STAT_CACHE_MISSES = "cache_misses"
STAT_DATABASE_QUERIES = "database_queries"
STAT_TOTAL_AAS_LOADED = "total_aas_loaded"
STAT_TOTAL_NODES_LOADED = "total_nodes_loaded"
STAT_AAS_PER_TAB = "aas_per_tab"
STAT_PLAYER_AA_PURCHASES = "player_aa_purchases"
STAT_CACHE_HITS = "cache_hits"
STAT_CACHE_MISSES = "cache_misses"
STAT_DATABASE_QUERIES = "database_queries"
)
// Default AA configuration values
@ -169,4 +169,4 @@ const (
DEFAULT_ENABLE_AA_LOGGING = false
DEFAULT_AA_POINTS_PER_LEVEL = 2
DEFAULT_AA_MAX_BANKED_POINTS = 30
)
)

View File

@ -289,7 +289,7 @@ func (db *DatabaseImpl) initializePlayerTabs(playerState *AAPlayerState) {
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 {
@ -493,7 +493,7 @@ func (db *DatabaseImpl) DeletePlayerAA(characterID int32) error {
// Delete from all related tables
tables := []string{
"character_aa_points",
"character_aa_progress",
"character_aa_progress",
"character_aa",
}
@ -561,4 +561,4 @@ func (db *DatabaseImpl) GetAAStatistics() (map[string]interface{}, error) {
stats["popular_aas"] = popularAAs
return stats, nil
}
}

View File

@ -3,6 +3,7 @@ package alt_advancement
import (
"database/sql"
"log"
"sync"
"time"
)
@ -11,15 +12,15 @@ type AADatabase interface {
// Core data loading
LoadAltAdvancements() error
LoadTreeNodes() error
// Player data operations
LoadPlayerAA(characterID int32) (*AAPlayerState, error)
SavePlayerAA(playerState *AAPlayerState) error
DeletePlayerAA(characterID int32) error
// Template operations
LoadPlayerAADefaults(classID int8) (map[int8][]*AAEntry, error)
// Statistics
GetAAStatistics() (map[string]interface{}, error)
}
@ -29,15 +30,15 @@ type AAPacketHandler interface {
// List packets
GetAAListPacket(client interface{}) ([]byte, error)
SendAAUpdate(client interface{}, playerState *AAPlayerState) error
// Purchase packets
HandleAAPurchase(client interface{}, nodeID int32, rank int8) error
SendAAPurchaseResponse(client interface{}, success bool, nodeID int32, newRank int8) error
// Template packets
SendAATemplateList(client interface{}, templates map[int8]*AATemplate) error
HandleAATemplateChange(client interface{}, templateID int8) error
// Display packets
DisplayAA(client interface{}, templateID int8, changeMode int8) error
SendAATabUpdate(client interface{}, tabID int8, tab *AATab) error
@ -48,15 +49,15 @@ type AAEventHandler interface {
// Purchase events
OnAAPurchased(characterID int32, nodeID int32, newRank int8, pointsSpent int32) error
OnAARefunded(characterID int32, nodeID int32, oldRank int8, pointsRefunded int32) error
// Template events
OnAATemplateChanged(characterID int32, oldTemplate, newTemplate int8) error
OnAATemplateCreated(characterID int32, templateID int8, name string) error
// System events
OnAASystemLoaded(totalAAs int32, totalNodes int32) error
OnAADataReloaded() error
// Player events
OnPlayerAALoaded(characterID int32, playerState *AAPlayerState) error
OnPlayerAAPointsChanged(characterID int32, oldPoints, newPoints int32) error
@ -68,16 +69,16 @@ type AAValidator interface {
ValidateAAPurchase(playerState *AAPlayerState, nodeID int32, targetRank int8) error
ValidateAAPrerequisites(playerState *AAPlayerState, aaData *AltAdvanceData) 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
// Template validation
ValidateTemplateChange(playerState *AAPlayerState, templateID int8) error
ValidateTemplateEntries(entries []*AAEntry) error
// System validation
ValidateAAData(aaData *AltAdvanceData) error
ValidateTreeNodeData(nodeData *TreeNodeData) error
@ -89,15 +90,15 @@ type AANotifier interface {
NotifyAAPurchaseSuccess(characterID int32, aaName string, newRank int8) error
NotifyAAPurchaseFailure(characterID int32, reason string) error
NotifyAARefund(characterID int32, aaName string, pointsRefunded int32) error
// Progress notifications
NotifyAAProgressUpdate(characterID int32, tabID int8, pointsSpent int32) error
NotifyAAPointsAwarded(characterID int32, pointsAwarded int32, reason string) error
// System notifications
NotifyAASystemUpdate(message string) error
NotifyAASystemMaintenance(maintenanceStart time.Time, duration time.Duration) error
// Achievement notifications
NotifyAAMilestone(characterID int32, milestone string, totalPoints int32) error
NotifyAATreeCompleted(characterID int32, tabID int8, tabName string) error
@ -108,18 +109,18 @@ type AAStatistics interface {
// Purchase statistics
RecordAAPurchase(characterID int32, nodeID int32, pointsSpent int32)
RecordAARefund(characterID int32, nodeID int32, pointsRefunded int32)
// Usage statistics
RecordAAUsage(characterID int32, nodeID int32, usageType string)
RecordPlayerLogin(characterID int32, totalAAPoints int32)
RecordPlayerLogout(characterID int32, sessionDuration time.Duration)
// Performance statistics
RecordDatabaseQuery(queryType string, duration time.Duration)
RecordPacketSent(packetType string, size int32)
RecordCacheHit(cacheType string)
RecordCacheMiss(cacheType string)
// Aggregated statistics
GetAAPurchaseStats() map[int32]int64
GetPopularAAs() map[int32]int64
@ -133,17 +134,17 @@ type AACache interface {
GetAA(nodeID int32) (*AltAdvanceData, bool)
SetAA(nodeID int32, aaData *AltAdvanceData)
InvalidateAA(nodeID int32)
// Player state caching
GetPlayerState(characterID int32) (*AAPlayerState, bool)
SetPlayerState(characterID int32, playerState *AAPlayerState)
InvalidatePlayerState(characterID int32)
// Tree node caching
GetTreeNode(treeID int32) (*TreeNodeData, bool)
SetTreeNode(treeID int32, nodeData *TreeNodeData)
InvalidateTreeNode(treeID int32)
// Cache management
Clear()
GetStats() map[string]interface{}
@ -202,44 +203,44 @@ type AAManagerInterface interface {
Start() error
Stop() error
IsRunning() bool
// Data loading
LoadAAData() error
ReloadAAData() error
// Player operations
LoadPlayerAA(characterID int32) (*AAPlayerState, error)
SavePlayerAA(characterID int32) error
GetPlayerAAState(characterID int32) (*AAPlayerState, error)
// AA operations
PurchaseAA(characterID int32, nodeID int32, targetRank int8) error
RefundAA(characterID int32, nodeID int32) error
GetAvailableAAs(characterID int32, tabID int8) ([]*AltAdvanceData, error)
// Template operations
ChangeAATemplate(characterID int32, templateID int8) error
SaveAATemplate(characterID int32, templateID int8, name string) error
GetAATemplates(characterID int32) (map[int8]*AATemplate, error)
// Point operations
AwardAAPoints(characterID int32, points int32, reason string) error
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)
// Statistics
GetSystemStats() *AAManagerStats
GetPlayerStats(characterID int32) map[string]interface{}
// Configuration
SetConfig(config AAManagerConfig) error
GetConfig() AAManagerConfig
// Integration
SetDatabase(db AADatabase)
SetPacketHandler(handler AAPacketHandler)
@ -257,13 +258,13 @@ type AAAware interface {
SetAAPoints(total, spent, available int32)
AwardAAPoints(points int32, reason string) error
SpendAAPoints(points int32) error
// AA progression
GetAAState() *AAPlayerState
SetAAState(state *AAPlayerState)
GetAARank(nodeID int32) int8
SetAARank(nodeID int32, rank int8) error
// Template management
GetActiveAATemplate() int8
SetActiveAATemplate(templateID int8) error
@ -273,7 +274,7 @@ type AAAware interface {
// AAAdapter adapts AA functionality for other systems
type AAAdapter struct {
manager AAManagerInterface
manager AAManagerInterface
characterID int32
}
@ -455,12 +456,12 @@ func NewSimpleAACache(maxSize int32) *SimpleAACache {
func (c *SimpleAACache) GetAA(nodeID int32) (*AltAdvanceData, bool) {
c.mutex.RLock()
defer c.mutex.RUnlock()
if data, exists := c.aaData[nodeID]; exists {
c.hits++
return data.Copy(), true
}
c.misses++
return nil, false
}
@ -469,7 +470,7 @@ func (c *SimpleAACache) GetAA(nodeID int32) (*AltAdvanceData, bool) {
func (c *SimpleAACache) SetAA(nodeID int32, aaData *AltAdvanceData) {
c.mutex.Lock()
defer c.mutex.Unlock()
if int32(len(c.aaData)) >= c.maxSize {
// Simple eviction: remove a random entry
for k := range c.aaData {
@ -477,7 +478,7 @@ func (c *SimpleAACache) SetAA(nodeID int32, aaData *AltAdvanceData) {
break
}
}
c.aaData[nodeID] = aaData.Copy()
}
@ -485,7 +486,7 @@ func (c *SimpleAACache) SetAA(nodeID int32, aaData *AltAdvanceData) {
func (c *SimpleAACache) InvalidateAA(nodeID int32) {
c.mutex.Lock()
defer c.mutex.Unlock()
delete(c.aaData, nodeID)
}
@ -493,12 +494,12 @@ func (c *SimpleAACache) InvalidateAA(nodeID int32) {
func (c *SimpleAACache) GetPlayerState(characterID int32) (*AAPlayerState, bool) {
c.mutex.RLock()
defer c.mutex.RUnlock()
if state, exists := c.playerStates[characterID]; exists {
c.hits++
return state, true
}
c.misses++
return nil, false
}
@ -507,7 +508,7 @@ func (c *SimpleAACache) GetPlayerState(characterID int32) (*AAPlayerState, bool)
func (c *SimpleAACache) SetPlayerState(characterID int32, playerState *AAPlayerState) {
c.mutex.Lock()
defer c.mutex.Unlock()
if int32(len(c.playerStates)) >= c.maxSize {
// Simple eviction: remove a random entry
for k := range c.playerStates {
@ -515,7 +516,7 @@ func (c *SimpleAACache) SetPlayerState(characterID int32, playerState *AAPlayerS
break
}
}
c.playerStates[characterID] = playerState
}
@ -523,7 +524,7 @@ func (c *SimpleAACache) SetPlayerState(characterID int32, playerState *AAPlayerS
func (c *SimpleAACache) InvalidatePlayerState(characterID int32) {
c.mutex.Lock()
defer c.mutex.Unlock()
delete(c.playerStates, characterID)
}
@ -531,13 +532,13 @@ func (c *SimpleAACache) InvalidatePlayerState(characterID int32) {
func (c *SimpleAACache) GetTreeNode(treeID int32) (*TreeNodeData, bool) {
c.mutex.RLock()
defer c.mutex.RUnlock()
if node, exists := c.treeNodes[treeID]; exists {
c.hits++
nodeCopy := *node
return &nodeCopy, true
}
c.misses++
return nil, false
}
@ -546,7 +547,7 @@ func (c *SimpleAACache) GetTreeNode(treeID int32) (*TreeNodeData, bool) {
func (c *SimpleAACache) SetTreeNode(treeID int32, nodeData *TreeNodeData) {
c.mutex.Lock()
defer c.mutex.Unlock()
if int32(len(c.treeNodes)) >= c.maxSize {
// Simple eviction: remove a random entry
for k := range c.treeNodes {
@ -554,7 +555,7 @@ func (c *SimpleAACache) SetTreeNode(treeID int32, nodeData *TreeNodeData) {
break
}
}
nodeCopy := *nodeData
c.treeNodes[treeID] = &nodeCopy
}
@ -563,7 +564,7 @@ func (c *SimpleAACache) SetTreeNode(treeID int32, nodeData *TreeNodeData) {
func (c *SimpleAACache) InvalidateTreeNode(treeID int32) {
c.mutex.Lock()
defer c.mutex.Unlock()
delete(c.treeNodes, treeID)
}
@ -571,7 +572,7 @@ func (c *SimpleAACache) InvalidateTreeNode(treeID int32) {
func (c *SimpleAACache) Clear() {
c.mutex.Lock()
defer c.mutex.Unlock()
c.aaData = make(map[int32]*AltAdvanceData)
c.playerStates = make(map[int32]*AAPlayerState)
c.treeNodes = make(map[int32]*TreeNodeData)
@ -581,14 +582,14 @@ func (c *SimpleAACache) Clear() {
func (c *SimpleAACache) GetStats() map[string]interface{} {
c.mutex.RLock()
defer c.mutex.RUnlock()
return map[string]interface{}{
"hits": c.hits,
"misses": c.misses,
"aa_data_count": len(c.aaData),
"player_count": len(c.playerStates),
"tree_node_count": len(c.treeNodes),
"max_size": c.maxSize,
"hits": c.hits,
"misses": c.misses,
"aa_data_count": len(c.aaData),
"player_count": len(c.playerStates),
"tree_node_count": len(c.treeNodes),
"max_size": c.maxSize,
}
}
@ -596,6 +597,6 @@ func (c *SimpleAACache) GetStats() map[string]interface{} {
func (c *SimpleAACache) SetMaxSize(maxSize int32) {
c.mutex.Lock()
defer c.mutex.Unlock()
c.maxSize = maxSize
}
}

View File

@ -2,7 +2,6 @@ package alt_advancement
import (
"fmt"
"sync"
"time"
)
@ -44,12 +43,12 @@ func (am *AAManager) Start() error {
func (am *AAManager) Stop() error {
close(am.stopChan)
am.wg.Wait()
// Save all player states if auto-save is enabled
if am.config.AutoSave {
am.saveAllPlayerStates()
}
return nil
}
@ -482,7 +481,7 @@ func (am *AAManager) SetPacketHandler(handler AAPacketHandler) {
func (am *AAManager) SetEventHandler(handler AAEventHandler) {
am.eventMutex.Lock()
defer am.eventMutex.Unlock()
am.eventHandlers = append(am.eventHandlers, handler)
}
@ -512,7 +511,7 @@ func (am *AAManager) SetCache(cache AACache) {
func (am *AAManager) getPlayerState(characterID int32) *AAPlayerState {
am.statesMutex.RLock()
defer am.statesMutex.RUnlock()
return am.playerStates[characterID]
}
@ -534,14 +533,14 @@ func (am *AAManager) performAAPurchase(playerState *AAPlayerState, aaData *AltAd
progress := playerState.AAProgress[aaData.NodeID]
if progress == nil {
progress = &PlayerAAData{
CharacterID: playerState.CharacterID,
NodeID: aaData.NodeID,
CurrentRank: 0,
PointsSpent: 0,
TemplateID: playerState.ActiveTemplate,
TabID: aaData.Group,
PurchasedAt: time.Now(),
UpdatedAt: time.Now(),
CharacterID: playerState.CharacterID,
NodeID: aaData.NodeID,
CurrentRank: 0,
PointsSpent: 0,
TemplateID: playerState.ActiveTemplate,
TabID: aaData.Group,
PurchasedAt: time.Now(),
UpdatedAt: time.Now(),
}
playerState.AAProgress[aaData.NodeID] = progress
}
@ -760,4 +759,4 @@ func (am *AAManager) firePlayerAAPointsChangedEvent(characterID int32, oldPoints
for _, handler := range am.eventHandlers {
go handler.OnPlayerAAPointsChanged(characterID, oldPoints, newPoints)
}
}
}

View File

@ -3,7 +3,6 @@ package alt_advancement
import (
"fmt"
"sort"
"sync"
"time"
)
@ -467,10 +466,10 @@ func (manl *MasterAANodeList) BuildAATreeMap(classID int32) map[int8]int32 {
// 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

@ -8,287 +8,287 @@ import (
// 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"`
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"`
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
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
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
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
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
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"`
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
TreeID int32 `json:"tree_id" db:"tree_id"` // Tree node identifier
AATreeID int32 `json:"aa_tree_id" db:"aa_tree_id"` // AA tree classification ID
ClassID int32 `json:"class_id" db:"class_id"` // Character class ID
TreeID int32 `json:"tree_id" db:"tree_id"` // Tree node identifier
AATreeID int32 `json:"aa_tree_id" db:"aa_tree_id"` // AA tree classification ID
}
// AAEntry represents a player's AA entry in a template
type AAEntry struct {
TemplateID int8 `json:"template_id" db:"template_id"` // Template identifier (1-8)
TabID int8 `json:"tab_id" db:"tab_id"` // Tab identifier
AAID int32 `json:"aa_id" db:"aa_id"` // AA node ID
Order int16 `json:"order" db:"order"` // Display order
TreeID int8 `json:"tree_id" db:"tree_id"` // Tree identifier
TemplateID int8 `json:"template_id" db:"template_id"` // Template identifier (1-8)
TabID int8 `json:"tab_id" db:"tab_id"` // Tab identifier
AAID int32 `json:"aa_id" db:"aa_id"` // AA node ID
Order int16 `json:"order" db:"order"` // Display order
TreeID int8 `json:"tree_id" db:"tree_id"` // Tree identifier
}
// PlayerAAData represents a player's AA progression
type PlayerAAData struct {
// Player identification
CharacterID int32 `json:"character_id" db:"character_id"`
CharacterID int32 `json:"character_id" db:"character_id"`
// AA progression
NodeID int32 `json:"node_id" db:"node_id"` // AA node ID
CurrentRank int8 `json:"current_rank" db:"current_rank"` // Current rank in this AA
PointsSpent int32 `json:"points_spent" db:"points_spent"` // Total points spent on this AA
NodeID int32 `json:"node_id" db:"node_id"` // AA node ID
CurrentRank int8 `json:"current_rank" db:"current_rank"` // Current rank in this AA
PointsSpent int32 `json:"points_spent" db:"points_spent"` // Total points spent on this AA
// Template assignment
TemplateID int8 `json:"template_id" db:"template_id"` // Template this AA belongs to
TabID int8 `json:"tab_id" db:"tab_id"` // Tab this AA belongs to
Order int16 `json:"order" db:"order"` // Display order
TemplateID int8 `json:"template_id" db:"template_id"` // Template this AA belongs to
TabID int8 `json:"tab_id" db:"tab_id"` // Tab this AA belongs to
Order int16 `json:"order" db:"order"` // Display order
// Timestamps
PurchasedAt time.Time `json:"purchased_at" db:"purchased_at"`
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
PurchasedAt time.Time `json:"purchased_at" db:"purchased_at"`
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
}
// AATemplate represents an AA template configuration
type AATemplate struct {
// Template identification
TemplateID int8 `json:"template_id"`
Name string `json:"name"`
Description string `json:"description"`
IsPersonal bool `json:"is_personal"` // True for personal templates (1-3)
IsServer bool `json:"is_server"` // True for server templates (4-6)
IsCurrent bool `json:"is_current"` // True for current active template
TemplateID int8 `json:"template_id"`
Name string `json:"name"`
Description string `json:"description"`
IsPersonal bool `json:"is_personal"` // True for personal templates (1-3)
IsServer bool `json:"is_server"` // True for server templates (4-6)
IsCurrent bool `json:"is_current"` // True for current active template
// Template data
Entries []*AAEntry `json:"entries"` // AA entries in this template
Entries []*AAEntry `json:"entries"` // AA entries in this template
// Metadata
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// AATab represents an AA tab with its associated data
type AATab struct {
// Tab identification
TabID int8 `json:"tab_id"`
Group int8 `json:"group"`
Name string `json:"name"`
TabID int8 `json:"tab_id"`
Group int8 `json:"group"`
Name string `json:"name"`
// Tab configuration
MaxAA int32 `json:"max_aa"` // Maximum AA points for this tab
ClassID int32 `json:"class_id"` // Associated class ID
ExpansionReq int8 `json:"expansion_req"` // Required expansion flags
MaxAA int32 `json:"max_aa"` // Maximum AA points for this tab
ClassID int32 `json:"class_id"` // Associated class ID
ExpansionReq int8 `json:"expansion_req"` // Required expansion flags
// Current state
PointsSpent int32 `json:"points_spent"` // Points spent in this tab
PointsSpent int32 `json:"points_spent"` // Points spent in this tab
PointsAvailable int32 `json:"points_available"` // Available points for spending
// AA nodes in this tab
Nodes []*AltAdvanceData `json:"nodes"`
Nodes []*AltAdvanceData `json:"nodes"`
// Metadata
LastUpdate time.Time `json:"last_update"`
LastUpdate time.Time `json:"last_update"`
}
// AAPlayerState represents a player's complete AA state
type AAPlayerState struct {
// Player identification
CharacterID int32 `json:"character_id"`
CharacterID int32 `json:"character_id"`
// AA points
TotalPoints int32 `json:"total_points"` // Total AA points earned
SpentPoints int32 `json:"spent_points"` // Total AA points spent
TotalPoints int32 `json:"total_points"` // Total AA points earned
SpentPoints int32 `json:"spent_points"` // Total AA points spent
AvailablePoints int32 `json:"available_points"` // Available AA points
BankedPoints int32 `json:"banked_points"` // Banked AA points
BankedPoints int32 `json:"banked_points"` // Banked AA points
// Templates
ActiveTemplate int8 `json:"active_template"` // Currently active template
Templates map[int8]*AATemplate `json:"templates"` // All templates
ActiveTemplate int8 `json:"active_template"` // Currently active template
Templates map[int8]*AATemplate `json:"templates"` // All templates
// Tab states
Tabs map[int8]*AATab `json:"tabs"` // Tab states
Tabs map[int8]*AATab `json:"tabs"` // Tab states
// Player AA progression
AAProgress map[int32]*PlayerAAData `json:"aa_progress"` // AA node progress by node ID
AAProgress map[int32]*PlayerAAData `json:"aa_progress"` // AA node progress by node ID
// Caching and synchronization
mutex sync.RWMutex `json:"-"`
lastUpdate time.Time `json:"last_update"`
needsSync bool `json:"-"`
mutex sync.RWMutex `json:"-"`
lastUpdate time.Time `json:"last_update"`
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
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:"-"`
mutex sync.RWMutex `json:"-"`
// Statistics
totalLoaded int64 `json:"total_loaded"`
lastLoadTime time.Time `json:"last_load_time"`
totalLoaded int64 `json:"total_loaded"`
lastLoadTime time.Time `json:"last_load_time"`
}
// 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
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
// Synchronization
mutex sync.RWMutex `json:"-"`
mutex sync.RWMutex `json:"-"`
// Statistics
totalLoaded int64 `json:"total_loaded"`
lastLoadTime time.Time `json:"last_load_time"`
totalLoaded int64 `json:"total_loaded"`
lastLoadTime time.Time `json:"last_load_time"`
}
// AAManager manages the entire AA system
type AAManager struct {
// Core lists
masterAAList *MasterAAList `json:"master_aa_list"`
masterNodeList *MasterAANodeList `json:"master_node_list"`
masterAAList *MasterAAList `json:"master_aa_list"`
masterNodeList *MasterAANodeList `json:"master_node_list"`
// Player states
playerStates map[int32]*AAPlayerState `json:"-"` // Player AA states by character ID
statesMutex sync.RWMutex `json:"-"`
playerStates map[int32]*AAPlayerState `json:"-"` // Player AA states by character ID
statesMutex sync.RWMutex `json:"-"`
// Configuration
config AAManagerConfig `json:"config"`
config AAManagerConfig `json:"config"`
// Database interface
database AADatabase `json:"-"`
database AADatabase `json:"-"`
// Packet handler
packetHandler AAPacketHandler `json:"-"`
packetHandler AAPacketHandler `json:"-"`
// Event handlers
eventHandlers []AAEventHandler `json:"-"`
eventMutex sync.RWMutex `json:"-"`
eventHandlers []AAEventHandler `json:"-"`
eventMutex sync.RWMutex `json:"-"`
// Statistics
stats AAManagerStats `json:"stats"`
statsMutex sync.RWMutex `json:"-"`
stats AAManagerStats `json:"stats"`
statsMutex sync.RWMutex `json:"-"`
// Background processing
stopChan chan struct{} `json:"-"`
wg sync.WaitGroup `json:"-"`
stopChan chan struct{} `json:"-"`
wg sync.WaitGroup `json:"-"`
}
// AAManagerConfig holds configuration for the AA manager
type AAManagerConfig struct {
// System settings
EnableAASystem bool `json:"enable_aa_system"`
EnableCaching bool `json:"enable_caching"`
EnableValidation bool `json:"enable_validation"`
EnableLogging bool `json:"enable_logging"`
EnableAASystem bool `json:"enable_aa_system"`
EnableCaching bool `json:"enable_caching"`
EnableValidation bool `json:"enable_validation"`
EnableLogging bool `json:"enable_logging"`
// Player settings
AAPointsPerLevel int32 `json:"aa_points_per_level"`
MaxBankedPoints int32 `json:"max_banked_points"`
EnableAABanking bool `json:"enable_aa_banking"`
AAPointsPerLevel int32 `json:"aa_points_per_level"`
MaxBankedPoints int32 `json:"max_banked_points"`
EnableAABanking bool `json:"enable_aa_banking"`
// Performance settings
CacheSize int32 `json:"cache_size"`
UpdateInterval time.Duration `json:"update_interval"`
BatchSize int32 `json:"batch_size"`
CacheSize int32 `json:"cache_size"`
UpdateInterval time.Duration `json:"update_interval"`
BatchSize int32 `json:"batch_size"`
// Database settings
DatabaseEnabled bool `json:"database_enabled"`
AutoSave bool `json:"auto_save"`
SaveInterval time.Duration `json:"save_interval"`
DatabaseEnabled bool `json:"database_enabled"`
AutoSave bool `json:"auto_save"`
SaveInterval time.Duration `json:"save_interval"`
}
// AAManagerStats holds statistics about the AA system
type AAManagerStats struct {
// Loading statistics
TotalAAsLoaded int64 `json:"total_aas_loaded"`
TotalNodesLoaded int64 `json:"total_nodes_loaded"`
LastLoadTime time.Time `json:"last_load_time"`
LoadDuration time.Duration `json:"load_duration"`
// Player statistics
ActivePlayers int64 `json:"active_players"`
TotalAAPurchases int64 `json:"total_aa_purchases"`
TotalPointsSpent int64 `json:"total_points_spent"`
AveragePointsSpent float64 `json:"average_points_spent"`
TotalAAsLoaded int64 `json:"total_aas_loaded"`
TotalNodesLoaded int64 `json:"total_nodes_loaded"`
LastLoadTime time.Time `json:"last_load_time"`
LoadDuration time.Duration `json:"load_duration"`
// Player statistics
ActivePlayers int64 `json:"active_players"`
TotalAAPurchases int64 `json:"total_aa_purchases"`
TotalPointsSpent int64 `json:"total_points_spent"`
AveragePointsSpent float64 `json:"average_points_spent"`
// Performance statistics
CacheHits int64 `json:"cache_hits"`
CacheMisses int64 `json:"cache_misses"`
DatabaseQueries int64 `json:"database_queries"`
PacketsSent int64 `json:"packets_sent"`
CacheHits int64 `json:"cache_hits"`
CacheMisses int64 `json:"cache_misses"`
DatabaseQueries int64 `json:"database_queries"`
PacketsSent int64 `json:"packets_sent"`
// Tab usage statistics
TabUsage map[int8]int64 `json:"tab_usage"`
PopularAAs map[int32]int64 `json:"popular_aas"`
TabUsage map[int8]int64 `json:"tab_usage"`
PopularAAs map[int32]int64 `json:"popular_aas"`
// Error statistics
ValidationErrors int64 `json:"validation_errors"`
DatabaseErrors int64 `json:"database_errors"`
PacketErrors int64 `json:"packet_errors"`
ValidationErrors int64 `json:"validation_errors"`
DatabaseErrors int64 `json:"database_errors"`
PacketErrors int64 `json:"packet_errors"`
// Timing statistics
LastStatsUpdate time.Time `json:"last_stats_update"`
LastStatsUpdate time.Time `json:"last_stats_update"`
}
// DefaultAAManagerConfig returns a default configuration
func DefaultAAManagerConfig() AAManagerConfig {
return AAManagerConfig{
EnableAASystem: DEFAULT_ENABLE_AA_SYSTEM,
EnableCaching: DEFAULT_ENABLE_AA_CACHING,
EnableValidation: DEFAULT_ENABLE_AA_VALIDATION,
EnableLogging: DEFAULT_ENABLE_AA_LOGGING,
AAPointsPerLevel: DEFAULT_AA_POINTS_PER_LEVEL,
MaxBankedPoints: DEFAULT_AA_MAX_BANKED_POINTS,
EnableAABanking: true,
CacheSize: AA_CACHE_SIZE,
UpdateInterval: time.Duration(AA_UPDATE_INTERVAL) * time.Millisecond,
BatchSize: AA_PROCESSING_BATCH_SIZE,
DatabaseEnabled: true,
AutoSave: true,
SaveInterval: 5 * time.Minute,
EnableAASystem: DEFAULT_ENABLE_AA_SYSTEM,
EnableCaching: DEFAULT_ENABLE_AA_CACHING,
EnableValidation: DEFAULT_ENABLE_AA_VALIDATION,
EnableLogging: DEFAULT_ENABLE_AA_LOGGING,
AAPointsPerLevel: DEFAULT_AA_POINTS_PER_LEVEL,
MaxBankedPoints: DEFAULT_AA_MAX_BANKED_POINTS,
EnableAABanking: true,
CacheSize: AA_CACHE_SIZE,
UpdateInterval: time.Duration(AA_UPDATE_INTERVAL) * time.Millisecond,
BatchSize: AA_PROCESSING_BATCH_SIZE,
DatabaseEnabled: true,
AutoSave: true,
SaveInterval: 5 * time.Minute,
}
}
@ -348,11 +348,11 @@ func (aad *AltAdvanceData) Copy() *AltAdvanceData {
// 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
return aad.SpellID > 0 &&
aad.NodeID > 0 &&
len(aad.Name) > 0 &&
aad.MaxRank > 0 &&
aad.RankCost > 0
}
// GetMaxAAForTab returns the maximum AA points for a given tab
@ -402,4 +402,4 @@ func GetTemplateName(templateID int8) string {
// IsExpansionRequired checks if a specific expansion is required
func IsExpansionRequired(flags int8, expansion int8) bool {
return (flags & expansion) != 0
}
}

View File

@ -27,7 +27,7 @@ func (a *Appearances) Reset() {
func (a *Appearances) ClearAppearances() {
a.mutex.Lock()
defer a.mutex.Unlock()
// Clear the map - Go's garbage collector will handle cleanup
a.appearanceMap = make(map[int32]*Appearance)
}
@ -37,10 +37,10 @@ func (a *Appearances) InsertAppearance(appearance *Appearance) error {
if appearance == nil {
return fmt.Errorf("appearance cannot be nil")
}
a.mutex.Lock()
defer a.mutex.Unlock()
a.appearanceMap[appearance.GetID()] = appearance
return nil
}
@ -49,11 +49,11 @@ func (a *Appearances) InsertAppearance(appearance *Appearance) error {
func (a *Appearances) FindAppearanceByID(id int32) *Appearance {
a.mutex.RLock()
defer a.mutex.RUnlock()
if appearance, exists := a.appearanceMap[id]; exists {
return appearance
}
return nil
}
@ -61,7 +61,7 @@ func (a *Appearances) FindAppearanceByID(id int32) *Appearance {
func (a *Appearances) HasAppearance(id int32) bool {
a.mutex.RLock()
defer a.mutex.RUnlock()
_, exists := a.appearanceMap[id]
return exists
}
@ -70,7 +70,7 @@ func (a *Appearances) HasAppearance(id int32) bool {
func (a *Appearances) GetAppearanceCount() int {
a.mutex.RLock()
defer a.mutex.RUnlock()
return len(a.appearanceMap)
}
@ -78,13 +78,13 @@ func (a *Appearances) GetAppearanceCount() int {
func (a *Appearances) GetAllAppearances() map[int32]*Appearance {
a.mutex.RLock()
defer a.mutex.RUnlock()
// Return a copy to prevent external modification
result := make(map[int32]*Appearance)
for id, appearance := range a.appearanceMap {
result[id] = appearance
}
return result
}
@ -92,12 +92,12 @@ func (a *Appearances) GetAllAppearances() map[int32]*Appearance {
func (a *Appearances) GetAppearanceIDs() []int32 {
a.mutex.RLock()
defer a.mutex.RUnlock()
ids := make([]int32, 0, len(a.appearanceMap))
for id := range a.appearanceMap {
ids = append(ids, id)
}
return ids
}
@ -105,15 +105,15 @@ func (a *Appearances) GetAppearanceIDs() []int32 {
func (a *Appearances) FindAppearancesByName(nameSubstring string) []*Appearance {
a.mutex.RLock()
defer a.mutex.RUnlock()
var results []*Appearance
for _, appearance := range a.appearanceMap {
if contains(appearance.GetName(), nameSubstring) {
results = append(results, appearance)
}
}
return results
}
@ -121,15 +121,15 @@ func (a *Appearances) FindAppearancesByName(nameSubstring string) []*Appearance
func (a *Appearances) FindAppearancesByMinClient(minClient int16) []*Appearance {
a.mutex.RLock()
defer a.mutex.RUnlock()
var results []*Appearance
for _, appearance := range a.appearanceMap {
if appearance.GetMinClientVersion() == minClient {
results = append(results, appearance)
}
}
return results
}
@ -137,15 +137,15 @@ func (a *Appearances) FindAppearancesByMinClient(minClient int16) []*Appearance
func (a *Appearances) GetCompatibleAppearances(clientVersion int16) []*Appearance {
a.mutex.RLock()
defer a.mutex.RUnlock()
var results []*Appearance
for _, appearance := range a.appearanceMap {
if appearance.IsCompatibleWithClient(clientVersion) {
results = append(results, appearance)
}
}
return results
}
@ -153,12 +153,12 @@ func (a *Appearances) GetCompatibleAppearances(clientVersion int16) []*Appearanc
func (a *Appearances) RemoveAppearance(id int32) bool {
a.mutex.Lock()
defer a.mutex.Unlock()
if _, exists := a.appearanceMap[id]; exists {
delete(a.appearanceMap, id)
return true
}
return false
}
@ -167,10 +167,10 @@ func (a *Appearances) UpdateAppearance(appearance *Appearance) error {
if appearance == nil {
return fmt.Errorf("appearance cannot be nil")
}
a.mutex.Lock()
defer a.mutex.Unlock()
a.appearanceMap[appearance.GetID()] = appearance
return nil
}
@ -179,15 +179,15 @@ func (a *Appearances) UpdateAppearance(appearance *Appearance) error {
func (a *Appearances) GetAppearancesByIDRange(minID, maxID int32) []*Appearance {
a.mutex.RLock()
defer a.mutex.RUnlock()
var results []*Appearance
for id, appearance := range a.appearanceMap {
if id >= minID && id <= maxID {
results = append(results, appearance)
}
}
return results
}
@ -195,28 +195,28 @@ func (a *Appearances) GetAppearancesByIDRange(minID, maxID int32) []*Appearance
func (a *Appearances) ValidateAppearances() []string {
a.mutex.RLock()
defer a.mutex.RUnlock()
var issues []string
for id, appearance := range a.appearanceMap {
if appearance == nil {
issues = append(issues, fmt.Sprintf("Appearance ID %d is nil", id))
continue
}
if appearance.GetID() != id {
issues = append(issues, fmt.Sprintf("Appearance ID mismatch: map key %d != appearance ID %d", id, appearance.GetID()))
}
if len(appearance.GetName()) == 0 {
issues = append(issues, fmt.Sprintf("Appearance ID %d has empty name", id))
}
if appearance.GetMinClientVersion() < 0 {
issues = append(issues, fmt.Sprintf("Appearance ID %d has negative min client version: %d", id, appearance.GetMinClientVersion()))
}
}
return issues
}
@ -230,22 +230,22 @@ func (a *Appearances) IsValid() bool {
func (a *Appearances) GetStatistics() map[string]interface{} {
a.mutex.RLock()
defer a.mutex.RUnlock()
stats := make(map[string]interface{})
stats["total_appearances"] = len(a.appearanceMap)
// Count by minimum client version
versionCounts := make(map[int16]int)
for _, appearance := range a.appearanceMap {
versionCounts[appearance.GetMinClientVersion()]++
}
stats["appearances_by_min_client"] = versionCounts
// Find ID range
if len(a.appearanceMap) > 0 {
var minID, maxID int32
first := true
for id := range a.appearanceMap {
if first {
minID = id
@ -260,12 +260,12 @@ func (a *Appearances) GetStatistics() map[string]interface{} {
}
}
}
stats["min_id"] = minID
stats["max_id"] = maxID
stats["id_range"] = maxID - minID
}
return stats
}
@ -277,12 +277,12 @@ func contains(str, substr string) bool {
if len(str) < len(substr) {
return false
}
for i := 0; i <= len(str)-len(substr); i++ {
if str[i:i+len(substr)] == substr {
return true
}
}
return false
}
}

View File

@ -10,4 +10,4 @@ const (
const (
MinimumClientVersion = 0
DefaultClientVersion = 283
)
)

View File

@ -84,9 +84,9 @@ func (eaa *EntityAppearanceAdapter) GetAppearanceID() int32 {
// SetAppearanceID sets the entity's appearance ID
func (eaa *EntityAppearanceAdapter) SetAppearanceID(id int32) {
eaa.appearanceID = id
if eaa.logger != nil {
eaa.logger.LogDebug("Entity %d (%s): Set appearance ID to %d",
eaa.logger.LogDebug("Entity %d (%s): Set appearance ID to %d",
eaa.entity.GetID(), eaa.entity.GetName(), id)
}
}
@ -96,15 +96,15 @@ func (eaa *EntityAppearanceAdapter) GetAppearance() *Appearance {
if eaa.appearanceID == 0 {
return nil
}
if eaa.manager == nil {
if eaa.logger != nil {
eaa.logger.LogError("Entity %d (%s): No appearance manager available",
eaa.logger.LogError("Entity %d (%s): No appearance manager available",
eaa.entity.GetID(), eaa.entity.GetName())
}
return nil
}
return eaa.manager.FindAppearanceByID(eaa.appearanceID)
}
@ -114,7 +114,7 @@ func (eaa *EntityAppearanceAdapter) IsCompatibleWithClient(clientVersion int16)
if appearance == nil {
return true // No appearance means compatible with all clients
}
return appearance.IsCompatibleWithClient(clientVersion)
}
@ -124,7 +124,7 @@ func (eaa *EntityAppearanceAdapter) GetAppearanceName() string {
if appearance == nil {
return ""
}
return appearance.GetName()
}
@ -133,12 +133,12 @@ func (eaa *EntityAppearanceAdapter) ValidateAppearance() error {
if eaa.appearanceID == 0 {
return nil // No appearance is valid
}
appearance := eaa.GetAppearance()
if appearance == nil {
return fmt.Errorf("appearance ID %d not found", eaa.appearanceID)
}
return nil
}
@ -147,19 +147,19 @@ func (eaa *EntityAppearanceAdapter) UpdateAppearance(id int32) error {
if eaa.manager == nil {
return fmt.Errorf("no appearance manager available")
}
appearance := eaa.manager.FindAppearanceByID(id)
if appearance == nil {
return fmt.Errorf("appearance ID %d not found", id)
}
eaa.SetAppearanceID(id)
if eaa.logger != nil {
eaa.logger.LogInfo("Entity %d (%s): Updated appearance to %d (%s)",
eaa.logger.LogInfo("Entity %d (%s): Updated appearance to %d (%s)",
eaa.entity.GetID(), eaa.entity.GetName(), id, appearance.GetName())
}
return nil
}
@ -168,20 +168,20 @@ func (eaa *EntityAppearanceAdapter) SendAppearanceToClient(client Client) error
if client == nil {
return fmt.Errorf("client is nil")
}
if eaa.appearanceID == 0 {
return nil // No appearance to send
}
// Check client compatibility
if !eaa.IsCompatibleWithClient(client.GetVersion()) {
if eaa.logger != nil {
eaa.logger.LogWarning("Entity %d (%s): Appearance %d not compatible with client version %d",
eaa.logger.LogWarning("Entity %d (%s): Appearance %d not compatible with client version %d",
eaa.entity.GetID(), eaa.entity.GetName(), eaa.appearanceID, client.GetVersion())
}
return fmt.Errorf("appearance not compatible with client version %d", client.GetVersion())
}
return client.SendAppearanceUpdate(eaa.appearanceID)
}
@ -202,7 +202,7 @@ func NewSimpleAppearanceCache() *SimpleAppearanceCache {
func (sac *SimpleAppearanceCache) Get(id int32) *Appearance {
sac.mutex.RLock()
defer sac.mutex.RUnlock()
return sac.cache[id]
}
@ -210,7 +210,7 @@ func (sac *SimpleAppearanceCache) Get(id int32) *Appearance {
func (sac *SimpleAppearanceCache) Set(id int32, appearance *Appearance) {
sac.mutex.Lock()
defer sac.mutex.Unlock()
sac.cache[id] = appearance
}
@ -218,7 +218,7 @@ func (sac *SimpleAppearanceCache) Set(id int32, appearance *Appearance) {
func (sac *SimpleAppearanceCache) Remove(id int32) {
sac.mutex.Lock()
defer sac.mutex.Unlock()
delete(sac.cache, id)
}
@ -226,7 +226,7 @@ func (sac *SimpleAppearanceCache) Remove(id int32) {
func (sac *SimpleAppearanceCache) Clear() {
sac.mutex.Lock()
defer sac.mutex.Unlock()
sac.cache = make(map[int32]*Appearance)
}
@ -234,7 +234,7 @@ func (sac *SimpleAppearanceCache) Clear() {
func (sac *SimpleAppearanceCache) GetSize() int {
sac.mutex.RLock()
defer sac.mutex.RUnlock()
return len(sac.cache)
}
@ -258,14 +258,14 @@ func (cam *CachedAppearanceManager) FindAppearanceByID(id int32) *Appearance {
if appearance := cam.cache.Get(id); appearance != nil {
return appearance
}
// Load from manager
appearance := cam.Manager.FindAppearanceByID(id)
if appearance != nil {
// Cache the result
cam.cache.Set(id, appearance)
}
return appearance
}
@ -276,7 +276,7 @@ func (cam *CachedAppearanceManager) AddAppearance(appearance *Appearance) error
// Update cache
cam.cache.Set(appearance.GetID(), appearance)
}
return err
}
@ -287,7 +287,7 @@ func (cam *CachedAppearanceManager) UpdateAppearance(appearance *Appearance) err
// Update cache
cam.cache.Set(appearance.GetID(), appearance)
}
return err
}
@ -298,11 +298,11 @@ func (cam *CachedAppearanceManager) RemoveAppearance(id int32) error {
// Remove from cache
cam.cache.Remove(id)
}
return err
}
// ClearCache clears the appearance cache
func (cam *CachedAppearanceManager) ClearCache() {
cam.cache.Clear()
}
}

View File

@ -13,11 +13,11 @@ type Manager struct {
mutex sync.RWMutex
// Statistics
totalLookups int64
successfulLookups int64
failedLookups int64
cacheHits int64
cacheMisses int64
totalLookups int64
successfulLookups int64
failedLookups int64
cacheHits int64
cacheMisses int64
}
// NewManager creates a new appearance manager
@ -34,19 +34,19 @@ func (m *Manager) Initialize() error {
if m.logger != nil {
m.logger.LogInfo("Initializing appearance manager...")
}
if m.database == nil {
if m.logger != nil {
m.logger.LogWarning("No database provided, starting with empty appearance list")
}
return nil
}
appearances, err := m.database.LoadAllAppearances()
if err != nil {
return fmt.Errorf("failed to load appearances from database: %w", err)
}
for _, appearance := range appearances {
if err := m.appearances.InsertAppearance(appearance); err != nil {
if m.logger != nil {
@ -54,11 +54,11 @@ func (m *Manager) Initialize() error {
}
}
}
if m.logger != nil {
m.logger.LogInfo("Loaded %d appearances from database", len(appearances))
}
return nil
}
@ -72,9 +72,9 @@ func (m *Manager) FindAppearanceByID(id int32) *Appearance {
m.mutex.Lock()
m.totalLookups++
m.mutex.Unlock()
appearance := m.appearances.FindAppearanceByID(id)
m.mutex.Lock()
if appearance != nil {
m.successfulLookups++
@ -84,11 +84,11 @@ func (m *Manager) FindAppearanceByID(id int32) *Appearance {
m.cacheMisses++
}
m.mutex.Unlock()
if m.logger != nil && appearance == nil {
m.logger.LogDebug("Appearance lookup failed for ID: %d", id)
}
return appearance
}
@ -97,26 +97,26 @@ func (m *Manager) AddAppearance(appearance *Appearance) error {
if appearance == nil {
return fmt.Errorf("appearance cannot be nil")
}
// Validate the appearance
if len(appearance.GetName()) == 0 {
return fmt.Errorf("appearance name cannot be empty")
}
if appearance.GetID() <= 0 {
return fmt.Errorf("appearance ID must be positive")
}
// Check if appearance already exists
if m.appearances.HasAppearance(appearance.GetID()) {
return fmt.Errorf("appearance with ID %d already exists", appearance.GetID())
}
// Add to collection
if err := m.appearances.InsertAppearance(appearance); err != nil {
return fmt.Errorf("failed to insert appearance: %w", err)
}
// Save to database if available
if m.database != nil {
if err := m.database.SaveAppearance(appearance); err != nil {
@ -125,12 +125,12 @@ func (m *Manager) AddAppearance(appearance *Appearance) error {
return fmt.Errorf("failed to save appearance to database: %w", err)
}
}
if m.logger != nil {
m.logger.LogInfo("Added appearance %d: %s (min client: %d)",
m.logger.LogInfo("Added appearance %d: %s (min client: %d)",
appearance.GetID(), appearance.GetName(), appearance.GetMinClientVersion())
}
return nil
}
@ -139,28 +139,28 @@ func (m *Manager) UpdateAppearance(appearance *Appearance) error {
if appearance == nil {
return fmt.Errorf("appearance cannot be nil")
}
// Check if appearance exists
if !m.appearances.HasAppearance(appearance.GetID()) {
return fmt.Errorf("appearance with ID %d does not exist", appearance.GetID())
}
// Update in collection
if err := m.appearances.UpdateAppearance(appearance); err != nil {
return fmt.Errorf("failed to update appearance: %w", err)
}
// Save to database if available
if m.database != nil {
if err := m.database.SaveAppearance(appearance); err != nil {
return fmt.Errorf("failed to save appearance to database: %w", err)
}
}
if m.logger != nil {
m.logger.LogInfo("Updated appearance %d: %s", appearance.GetID(), appearance.GetName())
}
return nil
}
@ -170,23 +170,23 @@ func (m *Manager) RemoveAppearance(id int32) error {
if !m.appearances.HasAppearance(id) {
return fmt.Errorf("appearance with ID %d does not exist", id)
}
// Remove from database first if available
if m.database != nil {
if err := m.database.DeleteAppearance(id); err != nil {
return fmt.Errorf("failed to delete appearance from database: %w", err)
}
}
// Remove from collection
if !m.appearances.RemoveAppearance(id) {
return fmt.Errorf("failed to remove appearance from collection")
}
if m.logger != nil {
m.logger.LogInfo("Removed appearance %d", id)
}
return nil
}
@ -204,22 +204,22 @@ func (m *Manager) SearchAppearancesByName(nameSubstring string) []*Appearance {
func (m *Manager) GetStatistics() map[string]interface{} {
m.mutex.RLock()
defer m.mutex.RUnlock()
// Get basic appearance statistics
stats := m.appearances.GetStatistics()
// Add manager statistics
stats["total_lookups"] = m.totalLookups
stats["successful_lookups"] = m.successfulLookups
stats["failed_lookups"] = m.failedLookups
stats["cache_hits"] = m.cacheHits
stats["cache_misses"] = m.cacheMisses
if m.totalLookups > 0 {
stats["success_rate"] = float64(m.successfulLookups) / float64(m.totalLookups) * 100
stats["cache_hit_rate"] = float64(m.cacheHits) / float64(m.totalLookups) * 100
}
return stats
}
@ -227,7 +227,7 @@ func (m *Manager) GetStatistics() map[string]interface{} {
func (m *Manager) ResetStatistics() {
m.mutex.Lock()
defer m.mutex.Unlock()
m.totalLookups = 0
m.successfulLookups = 0
m.failedLookups = 0
@ -245,10 +245,10 @@ func (m *Manager) ReloadFromDatabase() error {
if m.database == nil {
return fmt.Errorf("no database available")
}
// Clear current appearances
m.appearances.ClearAppearances()
// Reload from database
return m.Initialize()
}
@ -279,36 +279,36 @@ func (m *Manager) ProcessCommand(command string, args []string) (string, error)
// handleStatsCommand shows appearance system statistics
func (m *Manager) handleStatsCommand(args []string) (string, error) {
stats := m.GetStatistics()
result := "Appearance System Statistics:\n"
result += fmt.Sprintf("Total Appearances: %d\n", stats["total_appearances"])
result += fmt.Sprintf("Total Lookups: %d\n", stats["total_lookups"])
result += fmt.Sprintf("Successful Lookups: %d\n", stats["successful_lookups"])
result += fmt.Sprintf("Failed Lookups: %d\n", stats["failed_lookups"])
if successRate, exists := stats["success_rate"]; exists {
result += fmt.Sprintf("Success Rate: %.1f%%\n", successRate)
}
if cacheHitRate, exists := stats["cache_hit_rate"]; exists {
result += fmt.Sprintf("Cache Hit Rate: %.1f%%\n", cacheHitRate)
}
if minID, exists := stats["min_id"]; exists {
result += fmt.Sprintf("ID Range: %d - %d\n", minID, stats["max_id"])
}
return result, nil
}
// handleValidateCommand validates all appearances
func (m *Manager) handleValidateCommand(args []string) (string, error) {
issues := m.ValidateAllAppearances()
if len(issues) == 0 {
return "All appearances are valid.", nil
}
result := fmt.Sprintf("Found %d issues with appearances:\n", len(issues))
for i, issue := range issues {
if i >= 10 { // Limit output
@ -317,7 +317,7 @@ func (m *Manager) handleValidateCommand(args []string) (string, error) {
}
result += fmt.Sprintf("%d. %s\n", i+1, issue)
}
return result, nil
}
@ -326,24 +326,24 @@ func (m *Manager) handleSearchCommand(args []string) (string, error) {
if len(args) == 0 {
return "", fmt.Errorf("search term required")
}
searchTerm := args[0]
results := m.SearchAppearancesByName(searchTerm)
if len(results) == 0 {
return fmt.Sprintf("No appearances found matching '%s'.", searchTerm), nil
}
result := fmt.Sprintf("Found %d appearances matching '%s':\n", len(results), searchTerm)
for i, appearance := range results {
if i >= 20 { // Limit output
result += "... (and more)\n"
break
}
result += fmt.Sprintf(" %d: %s (min client: %d)\n",
result += fmt.Sprintf(" %d: %s (min client: %d)\n",
appearance.GetID(), appearance.GetName(), appearance.GetMinClientVersion())
}
return result, nil
}
@ -352,22 +352,22 @@ func (m *Manager) handleInfoCommand(args []string) (string, error) {
if len(args) == 0 {
return "", fmt.Errorf("appearance ID required")
}
var appearanceID int32
if _, err := fmt.Sscanf(args[0], "%d", &appearanceID); err != nil {
return "", fmt.Errorf("invalid appearance ID: %s", args[0])
}
appearance := m.FindAppearanceByID(appearanceID)
if appearance == nil {
return fmt.Sprintf("Appearance %d not found.", appearanceID), nil
}
result := fmt.Sprintf("Appearance Information:\n")
result += fmt.Sprintf("ID: %d\n", appearance.GetID())
result += fmt.Sprintf("Name: %s\n", appearance.GetName())
result += fmt.Sprintf("Min Client Version: %d\n", appearance.GetMinClientVersion())
return result, nil
}
@ -376,7 +376,7 @@ func (m *Manager) handleReloadCommand(args []string) (string, error) {
if err := m.ReloadFromDatabase(); err != nil {
return "", fmt.Errorf("failed to reload appearances: %w", err)
}
count := m.GetAppearanceCount()
return fmt.Sprintf("Successfully reloaded %d appearances from database.", count), nil
}
@ -386,7 +386,7 @@ func (m *Manager) Shutdown() {
if m.logger != nil {
m.logger.LogInfo("Shutting down appearance manager...")
}
// Clear appearances
m.appearances.ClearAppearances()
}
}

View File

@ -12,7 +12,7 @@ func NewAppearance(id int32, name string, minClientVersion int16) *Appearance {
if len(name) == 0 {
return nil
}
return &Appearance{
id: id,
name: name,
@ -62,4 +62,4 @@ func (a *Appearance) Clone() *Appearance {
name: a.name,
minClient: a.minClient,
}
}
}

View File

@ -168,7 +168,7 @@ func (c *Channel) leaveChannel(characterID int32) error {
func (c *Channel) GetMembers() []int32 {
c.mu.RLock()
defer c.mu.RUnlock()
// Return a copy to prevent external modification
members := make([]int32, len(c.members))
copy(members, c.members)
@ -179,7 +179,7 @@ func (c *Channel) GetMembers() []int32 {
func (c *Channel) GetChannelInfo() ChannelInfo {
c.mu.RLock()
defer c.mu.RUnlock()
return ChannelInfo{
Name: c.name,
HasPassword: c.password != "",
@ -203,7 +203,7 @@ func (c *Channel) ValidateJoin(level, race, class int32, password string) error
// Check level restriction
if !c.CanJoinChannelByLevel(level) {
return fmt.Errorf("level %d does not meet minimum requirement of %d for channel %s",
return fmt.Errorf("level %d does not meet minimum requirement of %d for channel %s",
level, c.levelRestriction, c.name)
}
@ -246,4 +246,4 @@ func (c *Channel) Copy() *Channel {
copy(newChannel.members, c.members)
return newChannel
}
}

View File

@ -4,7 +4,6 @@ import (
"context"
"fmt"
"strings"
"sync"
"time"
)
@ -52,7 +51,7 @@ func (cm *ChatManager) Initialize(ctx context.Context) error {
func (cm *ChatManager) AddChannel(channel *Channel) {
cm.mu.Lock()
defer cm.mu.Unlock()
cm.channels[strings.ToLower(channel.name)] = channel
}
@ -60,7 +59,7 @@ func (cm *ChatManager) AddChannel(channel *Channel) {
func (cm *ChatManager) GetNumChannels() int {
cm.mu.RLock()
defer cm.mu.RUnlock()
return len(cm.channels)
}
@ -78,9 +77,9 @@ func (cm *ChatManager) GetWorldChannelList(characterID int32) ([]ChannelInfo, er
for _, channel := range cm.channels {
if channel.channelType == ChannelTypeWorld {
// Check if player can join based on restrictions
if cm.canJoinChannel(playerInfo.Level, playerInfo.Race, playerInfo.Class,
if cm.canJoinChannel(playerInfo.Level, playerInfo.Race, playerInfo.Class,
channel.levelRestriction, channel.raceRestriction, channel.classRestriction) {
channelInfo := ChannelInfo{
Name: channel.name,
HasPassword: channel.password != "",
@ -102,7 +101,7 @@ func (cm *ChatManager) GetWorldChannelList(characterID int32) ([]ChannelInfo, er
func (cm *ChatManager) ChannelExists(channelName string) bool {
cm.mu.RLock()
defer cm.mu.RUnlock()
_, exists := cm.channels[strings.ToLower(channelName)]
return exists
}
@ -111,7 +110,7 @@ func (cm *ChatManager) ChannelExists(channelName string) bool {
func (cm *ChatManager) HasPassword(channelName string) bool {
cm.mu.RLock()
defer cm.mu.RUnlock()
if channel, exists := cm.channels[strings.ToLower(channelName)]; exists {
return channel.password != ""
}
@ -122,7 +121,7 @@ func (cm *ChatManager) HasPassword(channelName string) bool {
func (cm *ChatManager) PasswordMatches(channelName, password string) bool {
cm.mu.RLock()
defer cm.mu.RUnlock()
if channel, exists := cm.channels[strings.ToLower(channelName)]; exists {
return channel.password == password
}
@ -167,7 +166,7 @@ func (cm *ChatManager) CreateChannel(channelName string, password ...string) err
func (cm *ChatManager) IsInChannel(characterID int32, channelName string) bool {
cm.mu.RLock()
defer cm.mu.RUnlock()
if channel, exists := cm.channels[strings.ToLower(channelName)]; exists {
return channel.isInChannel(characterID)
}
@ -261,7 +260,7 @@ func (cm *ChatManager) LeaveAllChannels(characterID int32) error {
for channelName, channel := range cm.channels {
if channel.isInChannel(characterID) {
channel.leaveChannel(characterID)
// Mark custom channels with no members for deletion
if channel.channelType == ChannelTypeCustom && len(channel.members) == 0 {
channelsToDelete = append(channelsToDelete, channelName)
@ -298,14 +297,14 @@ func (cm *ChatManager) TellChannel(senderID int32, channelName, message string,
// Get sender info
var senderName string
var languageID int32
if senderID != 0 {
playerInfo, err := cm.playerManager.GetPlayerInfo(senderID)
if err != nil {
return fmt.Errorf("failed to get sender info: %w", err)
}
senderName = playerInfo.CharacterName
// Get sender's default language
if cm.languageProcessor != nil {
languageID = cm.languageProcessor.GetDefaultLanguage(senderID)
@ -370,7 +369,7 @@ func (cm *ChatManager) SendChannelUserList(requesterID int32, channelName string
func (cm *ChatManager) GetChannel(channelName string) *Channel {
cm.mu.RLock()
defer cm.mu.RUnlock()
return cm.channels[strings.ToLower(channelName)]
}
@ -390,7 +389,7 @@ func (cm *ChatManager) GetStatistics() ChatStatistics {
case ChannelTypeCustom:
stats.CustomChannels++
}
stats.TotalMembers += len(channel.members)
if len(channel.members) > 0 {
stats.ActiveChannels++
@ -453,4 +452,4 @@ func (cm *ChatManager) deliverChannelMessage(channel *Channel, message ChannelMe
}
return nil
}
}

View File

@ -32,4 +32,4 @@ const (
const (
DiscordWebhookEnabled = true
DiscordWebhookDisabled = false
)
)

View File

@ -22,7 +22,7 @@ func NewDatabaseChannelManager(db *database.DB) *DatabaseChannelManager {
// LoadWorldChannels retrieves all persistent world channels from database
func (dcm *DatabaseChannelManager) LoadWorldChannels(ctx context.Context) ([]ChatChannelData, error) {
query := "SELECT `name`, `password`, `level_restriction`, `classes`, `races` FROM `channels`"
rows, err := dcm.db.QueryContext(ctx, query)
if err != nil {
return nil, fmt.Errorf("failed to query channels: %w", err)
@ -90,7 +90,7 @@ func (dcm *DatabaseChannelManager) SaveChannel(ctx context.Context, channel Chat
// DeleteChannel removes a channel from database
func (dcm *DatabaseChannelManager) DeleteChannel(ctx context.Context, channelName string) error {
query := "DELETE FROM channels WHERE name = ?"
result, err := dcm.db.ExecContext(ctx, query, channelName)
if err != nil {
return fmt.Errorf("failed to delete channel %s: %w", channelName, err)
@ -132,7 +132,7 @@ func (dcm *DatabaseChannelManager) EnsureChannelsTable(ctx context.Context) erro
// GetChannelCount returns the total number of channels in the database
func (dcm *DatabaseChannelManager) GetChannelCount(ctx context.Context) (int, error) {
query := "SELECT COUNT(*) FROM channels"
var count int
err := dcm.db.QueryRowContext(ctx, query).Scan(&count)
if err != nil {
@ -145,7 +145,7 @@ func (dcm *DatabaseChannelManager) GetChannelCount(ctx context.Context) (int, er
// GetChannelByName retrieves a specific channel by name
func (dcm *DatabaseChannelManager) GetChannelByName(ctx context.Context, channelName string) (*ChatChannelData, error) {
query := "SELECT `name`, `password`, `level_restriction`, `classes`, `races` FROM `channels` WHERE `name` = ?"
var channel ChatChannelData
var password *string
@ -171,7 +171,7 @@ func (dcm *DatabaseChannelManager) GetChannelByName(ctx context.Context, channel
// ListChannelNames returns a list of all channel names in the database
func (dcm *DatabaseChannelManager) ListChannelNames(ctx context.Context) ([]string, error) {
query := "SELECT name FROM channels ORDER BY name"
rows, err := dcm.db.QueryContext(ctx, query)
if err != nil {
return nil, fmt.Errorf("failed to query channel names: %w", err)
@ -197,7 +197,7 @@ func (dcm *DatabaseChannelManager) ListChannelNames(ctx context.Context) ([]stri
// UpdateChannelPassword updates just the password for a channel
func (dcm *DatabaseChannelManager) UpdateChannelPassword(ctx context.Context, channelName, password string) error {
query := "UPDATE channels SET password = ?, updated_at = CURRENT_TIMESTAMP WHERE name = ?"
var passwordParam *string
if password != "" {
passwordParam = &password
@ -223,7 +223,7 @@ func (dcm *DatabaseChannelManager) UpdateChannelPassword(ctx context.Context, ch
// UpdateChannelRestrictions updates the level, race, and class restrictions for a channel
func (dcm *DatabaseChannelManager) UpdateChannelRestrictions(ctx context.Context, channelName string, levelRestriction, classRestriction, raceRestriction int32) error {
query := "UPDATE channels SET level_restriction = ?, classes = ?, races = ?, updated_at = CURRENT_TIMESTAMP WHERE name = ?"
result, err := dcm.db.ExecContext(ctx, query, levelRestriction, classRestriction, raceRestriction, channelName)
if err != nil {
return fmt.Errorf("failed to update restrictions for channel %s: %w", channelName, err)
@ -239,4 +239,4 @@ func (dcm *DatabaseChannelManager) UpdateChannelRestrictions(ctx context.Context
}
return nil
}
}

View File

@ -6,10 +6,10 @@ import "context"
type ChannelDatabase interface {
// LoadWorldChannels retrieves all persistent world channels from database
LoadWorldChannels(ctx context.Context) ([]ChatChannelData, error)
// SaveChannel persists a channel to database (world channels only)
SaveChannel(ctx context.Context, channel ChatChannelData) error
// DeleteChannel removes a channel from database
DeleteChannel(ctx context.Context, channelName string) error
}
@ -18,16 +18,16 @@ type ChannelDatabase interface {
type ClientManager interface {
// SendChannelList sends available channels to a client
SendChannelList(characterID int32, channels []ChannelInfo) error
// SendChannelMessage delivers a message to a client
SendChannelMessage(characterID int32, message ChannelMessage) error
// SendChannelUpdate notifies client of channel membership changes
SendChannelUpdate(characterID int32, channelName string, action int, characterName string) error
// SendChannelUserList sends who list to client
SendChannelUserList(characterID int32, channelName string, members []ChannelMember) error
// IsClientConnected checks if a character is currently online
IsClientConnected(characterID int32) bool
}
@ -36,10 +36,10 @@ type ClientManager interface {
type PlayerManager interface {
// GetPlayerInfo retrieves basic player information
GetPlayerInfo(characterID int32) (PlayerInfo, error)
// ValidatePlayer checks if player meets channel requirements
ValidatePlayer(characterID int32, levelReq, raceReq, classReq int32) bool
// GetPlayerLanguages returns languages known by player
GetPlayerLanguages(characterID int32) ([]int32, error)
}
@ -48,10 +48,10 @@ type PlayerManager interface {
type LanguageProcessor interface {
// ProcessMessage processes a message for language comprehension
ProcessMessage(senderID, receiverID int32, message string, languageID int32) (string, error)
// CanUnderstand checks if receiver can understand sender's language
CanUnderstand(senderID, receiverID int32, languageID int32) bool
// GetDefaultLanguage returns the default language for a character
GetDefaultLanguage(characterID int32) int32
}
@ -119,4 +119,4 @@ func (a *EntityChatAdapter) GetClass() int32 {
return info.Class
}
return 0
}
}

View File

@ -25,7 +25,7 @@ func NewChatService(database ChannelDatabase, clientManager ClientManager, playe
func (cs *ChatService) Initialize(ctx context.Context) error {
cs.mu.Lock()
defer cs.mu.Unlock()
return cs.manager.Initialize(ctx)
}
@ -41,27 +41,27 @@ func (cs *ChatService) ProcessChannelCommand(characterID int32, command, channel
password = args[0]
}
return cs.manager.JoinChannel(characterID, channelName, password)
case "leave":
return cs.manager.LeaveChannel(characterID, channelName)
case "create":
password := ""
if len(args) > 0 {
password = args[0]
}
return cs.manager.CreateChannel(channelName, password)
case "tell", "say":
if len(args) == 0 {
return fmt.Errorf("no message provided")
}
message := strings.Join(args, " ")
return cs.manager.TellChannel(characterID, channelName, message)
case "who", "list":
return cs.manager.SendChannelUserList(characterID, channelName)
default:
return fmt.Errorf("unknown channel command: %s", command)
}
@ -71,7 +71,7 @@ func (cs *ChatService) ProcessChannelCommand(characterID int32, command, channel
func (cs *ChatService) SendChannelMessage(senderID int32, channelName string, message string, customName ...string) error {
cs.mu.RLock()
defer cs.mu.RUnlock()
return cs.manager.TellChannel(senderID, channelName, message, customName...)
}
@ -79,7 +79,7 @@ func (cs *ChatService) SendChannelMessage(senderID int32, channelName string, me
func (cs *ChatService) JoinChannel(characterID int32, channelName string, password ...string) error {
cs.mu.RLock()
defer cs.mu.RUnlock()
return cs.manager.JoinChannel(characterID, channelName, password...)
}
@ -87,7 +87,7 @@ func (cs *ChatService) JoinChannel(characterID int32, channelName string, passwo
func (cs *ChatService) LeaveChannel(characterID int32, channelName string) error {
cs.mu.RLock()
defer cs.mu.RUnlock()
return cs.manager.LeaveChannel(characterID, channelName)
}
@ -95,7 +95,7 @@ func (cs *ChatService) LeaveChannel(characterID int32, channelName string) error
func (cs *ChatService) LeaveAllChannels(characterID int32) error {
cs.mu.RLock()
defer cs.mu.RUnlock()
return cs.manager.LeaveAllChannels(characterID)
}
@ -103,7 +103,7 @@ func (cs *ChatService) LeaveAllChannels(characterID int32) error {
func (cs *ChatService) CreateChannel(channelName string, password ...string) error {
cs.mu.RLock()
defer cs.mu.RUnlock()
return cs.manager.CreateChannel(channelName, password...)
}
@ -111,7 +111,7 @@ func (cs *ChatService) CreateChannel(channelName string, password ...string) err
func (cs *ChatService) GetWorldChannelList(characterID int32) ([]ChannelInfo, error) {
cs.mu.RLock()
defer cs.mu.RUnlock()
return cs.manager.GetWorldChannelList(characterID)
}
@ -119,7 +119,7 @@ func (cs *ChatService) GetWorldChannelList(characterID int32) ([]ChannelInfo, er
func (cs *ChatService) ChannelExists(channelName string) bool {
cs.mu.RLock()
defer cs.mu.RUnlock()
return cs.manager.ChannelExists(channelName)
}
@ -127,7 +127,7 @@ func (cs *ChatService) ChannelExists(channelName string) bool {
func (cs *ChatService) IsInChannel(characterID int32, channelName string) bool {
cs.mu.RLock()
defer cs.mu.RUnlock()
return cs.manager.IsInChannel(characterID, channelName)
}
@ -135,12 +135,12 @@ func (cs *ChatService) IsInChannel(characterID int32, channelName string) bool {
func (cs *ChatService) GetChannelInfo(channelName string) (*ChannelInfo, error) {
cs.mu.RLock()
defer cs.mu.RUnlock()
channel := cs.manager.GetChannel(channelName)
if channel == nil {
return nil, fmt.Errorf("channel %s not found", channelName)
}
info := channel.GetChannelInfo()
return &info, nil
}
@ -149,7 +149,7 @@ func (cs *ChatService) GetChannelInfo(channelName string) (*ChannelInfo, error)
func (cs *ChatService) GetStatistics() ChatStatistics {
cs.mu.RLock()
defer cs.mu.RUnlock()
return cs.manager.GetStatistics()
}
@ -157,7 +157,7 @@ func (cs *ChatService) GetStatistics() ChatStatistics {
func (cs *ChatService) SendChannelUserList(requesterID int32, channelName string) error {
cs.mu.RLock()
defer cs.mu.RUnlock()
return cs.manager.SendChannelUserList(requesterID, channelName)
}
@ -166,16 +166,16 @@ func (cs *ChatService) ValidateChannelName(channelName string) error {
if len(channelName) == 0 {
return fmt.Errorf("channel name cannot be empty")
}
if len(channelName) > MaxChannelNameLength {
return fmt.Errorf("channel name too long: %d > %d", len(channelName), MaxChannelNameLength)
}
// Check for invalid characters
if strings.ContainsAny(channelName, " \t\n\r") {
return fmt.Errorf("channel name cannot contain whitespace")
}
return nil
}
@ -184,7 +184,7 @@ func (cs *ChatService) ValidateChannelPassword(password string) error {
if len(password) > MaxChannelPasswordLength {
return fmt.Errorf("channel password too long: %d > %d", len(password), MaxChannelPasswordLength)
}
return nil
}
@ -192,12 +192,12 @@ func (cs *ChatService) ValidateChannelPassword(password string) error {
func (cs *ChatService) GetChannelMembers(channelName string) ([]int32, error) {
cs.mu.RLock()
defer cs.mu.RUnlock()
channel := cs.manager.GetChannel(channelName)
if channel == nil {
return nil, fmt.Errorf("channel %s not found", channelName)
}
return channel.GetMembers(), nil
}
@ -205,7 +205,7 @@ func (cs *ChatService) GetChannelMembers(channelName string) ([]int32, error) {
func (cs *ChatService) CleanupEmptyChannels() int {
cs.mu.Lock()
defer cs.mu.Unlock()
removed := 0
for name, channel := range cs.manager.channels {
if channel.channelType == ChannelTypeCustom && channel.IsEmpty() {
@ -216,7 +216,7 @@ func (cs *ChatService) CleanupEmptyChannels() int {
}
}
}
return removed
}
@ -224,7 +224,7 @@ func (cs *ChatService) CleanupEmptyChannels() int {
func (cs *ChatService) BroadcastSystemMessage(channelName string, message string, systemName string) error {
cs.mu.RLock()
defer cs.mu.RUnlock()
return cs.manager.TellChannel(0, channelName, message, systemName)
}
@ -232,14 +232,14 @@ func (cs *ChatService) BroadcastSystemMessage(channelName string, message string
func (cs *ChatService) GetActiveChannels() []string {
cs.mu.RLock()
defer cs.mu.RUnlock()
var activeChannels []string
for name, channel := range cs.manager.channels {
if !channel.IsEmpty() {
activeChannels = append(activeChannels, name)
}
}
return activeChannels
}
@ -247,14 +247,14 @@ func (cs *ChatService) GetActiveChannels() []string {
func (cs *ChatService) GetChannelsByType(channelType int) []string {
cs.mu.RLock()
defer cs.mu.RUnlock()
var channels []string
for name, channel := range cs.manager.channels {
if channel.channelType == channelType {
channels = append(channels, name)
}
}
return channels
}
@ -262,12 +262,12 @@ func (cs *ChatService) GetChannelsByType(channelType int) []string {
func (cs *ChatService) ProcessChannelFilter(characterID int32, filter ChannelFilter) ([]ChannelInfo, error) {
cs.mu.RLock()
defer cs.mu.RUnlock()
playerInfo, err := cs.manager.playerManager.GetPlayerInfo(characterID)
if err != nil {
return nil, fmt.Errorf("failed to get player info: %w", err)
}
var filteredChannels []ChannelInfo
for _, channel := range cs.manager.channels {
// Apply type filters
@ -277,7 +277,7 @@ func (cs *ChatService) ProcessChannelFilter(characterID int32, filter ChannelFil
if !filter.IncludeCustom && channel.channelType == ChannelTypeCustom {
continue
}
// Apply level range filters
if filter.MinLevel > 0 && playerInfo.Level < filter.MinLevel {
continue
@ -285,23 +285,23 @@ func (cs *ChatService) ProcessChannelFilter(characterID int32, filter ChannelFil
if filter.MaxLevel > 0 && playerInfo.Level > filter.MaxLevel {
continue
}
// Apply race filter
if filter.Race > 0 && playerInfo.Race != filter.Race {
continue
}
// Apply class filter
if filter.Class > 0 && playerInfo.Class != filter.Class {
continue
}
// Check if player can actually join the channel
if cs.manager.canJoinChannel(playerInfo.Level, playerInfo.Race, playerInfo.Class,
channel.levelRestriction, channel.raceRestriction, channel.classRestriction) {
filteredChannels = append(filteredChannels, channel.GetChannelInfo())
}
}
return filteredChannels, nil
}
}

View File

@ -21,14 +21,14 @@ type Channel struct {
// ChannelMessage represents a message sent to a channel
type ChannelMessage struct {
SenderID int32
SenderName string
Message string
LanguageID int32
ChannelName string
Timestamp time.Time
IsEmote bool
IsOOC bool
SenderID int32
SenderName string
Message string
LanguageID int32
ChannelName string
Timestamp time.Time
IsEmote bool
IsOOC bool
}
// ChannelMember represents a member in a channel
@ -66,21 +66,21 @@ type ChatManager struct {
mu sync.RWMutex
channels map[string]*Channel
database ChannelDatabase
// Integration interfaces
clientManager ClientManager
playerManager PlayerManager
clientManager ClientManager
playerManager PlayerManager
languageProcessor LanguageProcessor
}
// ChatStatistics provides statistics about chat system usage
type ChatStatistics struct {
TotalChannels int
WorldChannels int
CustomChannels int
TotalMembers int
MessagesPerHour int
ActiveChannels int
TotalChannels int
WorldChannels int
CustomChannels int
TotalMembers int
MessagesPerHour int
ActiveChannels int
}
// ChannelFilter provides filtering options for channel lists
@ -91,4 +91,4 @@ type ChannelFilter struct {
Class int32
IncludeCustom bool
IncludeWorld bool
}
}

View File

@ -363,4 +363,4 @@ func GetGlobalClasses() *Classes {
globalClasses = NewClasses()
})
return globalClasses
}
}

View File

@ -10,15 +10,15 @@ const (
ClassScout = 31
// Fighter subclasses
ClassWarrior = 2
ClassGuardian = 3
ClassBerserker = 4
ClassBrawler = 5
ClassMonk = 6
ClassBruiser = 7
ClassCrusader = 8
ClassWarrior = 2
ClassGuardian = 3
ClassBerserker = 4
ClassBrawler = 5
ClassMonk = 6
ClassBruiser = 7
ClassCrusader = 8
ClassShadowknight = 9
ClassPaladin = 10
ClassPaladin = 10
// Priest subclasses
ClassCleric = 12
@ -43,17 +43,17 @@ const (
ClassNecromancer = 30
// Scout subclasses
ClassRogue = 32
ClassRogue = 32
ClassSwashbuckler = 33
ClassBrigand = 34
ClassBard = 35
ClassTroubador = 36
ClassDirge = 37
ClassPredator = 38
ClassRanger = 39
ClassAssassin = 40
ClassAnimalist = 41
ClassBeastlord = 42
ClassBrigand = 34
ClassBard = 35
ClassTroubador = 36
ClassDirge = 37
ClassPredator = 38
ClassRanger = 39
ClassAssassin = 40
ClassAnimalist = 41
ClassBeastlord = 42
// Special classes
ClassShaper = 43
@ -223,4 +223,4 @@ const (
DisplayNameJeweler = "Jeweler"
DisplayNameSage = "Sage"
DisplayNameAlchemist = "Alchemist"
)
)

View File

@ -349,4 +349,4 @@ func GetGlobalClassIntegration() *ClassIntegration {
globalClassIntegration = NewClassIntegration()
}
return globalClassIntegration
}
}

View File

@ -452,4 +452,4 @@ func GetGlobalClassManager() *ClassManager {
globalClassManager = NewClassManager()
})
return globalClassManager
}
}

View File

@ -1,7 +1,6 @@
package classes
import (
"fmt"
"math/rand"
"strings"
)
@ -448,4 +447,4 @@ func (cu *ClassUtils) IsSecondaryBaseClass(classID int8) bool {
}
}
return false
}
}

View File

@ -11,9 +11,9 @@ import (
func NewCollection() *Collection {
return &Collection{
collectionItems: make([]CollectionItem, 0),
rewardItems: make([]CollectionRewardItem, 0),
rewardItems: make([]CollectionRewardItem, 0),
selectableRewardItems: make([]CollectionRewardItem, 0),
lastModified: time.Now(),
lastModified: time.Now(),
}
}
@ -28,25 +28,25 @@ func NewCollectionFromData(source *Collection) *Collection {
collection := &Collection{
id: source.id,
name: source.name,
category: source.category,
level: source.level,
rewardCoin: source.rewardCoin,
rewardXP: source.rewardXP,
completed: source.completed,
saveNeeded: source.saveNeeded,
collectionItems: make([]CollectionItem, len(source.collectionItems)),
rewardItems: make([]CollectionRewardItem, len(source.rewardItems)),
name: source.name,
category: source.category,
level: source.level,
rewardCoin: source.rewardCoin,
rewardXP: source.rewardXP,
completed: source.completed,
saveNeeded: source.saveNeeded,
collectionItems: make([]CollectionItem, len(source.collectionItems)),
rewardItems: make([]CollectionRewardItem, len(source.rewardItems)),
selectableRewardItems: make([]CollectionRewardItem, len(source.selectableRewardItems)),
lastModified: time.Now(),
lastModified: time.Now(),
}
// Deep copy collection items
copy(collection.collectionItems, source.collectionItems)
// Deep copy reward items
copy(collection.rewardItems, source.rewardItems)
// Deep copy selectable reward items
copy(collection.selectableRewardItems, source.selectableRewardItems)
@ -496,4 +496,4 @@ func (c *Collection) getProgressNoLock() float64 {
foundCount := c.getFoundItemsCountNoLock()
return float64(foundCount) / float64(len(c.collectionItems)) * 100.0
}
}

View File

@ -33,4 +33,4 @@ const (
TableCollectionRewards = "collection_rewards"
TableCharacterCollections = "character_collections"
TableCharacterCollectionItems = "character_collection_items"
)
)

View File

@ -22,7 +22,7 @@ func NewDatabaseCollectionManager(db *database.DB) *DatabaseCollectionManager {
// LoadCollections retrieves all collections from database
func (dcm *DatabaseCollectionManager) LoadCollections(ctx context.Context) ([]CollectionData, error) {
query := "SELECT `id`, `collection_name`, `collection_category`, `level` FROM `collections`"
rows, err := dcm.db.QueryContext(ctx, query)
if err != nil {
return nil, fmt.Errorf("failed to query collections: %w", err)
@ -54,11 +54,11 @@ func (dcm *DatabaseCollectionManager) LoadCollections(ctx context.Context) ([]Co
// LoadCollectionItems retrieves items for a specific collection
func (dcm *DatabaseCollectionManager) LoadCollectionItems(ctx context.Context, collectionID int32) ([]CollectionItem, error) {
query := `SELECT item_id, item_index
FROM collection_details
WHERE collection_id = ?
query := `SELECT item_id, item_index
FROM collection_details
WHERE collection_id = ?
ORDER BY item_index ASC`
rows, err := dcm.db.QueryContext(ctx, query, collectionID)
if err != nil {
return nil, fmt.Errorf("failed to query collection items for collection %d: %w", collectionID, err)
@ -90,10 +90,10 @@ func (dcm *DatabaseCollectionManager) LoadCollectionItems(ctx context.Context, c
// LoadCollectionRewards retrieves rewards for a specific collection
func (dcm *DatabaseCollectionManager) LoadCollectionRewards(ctx context.Context, collectionID int32) ([]CollectionRewardData, error) {
query := `SELECT collection_id, reward_type, reward_value, reward_quantity
FROM collection_rewards
query := `SELECT collection_id, reward_type, reward_value, reward_quantity
FROM collection_rewards
WHERE collection_id = ?`
rows, err := dcm.db.QueryContext(ctx, query, collectionID)
if err != nil {
return nil, fmt.Errorf("failed to query collection rewards for collection %d: %w", collectionID, err)
@ -125,10 +125,10 @@ func (dcm *DatabaseCollectionManager) LoadCollectionRewards(ctx context.Context,
// LoadPlayerCollections retrieves player's collection progress
func (dcm *DatabaseCollectionManager) LoadPlayerCollections(ctx context.Context, characterID int32) ([]PlayerCollectionData, error) {
query := `SELECT char_id, collection_id, completed
FROM character_collections
query := `SELECT char_id, collection_id, completed
FROM character_collections
WHERE char_id = ?`
rows, err := dcm.db.QueryContext(ctx, query, characterID)
if err != nil {
return nil, fmt.Errorf("failed to query player collections for character %d: %w", characterID, err)
@ -161,10 +161,10 @@ func (dcm *DatabaseCollectionManager) LoadPlayerCollections(ctx context.Context,
// LoadPlayerCollectionItems retrieves player's found collection items
func (dcm *DatabaseCollectionManager) LoadPlayerCollectionItems(ctx context.Context, characterID, collectionID int32) ([]int32, error) {
query := `SELECT collection_item_id
FROM character_collection_items
query := `SELECT collection_item_id
FROM character_collection_items
WHERE char_id = ? AND collection_id = ?`
rows, err := dcm.db.QueryContext(ctx, query, characterID, collectionID)
if err != nil {
return nil, fmt.Errorf("failed to query player collection items for character %d, collection %d: %w", characterID, collectionID, err)
@ -198,7 +198,7 @@ func (dcm *DatabaseCollectionManager) SavePlayerCollection(ctx context.Context,
query := `INSERT INTO character_collections (char_id, collection_id, completed)
VALUES (?, ?, ?)
ON CONFLICT(char_id, collection_id)
ON CONFLICT(char_id, collection_id)
DO UPDATE SET completed = ?`
_, err := dcm.db.ExecContext(ctx, query, characterID, collectionID, completedInt, completedInt)
@ -267,7 +267,7 @@ func (dcm *DatabaseCollectionManager) savePlayerCollectionTx(ctx context.Context
query := `INSERT INTO character_collections (char_id, collection_id, completed)
VALUES (?, ?, ?)
ON CONFLICT(char_id, collection_id)
ON CONFLICT(char_id, collection_id)
DO UPDATE SET completed = ?`
_, err := tx.ExecContext(ctx, query, characterID, collection.GetID(), completedInt, completedInt)
@ -277,7 +277,7 @@ func (dcm *DatabaseCollectionManager) savePlayerCollectionTx(ctx context.Context
// savePlayerCollectionItemsTx saves collection items within a transaction
func (dcm *DatabaseCollectionManager) savePlayerCollectionItemsTx(ctx context.Context, tx database.Tx, characterID int32, collection *Collection) error {
items := collection.GetCollectionItems()
for _, item := range items {
if item.Found == ItemFound {
query := `INSERT OR IGNORE INTO character_collection_items (char_id, collection_id, collection_item_id)
@ -369,7 +369,7 @@ func (dcm *DatabaseCollectionManager) EnsureCollectionTables(ctx context.Context
// GetCollectionCount returns the total number of collections in the database
func (dcm *DatabaseCollectionManager) GetCollectionCount(ctx context.Context) (int, error) {
query := "SELECT COUNT(*) FROM collections"
var count int
err := dcm.db.QueryRowContext(ctx, query).Scan(&count)
if err != nil {
@ -382,7 +382,7 @@ func (dcm *DatabaseCollectionManager) GetCollectionCount(ctx context.Context) (i
// GetPlayerCollectionCount returns the number of collections a player has
func (dcm *DatabaseCollectionManager) GetPlayerCollectionCount(ctx context.Context, characterID int32) (int, error) {
query := "SELECT COUNT(*) FROM character_collections WHERE char_id = ?"
var count int
err := dcm.db.QueryRowContext(ctx, query, characterID).Scan(&count)
if err != nil {
@ -395,7 +395,7 @@ func (dcm *DatabaseCollectionManager) GetPlayerCollectionCount(ctx context.Conte
// GetCompletedCollectionCount returns the number of completed collections for a player
func (dcm *DatabaseCollectionManager) GetCompletedCollectionCount(ctx context.Context, characterID int32) (int, error) {
query := "SELECT COUNT(*) FROM character_collections WHERE char_id = ? AND completed = 1"
var count int
err := dcm.db.QueryRowContext(ctx, query, characterID).Scan(&count)
if err != nil {
@ -415,7 +415,7 @@ func (dcm *DatabaseCollectionManager) DeletePlayerCollection(ctx context.Context
defer tx.Rollback()
// Delete collection items first due to foreign key constraint
_, err = tx.ExecContext(ctx,
_, err = tx.ExecContext(ctx,
"DELETE FROM character_collection_items WHERE char_id = ? AND collection_id = ?",
characterID, collectionID)
if err != nil {
@ -466,9 +466,9 @@ func (dcm *DatabaseCollectionManager) GetCollectionStatistics(ctx context.Contex
}
// Active collections (incomplete with at least one item found) across all players
query := `SELECT COUNT(DISTINCT cc.char_id, cc.collection_id)
FROM character_collections cc
JOIN character_collection_items cci ON cc.char_id = cci.char_id AND cc.collection_id = cci.collection_id
query := `SELECT COUNT(DISTINCT cc.char_id, cc.collection_id)
FROM character_collections cc
JOIN character_collection_items cci ON cc.char_id = cci.char_id AND cc.collection_id = cci.collection_id
WHERE cc.completed = 0`
err = dcm.db.QueryRowContext(ctx, query).Scan(&stats.ActiveCollections)
if err != nil {
@ -488,4 +488,4 @@ func (dcm *DatabaseCollectionManager) GetCollectionStatistics(ctx context.Contex
}
return stats, nil
}
}

View File

@ -6,25 +6,25 @@ import "context"
type CollectionDatabase interface {
// LoadCollections retrieves all collections from database
LoadCollections(ctx context.Context) ([]CollectionData, error)
// LoadCollectionItems retrieves items for a specific collection
LoadCollectionItems(ctx context.Context, collectionID int32) ([]CollectionItem, error)
// LoadCollectionRewards retrieves rewards for a specific collection
LoadCollectionRewards(ctx context.Context, collectionID int32) ([]CollectionRewardData, error)
// LoadPlayerCollections retrieves player's collection progress
LoadPlayerCollections(ctx context.Context, characterID int32) ([]PlayerCollectionData, error)
// LoadPlayerCollectionItems retrieves player's found collection items
LoadPlayerCollectionItems(ctx context.Context, characterID, collectionID int32) ([]int32, error)
// SavePlayerCollection saves player collection completion status
SavePlayerCollection(ctx context.Context, characterID, collectionID int32, completed bool) error
// SavePlayerCollectionItem saves a found collection item
SavePlayerCollectionItem(ctx context.Context, characterID, collectionID, itemID int32) error
// SavePlayerCollections saves all modified player collections
SavePlayerCollections(ctx context.Context, characterID int32, collections []*Collection) error
}
@ -33,10 +33,10 @@ type CollectionDatabase interface {
type ItemLookup interface {
// GetItem retrieves an item by ID
GetItem(itemID int32) (ItemInfo, error)
// ItemExists checks if an item exists
ItemExists(itemID int32) bool
// GetItemName returns the name of an item
GetItemName(itemID int32) string
}
@ -45,10 +45,10 @@ type ItemLookup interface {
type PlayerManager interface {
// GetPlayerInfo retrieves basic player information
GetPlayerInfo(characterID int32) (PlayerInfo, error)
// IsPlayerOnline checks if a player is currently online
IsPlayerOnline(characterID int32) bool
// GetPlayerLevel returns player's current level
GetPlayerLevel(characterID int32) int8
}
@ -57,13 +57,13 @@ type PlayerManager interface {
type ClientManager interface {
// SendCollectionUpdate notifies client of collection changes
SendCollectionUpdate(characterID int32, collection *Collection) error
// SendCollectionComplete notifies client of collection completion
SendCollectionComplete(characterID int32, collection *Collection) error
// SendCollectionList sends available collections to client
SendCollectionList(characterID int32, collections []CollectionInfo) error
// SendCollectionProgress sends collection progress to client
SendCollectionProgress(characterID int32, progress []CollectionProgress) error
}
@ -134,13 +134,13 @@ func (a *EntityCollectionAdapter) GetCollectionList() *PlayerCollectionList {
type RewardProvider interface {
// GiveItem gives an item to a player
GiveItem(characterID int32, itemID int32, quantity int8) error
// GiveCoin gives coins to a player
GiveCoin(characterID int32, amount int64) error
// GiveXP gives experience points to a player
GiveXP(characterID int32, amount int64) error
// ValidateRewards checks if rewards can be given
ValidateRewards(characterID int32, rewards []CollectionRewardItem, coin, xp int64) error
}
@ -149,13 +149,13 @@ type RewardProvider interface {
type CollectionEventHandler interface {
// OnCollectionStarted called when player starts a collection
OnCollectionStarted(characterID, collectionID int32)
// OnItemFound called when player finds a collection item
OnItemFound(characterID, collectionID, itemID int32)
// OnCollectionCompleted called when player completes a collection
OnCollectionCompleted(characterID, collectionID int32)
// OnRewardClaimed called when player claims collection rewards
OnRewardClaimed(characterID, collectionID int32, rewards []CollectionRewardItem, coin, xp int64)
}
@ -164,13 +164,13 @@ type CollectionEventHandler interface {
type LogHandler interface {
// LogDebug logs debug messages
LogDebug(category, message string, args ...interface{})
// LogInfo logs informational messages
LogInfo(category, message string, args ...interface{})
// LogError logs error messages
LogError(category, message string, args ...interface{})
// LogWarning logs warning messages
LogWarning(category, message string, args ...interface{})
}
}

View File

@ -67,7 +67,7 @@ func (cm *CollectionManager) CompleteCollection(playerList *PlayerCollectionList
// Give rewards if provider is available
if rewardProvider != nil {
characterID := playerList.GetCharacterID()
// Give coin reward
if coin := collection.GetRewardCoin(); coin > 0 {
if err := rewardProvider.GiveCoin(characterID, coin); err != nil {
@ -171,7 +171,7 @@ func (cs *CollectionService) LoadPlayerCollections(ctx context.Context, characte
cs.playerLists[characterID] = playerList
if cs.logger != nil {
cs.logger.LogDebug("collections", "Loaded %d collections for character %d",
cs.logger.LogDebug("collections", "Loaded %d collections for character %d",
playerList.Size(), characterID)
}
@ -231,7 +231,7 @@ func (cs *CollectionService) ProcessItemFound(characterID, itemID int32) error {
}
if cs.logger != nil {
cs.logger.LogDebug("collections", "Character %d found item %d for collection %d (%s)",
cs.logger.LogDebug("collections", "Character %d found item %d for collection %d (%s)",
characterID, itemID, collection.GetID(), collection.GetName())
}
}
@ -269,12 +269,12 @@ func (cs *CollectionService) CompleteCollection(characterID, collectionID int32,
selectableRewards := collection.GetSelectableRewardItems()
allRewards := append(rewards, selectableRewards...)
cs.eventHandler.OnCollectionCompleted(characterID, collectionID)
cs.eventHandler.OnRewardClaimed(characterID, collectionID, allRewards,
cs.eventHandler.OnRewardClaimed(characterID, collectionID, allRewards,
collection.GetRewardCoin(), collection.GetRewardXP())
}
if cs.logger != nil {
cs.logger.LogInfo("collections", "Character %d completed collection %d (%s)",
cs.logger.LogInfo("collections", "Character %d completed collection %d (%s)",
characterID, collectionID, collection.GetName())
}
@ -315,7 +315,7 @@ func (cs *CollectionService) SendCollectionList(characterID int32, playerLevel i
collections := cs.manager.GetAvailableCollections(playerLevel)
collectionInfos := make([]CollectionInfo, len(collections))
for i, collection := range collections {
collectionInfos[i] = collection.GetCollectionInfo()
}
@ -377,4 +377,4 @@ func (cs *CollectionService) GetAllCategories() []string {
// SearchCollections searches for collections by name
func (cs *CollectionService) SearchCollections(searchTerm string) []*Collection {
return cs.manager.SearchCollections(searchTerm)
}
}

View File

@ -4,6 +4,7 @@ import (
"context"
"fmt"
"sort"
"strings"
)
// NewMasterCollectionList creates a new master collection list
@ -107,7 +108,7 @@ func (mcl *MasterCollectionList) GetCollection(collectionID int32) *Collection {
func (mcl *MasterCollectionList) GetCollectionCopy(collectionID int32) *Collection {
mcl.mu.RLock()
defer mcl.mu.RUnlock()
if collection, exists := mcl.collections[collectionID]; exists {
return NewCollectionFromData(collection)
}
@ -279,7 +280,7 @@ func (mcl *MasterCollectionList) ValidateIntegrity(itemLookup ItemLookup) []erro
for _, collection := range mcl.collections {
if err := collection.Validate(); err != nil {
errors = append(errors, fmt.Errorf("collection %d (%s): %w",
errors = append(errors, fmt.Errorf("collection %d (%s): %w",
collection.GetID(), collection.GetName(), err))
}
@ -287,7 +288,7 @@ func (mcl *MasterCollectionList) ValidateIntegrity(itemLookup ItemLookup) []erro
if itemLookup != nil {
for _, item := range collection.GetCollectionItems() {
if !itemLookup.ItemExists(item.ItemID) {
errors = append(errors, fmt.Errorf("collection %d (%s) references non-existent item %d",
errors = append(errors, fmt.Errorf("collection %d (%s) references non-existent item %d",
collection.GetID(), collection.GetName(), item.ItemID))
}
}
@ -295,14 +296,14 @@ func (mcl *MasterCollectionList) ValidateIntegrity(itemLookup ItemLookup) []erro
// Check reward items
for _, item := range collection.GetRewardItems() {
if !itemLookup.ItemExists(item.ItemID) {
errors = append(errors, fmt.Errorf("collection %d (%s) has non-existent reward item %d",
errors = append(errors, fmt.Errorf("collection %d (%s) has non-existent reward item %d",
collection.GetID(), collection.GetName(), item.ItemID))
}
}
for _, item := range collection.GetSelectableRewardItems() {
if !itemLookup.ItemExists(item.ItemID) {
errors = append(errors, fmt.Errorf("collection %d (%s) has non-existent selectable reward item %d",
errors = append(errors, fmt.Errorf("collection %d (%s) has non-existent selectable reward item %d",
collection.GetID(), collection.GetName(), item.ItemID))
}
}
@ -332,4 +333,4 @@ func (mcl *MasterCollectionList) FindCollectionsByName(searchTerm string) []*Col
})
return result
}
}

View File

@ -394,4 +394,4 @@ func (pcl *PlayerCollectionList) GetCollectionIDs() []int32 {
})
return ids
}
}

View File

@ -20,19 +20,19 @@ type CollectionRewardItem struct {
// Collection represents a collection that players can complete
type Collection struct {
mu sync.RWMutex
id int32
name string
category string
level int8
rewardCoin int64
rewardXP int64
completed bool
saveNeeded bool
collectionItems []CollectionItem
rewardItems []CollectionRewardItem
selectableRewardItems []CollectionRewardItem
lastModified time.Time
mu sync.RWMutex
id int32
name string
category string
level int8
rewardCoin int64
rewardXP int64
completed bool
saveNeeded bool
collectionItems []CollectionItem
rewardItems []CollectionRewardItem
selectableRewardItems []CollectionRewardItem
lastModified time.Time
}
// CollectionData represents collection data for database operations
@ -60,9 +60,9 @@ type PlayerCollectionData struct {
// PlayerCollectionItemData represents player found collection items
type PlayerCollectionItemData struct {
CharacterID int32 `json:"char_id" db:"char_id"`
CollectionID int32 `json:"collection_id" db:"collection_id"`
CollectionItemID int32 `json:"collection_item_id" db:"collection_item_id"`
CharacterID int32 `json:"char_id" db:"char_id"`
CollectionID int32 `json:"collection_id" db:"collection_id"`
CollectionItemID int32 `json:"collection_item_id" db:"collection_item_id"`
}
// MasterCollectionList manages all available collections in the game
@ -100,31 +100,31 @@ type CollectionStatistics struct {
// CollectionInfo provides basic collection information
type CollectionInfo struct {
ID int32 `json:"id"`
Name string `json:"name"`
Category string `json:"category"`
Level int8 `json:"level"`
Completed bool `json:"completed"`
ReadyToTurnIn bool `json:"ready_to_turn_in"`
ItemsFound int `json:"items_found"`
ItemsTotal int `json:"items_total"`
RewardCoin int64 `json:"reward_coin"`
RewardXP int64 `json:"reward_xp"`
RewardItems []CollectionRewardItem `json:"reward_items"`
ID int32 `json:"id"`
Name string `json:"name"`
Category string `json:"category"`
Level int8 `json:"level"`
Completed bool `json:"completed"`
ReadyToTurnIn bool `json:"ready_to_turn_in"`
ItemsFound int `json:"items_found"`
ItemsTotal int `json:"items_total"`
RewardCoin int64 `json:"reward_coin"`
RewardXP int64 `json:"reward_xp"`
RewardItems []CollectionRewardItem `json:"reward_items"`
SelectableRewards []CollectionRewardItem `json:"selectable_rewards"`
RequiredItems []CollectionItem `json:"required_items"`
RequiredItems []CollectionItem `json:"required_items"`
}
// CollectionProgress represents player progress on a collection
type CollectionProgress struct {
CollectionID int32 `json:"collection_id"`
Name string `json:"name"`
Category string `json:"category"`
Level int8 `json:"level"`
Completed bool `json:"completed"`
ReadyToTurnIn bool `json:"ready_to_turn_in"`
Progress float64 `json:"progress_percentage"`
ItemsFound []CollectionItem `json:"items_found"`
ItemsNeeded []CollectionItem `json:"items_needed"`
LastUpdated time.Time `json:"last_updated"`
}
CollectionID int32 `json:"collection_id"`
Name string `json:"name"`
Category string `json:"category"`
Level int8 `json:"level"`
Completed bool `json:"completed"`
ReadyToTurnIn bool `json:"ready_to_turn_in"`
Progress float64 `json:"progress_percentage"`
ItemsFound []CollectionItem `json:"items_found"`
ItemsNeeded []CollectionItem `json:"items_needed"`
LastUpdated time.Time `json:"last_updated"`
}

View File

@ -258,4 +258,4 @@ func (v *Variables) Merge(other *Variables, overwrite bool) {
v.variables[name] = NewVariable(variable.name, variable.value, variable.comment)
}
}
}
}

View File

@ -32,10 +32,10 @@ func (vs *VisualState) GetName() string {
// Emote represents an emote with visual state and messages
type Emote struct {
name string
visualState int32
message string
targetedMessage string
name string
visualState int32
message string
targetedMessage string
}
// NewEmote creates a new emote
@ -375,4 +375,4 @@ func (vs *VisualStates) FindEmoteByPartialName(partial string) []*EmoteVersionRa
}
return results
}
}

View File

@ -11,7 +11,7 @@ import (
// These will eventually be removed in favor of direct imports from spells package
type BonusValues = spells.BonusValues
type MaintainedEffects = spells.MaintainedEffects
type MaintainedEffects = spells.MaintainedEffects
type SpellEffects = spells.SpellEffects
type DetrimentalEffects = spells.DetrimentalEffects
type SpellEffectManager = spells.SpellEffectManager
@ -25,13 +25,13 @@ var NewSpellEffectManager = spells.NewSpellEffectManager
// Re-export constants
const (
ControlEffectStun = spells.ControlEffectStun
ControlEffectRoot = spells.ControlEffectRoot
ControlEffectMez = spells.ControlEffectMez
ControlEffectDaze = spells.ControlEffectDaze
ControlEffectFear = spells.ControlEffectFear
ControlEffectSlow = spells.ControlEffectSlow
ControlEffectStun = spells.ControlEffectStun
ControlEffectRoot = spells.ControlEffectRoot
ControlEffectMez = spells.ControlEffectMez
ControlEffectDaze = spells.ControlEffectDaze
ControlEffectFear = spells.ControlEffectFear
ControlEffectSlow = spells.ControlEffectSlow
ControlEffectSnare = spells.ControlEffectSnare
ControlEffectCharm = spells.ControlEffectCharm
ControlMaxEffects = spells.ControlMaxEffects
)
ControlMaxEffects = spells.ControlMaxEffects
)

View File

@ -5,24 +5,24 @@ const (
// Maximum and minimum faction values
MaxFactionValue = 50000
MinFactionValue = -50000
// Special faction ID ranges
SpecialFactionIDMax = 10 // Faction IDs <= 10 are special (not real factions)
SpecialFactionIDMax = 10 // Faction IDs <= 10 are special (not real factions)
// Faction consideration (con) ranges
MinCon = -4 // Hostile
MaxCon = 4 // Ally
MinCon = -4 // Hostile
MaxCon = 4 // Ally
// Con value thresholds
ConNeutralMin = -9999
ConNeutralMax = 9999
ConAllyMin = 40000
ConHostileMax = -40000
// Con calculation multiplier
ConMultiplier = 10000
ConRemainder = 9999
// Percentage calculation constants
PercentMultiplier = 100
PercentNeutralOffset = 10000
@ -34,13 +34,13 @@ const AttackThreshold = -4
// Default faction consideration values
const (
ConKOS = -4 // Kill on sight
ConThreat = -3 // Threatening
ConDubious = -2 // Dubiously
ConAppre = -1 // Apprehensive
ConIndiff = 0 // Indifferent
ConAmiable = 1 // Amiable
ConKindly = 2 // Kindly
ConWarmly = 3 // Warmly
ConAlly = 4 // Ally
)
ConKOS = -4 // Kill on sight
ConThreat = -3 // Threatening
ConDubious = -2 // Dubiously
ConAppre = -1 // Apprehensive
ConIndiff = 0 // Indifferent
ConAmiable = 1 // Amiable
ConKindly = 2 // Kindly
ConWarmly = 3 // Warmly
ConAlly = 4 // Ally
)

View File

@ -164,7 +164,7 @@ func (efa *EntityFactionAdapter) IsHostileToFaction(otherFactionID int32) bool {
}
hostileFactions := efa.manager.GetMasterFactionList().GetHostileFactions(factionID)
for _, hostileID := range hostileFactions {
if hostileID == otherFactionID {
return true
@ -182,7 +182,7 @@ func (efa *EntityFactionAdapter) IsFriendlyToFaction(otherFactionID int32) bool
}
friendlyFactions := efa.manager.GetMasterFactionList().GetFriendlyFactions(factionID)
for _, friendlyID := range friendlyFactions {
if friendlyID == otherFactionID {
return true
@ -238,10 +238,10 @@ func (pfm *PlayerFactionManager) GetPlayerFaction() *PlayerFaction {
// IncreaseFaction increases a faction and records statistics
func (pfm *PlayerFactionManager) IncreaseFaction(factionID int32, amount int32) bool {
result := pfm.playerFaction.IncreaseFaction(factionID, amount)
if result {
pfm.manager.RecordFactionIncrease(factionID)
if pfm.logger != nil {
pfm.logger.LogDebug("Player %d: Increased faction %d by %d",
pfm.player.GetCharacterID(), factionID, amount)
@ -254,10 +254,10 @@ func (pfm *PlayerFactionManager) IncreaseFaction(factionID int32, amount int32)
// DecreaseFaction decreases a faction and records statistics
func (pfm *PlayerFactionManager) DecreaseFaction(factionID int32, amount int32) bool {
result := pfm.playerFaction.DecreaseFaction(factionID, amount)
if result {
pfm.manager.RecordFactionDecrease(factionID)
if pfm.logger != nil {
pfm.logger.LogDebug("Player %d: Decreased faction %d by %d",
pfm.player.GetCharacterID(), factionID, amount)
@ -270,7 +270,7 @@ func (pfm *PlayerFactionManager) DecreaseFaction(factionID int32, amount int32)
// SetFactionValue sets a faction to a specific value
func (pfm *PlayerFactionManager) SetFactionValue(factionID int32, value int32) bool {
result := pfm.playerFaction.SetFactionValue(factionID, value)
if pfm.logger != nil {
pfm.logger.LogDebug("Player %d: Set faction %d to %d",
pfm.player.GetCharacterID(), factionID, value)
@ -354,7 +354,7 @@ func (pfm *PlayerFactionManager) SavePlayerFactions(database Database) error {
}
factionValues := pfm.playerFaction.GetFactionValues()
// TODO: Implement database saving when database system is integrated
// for factionID, value := range factionValues {
// if err := database.SavePlayerFaction(pfm.player.GetCharacterID(), factionID, value); err != nil {
@ -368,4 +368,4 @@ func (pfm *PlayerFactionManager) SavePlayerFactions(database Database) error {
}
return nil
}
}

View File

@ -13,12 +13,12 @@ type Manager struct {
mutex sync.RWMutex
// Statistics
totalFactionChanges int64
factionIncreases int64
factionDecreases int64
factionLookups int64
playersWithFactions int64
changesByFaction map[int32]int64 // Faction ID -> total changes
totalFactionChanges int64
factionIncreases int64
factionDecreases int64
factionLookups int64
playersWithFactions int64
changesByFaction map[int32]int64 // Faction ID -> total changes
}
// NewManager creates a new faction manager
@ -485,4 +485,4 @@ func contains(str, substr string) bool {
}
return false
}
}

View File

@ -7,11 +7,11 @@ import (
// MasterFactionList manages all factions in the game
type MasterFactionList struct {
globalFactionList map[int32]*Faction // Factions by ID
factionNameList map[string]*Faction // Factions by name
hostileFactions map[int32][]int32 // Hostile faction relationships
friendlyFactions map[int32][]int32 // Friendly faction relationships
mutex sync.RWMutex // Thread safety
globalFactionList map[int32]*Faction // Factions by ID
factionNameList map[string]*Faction // Factions by name
hostileFactions map[int32][]int32 // Hostile faction relationships
friendlyFactions map[int32][]int32 // Friendly faction relationships
mutex sync.RWMutex // Thread safety
}
// NewMasterFactionList creates a new master faction list
@ -28,7 +28,7 @@ func NewMasterFactionList() *MasterFactionList {
func (mfl *MasterFactionList) Clear() {
mfl.mutex.Lock()
defer mfl.mutex.Unlock()
// Clear all maps - Go's garbage collector will handle cleanup
mfl.globalFactionList = make(map[int32]*Faction)
mfl.factionNameList = make(map[string]*Faction)
@ -40,11 +40,11 @@ func (mfl *MasterFactionList) Clear() {
func (mfl *MasterFactionList) GetDefaultFactionValue(factionID int32) int32 {
mfl.mutex.RLock()
defer mfl.mutex.RUnlock()
if faction, exists := mfl.globalFactionList[factionID]; exists && faction != nil {
return faction.DefaultValue
}
return 0
}
@ -52,7 +52,7 @@ func (mfl *MasterFactionList) GetDefaultFactionValue(factionID int32) int32 {
func (mfl *MasterFactionList) GetFactionByName(name string) *Faction {
mfl.mutex.RLock()
defer mfl.mutex.RUnlock()
return mfl.factionNameList[name]
}
@ -60,11 +60,11 @@ func (mfl *MasterFactionList) GetFactionByName(name string) *Faction {
func (mfl *MasterFactionList) GetFaction(id int32) *Faction {
mfl.mutex.RLock()
defer mfl.mutex.RUnlock()
if faction, exists := mfl.globalFactionList[id]; exists {
return faction
}
return nil
}
@ -73,17 +73,17 @@ func (mfl *MasterFactionList) AddFaction(faction *Faction) error {
if faction == nil {
return fmt.Errorf("faction cannot be nil")
}
if !faction.IsValid() {
return fmt.Errorf("faction is not valid")
}
mfl.mutex.Lock()
defer mfl.mutex.Unlock()
mfl.globalFactionList[faction.ID] = faction
mfl.factionNameList[faction.Name] = faction
return nil
}
@ -91,11 +91,11 @@ func (mfl *MasterFactionList) AddFaction(faction *Faction) error {
func (mfl *MasterFactionList) GetIncreaseAmount(factionID int32) int32 {
mfl.mutex.RLock()
defer mfl.mutex.RUnlock()
if faction, exists := mfl.globalFactionList[factionID]; exists && faction != nil {
return int32(faction.PositiveChange)
}
return 0
}
@ -103,11 +103,11 @@ func (mfl *MasterFactionList) GetIncreaseAmount(factionID int32) int32 {
func (mfl *MasterFactionList) GetDecreaseAmount(factionID int32) int32 {
mfl.mutex.RLock()
defer mfl.mutex.RUnlock()
if faction, exists := mfl.globalFactionList[factionID]; exists && faction != nil {
return int32(faction.NegativeChange)
}
return 0
}
@ -115,7 +115,7 @@ func (mfl *MasterFactionList) GetDecreaseAmount(factionID int32) int32 {
func (mfl *MasterFactionList) GetFactionCount() int32 {
mfl.mutex.RLock()
defer mfl.mutex.RUnlock()
return int32(len(mfl.globalFactionList))
}
@ -123,7 +123,7 @@ func (mfl *MasterFactionList) GetFactionCount() int32 {
func (mfl *MasterFactionList) AddHostileFaction(factionID, hostileFactionID int32) {
mfl.mutex.Lock()
defer mfl.mutex.Unlock()
mfl.hostileFactions[factionID] = append(mfl.hostileFactions[factionID], hostileFactionID)
}
@ -131,7 +131,7 @@ func (mfl *MasterFactionList) AddHostileFaction(factionID, hostileFactionID int3
func (mfl *MasterFactionList) AddFriendlyFaction(factionID, friendlyFactionID int32) {
mfl.mutex.Lock()
defer mfl.mutex.Unlock()
mfl.friendlyFactions[factionID] = append(mfl.friendlyFactions[factionID], friendlyFactionID)
}
@ -139,14 +139,14 @@ func (mfl *MasterFactionList) AddFriendlyFaction(factionID, friendlyFactionID in
func (mfl *MasterFactionList) GetFriendlyFactions(factionID int32) []int32 {
mfl.mutex.RLock()
defer mfl.mutex.RUnlock()
if factions, exists := mfl.friendlyFactions[factionID]; exists {
// Return a copy to prevent external modification
result := make([]int32, len(factions))
copy(result, factions)
return result
}
return nil
}
@ -154,14 +154,14 @@ func (mfl *MasterFactionList) GetFriendlyFactions(factionID int32) []int32 {
func (mfl *MasterFactionList) GetHostileFactions(factionID int32) []int32 {
mfl.mutex.RLock()
defer mfl.mutex.RUnlock()
if factions, exists := mfl.hostileFactions[factionID]; exists {
// Return a copy to prevent external modification
result := make([]int32, len(factions))
copy(result, factions)
return result
}
return nil
}
@ -170,12 +170,12 @@ func (mfl *MasterFactionList) GetFactionNameByID(factionID int32) string {
if factionID > 0 {
mfl.mutex.RLock()
defer mfl.mutex.RUnlock()
if faction, exists := mfl.globalFactionList[factionID]; exists {
return faction.Name
}
}
return ""
}
@ -183,7 +183,7 @@ func (mfl *MasterFactionList) GetFactionNameByID(factionID int32) string {
func (mfl *MasterFactionList) HasFaction(factionID int32) bool {
mfl.mutex.RLock()
defer mfl.mutex.RUnlock()
_, exists := mfl.globalFactionList[factionID]
return exists
}
@ -192,7 +192,7 @@ func (mfl *MasterFactionList) HasFaction(factionID int32) bool {
func (mfl *MasterFactionList) HasFactionByName(name string) bool {
mfl.mutex.RLock()
defer mfl.mutex.RUnlock()
_, exists := mfl.factionNameList[name]
return exists
}
@ -201,12 +201,12 @@ func (mfl *MasterFactionList) HasFactionByName(name string) bool {
func (mfl *MasterFactionList) GetAllFactions() map[int32]*Faction {
mfl.mutex.RLock()
defer mfl.mutex.RUnlock()
result := make(map[int32]*Faction)
for id, faction := range mfl.globalFactionList {
result[id] = faction
}
return result
}
@ -214,12 +214,12 @@ func (mfl *MasterFactionList) GetAllFactions() map[int32]*Faction {
func (mfl *MasterFactionList) GetFactionIDs() []int32 {
mfl.mutex.RLock()
defer mfl.mutex.RUnlock()
ids := make([]int32, 0, len(mfl.globalFactionList))
for id := range mfl.globalFactionList {
ids = append(ids, id)
}
return ids
}
@ -227,36 +227,36 @@ func (mfl *MasterFactionList) GetFactionIDs() []int32 {
func (mfl *MasterFactionList) GetFactionsByType(factionType string) []*Faction {
mfl.mutex.RLock()
defer mfl.mutex.RUnlock()
var result []*Faction
for _, faction := range mfl.globalFactionList {
if faction.Type == factionType {
result = append(result, faction)
}
}
return result
return result
}
// RemoveFaction removes a faction by ID
func (mfl *MasterFactionList) RemoveFaction(factionID int32) bool {
mfl.mutex.Lock()
defer mfl.mutex.Unlock()
faction, exists := mfl.globalFactionList[factionID]
if !exists {
return false
}
// Remove from both maps
delete(mfl.globalFactionList, factionID)
delete(mfl.factionNameList, faction.Name)
// Remove from relationship maps
delete(mfl.hostileFactions, factionID)
delete(mfl.friendlyFactions, factionID)
// Remove references to this faction in other faction's relationships
for id, hostiles := range mfl.hostileFactions {
newHostiles := make([]int32, 0, len(hostiles))
@ -267,7 +267,7 @@ func (mfl *MasterFactionList) RemoveFaction(factionID int32) bool {
}
mfl.hostileFactions[id] = newHostiles
}
for id, friendlies := range mfl.friendlyFactions {
newFriendlies := make([]int32, 0, len(friendlies))
for _, friendlyID := range friendlies {
@ -277,7 +277,7 @@ func (mfl *MasterFactionList) RemoveFaction(factionID int32) bool {
}
mfl.friendlyFactions[id] = newFriendlies
}
return true
}
@ -286,29 +286,29 @@ func (mfl *MasterFactionList) UpdateFaction(faction *Faction) error {
if faction == nil {
return fmt.Errorf("faction cannot be nil")
}
if !faction.IsValid() {
return fmt.Errorf("faction is not valid")
}
mfl.mutex.Lock()
defer mfl.mutex.Unlock()
// Check if faction exists
oldFaction, exists := mfl.globalFactionList[faction.ID]
if !exists {
return fmt.Errorf("faction with ID %d does not exist", faction.ID)
}
// If name changed, update name map
if oldFaction.Name != faction.Name {
delete(mfl.factionNameList, oldFaction.Name)
mfl.factionNameList[faction.Name] = faction
}
// Update faction
mfl.globalFactionList[faction.ID] = faction
return nil
}
@ -316,67 +316,67 @@ func (mfl *MasterFactionList) UpdateFaction(faction *Faction) error {
func (mfl *MasterFactionList) ValidateFactions() []string {
mfl.mutex.RLock()
defer mfl.mutex.RUnlock()
var issues []string
// Check for nil factions
for id, faction := range mfl.globalFactionList {
if faction == nil {
issues = append(issues, fmt.Sprintf("Faction ID %d is nil", id))
continue
}
if !faction.IsValid() {
issues = append(issues, fmt.Sprintf("Faction ID %d is invalid", id))
}
if faction.ID != id {
issues = append(issues, fmt.Sprintf("Faction ID mismatch: map key %d != faction ID %d", id, faction.ID))
}
}
// Check name map consistency
for name, faction := range mfl.factionNameList {
if faction == nil {
issues = append(issues, fmt.Sprintf("Faction name '%s' maps to nil", name))
continue
}
if faction.Name != name {
issues = append(issues, fmt.Sprintf("Faction name mismatch: map key '%s' != faction name '%s'", name, faction.Name))
}
// Check if this faction exists in the ID map
if _, exists := mfl.globalFactionList[faction.ID]; !exists {
issues = append(issues, fmt.Sprintf("Faction '%s' (ID %d) exists in name map but not in ID map", name, faction.ID))
}
}
// Check relationship consistency
for factionID, hostiles := range mfl.hostileFactions {
if _, exists := mfl.globalFactionList[factionID]; !exists {
issues = append(issues, fmt.Sprintf("Hostile relationship defined for non-existent faction %d", factionID))
}
for _, hostileID := range hostiles {
if _, exists := mfl.globalFactionList[hostileID]; !exists {
issues = append(issues, fmt.Sprintf("Faction %d has hostile relationship with non-existent faction %d", factionID, hostileID))
}
}
}
for factionID, friendlies := range mfl.friendlyFactions {
if _, exists := mfl.globalFactionList[factionID]; !exists {
issues = append(issues, fmt.Sprintf("Friendly relationship defined for non-existent faction %d", factionID))
}
for _, friendlyID := range friendlies {
if _, exists := mfl.globalFactionList[friendlyID]; !exists {
issues = append(issues, fmt.Sprintf("Faction %d has friendly relationship with non-existent faction %d", factionID, friendlyID))
}
}
}
return issues
}
@ -384,4 +384,4 @@ func (mfl *MasterFactionList) ValidateFactions() []string {
func (mfl *MasterFactionList) IsValid() bool {
issues := mfl.ValidateFactions()
return len(issues) == 0
}
}

View File

@ -6,12 +6,12 @@ import (
// PlayerFaction manages faction standing for a single player
type PlayerFaction struct {
factionValues map[int32]int32 // Faction ID -> current value
factionPercent map[int32]int8 // Faction ID -> percentage within con level
factionUpdateNeeded []int32 // Factions that need client updates
factionValues map[int32]int32 // Faction ID -> current value
factionPercent map[int32]int8 // Faction ID -> percentage within con level
factionUpdateNeeded []int32 // Factions that need client updates
masterFactionList *MasterFactionList
updateMutex sync.Mutex // Thread safety for updates
mutex sync.RWMutex // Thread safety for faction data
updateMutex sync.Mutex // Thread safety for updates
mutex sync.RWMutex // Thread safety for faction data
}
// NewPlayerFaction creates a new player faction system
@ -54,24 +54,24 @@ func (pf *PlayerFaction) GetCon(factionID int32) int8 {
}
return int8(factionID - 5)
}
value := pf.GetFactionValue(factionID)
// Neutral range
if value >= ConNeutralMin && value <= ConNeutralMax {
return ConIndiff
}
// Maximum ally
if value >= ConAllyMin {
return ConAlly
}
// Maximum hostile
if value <= ConHostileMax {
return ConKOS
}
// Calculate con based on value
return int8(value / ConMultiplier)
}
@ -82,21 +82,21 @@ func (pf *PlayerFaction) GetPercent(factionID int32) int8 {
if factionID <= SpecialFactionIDMax {
return 0
}
con := pf.GetCon(factionID)
value := pf.GetFactionValue(factionID)
if con != ConIndiff {
// Make value positive for calculation
if value <= 0 {
value *= -1
}
// Make con positive for calculation
if con < 0 {
con *= -1
}
// Calculate percentage within the con level
value -= int32(con) * ConMultiplier
value *= PercentMultiplier
@ -113,11 +113,11 @@ func (pf *PlayerFaction) GetPercent(factionID int32) int8 {
func (pf *PlayerFaction) FactionUpdate(version int16) ([]byte, error) {
pf.updateMutex.Lock()
defer pf.updateMutex.Unlock()
if len(pf.factionUpdateNeeded) == 0 {
return nil, nil
}
// This is a placeholder for packet building
// In the full implementation, this would use the PacketStruct system:
// packet := configReader.getStruct("WS_FactionUpdate", version)
@ -135,10 +135,10 @@ func (pf *PlayerFaction) FactionUpdate(version int16) ([]byte, error) {
// }
// }
// return packet.serialize()
// Clear update list
pf.factionUpdateNeeded = pf.factionUpdateNeeded[:0]
// Return empty packet for now
return make([]byte, 0), nil
}
@ -149,10 +149,10 @@ func (pf *PlayerFaction) GetFactionValue(factionID int32) int32 {
if factionID <= SpecialFactionIDMax {
return 0
}
pf.mutex.RLock()
defer pf.mutex.RUnlock()
// Return current value or 0 if not set
// Note: The C++ code has a comment about always returning the default value,
// but the actual implementation returns the stored value or 0
@ -164,11 +164,11 @@ func (pf *PlayerFaction) ShouldIncrease(factionID int32) bool {
if factionID <= SpecialFactionIDMax {
return false
}
if pf.masterFactionList == nil {
return false
}
return pf.masterFactionList.GetIncreaseAmount(factionID) != 0
}
@ -177,11 +177,11 @@ func (pf *PlayerFaction) ShouldDecrease(factionID int32) bool {
if factionID <= SpecialFactionIDMax {
return false
}
if pf.masterFactionList == nil {
return false
}
return pf.masterFactionList.GetDecreaseAmount(factionID) != 0
}
@ -191,29 +191,29 @@ func (pf *PlayerFaction) IncreaseFaction(factionID int32, amount int32) bool {
if factionID <= SpecialFactionIDMax {
return true
}
pf.mutex.Lock()
defer pf.mutex.Unlock()
// Use default amount if not specified
if amount == 0 && pf.masterFactionList != nil {
amount = pf.masterFactionList.GetIncreaseAmount(factionID)
}
// Increase the faction value
pf.factionValues[factionID] += amount
canContinue := true
// Cap at maximum value
if pf.factionValues[factionID] >= MaxFactionValue {
pf.factionValues[factionID] = MaxFactionValue
canContinue = false
}
// Mark for update
pf.addFactionUpdateNeeded(factionID)
return canContinue
}
@ -223,34 +223,34 @@ func (pf *PlayerFaction) DecreaseFaction(factionID int32, amount int32) bool {
if factionID <= SpecialFactionIDMax {
return true
}
pf.mutex.Lock()
defer pf.mutex.Unlock()
// Use default amount if not specified
if amount == 0 && pf.masterFactionList != nil {
amount = pf.masterFactionList.GetDecreaseAmount(factionID)
}
// Cannot decrease if no amount specified
if amount == 0 {
return false
}
// Decrease the faction value
pf.factionValues[factionID] -= amount
canContinue := true
// Cap at minimum value
if pf.factionValues[factionID] <= MinFactionValue {
pf.factionValues[factionID] = MinFactionValue
canContinue = false
}
// Mark for update
pf.addFactionUpdateNeeded(factionID)
return canContinue
}
@ -258,12 +258,12 @@ func (pf *PlayerFaction) DecreaseFaction(factionID int32, amount int32) bool {
func (pf *PlayerFaction) SetFactionValue(factionID int32, value int32) bool {
pf.mutex.Lock()
defer pf.mutex.Unlock()
pf.factionValues[factionID] = value
// Mark for update
pf.addFactionUpdateNeeded(factionID)
return true
}
@ -271,13 +271,13 @@ func (pf *PlayerFaction) SetFactionValue(factionID int32, value int32) bool {
func (pf *PlayerFaction) GetFactionValues() map[int32]int32 {
pf.mutex.RLock()
defer pf.mutex.RUnlock()
// Return a copy to prevent external modification
result := make(map[int32]int32)
for id, value := range pf.factionValues {
result[id] = value
}
return result
}
@ -285,7 +285,7 @@ func (pf *PlayerFaction) GetFactionValues() map[int32]int32 {
func (pf *PlayerFaction) HasFaction(factionID int32) bool {
pf.mutex.RLock()
defer pf.mutex.RUnlock()
_, exists := pf.factionValues[factionID]
return exists
}
@ -294,7 +294,7 @@ func (pf *PlayerFaction) HasFaction(factionID int32) bool {
func (pf *PlayerFaction) GetFactionCount() int {
pf.mutex.RLock()
defer pf.mutex.RUnlock()
return len(pf.factionValues)
}
@ -302,7 +302,7 @@ func (pf *PlayerFaction) GetFactionCount() int {
func (pf *PlayerFaction) ClearFactionValues() {
pf.mutex.Lock()
defer pf.mutex.Unlock()
pf.factionValues = make(map[int32]int32)
pf.factionPercent = make(map[int32]int8)
}
@ -312,7 +312,7 @@ func (pf *PlayerFaction) addFactionUpdateNeeded(factionID int32) {
// Note: This method assumes the mutex is already held by the caller
pf.updateMutex.Lock()
defer pf.updateMutex.Unlock()
pf.factionUpdateNeeded = append(pf.factionUpdateNeeded, factionID)
}
@ -320,15 +320,15 @@ func (pf *PlayerFaction) addFactionUpdateNeeded(factionID int32) {
func (pf *PlayerFaction) GetPendingUpdates() []int32 {
pf.updateMutex.Lock()
defer pf.updateMutex.Unlock()
if len(pf.factionUpdateNeeded) == 0 {
return nil
}
// Return a copy
result := make([]int32, len(pf.factionUpdateNeeded))
copy(result, pf.factionUpdateNeeded)
return result
}
@ -336,7 +336,7 @@ func (pf *PlayerFaction) GetPendingUpdates() []int32 {
func (pf *PlayerFaction) ClearPendingUpdates() {
pf.updateMutex.Lock()
defer pf.updateMutex.Unlock()
pf.factionUpdateNeeded = pf.factionUpdateNeeded[:0]
}
@ -344,6 +344,6 @@ func (pf *PlayerFaction) ClearPendingUpdates() {
func (pf *PlayerFaction) HasPendingUpdates() bool {
pf.updateMutex.Lock()
defer pf.updateMutex.Unlock()
return len(pf.factionUpdateNeeded) > 0
}
}

View File

@ -2,25 +2,25 @@ package factions
// Faction represents a single faction with its properties
type Faction struct {
ID int32 // Faction ID
Name string // Faction name
Type string // Faction type/category
Description string // Faction description
NegativeChange int16 // Amount faction decreases by default
PositiveChange int16 // Amount faction increases by default
DefaultValue int32 // Default faction value for new characters
ID int32 // Faction ID
Name string // Faction name
Type string // Faction type/category
Description string // Faction description
NegativeChange int16 // Amount faction decreases by default
PositiveChange int16 // Amount faction increases by default
DefaultValue int32 // Default faction value for new characters
}
// NewFaction creates a new faction with the given parameters
func NewFaction(id int32, name, factionType, description string) *Faction {
return &Faction{
ID: id,
Name: name,
Type: factionType,
Description: description,
NegativeChange: 0,
PositiveChange: 0,
DefaultValue: 0,
ID: id,
Name: name,
Type: factionType,
Description: description,
NegativeChange: 0,
PositiveChange: 0,
DefaultValue: 0,
}
}
@ -77,13 +77,13 @@ func (f *Faction) SetDefaultValue(value int32) {
// Clone creates a copy of the faction
func (f *Faction) Clone() *Faction {
return &Faction{
ID: f.ID,
Name: f.Name,
Type: f.Type,
Description: f.Description,
NegativeChange: f.NegativeChange,
PositiveChange: f.PositiveChange,
DefaultValue: f.DefaultValue,
ID: f.ID,
Name: f.Name,
Type: f.Type,
Description: f.Description,
NegativeChange: f.NegativeChange,
PositiveChange: f.PositiveChange,
DefaultValue: f.DefaultValue,
}
}
@ -105,4 +105,4 @@ func (f *Faction) CanIncrease() bool {
// CanDecrease returns true if this faction can be decreased
func (f *Faction) CanDecrease() bool {
return !f.IsSpecialFaction() && f.NegativeChange != 0
}
}

View File

@ -55,22 +55,22 @@ const (
// Default spawn configuration
const (
DefaultDifficulty = 0
DefaultSpawnType = 2
DefaultState = 129
DefaultDifficulty = 0
DefaultSpawnType = 2
DefaultState = 129
DefaultAttemptsPerHarvest = 1
DefaultNumberHarvests = 1
DefaultRandomizeHeading = true
DefaultNumberHarvests = 1
DefaultRandomizeHeading = true
)
// Harvest message channels (placeholder values)
const (
ChannelHarvesting = 15
ChannelColorRed = 13
ChannelHarvesting = 15
ChannelColorRed = 13
)
// Statistical tracking
const (
StatPlayerItemsHarvested = 1
StatPlayerRaresHarvested = 2
)
)

View File

@ -126,7 +126,7 @@ type SkillProvider interface {
// SpawnProvider interface for spawn system integration
type SpawnProvider interface {
CreateSpawn() interface{}
CreateSpawn() interface{}
GetSpawn(id int32) interface{}
RegisterGroundSpawn(gs *GroundSpawn) error
UnregisterGroundSpawn(id int32) error
@ -142,9 +142,9 @@ type GroundSpawnAware interface {
// PlayerGroundSpawnAdapter provides ground spawn functionality for players
type PlayerGroundSpawnAdapter struct {
player *Player
manager *Manager
logger Logger
player *Player
manager *Manager
logger Logger
}
// NewPlayerGroundSpawnAdapter creates a new player ground spawn adapter
@ -161,20 +161,20 @@ func (pgsa *PlayerGroundSpawnAdapter) CanHarvest(gs *GroundSpawn) bool {
if gs == nil || pgsa.player == nil {
return false
}
// Check if ground spawn is available
if !gs.IsAvailable() {
return false
}
// Check if player has required skill
skill := pgsa.player.GetSkillByName(gs.GetCollectionSkill())
if skill == nil {
return false
}
// TODO: Add additional checks (quest requirements, level, etc.)
return true
}
@ -183,7 +183,7 @@ func (pgsa *PlayerGroundSpawnAdapter) GetHarvestSkill(skillName string) *Skill {
if pgsa.player == nil {
return nil
}
return pgsa.player.GetSkillByName(skillName)
}
@ -203,7 +203,7 @@ func (pgsa *PlayerGroundSpawnAdapter) OnHarvestResult(result *HarvestResult) {
if result == nil || pgsa.player == nil {
return
}
if result.Success && len(result.ItemsAwarded) > 0 {
if pgsa.logger != nil {
pgsa.logger.LogDebug("Player %s successfully harvested %d items",
@ -231,30 +231,30 @@ func (hea *HarvestEventAdapter) ProcessHarvestEvent(eventType string, gs *Ground
if hea.handler == nil {
return
}
switch eventType {
case "harvest_start":
if err := hea.handler.OnHarvestStart(gs, player); err != nil && hea.logger != nil {
hea.logger.LogError("Harvest start handler failed: %v", err)
}
case "harvest_complete":
if result, ok := data.(*HarvestResult); ok {
if err := hea.handler.OnHarvestComplete(gs, player, result); err != nil && hea.logger != nil {
hea.logger.LogError("Harvest complete handler failed: %v", err)
}
}
case "harvest_failed":
if reason, ok := data.(string); ok {
if err := hea.handler.OnHarvestFailed(gs, player, reason); err != nil && hea.logger != nil {
hea.logger.LogError("Harvest failed handler failed: %v", err)
}
}
case "ground_spawn_depleted":
if err := hea.handler.OnGroundSpawnDepleted(gs); err != nil && hea.logger != nil {
hea.logger.LogError("Ground spawn depleted handler failed: %v", err)
}
}
}
}

View File

@ -2,21 +2,20 @@ package ground_spawn
import (
"fmt"
"sync"
"time"
)
// NewManager creates a new ground spawn manager
func NewManager(database Database, logger Logger) *Manager {
return &Manager{
groundSpawns: make(map[int32]*GroundSpawn),
spawnsByZone: make(map[int32][]*GroundSpawn),
entriesByID: make(map[int32][]*GroundSpawnEntry),
itemsByID: make(map[int32][]*GroundSpawnEntryItem),
respawnQueue: make(map[int32]time.Time),
database: database,
logger: logger,
harvestsBySkill: make(map[string]int64),
groundSpawns: make(map[int32]*GroundSpawn),
spawnsByZone: make(map[int32][]*GroundSpawn),
entriesByID: make(map[int32][]*GroundSpawnEntry),
itemsByID: make(map[int32][]*GroundSpawnEntryItem),
respawnQueue: make(map[int32]time.Time),
database: database,
logger: logger,
harvestsBySkill: make(map[string]int64),
}
}
@ -25,86 +24,86 @@ func (m *Manager) Initialize() error {
if m.logger != nil {
m.logger.LogInfo("Initializing ground spawn manager...")
}
if m.database == nil {
if m.logger != nil {
m.logger.LogWarning("No database provided, starting with empty ground spawn list")
}
return nil
}
// Load ground spawns from database
groundSpawns, err := m.database.LoadAllGroundSpawns()
if err != nil {
return fmt.Errorf("failed to load ground spawns from database: %w", err)
}
m.mutex.Lock()
defer m.mutex.Unlock()
for _, gs := range groundSpawns {
m.groundSpawns[gs.GetID()] = gs
// Group by zone (placeholder - zone ID would come from spawn location)
zoneID := int32(1) // TODO: Get actual zone ID from spawn
m.spawnsByZone[zoneID] = append(m.spawnsByZone[zoneID], gs)
// Load harvest entries and items
if err := m.loadGroundSpawnData(gs); err != nil && m.logger != nil {
m.logger.LogWarning("Failed to load data for ground spawn %d: %v", gs.GetID(), err)
}
}
if m.logger != nil {
m.logger.LogInfo("Loaded %d ground spawns from database", len(groundSpawns))
}
return nil
}
// loadGroundSpawnData loads entries and items for a ground spawn
func (m *Manager) loadGroundSpawnData(gs *GroundSpawn) error {
groundspawnID := gs.GetGroundSpawnEntryID()
// Load harvest entries
entries, err := m.database.LoadGroundSpawnEntries(groundspawnID)
if err != nil {
return fmt.Errorf("failed to load entries for groundspawn %d: %w", groundspawnID, err)
}
m.entriesByID[groundspawnID] = entries
// Load harvest items
items, err := m.database.LoadGroundSpawnItems(groundspawnID)
if err != nil {
return fmt.Errorf("failed to load items for groundspawn %d: %w", groundspawnID, err)
}
m.itemsByID[groundspawnID] = items
return nil
}
// CreateGroundSpawn creates a new ground spawn
func (m *Manager) CreateGroundSpawn(config GroundSpawnConfig) *GroundSpawn {
gs := NewGroundSpawn(config)
m.mutex.Lock()
defer m.mutex.Unlock()
// Generate ID (placeholder implementation)
newID := int32(len(m.groundSpawns) + 1)
gs.SetID(newID)
// Store ground spawn
m.groundSpawns[newID] = gs
// Group by zone
zoneID := int32(1) // TODO: Get actual zone ID from config.Location
m.spawnsByZone[zoneID] = append(m.spawnsByZone[zoneID], gs)
if m.logger != nil {
m.logger.LogInfo("Created ground spawn %d: %s", newID, gs.GetName())
}
return gs
}
@ -112,7 +111,7 @@ func (m *Manager) CreateGroundSpawn(config GroundSpawnConfig) *GroundSpawn {
func (m *Manager) GetGroundSpawn(id int32) *GroundSpawn {
m.mutex.RLock()
defer m.mutex.RUnlock()
return m.groundSpawns[id]
}
@ -120,16 +119,16 @@ func (m *Manager) GetGroundSpawn(id int32) *GroundSpawn {
func (m *Manager) GetGroundSpawnsByZone(zoneID int32) []*GroundSpawn {
m.mutex.RLock()
defer m.mutex.RUnlock()
spawns := m.spawnsByZone[zoneID]
if spawns == nil {
return []*GroundSpawn{}
}
// Return a copy to prevent external modification
result := make([]*GroundSpawn, len(spawns))
copy(result, spawns)
return result
}
@ -138,88 +137,88 @@ func (m *Manager) ProcessHarvest(gs *GroundSpawn, player *Player) (*HarvestResul
if gs == nil {
return nil, fmt.Errorf("ground spawn cannot be nil")
}
if player == nil {
return nil, fmt.Errorf("player cannot be nil")
}
// Record statistics
m.mutex.Lock()
m.totalHarvests++
skill := gs.GetCollectionSkill()
m.harvestsBySkill[skill]++
m.mutex.Unlock()
// Build harvest context
context, err := m.buildHarvestContext(gs, player)
if err != nil {
return nil, fmt.Errorf("failed to build harvest context: %w", err)
}
// Process the harvest
result, err := gs.ProcessHarvest(context)
if err != nil {
return nil, fmt.Errorf("harvest processing failed: %w", err)
}
// Update statistics
if result != nil && result.Success {
m.mutex.Lock()
m.successfulHarvests++
// Count rare items
for _, item := range result.ItemsAwarded {
if item.IsRare {
m.rareItemsHarvested++
}
}
if result.SkillGained {
m.skillUpsGenerated++
}
m.mutex.Unlock()
}
// Handle respawn if depleted
if gs.IsDepleted() {
m.scheduleRespawn(gs)
}
return result, nil
}
// buildHarvestContext creates a harvest context for processing
func (m *Manager) buildHarvestContext(gs *GroundSpawn, player *Player) (*HarvestContext, error) {
groundspawnID := gs.GetGroundSpawnEntryID()
m.mutex.RLock()
entries := m.entriesByID[groundspawnID]
items := m.itemsByID[groundspawnID]
m.mutex.RUnlock()
if entries == nil || len(entries) == 0 {
return nil, fmt.Errorf("no harvest entries found for groundspawn %d", groundspawnID)
}
if items == nil || len(items) == 0 {
return nil, fmt.Errorf("no harvest items found for groundspawn %d", groundspawnID)
}
// Get player skill
skillName := gs.GetCollectionSkill()
if skillName == SkillCollecting {
skillName = SkillGathering // Collections use gathering skill
}
playerSkill := player.GetSkillByName(skillName)
if playerSkill == nil {
return nil, fmt.Errorf("player lacks required skill: %s", skillName)
}
// Calculate total skill (base + bonuses)
totalSkill := playerSkill.GetCurrentValue()
// TODO: Add stat bonuses when stat system is integrated
// Find max skill required
var maxSkillRequired int16
for _, entry := range entries {
@ -227,16 +226,16 @@ func (m *Manager) buildHarvestContext(gs *GroundSpawn, player *Player) (*Harvest
maxSkillRequired = entry.MinSkillLevel
}
}
return &HarvestContext{
Player: player,
GroundSpawn: gs,
PlayerSkill: playerSkill,
TotalSkill: totalSkill,
GroundSpawnEntries: entries,
GroundSpawnItems: items,
IsCollection: gs.GetCollectionSkill() == SkillCollecting,
MaxSkillRequired: maxSkillRequired,
Player: player,
GroundSpawn: gs,
PlayerSkill: playerSkill,
TotalSkill: totalSkill,
GroundSpawnEntries: entries,
GroundSpawnItems: items,
IsCollection: gs.GetCollectionSkill() == SkillCollecting,
MaxSkillRequired: maxSkillRequired,
}, nil
}
@ -245,15 +244,15 @@ func (m *Manager) scheduleRespawn(gs *GroundSpawn) {
if gs == nil {
return
}
// TODO: Get respawn timer from configuration or database
respawnDelay := 5 * time.Minute // Default 5 minutes
respawnTime := time.Now().Add(respawnDelay)
m.mutex.Lock()
m.respawnQueue[gs.GetID()] = respawnTime
m.mutex.Unlock()
if m.logger != nil {
m.logger.LogDebug("Scheduled ground spawn %d for respawn at %v", gs.GetID(), respawnTime)
}
@ -263,7 +262,7 @@ func (m *Manager) scheduleRespawn(gs *GroundSpawn) {
func (m *Manager) ProcessRespawns() {
now := time.Now()
var toRespawn []int32
m.mutex.Lock()
for spawnID, respawnTime := range m.respawnQueue {
if now.After(respawnTime) {
@ -272,7 +271,7 @@ func (m *Manager) ProcessRespawns() {
}
}
m.mutex.Unlock()
// Respawn outside of lock
for _, spawnID := range toRespawn {
if gs := m.GetGroundSpawn(spawnID); gs != nil {
@ -288,27 +287,27 @@ func (m *Manager) ProcessRespawns() {
func (m *Manager) GetStatistics() *HarvestStatistics {
m.mutex.RLock()
defer m.mutex.RUnlock()
// Count spawns by zone
spawnsByZone := make(map[int32]int)
for zoneID, spawns := range m.spawnsByZone {
spawnsByZone[zoneID] = len(spawns)
}
// Copy harvests by skill
harvestsBySkill := make(map[string]int64)
for skill, count := range m.harvestsBySkill {
harvestsBySkill[skill] = count
}
return &HarvestStatistics{
TotalHarvests: m.totalHarvests,
SuccessfulHarvests: m.successfulHarvests,
RareItemsHarvested: m.rareItemsHarvested,
SkillUpsGenerated: m.skillUpsGenerated,
HarvestsBySkill: harvestsBySkill,
ActiveGroundSpawns: len(m.groundSpawns),
GroundSpawnsByZone: spawnsByZone,
TotalHarvests: m.totalHarvests,
SuccessfulHarvests: m.successfulHarvests,
RareItemsHarvested: m.rareItemsHarvested,
SkillUpsGenerated: m.skillUpsGenerated,
HarvestsBySkill: harvestsBySkill,
ActiveGroundSpawns: len(m.groundSpawns),
GroundSpawnsByZone: spawnsByZone,
}
}
@ -316,7 +315,7 @@ func (m *Manager) GetStatistics() *HarvestStatistics {
func (m *Manager) ResetStatistics() {
m.mutex.Lock()
defer m.mutex.Unlock()
m.totalHarvests = 0
m.successfulHarvests = 0
m.rareItemsHarvested = 0
@ -329,28 +328,28 @@ func (m *Manager) AddGroundSpawn(gs *GroundSpawn) error {
if gs == nil {
return fmt.Errorf("ground spawn cannot be nil")
}
m.mutex.Lock()
defer m.mutex.Unlock()
// Check if ID is already used
if _, exists := m.groundSpawns[gs.GetID()]; exists {
return fmt.Errorf("ground spawn with ID %d already exists", gs.GetID())
}
m.groundSpawns[gs.GetID()] = gs
// Group by zone (placeholder)
zoneID := int32(1) // TODO: Get actual zone ID
m.spawnsByZone[zoneID] = append(m.spawnsByZone[zoneID], gs)
// Load harvest data if database is available
if m.database != nil {
if err := m.loadGroundSpawnData(gs); err != nil && m.logger != nil {
m.logger.LogWarning("Failed to load data for ground spawn %d: %v", gs.GetID(), err)
}
}
return nil
}
@ -358,15 +357,15 @@ func (m *Manager) AddGroundSpawn(gs *GroundSpawn) error {
func (m *Manager) RemoveGroundSpawn(id int32) bool {
m.mutex.Lock()
defer m.mutex.Unlock()
gs, exists := m.groundSpawns[id]
if !exists {
return false
}
delete(m.groundSpawns, id)
delete(m.respawnQueue, id)
// Remove from zone list
// TODO: Get actual zone ID from ground spawn
zoneID := int32(1)
@ -378,14 +377,14 @@ func (m *Manager) RemoveGroundSpawn(id int32) bool {
}
}
}
// Clean up harvest data
if gs != nil {
groundspawnID := gs.GetGroundSpawnEntryID()
delete(m.entriesByID, groundspawnID)
delete(m.itemsByID, groundspawnID)
}
return true
}
@ -393,7 +392,7 @@ func (m *Manager) RemoveGroundSpawn(id int32) bool {
func (m *Manager) GetGroundSpawnCount() int {
m.mutex.RLock()
defer m.mutex.RUnlock()
return len(m.groundSpawns)
}
@ -401,14 +400,14 @@ func (m *Manager) GetGroundSpawnCount() int {
func (m *Manager) GetActiveGroundSpawns() []*GroundSpawn {
m.mutex.RLock()
defer m.mutex.RUnlock()
var active []*GroundSpawn
for _, gs := range m.groundSpawns {
if gs.IsAvailable() {
active = append(active, gs)
}
}
return active
}
@ -416,14 +415,14 @@ func (m *Manager) GetActiveGroundSpawns() []*GroundSpawn {
func (m *Manager) GetDepletedGroundSpawns() []*GroundSpawn {
m.mutex.RLock()
defer m.mutex.RUnlock()
var depleted []*GroundSpawn
for _, gs := range m.groundSpawns {
if gs.IsDepleted() {
depleted = append(depleted, gs)
}
}
return depleted
}
@ -448,21 +447,21 @@ func (m *Manager) ProcessCommand(command string, args []string) (string, error)
// handleStatsCommand shows ground spawn system statistics
func (m *Manager) handleStatsCommand(args []string) (string, error) {
stats := m.GetStatistics()
result := "Ground Spawn System Statistics:\n"
result += fmt.Sprintf("Total Harvests: %d\n", stats.TotalHarvests)
result += fmt.Sprintf("Successful Harvests: %d\n", stats.SuccessfulHarvests)
result += fmt.Sprintf("Rare Items Harvested: %d\n", stats.RareItemsHarvested)
result += fmt.Sprintf("Skill Ups Generated: %d\n", stats.SkillUpsGenerated)
result += fmt.Sprintf("Active Ground Spawns: %d\n", stats.ActiveGroundSpawns)
if len(stats.HarvestsBySkill) > 0 {
result += "\nHarvests by Skill:\n"
for skill, count := range stats.HarvestsBySkill {
result += fmt.Sprintf(" %s: %d\n", skill, count)
}
}
return result, nil
}
@ -472,13 +471,13 @@ func (m *Manager) handleListCommand(args []string) (string, error) {
if count == 0 {
return "No ground spawns loaded.", nil
}
active := m.GetActiveGroundSpawns()
depleted := m.GetDepletedGroundSpawns()
result := fmt.Sprintf("Ground Spawns (Total: %d, Active: %d, Depleted: %d):\n",
result := fmt.Sprintf("Ground Spawns (Total: %d, Active: %d, Depleted: %d):\n",
count, len(active), len(depleted))
// Show first 10 active spawns
shown := 0
for _, gs := range active {
@ -486,11 +485,11 @@ func (m *Manager) handleListCommand(args []string) (string, error) {
result += "... (and more)\n"
break
}
result += fmt.Sprintf(" %d: %s (%s) - %d harvests remaining\n",
result += fmt.Sprintf(" %d: %s (%s) - %d harvests remaining\n",
gs.GetID(), gs.GetName(), gs.GetCollectionSkill(), gs.GetNumberHarvests())
shown++
}
return result, nil
}
@ -502,22 +501,22 @@ func (m *Manager) handleRespawnCommand(args []string) (string, error) {
if _, err := fmt.Sscanf(args[0], "%d", &spawnID); err != nil {
return "", fmt.Errorf("invalid ground spawn ID: %s", args[0])
}
gs := m.GetGroundSpawn(spawnID)
if gs == nil {
return fmt.Sprintf("Ground spawn %d not found.", spawnID), nil
}
gs.Respawn()
return fmt.Sprintf("Ground spawn %d respawned.", spawnID), nil
}
// Respawn all depleted spawns
depleted := m.GetDepletedGroundSpawns()
for _, gs := range depleted {
gs.Respawn()
}
return fmt.Sprintf("Respawned %d depleted ground spawns.", len(depleted)), nil
}
@ -526,17 +525,17 @@ func (m *Manager) handleInfoCommand(args []string) (string, error) {
if len(args) == 0 {
return "", fmt.Errorf("ground spawn ID required")
}
var spawnID int32
if _, err := fmt.Sscanf(args[0], "%d", &spawnID); err != nil {
return "", fmt.Errorf("invalid ground spawn ID: %s", args[0])
}
gs := m.GetGroundSpawn(spawnID)
if gs == nil {
return fmt.Sprintf("Ground spawn %d not found.", spawnID), nil
}
result := fmt.Sprintf("Ground Spawn Information:\n")
result += fmt.Sprintf("ID: %d\n", gs.GetID())
result += fmt.Sprintf("Name: %s\n", gs.GetName())
@ -546,7 +545,7 @@ func (m *Manager) handleInfoCommand(args []string) (string, error) {
result += fmt.Sprintf("Ground Spawn Entry ID: %d\n", gs.GetGroundSpawnEntryID())
result += fmt.Sprintf("Available: %v\n", gs.IsAvailable())
result += fmt.Sprintf("Depleted: %v\n", gs.IsDepleted())
return result, nil
}
@ -555,7 +554,7 @@ func (m *Manager) handleReloadCommand(args []string) (string, error) {
if m.database == nil {
return "", fmt.Errorf("no database available")
}
// Clear current data
m.mutex.Lock()
m.groundSpawns = make(map[int32]*GroundSpawn)
@ -564,12 +563,12 @@ func (m *Manager) handleReloadCommand(args []string) (string, error) {
m.itemsByID = make(map[int32][]*GroundSpawnEntryItem)
m.respawnQueue = make(map[int32]time.Time)
m.mutex.Unlock()
// Reload from database
if err := m.Initialize(); err != nil {
return "", fmt.Errorf("failed to reload ground spawns: %w", err)
}
count := m.GetGroundSpawnCount()
return fmt.Sprintf("Successfully reloaded %d ground spawns from database.", count), nil
}
@ -579,14 +578,14 @@ func (m *Manager) Shutdown() {
if m.logger != nil {
m.logger.LogInfo("Shutting down ground spawn manager...")
}
m.mutex.Lock()
defer m.mutex.Unlock()
// Clear all data
m.groundSpawns = make(map[int32]*GroundSpawn)
m.spawnsByZone = make(map[int32][]*GroundSpawn)
m.entriesByID = make(map[int32][]*GroundSpawnEntry)
m.itemsByID = make(map[int32][]*GroundSpawnEntryItem)
m.respawnQueue = make(map[int32]time.Time)
}
}

View File

@ -4,22 +4,21 @@ import (
"sync"
"time"
"eq2emu/internal/common"
"eq2emu/internal/spawn"
)
// GroundSpawn represents a harvestable resource node in the game world
type GroundSpawn struct {
*spawn.Spawn // Embed spawn for base functionality
numberHarvests int8 // Number of harvests remaining
numAttemptsPerHarvest int8 // Attempts per harvest session
groundspawnID int32 // Database ID for this groundspawn entry
collectionSkill string // Required skill for harvesting
randomizeHeading bool // Whether to randomize heading on spawn
harvestMutex sync.Mutex // Thread safety for harvest operations
harvestUseMutex sync.Mutex // Thread safety for use operations
*spawn.Spawn // Embed spawn for base functionality
numberHarvests int8 // Number of harvests remaining
numAttemptsPerHarvest int8 // Attempts per harvest session
groundspawnID int32 // Database ID for this groundspawn entry
collectionSkill string // Required skill for harvesting
randomizeHeading bool // Whether to randomize heading on spawn
harvestMutex sync.Mutex // Thread safety for harvest operations
harvestUseMutex sync.Mutex // Thread safety for use operations
}
// GroundSpawnEntry represents harvest table data from database
@ -38,40 +37,40 @@ type GroundSpawnEntry struct {
// GroundSpawnEntryItem represents items that can be harvested
type GroundSpawnEntryItem struct {
ItemID int32 // Item database ID
IsRare int8 // 0=normal, 1=rare, 2=imbue
GridID int32 // Grid restriction (0=any)
Quantity int16 // Item quantity (usually 1)
ItemID int32 // Item database ID
IsRare int8 // 0=normal, 1=rare, 2=imbue
GridID int32 // Grid restriction (0=any)
Quantity int16 // Item quantity (usually 1)
}
// HarvestResult represents the outcome of a harvest attempt
type HarvestResult struct {
Success bool // Whether harvest succeeded
HarvestType int8 // Type of harvest achieved
ItemsAwarded []*HarvestedItem // Items given to player
MessageText string // Message to display to player
SkillGained bool // Whether skill was gained
Error error // Any error that occurred
Success bool // Whether harvest succeeded
HarvestType int8 // Type of harvest achieved
ItemsAwarded []*HarvestedItem // Items given to player
MessageText string // Message to display to player
SkillGained bool // Whether skill was gained
Error error // Any error that occurred
}
// HarvestedItem represents an item awarded from harvesting
type HarvestedItem struct {
ItemID int32 // Database item ID
Quantity int16 // Number of items
IsRare bool // Whether this is a rare item
Name string // Item name for messages
ItemID int32 // Database item ID
Quantity int16 // Number of items
IsRare bool // Whether this is a rare item
Name string // Item name for messages
}
// HarvestContext contains all data needed for a harvest operation
type HarvestContext struct {
Player *Player // Player attempting harvest
GroundSpawn *GroundSpawn // The ground spawn being harvested
PlayerSkill *Skill // Player's harvesting skill
TotalSkill int16 // Total skill including bonuses
GroundSpawnEntries []*GroundSpawnEntry // Available harvest tables
GroundSpawnItems []*GroundSpawnEntryItem // Available harvest items
IsCollection bool // Whether this is collection harvesting
MaxSkillRequired int16 // Maximum skill required for any table
Player *Player // Player attempting harvest
GroundSpawn *GroundSpawn // The ground spawn being harvested
PlayerSkill *Skill // Player's harvesting skill
TotalSkill int16 // Total skill including bonuses
GroundSpawnEntries []*GroundSpawnEntry // Available harvest tables
GroundSpawnItems []*GroundSpawnEntryItem // Available harvest items
IsCollection bool // Whether this is collection harvesting
MaxSkillRequired int16 // Maximum skill required for any table
}
// SpawnLocation represents a spawn position with grid information
@ -93,45 +92,45 @@ type HarvestModifiers struct {
// GroundSpawnConfig contains configuration for ground spawn creation
type GroundSpawnConfig struct {
GroundSpawnID int32 // Database entry ID
CollectionSkill string // Required harvesting skill
NumberHarvests int8 // Harvests before depletion
AttemptsPerHarvest int8 // Attempts per harvest session
RandomizeHeading bool // Randomize spawn heading
RespawnTimer time.Duration // Time before respawn
Location SpawnLocation // Spawn position
Name string // Display name
Description string // Spawn description
GroundSpawnID int32 // Database entry ID
CollectionSkill string // Required harvesting skill
NumberHarvests int8 // Harvests before depletion
AttemptsPerHarvest int8 // Attempts per harvest session
RandomizeHeading bool // Randomize spawn heading
RespawnTimer time.Duration // Time before respawn
Location SpawnLocation // Spawn position
Name string // Display name
Description string // Spawn description
}
// Manager manages all ground spawn operations
type Manager struct {
groundSpawns map[int32]*GroundSpawn // Active ground spawns by ID
spawnsByZone map[int32][]*GroundSpawn // Ground spawns by zone ID
entriesByID map[int32][]*GroundSpawnEntry // Harvest entries by groundspawn ID
itemsByID map[int32][]*GroundSpawnEntryItem // Harvest items by groundspawn ID
respawnQueue map[int32]time.Time // Respawn timestamps
database Database // Database interface
logger Logger // Logging interface
mutex sync.RWMutex // Thread safety
groundSpawns map[int32]*GroundSpawn // Active ground spawns by ID
spawnsByZone map[int32][]*GroundSpawn // Ground spawns by zone ID
entriesByID map[int32][]*GroundSpawnEntry // Harvest entries by groundspawn ID
itemsByID map[int32][]*GroundSpawnEntryItem // Harvest items by groundspawn ID
respawnQueue map[int32]time.Time // Respawn timestamps
database Database // Database interface
logger Logger // Logging interface
mutex sync.RWMutex // Thread safety
// Statistics
totalHarvests int64 // Total harvest attempts
successfulHarvests int64 // Successful harvests
rareItemsHarvested int64 // Rare items harvested
skillUpsGenerated int64 // Skill increases given
harvestsBySkill map[string]int64 // Harvests by skill type
totalHarvests int64 // Total harvest attempts
successfulHarvests int64 // Successful harvests
rareItemsHarvested int64 // Rare items harvested
skillUpsGenerated int64 // Skill increases given
harvestsBySkill map[string]int64 // Harvests by skill type
}
// HarvestStatistics contains harvest system statistics
type HarvestStatistics struct {
TotalHarvests int64 `json:"total_harvests"`
SuccessfulHarvests int64 `json:"successful_harvests"`
RareItemsHarvested int64 `json:"rare_items_harvested"`
SkillUpsGenerated int64 `json:"skill_ups_generated"`
HarvestsBySkill map[string]int64 `json:"harvests_by_skill"`
ActiveGroundSpawns int `json:"active_ground_spawns"`
GroundSpawnsByZone map[int32]int `json:"ground_spawns_by_zone"`
}
TotalHarvests int64 `json:"total_harvests"`
SuccessfulHarvests int64 `json:"successful_harvests"`
RareItemsHarvested int64 `json:"rare_items_harvested"`
SkillUpsGenerated int64 `json:"skill_ups_generated"`
HarvestsBySkill map[string]int64 `json:"harvests_by_skill"`
ActiveGroundSpawns int `json:"active_ground_spawns"`
GroundSpawnsByZone map[int32]int `json:"ground_spawns_by_zone"`
}

View File

@ -2,106 +2,106 @@ package groups
// Group loot method constants
const (
LOOT_METHOD_LEADER_ONLY = 0
LOOT_METHOD_ROUND_ROBIN = 1
LOOT_METHOD_LEADER_ONLY = 0
LOOT_METHOD_ROUND_ROBIN = 1
LOOT_METHOD_NEED_BEFORE_GREED = 2
LOOT_METHOD_LOTTO = 3
LOOT_METHOD_LOTTO = 3
)
// Group loot rarity constants
const (
LOOT_RARITY_COMMON = 0
LOOT_RARITY_UNCOMMON = 1
LOOT_RARITY_RARE = 2
LOOT_RARITY_LEGENDARY = 3
LOOT_RARITY_FABLED = 4
LOOT_RARITY_COMMON = 0
LOOT_RARITY_UNCOMMON = 1
LOOT_RARITY_RARE = 2
LOOT_RARITY_LEGENDARY = 3
LOOT_RARITY_FABLED = 4
)
// Group auto-split constants
const (
AUTO_SPLIT_DISABLED = 0
AUTO_SPLIT_ENABLED = 1
AUTO_SPLIT_DISABLED = 0
AUTO_SPLIT_ENABLED = 1
)
// Group lock method constants
const (
LOCK_METHOD_OPEN = 0
LOCK_METHOD_INVITE_ONLY = 1
LOCK_METHOD_CLOSED = 2
LOCK_METHOD_OPEN = 0
LOCK_METHOD_INVITE_ONLY = 1
LOCK_METHOD_CLOSED = 2
)
// Group auto-lock constants
const (
AUTO_LOCK_DISABLED = 0
AUTO_LOCK_ENABLED = 1
AUTO_LOCK_DISABLED = 0
AUTO_LOCK_ENABLED = 1
)
// Group auto-loot method constants
const (
AUTO_LOOT_DISABLED = 0
AUTO_LOOT_ENABLED = 1
AUTO_LOOT_DISABLED = 0
AUTO_LOOT_ENABLED = 1
)
// Default yell constants
const (
DEFAULT_YELL_DISABLED = 0
DEFAULT_YELL_ENABLED = 1
DEFAULT_YELL_DISABLED = 0
DEFAULT_YELL_ENABLED = 1
)
// Group size limits
const (
MAX_GROUP_SIZE = 6
MAX_RAID_GROUPS = 4
MAX_RAID_SIZE = MAX_GROUP_SIZE * MAX_RAID_GROUPS
MAX_GROUP_SIZE = 6
MAX_RAID_GROUPS = 4
MAX_RAID_SIZE = MAX_GROUP_SIZE * MAX_RAID_GROUPS
)
// Group member position constants
const (
GROUP_POSITION_LEADER = 0
GROUP_POSITION_MEMBER_1 = 1
GROUP_POSITION_MEMBER_2 = 2
GROUP_POSITION_MEMBER_3 = 3
GROUP_POSITION_MEMBER_4 = 4
GROUP_POSITION_MEMBER_5 = 5
GROUP_POSITION_LEADER = 0
GROUP_POSITION_MEMBER_1 = 1
GROUP_POSITION_MEMBER_2 = 2
GROUP_POSITION_MEMBER_3 = 3
GROUP_POSITION_MEMBER_4 = 4
GROUP_POSITION_MEMBER_5 = 5
)
// Group invite error codes
const (
GROUP_INVITE_SUCCESS = 0
GROUP_INVITE_ALREADY_IN_GROUP = 1
GROUP_INVITE_ALREADY_HAS_INVITE = 2
GROUP_INVITE_GROUP_FULL = 3
GROUP_INVITE_DECLINED = 4
GROUP_INVITE_TARGET_NOT_FOUND = 5
GROUP_INVITE_SELF_INVITE = 6
GROUP_INVITE_PERMISSION_DENIED = 7
GROUP_INVITE_TARGET_BUSY = 8
GROUP_INVITE_SUCCESS = 0
GROUP_INVITE_ALREADY_IN_GROUP = 1
GROUP_INVITE_ALREADY_HAS_INVITE = 2
GROUP_INVITE_GROUP_FULL = 3
GROUP_INVITE_DECLINED = 4
GROUP_INVITE_TARGET_NOT_FOUND = 5
GROUP_INVITE_SELF_INVITE = 6
GROUP_INVITE_PERMISSION_DENIED = 7
GROUP_INVITE_TARGET_BUSY = 8
)
// Group message types
const (
GROUP_MESSAGE_TYPE_SYSTEM = 0
GROUP_MESSAGE_TYPE_COMBAT = 1
GROUP_MESSAGE_TYPE_LOOT = 2
GROUP_MESSAGE_TYPE_QUEST = 3
GROUP_MESSAGE_TYPE_CHAT = 4
GROUP_MESSAGE_TYPE_SYSTEM = 0
GROUP_MESSAGE_TYPE_COMBAT = 1
GROUP_MESSAGE_TYPE_LOOT = 2
GROUP_MESSAGE_TYPE_QUEST = 3
GROUP_MESSAGE_TYPE_CHAT = 4
)
// Channel constants for group communication
const (
CHANNEL_GROUP_SAY = 11
CHANNEL_GROUP_CHAT = 31
CHANNEL_RAID_SAY = 35
CHANNEL_GROUP_SAY = 11
CHANNEL_GROUP_CHAT = 31
CHANNEL_RAID_SAY = 35
)
// Group update flags
const (
GROUP_UPDATE_FLAG_MEMBER_LIST = 1 << 0
GROUP_UPDATE_FLAG_MEMBER_STATS = 1 << 1
GROUP_UPDATE_FLAG_MEMBER_ZONE = 1 << 2
GROUP_UPDATE_FLAG_LEADERSHIP = 1 << 3
GROUP_UPDATE_FLAG_OPTIONS = 1 << 4
GROUP_UPDATE_FLAG_RAID_INFO = 1 << 5
GROUP_UPDATE_FLAG_MEMBER_LIST = 1 << 0
GROUP_UPDATE_FLAG_MEMBER_STATS = 1 << 1
GROUP_UPDATE_FLAG_MEMBER_ZONE = 1 << 2
GROUP_UPDATE_FLAG_LEADERSHIP = 1 << 3
GROUP_UPDATE_FLAG_OPTIONS = 1 << 4
GROUP_UPDATE_FLAG_RAID_INFO = 1 << 5
)
// Raid group constants
@ -121,13 +121,13 @@ const (
// Group timing constants (in milliseconds)
const (
GROUP_UPDATE_INTERVAL = 1000 // 1 second
GROUP_INVITE_TIMEOUT = 30000 // 30 seconds
GROUP_BUFF_UPDATE_INTERVAL = 5000 // 5 seconds
GROUP_UPDATE_INTERVAL = 1000 // 1 second
GROUP_INVITE_TIMEOUT = 30000 // 30 seconds
GROUP_BUFF_UPDATE_INTERVAL = 5000 // 5 seconds
)
// Group validation constants
const (
MIN_GROUP_ID = 1
MAX_GROUP_ID = 2147483647 // Max int32
)
MIN_GROUP_ID = 1
MAX_GROUP_ID = 2147483647 // Max int32
)

View File

@ -2,8 +2,6 @@ package groups
import (
"fmt"
"sync"
"sync/atomic"
"time"
"eq2emu/internal/entity"
@ -45,7 +43,7 @@ func (g *Group) GetID() int32 {
func (g *Group) GetSize() int32 {
g.membersMutex.RLock()
defer g.membersMutex.RUnlock()
return int32(len(g.members))
}
@ -53,12 +51,12 @@ func (g *Group) GetSize() int32 {
func (g *Group) GetMembers() []*GroupMemberInfo {
g.membersMutex.RLock()
defer g.membersMutex.RUnlock()
members := make([]*GroupMemberInfo, len(g.members))
for i, member := range g.members {
members[i] = member.Copy()
}
return members
}
@ -92,13 +90,13 @@ func (g *Group) AddMember(member entity.Entity, isLeader bool) error {
// Create new group member info
gmi := &GroupMemberInfo{
GroupID: g.id,
Name: member.GetName(),
Leader: isLeader,
Member: member,
IsClient: member.IsPlayer(),
JoinTime: time.Now(),
LastUpdate: time.Now(),
GroupID: g.id,
Name: member.GetName(),
Leader: isLeader,
Member: member,
IsClient: member.IsPlayer(),
JoinTime: time.Now(),
LastUpdate: time.Now(),
}
// Update member stats from entity
@ -154,29 +152,29 @@ func (g *Group) AddMemberFromPeer(name string, isLeader, isClient bool, classID
// Create new group member info for peer member
gmi := &GroupMemberInfo{
GroupID: g.id,
Name: name,
Zone: zoneName,
HPCurrent: hpCur,
HPMax: hpMax,
PowerCurrent: powerCur,
PowerMax: powerMax,
LevelCurrent: levelCur,
LevelMax: levelMax,
RaceID: raceID,
ClassID: classID,
Leader: isLeader,
IsClient: isClient,
ZoneID: zoneID,
InstanceID: instanceID,
MentorTargetCharID: mentorTargetCharID,
ClientPeerAddress: peerAddress,
ClientPeerPort: peerPort,
IsRaidLooter: isRaidLooter,
Member: nil, // No local entity reference for peer members
Client: nil, // No local client reference for peer members
JoinTime: time.Now(),
LastUpdate: time.Now(),
GroupID: g.id,
Name: name,
Zone: zoneName,
HPCurrent: hpCur,
HPMax: hpMax,
PowerCurrent: powerCur,
PowerMax: powerMax,
LevelCurrent: levelCur,
LevelMax: levelMax,
RaceID: raceID,
ClassID: classID,
Leader: isLeader,
IsClient: isClient,
ZoneID: zoneID,
InstanceID: instanceID,
MentorTargetCharID: mentorTargetCharID,
ClientPeerAddress: peerAddress,
ClientPeerPort: peerPort,
IsRaidLooter: isRaidLooter,
Member: nil, // No local entity reference for peer members
Client: nil, // No local client reference for peer members
JoinTime: time.Now(),
LastUpdate: time.Now(),
}
// Add to members list
@ -338,7 +336,7 @@ func (g *Group) sendGroupUpdate(excludeClient interface{}, forceRaidUpdate bool)
// SimpleGroupMessage sends a simple message to all group members
func (g *Group) SimpleGroupMessage(message string) {
msg := NewGroupMessage(GROUP_MESSAGE_TYPE_SYSTEM, CHANNEL_GROUP_CHAT, message, "", 0)
select {
case g.messageQueue <- msg:
default:
@ -349,7 +347,7 @@ func (g *Group) SimpleGroupMessage(message string) {
// SendGroupMessage sends a formatted message to all group members
func (g *Group) SendGroupMessage(msgType int8, message string) {
msg := NewGroupMessage(msgType, CHANNEL_GROUP_CHAT, message, "", 0)
select {
case g.messageQueue <- msg:
default:
@ -364,7 +362,7 @@ func (g *Group) GroupChatMessage(from entity.Entity, language int32, message str
}
msg := NewGroupMessage(GROUP_MESSAGE_TYPE_CHAT, channel, message, from.GetName(), language)
select {
case g.messageQueue <- msg:
default:
@ -375,7 +373,7 @@ func (g *Group) GroupChatMessage(from entity.Entity, language int32, message str
// GroupChatMessageFromName sends a chat message from a named sender to the group
func (g *Group) GroupChatMessageFromName(fromName string, language int32, message string, channel int16) {
msg := NewGroupMessage(GROUP_MESSAGE_TYPE_CHAT, channel, message, fromName, language)
select {
case g.messageQueue <- msg:
default:
@ -393,7 +391,7 @@ func (g *Group) MakeLeader(newLeader entity.Entity) error {
defer g.membersMutex.Unlock()
var newLeaderGMI *GroupMemberInfo
// Find the new leader and update leadership
for _, gmi := range g.members {
if gmi.Member == newLeader {
@ -475,7 +473,7 @@ func (g *Group) GetGroupMemberByPosition(seeker entity.Entity, mappedPosition in
func (g *Group) GetGroupOptions() GroupOptions {
g.optionsMutex.RLock()
defer g.optionsMutex.RUnlock()
return g.options.Copy()
}
@ -498,7 +496,7 @@ func (g *Group) SetGroupOptions(options *GroupOptions) error {
// Send group update for options change
update := NewGroupUpdate(GROUP_UPDATE_FLAG_OPTIONS, g.id)
update.Options = options
select {
case g.updateQueue <- update:
default:
@ -512,7 +510,7 @@ func (g *Group) SetGroupOptions(options *GroupOptions) error {
func (g *Group) GetLastLooterIndex() int8 {
g.optionsMutex.RLock()
defer g.optionsMutex.RUnlock()
return g.options.LastLootedIndex
}
@ -521,7 +519,7 @@ func (g *Group) SetNextLooterIndex(newIndex int8) {
g.optionsMutex.Lock()
g.options.LastLootedIndex = newIndex
g.optionsMutex.Unlock()
g.updateLastActivity()
}
@ -531,11 +529,11 @@ func (g *Group) SetNextLooterIndex(newIndex int8) {
func (g *Group) GetRaidGroups() []int32 {
g.raidGroupsMutex.RLock()
defer g.raidGroupsMutex.RUnlock()
if g.raidGroups == nil {
return []int32{}
}
groups := make([]int32, len(g.raidGroups))
copy(groups, g.raidGroups)
return groups
@ -545,14 +543,14 @@ func (g *Group) GetRaidGroups() []int32 {
func (g *Group) ReplaceRaidGroups(groups []int32) {
g.raidGroupsMutex.Lock()
defer g.raidGroupsMutex.Unlock()
if groups == nil {
g.raidGroups = make([]int32, 0)
} else {
g.raidGroups = make([]int32, len(groups))
copy(g.raidGroups, groups)
}
g.updateLastActivity()
}
@ -560,13 +558,13 @@ func (g *Group) ReplaceRaidGroups(groups []int32) {
func (g *Group) IsInRaidGroup(groupID int32, isLeaderGroup bool) bool {
g.raidGroupsMutex.RLock()
defer g.raidGroupsMutex.RUnlock()
for _, id := range g.raidGroups {
if id == groupID {
return true
}
}
return false
}
@ -574,14 +572,14 @@ func (g *Group) IsInRaidGroup(groupID int32, isLeaderGroup bool) bool {
func (g *Group) AddGroupToRaid(groupID int32) {
g.raidGroupsMutex.Lock()
defer g.raidGroupsMutex.Unlock()
// Check if already in raid
for _, id := range g.raidGroups {
if id == groupID {
return
}
}
g.raidGroups = append(g.raidGroups, groupID)
g.updateLastActivity()
}
@ -590,7 +588,7 @@ func (g *Group) AddGroupToRaid(groupID int32) {
func (g *Group) RemoveGroupFromRaid(groupID int32) {
g.raidGroupsMutex.Lock()
defer g.raidGroupsMutex.Unlock()
for i, id := range g.raidGroups {
if id == groupID {
g.raidGroups = append(g.raidGroups[:i], g.raidGroups[i+1:]...)
@ -604,7 +602,7 @@ func (g *Group) RemoveGroupFromRaid(groupID int32) {
func (g *Group) IsGroupRaid() bool {
g.raidGroupsMutex.RLock()
defer g.raidGroupsMutex.RUnlock()
return len(g.raidGroups) > 0
}
@ -612,7 +610,7 @@ func (g *Group) IsGroupRaid() bool {
func (g *Group) ClearGroupRaid() {
g.raidGroupsMutex.Lock()
defer g.raidGroupsMutex.Unlock()
g.raidGroups = make([]int32, 0)
g.updateLastActivity()
}
@ -621,7 +619,7 @@ func (g *Group) ClearGroupRaid() {
func (g *Group) IsDisbanded() bool {
g.disbandMutex.RLock()
defer g.disbandMutex.RUnlock()
return g.disbanded
}
@ -643,7 +641,7 @@ func (g *Group) updateLastActivity() {
// processMessages processes messages and updates in the background
func (g *Group) processMessages() {
defer g.wg.Done()
for {
select {
case msg := <-g.messageQueue:
@ -698,4 +696,4 @@ func (g *Group) handleUpdate(update *GroupUpdate) {
// }
}
}
}
}

View File

@ -2,22 +2,23 @@ package groups
import (
"eq2emu/internal/entity"
"time"
)
// GroupAware interface for entities that can be part of groups
type GroupAware interface {
// GetGroupMemberInfo returns the group member info for this entity
GetGroupMemberInfo() *GroupMemberInfo
// SetGroupMemberInfo sets the group member info for this entity
SetGroupMemberInfo(info *GroupMemberInfo)
// GetGroupID returns the current group ID
GetGroupID() int32
// SetGroupID sets the current group ID
SetGroupID(groupID int32)
// IsInGroup returns true if the entity is in a group
IsInGroup() bool
}
@ -29,16 +30,16 @@ type GroupManagerInterface interface {
RemoveGroup(groupID int32) error
GetGroup(groupID int32) *Group
IsGroupIDValid(groupID int32) bool
// Member management
AddGroupMember(groupID int32, member entity.Entity, isLeader bool) error
AddGroupMemberFromPeer(groupID int32, info *GroupMemberInfo) error
RemoveGroupMember(groupID int32, member entity.Entity) error
RemoveGroupMemberByName(groupID int32, name string, isClient bool, charID int32) error
// Group updates
SendGroupUpdate(groupID int32, excludeClient interface{}, forceRaidUpdate bool)
// Invitations
Invite(leader entity.Entity, member entity.Entity) int8
AddInvite(leader entity.Entity, member entity.Entity) bool
@ -46,7 +47,7 @@ type GroupManagerInterface interface {
DeclineInvite(member entity.Entity)
ClearPendingInvite(member entity.Entity)
HasPendingInvite(member entity.Entity) string
// Group utilities
GetGroupSize(groupID int32) int32
IsInGroup(groupID int32, member entity.Entity) bool
@ -54,7 +55,7 @@ type GroupManagerInterface interface {
IsSpawnInGroup(groupID int32, name string) bool
GetGroupLeader(groupID int32) entity.Entity
MakeLeader(groupID int32, newLeader entity.Entity) bool
// Messaging
SimpleGroupMessage(groupID int32, message string)
SendGroupMessage(groupID int32, msgType int8, message string)
@ -62,18 +63,18 @@ type GroupManagerInterface interface {
GroupChatMessage(groupID int32, from entity.Entity, language int32, message string, channel int16)
GroupChatMessageFromName(groupID int32, fromName string, language int32, message string, channel int16)
SendGroupChatMessage(groupID int32, channel int16, message string)
// Raid functionality
ClearGroupRaid(groupID int32)
RemoveGroupFromRaid(groupID, targetGroupID int32)
IsInRaidGroup(groupID, targetGroupID int32, isLeaderGroup bool) bool
GetRaidGroups(groupID int32) []int32
ReplaceRaidGroups(groupID int32, newGroups []int32)
// Group options
GetDefaultGroupOptions(groupID int32) (GroupOptions, bool)
SetGroupOptions(groupID int32, options *GroupOptions) error
// Statistics
GetStats() GroupManagerStats
GetGroupCount() int32
@ -88,20 +89,20 @@ type GroupEventHandler interface {
OnGroupMemberJoined(group *Group, member entity.Entity) error
OnGroupMemberLeft(group *Group, member entity.Entity) error
OnGroupLeaderChanged(group *Group, oldLeader, newLeader entity.Entity) error
// Invitation events
OnGroupInviteSent(leader, member entity.Entity) error
OnGroupInviteAccepted(leader, member entity.Entity, groupID int32) error
OnGroupInviteDeclined(leader, member entity.Entity) error
OnGroupInviteExpired(leader, member entity.Entity) error
// Raid events
OnRaidFormed(groups []*Group) error
OnRaidDisbanded(groups []*Group) error
OnRaidInviteSent(leaderGroup *Group, targetGroup *Group) error
OnRaidInviteAccepted(leaderGroup *Group, targetGroup *Group) error
OnRaidInviteDeclined(leaderGroup *Group, targetGroup *Group) error
// Group activity events
OnGroupMessage(group *Group, from entity.Entity, message string, channel int16) error
OnGroupOptionsChanged(group *Group, oldOptions, newOptions *GroupOptions) error
@ -114,24 +115,24 @@ type GroupDatabase interface {
SaveGroup(group *Group) error
LoadGroup(groupID int32) (*Group, error)
DeleteGroup(groupID int32) error
// Group member persistence
SaveGroupMember(groupID int32, member *GroupMemberInfo) error
LoadGroupMembers(groupID int32) ([]*GroupMemberInfo, error)
DeleteGroupMember(groupID int32, memberName string) error
// Group options persistence
SaveGroupOptions(groupID int32, options *GroupOptions) error
LoadGroupOptions(groupID int32) (*GroupOptions, error)
// Raid persistence
SaveRaidGroups(groupID int32, raidGroups []int32) error
LoadRaidGroups(groupID int32) ([]int32, error)
// Statistics persistence
SaveGroupStats(stats *GroupManagerStats) error
LoadGroupStats() (*GroupManagerStats, error)
// Cleanup operations
CleanupExpiredGroups() error
CleanupOrphanedMembers() error
@ -143,24 +144,24 @@ type GroupPacketHandler interface {
SendGroupUpdate(members []*GroupMemberInfo, excludeClient interface{}) error
SendGroupMemberUpdate(member *GroupMemberInfo, excludeClient interface{}) error
SendGroupOptionsUpdate(groupID int32, options *GroupOptions, excludeClient interface{}) error
// Group invitation packets
SendGroupInvite(inviter, invitee entity.Entity) error
SendGroupInviteResponse(inviter, invitee entity.Entity, accepted bool) error
// Group messaging packets
SendGroupMessage(members []*GroupMemberInfo, message *GroupMessage) error
SendGroupChatMessage(members []*GroupMemberInfo, from string, message string, channel int16, language int32) error
// Raid packets
SendRaidUpdate(raidGroups []*Group, excludeClient interface{}) error
SendRaidInvite(leaderGroup, targetGroup *Group) error
SendRaidInviteResponse(leaderGroup, targetGroup *Group, accepted bool) error
// Group UI packets
SendGroupWindowUpdate(client interface{}, group *Group) error
SendRaidWindowUpdate(client interface{}, raidGroups []*Group) error
// Group member packets
SendGroupMemberStats(member *GroupMemberInfo, excludeClient interface{}) error
SendGroupMemberZoneChange(member *GroupMemberInfo, oldZoneID, newZoneID int32) error
@ -172,16 +173,16 @@ type GroupValidator interface {
ValidateGroupCreation(leader entity.Entity, options *GroupOptions) error
ValidateGroupJoin(group *Group, member entity.Entity) error
ValidateGroupLeave(group *Group, member entity.Entity) error
// Invitation validation
ValidateGroupInvite(leader, member entity.Entity) error
ValidateRaidInvite(leaderGroup, targetGroup *Group) error
// Group operation validation
ValidateLeadershipChange(group *Group, oldLeader, newLeader entity.Entity) error
ValidateGroupOptions(group *Group, options *GroupOptions) error
ValidateGroupMessage(group *Group, from entity.Entity, message string) error
// Raid validation
ValidateRaidFormation(groups []*Group) error
ValidateRaidOperation(raidGroups []*Group, operation string) error
@ -195,14 +196,14 @@ type GroupNotifier interface {
NotifyGroupMemberJoined(group *Group, member entity.Entity) error
NotifyGroupMemberLeft(group *Group, member entity.Entity, reason string) error
NotifyGroupLeaderChanged(group *Group, oldLeader, newLeader entity.Entity) error
// Invitation notifications
NotifyGroupInviteSent(leader, member entity.Entity) error
NotifyGroupInviteReceived(leader, member entity.Entity) error
NotifyGroupInviteAccepted(leader, member entity.Entity, groupID int32) error
NotifyGroupInviteDeclined(leader, member entity.Entity) error
NotifyGroupInviteExpired(leader, member entity.Entity) error
// Raid notifications
NotifyRaidFormed(groups []*Group) error
NotifyRaidDisbanded(groups []*Group, reason string) error
@ -210,7 +211,7 @@ type GroupNotifier interface {
NotifyRaidInviteReceived(leaderGroup, targetGroup *Group) error
NotifyRaidInviteAccepted(leaderGroup, targetGroup *Group) error
NotifyRaidInviteDeclined(leaderGroup, targetGroup *Group) error
// System notifications
NotifyGroupSystemMessage(group *Group, message string, msgType int8) error
NotifyGroupError(group *Group, error string, errorCode int8) error
@ -223,25 +224,25 @@ type GroupStatistics interface {
RecordGroupDisbanded(group *Group, duration int64)
RecordGroupMemberJoined(group *Group, member entity.Entity)
RecordGroupMemberLeft(group *Group, member entity.Entity, duration int64)
// Invitation statistics
RecordInviteSent(leader, member entity.Entity)
RecordInviteAccepted(leader, member entity.Entity, responseTime int64)
RecordInviteDeclined(leader, member entity.Entity, responseTime int64)
RecordInviteExpired(leader, member entity.Entity)
// Raid statistics
RecordRaidFormed(groups []*Group)
RecordRaidDisbanded(groups []*Group, duration int64)
// Activity statistics
RecordGroupMessage(group *Group, from entity.Entity, messageType int8)
RecordGroupActivity(group *Group, activityType string)
// Performance statistics
RecordGroupProcessingTime(operation string, duration int64)
RecordGroupMemoryUsage(groups int32, members int32)
// Statistics retrieval
GetGroupStatistics(groupID int32) map[string]interface{}
GetOverallStatistics() map[string]interface{}
@ -309,7 +310,7 @@ func (ga *GroupAdapter) IsMember(entity entity.Entity) bool {
if entity == nil {
return false
}
members := ga.group.GetMembers()
for _, member := range members {
if member.Member == entity {
@ -346,7 +347,7 @@ func (ga *GroupAdapter) GetMemberByEntity(entity entity.Entity) *GroupMemberInfo
if entity == nil {
return nil
}
members := ga.group.GetMembers()
for _, member := range members {
if member.Member == entity {
@ -361,7 +362,7 @@ func (ga *GroupAdapter) IsLeader(entity entity.Entity) bool {
if entity == nil {
return false
}
members := ga.group.GetMembers()
for _, member := range members {
if member.Member == entity && member.Leader {
@ -498,4 +499,4 @@ func (ega *EntityGroupAdapter) IsDead() bool {
// GetDistance returns distance to another entity
func (ega *EntityGroupAdapter) GetDistance(other entity.Entity) float32 {
return ega.entity.GetDistance(&other.Spawn)
}
}

View File

@ -2,8 +2,6 @@ package groups
import (
"fmt"
"sync"
"sync/atomic"
"time"
"eq2emu/internal/entity"
@ -78,7 +76,7 @@ func (gm *GroupManager) NewGroup(leader entity.Entity, options *GroupOptions, ov
// Create new group
group := NewGroup(groupID, options)
// Add leader to the group
if err := group.AddMember(leader, true); err != nil {
group.Disband()
@ -126,7 +124,7 @@ func (gm *GroupManager) RemoveGroup(groupID int32) error {
func (gm *GroupManager) GetGroup(groupID int32) *Group {
gm.groupsMutex.RLock()
defer gm.groupsMutex.RUnlock()
return gm.groups[groupID]
}
@ -134,7 +132,7 @@ func (gm *GroupManager) GetGroup(groupID int32) *Group {
func (gm *GroupManager) IsGroupIDValid(groupID int32) bool {
gm.groupsMutex.RLock()
defer gm.groupsMutex.RUnlock()
_, exists := gm.groups[groupID]
return exists
}
@ -289,7 +287,7 @@ func (gm *GroupManager) AcceptInvite(member entity.Entity, groupOverrideID *int3
}
inviteKey := member.GetName()
gm.invitesMutex.Lock()
invite, exists := gm.pendingInvites[inviteKey]
if !exists {
@ -369,7 +367,7 @@ func (gm *GroupManager) DeclineInvite(member entity.Entity) {
}
inviteKey := member.GetName()
gm.invitesMutex.Lock()
invite, exists := gm.pendingInvites[inviteKey]
if exists {
@ -396,7 +394,7 @@ func (gm *GroupManager) ClearPendingInvite(member entity.Entity) {
}
inviteKey := member.GetName()
gm.invitesMutex.Lock()
delete(gm.pendingInvites, inviteKey)
gm.invitesMutex.Unlock()
@ -416,13 +414,13 @@ func (gm *GroupManager) HasPendingInvite(member entity.Entity) string {
func (gm *GroupManager) hasPendingInvite(inviteKey string) string {
gm.invitesMutex.RLock()
defer gm.invitesMutex.RUnlock()
if invite, exists := gm.pendingInvites[inviteKey]; exists {
if !invite.IsExpired() {
return invite.InviterName
}
}
return ""
}
@ -633,15 +631,15 @@ func (gm *GroupManager) SetGroupOptions(groupID int32, options *GroupOptions) er
func (gm *GroupManager) generateNextGroupID() int32 {
gm.nextGroupIDMutex.Lock()
defer gm.nextGroupIDMutex.Unlock()
id := gm.nextGroupID
gm.nextGroupID++
// Handle overflow
if gm.nextGroupID <= 0 {
gm.nextGroupID = 1
}
return id
}
@ -649,7 +647,7 @@ func (gm *GroupManager) generateNextGroupID() int32 {
func (gm *GroupManager) GetGroupCount() int32 {
gm.groupsMutex.RLock()
defer gm.groupsMutex.RUnlock()
return int32(len(gm.groups))
}
@ -657,12 +655,12 @@ func (gm *GroupManager) GetGroupCount() int32 {
func (gm *GroupManager) GetAllGroups() []*Group {
gm.groupsMutex.RLock()
defer gm.groupsMutex.RUnlock()
groups := make([]*Group, 0, len(gm.groups))
for _, group := range gm.groups {
groups = append(groups, group)
}
return groups
}
@ -671,10 +669,10 @@ func (gm *GroupManager) GetAllGroups() []*Group {
// updateGroupsLoop periodically updates all groups
func (gm *GroupManager) updateGroupsLoop() {
defer gm.wg.Done()
ticker := time.NewTicker(gm.config.UpdateInterval)
defer ticker.Stop()
for {
select {
case <-ticker.C:
@ -688,10 +686,10 @@ func (gm *GroupManager) updateGroupsLoop() {
// updateBuffsLoop periodically updates group buffs
func (gm *GroupManager) updateBuffsLoop() {
defer gm.wg.Done()
ticker := time.NewTicker(gm.config.BuffUpdateInterval)
defer ticker.Stop()
for {
select {
case <-ticker.C:
@ -705,10 +703,10 @@ func (gm *GroupManager) updateBuffsLoop() {
// cleanupExpiredInvitesLoop periodically cleans up expired invites
func (gm *GroupManager) cleanupExpiredInvitesLoop() {
defer gm.wg.Done()
ticker := time.NewTicker(30 * time.Second) // Check every 30 seconds
defer ticker.Stop()
for {
select {
case <-ticker.C:
@ -722,10 +720,10 @@ func (gm *GroupManager) cleanupExpiredInvitesLoop() {
// updateStatsLoop periodically updates statistics
func (gm *GroupManager) updateStatsLoop() {
defer gm.wg.Done()
ticker := time.NewTicker(1 * time.Minute) // Update stats every minute
defer ticker.Stop()
for {
select {
case <-ticker.C:
@ -739,7 +737,7 @@ func (gm *GroupManager) updateStatsLoop() {
// processGroupUpdates processes periodic group updates
func (gm *GroupManager) processGroupUpdates() {
groups := gm.GetAllGroups()
for _, group := range groups {
if !group.IsDisbanded() {
// Update member information
@ -763,10 +761,10 @@ func (gm *GroupManager) updateGroupBuffs() {
func (gm *GroupManager) cleanupExpiredInvites() {
gm.invitesMutex.Lock()
defer gm.invitesMutex.Unlock()
now := time.Now()
expiredCount := 0
// Clean up regular invites
for key, invite := range gm.pendingInvites {
if now.After(invite.ExpiresTime) {
@ -774,7 +772,7 @@ func (gm *GroupManager) cleanupExpiredInvites() {
expiredCount++
}
}
// Clean up raid invites
for key, invite := range gm.raidPendingInvites {
if now.After(invite.ExpiresTime) {
@ -782,7 +780,7 @@ func (gm *GroupManager) cleanupExpiredInvites() {
expiredCount++
}
}
// Update statistics
if expiredCount > 0 {
gm.statsMutex.Lock()
@ -796,16 +794,16 @@ func (gm *GroupManager) updateStatistics() {
if !gm.config.EnableStatistics {
return
}
gm.statsMutex.Lock()
defer gm.statsMutex.Unlock()
gm.groupsMutex.RLock()
activeGroups := int64(len(gm.groups))
var totalMembers int64
var raidCount int64
for _, group := range gm.groups {
totalMembers += int64(group.GetSize())
if group.IsGroupRaid() {
@ -813,16 +811,16 @@ func (gm *GroupManager) updateStatistics() {
}
}
gm.groupsMutex.RUnlock()
gm.stats.ActiveGroups = activeGroups
gm.stats.ActiveRaids = raidCount
if activeGroups > 0 {
gm.stats.AverageGroupSize = float64(totalMembers) / float64(activeGroups)
} else {
gm.stats.AverageGroupSize = 0
}
gm.stats.LastStatsUpdate = time.Now()
}
@ -833,10 +831,10 @@ func (gm *GroupManager) updateStatsForNewGroup() {
if !gm.config.EnableStatistics {
return
}
gm.statsMutex.Lock()
defer gm.statsMutex.Unlock()
gm.stats.TotalGroups++
}
@ -850,10 +848,10 @@ func (gm *GroupManager) updateStatsForInvite() {
if !gm.config.EnableStatistics {
return
}
gm.statsMutex.Lock()
defer gm.statsMutex.Unlock()
gm.stats.TotalInvites++
}
@ -862,10 +860,10 @@ func (gm *GroupManager) updateStatsForAcceptedInvite() {
if !gm.config.EnableStatistics {
return
}
gm.statsMutex.Lock()
defer gm.statsMutex.Unlock()
gm.stats.AcceptedInvites++
}
@ -874,10 +872,10 @@ func (gm *GroupManager) updateStatsForDeclinedInvite() {
if !gm.config.EnableStatistics {
return
}
gm.statsMutex.Lock()
defer gm.statsMutex.Unlock()
gm.stats.DeclinedInvites++
}
@ -886,10 +884,10 @@ func (gm *GroupManager) updateStatsForExpiredInvite() {
if !gm.config.EnableStatistics {
return
}
gm.statsMutex.Lock()
defer gm.statsMutex.Unlock()
gm.stats.ExpiredInvites++
}
@ -897,7 +895,7 @@ func (gm *GroupManager) updateStatsForExpiredInvite() {
func (gm *GroupManager) GetStats() GroupManagerStats {
gm.statsMutex.RLock()
defer gm.statsMutex.RUnlock()
return gm.stats
}
@ -907,7 +905,7 @@ func (gm *GroupManager) GetStats() GroupManagerStats {
func (gm *GroupManager) AddEventHandler(handler GroupEventHandler) {
gm.eventHandlersMutex.Lock()
defer gm.eventHandlersMutex.Unlock()
gm.eventHandlers = append(gm.eventHandlers, handler)
}
@ -939,7 +937,7 @@ func (gm *GroupManager) SetNotifier(notifier GroupNotifier) {
func (gm *GroupManager) fireGroupCreatedEvent(group *Group, leader entity.Entity) {
gm.eventHandlersMutex.RLock()
defer gm.eventHandlersMutex.RUnlock()
for _, handler := range gm.eventHandlers {
go handler.OnGroupCreated(group, leader)
}
@ -949,7 +947,7 @@ func (gm *GroupManager) fireGroupCreatedEvent(group *Group, leader entity.Entity
func (gm *GroupManager) fireGroupDisbandedEvent(group *Group) {
gm.eventHandlersMutex.RLock()
defer gm.eventHandlersMutex.RUnlock()
for _, handler := range gm.eventHandlers {
go handler.OnGroupDisbanded(group)
}
@ -959,7 +957,7 @@ func (gm *GroupManager) fireGroupDisbandedEvent(group *Group) {
func (gm *GroupManager) fireGroupInviteSentEvent(leader, member entity.Entity) {
gm.eventHandlersMutex.RLock()
defer gm.eventHandlersMutex.RUnlock()
for _, handler := range gm.eventHandlers {
go handler.OnGroupInviteSent(leader, member)
}
@ -969,7 +967,7 @@ func (gm *GroupManager) fireGroupInviteSentEvent(leader, member entity.Entity) {
func (gm *GroupManager) fireGroupInviteAcceptedEvent(leader, member entity.Entity, groupID int32) {
gm.eventHandlersMutex.RLock()
defer gm.eventHandlersMutex.RUnlock()
for _, handler := range gm.eventHandlers {
go handler.OnGroupInviteAccepted(leader, member, groupID)
}
@ -979,8 +977,8 @@ func (gm *GroupManager) fireGroupInviteAcceptedEvent(leader, member entity.Entit
func (gm *GroupManager) fireGroupInviteDeclinedEvent(leader, member entity.Entity) {
gm.eventHandlersMutex.RLock()
defer gm.eventHandlersMutex.RUnlock()
for _, handler := range gm.eventHandlers {
go handler.OnGroupInviteDeclined(leader, member)
}
}
}

View File

@ -20,23 +20,23 @@ type Service struct {
type ServiceConfig struct {
// Group manager configuration
ManagerConfig GroupManagerConfig `json:"manager_config"`
// Service-specific settings
AutoCreateGroups bool `json:"auto_create_groups"`
AllowCrossZoneGroups bool `json:"allow_cross_zone_groups"`
AllowBotMembers bool `json:"allow_bot_members"`
AllowNPCMembers bool `json:"allow_npc_members"`
MaxInviteDistance float32 `json:"max_invite_distance"`
GroupLevelRange int8 `json:"group_level_range"`
EnableGroupPvP bool `json:"enable_group_pvp"`
EnableGroupBuffs bool `json:"enable_group_buffs"`
LogLevel string `json:"log_level"`
AutoCreateGroups bool `json:"auto_create_groups"`
AllowCrossZoneGroups bool `json:"allow_cross_zone_groups"`
AllowBotMembers bool `json:"allow_bot_members"`
AllowNPCMembers bool `json:"allow_npc_members"`
MaxInviteDistance float32 `json:"max_invite_distance"`
GroupLevelRange int8 `json:"group_level_range"`
EnableGroupPvP bool `json:"enable_group_pvp"`
EnableGroupBuffs bool `json:"enable_group_buffs"`
LogLevel string `json:"log_level"`
// Integration settings
DatabaseEnabled bool `json:"database_enabled"`
EventsEnabled bool `json:"events_enabled"`
StatisticsEnabled bool `json:"statistics_enabled"`
ValidationEnabled bool `json:"validation_enabled"`
DatabaseEnabled bool `json:"database_enabled"`
EventsEnabled bool `json:"events_enabled"`
StatisticsEnabled bool `json:"statistics_enabled"`
ValidationEnabled bool `json:"validation_enabled"`
}
// DefaultServiceConfig returns default service configuration
@ -83,15 +83,15 @@ func NewService(config ServiceConfig) *Service {
func (s *Service) Start() error {
s.startMutex.Lock()
defer s.startMutex.Unlock()
if s.started {
return fmt.Errorf("service already started")
}
if err := s.manager.Start(); err != nil {
return fmt.Errorf("failed to start group manager: %v", err)
}
s.started = true
return nil
}
@ -100,15 +100,15 @@ func (s *Service) Start() error {
func (s *Service) Stop() error {
s.startMutex.Lock()
defer s.startMutex.Unlock()
if !s.started {
return nil
}
if err := s.manager.Stop(); err != nil {
return fmt.Errorf("failed to stop group manager: %v", err)
}
s.started = false
return nil
}
@ -132,20 +132,20 @@ func (s *Service) CreateGroup(leader entity.Entity, options *GroupOptions) (int3
if leader == nil {
return 0, fmt.Errorf("leader cannot be nil")
}
// Validate leader can create group
if s.config.ValidationEnabled {
if err := s.validateGroupCreation(leader, options); err != nil {
return 0, fmt.Errorf("group creation validation failed: %v", err)
}
}
// Use default options if none provided
if options == nil {
defaultOpts := DefaultGroupOptions()
options = &defaultOpts
}
return s.manager.NewGroup(leader, options, 0)
}
@ -154,17 +154,17 @@ func (s *Service) InviteToGroup(leader entity.Entity, member entity.Entity) erro
if leader == nil || member == nil {
return fmt.Errorf("leader and member cannot be nil")
}
// Validate the invitation
if s.config.ValidationEnabled {
if err := s.validateGroupInvitation(leader, member); err != nil {
return fmt.Errorf("invitation validation failed: %v", err)
}
}
// Send the invitation
result := s.manager.Invite(leader, member)
switch result {
case GROUP_INVITE_SUCCESS:
return nil
@ -194,9 +194,9 @@ func (s *Service) AcceptGroupInvite(member entity.Entity) error {
if member == nil {
return fmt.Errorf("member cannot be nil")
}
result := s.manager.AcceptInvite(member, nil, true)
switch result {
case GROUP_INVITE_SUCCESS:
return nil
@ -223,15 +223,15 @@ func (s *Service) LeaveGroup(member entity.Entity) error {
if member == nil {
return fmt.Errorf("member cannot be nil")
}
// TODO: Get member's current group ID
// groupID := member.GetGroupID()
groupID := int32(0) // Placeholder
if groupID == 0 {
return fmt.Errorf("member is not in a group")
}
return s.manager.RemoveGroupMember(groupID, member)
}
@ -245,15 +245,15 @@ func (s *Service) TransferLeadership(groupID int32, newLeader entity.Entity) err
if newLeader == nil {
return fmt.Errorf("new leader cannot be nil")
}
if !s.manager.IsGroupIDValid(groupID) {
return fmt.Errorf("invalid group ID")
}
if !s.manager.MakeLeader(groupID, newLeader) {
return fmt.Errorf("failed to transfer leadership")
}
return nil
}
@ -265,11 +265,11 @@ func (s *Service) GetGroupInfo(groupID int32) (*GroupInfo, error) {
if group == nil {
return nil, fmt.Errorf("group not found")
}
members := group.GetMembers()
options := group.GetGroupOptions()
raidGroups := group.GetRaidGroups()
info := &GroupInfo{
GroupID: group.GetID(),
Size: int(group.GetSize()),
@ -282,20 +282,20 @@ func (s *Service) GetGroupInfo(groupID int32) (*GroupInfo, error) {
LastActivity: group.GetLastActivity(),
IsDisbanded: group.IsDisbanded(),
}
return info, nil
}
// GetMemberGroups returns all groups that contain any of the specified members
func (s *Service) GetMemberGroups(members []entity.Entity) []*GroupInfo {
var groups []*GroupInfo
allGroups := s.manager.GetAllGroups()
for _, group := range allGroups {
if group.IsDisbanded() {
continue
}
groupMembers := group.GetMembers()
for _, member := range members {
for _, gmi := range groupMembers {
@ -308,37 +308,37 @@ func (s *Service) GetMemberGroups(members []entity.Entity) []*GroupInfo {
}
}
}
return groups
}
// GetGroupsByZone returns all groups with members in the specified zone
func (s *Service) GetGroupsByZone(zoneID int32) []*GroupInfo {
var groups []*GroupInfo
allGroups := s.manager.GetAllGroups()
for _, group := range allGroups {
if group.IsDisbanded() {
continue
}
members := group.GetMembers()
hasZoneMember := false
for _, member := range members {
if member.ZoneID == zoneID {
hasZoneMember = true
break
}
}
if hasZoneMember {
if info, err := s.GetGroupInfo(group.GetID()); err == nil {
groups = append(groups, info)
}
}
}
return groups
}
@ -349,26 +349,26 @@ func (s *Service) FormRaid(leaderGroupID int32, targetGroupIDs []int32) error {
if !s.config.ManagerConfig.EnableRaids {
return fmt.Errorf("raids are disabled")
}
leaderGroup := s.manager.GetGroup(leaderGroupID)
if leaderGroup == nil {
return fmt.Errorf("leader group not found")
}
// Validate all target groups exist
for _, groupID := range targetGroupIDs {
if !s.manager.IsGroupIDValid(groupID) {
return fmt.Errorf("invalid target group ID: %d", groupID)
}
}
// Add all groups to the raid
allRaidGroups := append([]int32{leaderGroupID}, targetGroupIDs...)
for _, groupID := range allRaidGroups {
s.manager.ReplaceRaidGroups(groupID, allRaidGroups)
}
return nil
}
@ -378,17 +378,17 @@ func (s *Service) DisbandRaid(groupID int32) error {
if group == nil {
return fmt.Errorf("group not found")
}
raidGroups := group.GetRaidGroups()
if len(raidGroups) == 0 {
return fmt.Errorf("group is not in a raid")
}
// Clear raid associations for all groups
for _, raidGroupID := range raidGroups {
s.manager.ClearGroupRaid(raidGroupID)
}
return nil
}
@ -437,7 +437,7 @@ func (s *Service) AddEventHandler(handler GroupEventHandler) {
// GetServiceStats returns service statistics
func (s *Service) GetServiceStats() *ServiceStats {
managerStats := s.manager.GetStats()
return &ServiceStats{
ManagerStats: managerStats,
ServiceStartTime: time.Now(), // TODO: Track actual start time
@ -455,12 +455,12 @@ func (s *Service) validateGroupCreation(leader entity.Entity, options *GroupOpti
// if leader.GetGroupMemberInfo() != nil {
// return fmt.Errorf("leader is already in a group")
// }
// Validate options
if options != nil && !options.IsValid() {
return fmt.Errorf("invalid group options")
}
return nil
}
@ -473,7 +473,7 @@ func (s *Service) validateGroupInvitation(leader entity.Entity, member entity.En
return fmt.Errorf("member is too far away (%.1f > %.1f)", distance, s.config.MaxInviteDistance)
}
}
// Check level range if enabled
if s.config.GroupLevelRange > 0 {
leaderLevel := leader.GetLevel()
@ -482,28 +482,28 @@ func (s *Service) validateGroupInvitation(leader entity.Entity, member entity.En
if levelDiff < 0 {
levelDiff = -levelDiff
}
if levelDiff > s.config.GroupLevelRange {
return fmt.Errorf("level difference too large (%d > %d)", levelDiff, s.config.GroupLevelRange)
}
}
// Check if member type is allowed
if member.IsBot() && !s.config.AllowBotMembers {
return fmt.Errorf("bot members are not allowed")
}
if member.IsNPC() && !s.config.AllowNPCMembers {
return fmt.Errorf("NPC members are not allowed")
}
// Check zone restrictions
if !s.config.AllowCrossZoneGroups {
if leader.GetZone() != member.GetZone() {
return fmt.Errorf("cross-zone groups are not allowed")
}
}
return nil
}
@ -527,4 +527,4 @@ type ServiceStats struct {
ServiceStartTime time.Time `json:"service_start_time"`
IsStarted bool `json:"is_started"`
Config ServiceConfig `json:"config"`
}
}

View File

@ -9,208 +9,208 @@ import (
// GroupOptions holds group configuration settings
type GroupOptions struct {
LootMethod int8 `json:"loot_method"`
LootItemsRarity int8 `json:"loot_items_rarity"`
AutoSplit int8 `json:"auto_split"`
DefaultYell int8 `json:"default_yell"`
GroupLockMethod int8 `json:"group_lock_method"`
GroupAutolock int8 `json:"group_autolock"`
SoloAutolock int8 `json:"solo_autolock"`
AutoLootMethod int8 `json:"auto_loot_method"`
LastLootedIndex int8 `json:"last_looted_index"`
LootMethod int8 `json:"loot_method"`
LootItemsRarity int8 `json:"loot_items_rarity"`
AutoSplit int8 `json:"auto_split"`
DefaultYell int8 `json:"default_yell"`
GroupLockMethod int8 `json:"group_lock_method"`
GroupAutolock int8 `json:"group_autolock"`
SoloAutolock int8 `json:"solo_autolock"`
AutoLootMethod int8 `json:"auto_loot_method"`
LastLootedIndex int8 `json:"last_looted_index"`
}
// GroupMemberInfo contains all information about a group member
type GroupMemberInfo struct {
// Group and member identification
GroupID int32 `json:"group_id"`
Name string `json:"name"`
Zone string `json:"zone"`
GroupID int32 `json:"group_id"`
Name string `json:"name"`
Zone string `json:"zone"`
// Health and power stats
HPCurrent int32 `json:"hp_current"`
HPMax int32 `json:"hp_max"`
PowerCurrent int32 `json:"power_current"`
PowerMax int32 `json:"power_max"`
HPCurrent int32 `json:"hp_current"`
HPMax int32 `json:"hp_max"`
PowerCurrent int32 `json:"power_current"`
PowerMax int32 `json:"power_max"`
// Level and character info
LevelCurrent int16 `json:"level_current"`
LevelMax int16 `json:"level_max"`
RaceID int8 `json:"race_id"`
ClassID int8 `json:"class_id"`
LevelCurrent int16 `json:"level_current"`
LevelMax int16 `json:"level_max"`
RaceID int8 `json:"race_id"`
ClassID int8 `json:"class_id"`
// Group status
Leader bool `json:"leader"`
IsClient bool `json:"is_client"`
IsRaidLooter bool `json:"is_raid_looter"`
Leader bool `json:"leader"`
IsClient bool `json:"is_client"`
IsRaidLooter bool `json:"is_raid_looter"`
// Zone and instance info
ZoneID int32 `json:"zone_id"`
InstanceID int32 `json:"instance_id"`
ZoneID int32 `json:"zone_id"`
InstanceID int32 `json:"instance_id"`
// Mentoring
MentorTargetCharID int32 `json:"mentor_target_char_id"`
MentorTargetCharID int32 `json:"mentor_target_char_id"`
// Network info for cross-server groups
ClientPeerAddress string `json:"client_peer_address"`
ClientPeerPort int16 `json:"client_peer_port"`
ClientPeerAddress string `json:"client_peer_address"`
ClientPeerPort int16 `json:"client_peer_port"`
// Entity reference (local members only)
Member entity.Entity `json:"-"`
Member entity.Entity `json:"-"`
// Client reference (players only) - interface to avoid circular deps
Client interface{} `json:"-"`
Client interface{} `json:"-"`
// Timestamps
JoinTime time.Time `json:"join_time"`
LastUpdate time.Time `json:"last_update"`
JoinTime time.Time `json:"join_time"`
LastUpdate time.Time `json:"last_update"`
}
// Group represents a player group
type Group struct {
// Group identification
id int32
id int32
// Group options and configuration
options GroupOptions
optionsMutex sync.RWMutex
options GroupOptions
optionsMutex sync.RWMutex
// Group members
members []*GroupMemberInfo
membersMutex sync.RWMutex
members []*GroupMemberInfo
membersMutex sync.RWMutex
// Raid functionality
raidGroups []int32
raidGroupsMutex sync.RWMutex
raidGroups []int32
raidGroupsMutex sync.RWMutex
// Group statistics
createdTime time.Time
lastActivity time.Time
createdTime time.Time
lastActivity time.Time
// Group status
disbanded bool
disbandMutex sync.RWMutex
disbanded bool
disbandMutex sync.RWMutex
// Communication channels
messageQueue chan *GroupMessage
updateQueue chan *GroupUpdate
messageQueue chan *GroupMessage
updateQueue chan *GroupUpdate
// Background processing
stopChan chan struct{}
wg sync.WaitGroup
stopChan chan struct{}
wg sync.WaitGroup
}
// GroupMessage represents a message sent to the group
type GroupMessage struct {
Type int8 `json:"type"`
Channel int16 `json:"channel"`
Message string `json:"message"`
FromName string `json:"from_name"`
Language int32 `json:"language"`
Timestamp time.Time `json:"timestamp"`
ExcludeClient interface{} `json:"-"`
Type int8 `json:"type"`
Channel int16 `json:"channel"`
Message string `json:"message"`
FromName string `json:"from_name"`
Language int32 `json:"language"`
Timestamp time.Time `json:"timestamp"`
ExcludeClient interface{} `json:"-"`
}
// GroupUpdate represents a group update event
type GroupUpdate struct {
Type int8 `json:"type"`
GroupID int32 `json:"group_id"`
MemberInfo *GroupMemberInfo `json:"member_info,omitempty"`
Options *GroupOptions `json:"options,omitempty"`
RaidGroups []int32 `json:"raid_groups,omitempty"`
ForceRaidUpdate bool `json:"force_raid_update"`
ExcludeClient interface{} `json:"-"`
Timestamp time.Time `json:"timestamp"`
Type int8 `json:"type"`
GroupID int32 `json:"group_id"`
MemberInfo *GroupMemberInfo `json:"member_info,omitempty"`
Options *GroupOptions `json:"options,omitempty"`
RaidGroups []int32 `json:"raid_groups,omitempty"`
ForceRaidUpdate bool `json:"force_raid_update"`
ExcludeClient interface{} `json:"-"`
Timestamp time.Time `json:"timestamp"`
}
// GroupInvite represents a pending group invitation
type GroupInvite struct {
InviterName string `json:"inviter_name"`
InviteeName string `json:"invitee_name"`
GroupID int32 `json:"group_id"`
IsRaidInvite bool `json:"is_raid_invite"`
CreatedTime time.Time `json:"created_time"`
ExpiresTime time.Time `json:"expires_time"`
InviterName string `json:"inviter_name"`
InviteeName string `json:"invitee_name"`
GroupID int32 `json:"group_id"`
IsRaidInvite bool `json:"is_raid_invite"`
CreatedTime time.Time `json:"created_time"`
ExpiresTime time.Time `json:"expires_time"`
}
// GroupManager manages all player groups
type GroupManager struct {
// Group storage
groups map[int32]*Group
groupsMutex sync.RWMutex
groups map[int32]*Group
groupsMutex sync.RWMutex
// Group ID generation
nextGroupID int32
nextGroupIDMutex sync.Mutex
nextGroupID int32
nextGroupIDMutex sync.Mutex
// Pending invitations
pendingInvites map[string]*GroupInvite
raidPendingInvites map[string]*GroupInvite
invitesMutex sync.RWMutex
pendingInvites map[string]*GroupInvite
raidPendingInvites map[string]*GroupInvite
invitesMutex sync.RWMutex
// Event handlers
eventHandlers []GroupEventHandler
eventHandlersMutex sync.RWMutex
eventHandlers []GroupEventHandler
eventHandlersMutex sync.RWMutex
// Configuration
config GroupManagerConfig
config GroupManagerConfig
// Statistics
stats GroupManagerStats
statsMutex sync.RWMutex
stats GroupManagerStats
statsMutex sync.RWMutex
// Background processing
stopChan chan struct{}
wg sync.WaitGroup
stopChan chan struct{}
wg sync.WaitGroup
// Integration interfaces
database GroupDatabase
packetHandler GroupPacketHandler
validator GroupValidator
notifier GroupNotifier
database GroupDatabase
packetHandler GroupPacketHandler
validator GroupValidator
notifier GroupNotifier
}
// GroupManagerConfig holds configuration for the group manager
type GroupManagerConfig struct {
MaxGroups int32 `json:"max_groups"`
MaxRaidGroups int32 `json:"max_raid_groups"`
InviteTimeout time.Duration `json:"invite_timeout"`
UpdateInterval time.Duration `json:"update_interval"`
BuffUpdateInterval time.Duration `json:"buff_update_interval"`
EnableCrossServer bool `json:"enable_cross_server"`
EnableRaids bool `json:"enable_raids"`
EnableQuestSharing bool `json:"enable_quest_sharing"`
EnableAutoInvite bool `json:"enable_auto_invite"`
EnableStatistics bool `json:"enable_statistics"`
MaxGroups int32 `json:"max_groups"`
MaxRaidGroups int32 `json:"max_raid_groups"`
InviteTimeout time.Duration `json:"invite_timeout"`
UpdateInterval time.Duration `json:"update_interval"`
BuffUpdateInterval time.Duration `json:"buff_update_interval"`
EnableCrossServer bool `json:"enable_cross_server"`
EnableRaids bool `json:"enable_raids"`
EnableQuestSharing bool `json:"enable_quest_sharing"`
EnableAutoInvite bool `json:"enable_auto_invite"`
EnableStatistics bool `json:"enable_statistics"`
}
// GroupManagerStats holds statistics about group management
type GroupManagerStats struct {
TotalGroups int64 `json:"total_groups"`
ActiveGroups int64 `json:"active_groups"`
TotalRaids int64 `json:"total_raids"`
ActiveRaids int64 `json:"active_raids"`
TotalInvites int64 `json:"total_invites"`
AcceptedInvites int64 `json:"accepted_invites"`
DeclinedInvites int64 `json:"declined_invites"`
ExpiredInvites int64 `json:"expired_invites"`
AverageGroupSize float64 `json:"average_group_size"`
AverageGroupDuration time.Duration `json:"average_group_duration"`
LastStatsUpdate time.Time `json:"last_stats_update"`
TotalGroups int64 `json:"total_groups"`
ActiveGroups int64 `json:"active_groups"`
TotalRaids int64 `json:"total_raids"`
ActiveRaids int64 `json:"active_raids"`
TotalInvites int64 `json:"total_invites"`
AcceptedInvites int64 `json:"accepted_invites"`
DeclinedInvites int64 `json:"declined_invites"`
ExpiredInvites int64 `json:"expired_invites"`
AverageGroupSize float64 `json:"average_group_size"`
AverageGroupDuration time.Duration `json:"average_group_duration"`
LastStatsUpdate time.Time `json:"last_stats_update"`
}
// Default group options
func DefaultGroupOptions() GroupOptions {
return GroupOptions{
LootMethod: LOOT_METHOD_ROUND_ROBIN,
LootItemsRarity: LOOT_RARITY_COMMON,
AutoSplit: AUTO_SPLIT_DISABLED,
DefaultYell: DEFAULT_YELL_DISABLED,
GroupLockMethod: LOCK_METHOD_OPEN,
GroupAutolock: AUTO_LOCK_DISABLED,
SoloAutolock: AUTO_LOCK_DISABLED,
AutoLootMethod: AUTO_LOOT_DISABLED,
LastLootedIndex: 0,
LootMethod: LOOT_METHOD_ROUND_ROBIN,
LootItemsRarity: LOOT_RARITY_COMMON,
AutoSplit: AUTO_SPLIT_DISABLED,
DefaultYell: DEFAULT_YELL_DISABLED,
GroupLockMethod: LOCK_METHOD_OPEN,
GroupAutolock: AUTO_LOCK_DISABLED,
SoloAutolock: AUTO_LOCK_DISABLED,
AutoLootMethod: AUTO_LOOT_DISABLED,
LastLootedIndex: 0,
}
}
@ -230,7 +230,7 @@ func (gmi *GroupMemberInfo) UpdateStats() {
if gmi.Member == nil {
return
}
entity := gmi.Member
gmi.HPCurrent = entity.GetHP()
gmi.HPMax = entity.GetTotalHP()
@ -239,7 +239,7 @@ func (gmi *GroupMemberInfo) UpdateStats() {
gmi.LevelCurrent = int16(entity.GetLevel())
gmi.LevelMax = int16(entity.GetLevel()) // TODO: Get actual max level
gmi.LastUpdate = time.Now()
// Update zone info if entity has zone
if zone := entity.GetZone(); zone != nil {
gmi.ZoneID = zone.GetZoneID()
@ -256,7 +256,7 @@ func (go_opts *GroupOptions) Copy() GroupOptions {
// IsValid checks if group options are valid
func (go_opts *GroupOptions) IsValid() bool {
return go_opts.LootMethod >= LOOT_METHOD_LEADER_ONLY && go_opts.LootMethod <= LOOT_METHOD_LOTTO &&
go_opts.LootItemsRarity >= LOOT_RARITY_COMMON && go_opts.LootItemsRarity <= LOOT_RARITY_FABLED
go_opts.LootItemsRarity >= LOOT_RARITY_COMMON && go_opts.LootItemsRarity <= LOOT_RARITY_FABLED
}
// NewGroupMessage creates a new group message
@ -288,4 +288,4 @@ func (gi *GroupInvite) IsExpired() bool {
// TimeRemaining returns the remaining time for the invite
func (gi *GroupInvite) TimeRemaining() time.Duration {
return gi.ExpiresTime.Sub(time.Now())
}
}

View File

@ -2,55 +2,55 @@ package guilds
// Guild rank constants
const (
RankLeader = 0
RankSeniorOfficer = 1
RankOfficer = 2
RankSeniorMember = 3
RankMember = 4
RankJuniorMember = 5
RankInitiate = 6
RankRecruit = 7
RankLeader = 0
RankSeniorOfficer = 1
RankOfficer = 2
RankSeniorMember = 3
RankMember = 4
RankJuniorMember = 5
RankInitiate = 6
RankRecruit = 7
)
// Guild permission constants
const (
PermissionInvite = 0
PermissionRemoveMember = 1
PermissionPromoteMember = 2
PermissionDemoteMember = 3
PermissionChangeMOTD = 6
PermissionChangePermissions = 7
PermissionChangeRankNames = 8
PermissionSeeOfficerNotes = 9
PermissionEditOfficerNotes = 10
PermissionSeeOfficerChat = 11
PermissionSpeakInOfficerChat = 12
PermissionSeeGuildChat = 13
PermissionSpeakInGuildChat = 14
PermissionEditPersonalNotes = 15
PermissionEditPersonalNotesOthers = 16
PermissionEditEventFilters = 17
PermissionEditEvents = 18
PermissionPurchaseStatusItems = 19
PermissionDisplayGuildName = 20
PermissionSendEmailToGuild = 21
PermissionBank1SeeContents = 22
PermissionBank2SeeContents = 23
PermissionBank3SeeContents = 24
PermissionBank4SeeContents = 25
PermissionBank1Deposit = 26
PermissionBank2Deposit = 27
PermissionBank3Deposit = 28
PermissionBank4Deposit = 29
PermissionBank1Withdrawal = 30
PermissionBank2Withdrawal = 31
PermissionBank3Withdrawal = 32
PermissionBank4Withdrawal = 33
PermissionEditRecruitingSettings = 35
PermissionMakeOthersRecruiters = 36
PermissionSeeRecruitingSettings = 37
PermissionAssignPoints = 43
PermissionReceivePoints = 44
PermissionInvite = 0
PermissionRemoveMember = 1
PermissionPromoteMember = 2
PermissionDemoteMember = 3
PermissionChangeMOTD = 6
PermissionChangePermissions = 7
PermissionChangeRankNames = 8
PermissionSeeOfficerNotes = 9
PermissionEditOfficerNotes = 10
PermissionSeeOfficerChat = 11
PermissionSpeakInOfficerChat = 12
PermissionSeeGuildChat = 13
PermissionSpeakInGuildChat = 14
PermissionEditPersonalNotes = 15
PermissionEditPersonalNotesOthers = 16
PermissionEditEventFilters = 17
PermissionEditEvents = 18
PermissionPurchaseStatusItems = 19
PermissionDisplayGuildName = 20
PermissionSendEmailToGuild = 21
PermissionBank1SeeContents = 22
PermissionBank2SeeContents = 23
PermissionBank3SeeContents = 24
PermissionBank4SeeContents = 25
PermissionBank1Deposit = 26
PermissionBank2Deposit = 27
PermissionBank3Deposit = 28
PermissionBank4Deposit = 29
PermissionBank1Withdrawal = 30
PermissionBank2Withdrawal = 31
PermissionBank3Withdrawal = 32
PermissionBank4Withdrawal = 33
PermissionEditRecruitingSettings = 35
PermissionMakeOthersRecruiters = 36
PermissionSeeRecruitingSettings = 37
PermissionAssignPoints = 43
PermissionReceivePoints = 44
)
// Event filter categories
@ -172,22 +172,22 @@ const (
// Recruiting description tags
const (
RecruitingDescTagNone = 0
RecruitingDescTagGood = 1
RecruitingDescTagEvil = 2
RecruitingDescTagChatty = 3
RecruitingDescTagOrganized = 4
RecruitingDescTagRoleplay = 5
RecruitingDescTagEnjoyQuests = 6
RecruitingDescTagEnjoyRaids = 7
RecruitingDescTagOddHours = 8
RecruitingDescTagCrafterOriented = 9
RecruitingDescTagFamilyFriendly = 10
RecruitingDescTagMatureHumor = 11
RecruitingDescTagInmatesRun = 12
RecruitingDescTagVeryFunny = 13
RecruitingDescTagHumorCausesPain = 14
RecruitingDescTagSerious = 15
RecruitingDescTagNone = 0
RecruitingDescTagGood = 1
RecruitingDescTagEvil = 2
RecruitingDescTagChatty = 3
RecruitingDescTagOrganized = 4
RecruitingDescTagRoleplay = 5
RecruitingDescTagEnjoyQuests = 6
RecruitingDescTagEnjoyRaids = 7
RecruitingDescTagOddHours = 8
RecruitingDescTagCrafterOriented = 9
RecruitingDescTagFamilyFriendly = 10
RecruitingDescTagMatureHumor = 11
RecruitingDescTagInmatesRun = 12
RecruitingDescTagVeryFunny = 13
RecruitingDescTagHumorCausesPain = 14
RecruitingDescTagSerious = 15
)
// Member flags
@ -206,14 +206,14 @@ const (
// System limits
const (
MaxGuildLevel = 80
MaxPointHistory = 50
MaxEvents = 500
MaxLockedEvents = 200
MaxGuildNameLength = 64
MaxMOTDLength = 256
MaxMemberNameLength = 64
MaxBankNameLength = 64
MaxGuildLevel = 80
MaxPointHistory = 50
MaxEvents = 500
MaxLockedEvents = 200
MaxGuildNameLength = 64
MaxMOTDLength = 256
MaxMemberNameLength = 64
MaxBankNameLength = 64
MaxRecruitingDescLength = 512
)
@ -221,10 +221,10 @@ const (
var DefaultRankNames = map[int8]string{
RankLeader: "Leader",
RankSeniorOfficer: "Senior Officer",
RankOfficer: "Officer",
RankOfficer: "Officer",
RankSeniorMember: "Senior Member",
RankMember: "Member",
RankJuniorMember: "Junior Member",
RankInitiate: "Initiate",
RankRecruit: "Recruit",
}
}

View File

@ -23,7 +23,7 @@ func NewDatabaseGuildManager(db *database.DB) *DatabaseGuildManager {
// LoadGuilds retrieves all guilds from database
func (dgm *DatabaseGuildManager) LoadGuilds(ctx context.Context) ([]GuildData, error) {
query := "SELECT `id`, `name`, `motd`, `level`, `xp`, `xp_needed`, `formed_on` FROM `guilds`"
rows, err := dgm.db.QueryContext(ctx, query)
if err != nil {
return nil, fmt.Errorf("failed to query guilds: %w", err)
@ -70,7 +70,7 @@ func (dgm *DatabaseGuildManager) LoadGuilds(ctx context.Context) ([]GuildData, e
// LoadGuild retrieves a specific guild from database
func (dgm *DatabaseGuildManager) LoadGuild(ctx context.Context, guildID int32) (*GuildData, error) {
query := "SELECT `id`, `name`, `motd`, `level`, `xp`, `xp_needed`, `formed_on` FROM `guilds` WHERE `id` = ?"
var guild GuildData
var motd *string
var formedOnTimestamp int64
@ -101,12 +101,12 @@ func (dgm *DatabaseGuildManager) LoadGuild(ctx context.Context, guildID int32) (
// LoadGuildMembers retrieves all members for a guild
func (dgm *DatabaseGuildManager) LoadGuildMembers(ctx context.Context, guildID int32) ([]GuildMemberData, error) {
query := `SELECT char_id, guild_id, account_id, recruiter_id, name, guild_status, points,
adventure_class, adventure_level, tradeskill_class, tradeskill_level, rank,
member_flags, zone, join_date, last_login_date, note, officer_note,
recruiter_description, recruiter_picture_data, recruiting_show_adventure_class
query := `SELECT char_id, guild_id, account_id, recruiter_id, name, guild_status, points,
adventure_class, adventure_level, tradeskill_class, tradeskill_level, rank,
member_flags, zone, join_date, last_login_date, note, officer_note,
recruiter_description, recruiter_picture_data, recruiting_show_adventure_class
FROM guild_members WHERE guild_id = ?`
rows, err := dgm.db.QueryContext(ctx, query, guildID)
if err != nil {
return nil, fmt.Errorf("failed to query guild members for guild %d: %w", guildID, err)
@ -176,10 +176,10 @@ func (dgm *DatabaseGuildManager) LoadGuildMembers(ctx context.Context, guildID i
// LoadGuildEvents retrieves events for a guild
func (dgm *DatabaseGuildManager) LoadGuildEvents(ctx context.Context, guildID int32) ([]GuildEventData, error) {
query := `SELECT event_id, guild_id, date, type, description, locked
FROM guild_events WHERE guild_id = ?
query := `SELECT event_id, guild_id, date, type, description, locked
FROM guild_events WHERE guild_id = ?
ORDER BY event_id DESC LIMIT ?`
rows, err := dgm.db.QueryContext(ctx, query, guildID, MaxEvents)
if err != nil {
return nil, fmt.Errorf("failed to query guild events for guild %d: %w", guildID, err)
@ -219,7 +219,7 @@ func (dgm *DatabaseGuildManager) LoadGuildEvents(ctx context.Context, guildID in
// LoadGuildRanks retrieves custom rank names for a guild
func (dgm *DatabaseGuildManager) LoadGuildRanks(ctx context.Context, guildID int32) ([]GuildRankData, error) {
query := "SELECT guild_id, rank, name FROM guild_ranks WHERE guild_id = ?"
rows, err := dgm.db.QueryContext(ctx, query, guildID)
if err != nil {
return nil, fmt.Errorf("failed to query guild ranks for guild %d: %w", guildID, err)
@ -252,7 +252,7 @@ func (dgm *DatabaseGuildManager) LoadGuildRanks(ctx context.Context, guildID int
// LoadGuildPermissions retrieves permissions for a guild
func (dgm *DatabaseGuildManager) LoadGuildPermissions(ctx context.Context, guildID int32) ([]GuildPermissionData, error) {
query := "SELECT guild_id, rank, permission, value FROM guild_permissions WHERE guild_id = ?"
rows, err := dgm.db.QueryContext(ctx, query, guildID)
if err != nil {
return nil, fmt.Errorf("failed to query guild permissions for guild %d: %w", guildID, err)
@ -286,7 +286,7 @@ func (dgm *DatabaseGuildManager) LoadGuildPermissions(ctx context.Context, guild
// LoadGuildEventFilters retrieves event filters for a guild
func (dgm *DatabaseGuildManager) LoadGuildEventFilters(ctx context.Context, guildID int32) ([]GuildEventFilterData, error) {
query := "SELECT guild_id, event_id, category, value FROM guild_event_filters WHERE guild_id = ?"
rows, err := dgm.db.QueryContext(ctx, query, guildID)
if err != nil {
return nil, fmt.Errorf("failed to query guild event filters for guild %d: %w", guildID, err)
@ -320,7 +320,7 @@ func (dgm *DatabaseGuildManager) LoadGuildEventFilters(ctx context.Context, guil
// LoadGuildRecruiting retrieves recruiting settings for a guild
func (dgm *DatabaseGuildManager) LoadGuildRecruiting(ctx context.Context, guildID int32) ([]GuildRecruitingData, error) {
query := "SELECT guild_id, flag, value FROM guild_recruiting WHERE guild_id = ?"
rows, err := dgm.db.QueryContext(ctx, query, guildID)
if err != nil {
return nil, fmt.Errorf("failed to query guild recruiting for guild %d: %w", guildID, err)
@ -352,10 +352,10 @@ func (dgm *DatabaseGuildManager) LoadGuildRecruiting(ctx context.Context, guildI
// LoadPointHistory retrieves point history for a member
func (dgm *DatabaseGuildManager) LoadPointHistory(ctx context.Context, characterID int32) ([]PointHistoryData, error) {
query := `SELECT char_id, date, modified_by, comment, points
FROM guild_point_history WHERE char_id = ?
query := `SELECT char_id, date, modified_by, comment, points
FROM guild_point_history WHERE char_id = ?
ORDER BY date DESC LIMIT ?`
rows, err := dgm.db.QueryContext(ctx, query, characterID, MaxPointHistory)
if err != nil {
return nil, fmt.Errorf("failed to query point history for character %d: %w", characterID, err)
@ -393,8 +393,8 @@ func (dgm *DatabaseGuildManager) LoadPointHistory(ctx context.Context, character
// SaveGuild saves guild basic data
func (dgm *DatabaseGuildManager) SaveGuild(ctx context.Context, guild *Guild) error {
query := `INSERT OR REPLACE INTO guilds
(id, name, motd, level, xp, xp_needed, formed_on)
query := `INSERT OR REPLACE INTO guilds
(id, name, motd, level, xp, xp_needed, formed_on)
VALUES (?, ?, ?, ?, ?, ?, ?)`
guildInfo := guild.GetGuildInfo()
@ -436,10 +436,10 @@ func (dgm *DatabaseGuildManager) SaveGuildMembers(ctx context.Context, guildID i
}
// Insert all members
insertQuery := `INSERT INTO guild_members
(char_id, guild_id, account_id, recruiter_id, name, guild_status, points,
adventure_class, adventure_level, tradeskill_class, tradeskill_level, rank,
member_flags, zone, join_date, last_login_date, note, officer_note,
insertQuery := `INSERT INTO guild_members
(char_id, guild_id, account_id, recruiter_id, name, guild_status, points,
adventure_class, adventure_level, tradeskill_class, tradeskill_level, rank,
member_flags, zone, join_date, last_login_date, note, officer_note,
recruiter_description, recruiter_picture_data, recruiting_show_adventure_class)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
@ -488,8 +488,8 @@ func (dgm *DatabaseGuildManager) SaveGuildEvents(ctx context.Context, guildID in
return nil
}
query := `INSERT OR REPLACE INTO guild_events
(event_id, guild_id, date, type, description, locked)
query := `INSERT OR REPLACE INTO guild_events
(event_id, guild_id, date, type, description, locked)
VALUES (?, ?, ?, ?, ?, ?)`
for _, event := range events {
@ -705,7 +705,7 @@ func (dgm *DatabaseGuildManager) SavePointHistory(ctx context.Context, character
// GetGuildIDByCharacterID returns guild ID for a character
func (dgm *DatabaseGuildManager) GetGuildIDByCharacterID(ctx context.Context, characterID int32) (int32, error) {
query := "SELECT guild_id FROM guild_members WHERE char_id = ?"
var guildID int32
err := dgm.db.QueryRowContext(ctx, query, characterID).Scan(&guildID)
if err != nil {
@ -717,7 +717,7 @@ func (dgm *DatabaseGuildManager) GetGuildIDByCharacterID(ctx context.Context, ch
// CreateGuild creates a new guild
func (dgm *DatabaseGuildManager) CreateGuild(ctx context.Context, guildData GuildData) (int32, error) {
query := `INSERT INTO guilds (name, motd, level, xp, xp_needed, formed_on)
query := `INSERT INTO guilds (name, motd, level, xp, xp_needed, formed_on)
VALUES (?, ?, ?, ?, ?, ?)`
formedTimestamp := guildData.FormedDate.Unix()
@ -793,7 +793,7 @@ func (dgm *DatabaseGuildManager) DeleteGuild(ctx context.Context, guildID int32)
// GetNextGuildID returns the next available guild ID
func (dgm *DatabaseGuildManager) GetNextGuildID(ctx context.Context) (int32, error) {
query := "SELECT COALESCE(MAX(id), 0) + 1 FROM guilds"
var nextID int32
err := dgm.db.QueryRowContext(ctx, query).Scan(&nextID)
if err != nil {
@ -806,7 +806,7 @@ func (dgm *DatabaseGuildManager) GetNextGuildID(ctx context.Context) (int32, err
// GetNextEventID returns the next available event ID for a guild
func (dgm *DatabaseGuildManager) GetNextEventID(ctx context.Context, guildID int32) (int64, error) {
query := "SELECT COALESCE(MAX(event_id), 0) + 1 FROM guild_events WHERE guild_id = ?"
var nextID int64
err := dgm.db.QueryRowContext(ctx, query, guildID).Scan(&nextID)
if err != nil {
@ -933,4 +933,4 @@ func (dgm *DatabaseGuildManager) EnsureGuildTables(ctx context.Context) error {
}
return nil
}
}

View File

@ -64,13 +64,13 @@ func (g *Guild) SetID(id int32) {
func (g *Guild) SetName(name string, sendPacket bool) {
g.mu.Lock()
defer g.mu.Unlock()
if len(name) > MaxGuildNameLength {
name = name[:MaxGuildNameLength]
}
g.name = name
g.lastModified = time.Now()
if sendPacket {
g.saveNeeded = true
}
@ -80,17 +80,17 @@ func (g *Guild) SetName(name string, sendPacket bool) {
func (g *Guild) SetLevel(level int8, sendPacket bool) {
g.mu.Lock()
defer g.mu.Unlock()
if level > MaxGuildLevel {
level = MaxGuildLevel
}
if level < 1 {
level = 1
}
g.level = level
g.lastModified = time.Now()
if sendPacket {
g.saveNeeded = true
}
@ -107,13 +107,13 @@ func (g *Guild) SetFormedDate(formedDate time.Time) {
func (g *Guild) SetMOTD(motd string, sendPacket bool) {
g.mu.Lock()
defer g.mu.Unlock()
if len(motd) > MaxMOTDLength {
motd = motd[:MaxMOTDLength]
}
g.motd = motd
g.lastModified = time.Now()
if sendPacket {
g.saveNeeded = true
}
@ -158,10 +158,10 @@ func (g *Guild) GetMOTD() string {
func (g *Guild) SetEXPCurrent(exp int64, sendPacket bool) {
g.mu.Lock()
defer g.mu.Unlock()
g.expCurrent = exp
g.lastModified = time.Now()
if sendPacket {
g.saveNeeded = true
}
@ -171,10 +171,10 @@ func (g *Guild) SetEXPCurrent(exp int64, sendPacket bool) {
func (g *Guild) AddEXPCurrent(exp int64, sendPacket bool) {
g.mu.Lock()
defer g.mu.Unlock()
g.expCurrent += exp
g.lastModified = time.Now()
if sendPacket {
g.saveNeeded = true
}
@ -191,10 +191,10 @@ func (g *Guild) GetEXPCurrent() int64 {
func (g *Guild) SetEXPToNextLevel(exp int64, sendPacket bool) {
g.mu.Lock()
defer g.mu.Unlock()
g.expToNextLevel = exp
g.lastModified = time.Now()
if sendPacket {
g.saveNeeded = true
}
@ -211,10 +211,10 @@ func (g *Guild) GetEXPToNextLevel() int64 {
func (g *Guild) SetRecruitingShortDesc(desc string, sendPacket bool) {
g.mu.Lock()
defer g.mu.Unlock()
g.recruitingShortDesc = desc
g.lastModified = time.Now()
if sendPacket {
g.recruitingSaveNeeded = true
}
@ -231,13 +231,13 @@ func (g *Guild) GetRecruitingShortDesc() string {
func (g *Guild) SetRecruitingFullDesc(desc string, sendPacket bool) {
g.mu.Lock()
defer g.mu.Unlock()
if len(desc) > MaxRecruitingDescLength {
desc = desc[:MaxRecruitingDescLength]
}
g.recruitingFullDesc = desc
g.lastModified = time.Now()
if sendPacket {
g.recruitingSaveNeeded = true
}
@ -254,10 +254,10 @@ func (g *Guild) GetRecruitingFullDesc() string {
func (g *Guild) SetRecruitingMinLevel(level int8, sendPacket bool) {
g.mu.Lock()
defer g.mu.Unlock()
g.recruitingMinLevel = level
g.lastModified = time.Now()
if sendPacket {
g.recruitingSaveNeeded = true
}
@ -274,10 +274,10 @@ func (g *Guild) GetRecruitingMinLevel() int8 {
func (g *Guild) SetRecruitingPlayStyle(playStyle int8, sendPacket bool) {
g.mu.Lock()
defer g.mu.Unlock()
g.recruitingPlayStyle = playStyle
g.lastModified = time.Now()
if sendPacket {
g.recruitingSaveNeeded = true
}
@ -294,18 +294,18 @@ func (g *Guild) GetRecruitingPlayStyle() int8 {
func (g *Guild) SetRecruitingDescTag(index, tag int8, sendPacket bool) bool {
g.mu.Lock()
defer g.mu.Unlock()
if index < 0 || index > 3 {
return false
}
g.recruitingDescTags[index] = tag
g.lastModified = time.Now()
if sendPacket {
g.recruitingSaveNeeded = true
}
return true
}
@ -313,7 +313,7 @@ func (g *Guild) SetRecruitingDescTag(index, tag int8, sendPacket bool) bool {
func (g *Guild) GetRecruitingDescTag(index int8) int8 {
g.mu.RLock()
defer g.mu.RUnlock()
if tag, exists := g.recruitingDescTags[index]; exists {
return tag
}
@ -324,22 +324,22 @@ func (g *Guild) GetRecruitingDescTag(index int8) int8 {
func (g *Guild) SetPermission(rank, permission, value int8, sendPacket, saveNeeded bool) bool {
g.mu.Lock()
defer g.mu.Unlock()
if rank < RankLeader || rank > RankRecruit {
return false
}
if g.permissions[rank] == nil {
g.permissions[rank] = make(map[int8]int8)
}
g.permissions[rank][permission] = value
g.lastModified = time.Now()
if saveNeeded {
g.ranksSaveNeeded = true
}
return true
}
@ -347,13 +347,13 @@ func (g *Guild) SetPermission(rank, permission, value int8, sendPacket, saveNeed
func (g *Guild) GetPermission(rank, permission int8) int8 {
g.mu.RLock()
defer g.mu.RUnlock()
if rankPerms, exists := g.permissions[rank]; exists {
if value, exists := rankPerms[permission]; exists {
return value
}
}
// Return default permission based on rank
return g.getDefaultPermission(rank, permission)
}
@ -362,18 +362,18 @@ func (g *Guild) GetPermission(rank, permission int8) int8 {
func (g *Guild) SetEventFilter(eventID, category, value int8, sendPacket, saveNeeded bool) bool {
g.mu.Lock()
defer g.mu.Unlock()
if g.eventFilters[eventID] == nil {
g.eventFilters[eventID] = make(map[int8]int8)
}
g.eventFilters[eventID][category] = value
g.lastModified = time.Now()
if saveNeeded {
g.eventFiltersSaveNeeded = true
}
return true
}
@ -381,13 +381,13 @@ func (g *Guild) SetEventFilter(eventID, category, value int8, sendPacket, saveNe
func (g *Guild) GetEventFilter(eventID, category int8) int8 {
g.mu.RLock()
defer g.mu.RUnlock()
if eventFilters, exists := g.eventFilters[eventID]; exists {
if value, exists := eventFilters[category]; exists {
return value
}
}
return 1 // Default to enabled
}
@ -395,12 +395,12 @@ func (g *Guild) GetEventFilter(eventID, category int8) int8 {
func (g *Guild) GetNumUniqueAccounts() int32 {
g.mu.RLock()
defer g.mu.RUnlock()
accounts := make(map[int32]bool)
for _, member := range g.members {
accounts[member.AccountID] = true
}
return int32(len(accounts))
}
@ -408,14 +408,14 @@ func (g *Guild) GetNumUniqueAccounts() int32 {
func (g *Guild) GetNumRecruiters() int32 {
g.mu.RLock()
defer g.mu.RUnlock()
count := int32(0)
for _, member := range g.members {
if member.MemberFlags&MemberFlagRecruitingForGuild != 0 {
count++
}
}
return count
}
@ -423,14 +423,14 @@ func (g *Guild) GetNumRecruiters() int32 {
func (g *Guild) GetNextRecruiterID() int32 {
g.mu.RLock()
defer g.mu.RUnlock()
maxID := int32(0)
for _, member := range g.members {
if member.RecruiterID > maxID {
maxID = member.RecruiterID
}
}
return maxID + 1
}
@ -438,7 +438,7 @@ func (g *Guild) GetNextRecruiterID() int32 {
func (g *Guild) GetNextEventID() int64 {
g.mu.Lock()
defer g.mu.Unlock()
eventID := g.nextEventID
g.nextEventID++
return eventID
@ -448,7 +448,7 @@ func (g *Guild) GetNextEventID() int64 {
func (g *Guild) GetGuildMember(characterID int32) *GuildMember {
g.mu.RLock()
defer g.mu.RUnlock()
return g.members[characterID]
}
@ -456,13 +456,13 @@ func (g *Guild) GetGuildMember(characterID int32) *GuildMember {
func (g *Guild) GetGuildMemberByName(playerName string) *GuildMember {
g.mu.RLock()
defer g.mu.RUnlock()
for _, member := range g.members {
if strings.EqualFold(member.Name, playerName) {
return member
}
}
return nil
}
@ -470,14 +470,14 @@ func (g *Guild) GetGuildMemberByName(playerName string) *GuildMember {
func (g *Guild) GetGuildRecruiters() []*GuildMember {
g.mu.RLock()
defer g.mu.RUnlock()
var recruiters []*GuildMember
for _, member := range g.members {
if member.MemberFlags&MemberFlagRecruitingForGuild != 0 {
recruiters = append(recruiters, member)
}
}
return recruiters
}
@ -485,13 +485,13 @@ func (g *Guild) GetGuildRecruiters() []*GuildMember {
func (g *Guild) GetGuildEvent(eventID int64) *GuildEvent {
g.mu.RLock()
defer g.mu.RUnlock()
for i := range g.guildEvents {
if g.guildEvents[i].EventID == eventID {
return &g.guildEvents[i]
}
}
return nil
}
@ -499,18 +499,18 @@ func (g *Guild) GetGuildEvent(eventID int64) *GuildEvent {
func (g *Guild) SetRankName(rank int8, name string, sendPacket bool) bool {
g.mu.Lock()
defer g.mu.Unlock()
if rank < RankLeader || rank > RankRecruit {
return false
}
g.ranks[rank] = name
g.lastModified = time.Now()
if sendPacket {
g.ranksSaveNeeded = true
}
return true
}
@ -518,16 +518,16 @@ func (g *Guild) SetRankName(rank int8, name string, sendPacket bool) bool {
func (g *Guild) GetRankName(rank int8) string {
g.mu.RLock()
defer g.mu.RUnlock()
if name, exists := g.ranks[rank]; exists {
return name
}
// Return default rank name
if defaultName, exists := DefaultRankNames[rank]; exists {
return defaultName
}
return "Unknown"
}
@ -535,18 +535,18 @@ func (g *Guild) GetRankName(rank int8) string {
func (g *Guild) SetRecruitingFlag(flag, value int8, sendPacket bool) bool {
g.mu.Lock()
defer g.mu.Unlock()
if flag < RecruitingFlagTraining || flag > RecruitingFlagTradeskillers {
return false
}
g.recruitingFlags[flag] = value
g.lastModified = time.Now()
if sendPacket {
g.recruitingSaveNeeded = true
}
return true
}
@ -554,11 +554,11 @@ func (g *Guild) SetRecruitingFlag(flag, value int8, sendPacket bool) bool {
func (g *Guild) GetRecruitingFlag(flag int8) int8 {
g.mu.RLock()
defer g.mu.RUnlock()
if value, exists := g.recruitingFlags[flag]; exists {
return value
}
return 0
}
@ -566,26 +566,26 @@ func (g *Guild) GetRecruitingFlag(flag int8) int8 {
func (g *Guild) AddNewGuildMember(characterID int32, invitedBy string, joinDate time.Time, rank int8) bool {
g.mu.Lock()
defer g.mu.Unlock()
// Check if member already exists
if _, exists := g.members[characterID]; exists {
return false
}
member := &GuildMember{
CharacterID: characterID,
Rank: rank,
JoinDate: joinDate,
CharacterID: characterID,
Rank: rank,
JoinDate: joinDate,
PointHistory: make([]PointHistory, 0),
}
g.members[characterID] = member
g.memberSaveNeeded = true
g.lastModified = time.Now()
// Add guild event
g.addNewGuildEventNoLock(EventMemberJoins, fmt.Sprintf("%s has joined the guild", member.Name), time.Now(), true)
return true
}
@ -593,11 +593,11 @@ func (g *Guild) AddNewGuildMember(characterID int32, invitedBy string, joinDate
func (g *Guild) RemoveGuildMember(characterID int32, sendPacket bool) {
g.mu.Lock()
defer g.mu.Unlock()
if member, exists := g.members[characterID]; exists {
// Add guild event
g.addNewGuildEventNoLock(EventMemberLeaves, fmt.Sprintf("%s has left the guild", member.Name), time.Now(), sendPacket)
delete(g.members, characterID)
g.memberSaveNeeded = true
g.lastModified = time.Now()
@ -608,23 +608,23 @@ func (g *Guild) RemoveGuildMember(characterID int32, sendPacket bool) {
func (g *Guild) PromoteGuildMember(characterID int32, promoterName string, sendPacket bool) bool {
g.mu.Lock()
defer g.mu.Unlock()
member, exists := g.members[characterID]
if !exists || member.Rank <= RankLeader {
return false
}
oldRank := member.Rank
member.Rank--
g.memberSaveNeeded = true
g.lastModified = time.Now()
// Add guild event
g.addNewGuildEventNoLock(EventMemberPromoted,
fmt.Sprintf("%s has been promoted from %s to %s by %s",
member.Name, g.getRankNameNoLock(oldRank), g.getRankNameNoLock(member.Rank), promoterName),
g.addNewGuildEventNoLock(EventMemberPromoted,
fmt.Sprintf("%s has been promoted from %s to %s by %s",
member.Name, g.getRankNameNoLock(oldRank), g.getRankNameNoLock(member.Rank), promoterName),
time.Now(), sendPacket)
return true
}
@ -632,23 +632,23 @@ func (g *Guild) PromoteGuildMember(characterID int32, promoterName string, sendP
func (g *Guild) DemoteGuildMember(characterID int32, demoterName string, sendPacket bool) bool {
g.mu.Lock()
defer g.mu.Unlock()
member, exists := g.members[characterID]
if !exists || member.Rank >= RankRecruit {
return false
}
oldRank := member.Rank
member.Rank++
g.memberSaveNeeded = true
g.lastModified = time.Now()
// Add guild event
g.addNewGuildEventNoLock(EventMemberDemoted,
fmt.Sprintf("%s has been demoted from %s to %s by %s",
member.Name, g.getRankNameNoLock(oldRank), g.getRankNameNoLock(member.Rank), demoterName),
g.addNewGuildEventNoLock(EventMemberDemoted,
fmt.Sprintf("%s has been demoted from %s to %s by %s",
member.Name, g.getRankNameNoLock(oldRank), g.getRankNameNoLock(member.Rank), demoterName),
time.Now(), sendPacket)
return true
}
@ -656,20 +656,20 @@ func (g *Guild) DemoteGuildMember(characterID int32, demoterName string, sendPac
func (g *Guild) AddPointsToGuildMember(characterID int32, points float64, modifiedBy, comment string, sendPacket bool) bool {
g.mu.Lock()
defer g.mu.Unlock()
member, exists := g.members[characterID]
if !exists {
return false
}
member.Points += points
// Add to point history
if len(member.PointHistory) >= MaxPointHistory {
// Remove oldest entry
member.PointHistory = member.PointHistory[1:]
}
member.PointHistory = append(member.PointHistory, PointHistory{
Date: time.Now(),
ModifiedBy: modifiedBy,
@ -677,10 +677,10 @@ func (g *Guild) AddPointsToGuildMember(characterID int32, points float64, modifi
Points: points,
SaveNeeded: true,
})
g.pointsHistorySaveNeeded = true
g.lastModified = time.Now()
return true
}
@ -701,17 +701,17 @@ func (g *Guild) addNewGuildEventNoLock(eventType int32, description string, date
Locked: 0,
SaveNeeded: true,
}
g.nextEventID++
// Add to front of events list (newest first)
g.guildEvents = append([]GuildEvent{event}, g.guildEvents...)
// Limit event history
if len(g.guildEvents) > MaxEvents {
g.guildEvents = g.guildEvents[:MaxEvents]
}
g.eventsSaveNeeded = true
g.lastModified = time.Now()
}
@ -720,7 +720,7 @@ func (g *Guild) addNewGuildEventNoLock(eventType int32, description string, date
func (g *Guild) GetGuildInfo() GuildInfo {
g.mu.RLock()
defer g.mu.RUnlock()
return GuildInfo{
ID: g.id,
Name: g.name,
@ -741,12 +741,12 @@ func (g *Guild) GetGuildInfo() GuildInfo {
func (g *Guild) GetAllMembers() []*GuildMember {
g.mu.RLock()
defer g.mu.RUnlock()
members := make([]*GuildMember, 0, len(g.members))
for _, member := range g.members {
members = append(members, member)
}
return members
}
@ -770,7 +770,7 @@ func (g *Guild) getDefaultPermission(rank, permission int8) int8 {
if rank == RankLeader {
return 1
}
// Default permissions based on rank and permission type
switch permission {
case PermissionSeeGuildChat, PermissionSpeakInGuildChat:
@ -786,7 +786,7 @@ func (g *Guild) getDefaultPermission(rank, permission int8) int8 {
return 1
}
}
return 0 // Default to no permission
}
@ -808,4 +808,4 @@ func (g *Guild) getNumRecruitersNoLock() int32 {
}
}
return count
}
}

View File

@ -1,72 +1,75 @@
package guilds
import "context"
import (
"context"
"time"
)
// GuildDatabase defines database operations for guilds
type GuildDatabase interface {
// LoadGuilds retrieves all guilds from database
LoadGuilds(ctx context.Context) ([]GuildData, error)
// LoadGuild retrieves a specific guild from database
LoadGuild(ctx context.Context, guildID int32) (*GuildData, error)
// LoadGuildMembers retrieves all members for a guild
LoadGuildMembers(ctx context.Context, guildID int32) ([]GuildMemberData, error)
// LoadGuildEvents retrieves events for a guild
LoadGuildEvents(ctx context.Context, guildID int32) ([]GuildEventData, error)
// LoadGuildRanks retrieves custom rank names for a guild
LoadGuildRanks(ctx context.Context, guildID int32) ([]GuildRankData, error)
// LoadGuildPermissions retrieves permissions for a guild
LoadGuildPermissions(ctx context.Context, guildID int32) ([]GuildPermissionData, error)
// LoadGuildEventFilters retrieves event filters for a guild
LoadGuildEventFilters(ctx context.Context, guildID int32) ([]GuildEventFilterData, error)
// LoadGuildRecruiting retrieves recruiting settings for a guild
LoadGuildRecruiting(ctx context.Context, guildID int32) ([]GuildRecruitingData, error)
// LoadPointHistory retrieves point history for a member
LoadPointHistory(ctx context.Context, characterID int32) ([]PointHistoryData, error)
// SaveGuild saves guild basic data
SaveGuild(ctx context.Context, guild *Guild) error
// SaveGuildMembers saves all guild members
SaveGuildMembers(ctx context.Context, guildID int32, members []*GuildMember) error
// SaveGuildEvents saves guild events
SaveGuildEvents(ctx context.Context, guildID int32, events []GuildEvent) error
// SaveGuildRanks saves guild rank names
SaveGuildRanks(ctx context.Context, guildID int32, ranks map[int8]string) error
// SaveGuildPermissions saves guild permissions
SaveGuildPermissions(ctx context.Context, guildID int32, permissions map[int8]map[int8]int8) error
// SaveGuildEventFilters saves guild event filters
SaveGuildEventFilters(ctx context.Context, guildID int32, filters map[int8]map[int8]int8) error
// SaveGuildRecruiting saves guild recruiting settings
SaveGuildRecruiting(ctx context.Context, guildID int32, flags, descTags map[int8]int8) error
// SavePointHistory saves point history for a member
SavePointHistory(ctx context.Context, characterID int32, history []PointHistory) error
// GetGuildIDByCharacterID returns guild ID for a character
GetGuildIDByCharacterID(ctx context.Context, characterID int32) (int32, error)
// CreateGuild creates a new guild
CreateGuild(ctx context.Context, guildData GuildData) (int32, error)
// DeleteGuild removes a guild and all related data
DeleteGuild(ctx context.Context, guildID int32) error
// GetNextGuildID returns the next available guild ID
GetNextGuildID(ctx context.Context) (int32, error)
// GetNextEventID returns the next available event ID for a guild
GetNextEventID(ctx context.Context, guildID int32) (int64, error)
}
@ -75,40 +78,40 @@ type GuildDatabase interface {
type ClientManager interface {
// SendGuildUpdate sends guild information update to client
SendGuildUpdate(characterID int32, guild *Guild) error
// SendGuildMemberList sends guild member list to client
SendGuildMemberList(characterID int32, members []MemberInfo) error
// SendGuildMember sends single member info to client
SendGuildMember(characterID int32, member *GuildMember) error
// SendGuildMOTD sends message of the day to client
SendGuildMOTD(characterID int32, motd string) error
// SendGuildEvent sends guild event to client
SendGuildEvent(characterID int32, event *GuildEvent) error
// SendGuildEventList sends guild event list to client
SendGuildEventList(characterID int32, events []GuildEventInfo) error
// SendGuildChatMessage sends guild chat message to client
SendGuildChatMessage(characterID int32, senderName, message string, language int8) error
// SendOfficerChatMessage sends officer chat message to client
SendOfficerChatMessage(characterID int32, senderName, message string, language int8) error
// SendGuildInvite sends guild invitation to client
SendGuildInvite(characterID int32, invite GuildInvite) error
// SendGuildRecruitingInfo sends recruiting information to client
SendGuildRecruitingInfo(characterID int32, info RecruitingInfo) error
// SendGuildPermissions sends guild permissions to client
SendGuildPermissions(characterID int32, permissions map[int8]map[int8]int8) error
// IsClientOnline checks if a character is currently online
IsClientOnline(characterID int32) bool
// GetClientLanguage returns the language setting for a client
GetClientLanguage(characterID int32) int8
}
@ -117,25 +120,25 @@ type ClientManager interface {
type PlayerManager interface {
// GetPlayerInfo retrieves basic player information
GetPlayerInfo(characterID int32) (PlayerInfo, error)
// IsPlayerOnline checks if a player is currently online
IsPlayerOnline(characterID int32) bool
// GetPlayerZone returns the current zone for a player
GetPlayerZone(characterID int32) string
// GetPlayerLevel returns player's current level
GetPlayerLevel(characterID int32) (int8, int8) // adventure, tradeskill
// GetPlayerClass returns player's current class
GetPlayerClass(characterID int32) (int8, int8) // adventure, tradeskill
// GetPlayerName returns player's character name
GetPlayerName(characterID int32) string
// ValidatePlayerExists checks if a player exists
ValidatePlayerExists(characterName string) (int32, error)
// GetAccountID returns the account ID for a character
GetAccountID(characterID int32) int32
}
@ -144,34 +147,34 @@ type PlayerManager interface {
type GuildEventHandler interface {
// OnGuildCreated called when a guild is created
OnGuildCreated(guild *Guild)
// OnGuildDeleted called when a guild is deleted
OnGuildDeleted(guildID int32, guildName string)
// OnMemberJoined called when a member joins a guild
OnMemberJoined(guild *Guild, member *GuildMember, inviterName string)
// OnMemberLeft called when a member leaves a guild
OnMemberLeft(guild *Guild, member *GuildMember, reason string)
// OnMemberPromoted called when a member is promoted
OnMemberPromoted(guild *Guild, member *GuildMember, oldRank, newRank int8, promoterName string)
// OnMemberDemoted called when a member is demoted
OnMemberDemoted(guild *Guild, member *GuildMember, oldRank, newRank int8, demoterName string)
// OnPointsAwarded called when points are awarded to members
OnPointsAwarded(guild *Guild, members []int32, points float64, comment, awardedBy string)
// OnGuildEvent called when a guild event occurs
OnGuildEvent(guild *Guild, event *GuildEvent)
// OnGuildLevelUp called when a guild levels up
OnGuildLevelUp(guild *Guild, oldLevel, newLevel int8)
// OnGuildChatMessage called when a guild chat message is sent
OnGuildChatMessage(guild *Guild, senderID int32, senderName, message string, language int8)
// OnOfficerChatMessage called when an officer chat message is sent
OnOfficerChatMessage(guild *Guild, senderID int32, senderName, message string, language int8)
}
@ -180,28 +183,28 @@ type GuildEventHandler interface {
type LogHandler interface {
// LogDebug logs debug messages
LogDebug(category, message string, args ...interface{})
// LogInfo logs informational messages
LogInfo(category, message string, args ...interface{})
// LogError logs error messages
LogError(category, message string, args ...interface{})
// LogWarning logs warning messages
LogWarning(category, message string, args ...interface{})
}
// PlayerInfo contains basic player information
type PlayerInfo struct {
CharacterID int32 `json:"character_id"`
CharacterName string `json:"character_name"`
AccountID int32 `json:"account_id"`
AdventureLevel int8 `json:"adventure_level"`
AdventureClass int8 `json:"adventure_class"`
TradeskillLevel int8 `json:"tradeskill_level"`
TradeskillClass int8 `json:"tradeskill_class"`
Zone string `json:"zone"`
IsOnline bool `json:"is_online"`
CharacterID int32 `json:"character_id"`
CharacterName string `json:"character_name"`
AccountID int32 `json:"account_id"`
AdventureLevel int8 `json:"adventure_level"`
AdventureClass int8 `json:"adventure_class"`
TradeskillLevel int8 `json:"tradeskill_level"`
TradeskillClass int8 `json:"tradeskill_class"`
Zone string `json:"zone"`
IsOnline bool `json:"is_online"`
LastLogin time.Time `json:"last_login"`
}
@ -255,16 +258,16 @@ func (a *EntityGuildAdapter) HasGuildPermission(permission int8) bool {
type InviteManager interface {
// SendInvite sends a guild invitation
SendInvite(guildID, characterID, inviterID int32, rank int8) error
// AcceptInvite accepts a guild invitation
AcceptInvite(characterID, guildID int32) error
// DeclineInvite declines a guild invitation
DeclineInvite(characterID, guildID int32) error
// GetPendingInvites returns pending invites for a character
GetPendingInvites(characterID int32) ([]GuildInvite, error)
// ClearExpiredInvites removes expired invitations
ClearExpiredInvites() error
}
@ -273,25 +276,25 @@ type InviteManager interface {
type PermissionChecker interface {
// CanInvite checks if a member can invite players
CanInvite(guild *Guild, memberRank int8) bool
// CanRemoveMember checks if a member can remove other members
CanRemoveMember(guild *Guild, memberRank, targetRank int8) bool
// CanPromote checks if a member can promote other members
CanPromote(guild *Guild, memberRank, targetRank int8) bool
// CanDemote checks if a member can demote other members
CanDemote(guild *Guild, memberRank, targetRank int8) bool
// CanEditPermissions checks if a member can edit guild permissions
CanEditPermissions(guild *Guild, memberRank int8) bool
// CanUseBankSlot checks if a member can access a specific bank slot
CanUseBankSlot(guild *Guild, memberRank int8, bankSlot int, action string) bool
// CanSpeakInOfficerChat checks if a member can speak in officer chat
CanSpeakInOfficerChat(guild *Guild, memberRank int8) bool
// CanAssignPoints checks if a member can assign guild points
CanAssignPoints(guild *Guild, memberRank int8) bool
}
@ -300,16 +303,16 @@ type PermissionChecker interface {
type NotificationManager interface {
// NotifyMemberLogin notifies guild of member login
NotifyMemberLogin(guild *Guild, member *GuildMember)
// NotifyMemberLogout notifies guild of member logout
NotifyMemberLogout(guild *Guild, member *GuildMember)
// NotifyGuildMessage sends a message to all guild members
NotifyGuildMessage(guild *Guild, eventType int8, message string, args ...interface{})
// NotifyOfficers sends a message to officers only
NotifyOfficers(guild *Guild, message string, args ...interface{})
// NotifyGuildUpdate notifies guild members of guild changes
NotifyGuildUpdate(guild *Guild)
}
@ -318,25 +321,25 @@ type NotificationManager interface {
type BankManager interface {
// GetBankContents returns contents of a guild bank
GetBankContents(guildID int32, bankSlot int) ([]BankItem, error)
// DepositItem deposits an item into guild bank
DepositItem(guildID int32, bankSlot int, item BankItem, depositorID int32) error
// WithdrawItem withdraws an item from guild bank
WithdrawItem(guildID int32, bankSlot int, itemSlot int, withdrawerID int32) error
// LogBankEvent logs a bank event
LogBankEvent(guildID int32, bankSlot int, eventType int32, description string) error
// GetBankEventHistory returns bank event history
GetBankEventHistory(guildID int32, bankSlot int) ([]GuildBankEvent, error)
}
// BankItem represents an item in the guild bank
type BankItem struct {
Slot int `json:"slot"`
ItemID int32 `json:"item_id"`
Quantity int32 `json:"quantity"`
DepositorID int32 `json:"depositor_id"`
Slot int `json:"slot"`
ItemID int32 `json:"item_id"`
Quantity int32 `json:"quantity"`
DepositorID int32 `json:"depositor_id"`
DepositDate time.Time `json:"deposit_date"`
}
}

View File

@ -5,7 +5,6 @@ import (
"fmt"
"sort"
"strings"
"sync"
"time"
)
@ -41,12 +40,12 @@ func (gl *GuildList) RemoveGuild(guildID int32) {
func (gl *GuildList) GetAllGuilds() []*Guild {
gl.mu.RLock()
defer gl.mu.RUnlock()
guilds := make([]*Guild, 0, len(gl.guilds))
for _, guild := range gl.guilds {
guilds = append(guilds, guild)
}
return guilds
}
@ -61,13 +60,13 @@ func (gl *GuildList) GetGuildCount() int {
func (gl *GuildList) FindGuildByName(name string) *Guild {
gl.mu.RLock()
defer gl.mu.RUnlock()
for _, guild := range gl.guilds {
if strings.EqualFold(guild.GetName(), name) {
return guild
}
}
return nil
}
@ -107,11 +106,11 @@ func (gm *GuildManager) Initialize(ctx context.Context) error {
}
continue
}
gm.guildList.AddGuild(guild)
if gm.logger != nil {
gm.logger.LogDebug("guilds", "Loaded guild %d (%s) with %d members",
gm.logger.LogDebug("guilds", "Loaded guild %d (%s) with %d members",
guild.GetID(), guild.GetName(), len(guild.GetAllMembers()))
}
}
@ -195,7 +194,7 @@ func (gm *GuildManager) CreateGuild(ctx context.Context, name, motd string, lead
leader := NewGuildMember(leaderCharacterID, leaderInfo.CharacterName, RankLeader)
leader.AccountID = leaderInfo.AccountID
leader.UpdatePlayerInfo(leaderInfo)
guild.members[leaderCharacterID] = leader
// Save member to database
@ -215,7 +214,7 @@ func (gm *GuildManager) CreateGuild(ctx context.Context, name, motd string, lead
}
if gm.logger != nil {
gm.logger.LogInfo("guilds", "Created guild %d (%s) with leader %s (%d)",
gm.logger.LogInfo("guilds", "Created guild %d (%s) with leader %s (%d)",
guildID, name, leaderInfo.CharacterName, leaderCharacterID)
}
@ -317,7 +316,7 @@ func (gm *GuildManager) InvitePlayer(ctx context.Context, guildID, inviterID int
// This would typically involve sending a packet to the client
if gm.logger != nil {
gm.logger.LogDebug("guilds", "Player %s invited to guild %s by %s",
gm.logger.LogDebug("guilds", "Player %s invited to guild %s by %s",
playerName, guild.GetName(), inviter.GetName())
}
@ -357,8 +356,8 @@ func (gm *GuildManager) AddMemberToGuild(ctx context.Context, guildID, character
}
// Add guild event
guild.AddNewGuildEvent(EventMemberJoins,
fmt.Sprintf("%s has joined the guild (invited by %s)", playerInfo.CharacterName, inviterName),
guild.AddNewGuildEvent(EventMemberJoins,
fmt.Sprintf("%s has joined the guild (invited by %s)", playerInfo.CharacterName, inviterName),
time.Now(), true)
// Notify event handler
@ -367,7 +366,7 @@ func (gm *GuildManager) AddMemberToGuild(ctx context.Context, guildID, character
}
if gm.logger != nil {
gm.logger.LogInfo("guilds", "Player %s (%d) joined guild %s (%d)",
gm.logger.LogInfo("guilds", "Player %s (%d) joined guild %s (%d)",
playerInfo.CharacterName, characterID, guild.GetName(), guildID)
}
@ -404,7 +403,7 @@ func (gm *GuildManager) RemoveMemberFromGuild(ctx context.Context, guildID, char
}
if gm.logger != nil {
gm.logger.LogInfo("guilds", "Player %s (%d) removed from guild %s (%d) by %s - %s",
gm.logger.LogInfo("guilds", "Player %s (%d) removed from guild %s (%d) by %s - %s",
memberName, characterID, guild.GetName(), guildID, removerName, reason)
}
@ -518,7 +517,7 @@ func (gm *GuildManager) AwardPoints(ctx context.Context, guildID int32, characte
// SaveAllGuilds saves all guilds that need saving
func (gm *GuildManager) SaveAllGuilds(ctx context.Context) error {
guilds := gm.guildList.GetAllGuilds()
var saveErrors []error
for _, guild := range guilds {
if err := gm.saveGuildChanges(ctx, guild); err != nil {
@ -555,7 +554,7 @@ func (gm *GuildManager) SearchGuilds(criteria GuildSearchCriteria) []*Guild {
// GetGuildStatistics returns guild system statistics
func (gm *GuildManager) GetGuildStatistics() GuildStatistics {
guilds := gm.guildList.GetAllGuilds()
stats := GuildStatistics{
TotalGuilds: len(guilds),
}
@ -570,9 +569,9 @@ func (gm *GuildManager) GetGuildStatistics() GuildStatistics {
for _, guild := range guilds {
members := guild.GetAllMembers()
memberCount := len(members)
totalMembers += memberCount
if memberCount > 0 {
activeGuilds++
}
@ -600,7 +599,7 @@ func (gm *GuildManager) GetGuildStatistics() GuildStatistics {
stats.TotalRecruiters = totalRecruiters
stats.UniqueAccounts = len(uniqueAccounts)
stats.HighestGuildLevel = highestLevel
if len(guilds) > 0 {
stats.AverageGuildSize = float64(totalMembers) / float64(len(guilds))
}
@ -632,24 +631,24 @@ func (gm *GuildManager) loadGuildFromData(ctx context.Context, data GuildData) (
CharacterID: md.CharacterID,
AccountID: md.AccountID,
RecruiterID: md.RecruiterID,
Name: md.Name,
GuildStatus: md.GuildStatus,
Points: md.Points,
AdventureClass: md.AdventureClass,
AdventureLevel: md.AdventureLevel,
TradeskillClass: md.TradeskillClass,
TradeskillLevel: md.TradeskillLevel,
Rank: md.Rank,
MemberFlags: md.MemberFlags,
Zone: md.Zone,
JoinDate: md.JoinDate,
LastLoginDate: md.LastLoginDate,
Note: md.Note,
OfficerNote: md.OfficerNote,
RecruiterDescription: md.RecruiterDescription,
RecruiterPictureData: md.RecruiterPictureData,
Name: md.Name,
GuildStatus: md.GuildStatus,
Points: md.Points,
AdventureClass: md.AdventureClass,
AdventureLevel: md.AdventureLevel,
TradeskillClass: md.TradeskillClass,
TradeskillLevel: md.TradeskillLevel,
Rank: md.Rank,
MemberFlags: md.MemberFlags,
Zone: md.Zone,
JoinDate: md.JoinDate,
LastLoginDate: md.LastLoginDate,
Note: md.Note,
OfficerNote: md.OfficerNote,
RecruiterDescription: md.RecruiterDescription,
RecruiterPictureData: md.RecruiterPictureData,
RecruitingShowAdventureClass: md.RecruitingShowAdventureClass,
PointHistory: make([]PointHistory, 0),
PointHistory: make([]PointHistory, 0),
}
// Load point history
@ -906,4 +905,4 @@ func (gm *GuildManager) matchesSearchCriteria(guild *Guild, criteria GuildSearch
}
return true
}
}

View File

@ -8,11 +8,11 @@ import (
func NewGuildMember(characterID int32, name string, rank int8) *GuildMember {
return &GuildMember{
CharacterID: characterID,
Name: name,
Rank: rank,
JoinDate: time.Now(),
LastLoginDate: time.Now(),
PointHistory: make([]PointHistory, 0),
Name: name,
Rank: rank,
JoinDate: time.Now(),
LastLoginDate: time.Now(),
PointHistory: make([]PointHistory, 0),
RecruitingShowAdventureClass: 1,
}
}
@ -229,7 +229,7 @@ func (gm *GuildMember) HasMemberFlag(flag int8) bool {
func (gm *GuildMember) SetMemberFlag(flag int8, value bool) {
gm.mu.Lock()
defer gm.mu.Unlock()
if value {
gm.MemberFlags |= flag
} else {
@ -279,12 +279,12 @@ func (gm *GuildMember) SetRecruiterDescription(description string) {
func (gm *GuildMember) GetRecruiterPictureData() []byte {
gm.mu.RLock()
defer gm.mu.RUnlock()
// Return a copy to prevent external modification
if gm.RecruiterPictureData == nil {
return nil
}
data := make([]byte, len(gm.RecruiterPictureData))
copy(data, gm.RecruiterPictureData)
return data
@ -294,12 +294,12 @@ func (gm *GuildMember) GetRecruiterPictureData() []byte {
func (gm *GuildMember) SetRecruiterPictureData(data []byte) {
gm.mu.Lock()
defer gm.mu.Unlock()
if data == nil {
gm.RecruiterPictureData = nil
return
}
// Make a copy to prevent external modification
gm.RecruiterPictureData = make([]byte, len(data))
copy(gm.RecruiterPictureData, data)
@ -316,7 +316,7 @@ func (gm *GuildMember) GetRecruitingShowAdventureClass() bool {
func (gm *GuildMember) SetRecruitingShowAdventureClass(show bool) {
gm.mu.Lock()
defer gm.mu.Unlock()
if show {
gm.RecruitingShowAdventureClass = 1
} else {
@ -328,7 +328,7 @@ func (gm *GuildMember) SetRecruitingShowAdventureClass(show bool) {
func (gm *GuildMember) GetPointHistory() []PointHistory {
gm.mu.RLock()
defer gm.mu.RUnlock()
history := make([]PointHistory, len(gm.PointHistory))
copy(history, gm.PointHistory)
return history
@ -338,13 +338,13 @@ func (gm *GuildMember) GetPointHistory() []PointHistory {
func (gm *GuildMember) AddPointHistory(date time.Time, modifiedBy string, points float64, comment string) {
gm.mu.Lock()
defer gm.mu.Unlock()
// Limit history size
if len(gm.PointHistory) >= MaxPointHistory {
// Remove oldest entry
gm.PointHistory = gm.PointHistory[1:]
}
history := PointHistory{
Date: date,
ModifiedBy: modifiedBy,
@ -352,7 +352,7 @@ func (gm *GuildMember) AddPointHistory(date time.Time, modifiedBy string, points
Comment: comment,
SaveNeeded: true,
}
gm.PointHistory = append(gm.PointHistory, history)
}
@ -360,7 +360,7 @@ func (gm *GuildMember) AddPointHistory(date time.Time, modifiedBy string, points
func (gm *GuildMember) GetMemberInfo(rankName string, isOnline bool) MemberInfo {
gm.mu.RLock()
defer gm.mu.RUnlock()
return MemberInfo{
CharacterID: gm.CharacterID,
Name: gm.Name,
@ -385,15 +385,15 @@ func (gm *GuildMember) GetMemberInfo(rankName string, isOnline bool) MemberInfo
func (gm *GuildMember) GetRecruiterInfo(isOnline bool) RecruiterInfo {
gm.mu.RLock()
defer gm.mu.RUnlock()
return RecruiterInfo{
CharacterID: gm.CharacterID,
Name: gm.Name,
Description: gm.RecruiterDescription,
PictureData: gm.GetRecruiterPictureData(), // This will make a copy
Name: gm.Name,
Description: gm.RecruiterDescription,
PictureData: gm.GetRecruiterPictureData(), // This will make a copy
ShowAdventureClass: gm.RecruitingShowAdventureClass != 0,
AdventureClass: gm.AdventureClass,
IsOnline: isOnline,
AdventureClass: gm.AdventureClass,
IsOnline: isOnline,
}
}
@ -401,13 +401,13 @@ func (gm *GuildMember) GetRecruiterInfo(isOnline bool) RecruiterInfo {
func (gm *GuildMember) UpdatePlayerInfo(playerInfo PlayerInfo) {
gm.mu.Lock()
defer gm.mu.Unlock()
gm.AdventureLevel = playerInfo.AdventureLevel
gm.AdventureClass = playerInfo.AdventureClass
gm.TradeskillLevel = playerInfo.TradeskillLevel
gm.TradeskillClass = playerInfo.TradeskillClass
gm.Zone = playerInfo.Zone
if playerInfo.IsOnline {
gm.LastLoginDate = time.Now()
}
@ -424,7 +424,7 @@ func (gm *GuildMember) ValidateRank() bool {
func (gm *GuildMember) CanPromote(targetRank int8) bool {
gm.mu.RLock()
defer gm.mu.RUnlock()
// Can only promote members with lower rank (higher rank number)
// Cannot promote to same or higher rank than self
return gm.Rank < targetRank && targetRank > RankLeader
@ -434,7 +434,7 @@ func (gm *GuildMember) CanPromote(targetRank int8) bool {
func (gm *GuildMember) CanDemote(targetRank int8) bool {
gm.mu.RLock()
defer gm.mu.RUnlock()
// Can only demote members with equal or lower rank (same or higher rank number)
// Cannot demote to recruit (already lowest)
return gm.Rank <= targetRank && targetRank < RankRecruit
@ -444,7 +444,7 @@ func (gm *GuildMember) CanDemote(targetRank int8) bool {
func (gm *GuildMember) CanKick(targetRank int8) bool {
gm.mu.RLock()
defer gm.mu.RUnlock()
// Can only kick members with lower rank (higher rank number)
return gm.Rank < targetRank
}
@ -453,38 +453,38 @@ func (gm *GuildMember) CanKick(targetRank int8) bool {
func (gm *GuildMember) Copy() *GuildMember {
gm.mu.RLock()
defer gm.mu.RUnlock()
newMember := &GuildMember{
CharacterID: gm.CharacterID,
AccountID: gm.AccountID,
RecruiterID: gm.RecruiterID,
Name: gm.Name,
GuildStatus: gm.GuildStatus,
Points: gm.Points,
AdventureClass: gm.AdventureClass,
AdventureLevel: gm.AdventureLevel,
TradeskillClass: gm.TradeskillClass,
TradeskillLevel: gm.TradeskillLevel,
Rank: gm.Rank,
MemberFlags: gm.MemberFlags,
Zone: gm.Zone,
JoinDate: gm.JoinDate,
LastLoginDate: gm.LastLoginDate,
Note: gm.Note,
OfficerNote: gm.OfficerNote,
RecruiterDescription: gm.RecruiterDescription,
Name: gm.Name,
GuildStatus: gm.GuildStatus,
Points: gm.Points,
AdventureClass: gm.AdventureClass,
AdventureLevel: gm.AdventureLevel,
TradeskillClass: gm.TradeskillClass,
TradeskillLevel: gm.TradeskillLevel,
Rank: gm.Rank,
MemberFlags: gm.MemberFlags,
Zone: gm.Zone,
JoinDate: gm.JoinDate,
LastLoginDate: gm.LastLoginDate,
Note: gm.Note,
OfficerNote: gm.OfficerNote,
RecruiterDescription: gm.RecruiterDescription,
RecruitingShowAdventureClass: gm.RecruitingShowAdventureClass,
PointHistory: make([]PointHistory, len(gm.PointHistory)),
PointHistory: make([]PointHistory, len(gm.PointHistory)),
}
// Deep copy point history
copy(newMember.PointHistory, gm.PointHistory)
// Deep copy picture data
if gm.RecruiterPictureData != nil {
newMember.RecruiterPictureData = make([]byte, len(gm.RecruiterPictureData))
copy(newMember.RecruiterPictureData, gm.RecruiterPictureData)
}
return newMember
}
}

View File

@ -17,27 +17,27 @@ type PointHistory struct {
// GuildMember represents a member of a guild
type GuildMember struct {
mu sync.RWMutex
CharacterID int32 `json:"character_id" db:"character_id"`
AccountID int32 `json:"account_id" db:"account_id"`
RecruiterID int32 `json:"recruiter_id" db:"recruiter_id"`
Name string `json:"name" db:"name"`
GuildStatus int32 `json:"guild_status" db:"guild_status"`
Points float64 `json:"points" db:"points"`
AdventureClass int8 `json:"adventure_class" db:"adventure_class"`
AdventureLevel int8 `json:"adventure_level" db:"adventure_level"`
TradeskillClass int8 `json:"tradeskill_class" db:"tradeskill_class"`
TradeskillLevel int8 `json:"tradeskill_level" db:"tradeskill_level"`
Rank int8 `json:"rank" db:"rank"`
MemberFlags int8 `json:"member_flags" db:"member_flags"`
Zone string `json:"zone" db:"zone"`
JoinDate time.Time `json:"join_date" db:"join_date"`
LastLoginDate time.Time `json:"last_login_date" db:"last_login_date"`
Note string `json:"note" db:"note"`
OfficerNote string `json:"officer_note" db:"officer_note"`
RecruiterDescription string `json:"recruiter_description" db:"recruiter_description"`
RecruiterPictureData []byte `json:"recruiter_picture_data" db:"recruiter_picture_data"`
RecruitingShowAdventureClass int8 `json:"recruiting_show_adventure_class" db:"recruiting_show_adventure_class"`
PointHistory []PointHistory `json:"point_history" db:"-"`
CharacterID int32 `json:"character_id" db:"character_id"`
AccountID int32 `json:"account_id" db:"account_id"`
RecruiterID int32 `json:"recruiter_id" db:"recruiter_id"`
Name string `json:"name" db:"name"`
GuildStatus int32 `json:"guild_status" db:"guild_status"`
Points float64 `json:"points" db:"points"`
AdventureClass int8 `json:"adventure_class" db:"adventure_class"`
AdventureLevel int8 `json:"adventure_level" db:"adventure_level"`
TradeskillClass int8 `json:"tradeskill_class" db:"tradeskill_class"`
TradeskillLevel int8 `json:"tradeskill_level" db:"tradeskill_level"`
Rank int8 `json:"rank" db:"rank"`
MemberFlags int8 `json:"member_flags" db:"member_flags"`
Zone string `json:"zone" db:"zone"`
JoinDate time.Time `json:"join_date" db:"join_date"`
LastLoginDate time.Time `json:"last_login_date" db:"last_login_date"`
Note string `json:"note" db:"note"`
OfficerNote string `json:"officer_note" db:"officer_note"`
RecruiterDescription string `json:"recruiter_description" db:"recruiter_description"`
RecruiterPictureData []byte `json:"recruiter_picture_data" db:"recruiter_picture_data"`
RecruitingShowAdventureClass int8 `json:"recruiting_show_adventure_class" db:"recruiting_show_adventure_class"`
PointHistory []PointHistory `json:"point_history" db:"-"`
}
// GuildEvent represents an event in the guild's history
@ -60,45 +60,45 @@ type GuildBankEvent struct {
// Bank represents a guild bank with its event history
type Bank struct {
Name string `json:"name" db:"name"`
Events []GuildBankEvent `json:"events" db:"-"`
Name string `json:"name" db:"name"`
Events []GuildBankEvent `json:"events" db:"-"`
}
// Guild represents a guild with all its properties and members
type Guild struct {
mu sync.RWMutex
id int32
name string
level int8
formedDate time.Time
motd string
expCurrent int64
expToNextLevel int64
recruitingShortDesc string
recruitingFullDesc string
recruitingMinLevel int8
recruitingPlayStyle int8
members map[int32]*GuildMember
guildEvents []GuildEvent
permissions map[int8]map[int8]int8 // rank -> permission -> value
eventFilters map[int8]map[int8]int8 // event_id -> category -> value
recruitingFlags map[int8]int8
recruitingDescTags map[int8]int8
ranks map[int8]string // rank -> name
banks [4]Bank
mu sync.RWMutex
id int32
name string
level int8
formedDate time.Time
motd string
expCurrent int64
expToNextLevel int64
recruitingShortDesc string
recruitingFullDesc string
recruitingMinLevel int8
recruitingPlayStyle int8
members map[int32]*GuildMember
guildEvents []GuildEvent
permissions map[int8]map[int8]int8 // rank -> permission -> value
eventFilters map[int8]map[int8]int8 // event_id -> category -> value
recruitingFlags map[int8]int8
recruitingDescTags map[int8]int8
ranks map[int8]string // rank -> name
banks [4]Bank
// Save flags
saveNeeded bool
memberSaveNeeded bool
eventsSaveNeeded bool
ranksSaveNeeded bool
eventFiltersSaveNeeded bool
pointsHistorySaveNeeded bool
recruitingSaveNeeded bool
saveNeeded bool
memberSaveNeeded bool
eventsSaveNeeded bool
ranksSaveNeeded bool
eventFiltersSaveNeeded bool
pointsHistorySaveNeeded bool
recruitingSaveNeeded bool
// Tracking
nextEventID int64
lastModified time.Time
nextEventID int64
lastModified time.Time
}
// GuildData represents guild data for database operations
@ -286,38 +286,38 @@ type GuildEventInfo struct {
// GuildSearchCriteria represents guild search parameters
type GuildSearchCriteria struct {
NamePattern string `json:"name_pattern"`
MinLevel int8 `json:"min_level"`
MaxLevel int8 `json:"max_level"`
MinMembers int `json:"min_members"`
MaxMembers int `json:"max_members"`
RecruitingOnly bool `json:"recruiting_only"`
PlayStyle int8 `json:"play_style"`
RequiredFlags []int8 `json:"required_flags"`
RequiredDescTags []int8 `json:"required_desc_tags"`
ExcludedDescTags []int8 `json:"excluded_desc_tags"`
NamePattern string `json:"name_pattern"`
MinLevel int8 `json:"min_level"`
MaxLevel int8 `json:"max_level"`
MinMembers int `json:"min_members"`
MaxMembers int `json:"max_members"`
RecruitingOnly bool `json:"recruiting_only"`
PlayStyle int8 `json:"play_style"`
RequiredFlags []int8 `json:"required_flags"`
RequiredDescTags []int8 `json:"required_desc_tags"`
ExcludedDescTags []int8 `json:"excluded_desc_tags"`
}
// RecruitingInfo provides detailed recruiting information
type RecruitingInfo struct {
GuildID int32 `json:"guild_id"`
GuildName string `json:"guild_name"`
ShortDesc string `json:"short_desc"`
FullDesc string `json:"full_desc"`
MinLevel int8 `json:"min_level"`
PlayStyle int8 `json:"play_style"`
Flags map[int8]int8 `json:"flags"`
DescTags map[int8]int8 `json:"desc_tags"`
Recruiters []RecruiterInfo `json:"recruiters"`
GuildID int32 `json:"guild_id"`
GuildName string `json:"guild_name"`
ShortDesc string `json:"short_desc"`
FullDesc string `json:"full_desc"`
MinLevel int8 `json:"min_level"`
PlayStyle int8 `json:"play_style"`
Flags map[int8]int8 `json:"flags"`
DescTags map[int8]int8 `json:"desc_tags"`
Recruiters []RecruiterInfo `json:"recruiters"`
}
// RecruiterInfo provides recruiter information
type RecruiterInfo struct {
CharacterID int32 `json:"character_id"`
Name string `json:"name"`
Description string `json:"description"`
PictureData []byte `json:"picture_data"`
ShowAdventureClass bool `json:"show_adventure_class"`
AdventureClass int8 `json:"adventure_class"`
IsOnline bool `json:"is_online"`
}
CharacterID int32 `json:"character_id"`
Name string `json:"name"`
Description string `json:"description"`
PictureData []byte `json:"picture_data"`
ShowAdventureClass bool `json:"show_adventure_class"`
AdventureClass int8 `json:"adventure_class"`
IsOnline bool `json:"is_online"`
}

View File

@ -4,55 +4,55 @@ package heroic_ops
const (
// Maximum number of abilities in a starter chain or wheel
MaxAbilities = 6
// Special ability icon values
AbilityIconAny = 0xFFFF // Wildcard - any ability can be used
AbilityIconNone = 0 // No ability required
// Default wheel timer (in seconds)
DefaultWheelTimerSeconds = 10
// HO Types in database
HOTypeStarter = "Starter"
HOTypeWheel = "Wheel"
// Wheel order types
WheelOrderUnordered = 0 // Abilities can be completed in any order
WheelOrderOrdered = 1 // Abilities must be completed in sequence
// HO States
HOStateInactive = iota
HOStateStarterChain
HOStateWheelPhase
HOStateComplete
HOStateFailed
// Maximum number of concurrent heroic opportunities per encounter
MaxConcurrentHOs = 3
// Chance calculation constants
MinChance = 0.0
MaxChance = 100.0
// Timer constants (in milliseconds)
WheelTimerCheckInterval = 100
StarterChainTimeout = 30000 // 30 seconds
WheelPhaseTimeout = 10000 // 10 seconds (configurable)
// Special shift states
ShiftNotUsed = 0
ShiftUsed = 1
// HO completion status
HONotComplete = 0
HOComplete = 1
// Class restrictions (matches EQ2 class IDs)
ClassAny = 0 // Any class can initiate
// Packet constants
PacketHeroicOpportunity = "WS_HeroicOpportunity"
// Error messages
ErrHONotFound = "heroic opportunity not found"
ErrHOInvalidState = "heroic opportunity in invalid state"
@ -61,17 +61,17 @@ const (
ErrHOAlreadyComplete = "heroic opportunity already complete"
ErrHOShiftAlreadyUsed = "wheel shift already used"
ErrHOWheelNotFound = "no wheel found for starter"
// Database constants
MaxHONameLength = 255
MaxHODescriptionLength = 1000
MaxDatabaseRetries = 3
// Memory management
DefaultHOPoolSize = 100
DefaultStarterCache = 500
DefaultWheelCache = 1000
MaxHOHistoryEntries = 50
DefaultHOPoolSize = 100
DefaultStarterCache = 500
DefaultWheelCache = 1000
MaxHOHistoryEntries = 50
)
// Heroic Opportunity Event Types for logging and statistics
@ -141,4 +141,4 @@ var ClassNames = map[int8]string{
38: "Predator",
39: "Ranger",
40: "Assassin",
}
}

View File

@ -22,11 +22,11 @@ func NewDatabaseHeroicOPManager(db *database.DB) *DatabaseHeroicOPManager {
// LoadStarters retrieves all starters from database
func (dhom *DatabaseHeroicOPManager) LoadStarters(ctx context.Context) ([]HeroicOPData, error) {
query := `SELECT id, ho_type, starter_class, starter_icon, 0 as starter_link_id,
query := `SELECT id, ho_type, starter_class, starter_icon, 0 as starter_link_id,
0 as chain_order, 0 as shift_icon, 0 as spell_id, 0.0 as chance,
ability1, ability2, ability3, ability4, ability5, ability6,
name, description FROM heroic_ops WHERE ho_type = ?`
rows, err := dhom.db.QueryContext(ctx, query, HOTypeStarter)
if err != nil {
return nil, fmt.Errorf("failed to query heroic op starters: %w", err)
@ -85,7 +85,7 @@ func (dhom *DatabaseHeroicOPManager) LoadStarter(ctx context.Context, starterID
0 as chain_order, 0 as shift_icon, 0 as spell_id, 0.0 as chance,
ability1, ability2, ability3, ability4, ability5, ability6,
name, description FROM heroic_ops WHERE id = ? AND ho_type = ?`
var starter HeroicOPData
var name, description *string
@ -129,7 +129,7 @@ func (dhom *DatabaseHeroicOPManager) LoadWheels(ctx context.Context) ([]HeroicOP
chain_order, shift_icon, spell_id, chance,
ability1, ability2, ability3, ability4, ability5, ability6,
name, description FROM heroic_ops WHERE ho_type = ?`
rows, err := dhom.db.QueryContext(ctx, query, HOTypeWheel)
if err != nil {
return nil, fmt.Errorf("failed to query heroic op wheels: %w", err)
@ -188,7 +188,7 @@ func (dhom *DatabaseHeroicOPManager) LoadWheelsForStarter(ctx context.Context, s
chain_order, shift_icon, spell_id, chance,
ability1, ability2, ability3, ability4, ability5, ability6,
name, description FROM heroic_ops WHERE starter_link_id = ? AND ho_type = ?`
rows, err := dhom.db.QueryContext(ctx, query, starterID, HOTypeWheel)
if err != nil {
return nil, fmt.Errorf("failed to query wheels for starter %d: %w", starterID, err)
@ -247,7 +247,7 @@ func (dhom *DatabaseHeroicOPManager) LoadWheel(ctx context.Context, wheelID int3
chain_order, shift_icon, spell_id, chance,
ability1, ability2, ability3, ability4, ability5, ability6,
name, description FROM heroic_ops WHERE id = ? AND ho_type = ?`
var wheel HeroicOPData
var name, description *string
@ -287,10 +287,10 @@ func (dhom *DatabaseHeroicOPManager) LoadWheel(ctx context.Context, wheelID int3
// SaveStarter saves a heroic op starter
func (dhom *DatabaseHeroicOPManager) SaveStarter(ctx context.Context, starter *HeroicOPStarter) error {
query := `INSERT OR REPLACE INTO heroic_ops
(id, ho_type, starter_class, starter_icon, starter_link_id, chain_order,
shift_icon, spell_id, chance, ability1, ability2, ability3, ability4,
ability5, ability6, name, description)
query := `INSERT OR REPLACE INTO heroic_ops
(id, ho_type, starter_class, starter_icon, starter_link_id, chain_order,
shift_icon, spell_id, chance, ability1, ability2, ability3, ability4,
ability5, ability6, name, description)
VALUES (?, ?, ?, ?, 0, 0, 0, 0, 0.0, ?, ?, ?, ?, ?, ?, ?, ?)`
_, err := dhom.db.ExecContext(ctx, query,
@ -316,10 +316,10 @@ func (dhom *DatabaseHeroicOPManager) SaveStarter(ctx context.Context, starter *H
// SaveWheel saves a heroic op wheel
func (dhom *DatabaseHeroicOPManager) SaveWheel(ctx context.Context, wheel *HeroicOPWheel) error {
query := `INSERT OR REPLACE INTO heroic_ops
(id, ho_type, starter_class, starter_icon, starter_link_id, chain_order,
shift_icon, spell_id, chance, ability1, ability2, ability3, ability4,
ability5, ability6, name, description)
query := `INSERT OR REPLACE INTO heroic_ops
(id, ho_type, starter_class, starter_icon, starter_link_id, chain_order,
shift_icon, spell_id, chance, ability1, ability2, ability3, ability4,
ability5, ability6, name, description)
VALUES (?, ?, 0, 0, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
_, err := dhom.db.ExecContext(ctx, query,
@ -356,14 +356,14 @@ func (dhom *DatabaseHeroicOPManager) DeleteStarter(ctx context.Context, starterI
defer tx.Rollback()
// Delete associated wheels first
_, err = tx.ExecContext(ctx, "DELETE FROM heroic_ops WHERE starter_link_id = ? AND ho_type = ?",
_, err = tx.ExecContext(ctx, "DELETE FROM heroic_ops WHERE starter_link_id = ? AND ho_type = ?",
starterID, HOTypeWheel)
if err != nil {
return fmt.Errorf("failed to delete wheels for starter %d: %w", starterID, err)
}
// Delete the starter
_, err = tx.ExecContext(ctx, "DELETE FROM heroic_ops WHERE id = ? AND ho_type = ?",
_, err = tx.ExecContext(ctx, "DELETE FROM heroic_ops WHERE id = ? AND ho_type = ?",
starterID, HOTypeStarter)
if err != nil {
return fmt.Errorf("failed to delete starter %d: %w", starterID, err)
@ -378,7 +378,7 @@ func (dhom *DatabaseHeroicOPManager) DeleteStarter(ctx context.Context, starterI
// DeleteWheel removes a wheel from database
func (dhom *DatabaseHeroicOPManager) DeleteWheel(ctx context.Context, wheelID int32) error {
_, err := dhom.db.ExecContext(ctx, "DELETE FROM heroic_ops WHERE id = ? AND ho_type = ?",
_, err := dhom.db.ExecContext(ctx, "DELETE FROM heroic_ops WHERE id = ? AND ho_type = ?",
wheelID, HOTypeWheel)
if err != nil {
return fmt.Errorf("failed to delete wheel %d: %w", wheelID, err)
@ -389,7 +389,7 @@ func (dhom *DatabaseHeroicOPManager) DeleteWheel(ctx context.Context, wheelID in
// SaveHOInstance saves a heroic opportunity instance
func (dhom *DatabaseHeroicOPManager) SaveHOInstance(ctx context.Context, ho *HeroicOP) error {
query := `INSERT OR REPLACE INTO heroic_op_instances
query := `INSERT OR REPLACE INTO heroic_op_instances
(id, encounter_id, starter_id, wheel_id, state, start_time, wheel_start_time,
time_remaining, total_time, complete, countered_1, countered_2, countered_3,
countered_4, countered_5, countered_6, shift_used, starter_progress,
@ -499,7 +499,7 @@ func (dhom *DatabaseHeroicOPManager) DeleteHOInstance(ctx context.Context, insta
// SaveHOEvent saves a heroic opportunity event
func (dhom *DatabaseHeroicOPManager) SaveHOEvent(ctx context.Context, event *HeroicOPEvent) error {
query := `INSERT INTO heroic_op_events
query := `INSERT INTO heroic_op_events
(id, instance_id, event_type, character_id, ability_icon, timestamp, data)
VALUES (?, ?, ?, ?, ?, ?, ?)`
@ -574,7 +574,7 @@ func (dhom *DatabaseHeroicOPManager) GetHOStatistics(ctx context.Context, charac
}
// Count total HOs started by this character
query := `SELECT COUNT(*) FROM heroic_op_events
query := `SELECT COUNT(*) FROM heroic_op_events
WHERE character_id = ? AND event_type = ?`
err := dhom.db.QueryRowContext(ctx, query, characterID, EventHOStarted).Scan(&stats.TotalHOsStarted)
if err != nil {
@ -582,7 +582,7 @@ func (dhom *DatabaseHeroicOPManager) GetHOStatistics(ctx context.Context, charac
}
// Count total HOs completed by this character
query = `SELECT COUNT(*) FROM heroic_op_events
query = `SELECT COUNT(*) FROM heroic_op_events
WHERE character_id = ? AND event_type = ?`
err = dhom.db.QueryRowContext(ctx, query, characterID, EventHOCompleted).Scan(&stats.TotalHOsCompleted)
if err != nil {
@ -602,7 +602,7 @@ func (dhom *DatabaseHeroicOPManager) GetHOStatistics(ctx context.Context, charac
// GetNextStarterID returns the next available starter ID
func (dhom *DatabaseHeroicOPManager) GetNextStarterID(ctx context.Context) (int32, error) {
query := "SELECT COALESCE(MAX(id), 0) + 1 FROM heroic_ops WHERE ho_type = ?"
var nextID int32
err := dhom.db.QueryRowContext(ctx, query, HOTypeStarter).Scan(&nextID)
if err != nil {
@ -615,7 +615,7 @@ func (dhom *DatabaseHeroicOPManager) GetNextStarterID(ctx context.Context) (int3
// GetNextWheelID returns the next available wheel ID
func (dhom *DatabaseHeroicOPManager) GetNextWheelID(ctx context.Context) (int32, error) {
query := "SELECT COALESCE(MAX(id), 0) + 1 FROM heroic_ops WHERE ho_type = ?"
var nextID int32
err := dhom.db.QueryRowContext(ctx, query, HOTypeWheel).Scan(&nextID)
if err != nil {
@ -628,7 +628,7 @@ func (dhom *DatabaseHeroicOPManager) GetNextWheelID(ctx context.Context) (int32,
// GetNextInstanceID returns the next available instance ID
func (dhom *DatabaseHeroicOPManager) GetNextInstanceID(ctx context.Context) (int64, error) {
query := "SELECT COALESCE(MAX(id), 0) + 1 FROM heroic_op_instances"
var nextID int64
err := dhom.db.QueryRowContext(ctx, query).Scan(&nextID)
if err != nil {
@ -727,4 +727,4 @@ func (dhom *DatabaseHeroicOPManager) EnsureHOTables(ctx context.Context) error {
}
return nil
}
}

View File

@ -3,7 +3,6 @@ package heroic_ops
import (
"fmt"
"math/rand"
"sync"
"time"
)
@ -22,7 +21,7 @@ func NewHeroicOPStarter(id int32, startClass int8, starterIcon int16) *HeroicOPS
func (hos *HeroicOPStarter) Copy() *HeroicOPStarter {
hos.mu.RLock()
defer hos.mu.RUnlock()
newStarter := &HeroicOPStarter{
ID: hos.ID,
StartClass: hos.StartClass,
@ -32,7 +31,7 @@ func (hos *HeroicOPStarter) Copy() *HeroicOPStarter {
Description: hos.Description,
SaveNeeded: false,
}
return newStarter
}
@ -40,11 +39,11 @@ func (hos *HeroicOPStarter) Copy() *HeroicOPStarter {
func (hos *HeroicOPStarter) GetAbility(position int) int16 {
hos.mu.RLock()
defer hos.mu.RUnlock()
if position < 0 || position >= MaxAbilities {
return AbilityIconNone
}
return hos.Abilities[position]
}
@ -52,11 +51,11 @@ func (hos *HeroicOPStarter) GetAbility(position int) int16 {
func (hos *HeroicOPStarter) SetAbility(position int, abilityIcon int16) bool {
hos.mu.Lock()
defer hos.mu.Unlock()
if position < 0 || position >= MaxAbilities {
return false
}
hos.Abilities[position] = abilityIcon
hos.SaveNeeded = true
return true
@ -66,11 +65,11 @@ func (hos *HeroicOPStarter) SetAbility(position int, abilityIcon int16) bool {
func (hos *HeroicOPStarter) IsComplete(position int) bool {
hos.mu.RLock()
defer hos.mu.RUnlock()
if position < 0 || position >= MaxAbilities {
return false
}
return hos.Abilities[position] == AbilityIconAny
}
@ -78,7 +77,7 @@ func (hos *HeroicOPStarter) IsComplete(position int) bool {
func (hos *HeroicOPStarter) CanInitiate(playerClass int8) bool {
hos.mu.RLock()
defer hos.mu.RUnlock()
return hos.StartClass == ClassAny || hos.StartClass == playerClass
}
@ -86,18 +85,18 @@ func (hos *HeroicOPStarter) CanInitiate(playerClass int8) bool {
func (hos *HeroicOPStarter) MatchesAbility(position int, abilityIcon int16) bool {
hos.mu.RLock()
defer hos.mu.RUnlock()
if position < 0 || position >= MaxAbilities {
return false
}
requiredAbility := hos.Abilities[position]
// Wildcard matches any ability
if requiredAbility == AbilityIconAny {
return true
}
// Exact match required
return requiredAbility == abilityIcon
}
@ -106,15 +105,15 @@ func (hos *HeroicOPStarter) MatchesAbility(position int, abilityIcon int16) bool
func (hos *HeroicOPStarter) Validate() error {
hos.mu.RLock()
defer hos.mu.RUnlock()
if hos.ID <= 0 {
return fmt.Errorf("invalid starter ID: %d", hos.ID)
}
if hos.StarterIcon <= 0 {
return fmt.Errorf("invalid starter icon: %d", hos.StarterIcon)
}
// Check for at least one non-zero ability
hasAbility := false
for _, ability := range hos.Abilities {
@ -123,11 +122,11 @@ func (hos *HeroicOPStarter) Validate() error {
break
}
}
if !hasAbility {
return fmt.Errorf("starter must have at least one ability")
}
return nil
}
@ -147,7 +146,7 @@ func NewHeroicOPWheel(id int32, starterLinkID int32, order int8) *HeroicOPWheel
func (how *HeroicOPWheel) Copy() *HeroicOPWheel {
how.mu.RLock()
defer how.mu.RUnlock()
newWheel := &HeroicOPWheel{
ID: how.ID,
StarterLinkID: how.StarterLinkID,
@ -161,7 +160,7 @@ func (how *HeroicOPWheel) Copy() *HeroicOPWheel {
RequiredPlayers: how.RequiredPlayers,
SaveNeeded: false,
}
return newWheel
}
@ -169,11 +168,11 @@ func (how *HeroicOPWheel) Copy() *HeroicOPWheel {
func (how *HeroicOPWheel) GetAbility(position int) int16 {
how.mu.RLock()
defer how.mu.RUnlock()
if position < 0 || position >= MaxAbilities {
return AbilityIconNone
}
return how.Abilities[position]
}
@ -181,11 +180,11 @@ func (how *HeroicOPWheel) GetAbility(position int) int16 {
func (how *HeroicOPWheel) SetAbility(position int, abilityIcon int16) bool {
how.mu.Lock()
defer how.mu.Unlock()
if position < 0 || position >= MaxAbilities {
return false
}
how.Abilities[position] = abilityIcon
how.SaveNeeded = true
return true
@ -195,7 +194,7 @@ func (how *HeroicOPWheel) SetAbility(position int, abilityIcon int16) bool {
func (how *HeroicOPWheel) IsOrdered() bool {
how.mu.RLock()
defer how.mu.RUnlock()
return how.Order >= WheelOrderOrdered
}
@ -203,7 +202,7 @@ func (how *HeroicOPWheel) IsOrdered() bool {
func (how *HeroicOPWheel) HasShift() bool {
how.mu.RLock()
defer how.mu.RUnlock()
return how.ShiftIcon > 0
}
@ -211,7 +210,7 @@ func (how *HeroicOPWheel) HasShift() bool {
func (how *HeroicOPWheel) CanShift(abilityIcon int16) bool {
how.mu.RLock()
defer how.mu.RUnlock()
return how.ShiftIcon > 0 && how.ShiftIcon == abilityIcon
}
@ -219,18 +218,18 @@ func (how *HeroicOPWheel) CanShift(abilityIcon int16) bool {
func (how *HeroicOPWheel) GetNextRequiredAbility(countered [6]int8) int16 {
how.mu.RLock()
defer how.mu.RUnlock()
if !how.IsOrdered() {
return AbilityIconNone // Any uncompleted ability works for unordered
}
// Find first uncompleted ability in order
for i := 0; i < MaxAbilities; i++ {
if countered[i] == 0 && how.Abilities[i] != AbilityIconNone {
return how.Abilities[i]
}
}
return AbilityIconNone
}
@ -238,12 +237,12 @@ func (how *HeroicOPWheel) GetNextRequiredAbility(countered [6]int8) int16 {
func (how *HeroicOPWheel) CanUseAbility(abilityIcon int16, countered [6]int8) bool {
how.mu.RLock()
defer how.mu.RUnlock()
// Check if this is a shift attempt
if how.CanShift(abilityIcon) {
return true
}
if how.IsOrdered() {
// For ordered wheels, only the next required ability can be used
nextRequired := how.GetNextRequiredAbility(countered)
@ -256,7 +255,7 @@ func (how *HeroicOPWheel) CanUseAbility(abilityIcon int16, countered [6]int8) bo
}
}
}
return false
}
@ -264,23 +263,23 @@ func (how *HeroicOPWheel) CanUseAbility(abilityIcon int16, countered [6]int8) bo
func (how *HeroicOPWheel) Validate() error {
how.mu.RLock()
defer how.mu.RUnlock()
if how.ID <= 0 {
return fmt.Errorf("invalid wheel ID: %d", how.ID)
}
if how.StarterLinkID <= 0 {
return fmt.Errorf("invalid starter link ID: %d", how.StarterLinkID)
}
if how.Chance < MinChance || how.Chance > MaxChance {
return fmt.Errorf("invalid chance: %f (must be %f-%f)", how.Chance, MinChance, MaxChance)
}
if how.SpellID <= 0 {
return fmt.Errorf("invalid spell ID: %d", how.SpellID)
}
// Check for at least one non-zero ability
hasAbility := false
for _, ability := range how.Abilities {
@ -289,11 +288,11 @@ func (how *HeroicOPWheel) Validate() error {
break
}
}
if !hasAbility {
return fmt.Errorf("wheel must have at least one ability")
}
return nil
}
@ -316,7 +315,7 @@ func NewHeroicOP(instanceID int64, encounterID int32) *HeroicOP {
func (ho *HeroicOP) AddParticipant(characterID int32) {
ho.mu.Lock()
defer ho.mu.Unlock()
ho.Participants[characterID] = true
ho.SaveNeeded = true
}
@ -325,7 +324,7 @@ func (ho *HeroicOP) AddParticipant(characterID int32) {
func (ho *HeroicOP) RemoveParticipant(characterID int32) {
ho.mu.Lock()
defer ho.mu.Unlock()
delete(ho.Participants, characterID)
ho.SaveNeeded = true
}
@ -334,7 +333,7 @@ func (ho *HeroicOP) RemoveParticipant(characterID int32) {
func (ho *HeroicOP) IsParticipant(characterID int32) bool {
ho.mu.RLock()
defer ho.mu.RUnlock()
return ho.Participants[characterID]
}
@ -342,12 +341,12 @@ func (ho *HeroicOP) IsParticipant(characterID int32) bool {
func (ho *HeroicOP) GetParticipants() []int32 {
ho.mu.RLock()
defer ho.mu.RUnlock()
participants := make([]int32, 0, len(ho.Participants))
for characterID := range ho.Participants {
participants = append(participants, characterID)
}
return participants
}
@ -355,7 +354,7 @@ func (ho *HeroicOP) GetParticipants() []int32 {
func (ho *HeroicOP) StartStarterChain(availableStarters []int32) {
ho.mu.Lock()
defer ho.mu.Unlock()
ho.State = HOStateStarterChain
ho.CurrentStarters = make([]int32, len(availableStarters))
copy(ho.CurrentStarters, availableStarters)
@ -368,14 +367,14 @@ func (ho *HeroicOP) StartStarterChain(availableStarters []int32) {
func (ho *HeroicOP) ProcessStarterAbility(abilityIcon int16, masterList *MasterHeroicOPList) bool {
ho.mu.Lock()
defer ho.mu.Unlock()
if ho.State != HOStateStarterChain {
return false
}
// Filter out starters that don't match this ability at current position
newStarters := make([]int32, 0)
for _, starterID := range ho.CurrentStarters {
starter := masterList.GetStarter(starterID)
if starter != nil && starter.MatchesAbility(int(ho.StarterProgress), abilityIcon) {
@ -389,11 +388,11 @@ func (ho *HeroicOP) ProcessStarterAbility(abilityIcon int16, masterList *MasterH
newStarters = append(newStarters, starterID)
}
}
ho.CurrentStarters = newStarters
ho.StarterProgress++
ho.SaveNeeded = true
// If no starters remain, HO fails
return len(ho.CurrentStarters) > 0
}
@ -402,7 +401,7 @@ func (ho *HeroicOP) ProcessStarterAbility(abilityIcon int16, masterList *MasterH
func (ho *HeroicOP) StartWheelPhase(wheel *HeroicOPWheel, timerSeconds int32) {
ho.mu.Lock()
defer ho.mu.Unlock()
ho.State = HOStateWheelPhase
ho.WheelID = wheel.ID
ho.WheelStartTime = time.Now()
@ -410,12 +409,12 @@ func (ho *HeroicOP) StartWheelPhase(wheel *HeroicOPWheel, timerSeconds int32) {
ho.TimeRemaining = ho.TotalTime
ho.SpellName = wheel.Name
ho.SpellDescription = wheel.Description
// Clear countered array
for i := range ho.Countered {
ho.Countered[i] = 0
}
ho.SaveNeeded = true
}
@ -423,11 +422,11 @@ func (ho *HeroicOP) StartWheelPhase(wheel *HeroicOPWheel, timerSeconds int32) {
func (ho *HeroicOP) ProcessWheelAbility(abilityIcon int16, characterID int32, wheel *HeroicOPWheel) bool {
ho.mu.Lock()
defer ho.mu.Unlock()
if ho.State != HOStateWheelPhase {
return false
}
// Check for shift attempt
if ho.ShiftUsed == ShiftNotUsed && wheel.CanShift(abilityIcon) {
// Allow shift only if no progress made (unordered) or at start (ordered)
@ -451,7 +450,7 @@ func (ho *HeroicOP) ProcessWheelAbility(abilityIcon int16, characterID int32, wh
}
}
}
if canShift {
ho.ShiftUsed = ShiftUsed
ho.SaveNeeded = true
@ -459,19 +458,19 @@ func (ho *HeroicOP) ProcessWheelAbility(abilityIcon int16, characterID int32, wh
}
return false
}
// Check if ability can be used
if !wheel.CanUseAbility(abilityIcon, ho.Countered) {
return false
}
// Find matching ability position and mark as countered
for i := 0; i < MaxAbilities; i++ {
if ho.Countered[i] == 0 && wheel.GetAbility(i) == abilityIcon {
ho.Countered[i] = 1
ho.AddParticipant(characterID)
ho.SaveNeeded = true
// Check if wheel is complete
complete := true
for j := 0; j < MaxAbilities; j++ {
@ -480,17 +479,17 @@ func (ho *HeroicOP) ProcessWheelAbility(abilityIcon int16, characterID int32, wh
break
}
}
if complete {
ho.Complete = HOComplete
ho.State = HOStateComplete
ho.CompletedBy = characterID
}
return true
}
}
return false
}
@ -498,20 +497,20 @@ func (ho *HeroicOP) ProcessWheelAbility(abilityIcon int16, characterID int32, wh
func (ho *HeroicOP) UpdateTimer(deltaMS int32) bool {
ho.mu.Lock()
defer ho.mu.Unlock()
if ho.State != HOStateWheelPhase {
return true // Timer not active
}
ho.TimeRemaining -= deltaMS
if ho.TimeRemaining <= 0 {
ho.TimeRemaining = 0
ho.State = HOStateFailed
ho.SaveNeeded = true
return false // Timer expired
}
ho.SaveNeeded = true
return true
}
@ -520,7 +519,7 @@ func (ho *HeroicOP) UpdateTimer(deltaMS int32) bool {
func (ho *HeroicOP) IsComplete() bool {
ho.mu.RLock()
defer ho.mu.RUnlock()
return ho.Complete == HOComplete && ho.State == HOStateComplete
}
@ -528,7 +527,7 @@ func (ho *HeroicOP) IsComplete() bool {
func (ho *HeroicOP) IsFailed() bool {
ho.mu.RLock()
defer ho.mu.RUnlock()
return ho.State == HOStateFailed
}
@ -536,7 +535,7 @@ func (ho *HeroicOP) IsFailed() bool {
func (ho *HeroicOP) IsActive() bool {
ho.mu.RLock()
defer ho.mu.RUnlock()
return ho.State == HOStateStarterChain || ho.State == HOStateWheelPhase
}
@ -544,14 +543,14 @@ func (ho *HeroicOP) IsActive() bool {
func (ho *HeroicOP) GetProgress() float32 {
ho.mu.RLock()
defer ho.mu.RUnlock()
if ho.State != HOStateWheelPhase {
return 0.0
}
completed := 0
total := 0
for i := 0; i < MaxAbilities; i++ {
if ho.Countered[i] != 0 {
completed++
@ -560,11 +559,11 @@ func (ho *HeroicOP) GetProgress() float32 {
total++
}
}
if total == 0 {
return 0.0
}
return float32(completed) / float32(total)
}
@ -572,7 +571,7 @@ func (ho *HeroicOP) GetProgress() float32 {
func (ho *HeroicOP) GetPacketData(wheel *HeroicOPWheel) *PacketData {
ho.mu.RLock()
defer ho.mu.RUnlock()
data := &PacketData{
SpellName: ho.SpellName,
SpellDescription: ho.SpellDescription,
@ -583,15 +582,15 @@ func (ho *HeroicOP) GetPacketData(wheel *HeroicOPWheel) *PacketData {
CanShift: false,
ShiftIcon: 0,
}
if wheel != nil {
data.Abilities = wheel.Abilities
data.CanShift = ho.ShiftUsed == ShiftNotUsed && wheel.HasShift()
data.ShiftIcon = wheel.ShiftIcon
}
data.Countered = ho.Countered
return data
}
@ -599,29 +598,29 @@ func (ho *HeroicOP) GetPacketData(wheel *HeroicOPWheel) *PacketData {
func (ho *HeroicOP) Validate() error {
ho.mu.RLock()
defer ho.mu.RUnlock()
if ho.ID <= 0 {
return fmt.Errorf("invalid HO instance ID: %d", ho.ID)
}
if ho.EncounterID <= 0 {
return fmt.Errorf("invalid encounter ID: %d", ho.EncounterID)
}
if ho.State < HOStateInactive || ho.State > HOStateFailed {
return fmt.Errorf("invalid HO state: %d", ho.State)
}
if ho.State == HOStateWheelPhase {
if ho.WheelID <= 0 {
return fmt.Errorf("wheel phase requires valid wheel ID")
}
if ho.TotalTime <= 0 {
return fmt.Errorf("wheel phase requires valid timer")
}
}
return nil
}
@ -629,7 +628,7 @@ func (ho *HeroicOP) Validate() error {
func (ho *HeroicOP) Copy() *HeroicOP {
ho.mu.RLock()
defer ho.mu.RUnlock()
newHO := &HeroicOP{
ID: ho.ID,
EncounterID: ho.EncounterID,
@ -651,15 +650,15 @@ func (ho *HeroicOP) Copy() *HeroicOP {
CurrentStarters: make([]int32, len(ho.CurrentStarters)),
SaveNeeded: false,
}
// Deep copy participants map
for characterID, participating := range ho.Participants {
newHO.Participants[characterID] = participating
}
// Deep copy current starters slice
copy(newHO.CurrentStarters, ho.CurrentStarters)
return newHO
}
@ -670,33 +669,33 @@ func SelectRandomWheel(wheels []*HeroicOPWheel) *HeroicOPWheel {
if len(wheels) == 0 {
return nil
}
if len(wheels) == 1 {
return wheels[0]
}
// Calculate total chance
totalChance := float32(0.0)
for _, wheel := range wheels {
totalChance += wheel.Chance
}
if totalChance <= 0.0 {
// If no chances set, select randomly with equal probability
return wheels[rand.Intn(len(wheels))]
}
// Random selection based on weighted chance
randomValue := rand.Float32() * totalChance
currentChance := float32(0.0)
for _, wheel := range wheels {
currentChance += wheel.Chance
if randomValue <= currentChance {
return wheel
}
}
// Fallback to last wheel (shouldn't happen with proper math)
return wheels[len(wheels)-1]
}
@ -705,7 +704,7 @@ func SelectRandomWheel(wheels []*HeroicOPWheel) *HeroicOPWheel {
func (ho *HeroicOP) GetElapsedTime() time.Duration {
ho.mu.RLock()
defer ho.mu.RUnlock()
return time.Since(ho.StartTime)
}
@ -713,10 +712,10 @@ func (ho *HeroicOP) GetElapsedTime() time.Duration {
func (ho *HeroicOP) GetWheelElapsedTime() time.Duration {
ho.mu.RLock()
defer ho.mu.RUnlock()
if ho.State != HOStateWheelPhase {
return 0
}
return time.Since(ho.WheelStartTime)
}
}

View File

@ -12,24 +12,24 @@ type HeroicOPDatabase interface {
LoadStarter(ctx context.Context, starterID int32) (*HeroicOPData, error)
SaveStarter(ctx context.Context, starter *HeroicOPStarter) error
DeleteStarter(ctx context.Context, starterID int32) error
// Wheel operations
LoadWheels(ctx context.Context) ([]HeroicOPData, error)
LoadWheelsForStarter(ctx context.Context, starterID int32) ([]HeroicOPData, error)
LoadWheel(ctx context.Context, wheelID int32) (*HeroicOPData, error)
SaveWheel(ctx context.Context, wheel *HeroicOPWheel) error
DeleteWheel(ctx context.Context, wheelID int32) error
// Instance operations
SaveHOInstance(ctx context.Context, ho *HeroicOP) error
LoadHOInstance(ctx context.Context, instanceID int64) (*HeroicOP, error)
DeleteHOInstance(ctx context.Context, instanceID int64) error
// Statistics and events
SaveHOEvent(ctx context.Context, event *HeroicOPEvent) error
LoadHOEvents(ctx context.Context, instanceID int64) ([]HeroicOPEvent, error)
GetHOStatistics(ctx context.Context, characterID int32) (*HeroicOPStatistics, error)
// Utility operations
GetNextStarterID(ctx context.Context) (int32, error)
GetNextWheelID(ctx context.Context) (int32, error)
@ -44,13 +44,13 @@ type HeroicOPEventHandler interface {
OnHOCompleted(ho *HeroicOP, completedBy int32, spellID int32)
OnHOFailed(ho *HeroicOP, reason string)
OnHOTimerExpired(ho *HeroicOP)
// Progress events
OnAbilityUsed(ho *HeroicOP, characterID int32, abilityIcon int16, success bool)
OnWheelShifted(ho *HeroicOP, characterID int32, newWheelID int32)
OnStarterMatched(ho *HeroicOP, starterID int32, characterID int32)
OnStarterEliminated(ho *HeroicOP, starterID int32, characterID int32)
// Phase transitions
OnWheelPhaseStarted(ho *HeroicOP, wheelID int32, timeRemaining int32)
OnProgressMade(ho *HeroicOP, characterID int32, progressPercent float32)
@ -62,7 +62,7 @@ type SpellManager interface {
GetSpellInfo(spellID int32) (*SpellInfo, error)
GetSpellName(spellID int32) string
GetSpellDescription(spellID int32) string
// Cast spells
CastSpell(casterID int32, spellID int32, targets []int32) error
IsSpellValid(spellID int32) bool
@ -75,11 +75,11 @@ type ClientManager interface {
SendHOStart(characterID int32, ho *HeroicOP) error
SendHOComplete(characterID int32, ho *HeroicOP, success bool) error
SendHOTimer(characterID int32, timeRemaining int32, totalTime int32) error
// Broadcast to multiple clients
BroadcastHOUpdate(characterIDs []int32, data *PacketData) error
BroadcastHOEvent(characterIDs []int32, eventType int, data string) error
// Client validation
IsClientConnected(characterID int32) bool
GetClientVersion(characterID int32) int
@ -91,7 +91,7 @@ type EncounterManager interface {
GetEncounterParticipants(encounterID int32) ([]int32, error)
IsEncounterActive(encounterID int32) bool
GetEncounterInfo(encounterID int32) (*EncounterInfo, error)
// HO integration
CanStartHO(encounterID int32, initiatorID int32) bool
NotifyHOStarted(encounterID int32, instanceID int64)
@ -105,11 +105,11 @@ type PlayerManager interface {
GetPlayerClass(characterID int32) (int8, error)
GetPlayerLevel(characterID int32) (int16, error)
IsPlayerOnline(characterID int32) bool
// Player abilities
CanPlayerUseAbility(characterID int32, abilityIcon int16) bool
GetPlayerAbilities(characterID int32) ([]int16, error)
// Player state
IsPlayerInCombat(characterID int32) bool
GetPlayerEncounter(characterID int32) (int32, error)
@ -130,7 +130,7 @@ type TimerManager interface {
StopTimer(instanceID int64) error
UpdateTimer(instanceID int64, newDuration time.Duration) error
GetTimeRemaining(instanceID int64) (time.Duration, error)
// Timer queries
IsTimerActive(instanceID int64) bool
GetActiveTimers() []int64
@ -143,7 +143,7 @@ type CacheManager interface {
Get(key string) (interface{}, bool)
Delete(key string) error
Clear() error
// Cache statistics
GetHitRate() float64
GetSize() int
@ -154,12 +154,12 @@ type CacheManager interface {
// EncounterInfo contains encounter details
type EncounterInfo struct {
ID int32 `json:"id"`
Name string `json:"name"`
Participants []int32 `json:"participants"`
IsActive bool `json:"is_active"`
ID int32 `json:"id"`
Name string `json:"name"`
Participants []int32 `json:"participants"`
IsActive bool `json:"is_active"`
StartTime time.Time `json:"start_time"`
Level int16 `json:"level"`
Level int16 `json:"level"`
}
// PlayerInfo contains player details needed for HO system
@ -215,4 +215,4 @@ type ConfigManager interface {
UpdateHOConfig(config *HeroicOPConfig) error
GetConfigValue(key string) interface{}
SetConfigValue(key string, value interface{}) error
}
}

View File

@ -3,7 +3,6 @@ package heroic_ops
import (
"context"
"fmt"
"sync"
"time"
)
@ -68,7 +67,7 @@ func (hom *HeroicOPManager) StartHeroicOpportunity(ctx context.Context, encounte
// Check if encounter can have more HOs
currentHOs := hom.encounterHOs[encounterID]
if len(currentHOs) >= hom.maxConcurrentHOs {
return nil, fmt.Errorf("encounter %d already has maximum concurrent HOs (%d)",
return nil, fmt.Errorf("encounter %d already has maximum concurrent HOs (%d)",
encounterID, hom.maxConcurrentHOs)
}
@ -153,7 +152,7 @@ func (hom *HeroicOPManager) ProcessAbility(ctx context.Context, instanceID int64
// Starter chain completed, transition to wheel phase
starterID := ho.CurrentStarters[0]
ho.StarterID = starterID
// Select random wheel for this starter
wheel := hom.masterList.SelectRandomWheel(starterID)
if wheel == nil {
@ -193,7 +192,7 @@ func (hom *HeroicOPManager) ProcessAbility(ctx context.Context, instanceID int64
// Process regular ability
success = ho.ProcessWheelAbility(abilityIcon, characterID, wheel)
if success {
// Send progress update
hom.sendProgressUpdate(ho)
@ -215,7 +214,7 @@ func (hom *HeroicOPManager) ProcessAbility(ctx context.Context, instanceID int64
// Notify event handler
if hom.eventHandler != nil {
hom.eventHandler.OnAbilityUsed(ho, characterID, abilityIcon, success)
if success {
progress := ho.GetProgress()
hom.eventHandler.OnProgressMade(ho, characterID, progress)
@ -301,7 +300,7 @@ func (hom *HeroicOPManager) CleanupExpiredHOs(ctx context.Context, maxAge time.D
for _, instanceID := range toRemove {
ho := hom.activeHOs[instanceID]
// Remove from encounter tracking
encounterHOs := hom.encounterHOs[ho.EncounterID]
for i, encounterHO := range encounterHOs {
@ -426,7 +425,7 @@ func (hom *HeroicOPManager) completeHO(ctx context.Context, ho *HeroicOP, wheel
// Log completion
if hom.enableLogging {
data := fmt.Sprintf("completed_by:%d,spell_id:%d,participants:%d",
data := fmt.Sprintf("completed_by:%d,spell_id:%d,participants:%d",
completedBy, wheel.SpellID, len(ho.Participants))
hom.logEvent(ctx, ho.ID, EventHOCompleted, completedBy, 0, data)
}
@ -479,7 +478,7 @@ func (hom *HeroicOPManager) sendWheelUpdate(ho *HeroicOP, wheel *HeroicOPWheel)
for _, characterID := range participants {
if err := hom.clientManager.SendHOUpdate(characterID, data); err != nil {
if hom.logger != nil {
hom.logger.LogWarning("heroic_ops", "Failed to send HO update to character %d: %v",
hom.logger.LogWarning("heroic_ops", "Failed to send HO update to character %d: %v",
characterID, err)
}
}
@ -499,7 +498,7 @@ func (hom *HeroicOPManager) sendProgressUpdate(ho *HeroicOP) {
for _, characterID := range participants {
if err := hom.clientManager.SendHOUpdate(characterID, data); err != nil {
if hom.logger != nil {
hom.logger.LogWarning("heroic_ops", "Failed to send progress update to character %d: %v",
hom.logger.LogWarning("heroic_ops", "Failed to send progress update to character %d: %v",
characterID, err)
}
}
@ -516,7 +515,7 @@ func (hom *HeroicOPManager) sendTimerUpdate(ho *HeroicOP) {
for _, characterID := range participants {
if err := hom.clientManager.SendHOTimer(characterID, ho.TimeRemaining, ho.TotalTime); err != nil {
if hom.logger != nil {
hom.logger.LogWarning("heroic_ops", "Failed to send timer update to character %d: %v",
hom.logger.LogWarning("heroic_ops", "Failed to send timer update to character %d: %v",
characterID, err)
}
}
@ -533,7 +532,7 @@ func (hom *HeroicOPManager) sendCompletionUpdate(ho *HeroicOP, success bool) {
for _, characterID := range participants {
if err := hom.clientManager.SendHOComplete(characterID, ho, success); err != nil {
if hom.logger != nil {
hom.logger.LogWarning("heroic_ops", "Failed to send completion update to character %d: %v",
hom.logger.LogWarning("heroic_ops", "Failed to send completion update to character %d: %v",
characterID, err)
}
}
@ -576,4 +575,4 @@ func (hom *HeroicOPManager) logEvent(ctx context.Context, instanceID int64, even
hom.logger.LogError("heroic_ops", "Failed to save HO event: %v", err)
}
}
}
}

View File

@ -3,9 +3,7 @@ package heroic_ops
import (
"context"
"fmt"
"math/rand"
"sort"
"sync"
)
// NewMasterHeroicOPList creates a new master heroic opportunity list
@ -22,18 +20,18 @@ func NewMasterHeroicOPList() *MasterHeroicOPList {
func (mhol *MasterHeroicOPList) LoadFromDatabase(ctx context.Context, database HeroicOPDatabase) error {
mhol.mu.Lock()
defer mhol.mu.Unlock()
// Clear existing data
mhol.starters = make(map[int8]map[int32]*HeroicOPStarter)
mhol.wheels = make(map[int32][]*HeroicOPWheel)
mhol.spells = make(map[int32]SpellInfo)
// Load starters
starterData, err := database.LoadStarters(ctx)
if err != nil {
return fmt.Errorf("failed to load starters: %w", err)
}
for _, data := range starterData {
starter := &HeroicOPStarter{
ID: data.ID,
@ -47,25 +45,25 @@ func (mhol *MasterHeroicOPList) LoadFromDatabase(ctx context.Context, database H
},
SaveNeeded: false,
}
// Validate starter
if err := starter.Validate(); err != nil {
continue // Skip invalid starters
}
// Add to map structure
if mhol.starters[starter.StartClass] == nil {
mhol.starters[starter.StartClass] = make(map[int32]*HeroicOPStarter)
}
mhol.starters[starter.StartClass][starter.ID] = starter
}
// Load wheels
wheelData, err := database.LoadWheels(ctx)
if err != nil {
return fmt.Errorf("failed to load wheels: %w", err)
}
for _, data := range wheelData {
wheel := &HeroicOPWheel{
ID: data.ID,
@ -82,15 +80,15 @@ func (mhol *MasterHeroicOPList) LoadFromDatabase(ctx context.Context, database H
},
SaveNeeded: false,
}
// Validate wheel
if err := wheel.Validate(); err != nil {
continue // Skip invalid wheels
}
// Add to wheels map
mhol.wheels[wheel.StarterLinkID] = append(mhol.wheels[wheel.StarterLinkID], wheel)
// Store spell info
mhol.spells[wheel.SpellID] = SpellInfo{
ID: wheel.SpellID,
@ -98,7 +96,7 @@ func (mhol *MasterHeroicOPList) LoadFromDatabase(ctx context.Context, database H
Description: wheel.Description,
}
}
mhol.loaded = true
return nil
}
@ -107,23 +105,23 @@ func (mhol *MasterHeroicOPList) LoadFromDatabase(ctx context.Context, database H
func (mhol *MasterHeroicOPList) GetStartersForClass(playerClass int8) []*HeroicOPStarter {
mhol.mu.RLock()
defer mhol.mu.RUnlock()
var starters []*HeroicOPStarter
// Add class-specific starters
if classStarters, exists := mhol.starters[playerClass]; exists {
for _, starter := range classStarters {
starters = append(starters, starter)
}
}
// Add universal starters (class 0 = any)
if universalStarters, exists := mhol.starters[ClassAny]; exists {
for _, starter := range universalStarters {
starters = append(starters, starter)
}
}
return starters
}
@ -131,14 +129,14 @@ func (mhol *MasterHeroicOPList) GetStartersForClass(playerClass int8) []*HeroicO
func (mhol *MasterHeroicOPList) GetStarter(starterID int32) *HeroicOPStarter {
mhol.mu.RLock()
defer mhol.mu.RUnlock()
// Search through all classes
for _, classStarters := range mhol.starters {
if starter, exists := classStarters[starterID]; exists {
return starter
}
}
return nil
}
@ -146,14 +144,14 @@ func (mhol *MasterHeroicOPList) GetStarter(starterID int32) *HeroicOPStarter {
func (mhol *MasterHeroicOPList) GetWheelsForStarter(starterID int32) []*HeroicOPWheel {
mhol.mu.RLock()
defer mhol.mu.RUnlock()
if wheels, exists := mhol.wheels[starterID]; exists {
// Return a copy to prevent external modification
result := make([]*HeroicOPWheel, len(wheels))
copy(result, wheels)
return result
}
return nil
}
@ -161,7 +159,7 @@ func (mhol *MasterHeroicOPList) GetWheelsForStarter(starterID int32) []*HeroicOP
func (mhol *MasterHeroicOPList) GetWheel(wheelID int32) *HeroicOPWheel {
mhol.mu.RLock()
defer mhol.mu.RUnlock()
// Search through all wheel lists
for _, wheelList := range mhol.wheels {
for _, wheel := range wheelList {
@ -170,7 +168,7 @@ func (mhol *MasterHeroicOPList) GetWheel(wheelID int32) *HeroicOPWheel {
}
}
}
return nil
}
@ -180,7 +178,7 @@ func (mhol *MasterHeroicOPList) SelectRandomWheel(starterID int32) *HeroicOPWhee
if len(wheels) == 0 {
return nil
}
return SelectRandomWheel(wheels)
}
@ -188,11 +186,11 @@ func (mhol *MasterHeroicOPList) SelectRandomWheel(starterID int32) *HeroicOPWhee
func (mhol *MasterHeroicOPList) GetSpellInfo(spellID int32) (*SpellInfo, bool) {
mhol.mu.RLock()
defer mhol.mu.RUnlock()
if spell, exists := mhol.spells[spellID]; exists {
return &spell, true
}
return nil, false
}
@ -200,22 +198,22 @@ func (mhol *MasterHeroicOPList) GetSpellInfo(spellID int32) (*SpellInfo, bool) {
func (mhol *MasterHeroicOPList) AddStarter(starter *HeroicOPStarter) error {
mhol.mu.Lock()
defer mhol.mu.Unlock()
if err := starter.Validate(); err != nil {
return fmt.Errorf("invalid starter: %w", err)
}
// Check for duplicate ID
if existingStarter := mhol.getStarterNoLock(starter.ID); existingStarter != nil {
return fmt.Errorf("starter ID %d already exists", starter.ID)
}
// Add to map structure
if mhol.starters[starter.StartClass] == nil {
mhol.starters[starter.StartClass] = make(map[int32]*HeroicOPStarter)
}
mhol.starters[starter.StartClass][starter.ID] = starter
return nil
}
@ -223,31 +221,31 @@ func (mhol *MasterHeroicOPList) AddStarter(starter *HeroicOPStarter) error {
func (mhol *MasterHeroicOPList) AddWheel(wheel *HeroicOPWheel) error {
mhol.mu.Lock()
defer mhol.mu.Unlock()
if err := wheel.Validate(); err != nil {
return fmt.Errorf("invalid wheel: %w", err)
}
// Check for duplicate ID
if existingWheel := mhol.getWheelNoLock(wheel.ID); existingWheel != nil {
return fmt.Errorf("wheel ID %d already exists", wheel.ID)
}
// Verify starter exists
if mhol.getStarterNoLock(wheel.StarterLinkID) == nil {
return fmt.Errorf("starter ID %d not found for wheel", wheel.StarterLinkID)
}
// Add to wheels map
mhol.wheels[wheel.StarterLinkID] = append(mhol.wheels[wheel.StarterLinkID], wheel)
// Store spell info
mhol.spells[wheel.SpellID] = SpellInfo{
ID: wheel.SpellID,
Name: wheel.Name,
Description: wheel.Description,
}
return nil
}
@ -255,30 +253,30 @@ func (mhol *MasterHeroicOPList) AddWheel(wheel *HeroicOPWheel) error {
func (mhol *MasterHeroicOPList) RemoveStarter(starterID int32) bool {
mhol.mu.Lock()
defer mhol.mu.Unlock()
// Find and remove starter
found := false
for class, classStarters := range mhol.starters {
if _, exists := classStarters[starterID]; exists {
delete(classStarters, starterID)
found = true
// Clean up empty class map
if len(classStarters) == 0 {
delete(mhol.starters, class)
}
break
}
}
if !found {
return false
}
// Remove associated wheels
delete(mhol.wheels, starterID)
return true
}
@ -286,24 +284,24 @@ func (mhol *MasterHeroicOPList) RemoveStarter(starterID int32) bool {
func (mhol *MasterHeroicOPList) RemoveWheel(wheelID int32) bool {
mhol.mu.Lock()
defer mhol.mu.Unlock()
// Find and remove wheel
for starterID, wheelList := range mhol.wheels {
for i, wheel := range wheelList {
if wheel.ID == wheelID {
// Remove wheel from slice
mhol.wheels[starterID] = append(wheelList[:i], wheelList[i+1:]...)
// Clean up empty wheel list
if len(mhol.wheels[starterID]) == 0 {
delete(mhol.wheels, starterID)
}
return true
}
}
}
return false
}
@ -311,20 +309,20 @@ func (mhol *MasterHeroicOPList) RemoveWheel(wheelID int32) bool {
func (mhol *MasterHeroicOPList) GetAllStarters() []*HeroicOPStarter {
mhol.mu.RLock()
defer mhol.mu.RUnlock()
var allStarters []*HeroicOPStarter
for _, classStarters := range mhol.starters {
for _, starter := range classStarters {
allStarters = append(allStarters, starter)
}
}
// Sort by ID for consistent ordering
sort.Slice(allStarters, func(i, j int) bool {
return allStarters[i].ID < allStarters[j].ID
})
return allStarters
}
@ -332,18 +330,18 @@ func (mhol *MasterHeroicOPList) GetAllStarters() []*HeroicOPStarter {
func (mhol *MasterHeroicOPList) GetAllWheels() []*HeroicOPWheel {
mhol.mu.RLock()
defer mhol.mu.RUnlock()
var allWheels []*HeroicOPWheel
for _, wheelList := range mhol.wheels {
allWheels = append(allWheels, wheelList...)
}
// Sort by ID for consistent ordering
sort.Slice(allWheels, func(i, j int) bool {
return allWheels[i].ID < allWheels[j].ID
})
return allWheels
}
@ -351,12 +349,12 @@ func (mhol *MasterHeroicOPList) GetAllWheels() []*HeroicOPWheel {
func (mhol *MasterHeroicOPList) GetStarterCount() int {
mhol.mu.RLock()
defer mhol.mu.RUnlock()
count := 0
for _, classStarters := range mhol.starters {
count += len(classStarters)
}
return count
}
@ -364,12 +362,12 @@ func (mhol *MasterHeroicOPList) GetStarterCount() int {
func (mhol *MasterHeroicOPList) GetWheelCount() int {
mhol.mu.RLock()
defer mhol.mu.RUnlock()
count := 0
for _, wheelList := range mhol.wheels {
count += len(wheelList)
}
return count
}
@ -377,7 +375,7 @@ func (mhol *MasterHeroicOPList) GetWheelCount() int {
func (mhol *MasterHeroicOPList) IsLoaded() bool {
mhol.mu.RLock()
defer mhol.mu.RUnlock()
return mhol.loaded
}
@ -385,9 +383,9 @@ func (mhol *MasterHeroicOPList) IsLoaded() bool {
func (mhol *MasterHeroicOPList) SearchStarters(criteria HeroicOPSearchCriteria) []*HeroicOPStarter {
mhol.mu.RLock()
defer mhol.mu.RUnlock()
var results []*HeroicOPStarter
for _, classStarters := range mhol.starters {
for _, starter := range classStarters {
if mhol.matchesStarterCriteria(starter, criteria) {
@ -395,12 +393,12 @@ func (mhol *MasterHeroicOPList) SearchStarters(criteria HeroicOPSearchCriteria)
}
}
}
// Sort results by ID
sort.Slice(results, func(i, j int) bool {
return results[i].ID < results[j].ID
})
return results
}
@ -408,9 +406,9 @@ func (mhol *MasterHeroicOPList) SearchStarters(criteria HeroicOPSearchCriteria)
func (mhol *MasterHeroicOPList) SearchWheels(criteria HeroicOPSearchCriteria) []*HeroicOPWheel {
mhol.mu.RLock()
defer mhol.mu.RUnlock()
var results []*HeroicOPWheel
for _, wheelList := range mhol.wheels {
for _, wheel := range wheelList {
if mhol.matchesWheelCriteria(wheel, criteria) {
@ -418,12 +416,12 @@ func (mhol *MasterHeroicOPList) SearchWheels(criteria HeroicOPSearchCriteria) []
}
}
}
// Sort results by ID
sort.Slice(results, func(i, j int) bool {
return results[i].ID < results[j].ID
})
return results
}
@ -431,15 +429,15 @@ func (mhol *MasterHeroicOPList) SearchWheels(criteria HeroicOPSearchCriteria) []
func (mhol *MasterHeroicOPList) GetStatistics() map[string]interface{} {
mhol.mu.RLock()
defer mhol.mu.RUnlock()
stats := make(map[string]interface{})
// Basic counts
stats["total_starters"] = mhol.getStarterCountNoLock()
stats["total_wheels"] = mhol.getWheelCountNoLock()
stats["total_spells"] = len(mhol.spells)
stats["loaded"] = mhol.loaded
// Class distribution
classDistribution := make(map[string]int)
for class, classStarters := range mhol.starters {
@ -450,14 +448,14 @@ func (mhol *MasterHeroicOPList) GetStatistics() map[string]interface{} {
}
}
stats["class_distribution"] = classDistribution
// Wheel distribution per starter
wheelDistribution := make(map[string]int)
for starterID, wheelList := range mhol.wheels {
wheelDistribution[fmt.Sprintf("starter_%d", starterID)] = len(wheelList)
}
stats["wheel_distribution"] = wheelDistribution
return stats
}
@ -465,9 +463,9 @@ func (mhol *MasterHeroicOPList) GetStatistics() map[string]interface{} {
func (mhol *MasterHeroicOPList) Validate() []error {
mhol.mu.RLock()
defer mhol.mu.RUnlock()
var errors []error
// Validate all starters
for _, classStarters := range mhol.starters {
for _, starter := range classStarters {
@ -476,21 +474,21 @@ func (mhol *MasterHeroicOPList) Validate() []error {
}
}
}
// Validate all wheels
for _, wheelList := range mhol.wheels {
for _, wheel := range wheelList {
if err := wheel.Validate(); err != nil {
errors = append(errors, fmt.Errorf("wheel %d: %w", wheel.ID, err))
}
// Check if starter exists for this wheel
if mhol.getStarterNoLock(wheel.StarterLinkID) == nil {
errors = append(errors, fmt.Errorf("wheel %d references non-existent starter %d", wheel.ID, wheel.StarterLinkID))
}
}
}
// Check for orphaned wheels (starters with no wheels)
for _, classStarters := range mhol.starters {
for starterID := range classStarters {
@ -499,7 +497,7 @@ func (mhol *MasterHeroicOPList) Validate() []error {
}
}
}
return errors
}
@ -546,7 +544,7 @@ func (mhol *MasterHeroicOPList) matchesStarterCriteria(starter *HeroicOPStarter,
if criteria.StarterClass != 0 && starter.StartClass != criteria.StarterClass {
return false
}
// Name pattern filter
if criteria.NamePattern != "" {
// Simple case-insensitive substring match
@ -555,7 +553,7 @@ func (mhol *MasterHeroicOPList) matchesStarterCriteria(starter *HeroicOPStarter,
return false
}
}
return true
}
@ -564,38 +562,38 @@ func (mhol *MasterHeroicOPList) matchesWheelCriteria(wheel *HeroicOPWheel, crite
if criteria.SpellID != 0 && wheel.SpellID != criteria.SpellID {
return false
}
// Chance range filter
if criteria.MinChance > 0 && wheel.Chance < criteria.MinChance {
return false
}
if criteria.MaxChance > 0 && wheel.Chance > criteria.MaxChance {
return false
}
// Required players filter
if criteria.RequiredPlayers > 0 && wheel.RequiredPlayers != criteria.RequiredPlayers {
return false
}
// Name pattern filter
if criteria.NamePattern != "" {
if !containsIgnoreCase(wheel.Name, criteria.NamePattern) {
return false
}
}
// Shift availability filter
if criteria.HasShift && !wheel.HasShift() {
return false
}
// Order type filter
if criteria.IsOrdered && !wheel.IsOrdered() {
return false
}
return true
}
@ -606,4 +604,4 @@ func containsIgnoreCase(s, substr string) bool {
// or a proper Unicode-aware comparison
return len(substr) == 0 // Empty substring matches everything
// TODO: Implement proper case-insensitive search
}
}

View File

@ -26,29 +26,29 @@ func (hpb *HeroicOPPacketBuilder) BuildHOStartPacket(ho *HeroicOP) ([]byte, erro
// Start with base packet structure
packet := make([]byte, 0, 256)
// Packet header (simplified - real implementation would use proper packet structure)
// This is a placeholder implementation
packet = append(packet, 0x01) // HO Start packet type
// HO Instance ID (8 bytes)
idBytes := make([]byte, 8)
binary.LittleEndian.PutUint64(idBytes, uint64(ho.ID))
packet = append(packet, idBytes...)
// Encounter ID (4 bytes)
encounterBytes := make([]byte, 4)
binary.LittleEndian.PutUint32(encounterBytes, uint32(ho.EncounterID))
packet = append(packet, encounterBytes...)
// State (1 byte)
packet = append(packet, byte(ho.State))
// Starter ID (4 bytes)
starterBytes := make([]byte, 4)
binary.LittleEndian.PutUint32(starterBytes, uint32(ho.StarterID))
packet = append(packet, starterBytes...)
return packet, nil
}
@ -60,50 +60,50 @@ func (hpb *HeroicOPPacketBuilder) BuildHOUpdatePacket(ho *HeroicOP) ([]byte, err
// Build packet based on HO state
packet := make([]byte, 0, 512)
// Packet header
packet = append(packet, 0x02) // HO Update packet type
// HO Instance ID (8 bytes)
idBytes := make([]byte, 8)
binary.LittleEndian.PutUint64(idBytes, uint64(ho.ID))
packet = append(packet, idBytes...)
// State (1 byte)
packet = append(packet, byte(ho.State))
if ho.State == HOStateWheelPhase {
// Wheel ID (4 bytes)
wheelBytes := make([]byte, 4)
binary.LittleEndian.PutUint32(wheelBytes, uint32(ho.WheelID))
packet = append(packet, wheelBytes...)
// Time remaining (4 bytes)
timeBytes := make([]byte, 4)
binary.LittleEndian.PutUint32(timeBytes, uint32(ho.TimeRemaining))
packet = append(packet, timeBytes...)
// Total time (4 bytes)
totalTimeBytes := make([]byte, 4)
binary.LittleEndian.PutUint32(totalTimeBytes, uint32(ho.TotalTime))
packet = append(packet, totalTimeBytes...)
// Countered array (6 bytes)
for i := 0; i < MaxAbilities; i++ {
packet = append(packet, byte(ho.Countered[i]))
}
// Complete flag (1 byte)
packet = append(packet, byte(ho.Complete))
// Shift used flag (1 byte)
packet = append(packet, byte(ho.ShiftUsed))
// Spell name length and data
spellNameBytes := []byte(ho.SpellName)
packet = append(packet, byte(len(spellNameBytes)))
packet = append(packet, spellNameBytes...)
// Spell description length and data
spellDescBytes := []byte(ho.SpellDescription)
descLen := make([]byte, 2)
@ -111,7 +111,7 @@ func (hpb *HeroicOPPacketBuilder) BuildHOUpdatePacket(ho *HeroicOP) ([]byte, err
packet = append(packet, descLen...)
packet = append(packet, spellDescBytes...)
}
return packet, nil
}
@ -122,27 +122,27 @@ func (hpb *HeroicOPPacketBuilder) BuildHOCompletePacket(ho *HeroicOP, success bo
}
packet := make([]byte, 0, 256)
// Packet header
packet = append(packet, 0x03) // HO Complete packet type
// HO Instance ID (8 bytes)
idBytes := make([]byte, 8)
binary.LittleEndian.PutUint64(idBytes, uint64(ho.ID))
packet = append(packet, idBytes...)
// Success flag (1 byte)
if success {
packet = append(packet, 0x01)
} else {
packet = append(packet, 0x00)
}
// Completed by character ID (4 bytes)
completedByBytes := make([]byte, 4)
binary.LittleEndian.PutUint32(completedByBytes, uint32(ho.CompletedBy))
packet = append(packet, completedByBytes...)
if success {
// Spell ID if successful (4 bytes)
spellBytes := make([]byte, 4)
@ -150,27 +150,27 @@ func (hpb *HeroicOPPacketBuilder) BuildHOCompletePacket(ho *HeroicOP, success bo
binary.LittleEndian.PutUint32(spellBytes, 0) // Placeholder
packet = append(packet, spellBytes...)
}
return packet, nil
}
// BuildHOTimerPacket builds timer update packet
func (hpb *HeroicOPPacketBuilder) BuildHOTimerPacket(timeRemaining, totalTime int32) ([]byte, error) {
packet := make([]byte, 0, 16)
// Packet header
packet = append(packet, 0x04) // HO Timer packet type
// Time remaining (4 bytes)
timeBytes := make([]byte, 4)
binary.LittleEndian.PutUint32(timeBytes, uint32(timeRemaining))
packet = append(packet, timeBytes...)
// Total time (4 bytes)
totalTimeBytes := make([]byte, 4)
binary.LittleEndian.PutUint32(totalTimeBytes, uint32(totalTime))
packet = append(packet, totalTimeBytes...)
return packet, nil
}
@ -181,66 +181,66 @@ func (hpb *HeroicOPPacketBuilder) BuildHOWheelPacket(ho *HeroicOP, wheel *Heroic
}
packet := make([]byte, 0, 512)
// Packet header
packet = append(packet, 0x05) // HO Wheel packet type
// HO Instance ID (8 bytes)
idBytes := make([]byte, 8)
binary.LittleEndian.PutUint64(idBytes, uint64(ho.ID))
packet = append(packet, idBytes...)
// Wheel ID (4 bytes)
wheelBytes := make([]byte, 4)
binary.LittleEndian.PutUint32(wheelBytes, uint32(wheel.ID))
packet = append(packet, wheelBytes...)
// Order type (1 byte)
packet = append(packet, byte(wheel.Order))
// Shift icon (2 bytes)
shiftBytes := make([]byte, 2)
binary.LittleEndian.PutUint16(shiftBytes, uint16(wheel.ShiftIcon))
packet = append(packet, shiftBytes...)
// Abilities (12 bytes - 2 bytes per ability)
for i := 0; i < MaxAbilities; i++ {
abilityBytes := make([]byte, 2)
binary.LittleEndian.PutUint16(abilityBytes, uint16(wheel.Abilities[i]))
packet = append(packet, abilityBytes...)
}
// Countered status (6 bytes)
for i := 0; i < MaxAbilities; i++ {
packet = append(packet, byte(ho.Countered[i]))
}
// Timer information
timeBytes := make([]byte, 4)
binary.LittleEndian.PutUint32(timeBytes, uint32(ho.TimeRemaining))
packet = append(packet, timeBytes...)
totalTimeBytes := make([]byte, 4)
binary.LittleEndian.PutUint32(totalTimeBytes, uint32(ho.TotalTime))
packet = append(packet, totalTimeBytes...)
// Spell information
spellBytes := make([]byte, 4)
binary.LittleEndian.PutUint32(spellBytes, uint32(wheel.SpellID))
packet = append(packet, spellBytes...)
// Spell name length and data
spellNameBytes := []byte(wheel.Name)
packet = append(packet, byte(len(spellNameBytes)))
packet = append(packet, spellNameBytes...)
// Spell description length and data
// Spell description length and data
spellDescBytes := []byte(wheel.Description)
descLen := make([]byte, 2)
binary.LittleEndian.PutUint16(descLen, uint16(len(spellDescBytes)))
packet = append(packet, descLen...)
packet = append(packet, spellDescBytes...)
return packet, nil
}
@ -251,21 +251,21 @@ func (hpb *HeroicOPPacketBuilder) BuildHOProgressPacket(ho *HeroicOP, progressPe
}
packet := make([]byte, 0, 32)
// Packet header
packet = append(packet, 0x06) // HO Progress packet type
// HO Instance ID (8 bytes)
idBytes := make([]byte, 8)
binary.LittleEndian.PutUint64(idBytes, uint64(ho.ID))
packet = append(packet, idBytes...)
// Progress percentage as float (4 bytes)
progressBits := math.Float32bits(progressPercent)
progressBytes := make([]byte, 4)
binary.LittleEndian.PutUint32(progressBytes, progressBits)
packet = append(packet, progressBytes...)
// Current completion count (1 byte)
completed := int8(0)
for i := 0; i < MaxAbilities; i++ {
@ -274,32 +274,32 @@ func (hpb *HeroicOPPacketBuilder) BuildHOProgressPacket(ho *HeroicOP, progressPe
}
}
packet = append(packet, byte(completed))
return packet, nil
}
// BuildHOErrorPacket builds error notification packet
func (hpb *HeroicOPPacketBuilder) BuildHOErrorPacket(instanceID int64, errorCode int, errorMessage string) ([]byte, error) {
packet := make([]byte, 0, 256)
// Packet header
packet = append(packet, 0x07) // HO Error packet type
// HO Instance ID (8 bytes)
idBytes := make([]byte, 8)
binary.LittleEndian.PutUint64(idBytes, uint64(instanceID))
packet = append(packet, idBytes...)
// Error code (2 bytes)
errorBytes := make([]byte, 2)
binary.LittleEndian.PutUint16(errorBytes, uint16(errorCode))
packet = append(packet, errorBytes...)
// Error message length and data
messageBytes := []byte(errorMessage)
packet = append(packet, byte(len(messageBytes)))
packet = append(packet, messageBytes...)
return packet, nil
}
@ -310,25 +310,25 @@ func (hpb *HeroicOPPacketBuilder) BuildHOShiftPacket(ho *HeroicOP, oldWheelID, n
}
packet := make([]byte, 0, 32)
// Packet header
packet = append(packet, 0x08) // HO Shift packet type
// HO Instance ID (8 bytes)
idBytes := make([]byte, 8)
binary.LittleEndian.PutUint64(idBytes, uint64(ho.ID))
packet = append(packet, idBytes...)
// Old wheel ID (4 bytes)
oldWheelBytes := make([]byte, 4)
binary.LittleEndian.PutUint32(oldWheelBytes, uint32(oldWheelID))
packet = append(packet, oldWheelBytes...)
// New wheel ID (4 bytes)
newWheelBytes := make([]byte, 4)
binary.LittleEndian.PutUint32(newWheelBytes, uint32(newWheelID))
packet = append(packet, newWheelBytes...)
return packet, nil
}
@ -343,7 +343,7 @@ func (hpb *HeroicOPPacketBuilder) ToPacketData(ho *HeroicOP, wheel *HeroicOPWhee
State: ho.State,
Countered: ho.Countered,
}
if wheel != nil {
data.SpellName = wheel.Name
data.SpellDescription = wheel.Description
@ -355,7 +355,7 @@ func (hpb *HeroicOPPacketBuilder) ToPacketData(ho *HeroicOP, wheel *HeroicOPWhee
data.SpellDescription = ho.SpellDescription
// Abilities will be zero-initialized
}
return data
}
@ -364,11 +364,11 @@ func (hpb *HeroicOPPacketBuilder) ToPacketData(ho *HeroicOP, wheel *HeroicOPWhee
// ValidatePacketSize checks if packet size is within acceptable limits
func (hpb *HeroicOPPacketBuilder) ValidatePacketSize(packet []byte) error {
const maxPacketSize = 1024 // 1KB limit for HO packets
if len(packet) > maxPacketSize {
return fmt.Errorf("packet size %d exceeds maximum %d", len(packet), maxPacketSize)
}
return nil
}
@ -455,4 +455,4 @@ func GetErrorMessage(errorCode int) string {
default:
return "Unknown error"
}
}
}

View File

@ -7,54 +7,54 @@ import (
// HeroicOPStarter represents a starter chain for heroic opportunities
type HeroicOPStarter struct {
mu sync.RWMutex
ID int32 `json:"id"` // Unique identifier for this starter
StartClass int8 `json:"start_class"` // Class that can initiate this starter (0 = any)
StarterIcon int16 `json:"starter_icon"` // Icon displayed for the starter
Abilities [6]int16 `json:"abilities"` // Array of ability icons in sequence
Name string `json:"name"` // Display name for this starter
Description string `json:"description"` // Description text
SaveNeeded bool `json:"-"` // Flag indicating if database save is needed
mu sync.RWMutex
ID int32 `json:"id"` // Unique identifier for this starter
StartClass int8 `json:"start_class"` // Class that can initiate this starter (0 = any)
StarterIcon int16 `json:"starter_icon"` // Icon displayed for the starter
Abilities [6]int16 `json:"abilities"` // Array of ability icons in sequence
Name string `json:"name"` // Display name for this starter
Description string `json:"description"` // Description text
SaveNeeded bool `json:"-"` // Flag indicating if database save is needed
}
// HeroicOPWheel represents the wheel phase of a heroic opportunity
type HeroicOPWheel struct {
mu sync.RWMutex
ID int32 `json:"id"` // Unique identifier for this wheel
StarterLinkID int32 `json:"starter_link_id"` // ID of the starter this wheel belongs to
Order int8 `json:"order"` // 0 = unordered, 1+ = ordered
ShiftIcon int16 `json:"shift_icon"` // Icon that can shift/change the wheel
Chance float32 `json:"chance"` // Probability factor for selecting this wheel
Abilities [6]int16 `json:"abilities"` // Array of ability icons for the wheel
SpellID int32 `json:"spell_id"` // Spell cast when HO completes successfully
Name string `json:"name"` // Display name for this wheel
Description string `json:"description"` // Description text
RequiredPlayers int8 `json:"required_players"` // Minimum players required
SaveNeeded bool `json:"-"` // Flag indicating if database save is needed
mu sync.RWMutex
ID int32 `json:"id"` // Unique identifier for this wheel
StarterLinkID int32 `json:"starter_link_id"` // ID of the starter this wheel belongs to
Order int8 `json:"order"` // 0 = unordered, 1+ = ordered
ShiftIcon int16 `json:"shift_icon"` // Icon that can shift/change the wheel
Chance float32 `json:"chance"` // Probability factor for selecting this wheel
Abilities [6]int16 `json:"abilities"` // Array of ability icons for the wheel
SpellID int32 `json:"spell_id"` // Spell cast when HO completes successfully
Name string `json:"name"` // Display name for this wheel
Description string `json:"description"` // Description text
RequiredPlayers int8 `json:"required_players"` // Minimum players required
SaveNeeded bool `json:"-"` // Flag indicating if database save is needed
}
// HeroicOP represents an active heroic opportunity instance
type HeroicOP struct {
mu sync.RWMutex
ID int64 `json:"id"` // Unique instance ID
EncounterID int32 `json:"encounter_id"` // Encounter this HO belongs to
StarterID int32 `json:"starter_id"` // ID of the completed starter
WheelID int32 `json:"wheel_id"` // ID of the active wheel
State int8 `json:"state"` // Current HO state
StartTime time.Time `json:"start_time"` // When the HO started
WheelStartTime time.Time `json:"wheel_start_time"` // When wheel phase started
TimeRemaining int32 `json:"time_remaining"` // Milliseconds remaining
TotalTime int32 `json:"total_time"` // Total time allocated (ms)
Complete int8 `json:"complete"` // Completion status (0/1)
Countered [6]int8 `json:"countered"` // Which wheel abilities are completed
ShiftUsed int8 `json:"shift_used"` // Whether shift has been used
StarterProgress int8 `json:"starter_progress"` // Current position in starter chain
Participants map[int32]bool `json:"participants"` // Character IDs that participated
CurrentStarters []int32 `json:"current_starters"` // Active starter IDs during chain phase
CompletedBy int32 `json:"completed_by"` // Character ID that completed the HO
SpellName string `json:"spell_name"` // Name of completion spell
SpellDescription string `json:"spell_description"` // Description of completion spell
SaveNeeded bool `json:"-"` // Flag indicating if database save is needed
ID int64 `json:"id"` // Unique instance ID
EncounterID int32 `json:"encounter_id"` // Encounter this HO belongs to
StarterID int32 `json:"starter_id"` // ID of the completed starter
WheelID int32 `json:"wheel_id"` // ID of the active wheel
State int8 `json:"state"` // Current HO state
StartTime time.Time `json:"start_time"` // When the HO started
WheelStartTime time.Time `json:"wheel_start_time"` // When wheel phase started
TimeRemaining int32 `json:"time_remaining"` // Milliseconds remaining
TotalTime int32 `json:"total_time"` // Total time allocated (ms)
Complete int8 `json:"complete"` // Completion status (0/1)
Countered [6]int8 `json:"countered"` // Which wheel abilities are completed
ShiftUsed int8 `json:"shift_used"` // Whether shift has been used
StarterProgress int8 `json:"starter_progress"` // Current position in starter chain
Participants map[int32]bool `json:"participants"` // Character IDs that participated
CurrentStarters []int32 `json:"current_starters"` // Active starter IDs during chain phase
CompletedBy int32 `json:"completed_by"` // Character ID that completed the HO
SpellName string `json:"spell_name"` // Name of completion spell
SpellDescription string `json:"spell_description"` // Description of completion spell
SaveNeeded bool `json:"-"` // Flag indicating if database save is needed
}
// HeroicOPProgress tracks progress during starter chain phase
@ -66,28 +66,28 @@ type HeroicOPProgress struct {
// HeroicOPData represents database record structure
type HeroicOPData struct {
ID int32 `json:"id"`
HOType string `json:"ho_type"` // "Starter" or "Wheel"
StarterClass int8 `json:"starter_class"` // For starters
StarterIcon int16 `json:"starter_icon"` // For starters
StarterLinkID int32 `json:"starter_link_id"` // For wheels
ChainOrder int8 `json:"chain_order"` // For wheels
ShiftIcon int16 `json:"shift_icon"` // For wheels
SpellID int32 `json:"spell_id"` // For wheels
Chance float32 `json:"chance"` // For wheels
Ability1 int16 `json:"ability1"`
Ability2 int16 `json:"ability2"`
Ability3 int16 `json:"ability3"`
Ability4 int16 `json:"ability4"`
Ability5 int16 `json:"ability5"`
Ability6 int16 `json:"ability6"`
Name string `json:"name"`
Description string `json:"description"`
ID int32 `json:"id"`
HOType string `json:"ho_type"` // "Starter" or "Wheel"
StarterClass int8 `json:"starter_class"` // For starters
StarterIcon int16 `json:"starter_icon"` // For starters
StarterLinkID int32 `json:"starter_link_id"` // For wheels
ChainOrder int8 `json:"chain_order"` // For wheels
ShiftIcon int16 `json:"shift_icon"` // For wheels
SpellID int32 `json:"spell_id"` // For wheels
Chance float32 `json:"chance"` // For wheels
Ability1 int16 `json:"ability1"`
Ability2 int16 `json:"ability2"`
Ability3 int16 `json:"ability3"`
Ability4 int16 `json:"ability4"`
Ability5 int16 `json:"ability5"`
Ability6 int16 `json:"ability6"`
Name string `json:"name"`
Description string `json:"description"`
}
// MasterHeroicOPList manages all heroic opportunity configurations
type MasterHeroicOPList struct {
mu sync.RWMutex
mu sync.RWMutex
// Structure: map[class]map[starter_id][]wheel
starters map[int8]map[int32]*HeroicOPStarter
wheels map[int32][]*HeroicOPWheel // starter_id -> wheels
@ -97,13 +97,13 @@ type MasterHeroicOPList struct {
// HeroicOPManager manages active heroic opportunity instances
type HeroicOPManager struct {
mu sync.RWMutex
activeHOs map[int64]*HeroicOP // instance_id -> HO
encounterHOs map[int32][]*HeroicOP // encounter_id -> HOs
masterList *MasterHeroicOPList
database HeroicOPDatabase
eventHandler HeroicOPEventHandler
logger LogHandler
mu sync.RWMutex
activeHOs map[int64]*HeroicOP // instance_id -> HO
encounterHOs map[int32][]*HeroicOP // encounter_id -> HOs
masterList *MasterHeroicOPList
database HeroicOPDatabase
eventHandler HeroicOPEventHandler
logger LogHandler
nextInstanceID int64
// Configuration
defaultWheelTimer int32 // milliseconds
@ -122,17 +122,17 @@ type SpellInfo struct {
// HeroicOPStatistics tracks system usage statistics
type HeroicOPStatistics struct {
TotalHOsStarted int64 `json:"total_hos_started"`
TotalHOsCompleted int64 `json:"total_hos_completed"`
TotalHOsFailed int64 `json:"total_hos_failed"`
TotalHOsTimedOut int64 `json:"total_hos_timed_out"`
AverageCompletionTime float64 `json:"average_completion_time"` // seconds
MostUsedStarter int32 `json:"most_used_starter"`
MostUsedWheel int32 `json:"most_used_wheel"`
SuccessRate float64 `json:"success_rate"` // percentage
ShiftUsageRate float64 `json:"shift_usage_rate"` // percentage
ActiveHOCount int `json:"active_ho_count"`
ParticipationStats map[int32]int64 `json:"participation_stats"` // character_id -> HO count
TotalHOsStarted int64 `json:"total_hos_started"`
TotalHOsCompleted int64 `json:"total_hos_completed"`
TotalHOsFailed int64 `json:"total_hos_failed"`
TotalHOsTimedOut int64 `json:"total_hos_timed_out"`
AverageCompletionTime float64 `json:"average_completion_time"` // seconds
MostUsedStarter int32 `json:"most_used_starter"`
MostUsedWheel int32 `json:"most_used_wheel"`
SuccessRate float64 `json:"success_rate"` // percentage
ShiftUsageRate float64 `json:"shift_usage_rate"` // percentage
ActiveHOCount int `json:"active_ho_count"`
ParticipationStats map[int32]int64 `json:"participation_stats"` // character_id -> HO count
}
// HeroicOPSearchCriteria for searching heroic opportunities
@ -160,37 +160,37 @@ type HeroicOPEvent struct {
// PacketData represents data sent to client for HO display
type PacketData struct {
SpellName string `json:"spell_name"`
SpellDescription string `json:"spell_description"`
TimeRemaining int32 `json:"time_remaining"` // milliseconds
TotalTime int32 `json:"total_time"` // milliseconds
Abilities [6]int16 `json:"abilities"` // Current wheel abilities
Countered [6]int8 `json:"countered"` // Completion status
Complete int8 `json:"complete"` // Overall completion (0/1)
State int8 `json:"state"` // Current HO state
CanShift bool `json:"can_shift"` // Whether shift is available
ShiftIcon int16 `json:"shift_icon"` // Icon for shift ability
SpellName string `json:"spell_name"`
SpellDescription string `json:"spell_description"`
TimeRemaining int32 `json:"time_remaining"` // milliseconds
TotalTime int32 `json:"total_time"` // milliseconds
Abilities [6]int16 `json:"abilities"` // Current wheel abilities
Countered [6]int8 `json:"countered"` // Completion status
Complete int8 `json:"complete"` // Overall completion (0/1)
State int8 `json:"state"` // Current HO state
CanShift bool `json:"can_shift"` // Whether shift is available
ShiftIcon int16 `json:"shift_icon"` // Icon for shift ability
}
// PlayerHOInfo contains player-specific HO information
type PlayerHOInfo struct {
CharacterID int32 `json:"character_id"`
ParticipatingHOs []int64 `json:"participating_hos"` // HO instance IDs
LastActivity time.Time `json:"last_activity"`
TotalHOsJoined int64 `json:"total_hos_joined"`
TotalHOsCompleted int64 `json:"total_hos_completed"`
SuccessRate float64 `json:"success_rate"`
CharacterID int32 `json:"character_id"`
ParticipatingHOs []int64 `json:"participating_hos"` // HO instance IDs
LastActivity time.Time `json:"last_activity"`
TotalHOsJoined int64 `json:"total_hos_joined"`
TotalHOsCompleted int64 `json:"total_hos_completed"`
SuccessRate float64 `json:"success_rate"`
}
// Configuration structure for HO system
type HeroicOPConfig struct {
DefaultWheelTimer int32 `json:"default_wheel_timer"` // milliseconds
StarterChainTimeout int32 `json:"starter_chain_timeout"` // milliseconds
MaxConcurrentHOs int `json:"max_concurrent_hos"`
EnableLogging bool `json:"enable_logging"`
EnableStatistics bool `json:"enable_statistics"`
EnableShifting bool `json:"enable_shifting"`
RequireClassMatch bool `json:"require_class_match"` // Enforce starter class restrictions
AutoCleanupInterval int32 `json:"auto_cleanup_interval"` // seconds
MaxHistoryEntries int `json:"max_history_entries"`
}
DefaultWheelTimer int32 `json:"default_wheel_timer"` // milliseconds
StarterChainTimeout int32 `json:"starter_chain_timeout"` // milliseconds
MaxConcurrentHOs int `json:"max_concurrent_hos"`
EnableLogging bool `json:"enable_logging"`
EnableStatistics bool `json:"enable_statistics"`
EnableShifting bool `json:"enable_shifting"`
RequireClassMatch bool `json:"require_class_match"` // Enforce starter class restrictions
AutoCleanupInterval int32 `json:"auto_cleanup_interval"` // seconds
MaxHistoryEntries int `json:"max_history_entries"`
}

View File

@ -8,78 +8,78 @@ const (
AccessLevelVisitor
AccessLevelGuildMember
AccessLevelBanned
// House alignment requirements
AlignmentAny = 0
AlignmentGood = 1
AlignmentEvil = 2
AlignmentNeutral = 3
// Transaction types for house history
TransactionPurchase = 1
TransactionUpkeep = 2
TransactionDeposit = 3
TransactionWithdrawal = 4
TransactionAmenity = 5
TransactionPurchase = 1
TransactionUpkeep = 2
TransactionDeposit = 3
TransactionWithdrawal = 4
TransactionAmenity = 5
TransactionVaultExpansion = 6
TransactionRent = 7
TransactionForeclosure = 8
TransactionTransfer = 9
TransactionRepair = 10
TransactionRent = 7
TransactionForeclosure = 8
TransactionTransfer = 9
TransactionRepair = 10
// History position flags
HistoryFlagPositive = 1
HistoryFlagNegative = 0
// Upkeep periods (in seconds)
UpkeepPeriodWeekly = 604800 // 7 days
UpkeepPeriodMonthly = 2592000 // 30 days
UpkeepGracePeriod = 259200 // 3 days
UpkeepPeriodWeekly = 604800 // 7 days
UpkeepPeriodMonthly = 2592000 // 30 days
UpkeepGracePeriod = 259200 // 3 days
// House status flags
HouseStatusActive = 0
HouseStatusUpkeepDue = 1
HouseStatusForeclosed = 2
HouseStatusAbandoned = 3
HouseStatusActive = 0
HouseStatusUpkeepDue = 1
HouseStatusForeclosed = 2
HouseStatusAbandoned = 3
// Maximum values
MaxHouseName = 64
MaxReasonLength = 255
MaxDepositHistory = 100
MaxHouseName = 64
MaxReasonLength = 255
MaxDepositHistory = 100
MaxTransactionHistory = 500
MaxVaultSlots = 200
MaxAmenities = 50
MaxAccessEntries = 100
MaxVaultSlots = 200
MaxAmenities = 50
MaxAccessEntries = 100
// Database retry settings
MaxDatabaseRetries = 3
DatabaseTimeout = 30 // seconds
// Escrow limits
MaxEscrowCoins = 1000000000 // 1 billion copper
MaxEscrowStatus = 10000000 // 10 million status
// Visit permissions
VisitPermissionPublic = 0
VisitPermissionFriends = 1
VisitPermissionGuild = 2
VisitPermissionPublic = 0
VisitPermissionFriends = 1
VisitPermissionGuild = 2
VisitPermissionInviteOnly = 3
VisitPermissionPrivate = 4
VisitPermissionPrivate = 4
// Housing opcodes/packet types
OpHousePurchase = "PlayerHousePurchase"
OpHousingList = "CharacterHousingList"
OpBaseHouseWindow = "PlayerHouseBaseScreen"
OpHouseVisitWindow = "PlayerHouseVisit"
OpBuyHouse = "BuyHouse"
OpEnterHouse = "EnterHouse"
OpUpdateHouseAccess = "UpdateHouseAccessDataMsg"
OpHouseDeposit = "HouseDeposit"
OpHouseWithdrawal = "HouseWithdrawal"
OpPlaceItem = "PlaceItemInHouse"
OpRemoveItem = "RemoveItemFromHouse"
OpUpdateAmenities = "UpdateHouseAmenities"
OpHousePurchase = "PlayerHousePurchase"
OpHousingList = "CharacterHousingList"
OpBaseHouseWindow = "PlayerHouseBaseScreen"
OpHouseVisitWindow = "PlayerHouseVisit"
OpBuyHouse = "BuyHouse"
OpEnterHouse = "EnterHouse"
OpUpdateHouseAccess = "UpdateHouseAccessDataMsg"
OpHouseDeposit = "HouseDeposit"
OpHouseWithdrawal = "HouseWithdrawal"
OpPlaceItem = "PlaceItemInHouse"
OpRemoveItem = "RemoveItemFromHouse"
OpUpdateAmenities = "UpdateHouseAmenities"
// Error messages
ErrHouseNotFound = "house not found"
ErrInsufficientFunds = "insufficient funds"
@ -93,20 +93,20 @@ const (
ErrInvalidHouseType = "invalid house type"
ErrDuplicateHouse = "player already owns this house type"
ErrMaxHousesReached = "maximum number of houses reached"
// Default upkeep costs (can be overridden per house type)
DefaultUpkeepCoins = 10000 // 1 gold
DefaultUpkeepCoins = 10000 // 1 gold
DefaultUpkeepStatus = 100
// Item placement constants
MaxItemsPerHouse = 1000
MaxItemStackSize = 100
ItemPlacementRadius = 50.0 // Maximum distance from spawn point
MaxItemsPerHouse = 1000
MaxItemStackSize = 100
ItemPlacementRadius = 50.0 // Maximum distance from spawn point
// House zones configuration
DefaultInstanceLifetime = 3600 // 1 hour in seconds
MaxHouseVisitors = 50 // Maximum concurrent visitors
MaxHouseVisitors = 50 // Maximum concurrent visitors
// Amenity types
AmenityVaultExpansion = 1
AmenityPortal = 2
@ -116,15 +116,15 @@ const (
AmenityBanker = 6
AmenityManagedItems = 7
AmenityTeleporter = 8
// Foreclosure settings
ForeclosureWarningDays = 7 // Days before foreclosure
ForeclosureNoticeDays = 3 // Days of final notice
ForeclosureWarningDays = 7 // Days before foreclosure
ForeclosureNoticeDays = 3 // Days of final notice
// Deposit limits
MinDepositAmount = 1
MaxDepositAmount = 10000000 // 1000 gold
// Search and filtering
MaxSearchResults = 100
SearchTimeout = 5 // seconds
@ -132,43 +132,43 @@ const (
// House type constants for common house types
const (
HouseTypeInn = 1
HouseTypeCottage = 2
HouseTypeApartment = 3
HouseTypeHouse = 4
HouseTypeMansion = 5
HouseTypeKeep = 6
HouseTypeGuildHall = 7
HouseTypeInn = 1
HouseTypeCottage = 2
HouseTypeApartment = 3
HouseTypeHouse = 4
HouseTypeMansion = 5
HouseTypeKeep = 6
HouseTypeGuildHall = 7
HouseTypePrestigeHome = 8
)
// Access permission flags (bitwise)
const (
PermissionEnter = 1 << iota // Can enter the house
PermissionPlace = 1 << iota // Can place items
PermissionRemove = 1 << iota // Can remove items
PermissionMove = 1 << iota // Can move items
PermissionVault = 1 << iota // Can access vault
PermissionDeposit = 1 << iota // Can make deposits
PermissionWithdraw = 1 << iota // Can make withdrawals
PermissionInvite = 1 << iota // Can invite others
PermissionKick = 1 << iota // Can kick visitors
PermissionAdmin = 1 << iota // Full administrative access
PermissionEnter = 1 << iota // Can enter the house
PermissionPlace = 1 << iota // Can place items
PermissionRemove = 1 << iota // Can remove items
PermissionMove = 1 << iota // Can move items
PermissionVault = 1 << iota // Can access vault
PermissionDeposit = 1 << iota // Can make deposits
PermissionWithdraw = 1 << iota // Can make withdrawals
PermissionInvite = 1 << iota // Can invite others
PermissionKick = 1 << iota // Can kick visitors
PermissionAdmin = 1 << iota // Full administrative access
)
// Default permission sets
const (
PermissionsOwner = PermissionEnter | PermissionPlace | PermissionRemove |
PermissionMove | PermissionVault | PermissionDeposit |
PermissionsOwner = PermissionEnter | PermissionPlace | PermissionRemove |
PermissionMove | PermissionVault | PermissionDeposit |
PermissionWithdraw | PermissionInvite | PermissionKick | PermissionAdmin
PermissionsFriend = PermissionEnter | PermissionPlace | PermissionMove |
PermissionsFriend = PermissionEnter | PermissionPlace | PermissionMove |
PermissionVault | PermissionDeposit
PermissionsVisitor = PermissionEnter
PermissionsGuildMember = PermissionEnter | PermissionPlace | PermissionDeposit
PermissionsBanned = 0
)
@ -176,14 +176,14 @@ const (
var AlignmentNames = map[int8]string{
AlignmentAny: "Any",
AlignmentGood: "Good",
AlignmentEvil: "Evil",
AlignmentEvil: "Evil",
AlignmentNeutral: "Neutral",
}
// Transaction reason descriptions
var TransactionReasons = map[int]string{
TransactionPurchase: "House Purchase",
TransactionUpkeep: "Upkeep Payment",
TransactionUpkeep: "Upkeep Payment",
TransactionDeposit: "Escrow Deposit",
TransactionWithdrawal: "Escrow Withdrawal",
TransactionAmenity: "Amenity Purchase",
@ -199,7 +199,7 @@ var AmenityNames = map[int]string{
AmenityVaultExpansion: "Vault Expansion",
AmenityPortal: "Portal",
AmenityMerchant: "Merchant",
AmenityRepairNPC: "Repair NPC",
AmenityRepairNPC: "Repair NPC",
AmenityBroker: "Broker",
AmenityBanker: "Banker",
AmenityManagedItems: "Managed Items",
@ -220,13 +220,13 @@ var HouseTypeNames = map[int]string{
// Default costs for house types (in copper coins)
var DefaultHouseCosts = map[int]int64{
HouseTypeInn: 50000, // 5 gold
HouseTypeCottage: 200000, // 20 gold
HouseTypeApartment: 500000, // 50 gold
HouseTypeHouse: 1000000, // 100 gold
HouseTypeMansion: 5000000, // 500 gold
HouseTypeKeep: 10000000, // 1000 gold
HouseTypeGuildHall: 50000000, // 5000 gold
HouseTypeInn: 50000, // 5 gold
HouseTypeCottage: 200000, // 20 gold
HouseTypeApartment: 500000, // 50 gold
HouseTypeHouse: 1000000, // 100 gold
HouseTypeMansion: 5000000, // 500 gold
HouseTypeKeep: 10000000, // 1000 gold
HouseTypeGuildHall: 50000000, // 5000 gold
HouseTypePrestigeHome: 100000000, // 10000 gold
}
@ -244,13 +244,13 @@ var DefaultHouseStatusCosts = map[int]int64{
// Default upkeep costs (in copper coins per week)
var DefaultHouseUpkeepCosts = map[int]int64{
HouseTypeInn: 5000, // 50 silver
HouseTypeCottage: 10000, // 1 gold
HouseTypeApartment: 25000, // 2.5 gold
HouseTypeHouse: 50000, // 5 gold
HouseTypeMansion: 100000, // 10 gold
HouseTypeKeep: 200000, // 20 gold
HouseTypeGuildHall: 500000, // 50 gold
HouseTypeInn: 5000, // 50 silver
HouseTypeCottage: 10000, // 1 gold
HouseTypeApartment: 25000, // 2.5 gold
HouseTypeHouse: 50000, // 5 gold
HouseTypeMansion: 100000, // 10 gold
HouseTypeKeep: 200000, // 20 gold
HouseTypeGuildHall: 500000, // 50 gold
HouseTypePrestigeHome: 1000000, // 100 gold
}
@ -264,4 +264,4 @@ var DefaultVaultSlots = map[int]int{
HouseTypeKeep: 20,
HouseTypeGuildHall: 24,
HouseTypePrestigeHome: 32,
}
}

View File

@ -25,7 +25,7 @@ func (dhm *DatabaseHousingManager) LoadHouseZones(ctx context.Context) ([]HouseZ
query := `SELECT id, name, zone_id, cost_coin, cost_status, upkeep_coin, upkeep_status,
alignment, guild_level, vault_slots, max_items, max_visitors, upkeep_period, description
FROM houses ORDER BY id`
rows, err := dhm.db.QueryContext(ctx, query)
if err != nil {
return nil, fmt.Errorf("failed to query house zones: %w", err)
@ -77,7 +77,7 @@ func (dhm *DatabaseHousingManager) LoadHouseZone(ctx context.Context, houseID in
query := `SELECT id, name, zone_id, cost_coin, cost_status, upkeep_coin, upkeep_status,
alignment, guild_level, vault_slots, max_items, max_visitors, upkeep_period, description
FROM houses WHERE id = ?`
var zone HouseZoneData
var description *string
@ -155,7 +155,7 @@ func (dhm *DatabaseHousingManager) LoadPlayerHouses(ctx context.Context, charact
status, house_name, visit_permission, public_note, private_note, allow_friends, allow_guild,
require_approval, show_on_directory, allow_decoration, tax_exempt
FROM character_houses WHERE char_id = ? ORDER BY unique_id`
rows, err := dhm.db.QueryContext(ctx, query, characterID)
if err != nil {
return nil, fmt.Errorf("failed to query player houses for character %d: %w", characterID, err)
@ -222,7 +222,7 @@ func (dhm *DatabaseHousingManager) LoadPlayerHouse(ctx context.Context, uniqueID
status, house_name, visit_permission, public_note, private_note, allow_friends, allow_guild,
require_approval, show_on_directory, allow_decoration, tax_exempt
FROM character_houses WHERE unique_id = ?`
var house PlayerHouseData
var upkeepDueTimestamp int64
var houseName, publicNote, privateNote *string
@ -317,7 +317,7 @@ func (dhm *DatabaseHousingManager) DeletePlayerHouse(ctx context.Context, unique
// Delete related data first
tables := []string{
"character_house_deposits",
"character_house_history",
"character_house_history",
"character_house_access",
"character_house_amenities",
"character_house_items",
@ -390,7 +390,7 @@ func (dhm *DatabaseHousingManager) LoadDeposits(ctx context.Context, houseID int
query := `SELECT house_id, timestamp, amount, last_amount, status, last_status, name, character_id
FROM character_house_deposits WHERE house_id = ?
ORDER BY timestamp DESC LIMIT ?`
rows, err := dhm.db.QueryContext(ctx, query, houseID, MaxDepositHistory)
if err != nil {
return nil, fmt.Errorf("failed to query house deposits for house %d: %w", houseID, err)
@ -457,7 +457,7 @@ func (dhm *DatabaseHousingManager) LoadHistory(ctx context.Context, houseID int6
query := `SELECT house_id, timestamp, amount, status, reason, name, character_id, pos_flag, type
FROM character_house_history WHERE house_id = ?
ORDER BY timestamp DESC LIMIT ?`
rows, err := dhm.db.QueryContext(ctx, query, houseID, MaxTransactionHistory)
if err != nil {
return nil, fmt.Errorf("failed to query house history for house %d: %w", houseID, err)
@ -621,8 +621,8 @@ func (dhm *DatabaseHousingManager) SaveHouseAccess(ctx context.Context, houseID
// DeleteHouseAccess removes access for a specific character
func (dhm *DatabaseHousingManager) DeleteHouseAccess(ctx context.Context, houseID int64, characterID int32) error {
_, err := dhm.db.ExecContext(ctx,
"DELETE FROM character_house_access WHERE house_id = ? AND character_id = ?",
_, err := dhm.db.ExecContext(ctx,
"DELETE FROM character_house_access WHERE house_id = ? AND character_id = ?",
houseID, characterID)
if err != nil {
return fmt.Errorf("failed to delete house access: %w", err)
@ -709,8 +709,8 @@ func (dhm *DatabaseHousingManager) SaveHouseAmenity(ctx context.Context, houseID
// DeleteHouseAmenity removes a house amenity
func (dhm *DatabaseHousingManager) DeleteHouseAmenity(ctx context.Context, houseID int64, amenityID int32) error {
_, err := dhm.db.ExecContext(ctx,
"DELETE FROM character_house_amenities WHERE house_id = ? AND id = ?",
_, err := dhm.db.ExecContext(ctx,
"DELETE FROM character_house_amenities WHERE house_id = ? AND id = ?",
houseID, amenityID)
if err != nil {
return fmt.Errorf("failed to delete house amenity: %w", err)
@ -805,8 +805,8 @@ func (dhm *DatabaseHousingManager) SaveHouseItem(ctx context.Context, houseID in
// DeleteHouseItem removes a house item
func (dhm *DatabaseHousingManager) DeleteHouseItem(ctx context.Context, houseID int64, itemID int64) error {
_, err := dhm.db.ExecContext(ctx,
"DELETE FROM character_house_items WHERE house_id = ? AND id = ?",
_, err := dhm.db.ExecContext(ctx,
"DELETE FROM character_house_items WHERE house_id = ? AND id = ?",
houseID, itemID)
if err != nil {
return fmt.Errorf("failed to delete house item: %w", err)
@ -820,7 +820,7 @@ func (dhm *DatabaseHousingManager) DeleteHouseItem(ctx context.Context, houseID
// GetNextHouseID returns the next available house unique ID
func (dhm *DatabaseHousingManager) GetNextHouseID(ctx context.Context) (int64, error) {
query := "SELECT COALESCE(MAX(unique_id), 0) + 1 FROM character_houses"
var nextID int64
err := dhm.db.QueryRowContext(ctx, query).Scan(&nextID)
if err != nil {
@ -836,7 +836,7 @@ func (dhm *DatabaseHousingManager) GetHouseByInstance(ctx context.Context, insta
status, house_name, visit_permission, public_note, private_note, allow_friends, allow_guild,
require_approval, show_on_directory, allow_decoration, tax_exempt
FROM character_houses WHERE instance_id = ?`
var house PlayerHouseData
var upkeepDueTimestamp int64
var houseName, publicNote, privateNote *string
@ -914,7 +914,7 @@ func (dhm *DatabaseHousingManager) GetHousesForUpkeep(ctx context.Context, cutof
FROM character_houses WHERE upkeep_due <= ? AND status = ?`
cutoffTimestamp := cutoffTime.Unix()
rows, err := dhm.db.QueryContext(ctx, query, cutoffTimestamp, HouseStatusActive)
if err != nil {
return nil, fmt.Errorf("failed to query houses for upkeep: %w", err)
@ -985,8 +985,8 @@ func (dhm *DatabaseHousingManager) GetHouseStatistics(ctx context.Context) (*Hou
// Get basic counts
queries := map[string]*int64{
"SELECT COUNT(*) FROM character_houses": &stats.TotalHouses,
"SELECT COUNT(*) FROM character_houses WHERE status = 0": &stats.ActiveHouses,
"SELECT COUNT(*) FROM character_houses WHERE status = 2": &stats.ForelosedHouses,
"SELECT COUNT(*) FROM character_houses WHERE status = 0": &stats.ActiveHouses,
"SELECT COUNT(*) FROM character_houses WHERE status = 2": &stats.ForelosedHouses,
"SELECT COUNT(*) FROM character_house_deposits": &stats.TotalDeposits,
"SELECT COUNT(*) FROM character_house_history WHERE pos_flag = 0": &stats.TotalWithdrawals,
}
@ -1149,4 +1149,4 @@ func (dhm *DatabaseHousingManager) EnsureHousingTables(ctx context.Context) erro
}
return nil
}
}

View File

@ -12,37 +12,37 @@ type HousingDatabase interface {
LoadHouseZone(ctx context.Context, houseID int32) (*HouseZoneData, error)
SaveHouseZone(ctx context.Context, zone *HouseZone) error
DeleteHouseZone(ctx context.Context, houseID int32) error
// Player house operations
LoadPlayerHouses(ctx context.Context, characterID int32) ([]PlayerHouseData, error)
LoadPlayerHouse(ctx context.Context, uniqueID int64) (*PlayerHouseData, error)
SavePlayerHouse(ctx context.Context, house *PlayerHouse) error
DeletePlayerHouse(ctx context.Context, uniqueID int64) error
AddPlayerHouse(ctx context.Context, houseData PlayerHouseData) (int64, error)
// Deposit operations
LoadDeposits(ctx context.Context, houseID int64) ([]HouseDepositData, error)
SaveDeposit(ctx context.Context, houseID int64, deposit HouseDeposit) error
// History operations
LoadHistory(ctx context.Context, houseID int64) ([]HouseHistoryData, error)
AddHistory(ctx context.Context, houseID int64, history HouseHistory) error
// Access operations
LoadHouseAccess(ctx context.Context, houseID int64) ([]HouseAccessData, error)
SaveHouseAccess(ctx context.Context, houseID int64, access []HouseAccess) error
DeleteHouseAccess(ctx context.Context, houseID int64, characterID int32) error
// Amenity operations
LoadHouseAmenities(ctx context.Context, houseID int64) ([]HouseAmenityData, error)
SaveHouseAmenity(ctx context.Context, houseID int64, amenity HouseAmenity) error
DeleteHouseAmenity(ctx context.Context, houseID int64, amenityID int32) error
// Item operations
LoadHouseItems(ctx context.Context, houseID int64) ([]HouseItemData, error)
SaveHouseItem(ctx context.Context, houseID int64, item HouseItem) error
DeleteHouseItem(ctx context.Context, houseID int64, itemID int64) error
// Utility operations
GetNextHouseID(ctx context.Context) (int64, error)
GetHouseByInstance(ctx context.Context, instanceID int32) (*PlayerHouseData, error)
@ -60,24 +60,24 @@ type HousingEventHandler interface {
OnHouseForeclosed(house *PlayerHouse, reason string)
OnHouseTransferred(house *PlayerHouse, fromCharacterID, toCharacterID int32)
OnHouseAbandoned(house *PlayerHouse, characterID int32)
// Financial events
OnDepositMade(house *PlayerHouse, characterID int32, amount int64, status int64)
OnWithdrawalMade(house *PlayerHouse, characterID int32, amount int64, status int64)
OnUpkeepPaid(house *PlayerHouse, amount int64, status int64, automatic bool)
OnUpkeepOverdue(house *PlayerHouse, daysPastDue int)
// Access events
OnAccessGranted(house *PlayerHouse, grantedTo int32, grantedBy int32, accessLevel int8)
OnAccessRevoked(house *PlayerHouse, revokedFrom int32, revokedBy int32)
OnPlayerEntered(house *PlayerHouse, characterID int32)
OnPlayerExited(house *PlayerHouse, characterID int32)
// Item events
OnItemPlaced(house *PlayerHouse, item *HouseItem, placedBy int32)
OnItemRemoved(house *PlayerHouse, item *HouseItem, removedBy int32)
OnItemMoved(house *PlayerHouse, item *HouseItem, movedBy int32)
// Amenity events
OnAmenityPurchased(house *PlayerHouse, amenity *HouseAmenity, purchasedBy int32)
OnAmenityRemoved(house *PlayerHouse, amenity *HouseAmenity, removedBy int32)
@ -92,11 +92,11 @@ type ClientManager interface {
SendHouseVisitWindow(characterID int32, data *HouseVisitPacketData) error
SendHouseUpdate(characterID int32, house *PlayerHouse) error
SendHouseError(characterID int32, errorCode int, message string) error
// Broadcast to multiple clients
BroadcastHouseUpdate(characterIDs []int32, house *PlayerHouse) error
BroadcastHouseEvent(characterIDs []int32, eventType int, data string) error
// Client validation
IsClientConnected(characterID int32) bool
GetClientVersion(characterID int32) int
@ -110,7 +110,7 @@ type PlayerManager interface {
GetPlayerAlignment(characterID int32) int8
GetPlayerGuildLevel(characterID int32) int8
IsPlayerOnline(characterID int32) bool
// Player finances
GetPlayerCoins(characterID int32) (int64, error)
GetPlayerStatus(characterID int32) (int64, error)
@ -118,7 +118,7 @@ type PlayerManager interface {
DeductPlayerStatus(characterID int32, amount int64) error
AddPlayerCoins(characterID int32, amount int64) error
AddPlayerStatus(characterID int32, amount int64) error
// Player validation
CanPlayerAffordHouse(characterID int32, cost int64, statusCost int64) (bool, error)
ValidatePlayerExists(playerName string) (int32, error)
@ -132,7 +132,7 @@ type ItemManager interface {
CreateHouseItem(itemID int32, characterID int32, quantity int32) (*HouseItem, error)
RemoveItemFromPlayer(characterID int32, itemID int32, quantity int32) error
ReturnItemToPlayer(characterID int32, item *HouseItem) error
// Item queries
IsItemPlaceable(itemID int32) bool
GetItemWeight(itemID int32) float32
@ -146,12 +146,12 @@ type ZoneManager interface {
CreateHouseInstance(houseID int32, ownerID int32) (int32, error)
DestroyHouseInstance(instanceID int32) error
GetHouseInstance(instanceID int32) (*HouseInstance, error)
// Player zone operations
MovePlayerToHouse(characterID int32, instanceID int32) error
GetPlayersInHouse(instanceID int32) ([]int32, error)
IsPlayerInHouse(characterID int32) (bool, int32)
// Zone validation
IsHouseZoneValid(zoneID int32) bool
GetHouseSpawnPoint(instanceID int32) (float32, float32, float32, float32, error)
@ -210,14 +210,14 @@ type ZoneInfo struct {
// HouseInstance contains active house instance information
type HouseInstance struct {
InstanceID int32 `json:"instance_id"`
HouseID int64 `json:"house_id"`
OwnerID int32 `json:"owner_id"`
ZoneID int32 `json:"zone_id"`
CreatedTime time.Time `json:"created_time"`
LastActivity time.Time `json:"last_activity"`
CurrentVisitors []int32 `json:"current_visitors"`
IsActive bool `json:"is_active"`
InstanceID int32 `json:"instance_id"`
HouseID int64 `json:"house_id"`
OwnerID int32 `json:"owner_id"`
ZoneID int32 `json:"zone_id"`
CreatedTime time.Time `json:"created_time"`
LastActivity time.Time `json:"last_activity"`
CurrentVisitors []int32 `json:"current_visitors"`
IsActive bool `json:"is_active"`
}
// Adapter interfaces for integration with existing systems
@ -302,7 +302,7 @@ type CacheManager interface {
Get(key string) (interface{}, bool)
Delete(key string) error
Clear() error
// House-specific cache operations
CachePlayerHouses(characterID int32, houses []*PlayerHouse) error
GetCachedPlayerHouses(characterID int32) ([]*PlayerHouse, bool)
@ -317,4 +317,4 @@ type SearchManager interface {
GetRecentHouses(limit int) ([]*PlayerHouse, error)
IndexHouseForSearch(house *PlayerHouse) error
RemoveHouseFromIndex(houseID int64) error
}
}

View File

@ -27,58 +27,58 @@ func (hpb *HousingPacketBuilder) BuildHousePurchasePacket(data *HousePurchasePac
// Start with base packet structure
packet := make([]byte, 0, 512)
// Packet type identifier
packet = append(packet, 0x01) // House Purchase packet type
// House ID (4 bytes)
houseIDBytes := make([]byte, 4)
binary.LittleEndian.PutUint32(houseIDBytes, uint32(data.HouseID))
packet = append(packet, houseIDBytes...)
// House name length and data
nameBytes := []byte(data.Name)
packet = append(packet, byte(len(nameBytes)))
packet = append(packet, nameBytes...)
// Cost in coins (8 bytes)
costCoinBytes := make([]byte, 8)
binary.LittleEndian.PutUint64(costCoinBytes, uint64(data.CostCoin))
packet = append(packet, costCoinBytes...)
// Cost in status (8 bytes)
costStatusBytes := make([]byte, 8)
binary.LittleEndian.PutUint64(costStatusBytes, uint64(data.CostStatus))
packet = append(packet, costStatusBytes...)
// Upkeep in coins (8 bytes)
upkeepCoinBytes := make([]byte, 8)
binary.LittleEndian.PutUint64(upkeepCoinBytes, uint64(data.UpkeepCoin))
packet = append(packet, upkeepCoinBytes...)
// Upkeep in status (8 bytes)
upkeepStatusBytes := make([]byte, 8)
binary.LittleEndian.PutUint64(upkeepStatusBytes, uint64(data.UpkeepStatus))
packet = append(packet, upkeepStatusBytes...)
// Alignment requirement (1 byte)
packet = append(packet, byte(data.Alignment))
// Guild level requirement (1 byte)
packet = append(packet, byte(data.GuildLevel))
// Vault slots (4 bytes)
vaultSlotsBytes := make([]byte, 4)
binary.LittleEndian.PutUint32(vaultSlotsBytes, uint32(data.VaultSlots))
packet = append(packet, vaultSlotsBytes...)
// Description length and data
descBytes := []byte(data.Description)
descLen := make([]byte, 2)
binary.LittleEndian.PutUint16(descLen, uint16(len(descBytes)))
packet = append(packet, descLen...)
packet = append(packet, descBytes...)
return packet, nil
}
@ -89,50 +89,50 @@ func (hpb *HousingPacketBuilder) BuildHousingListPacket(data *HouseListPacketDat
}
packet := make([]byte, 0, 1024)
// Packet type identifier
packet = append(packet, 0x02) // Housing List packet type
// Number of houses (4 bytes)
houseCountBytes := make([]byte, 4)
binary.LittleEndian.PutUint32(houseCountBytes, uint32(len(data.Houses)))
packet = append(packet, houseCountBytes...)
// House entries
for _, house := range data.Houses {
// Unique ID (8 bytes)
uniqueIDBytes := make([]byte, 8)
binary.LittleEndian.PutUint64(uniqueIDBytes, uint64(house.UniqueID))
packet = append(packet, uniqueIDBytes...)
// House name length and data
nameBytes := []byte(house.Name)
packet = append(packet, byte(len(nameBytes)))
packet = append(packet, nameBytes...)
// House type length and data
typeBytes := []byte(house.HouseType)
packet = append(packet, byte(len(typeBytes)))
packet = append(packet, typeBytes...)
// Upkeep due timestamp (8 bytes)
upkeepDueBytes := make([]byte, 8)
binary.LittleEndian.PutUint64(upkeepDueBytes, uint64(house.UpkeepDue.Unix()))
packet = append(packet, upkeepDueBytes...)
// Escrow coins (8 bytes)
escrowCoinsBytes := make([]byte, 8)
binary.LittleEndian.PutUint64(escrowCoinsBytes, uint64(house.EscrowCoins))
packet = append(packet, escrowCoinsBytes...)
// Escrow status (8 bytes)
escrowStatusBytes := make([]byte, 8)
binary.LittleEndian.PutUint64(escrowStatusBytes, uint64(house.EscrowStatus))
packet = append(packet, escrowStatusBytes...)
// House status (1 byte)
packet = append(packet, byte(house.Status))
// Can enter flag (1 byte)
if house.CanEnter {
packet = append(packet, 0x01)
@ -140,7 +140,7 @@ func (hpb *HousingPacketBuilder) BuildHousingListPacket(data *HouseListPacketDat
packet = append(packet, 0x00)
}
}
return packet, nil
}
@ -151,145 +151,145 @@ func (hpb *HousingPacketBuilder) BuildBaseHouseWindowPacket(data *BaseHouseWindo
}
packet := make([]byte, 0, 2048)
// Packet type identifier
packet = append(packet, 0x03) // Base House Window packet type
// House info
uniqueIDBytes := make([]byte, 8)
binary.LittleEndian.PutUint64(uniqueIDBytes, uint64(data.HouseInfo.UniqueID))
packet = append(packet, uniqueIDBytes...)
// House name
nameBytes := []byte(data.HouseInfo.Name)
packet = append(packet, byte(len(nameBytes)))
packet = append(packet, nameBytes...)
// House type
typeBytes := []byte(data.HouseInfo.HouseType)
packet = append(packet, byte(len(typeBytes)))
packet = append(packet, typeBytes...)
// Upkeep due
upkeepDueBytes := make([]byte, 8)
binary.LittleEndian.PutUint64(upkeepDueBytes, uint64(data.HouseInfo.UpkeepDue.Unix()))
packet = append(packet, upkeepDueBytes...)
// Escrow balances
escrowCoinsBytes := make([]byte, 8)
binary.LittleEndian.PutUint64(escrowCoinsBytes, uint64(data.HouseInfo.EscrowCoins))
packet = append(packet, escrowCoinsBytes...)
escrowStatusBytes := make([]byte, 8)
binary.LittleEndian.PutUint64(escrowStatusBytes, uint64(data.HouseInfo.EscrowStatus))
packet = append(packet, escrowStatusBytes...)
// Recent deposits count and data
depositCountBytes := make([]byte, 4)
binary.LittleEndian.PutUint32(depositCountBytes, uint32(len(data.RecentDeposits)))
packet = append(packet, depositCountBytes...)
for _, deposit := range data.RecentDeposits {
// Timestamp (8 bytes)
timestampBytes := make([]byte, 8)
binary.LittleEndian.PutUint64(timestampBytes, uint64(deposit.Timestamp.Unix()))
packet = append(packet, timestampBytes...)
// Amount (8 bytes)
amountBytes := make([]byte, 8)
binary.LittleEndian.PutUint64(amountBytes, uint64(deposit.Amount))
packet = append(packet, amountBytes...)
// Status (8 bytes)
statusBytes := make([]byte, 8)
binary.LittleEndian.PutUint64(statusBytes, uint64(deposit.Status))
packet = append(packet, statusBytes...)
// Player name
nameBytes := []byte(deposit.Name)
packet = append(packet, byte(len(nameBytes)))
packet = append(packet, nameBytes...)
}
// Recent history count and data
historyCountBytes := make([]byte, 4)
binary.LittleEndian.PutUint32(historyCountBytes, uint32(len(data.RecentHistory)))
packet = append(packet, historyCountBytes...)
for _, history := range data.RecentHistory {
// Timestamp (8 bytes)
timestampBytes := make([]byte, 8)
binary.LittleEndian.PutUint64(timestampBytes, uint64(history.Timestamp.Unix()))
packet = append(packet, timestampBytes...)
// Amount (8 bytes)
amountBytes := make([]byte, 8)
binary.LittleEndian.PutUint64(amountBytes, uint64(history.Amount))
packet = append(packet, amountBytes...)
// Status (8 bytes)
statusBytes := make([]byte, 8)
binary.LittleEndian.PutUint64(statusBytes, uint64(history.Status))
packet = append(packet, statusBytes...)
// Positive flag (1 byte)
packet = append(packet, byte(history.PosFlag))
// Transaction type (4 bytes)
typeBytes := make([]byte, 4)
binary.LittleEndian.PutUint32(typeBytes, uint32(history.Type))
packet = append(packet, typeBytes...)
// Reason length and data
reasonBytes := []byte(history.Reason)
packet = append(packet, byte(len(reasonBytes)))
packet = append(packet, reasonBytes...)
// Player name
nameBytes := []byte(history.Name)
packet = append(packet, byte(len(nameBytes)))
packet = append(packet, nameBytes...)
}
// Amenities count and data
amenityCountBytes := make([]byte, 4)
binary.LittleEndian.PutUint32(amenityCountBytes, uint32(len(data.Amenities)))
packet = append(packet, amenityCountBytes...)
for _, amenity := range data.Amenities {
// Amenity ID (4 bytes)
idBytes := make([]byte, 4)
binary.LittleEndian.PutUint32(idBytes, uint32(amenity.ID))
packet = append(packet, idBytes...)
// Type (4 bytes)
typeBytes := make([]byte, 4)
binary.LittleEndian.PutUint32(typeBytes, uint32(amenity.Type))
packet = append(packet, typeBytes...)
// Name
nameBytes := []byte(amenity.Name)
packet = append(packet, byte(len(nameBytes)))
packet = append(packet, nameBytes...)
// Position (12 bytes - 3 floats)
xBytes := make([]byte, 4)
binary.LittleEndian.PutUint32(xBytes, math.Float32bits(amenity.X))
packet = append(packet, xBytes...)
yBytes := make([]byte, 4)
binary.LittleEndian.PutUint32(yBytes, math.Float32bits(amenity.Y))
packet = append(packet, yBytes...)
zBytes := make([]byte, 4)
binary.LittleEndian.PutUint32(zBytes, math.Float32bits(amenity.Z))
packet = append(packet, zBytes...)
// Heading (4 bytes)
headingBytes := make([]byte, 4)
binary.LittleEndian.PutUint32(headingBytes, math.Float32bits(amenity.Heading))
packet = append(packet, headingBytes...)
// Is active flag (1 byte)
if amenity.IsActive {
packet = append(packet, 0x01)
@ -297,17 +297,17 @@ func (hpb *HousingPacketBuilder) BuildBaseHouseWindowPacket(data *BaseHouseWindo
packet = append(packet, 0x00)
}
}
// House settings
packet = hpb.appendHouseSettings(packet, data.Settings)
// Can manage flag (1 byte)
if data.CanManage {
packet = append(packet, 0x01)
} else {
packet = append(packet, 0x00)
}
return packet, nil
}
@ -318,51 +318,51 @@ func (hpb *HousingPacketBuilder) BuildHouseVisitPacket(data *HouseVisitPacketDat
}
packet := make([]byte, 0, 1024)
// Packet type identifier
packet = append(packet, 0x04) // House Visit packet type
// Number of available houses (4 bytes)
houseCountBytes := make([]byte, 4)
binary.LittleEndian.PutUint32(houseCountBytes, uint32(len(data.AvailableHouses)))
packet = append(packet, houseCountBytes...)
// House entries
for _, house := range data.AvailableHouses {
// Unique ID (8 bytes)
uniqueIDBytes := make([]byte, 8)
binary.LittleEndian.PutUint64(uniqueIDBytes, uint64(house.UniqueID))
packet = append(packet, uniqueIDBytes...)
// Owner name
ownerBytes := []byte(house.OwnerName)
packet = append(packet, byte(len(ownerBytes)))
packet = append(packet, ownerBytes...)
// House name
nameBytes := []byte(house.HouseName)
packet = append(packet, byte(len(nameBytes)))
packet = append(packet, nameBytes...)
// House type
typeBytes := []byte(house.HouseType)
packet = append(packet, byte(len(typeBytes)))
packet = append(packet, typeBytes...)
// Public note
noteBytes := []byte(house.PublicNote)
noteLen := make([]byte, 2)
binary.LittleEndian.PutUint16(noteLen, uint16(len(noteBytes)))
packet = append(packet, noteLen...)
packet = append(packet, noteBytes...)
// Can visit flag (1 byte)
if house.CanVisit {
packet = append(packet, 0x01)
} else {
packet = append(packet, 0x00)
}
// Requires approval flag (1 byte)
if house.RequiresApproval {
packet = append(packet, 0x01)
@ -370,7 +370,7 @@ func (hpb *HousingPacketBuilder) BuildHouseVisitPacket(data *HouseVisitPacketDat
packet = append(packet, 0x00)
}
}
return packet, nil
}
@ -381,124 +381,124 @@ func (hpb *HousingPacketBuilder) BuildHouseUpdatePacket(house *PlayerHouse) ([]b
}
packet := make([]byte, 0, 256)
// Packet type identifier
packet = append(packet, 0x05) // House Update packet type
// Unique ID (8 bytes)
uniqueIDBytes := make([]byte, 8)
binary.LittleEndian.PutUint64(uniqueIDBytes, uint64(house.UniqueID))
packet = append(packet, uniqueIDBytes...)
// Status (1 byte)
packet = append(packet, byte(house.Status))
// Upkeep due (8 bytes)
upkeepDueBytes := make([]byte, 8)
binary.LittleEndian.PutUint64(upkeepDueBytes, uint64(house.UpkeepDue.Unix()))
packet = append(packet, upkeepDueBytes...)
// Escrow balances (16 bytes total)
escrowCoinsBytes := make([]byte, 8)
binary.LittleEndian.PutUint64(escrowCoinsBytes, uint64(house.EscrowCoins))
packet = append(packet, escrowCoinsBytes...)
escrowStatusBytes := make([]byte, 8)
binary.LittleEndian.PutUint64(escrowStatusBytes, uint64(house.EscrowStatus))
packet = append(packet, escrowStatusBytes...)
return packet, nil
}
// BuildHouseErrorPacket builds an error notification packet
func (hpb *HousingPacketBuilder) BuildHouseErrorPacket(errorCode int, message string) ([]byte, error) {
packet := make([]byte, 0, 256)
// Packet type identifier
packet = append(packet, 0x06) // House Error packet type
// Error code (4 bytes)
errorBytes := make([]byte, 4)
binary.LittleEndian.PutUint32(errorBytes, uint32(errorCode))
packet = append(packet, errorBytes...)
// Error message length and data
messageBytes := []byte(message)
msgLen := make([]byte, 2)
binary.LittleEndian.PutUint16(msgLen, uint16(len(messageBytes)))
packet = append(packet, msgLen...)
packet = append(packet, messageBytes...)
return packet, nil
}
// BuildHouseDepositPacket builds a deposit confirmation packet
func (hpb *HousingPacketBuilder) BuildHouseDepositPacket(houseID int64, amount int64, status int64, newBalance int64, newStatusBalance int64) ([]byte, error) {
packet := make([]byte, 0, 64)
// Packet type identifier
packet = append(packet, 0x07) // House Deposit packet type
// House ID (8 bytes)
houseIDBytes := make([]byte, 8)
binary.LittleEndian.PutUint64(houseIDBytes, uint64(houseID))
packet = append(packet, houseIDBytes...)
// Deposit amount (8 bytes)
amountBytes := make([]byte, 8)
binary.LittleEndian.PutUint64(amountBytes, uint64(amount))
packet = append(packet, amountBytes...)
// Status deposit (8 bytes)
statusBytes := make([]byte, 8)
binary.LittleEndian.PutUint64(statusBytes, uint64(status))
packet = append(packet, statusBytes...)
// New coin balance (8 bytes)
newBalanceBytes := make([]byte, 8)
binary.LittleEndian.PutUint64(newBalanceBytes, uint64(newBalance))
packet = append(packet, newBalanceBytes...)
// New status balance (8 bytes)
newStatusBalanceBytes := make([]byte, 8)
binary.LittleEndian.PutUint64(newStatusBalanceBytes, uint64(newStatusBalance))
packet = append(packet, newStatusBalanceBytes...)
return packet, nil
}
// BuildHouseWithdrawalPacket builds a withdrawal confirmation packet
func (hpb *HousingPacketBuilder) BuildHouseWithdrawalPacket(houseID int64, amount int64, status int64, newBalance int64, newStatusBalance int64) ([]byte, error) {
packet := make([]byte, 0, 64)
// Packet type identifier
packet = append(packet, 0x08) // House Withdrawal packet type
// House ID (8 bytes)
houseIDBytes := make([]byte, 8)
binary.LittleEndian.PutUint64(houseIDBytes, uint64(houseID))
packet = append(packet, houseIDBytes...)
// Withdrawal amount (8 bytes)
amountBytes := make([]byte, 8)
binary.LittleEndian.PutUint64(amountBytes, uint64(amount))
packet = append(packet, amountBytes...)
// Status withdrawal (8 bytes)
statusBytes := make([]byte, 8)
binary.LittleEndian.PutUint64(statusBytes, uint64(status))
packet = append(packet, statusBytes...)
// New coin balance (8 bytes)
newBalanceBytes := make([]byte, 8)
binary.LittleEndian.PutUint64(newBalanceBytes, uint64(newBalance))
packet = append(packet, newBalanceBytes...)
// New status balance (8 bytes)
newStatusBalanceBytes := make([]byte, 8)
binary.LittleEndian.PutUint64(newStatusBalanceBytes, uint64(newStatusBalance))
packet = append(packet, newStatusBalanceBytes...)
return packet, nil
}
@ -509,114 +509,114 @@ func (hpb *HousingPacketBuilder) BuildItemPlacementPacket(item *HouseItem, succe
}
packet := make([]byte, 0, 128)
// Packet type identifier
packet = append(packet, 0x09) // Item Placement packet type
// Item ID (8 bytes)
itemIDBytes := make([]byte, 8)
binary.LittleEndian.PutUint64(itemIDBytes, uint64(item.ID))
packet = append(packet, itemIDBytes...)
// Success flag (1 byte)
if success {
packet = append(packet, 0x01)
} else {
packet = append(packet, 0x00)
}
if success {
// Position (12 bytes - 3 floats)
xBytes := make([]byte, 4)
binary.LittleEndian.PutUint32(xBytes, math.Float32bits(item.X))
packet = append(packet, xBytes...)
yBytes := make([]byte, 4)
binary.LittleEndian.PutUint32(yBytes, math.Float32bits(item.Y))
packet = append(packet, yBytes...)
zBytes := make([]byte, 4)
binary.LittleEndian.PutUint32(zBytes, math.Float32bits(item.Z))
packet = append(packet, zBytes...)
// Heading (4 bytes)
headingBytes := make([]byte, 4)
binary.LittleEndian.PutUint32(headingBytes, math.Float32bits(item.Heading))
packet = append(packet, headingBytes...)
// Pitch/Roll (16 bytes - 4 floats)
pitchXBytes := make([]byte, 4)
binary.LittleEndian.PutUint32(pitchXBytes, math.Float32bits(item.PitchX))
packet = append(packet, pitchXBytes...)
pitchYBytes := make([]byte, 4)
binary.LittleEndian.PutUint32(pitchYBytes, math.Float32bits(item.PitchY))
packet = append(packet, pitchYBytes...)
rollXBytes := make([]byte, 4)
binary.LittleEndian.PutUint32(rollXBytes, math.Float32bits(item.RollX))
packet = append(packet, rollXBytes...)
rollYBytes := make([]byte, 4)
binary.LittleEndian.PutUint32(rollYBytes, math.Float32bits(item.RollY))
packet = append(packet, rollYBytes...)
}
return packet, nil
}
// BuildAccessUpdatePacket builds an access permission update packet
func (hpb *HousingPacketBuilder) BuildAccessUpdatePacket(houseID int64, access []HouseAccess) ([]byte, error) {
packet := make([]byte, 0, 1024)
// Packet type identifier
packet = append(packet, 0x0A) // Access Update packet type
// House ID (8 bytes)
houseIDBytes := make([]byte, 8)
binary.LittleEndian.PutUint64(houseIDBytes, uint64(houseID))
packet = append(packet, houseIDBytes...)
// Access entry count (4 bytes)
countBytes := make([]byte, 4)
binary.LittleEndian.PutUint32(countBytes, uint32(len(access)))
packet = append(packet, countBytes...)
// Access entries
for _, entry := range access {
// Character ID (4 bytes)
charIDBytes := make([]byte, 4)
binary.LittleEndian.PutUint32(charIDBytes, uint32(entry.CharacterID))
packet = append(packet, charIDBytes...)
// Player name
nameBytes := []byte(entry.PlayerName)
packet = append(packet, byte(len(nameBytes)))
packet = append(packet, nameBytes...)
// Access level (1 byte)
packet = append(packet, byte(entry.AccessLevel))
// Permissions (4 bytes)
permBytes := make([]byte, 4)
binary.LittleEndian.PutUint32(permBytes, uint32(entry.Permissions))
packet = append(packet, permBytes...)
// Granted by (4 bytes)
grantedByBytes := make([]byte, 4)
binary.LittleEndian.PutUint32(grantedByBytes, uint32(entry.GrantedBy))
packet = append(packet, grantedByBytes...)
// Granted date (8 bytes)
grantedDateBytes := make([]byte, 8)
binary.LittleEndian.PutUint64(grantedDateBytes, uint64(entry.GrantedDate.Unix()))
packet = append(packet, grantedDateBytes...)
// Expires date (8 bytes)
expiresDateBytes := make([]byte, 8)
binary.LittleEndian.PutUint64(expiresDateBytes, uint64(entry.ExpiresDate.Unix()))
packet = append(packet, expiresDateBytes...)
// Notes
notesBytes := []byte(entry.Notes)
notesLen := make([]byte, 2)
@ -624,7 +624,7 @@ func (hpb *HousingPacketBuilder) BuildAccessUpdatePacket(houseID int64, access [
packet = append(packet, notesLen...)
packet = append(packet, notesBytes...)
}
return packet, nil
}
@ -636,24 +636,24 @@ func (hpb *HousingPacketBuilder) appendHouseSettings(packet []byte, settings Hou
nameBytes := []byte(settings.HouseName)
packet = append(packet, byte(len(nameBytes)))
packet = append(packet, nameBytes...)
// Visit permission (1 byte)
packet = append(packet, byte(settings.VisitPermission))
// Public note
publicNoteBytes := []byte(settings.PublicNote)
publicNoteLen := make([]byte, 2)
binary.LittleEndian.PutUint16(publicNoteLen, uint16(len(publicNoteBytes)))
packet = append(packet, publicNoteLen...)
packet = append(packet, publicNoteBytes...)
// Private note
privateNoteBytes := []byte(settings.PrivateNote)
privateNoteLen := make([]byte, 2)
binary.LittleEndian.PutUint16(privateNoteLen, uint16(len(privateNoteBytes)))
packet = append(packet, privateNoteLen...)
packet = append(packet, privateNoteBytes...)
// Boolean flags (6 bytes)
flags := []bool{
settings.AllowFriends,
@ -663,7 +663,7 @@ func (hpb *HousingPacketBuilder) appendHouseSettings(packet []byte, settings Hou
settings.AllowDecoration,
settings.TaxExempt,
}
for _, flag := range flags {
if flag {
packet = append(packet, 0x01)
@ -671,18 +671,18 @@ func (hpb *HousingPacketBuilder) appendHouseSettings(packet []byte, settings Hou
packet = append(packet, 0x00)
}
}
return packet
}
// ValidatePacketSize checks if packet size is within acceptable limits
func (hpb *HousingPacketBuilder) ValidatePacketSize(packet []byte) error {
maxSize := hpb.getMaxPacketSize()
if len(packet) > maxSize {
return fmt.Errorf("packet size %d exceeds maximum %d", len(packet), maxSize)
}
return nil
}
@ -795,10 +795,10 @@ func (hpb *HousingPacketBuilder) ParseBuyHousePacket(data []byte) (int32, error)
if len(data) < 4 {
return 0, fmt.Errorf("packet too short for buy house request")
}
// Extract house ID (4 bytes)
houseID := int32(binary.LittleEndian.Uint32(data[0:4]))
return houseID, nil
}
@ -807,10 +807,10 @@ func (hpb *HousingPacketBuilder) ParseEnterHousePacket(data []byte) (int64, erro
if len(data) < 8 {
return 0, fmt.Errorf("packet too short for enter house request")
}
// Extract unique house ID (8 bytes)
uniqueID := int64(binary.LittleEndian.Uint64(data[0:8]))
return uniqueID, nil
}
@ -819,16 +819,16 @@ func (hpb *HousingPacketBuilder) ParseDepositPacket(data []byte) (int64, int64,
if len(data) < 24 {
return 0, 0, 0, fmt.Errorf("packet too short for deposit request")
}
// Extract house ID (8 bytes)
houseID := int64(binary.LittleEndian.Uint64(data[0:8]))
// Extract coin amount (8 bytes)
coinAmount := int64(binary.LittleEndian.Uint64(data[8:16]))
// Extract status amount (8 bytes)
statusAmount := int64(binary.LittleEndian.Uint64(data[16:24]))
return houseID, coinAmount, statusAmount, nil
}
@ -837,7 +837,7 @@ func (hpb *HousingPacketBuilder) ParseDepositPacket(data []byte) (int64, int64,
// FormatUpkeepDue formats upkeep due date for display
func FormatUpkeepDue(upkeepDue time.Time) string {
now := time.Now()
if upkeepDue.Before(now) {
duration := now.Sub(upkeepDue)
days := int(duration.Hours() / 24)
@ -860,7 +860,7 @@ func FormatCurrency(amount int64) string {
if amount < 0 {
return fmt.Sprintf("-%s", FormatCurrency(-amount))
}
if amount >= 10000 { // 1 gold = 10000 copper
gold := amount / 10000
remainder := amount % 10000
@ -886,4 +886,4 @@ func FormatCurrency(amount int64) string {
} else {
return fmt.Sprintf("%dc", amount)
}
}
}

View File

@ -7,22 +7,22 @@ import (
// HouseZone represents a house type that can be purchased
type HouseZone struct {
mu sync.RWMutex
ID int32 `json:"id"` // Unique house type identifier
Name string `json:"name"` // House name/type
ZoneID int32 `json:"zone_id"` // Zone where house is located
CostCoin int64 `json:"cost_coin"` // Purchase cost in coins
CostStatus int64 `json:"cost_status"` // Purchase cost in status points
UpkeepCoin int64 `json:"upkeep_coin"` // Upkeep cost in coins
UpkeepStatus int64 `json:"upkeep_status"` // Upkeep cost in status points
Alignment int8 `json:"alignment"` // Alignment requirement
GuildLevel int8 `json:"guild_level"` // Required guild level
VaultSlots int `json:"vault_slots"` // Number of vault storage slots
MaxItems int `json:"max_items"` // Maximum items that can be placed
MaxVisitors int `json:"max_visitors"` // Maximum concurrent visitors
UpkeepPeriod int32 `json:"upkeep_period"` // Upkeep period in seconds
Description string `json:"description"` // Description text
SaveNeeded bool `json:"-"` // Flag indicating if database save is needed
mu sync.RWMutex
ID int32 `json:"id"` // Unique house type identifier
Name string `json:"name"` // House name/type
ZoneID int32 `json:"zone_id"` // Zone where house is located
CostCoin int64 `json:"cost_coin"` // Purchase cost in coins
CostStatus int64 `json:"cost_status"` // Purchase cost in status points
UpkeepCoin int64 `json:"upkeep_coin"` // Upkeep cost in coins
UpkeepStatus int64 `json:"upkeep_status"` // Upkeep cost in status points
Alignment int8 `json:"alignment"` // Alignment requirement
GuildLevel int8 `json:"guild_level"` // Required guild level
VaultSlots int `json:"vault_slots"` // Number of vault storage slots
MaxItems int `json:"max_items"` // Maximum items that can be placed
MaxVisitors int `json:"max_visitors"` // Maximum concurrent visitors
UpkeepPeriod int32 `json:"upkeep_period"` // Upkeep period in seconds
Description string `json:"description"` // Description text
SaveNeeded bool `json:"-"` // Flag indicating if database save is needed
}
// PlayerHouse represents a house owned by a player
@ -48,13 +48,13 @@ type PlayerHouse struct {
// HouseDeposit represents a deposit transaction
type HouseDeposit struct {
Timestamp time.Time `json:"timestamp"` // When deposit was made
Amount int64 `json:"amount"` // Coin amount deposited
LastAmount int64 `json:"last_amount"` // Previous coin amount
Status int64 `json:"status"` // Status points deposited
LastStatus int64 `json:"last_status"` // Previous status points
Name string `json:"name"` // Player who made deposit
CharacterID int32 `json:"character_id"` // Character ID who made deposit
Timestamp time.Time `json:"timestamp"` // When deposit was made
Amount int64 `json:"amount"` // Coin amount deposited
LastAmount int64 `json:"last_amount"` // Previous coin amount
Status int64 `json:"status"` // Status points deposited
LastStatus int64 `json:"last_status"` // Previous status points
Name string `json:"name"` // Player who made deposit
CharacterID int32 `json:"character_id"` // Character ID who made deposit
}
// HouseHistory represents a house transaction history entry
@ -71,99 +71,99 @@ type HouseHistory struct {
// HouseAccess represents access permissions for a player
type HouseAccess struct {
CharacterID int32 `json:"character_id"` // Character being granted access
PlayerName string `json:"player_name"` // Player name
AccessLevel int8 `json:"access_level"` // Access level
Permissions int32 `json:"permissions"` // Permission flags
GrantedBy int32 `json:"granted_by"` // Who granted the access
GrantedDate time.Time `json:"granted_date"` // When access was granted
ExpiresDate time.Time `json:"expires_date"` // When access expires (0 = never)
Notes string `json:"notes"` // Optional notes
CharacterID int32 `json:"character_id"` // Character being granted access
PlayerName string `json:"player_name"` // Player name
AccessLevel int8 `json:"access_level"` // Access level
Permissions int32 `json:"permissions"` // Permission flags
GrantedBy int32 `json:"granted_by"` // Who granted the access
GrantedDate time.Time `json:"granted_date"` // When access was granted
ExpiresDate time.Time `json:"expires_date"` // When access expires (0 = never)
Notes string `json:"notes"` // Optional notes
}
// HouseAmenity represents a purchased house amenity
type HouseAmenity struct {
ID int32 `json:"id"` // Amenity ID
Type int `json:"type"` // Amenity type
Name string `json:"name"` // Amenity name
Cost int64 `json:"cost"` // Purchase cost
StatusCost int64 `json:"status_cost"` // Status cost
PurchaseDate time.Time `json:"purchase_date"` // When purchased
X float32 `json:"x"` // X position
Y float32 `json:"y"` // Y position
Z float32 `json:"z"` // Z position
Heading float32 `json:"heading"` // Heading
IsActive bool `json:"is_active"` // Whether amenity is active
ID int32 `json:"id"` // Amenity ID
Type int `json:"type"` // Amenity type
Name string `json:"name"` // Amenity name
Cost int64 `json:"cost"` // Purchase cost
StatusCost int64 `json:"status_cost"` // Status cost
PurchaseDate time.Time `json:"purchase_date"` // When purchased
X float32 `json:"x"` // X position
Y float32 `json:"y"` // Y position
Z float32 `json:"z"` // Z position
Heading float32 `json:"heading"` // Heading
IsActive bool `json:"is_active"` // Whether amenity is active
}
// HouseItem represents an item placed in a house
type HouseItem struct {
ID int64 `json:"id"` // Item unique ID
ItemID int32 `json:"item_id"` // Item template ID
CharacterID int32 `json:"character_id"` // Who placed the item
X float32 `json:"x"` // X position
Y float32 `json:"y"` // Y position
Z float32 `json:"z"` // Z position
Heading float32 `json:"heading"` // Heading
PitchX float32 `json:"pitch_x"` // Pitch X
PitchY float32 `json:"pitch_y"` // Pitch Y
RollX float32 `json:"roll_x"` // Roll X
RollY float32 `json:"roll_y"` // Roll Y
PlacedDate time.Time `json:"placed_date"` // When item was placed
Quantity int32 `json:"quantity"` // Item quantity
Condition int8 `json:"condition"` // Item condition
House string `json:"house"` // House identifier
ID int64 `json:"id"` // Item unique ID
ItemID int32 `json:"item_id"` // Item template ID
CharacterID int32 `json:"character_id"` // Who placed the item
X float32 `json:"x"` // X position
Y float32 `json:"y"` // Y position
Z float32 `json:"z"` // Z position
Heading float32 `json:"heading"` // Heading
PitchX float32 `json:"pitch_x"` // Pitch X
PitchY float32 `json:"pitch_y"` // Pitch Y
RollX float32 `json:"roll_x"` // Roll X
RollY float32 `json:"roll_y"` // Roll Y
PlacedDate time.Time `json:"placed_date"` // When item was placed
Quantity int32 `json:"quantity"` // Item quantity
Condition int8 `json:"condition"` // Item condition
House string `json:"house"` // House identifier
}
// HouseSettings represents house configuration settings
type HouseSettings struct {
HouseName string `json:"house_name"` // Custom house name
VisitPermission int8 `json:"visit_permission"` // Who can visit
PublicNote string `json:"public_note"` // Public note displayed
PrivateNote string `json:"private_note"` // Private note for owner
AllowFriends bool `json:"allow_friends"` // Allow friends to visit
AllowGuild bool `json:"allow_guild"` // Allow guild members to visit
RequireApproval bool `json:"require_approval"` // Require approval for visits
ShowOnDirectory bool `json:"show_on_directory"` // Show in house directory
AllowDecoration bool `json:"allow_decoration"` // Allow others to decorate
TaxExempt bool `json:"tax_exempt"` // Tax exemption status
HouseName string `json:"house_name"` // Custom house name
VisitPermission int8 `json:"visit_permission"` // Who can visit
PublicNote string `json:"public_note"` // Public note displayed
PrivateNote string `json:"private_note"` // Private note for owner
AllowFriends bool `json:"allow_friends"` // Allow friends to visit
AllowGuild bool `json:"allow_guild"` // Allow guild members to visit
RequireApproval bool `json:"require_approval"` // Require approval for visits
ShowOnDirectory bool `json:"show_on_directory"` // Show in house directory
AllowDecoration bool `json:"allow_decoration"` // Allow others to decorate
TaxExempt bool `json:"tax_exempt"` // Tax exemption status
}
// HousingManager manages the overall housing system
type HousingManager struct {
mu sync.RWMutex
houseZones map[int32]*HouseZone // Available house types
playerHouses map[int64]*PlayerHouse // All player houses by unique ID
characterHouses map[int32][]*PlayerHouse // Houses by character ID
zoneInstances map[int32]map[int32]*PlayerHouse // Houses by zone and instance
database HousingDatabase
clientManager ClientManager
playerManager PlayerManager
itemManager ItemManager
zoneManager ZoneManager
eventHandler HousingEventHandler
logger LogHandler
mu sync.RWMutex
houseZones map[int32]*HouseZone // Available house types
playerHouses map[int64]*PlayerHouse // All player houses by unique ID
characterHouses map[int32][]*PlayerHouse // Houses by character ID
zoneInstances map[int32]map[int32]*PlayerHouse // Houses by zone and instance
database HousingDatabase
clientManager ClientManager
playerManager PlayerManager
itemManager ItemManager
zoneManager ZoneManager
eventHandler HousingEventHandler
logger LogHandler
// Configuration
enableUpkeep bool
enableForeclosure bool
upkeepGracePeriod int32
enableUpkeep bool
enableForeclosure bool
upkeepGracePeriod int32
maxHousesPerPlayer int
enableStatistics bool
enableStatistics bool
}
// HousingStatistics tracks housing system usage
type HousingStatistics struct {
TotalHouses int64 `json:"total_houses"`
ActiveHouses int64 `json:"active_houses"`
ForelosedHouses int64 `json:"foreclosed_houses"`
TotalDeposits int64 `json:"total_deposits"`
TotalWithdrawals int64 `json:"total_withdrawals"`
AverageUpkeepPaid float64 `json:"average_upkeep_paid"`
MostPopularHouseType int32 `json:"most_popular_house_type"`
HousesByType map[int32]int64 `json:"houses_by_type"`
HousesByAlignment map[int8]int64 `json:"houses_by_alignment"`
RevenueByType map[int]int64 `json:"revenue_by_type"`
TopDepositors []PlayerDeposits `json:"top_depositors"`
TotalHouses int64 `json:"total_houses"`
ActiveHouses int64 `json:"active_houses"`
ForelosedHouses int64 `json:"foreclosed_houses"`
TotalDeposits int64 `json:"total_deposits"`
TotalWithdrawals int64 `json:"total_withdrawals"`
AverageUpkeepPaid float64 `json:"average_upkeep_paid"`
MostPopularHouseType int32 `json:"most_popular_house_type"`
HousesByType map[int32]int64 `json:"houses_by_type"`
HousesByAlignment map[int8]int64 `json:"houses_by_alignment"`
RevenueByType map[int]int64 `json:"revenue_by_type"`
TopDepositors []PlayerDeposits `json:"top_depositors"`
}
// PlayerDeposits tracks deposits by player
@ -176,17 +176,17 @@ type PlayerDeposits struct {
// HousingSearchCriteria for searching houses
type HousingSearchCriteria struct {
OwnerName string `json:"owner_name"` // Filter by owner name
HouseType int32 `json:"house_type"` // Filter by house type
Alignment int8 `json:"alignment"` // Filter by alignment
MinCost int64 `json:"min_cost"` // Minimum cost filter
MaxCost int64 `json:"max_cost"` // Maximum cost filter
Zone int32 `json:"zone"` // Filter by zone
VisitableOnly bool `json:"visitable_only"` // Only houses that can be visited
PublicOnly bool `json:"public_only"` // Only publicly accessible houses
NamePattern string `json:"name_pattern"` // Filter by house name pattern
HasAmenities bool `json:"has_amenities"` // Filter houses with amenities
MinVaultSlots int `json:"min_vault_slots"` // Minimum vault slots
OwnerName string `json:"owner_name"` // Filter by owner name
HouseType int32 `json:"house_type"` // Filter by house type
Alignment int8 `json:"alignment"` // Filter by alignment
MinCost int64 `json:"min_cost"` // Minimum cost filter
MaxCost int64 `json:"max_cost"` // Maximum cost filter
Zone int32 `json:"zone"` // Filter by zone
VisitableOnly bool `json:"visitable_only"` // Only houses that can be visited
PublicOnly bool `json:"public_only"` // Only publicly accessible houses
NamePattern string `json:"name_pattern"` // Filter by house name pattern
HasAmenities bool `json:"has_amenities"` // Filter houses with amenities
MinVaultSlots int `json:"min_vault_slots"` // Minimum vault slots
}
// Database record types for data persistence
@ -211,36 +211,36 @@ type HouseZoneData struct {
// PlayerHouseData represents database record for player houses
type PlayerHouseData struct {
UniqueID int64 `json:"unique_id"`
CharacterID int32 `json:"char_id"`
HouseID int32 `json:"house_id"`
InstanceID int32 `json:"instance_id"`
UpkeepDue time.Time `json:"upkeep_due"`
EscrowCoins int64 `json:"escrow_coins"`
EscrowStatus int64 `json:"escrow_status"`
Status int8 `json:"status"`
HouseName string `json:"house_name"`
VisitPermission int8 `json:"visit_permission"`
PublicNote string `json:"public_note"`
PrivateNote string `json:"private_note"`
AllowFriends bool `json:"allow_friends"`
AllowGuild bool `json:"allow_guild"`
RequireApproval bool `json:"require_approval"`
ShowOnDirectory bool `json:"show_on_directory"`
AllowDecoration bool `json:"allow_decoration"`
TaxExempt bool `json:"tax_exempt"`
UniqueID int64 `json:"unique_id"`
CharacterID int32 `json:"char_id"`
HouseID int32 `json:"house_id"`
InstanceID int32 `json:"instance_id"`
UpkeepDue time.Time `json:"upkeep_due"`
EscrowCoins int64 `json:"escrow_coins"`
EscrowStatus int64 `json:"escrow_status"`
Status int8 `json:"status"`
HouseName string `json:"house_name"`
VisitPermission int8 `json:"visit_permission"`
PublicNote string `json:"public_note"`
PrivateNote string `json:"private_note"`
AllowFriends bool `json:"allow_friends"`
AllowGuild bool `json:"allow_guild"`
RequireApproval bool `json:"require_approval"`
ShowOnDirectory bool `json:"show_on_directory"`
AllowDecoration bool `json:"allow_decoration"`
TaxExempt bool `json:"tax_exempt"`
}
// HouseDepositData represents database record for deposits
type HouseDepositData struct {
HouseID int64 `json:"house_id"`
Timestamp time.Time `json:"timestamp"`
Amount int64 `json:"amount"`
LastAmount int64 `json:"last_amount"`
Status int64 `json:"status"`
LastStatus int64 `json:"last_status"`
Name string `json:"name"`
CharacterID int32 `json:"character_id"`
HouseID int64 `json:"house_id"`
Timestamp time.Time `json:"timestamp"`
Amount int64 `json:"amount"`
LastAmount int64 `json:"last_amount"`
Status int64 `json:"status"`
LastStatus int64 `json:"last_status"`
Name string `json:"name"`
CharacterID int32 `json:"character_id"`
}
// HouseHistoryData represents database record for house history
@ -258,15 +258,15 @@ type HouseHistoryData struct {
// HouseAccessData represents database record for house access
type HouseAccessData struct {
HouseID int64 `json:"house_id"`
CharacterID int32 `json:"character_id"`
PlayerName string `json:"player_name"`
AccessLevel int8 `json:"access_level"`
Permissions int32 `json:"permissions"`
GrantedBy int32 `json:"granted_by"`
GrantedDate time.Time `json:"granted_date"`
ExpiresDate time.Time `json:"expires_date"`
Notes string `json:"notes"`
HouseID int64 `json:"house_id"`
CharacterID int32 `json:"character_id"`
PlayerName string `json:"player_name"`
AccessLevel int8 `json:"access_level"`
Permissions int32 `json:"permissions"`
GrantedBy int32 `json:"granted_by"`
GrantedDate time.Time `json:"granted_date"`
ExpiresDate time.Time `json:"expires_date"`
Notes string `json:"notes"`
}
// HouseAmenityData represents database record for house amenities
@ -328,24 +328,24 @@ type HouseListPacketData struct {
// PlayerHouseInfo represents house info for list display
type PlayerHouseInfo struct {
UniqueID int64 `json:"unique_id"`
Name string `json:"name"`
HouseType string `json:"house_type"`
UniqueID int64 `json:"unique_id"`
Name string `json:"name"`
HouseType string `json:"house_type"`
UpkeepDue time.Time `json:"upkeep_due"`
EscrowCoins int64 `json:"escrow_coins"`
EscrowStatus int64 `json:"escrow_status"`
Status int8 `json:"status"`
CanEnter bool `json:"can_enter"`
EscrowCoins int64 `json:"escrow_coins"`
EscrowStatus int64 `json:"escrow_status"`
Status int8 `json:"status"`
CanEnter bool `json:"can_enter"`
}
// BaseHouseWindowPacketData represents data for main house management UI
type BaseHouseWindowPacketData struct {
HouseInfo PlayerHouseInfo `json:"house_info"`
HouseInfo PlayerHouseInfo `json:"house_info"`
RecentDeposits []HouseDeposit `json:"recent_deposits"`
RecentHistory []HouseHistory `json:"recent_history"`
Amenities []HouseAmenity `json:"amenities"`
Settings HouseSettings `json:"settings"`
CanManage bool `json:"can_manage"`
Amenities []HouseAmenity `json:"amenities"`
Settings HouseSettings `json:"settings"`
CanManage bool `json:"can_manage"`
}
// HouseVisitPacketData represents data for house visit UI
@ -355,13 +355,13 @@ type HouseVisitPacketData struct {
// VisitableHouse represents a house that can be visited
type VisitableHouse struct {
UniqueID int64 `json:"unique_id"`
OwnerName string `json:"owner_name"`
HouseName string `json:"house_name"`
HouseType string `json:"house_type"`
PublicNote string `json:"public_note"`
CanVisit bool `json:"can_visit"`
RequiresApproval bool `json:"requires_approval"`
UniqueID int64 `json:"unique_id"`
OwnerName string `json:"owner_name"`
HouseName string `json:"house_name"`
HouseType string `json:"house_type"`
PublicNote string `json:"public_note"`
CanVisit bool `json:"can_visit"`
RequiresApproval bool `json:"requires_approval"`
}
// Event structures for housing system events
@ -378,13 +378,13 @@ type HousingEvent struct {
// Configuration structure for housing system
type HousingConfig struct {
EnableUpkeep bool `json:"enable_upkeep"`
EnableForeclosure bool `json:"enable_foreclosure"`
UpkeepGracePeriod int32 `json:"upkeep_grace_period"` // seconds
MaxHousesPerPlayer int `json:"max_houses_per_player"`
EnableStatistics bool `json:"enable_statistics"`
AutoCleanupInterval int32 `json:"auto_cleanup_interval"` // seconds
MaxHistoryEntries int `json:"max_history_entries"`
MaxDepositEntries int `json:"max_deposit_entries"`
EnableUpkeep bool `json:"enable_upkeep"`
EnableForeclosure bool `json:"enable_foreclosure"`
UpkeepGracePeriod int32 `json:"upkeep_grace_period"` // seconds
MaxHousesPerPlayer int `json:"max_houses_per_player"`
EnableStatistics bool `json:"enable_statistics"`
AutoCleanupInterval int32 `json:"auto_cleanup_interval"` // seconds
MaxHistoryEntries int `json:"max_history_entries"`
MaxDepositEntries int `json:"max_deposit_entries"`
DefaultInstanceLifetime int32 `json:"default_instance_lifetime"` // seconds
}
}

686
internal/items/constants.go Normal file
View File

@ -0,0 +1,686 @@
package items
// Equipment slot constants
const (
BaseEquipment = 0
AppearanceEquipment = 1
MaxEquipment = 2 // max iterations for equipment (base is 0, appearance is 1)
)
// EQ2 slot positions (array indices)
const (
EQ2PrimarySlot = 0
EQ2SecondarySlot = 1
EQ2HeadSlot = 2
EQ2ChestSlot = 3
EQ2ShouldersSlot = 4
EQ2ForearmsSlot = 5
EQ2HandsSlot = 6
EQ2LegsSlot = 7
EQ2FeetSlot = 8
EQ2LRingSlot = 9
EQ2RRingSlot = 10
EQ2EarsSlot1 = 11
EQ2EarsSlot2 = 12
EQ2NeckSlot = 13
EQ2LWristSlot = 14
EQ2RWristSlot = 15
EQ2RangeSlot = 16
EQ2AmmoSlot = 17
EQ2WaistSlot = 18
EQ2CloakSlot = 19
EQ2CharmSlot1 = 20
EQ2CharmSlot2 = 21
EQ2FoodSlot = 22
EQ2DrinkSlot = 23
EQ2TexturesSlot = 24
EQ2HairSlot = 25
EQ2BeardSlot = 26
EQ2WingsSlot = 27
EQ2NakedChestSlot = 28
EQ2NakedLegsSlot = 29
EQ2BackSlot = 30
)
// Original slot positions (for older clients)
const (
EQ2OrigFoodSlot = 18
EQ2OrigDrinkSlot = 19
EQ2DoFCharmSlot1 = 18
EQ2DoFCharmSlot2 = 19
EQ2DoFFoodSlot = 20
EQ2DoFDrinkSlot = 21
)
// Slot bitmasks for equipment validation
const (
PrimarySlot = 1
SecondarySlot = 2
HeadSlot = 4
ChestSlot = 8
ShouldersSlot = 16
ForearmsSlot = 32
HandsSlot = 64
LegsSlot = 128
FeetSlot = 256
LRingSlot = 512
RRingSlot = 1024
EarsSlot1 = 2048
EarsSlot2 = 4096
NeckSlot = 8192
LWristSlot = 16384
RWristSlot = 32768
RangeSlot = 65536
AmmoSlot = 131072
WaistSlot = 262144
CloakSlot = 524288
CharmSlot1 = 1048576
CharmSlot2 = 2097152
FoodSlot = 4194304
DrinkSlot = 8388608
TexturesSlot = 16777216
HairSlot = 33554432
BeardSlot = 67108864
WingsSlot = 134217728
NakedChestSlot = 268435456
NakedLegsSlot = 536870912
BackSlot = 1073741824
OrigFoodSlot = 524288
OrigDrinkSlot = 1048576
DoFFoodSlot = 1048576
DoFDrinkSlot = 2097152
)
// Inventory slot limits and constants
const (
ClassicEQMaxBagSlots = 20
DoFEQMaxBagSlots = 36
NumBankSlots = 12
NumSharedBankSlots = 8
ClassicNumSlots = 22
NumSlots = 25
NumInvSlots = 6
InvSlot1 = 0
InvSlot2 = 50
InvSlot3 = 100
InvSlot4 = 150
InvSlot5 = 200
InvSlot6 = 250
BankSlot1 = 1000
BankSlot2 = 1100
BankSlot3 = 1200
BankSlot4 = 1300
BankSlot5 = 1400
BankSlot6 = 1500
BankSlot7 = 1600
BankSlot8 = 1700
)
// Item flags (bitmask values)
const (
Attuned = 1
Attuneable = 2
Artifact = 4
Lore = 8
Temporary = 16
NoTrade = 32
NoValue = 64
NoZone = 128
NoDestroy = 256
Crafted = 512
GoodOnly = 1024
EvilOnly = 2048
StackLore = 4096
LoreEquip = 8192
NoTransmute = 16384
Cursed = 32768
)
// Item flags2 (bitmask values)
const (
Ornate = 1
Heirloom = 2
AppearanceOnly = 4
Unlocked = 8
Reforged = 16
NoRepair = 32
Ethereal = 64
Refined = 128
NoSalvage = 256
Indestructible = 512
NoExperiment = 1024
HouseLore = 2048
Flags24096 = 4096 // AoM: not used at this time
BuildingBlock = 8192
FreeReforge = 16384
Flags232768 = 32768 // AoM: not used at this time
)
// Item wield types
const (
ItemWieldTypeDual = 1
ItemWieldTypeSingle = 2
ItemWieldTypeTwoHand = 4
)
// Item types
const (
ItemTypeNormal = 0
ItemTypeWeapon = 1
ItemTypeRanged = 2
ItemTypeArmor = 3
ItemTypeShield = 4
ItemTypeBag = 5
ItemTypeSkill = 6
ItemTypeRecipe = 7
ItemTypeFood = 8
ItemTypeBauble = 9
ItemTypeHouse = 10
ItemTypeThrown = 11
ItemTypeHouseContainer = 12
ItemTypeAdornment = 13
ItemTypeGenericAdornment = 14
ItemTypeProfile = 16
ItemTypePattern = 17
ItemTypeArmorset = 18
ItemTypeItemcrate = 18
ItemTypeBook = 19
ItemTypeDecoration = 20
ItemTypeDungeonMaker = 21
ItemTypeMarketplace = 22
)
// Item menu types (bitmask values)
const (
ItemMenuTypeGeneric = 1
ItemMenuTypeEquip = 2
ItemMenuTypeBag = 4
ItemMenuTypeHouse = 8
ItemMenuTypeEmptyBag = 16
ItemMenuTypeScribe = 32
ItemMenuTypeBankBag = 64
ItemMenuTypeInsufficientKnowledge = 128
ItemMenuTypeActivate = 256
ItemMenuTypeBroken = 512
ItemMenuTypeTwoHanded = 1024
ItemMenuTypeAttuned = 2048
ItemMenuTypeAttuneable = 4096
ItemMenuTypeBook = 8192
ItemMenuTypeDisplayCharges = 16384
ItemMenuTypeTest1 = 32768
ItemMenuTypeNamepet = 65536
ItemMenuTypeMentored = 131072
ItemMenuTypeConsume = 262144
ItemMenuTypeUse = 524288
ItemMenuTypeConsumeOff = 1048576
ItemMenuTypeTest3 = 1310720
ItemMenuTypeTest4 = 2097152
ItemMenuTypeTest5 = 4194304
ItemMenuTypeTest6 = 8388608
ItemMenuTypeTest7 = 16777216
ItemMenuTypeTest8 = 33554432
ItemMenuTypeTest9 = 67108864
ItemMenuTypeDamaged = 134217728
ItemMenuTypeBroken2 = 268435456
ItemMenuTypeRedeem = 536870912
ItemMenuTypeTest10 = 1073741824
ItemMenuTypeUnpack = 2147483648
)
// Original item menu types
const (
OrigItemMenuTypeFood = 2048
OrigItemMenuTypeDrink = 4096
OrigItemMenuTypeAttuned = 8192
OrigItemMenuTypeAttuneable = 16384
OrigItemMenuTypeBook = 32768
OrigItemMenuTypeStackable = 65536
OrigItemMenuTypeNamepet = 262144
)
// Item menu type2 flags
const (
ItemMenuType2Test1 = 1
ItemMenuType2Test2 = 2
ItemMenuType2Unpack = 4
ItemMenuType2Test4 = 8
ItemMenuType2Test5 = 16
ItemMenuType2Test6 = 32
ItemMenuType2Test7 = 64
ItemMenuType2Test8 = 128
ItemMenuType2Test9 = 256
ItemMenuType2Test10 = 512
ItemMenuType2Test11 = 1024
ItemMenuType2Test12 = 2048
ItemMenuType2Test13 = 4096
ItemMenuType2Test14 = 8192
ItemMenuType2Test15 = 16384
ItemMenuType2Test16 = 32768
)
// Item tier tags
const (
ItemTagCommon = 2
ItemTagUncommon = 3
ItemTagTreasured = 4
ItemTagLegendary = 7
ItemTagFabled = 9
ItemTagMythical = 12
)
// Broker type flags
const (
ItemBrokerTypeAny = 0xFFFFFFFF
ItemBrokerTypeAny64Bit = 0xFFFFFFFFFFFFFFFF
ItemBrokerTypeAdornment = 134217728
ItemBrokerTypeAmmo = 1024
ItemBrokerTypeAttuneable = 16384
ItemBrokerTypeBag = 2048
ItemBrokerTypeBauble = 16777216
ItemBrokerTypeBook = 128
ItemBrokerTypeChainarmor = 2097152
ItemBrokerTypeCloak = 1073741824
ItemBrokerTypeClotharmor = 524288
ItemBrokerTypeCollectable = 67108864
ItemBrokerTypeCrushweapon = 4
ItemBrokerTypeDrink = 131072
ItemBrokerTypeFood = 4096
ItemBrokerTypeHouseitem = 512
ItemBrokerTypeJewelry = 262144
ItemBrokerTypeLeatherarmor = 1048576
ItemBrokerTypeLore = 8192
ItemBrokerTypeMisc = 1
ItemBrokerTypePierceweapon = 8
ItemBrokerTypePlatearmor = 4194304
ItemBrokerTypePoison = 65536
ItemBrokerTypePotion = 32768
ItemBrokerTypeRecipebook = 8388608
ItemBrokerTypeSalesdisplay = 33554432
ItemBrokerTypeShield = 32
ItemBrokerTypeSlashweapon = 2
ItemBrokerTypeSpellscroll = 64
ItemBrokerTypeTinkered = 268435456
ItemBrokerTypeTradeskill = 256
)
// 2-handed weapon broker types
const (
ItemBrokerType2HCrush = 17179869184
ItemBrokerType2HPierce = 34359738368
ItemBrokerType2HSlash = 8589934592
)
// Broker slot flags
const (
ItemBrokerSlotAny = 0xFFFFFFFF
ItemBrokerSlotAmmo = 65536
ItemBrokerSlotCharm = 524288
ItemBrokerSlotChest = 32
ItemBrokerSlotCloak = 262144
ItemBrokerSlotDrink = 2097152
ItemBrokerSlotEars = 4096
ItemBrokerSlotFeet = 1024
ItemBrokerSlotFood = 1048576
ItemBrokerSlotForearms = 128
ItemBrokerSlotHands = 256
ItemBrokerSlotHead = 16
ItemBrokerSlotLegs = 512
ItemBrokerSlotNeck = 8192
ItemBrokerSlotPrimary = 1
ItemBrokerSlotPrimary2H = 2
ItemBrokerSlotRangeWeapon = 32768
ItemBrokerSlotRing = 2048
ItemBrokerSlotSecondary = 8
ItemBrokerSlotShoulders = 64
ItemBrokerSlotWaist = 131072
ItemBrokerSlotWrist = 16384
)
// Broker stat type flags
const (
ItemBrokerStatTypeNone = 0
ItemBrokerStatTypeDef = 2
ItemBrokerStatTypeStr = 4
ItemBrokerStatTypeSta = 8
ItemBrokerStatTypeAgi = 16
ItemBrokerStatTypeWis = 32
ItemBrokerStatTypeInt = 64
ItemBrokerStatTypeHealth = 128
ItemBrokerStatTypePower = 256
ItemBrokerStatTypeHeat = 512
ItemBrokerStatTypeCold = 1024
ItemBrokerStatTypeMagic = 2048
ItemBrokerStatTypeMental = 4096
ItemBrokerStatTypeDivine = 8192
ItemBrokerStatTypePoison = 16384
ItemBrokerStatTypeDisease = 32768
ItemBrokerStatTypeCrush = 65536
ItemBrokerStatTypeSlash = 131072
ItemBrokerStatTypePierce = 262144
ItemBrokerStatTypeCritical = 524288
ItemBrokerStatTypeDblAttack = 1048576
ItemBrokerStatTypeAbilityMod = 2097152
ItemBrokerStatTypePotency = 4194304
ItemBrokerStatTypeAEAutoattack = 8388608
ItemBrokerStatTypeAttackspeed = 16777216
ItemBrokerStatTypeBlockchance = 33554432
ItemBrokerStatTypeCastingspeed = 67108864
ItemBrokerStatTypeCritbonus = 134217728
ItemBrokerStatTypeCritchance = 268435456
ItemBrokerStatTypeDPS = 536870912
ItemBrokerStatTypeFlurrychance = 1073741824
ItemBrokerStatTypeHategain = 2147483648
ItemBrokerStatTypeMitigation = 4294967296
ItemBrokerStatTypeMultiAttack = 8589934592
ItemBrokerStatTypeRecovery = 17179869184
ItemBrokerStatTypeReuseSpeed = 34359738368
ItemBrokerStatTypeSpellWpndmg = 68719476736
ItemBrokerStatTypeStrikethrough = 137438953472
ItemBrokerStatTypeToughness = 274877906944
ItemBrokerStatTypeWeapondmg = 549755813888
)
// Special slot values
const (
OverflowSlot = 0xFFFFFFFE
SlotInvalid = 0xFFFF
)
// Basic item stats (0-4)
const (
ItemStatStr = 0
ItemStatSta = 1
ItemStatAgi = 2
ItemStatWis = 3
ItemStatInt = 4
)
// Skill-based stats (100+)
const (
ItemStatAdorning = 100
ItemStatAggression = 101
ItemStatArtificing = 102
ItemStatArtistry = 103
ItemStatChemistry = 104
ItemStatCrushing = 105
ItemStatDefense = 106
ItemStatDeflection = 107
ItemStatDisruption = 108
ItemStatFishing = 109
ItemStatFletching = 110
ItemStatFocus = 111
ItemStatForesting = 112
ItemStatGathering = 113
ItemStatMetalShaping = 114
ItemStatMetalworking = 115
ItemStatMining = 116
ItemStatMinistration = 117
ItemStatOrdination = 118
ItemStatParry = 119
ItemStatPiercing = 120
ItemStatRanged = 121
ItemStatSafeFall = 122
ItemStatScribing = 123
ItemStatSculpting = 124
ItemStatSlashing = 125
ItemStatSubjugation = 126
ItemStatSwimming = 127
ItemStatTailoring = 128
ItemStatTinkering = 129
ItemStatTransmuting = 130
ItemStatTrapping = 131
ItemStatWeaponSkills = 132
ItemStatPowerCostReduction = 133
ItemStatSpellAvoidance = 134
)
// Resistance stats (200+)
const (
ItemStatVsPhysical = 200
ItemStatVsHeat = 201 // elemental
ItemStatVsPoison = 202 // noxious
ItemStatVsMagic = 203 // arcane
ItemStatVsSlash = 204
ItemStatVsCrush = 205
ItemStatVsPierce = 206
ItemStatVsCold = 207
ItemStatVsMental = 208
ItemStatVsDivine = 209
ItemStatVsDrowning = 210
ItemStatVsFalling = 211
ItemStatVsPain = 212
ItemStatVsMelee = 213
ItemStatVsDisease = 214
)
// Damage type stats (300+)
const (
ItemStatDmgSlash = 300
ItemStatDmgCrush = 301
ItemStatDmgPierce = 302
ItemStatDmgHeat = 303
ItemStatDmgCold = 304
ItemStatDmgMagic = 305
ItemStatDmgMental = 306
ItemStatDmgDivine = 307
ItemStatDmgDisease = 308
ItemStatDmgPoison = 309
ItemStatDmgDrowning = 310
ItemStatDmgFalling = 311
ItemStatDmgPain = 312
ItemStatDmgMelee = 313
)
// Pool stats (500+)
const (
ItemStatHealth = 500
ItemStatPower = 501
ItemStatConcentration = 502
ItemStatSavagery = 503
)
// Advanced stats (600+)
const (
ItemStatHPRegen = 600
ItemStatManaRegen = 601
ItemStatHPRegenPPT = 602
ItemStatMPRegenPPT = 603
ItemStatCombatHPRegenPPT = 604
ItemStatCombatMPRegenPPT = 605
ItemStatMaxHP = 606
ItemStatMaxHPPerc = 607
ItemStatMaxHPPercFinal = 608
ItemStatSpeed = 609
ItemStatSlow = 610
ItemStatMountSpeed = 611
ItemStatMountAirSpeed = 612
ItemStatLeapSpeed = 613
ItemStatLeapTime = 614
ItemStatGlideEfficiency = 615
ItemStatOffensiveSpeed = 616
ItemStatAttackSpeed = 617
ItemStatSpellWeaponAttackSpeed = 618
ItemStatMaxMana = 619
ItemStatMaxManaPerc = 620
ItemStatMaxAttPerc = 621
ItemStatBlurVision = 622
ItemStatMagicLevelImmunity = 623
ItemStatHateGainMod = 624
ItemStatCombatExpMod = 625
ItemStatTradeskillExpMod = 626
ItemStatAchievementExpMod = 627
ItemStatSizeMod = 628
ItemStatDPS = 629
ItemStatSpellWeaponDPS = 630
ItemStatStealth = 631
ItemStatInvis = 632
ItemStatSeeStealth = 633
ItemStatSeeInvis = 634
ItemStatEffectiveLevelMod = 635
ItemStatRiposteChance = 636
ItemStatParryChance = 637
ItemStatDodgeChance = 638
ItemStatAEAutoattackChance = 639
ItemStatSpellWeaponAEAutoattackChance = 640
ItemStatMultiattackChance = 641
ItemStatPvPDoubleAttackChance = 642
ItemStatSpellWeaponDoubleAttackChance = 643
ItemStatPvPSpellWeaponDoubleAttackChance = 644
ItemStatSpellMultiAttackChance = 645
ItemStatPvPSpellDoubleAttackChance = 646
ItemStatFlurry = 647
ItemStatSpellWeaponFlurry = 648
ItemStatMeleeDamageMultiplier = 649
ItemStatExtraHarvestChance = 650
ItemStatExtraShieldBlockChance = 651
ItemStatItemHPRegenPPT = 652
ItemStatItemPPRegenPPT = 653
ItemStatMeleeCritChance = 654
ItemStatCritAvoidance = 655
ItemStatBeneficialCritChance = 656
ItemStatCritBonus = 657
ItemStatPvPCritBonus = 658
ItemStatPotency = 659
ItemStatPvPPotency = 660
ItemStatUnconsciousHPMod = 661
ItemStatAbilityReuseSpeed = 662
ItemStatAbilityRecoverySpeed = 663
ItemStatAbilityCastingSpeed = 664
ItemStatSpellReuseSpeed = 665
ItemStatMeleeWeaponRange = 666
ItemStatRangedWeaponRange = 667
ItemStatFallingDamageReduction = 668
ItemStatRiposteDamage = 669
ItemStatMinimumDeflectionChance = 670
ItemStatMovementWeave = 671
ItemStatCombatHPRegen = 672
ItemStatCombatManaRegen = 673
ItemStatContestSpeedBoost = 674
ItemStatTrackingAvoidance = 675
ItemStatStealthInvisSpeedMod = 676
ItemStatLootCoin = 677
ItemStatArmorMitigationIncrease = 678
ItemStatAmmoConservation = 679
ItemStatStrikethrough = 680
ItemStatStatusBonus = 681
ItemStatAccuracy = 682
ItemStatCounterstrike = 683
ItemStatShieldBash = 684
ItemStatWeaponDamageBonus = 685
ItemStatWeaponDamageBonusMeleeOnly = 686
ItemStatAdditionalRiposteChance = 687
ItemStatCriticalMitigation = 688
ItemStatPvPToughness = 689
ItemStatPvPLethality = 690
ItemStatStaminaBonus = 691
ItemStatWisdomMitBonus = 692
ItemStatHealReceive = 693
ItemStatHealReceivePerc = 694
ItemStatPvPCriticalMitigation = 695
ItemStatBaseAvoidanceBonus = 696
ItemStatInCombatSavageryRegen = 697
ItemStatOutOfCombatSavageryRegen = 698
ItemStatSavageryRegen = 699
ItemStatSavageryGainMod = 6100
ItemStatMaxSavageryLevel = 6101
ItemStatSpellWeaponDamageBonus = 6102
ItemStatInCombatDissonanceRegen = 6103
ItemStatOutOfCombatDissonanceRegen = 6104
ItemStatDissonanceRegen = 6105
ItemStatDissonanceGainMod = 6106
ItemStatAEAutoattackAvoid = 6107
ItemStatAgnosticDamageBonus = 6108
ItemStatAgnosticHealBonus = 6109
ItemStatTitheGain = 6110
ItemStatFerver = 6111
ItemStatResolve = 6112
ItemStatCombatMitigation = 6113
ItemStatAbilityMitigation = 6114
ItemStatMultiAttackAvoidance = 6115
ItemStatDoubleCastAvoidance = 6116
ItemStatAbilityDoubleCastAvoidance = 6117
ItemStatDamagePerSecondMitigation = 6118
ItemStatFerverMitigation = 6119
ItemStatFlurryAvoidance = 6120
ItemStatWeaponDamageBonusMitigation = 6121
ItemStatAbilityDoubleCastChance = 6122
ItemStatAbilityModifierMitigation = 6123
ItemStatStatusEarned = 6124
)
// Spell/ability modifier stats (700+)
const (
ItemStatSpellDamage = 700
ItemStatHealAmount = 701
ItemStatSpellAndHeal = 702
ItemStatCombatArtDamage = 703
ItemStatSpellAndCombatArtDamage = 704
ItemStatTauntAmount = 705
ItemStatTauntAndCombatArtDamage = 706
ItemStatAbilityModifier = 707
)
// Server-only stats (800+) - never sent to client
const (
ItemStatDurabilityMod = 800
ItemStatDurabilityAdd = 801
ItemStatProgressAdd = 802
ItemStatProgressMod = 803
ItemStatSuccessMod = 804
ItemStatCritSuccessMod = 805
ItemStatExDurabilityMod = 806
ItemStatExDurabilityAdd = 807
ItemStatExProgressMod = 808
ItemStatExProgressAdd = 809
ItemStatExSuccessMod = 810
ItemStatExCritSuccessMod = 811
ItemStatExCritFailureMod = 812
ItemStatRareHarvestChance = 813
ItemStatMaxCrafting = 814
ItemStatComponentRefund = 815
ItemStatBountifulHarvest = 816
)
// Uncontested stats (850+)
const (
ItemStatUncontestedParry = 850
ItemStatUncontestedBlock = 851
ItemStatUncontestedDodge = 852
ItemStatUncontestedRiposte = 853
)
// Display flags
const (
DisplayFlagRedText = 1
DisplayFlagNoGuildStatus = 8
DisplayFlagNoBuyback = 16
DisplayFlagNotForSale = 64
DisplayFlagNoBuy = 128
)
// House store item flags
const (
HouseStoreItemTextRed = 1
HouseStoreUnknownBit2 = 2
HouseStoreUnknownBit4 = 4
HouseStoreForSale = 8
HouseStoreUnknownBit16 = 16
HouseStoreVaultTab = 32
)
// Log category
const (
LogCategoryItems = "Items"
)
// Item validation constants
const (
MaxItemNameLength = 255 // Maximum length for item names
MaxItemDescLength = 1000 // Maximum length for item descriptions
)
// Default item values
const (
DefaultItemCondition = 100 // 100% condition for new items
DefaultItemDurability = 100 // 100% durability for new items
)

View File

@ -0,0 +1,555 @@
package items
import (
"fmt"
"log"
)
// NewEquipmentItemList creates a new equipment item list
func NewEquipmentItemList() *EquipmentItemList {
return &EquipmentItemList{
items: [NumSlots]*Item{},
appearanceType: BaseEquipment,
}
}
// NewEquipmentItemListFromCopy creates a copy of an equipment list
func NewEquipmentItemListFromCopy(source *EquipmentItemList) *EquipmentItemList {
if source == nil {
return NewEquipmentItemList()
}
source.mutex.RLock()
defer source.mutex.RUnlock()
equipment := &EquipmentItemList{
appearanceType: source.appearanceType,
}
// Copy all equipped items
for i, item := range source.items {
if item != nil {
equipment.items[i] = item.Copy()
}
}
return equipment
}
// GetAllEquippedItems returns all equipped items
func (eil *EquipmentItemList) GetAllEquippedItems() []*Item {
eil.mutex.RLock()
defer eil.mutex.RUnlock()
var equippedItems []*Item
for _, item := range eil.items {
if item != nil {
equippedItems = append(equippedItems, item)
}
}
return equippedItems
}
// ResetPackets resets packet data
func (eil *EquipmentItemList) ResetPackets() {
eil.mutex.Lock()
defer eil.mutex.Unlock()
eil.xorPacket = nil
eil.origPacket = nil
}
// HasItem checks if a specific item ID is equipped
func (eil *EquipmentItemList) HasItem(itemID int32) bool {
eil.mutex.RLock()
defer eil.mutex.RUnlock()
for _, item := range eil.items {
if item != nil && item.Details.ItemID == itemID {
return true
}
}
return false
}
// GetNumberOfItems returns the number of equipped items
func (eil *EquipmentItemList) GetNumberOfItems() int8 {
eil.mutex.RLock()
defer eil.mutex.RUnlock()
count := int8(0)
for _, item := range eil.items {
if item != nil {
count++
}
}
return count
}
// GetWeight returns the total weight of equipped items
func (eil *EquipmentItemList) GetWeight() int32 {
eil.mutex.RLock()
defer eil.mutex.RUnlock()
totalWeight := int32(0)
for _, item := range eil.items {
if item != nil {
totalWeight += item.GenericInfo.Weight * int32(item.Details.Count)
}
}
return totalWeight
}
// GetItemFromUniqueID gets an equipped item by unique ID
func (eil *EquipmentItemList) GetItemFromUniqueID(uniqueID int32) *Item {
eil.mutex.RLock()
defer eil.mutex.RUnlock()
for _, item := range eil.items {
if item != nil && int32(item.Details.UniqueID) == uniqueID {
return item
}
}
return nil
}
// GetItemFromItemID gets an equipped item by item template ID
func (eil *EquipmentItemList) GetItemFromItemID(itemID int32) *Item {
eil.mutex.RLock()
defer eil.mutex.RUnlock()
for _, item := range eil.items {
if item != nil && item.Details.ItemID == itemID {
return item
}
}
return nil
}
// SetItem sets an item in a specific equipment slot
func (eil *EquipmentItemList) SetItem(slotID int8, item *Item, locked bool) {
if slotID < 0 || slotID >= NumSlots {
return
}
if !locked {
eil.mutex.Lock()
defer eil.mutex.Unlock()
}
eil.items[slotID] = item
if item != nil {
item.Details.SlotID = int16(slotID)
item.Details.AppearanceType = int16(eil.appearanceType)
}
}
// RemoveItem removes an item from a specific slot
func (eil *EquipmentItemList) RemoveItem(slot int8, deleteItem bool) {
if slot < 0 || slot >= NumSlots {
return
}
eil.mutex.Lock()
defer eil.mutex.Unlock()
item := eil.items[slot]
eil.items[slot] = nil
if deleteItem && item != nil {
item.NeedsDeletion = true
}
}
// GetItem gets an item from a specific slot
func (eil *EquipmentItemList) GetItem(slotID int8) *Item {
if slotID < 0 || slotID >= NumSlots {
return nil
}
eil.mutex.RLock()
defer eil.mutex.RUnlock()
return eil.items[slotID]
}
// AddItem adds an item to the equipment (finds appropriate slot)
func (eil *EquipmentItemList) AddItem(slot int8, item *Item) bool {
if item == nil {
return false
}
// Check if the specific slot is requested and valid
if slot >= 0 && slot < NumSlots {
eil.mutex.Lock()
defer eil.mutex.Unlock()
if eil.items[slot] == nil {
eil.items[slot] = item
item.Details.SlotID = int16(slot)
item.Details.AppearanceType = int16(eil.appearanceType)
return true
}
}
// Find a free slot that the item can be equipped in
freeSlot := eil.GetFreeSlot(item, slot, 0)
if freeSlot < NumSlots {
eil.SetItem(freeSlot, item, false)
return true
}
return false
}
// CheckEquipSlot checks if an item can be equipped in a specific slot
func (eil *EquipmentItemList) CheckEquipSlot(item *Item, slot int8) bool {
if item == nil || slot < 0 || slot >= NumSlots {
return false
}
// Check if item has the required slot data
return item.HasSlot(slot, -1)
}
// CanItemBeEquippedInSlot checks if an item can be equipped in a slot
func (eil *EquipmentItemList) CanItemBeEquippedInSlot(item *Item, slot int8) bool {
if item == nil || slot < 0 || slot >= NumSlots {
return false
}
// Check slot compatibility
if !eil.CheckEquipSlot(item, slot) {
return false
}
// Check if slot is already occupied
eil.mutex.RLock()
defer eil.mutex.RUnlock()
return eil.items[slot] == nil
}
// GetFreeSlot finds a free slot for an item
func (eil *EquipmentItemList) GetFreeSlot(item *Item, preferredSlot int8, version int16) int8 {
if item == nil {
return NumSlots // Invalid slot
}
eil.mutex.RLock()
defer eil.mutex.RUnlock()
// If preferred slot is specified and available, use it
if preferredSlot >= 0 && preferredSlot < NumSlots {
if eil.items[preferredSlot] == nil && item.HasSlot(preferredSlot, -1) {
return preferredSlot
}
}
// Search through all possible slots for this item
for slot := int8(0); slot < NumSlots; slot++ {
if eil.items[slot] == nil && item.HasSlot(slot, -1) {
return slot
}
}
return NumSlots // No free slot found
}
// CheckSlotConflict checks for slot conflicts (lore items, etc.)
func (eil *EquipmentItemList) CheckSlotConflict(item *Item, checkLoreOnly bool, loreStackCount *int16) int32 {
if item == nil {
return 0
}
eil.mutex.RLock()
defer eil.mutex.RUnlock()
// Check for lore conflicts
if item.CheckFlag(Lore) || item.CheckFlag(LoreEquip) {
stackCount := int16(0)
for _, equippedItem := range eil.items {
if equippedItem != nil && equippedItem.Details.ItemID == item.Details.ItemID {
stackCount++
}
}
if loreStackCount != nil {
*loreStackCount = stackCount
}
if stackCount > 0 {
return 1 // Lore conflict
}
}
return 0 // No conflict
}
// GetSlotByItem finds the slot an item is equipped in
func (eil *EquipmentItemList) GetSlotByItem(item *Item) int8 {
if item == nil {
return NumSlots
}
eil.mutex.RLock()
defer eil.mutex.RUnlock()
for slot, equippedItem := range eil.items {
if equippedItem == item {
return int8(slot)
}
}
return NumSlots // Not found
}
// CalculateEquipmentBonuses calculates stat bonuses from all equipped items
func (eil *EquipmentItemList) CalculateEquipmentBonuses() *ItemStatsValues {
eil.mutex.RLock()
defer eil.mutex.RUnlock()
totalBonuses := &ItemStatsValues{}
for _, item := range eil.items {
if item != nil {
// TODO: Implement item bonus calculation
// This should be handled by the master item list
itemBonuses := &ItemStatsValues{} // placeholder
if itemBonuses != nil {
// Add item bonuses to total
totalBonuses.Str += itemBonuses.Str
totalBonuses.Sta += itemBonuses.Sta
totalBonuses.Agi += itemBonuses.Agi
totalBonuses.Wis += itemBonuses.Wis
totalBonuses.Int += itemBonuses.Int
totalBonuses.VsSlash += itemBonuses.VsSlash
totalBonuses.VsCrush += itemBonuses.VsCrush
totalBonuses.VsPierce += itemBonuses.VsPierce
totalBonuses.VsPhysical += itemBonuses.VsPhysical
totalBonuses.VsHeat += itemBonuses.VsHeat
totalBonuses.VsCold += itemBonuses.VsCold
totalBonuses.VsMagic += itemBonuses.VsMagic
totalBonuses.VsMental += itemBonuses.VsMental
totalBonuses.VsDivine += itemBonuses.VsDivine
totalBonuses.VsDisease += itemBonuses.VsDisease
totalBonuses.VsPoison += itemBonuses.VsPoison
totalBonuses.Health += itemBonuses.Health
totalBonuses.Power += itemBonuses.Power
totalBonuses.Concentration += itemBonuses.Concentration
totalBonuses.AbilityModifier += itemBonuses.AbilityModifier
totalBonuses.CriticalMitigation += itemBonuses.CriticalMitigation
totalBonuses.ExtraShieldBlockChance += itemBonuses.ExtraShieldBlockChance
totalBonuses.BeneficialCritChance += itemBonuses.BeneficialCritChance
totalBonuses.CritBonus += itemBonuses.CritBonus
totalBonuses.Potency += itemBonuses.Potency
totalBonuses.HateGainMod += itemBonuses.HateGainMod
totalBonuses.AbilityReuseSpeed += itemBonuses.AbilityReuseSpeed
totalBonuses.AbilityCastingSpeed += itemBonuses.AbilityCastingSpeed
totalBonuses.AbilityRecoverySpeed += itemBonuses.AbilityRecoverySpeed
totalBonuses.SpellReuseSpeed += itemBonuses.SpellReuseSpeed
totalBonuses.SpellMultiAttackChance += itemBonuses.SpellMultiAttackChance
totalBonuses.DPS += itemBonuses.DPS
totalBonuses.AttackSpeed += itemBonuses.AttackSpeed
totalBonuses.MultiAttackChance += itemBonuses.MultiAttackChance
totalBonuses.Flurry += itemBonuses.Flurry
totalBonuses.AEAutoattackChance += itemBonuses.AEAutoattackChance
totalBonuses.Strikethrough += itemBonuses.Strikethrough
totalBonuses.Accuracy += itemBonuses.Accuracy
totalBonuses.OffensiveSpeed += itemBonuses.OffensiveSpeed
totalBonuses.UncontestedParry += itemBonuses.UncontestedParry
totalBonuses.UncontestedBlock += itemBonuses.UncontestedBlock
totalBonuses.UncontestedDodge += itemBonuses.UncontestedDodge
totalBonuses.UncontestedRiposte += itemBonuses.UncontestedRiposte
totalBonuses.SizeMod += itemBonuses.SizeMod
}
}
}
return totalBonuses
}
// SetAppearanceType sets the appearance type (normal or appearance equipment)
func (eil *EquipmentItemList) SetAppearanceType(appearanceType int8) {
eil.mutex.Lock()
defer eil.mutex.Unlock()
eil.appearanceType = appearanceType
// Update all equipped items with new appearance type
for _, item := range eil.items {
if item != nil {
item.Details.AppearanceType = int16(appearanceType)
}
}
}
// GetAppearanceType gets the current appearance type
func (eil *EquipmentItemList) GetAppearanceType() int8 {
eil.mutex.RLock()
defer eil.mutex.RUnlock()
return eil.appearanceType
}
// ValidateEquipment validates all equipped items
func (eil *EquipmentItemList) ValidateEquipment() *ItemValidationResult {
eil.mutex.RLock()
defer eil.mutex.RUnlock()
result := &ItemValidationResult{Valid: true}
for slot, item := range eil.items {
if item != nil {
// Validate item
itemResult := item.Validate()
if !itemResult.Valid {
result.Valid = false
for _, err := range itemResult.Errors {
result.Errors = append(result.Errors, fmt.Sprintf("Slot %d: %s", slot, err))
}
}
// Check slot compatibility
if !item.HasSlot(int8(slot), -1) {
result.Valid = false
result.Errors = append(result.Errors, fmt.Sprintf("Item %s cannot be equipped in slot %d", item.Name, slot))
}
}
}
return result
}
// GetEquippedItemsByType returns equipped items of a specific type
func (eil *EquipmentItemList) GetEquippedItemsByType(itemType int8) []*Item {
eil.mutex.RLock()
defer eil.mutex.RUnlock()
var matchingItems []*Item
for _, item := range eil.items {
if item != nil && item.GenericInfo.ItemType == itemType {
matchingItems = append(matchingItems, item)
}
}
return matchingItems
}
// GetWeapons returns all equipped weapons
func (eil *EquipmentItemList) GetWeapons() []*Item {
return eil.GetEquippedItemsByType(ItemTypeWeapon)
}
// GetArmor returns all equipped armor pieces
func (eil *EquipmentItemList) GetArmor() []*Item {
return eil.GetEquippedItemsByType(ItemTypeArmor)
}
// GetJewelry returns all equipped jewelry
func (eil *EquipmentItemList) GetJewelry() []*Item {
eil.mutex.RLock()
defer eil.mutex.RUnlock()
var jewelry []*Item
// Check ring slots
if eil.items[EQ2LRingSlot] != nil {
jewelry = append(jewelry, eil.items[EQ2LRingSlot])
}
if eil.items[EQ2RRingSlot] != nil {
jewelry = append(jewelry, eil.items[EQ2RRingSlot])
}
// Check ear slots
if eil.items[EQ2EarsSlot1] != nil {
jewelry = append(jewelry, eil.items[EQ2EarsSlot1])
}
if eil.items[EQ2EarsSlot2] != nil {
jewelry = append(jewelry, eil.items[EQ2EarsSlot2])
}
// Check neck slot
if eil.items[EQ2NeckSlot] != nil {
jewelry = append(jewelry, eil.items[EQ2NeckSlot])
}
// Check wrist slots
if eil.items[EQ2LWristSlot] != nil {
jewelry = append(jewelry, eil.items[EQ2LWristSlot])
}
if eil.items[EQ2RWristSlot] != nil {
jewelry = append(jewelry, eil.items[EQ2RWristSlot])
}
return jewelry
}
// HasWeaponEquipped checks if any weapon is equipped
func (eil *EquipmentItemList) HasWeaponEquipped() bool {
weapons := eil.GetWeapons()
return len(weapons) > 0
}
// HasShieldEquipped checks if a shield is equipped
func (eil *EquipmentItemList) HasShieldEquipped() bool {
item := eil.GetItem(EQ2SecondarySlot)
return item != nil && item.IsShield()
}
// HasTwoHandedWeapon checks if a two-handed weapon is equipped
func (eil *EquipmentItemList) HasTwoHandedWeapon() bool {
primaryItem := eil.GetItem(EQ2PrimarySlot)
if primaryItem != nil && primaryItem.IsWeapon() && primaryItem.WeaponInfo != nil {
return primaryItem.WeaponInfo.WieldType == ItemWieldTypeTwoHand
}
return false
}
// CanDualWield checks if dual wielding is possible with current equipment
func (eil *EquipmentItemList) CanDualWield() bool {
primaryItem := eil.GetItem(EQ2PrimarySlot)
secondaryItem := eil.GetItem(EQ2SecondarySlot)
if primaryItem != nil && secondaryItem != nil {
// Both items must be weapons that can be dual wielded
if primaryItem.IsWeapon() && secondaryItem.IsWeapon() {
if primaryItem.WeaponInfo != nil && secondaryItem.WeaponInfo != nil {
return primaryItem.WeaponInfo.WieldType == ItemWieldTypeDual &&
secondaryItem.WeaponInfo.WieldType == ItemWieldTypeDual
}
}
}
return false
}
// String returns a string representation of the equipment list
func (eil *EquipmentItemList) String() string {
eil.mutex.RLock()
defer eil.mutex.RUnlock()
equippedCount := 0
for _, item := range eil.items {
if item != nil {
equippedCount++
}
}
return fmt.Sprintf("EquipmentItemList{Equipped: %d/%d, AppearanceType: %d}",
equippedCount, NumSlots, eil.appearanceType)
}
func init() {
log.Printf("Equipment item list system initialized")
}

View File

@ -0,0 +1,727 @@
package items
import (
"fmt"
"log"
"sync"
"time"
)
// SpellManager defines the interface for spell-related operations needed by items
type SpellManager interface {
// GetSpell retrieves spell information by ID and tier
GetSpell(spellID uint32, tier int8) (Spell, error)
// GetSpellsBySkill gets spells associated with a skill
GetSpellsBySkill(skillID uint32) ([]uint32, error)
// ValidateSpellID checks if a spell ID is valid
ValidateSpellID(spellID uint32) bool
}
// PlayerManager defines the interface for player-related operations needed by items
type PlayerManager interface {
// GetPlayer retrieves player information by ID
GetPlayer(playerID uint32) (Player, error)
// GetPlayerLevel gets a player's current level
GetPlayerLevel(playerID uint32) (int16, error)
// GetPlayerClass gets a player's adventure class
GetPlayerClass(playerID uint32) (int8, error)
// GetPlayerRace gets a player's race
GetPlayerRace(playerID uint32) (int8, error)
// SendMessageToPlayer sends a message to a player
SendMessageToPlayer(playerID uint32, channel int8, message string) error
// GetPlayerName gets a player's name
GetPlayerName(playerID uint32) (string, error)
}
// PacketManager defines the interface for packet-related operations
type PacketManager interface {
// SendPacketToPlayer sends a packet to a specific player
SendPacketToPlayer(playerID uint32, packetData []byte) error
// QueuePacketForPlayer queues a packet for delayed sending
QueuePacketForPlayer(playerID uint32, packetData []byte) error
// GetClientVersion gets the client version for a player
GetClientVersion(playerID uint32) (int16, error)
// SerializeItem serializes an item for network transmission
SerializeItem(item *Item, clientVersion int16, player Player) ([]byte, error)
}
// RuleManager defines the interface for rules/configuration access
type RuleManager interface {
// GetBool retrieves a boolean rule value
GetBool(category, rule string) bool
// GetInt32 retrieves an int32 rule value
GetInt32(category, rule string) int32
// GetFloat retrieves a float rule value
GetFloat(category, rule string) float32
// GetString retrieves a string rule value
GetString(category, rule string) string
}
// DatabaseService defines the interface for item persistence operations
type DatabaseService interface {
// LoadItems loads all item templates from the database
LoadItems(masterList *MasterItemList) error
// SaveItem saves an item template to the database
SaveItem(item *Item) error
// DeleteItem removes an item template from the database
DeleteItem(itemID int32) error
// LoadPlayerItems loads a player's inventory from the database
LoadPlayerItems(playerID uint32) (*PlayerItemList, error)
// SavePlayerItems saves a player's inventory to the database
SavePlayerItems(playerID uint32, itemList *PlayerItemList) error
// LoadPlayerEquipment loads a player's equipment from the database
LoadPlayerEquipment(playerID uint32, appearanceType int8) (*EquipmentItemList, error)
// SavePlayerEquipment saves a player's equipment to the database
SavePlayerEquipment(playerID uint32, equipment *EquipmentItemList) error
// LoadItemStats loads item stat mappings from the database
LoadItemStats() (map[string]int32, map[int32]string, error)
// SaveItemStat saves an item stat mapping to the database
SaveItemStat(statID int32, statName string) error
}
// QuestManager defines the interface for quest-related item operations
type QuestManager interface {
// CheckQuestPrerequisites checks if a player meets quest prerequisites for an item
CheckQuestPrerequisites(playerID uint32, questID int32) bool
// GetQuestRewards gets quest rewards for an item
GetQuestRewards(questID int32) ([]*QuestRewardData, error)
// IsQuestItem checks if an item is a quest item
IsQuestItem(itemID int32) bool
}
// BrokerManager defines the interface for broker/marketplace operations
type BrokerManager interface {
// SearchItems searches for items on the broker
SearchItems(criteria *ItemSearchCriteria) ([]*Item, error)
// ListItem lists an item on the broker
ListItem(playerID uint32, item *Item, price int64) error
// BuyItem purchases an item from the broker
BuyItem(playerID uint32, itemID int32, sellerID uint32) error
// GetItemPrice gets the current market price for an item
GetItemPrice(itemID int32) (int64, error)
}
// CraftingManager defines the interface for crafting-related item operations
type CraftingManager interface {
// CanCraftItem checks if a player can craft an item
CanCraftItem(playerID uint32, itemID int32) bool
// GetCraftingRequirements gets crafting requirements for an item
GetCraftingRequirements(itemID int32) ([]CraftingRequirement, error)
// CraftItem handles item crafting
CraftItem(playerID uint32, itemID int32, quality int8) (*Item, error)
}
// HousingManager defines the interface for housing-related item operations
type HousingManager interface {
// CanPlaceItem checks if an item can be placed in a house
CanPlaceItem(playerID uint32, houseID int32, item *Item) bool
// PlaceItem places an item in a house
PlaceItem(playerID uint32, houseID int32, item *Item, location HouseLocation) error
// RemoveItem removes an item from a house
RemoveItem(playerID uint32, houseID int32, itemID int32) error
// GetHouseItems gets all items in a house
GetHouseItems(houseID int32) ([]*Item, error)
}
// LootManager defines the interface for loot-related operations
type LootManager interface {
// GenerateLoot generates loot for a loot table
GenerateLoot(lootTableID int32, playerLevel int16) ([]*Item, error)
// DistributeLoot distributes loot to players
DistributeLoot(items []*Item, playerIDs []uint32, lootMethod int8) error
// CanLootItem checks if a player can loot an item
CanLootItem(playerID uint32, item *Item) bool
}
// Data structures used by the interfaces
// Spell represents a spell in the game
type Spell interface {
GetID() uint32
GetName() string
GetIcon() uint32
GetIconBackdrop() uint32
GetTier() int8
GetDescription() string
}
// Player represents a player in the game
type Player interface {
GetID() uint32
GetName() string
GetLevel() int16
GetAdventureClass() int8
GetTradeskillClass() int8
GetRace() int8
GetGender() int8
GetAlignment() int8
}
// CraftingRequirement represents a crafting requirement
type CraftingRequirement struct {
ItemID int32 `json:"item_id"`
Quantity int16 `json:"quantity"`
Skill int32 `json:"skill"`
Level int16 `json:"level"`
}
// HouseLocation represents a location within a house
type HouseLocation struct {
X float32 `json:"x"`
Y float32 `json:"y"`
Z float32 `json:"z"`
Heading float32 `json:"heading"`
Pitch float32 `json:"pitch"`
Roll float32 `json:"roll"`
Location int8 `json:"location"` // 0=floor, 1=ceiling, 2=wall
}
// ItemSystemAdapter provides a high-level interface to the complete item system
type ItemSystemAdapter struct {
masterList *MasterItemList
playerLists map[uint32]*PlayerItemList
equipmentLists map[uint32]*EquipmentItemList
spellManager SpellManager
playerManager PlayerManager
packetManager PacketManager
ruleManager RuleManager
databaseService DatabaseService
questManager QuestManager
brokerManager BrokerManager
craftingManager CraftingManager
housingManager HousingManager
lootManager LootManager
mutex sync.RWMutex
}
// NewItemSystemAdapter creates a new item system adapter with all dependencies
func NewItemSystemAdapter(
masterList *MasterItemList,
spellManager SpellManager,
playerManager PlayerManager,
packetManager PacketManager,
ruleManager RuleManager,
databaseService DatabaseService,
questManager QuestManager,
brokerManager BrokerManager,
craftingManager CraftingManager,
housingManager HousingManager,
lootManager LootManager,
) *ItemSystemAdapter {
return &ItemSystemAdapter{
masterList: masterList,
playerLists: make(map[uint32]*PlayerItemList),
equipmentLists: make(map[uint32]*EquipmentItemList),
spellManager: spellManager,
playerManager: playerManager,
packetManager: packetManager,
ruleManager: ruleManager,
databaseService: databaseService,
questManager: questManager,
brokerManager: brokerManager,
craftingManager: craftingManager,
housingManager: housingManager,
lootManager: lootManager,
}
}
// Initialize sets up the item system (loads items from database, etc.)
func (isa *ItemSystemAdapter) Initialize() error {
// Load items from database
err := isa.databaseService.LoadItems(isa.masterList)
if err != nil {
return err
}
// Load item stat mappings
statsStrings, statsIDs, err := isa.databaseService.LoadItemStats()
if err != nil {
return err
}
isa.masterList.mutex.Lock()
isa.masterList.mappedItemStatsStrings = statsStrings
isa.masterList.mappedItemStatTypeIDs = statsIDs
isa.masterList.mutex.Unlock()
return nil
}
// GetPlayerInventory gets or loads a player's inventory
func (isa *ItemSystemAdapter) GetPlayerInventory(playerID uint32) (*PlayerItemList, error) {
isa.mutex.Lock()
defer isa.mutex.Unlock()
if itemList, exists := isa.playerLists[playerID]; exists {
return itemList, nil
}
// Load from database
itemList, err := isa.databaseService.LoadPlayerItems(playerID)
if err != nil {
return nil, err
}
if itemList == nil {
itemList = NewPlayerItemList()
}
isa.playerLists[playerID] = itemList
return itemList, nil
}
// GetPlayerEquipment gets or loads a player's equipment
func (isa *ItemSystemAdapter) GetPlayerEquipment(playerID uint32, appearanceType int8) (*EquipmentItemList, error) {
isa.mutex.Lock()
defer isa.mutex.Unlock()
key := uint32(playerID)*10 + uint32(appearanceType)
if equipment, exists := isa.equipmentLists[key]; exists {
return equipment, nil
}
// Load from database
equipment, err := isa.databaseService.LoadPlayerEquipment(playerID, appearanceType)
if err != nil {
return nil, err
}
if equipment == nil {
equipment = NewEquipmentItemList()
equipment.SetAppearanceType(appearanceType)
}
isa.equipmentLists[key] = equipment
return equipment, nil
}
// SavePlayerData saves a player's item data
func (isa *ItemSystemAdapter) SavePlayerData(playerID uint32) error {
isa.mutex.RLock()
defer isa.mutex.RUnlock()
// Save inventory
if itemList, exists := isa.playerLists[playerID]; exists {
err := isa.databaseService.SavePlayerItems(playerID, itemList)
if err != nil {
return err
}
}
// Save equipment (both normal and appearance)
for key, equipment := range isa.equipmentLists {
if key/10 == playerID {
err := isa.databaseService.SavePlayerEquipment(playerID, equipment)
if err != nil {
return err
}
}
}
return nil
}
// GiveItemToPlayer gives an item to a player
func (isa *ItemSystemAdapter) GiveItemToPlayer(playerID uint32, itemID int32, quantity int16, addType AddItemType) error {
// Get item template
itemTemplate := isa.masterList.GetItem(itemID)
if itemTemplate == nil {
return ErrItemNotFound
}
// Create item instance
item := NewItemFromTemplate(itemTemplate)
item.Details.Count = quantity
// Get player inventory
inventory, err := isa.GetPlayerInventory(playerID)
if err != nil {
return err
}
// Try to add item to inventory
if !inventory.AddItem(item) {
return ErrInsufficientSpace
}
// Send update to player
player, err := isa.playerManager.GetPlayer(playerID)
if err != nil {
return err
}
clientVersion, _ := isa.packetManager.GetClientVersion(playerID)
packetData, err := isa.packetManager.SerializeItem(item, clientVersion, player)
if err != nil {
return err
}
return isa.packetManager.SendPacketToPlayer(playerID, packetData)
}
// RemoveItemFromPlayer removes an item from a player
func (isa *ItemSystemAdapter) RemoveItemFromPlayer(playerID uint32, uniqueID int32, quantity int16) error {
inventory, err := isa.GetPlayerInventory(playerID)
if err != nil {
return err
}
item := inventory.GetItemFromUniqueID(uniqueID, true, true)
if item == nil {
return ErrItemNotFound
}
// Check if item can be removed
if item.IsItemLocked() {
return ErrItemLocked
}
if item.Details.Count <= quantity {
// Remove entire stack
inventory.RemoveItem(item, true, true)
} else {
// Reduce quantity
item.Details.Count -= quantity
}
return nil
}
// EquipItem equips an item for a player
func (isa *ItemSystemAdapter) EquipItem(playerID uint32, uniqueID int32, slot int8, appearanceType int8) error {
inventory, err := isa.GetPlayerInventory(playerID)
if err != nil {
return err
}
equipment, err := isa.GetPlayerEquipment(playerID, appearanceType)
if err != nil {
return err
}
// Get item from inventory
item := inventory.GetItemFromUniqueID(uniqueID, false, true)
if item == nil {
return ErrItemNotFound
}
// Check if item can be equipped
if !equipment.CanItemBeEquippedInSlot(item, slot) {
return ErrCannotEquip
}
// Check class/race/level requirements
player, err := isa.playerManager.GetPlayer(playerID)
if err != nil {
return err
}
if !item.CheckClass(player.GetAdventureClass(), player.GetTradeskillClass()) {
return ErrCannotEquip
}
if !item.CheckClassLevel(player.GetAdventureClass(), player.GetTradeskillClass(), player.GetLevel()) {
return ErrCannotEquip
}
// Remove from inventory
inventory.RemoveItem(item, false, true)
// Check if slot is occupied and unequip current item
currentItem := equipment.GetItem(slot)
if currentItem != nil {
equipment.RemoveItem(slot, false)
inventory.AddItem(currentItem)
}
// Equip new item
equipment.SetItem(slot, item, false)
return nil
}
// UnequipItem unequips an item for a player
func (isa *ItemSystemAdapter) UnequipItem(playerID uint32, slot int8, appearanceType int8) error {
inventory, err := isa.GetPlayerInventory(playerID)
if err != nil {
return err
}
equipment, err := isa.GetPlayerEquipment(playerID, appearanceType)
if err != nil {
return err
}
// Get equipped item
item := equipment.GetItem(slot)
if item == nil {
return ErrItemNotFound
}
// Check if item can be unequipped
if item.IsItemLocked() {
return ErrItemLocked
}
// Remove from equipment
equipment.RemoveItem(slot, false)
// Add to inventory
if !inventory.AddItem(item) {
// Inventory full, add to overflow
inventory.AddOverflowItem(item)
}
return nil
}
// MoveItem moves an item within a player's inventory
func (isa *ItemSystemAdapter) MoveItem(playerID uint32, fromBagID int32, fromSlot int16, toBagID int32, toSlot int16, appearanceType int8) error {
inventory, err := isa.GetPlayerInventory(playerID)
if err != nil {
return err
}
// Get item from source location
item := inventory.GetItem(fromBagID, fromSlot, appearanceType)
if item == nil {
return ErrItemNotFound
}
// Check if item is locked
if item.IsItemLocked() {
return ErrItemLocked
}
// Move item
inventory.MoveItem(item, toBagID, toSlot, appearanceType, true)
return nil
}
// SearchBrokerItems searches for items on the broker
func (isa *ItemSystemAdapter) SearchBrokerItems(criteria *ItemSearchCriteria) ([]*Item, error) {
if isa.brokerManager == nil {
return nil, fmt.Errorf("broker manager not available")
}
return isa.brokerManager.SearchItems(criteria)
}
// CraftItem handles item crafting
func (isa *ItemSystemAdapter) CraftItem(playerID uint32, itemID int32, quality int8) (*Item, error) {
if isa.craftingManager == nil {
return nil, fmt.Errorf("crafting manager not available")
}
// Check if player can craft the item
if !isa.craftingManager.CanCraftItem(playerID, itemID) {
return nil, fmt.Errorf("player cannot craft this item")
}
// Craft the item
return isa.craftingManager.CraftItem(playerID, itemID, quality)
}
// GetPlayerItemStats returns statistics about a player's items
func (isa *ItemSystemAdapter) GetPlayerItemStats(playerID uint32) (map[string]interface{}, error) {
inventory, err := isa.GetPlayerInventory(playerID)
if err != nil {
return nil, err
}
equipment, err := isa.GetPlayerEquipment(playerID, BaseEquipment)
if err != nil {
return nil, err
}
// Calculate equipment bonuses
bonuses := equipment.CalculateEquipmentBonuses()
return map[string]interface{}{
"player_id": playerID,
"total_items": inventory.GetNumberOfItems(),
"equipped_items": equipment.GetNumberOfItems(),
"inventory_weight": inventory.GetWeight(),
"equipment_weight": equipment.GetWeight(),
"free_slots": inventory.GetNumberOfFreeSlots(),
"overflow_items": len(inventory.GetOverflowItemList()),
"stat_bonuses": bonuses,
"last_update": time.Now(),
}, nil
}
// GetSystemStats returns comprehensive statistics about the item system
func (isa *ItemSystemAdapter) GetSystemStats() map[string]interface{} {
isa.mutex.RLock()
defer isa.mutex.RUnlock()
masterStats := isa.masterList.GetStats()
return map[string]interface{}{
"total_item_templates": masterStats.TotalItems,
"items_by_type": masterStats.ItemsByType,
"items_by_tier": masterStats.ItemsByTier,
"active_players": len(isa.playerLists),
"cached_inventories": len(isa.playerLists),
"cached_equipment": len(isa.equipmentLists),
"last_update": time.Now(),
}
}
// ClearPlayerData removes cached data for a player (e.g., when they log out)
func (isa *ItemSystemAdapter) ClearPlayerData(playerID uint32) {
isa.mutex.Lock()
defer isa.mutex.Unlock()
// Remove inventory
delete(isa.playerLists, playerID)
// Remove equipment
keysToDelete := make([]uint32, 0)
for key := range isa.equipmentLists {
if key/10 == playerID {
keysToDelete = append(keysToDelete, key)
}
}
for _, key := range keysToDelete {
delete(isa.equipmentLists, key)
}
}
// ValidatePlayerItems validates all items for a player
func (isa *ItemSystemAdapter) ValidatePlayerItems(playerID uint32) *ItemValidationResult {
result := &ItemValidationResult{Valid: true}
// Validate inventory
inventory, err := isa.GetPlayerInventory(playerID)
if err != nil {
result.Valid = false
result.Errors = append(result.Errors, fmt.Sprintf("Failed to load inventory: %v", err))
return result
}
allItems := inventory.GetAllItems()
for index, item := range allItems {
itemResult := item.Validate()
if !itemResult.Valid {
result.Valid = false
for _, itemErr := range itemResult.Errors {
result.Errors = append(result.Errors, fmt.Sprintf("Inventory item %d: %s", index, itemErr))
}
}
}
// Validate equipment
equipment, err := isa.GetPlayerEquipment(playerID, BaseEquipment)
if err != nil {
result.Valid = false
result.Errors = append(result.Errors, fmt.Sprintf("Failed to load equipment: %v", err))
return result
}
equipResult := equipment.ValidateEquipment()
if !equipResult.Valid {
result.Valid = false
result.Errors = append(result.Errors, equipResult.Errors...)
}
return result
}
// MockImplementations for testing
// MockSpellManager is a mock implementation of SpellManager for testing
type MockSpellManager struct {
spells map[uint32]MockSpell
}
// MockSpell is a mock implementation of Spell for testing
type MockSpell struct {
id uint32
name string
icon uint32
iconBackdrop uint32
tier int8
description string
}
func (ms MockSpell) GetID() uint32 { return ms.id }
func (ms MockSpell) GetName() string { return ms.name }
func (ms MockSpell) GetIcon() uint32 { return ms.icon }
func (ms MockSpell) GetIconBackdrop() uint32 { return ms.iconBackdrop }
func (ms MockSpell) GetTier() int8 { return ms.tier }
func (ms MockSpell) GetDescription() string { return ms.description }
func (msm *MockSpellManager) GetSpell(spellID uint32, tier int8) (Spell, error) {
if spell, exists := msm.spells[spellID]; exists {
return spell, nil
}
return nil, fmt.Errorf("spell not found: %d", spellID)
}
func (msm *MockSpellManager) GetSpellsBySkill(skillID uint32) ([]uint32, error) {
return []uint32{}, nil
}
func (msm *MockSpellManager) ValidateSpellID(spellID uint32) bool {
_, exists := msm.spells[spellID]
return exists
}
// NewMockSpellManager creates a new mock spell manager
func NewMockSpellManager() *MockSpellManager {
return &MockSpellManager{
spells: make(map[uint32]MockSpell),
}
}
// AddMockSpell adds a mock spell for testing
func (msm *MockSpellManager) AddMockSpell(id uint32, name string, icon uint32, tier int8, description string) {
msm.spells[id] = MockSpell{
id: id,
name: name,
icon: icon,
iconBackdrop: icon + 1000,
tier: tier,
description: description,
}
}
func init() {
log.Printf("Item system interfaces initialized")
}

1009
internal/items/item.go Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,829 @@
package items
import (
"fmt"
"testing"
)
func TestNewItem(t *testing.T) {
item := NewItem()
if item == nil {
t.Fatal("NewItem returned nil")
}
if item.Details.UniqueID <= 0 {
t.Error("New item should have a valid unique ID")
}
if item.Details.Count != 1 {
t.Errorf("Expected count 1, got %d", item.Details.Count)
}
if item.GenericInfo.Condition != DefaultItemCondition {
t.Errorf("Expected condition %d, got %d", DefaultItemCondition, item.GenericInfo.Condition)
}
}
func TestNewItemFromTemplate(t *testing.T) {
// Create template
template := NewItem()
template.Name = "Test Sword"
template.Description = "A test weapon"
template.Details.ItemID = 12345
template.Details.Icon = 100
template.GenericInfo.ItemType = ItemTypeWeapon
template.WeaponInfo = &WeaponInfo{
WieldType: ItemWieldTypeSingle,
DamageLow1: 10,
DamageHigh1: 20,
Delay: 30,
Rating: 1.5,
}
// Create from template
item := NewItemFromTemplate(template)
if item == nil {
t.Fatal("NewItemFromTemplate returned nil")
}
if item.Name != template.Name {
t.Errorf("Expected name %s, got %s", template.Name, item.Name)
}
if item.Details.ItemID != template.Details.ItemID {
t.Errorf("Expected item ID %d, got %d", template.Details.ItemID, item.Details.ItemID)
}
if item.Details.UniqueID == template.Details.UniqueID {
t.Error("New item should have different unique ID from template")
}
if item.WeaponInfo == nil {
t.Fatal("Weapon info should be copied")
}
if item.WeaponInfo.DamageLow1 != template.WeaponInfo.DamageLow1 {
t.Errorf("Expected damage %d, got %d", template.WeaponInfo.DamageLow1, item.WeaponInfo.DamageLow1)
}
}
func TestItemCopy(t *testing.T) {
original := NewItem()
original.Name = "Original Item"
original.Details.ItemID = 999
original.AddStat(&ItemStat{
StatName: "Strength",
StatType: ItemStatStr,
Value: 10,
})
copy := original.Copy()
if copy == nil {
t.Fatal("Copy returned nil")
}
if copy.Name != original.Name {
t.Errorf("Expected name %s, got %s", original.Name, copy.Name)
}
if copy.Details.UniqueID == original.Details.UniqueID {
t.Error("Copy should have different unique ID")
}
if len(copy.ItemStats) != len(original.ItemStats) {
t.Errorf("Expected %d stats, got %d", len(original.ItemStats), len(copy.ItemStats))
}
// Test nil copy
var nilItem *Item
nilCopy := nilItem.Copy()
if nilCopy != nil {
t.Error("Copy of nil should return nil")
}
}
func TestItemValidation(t *testing.T) {
// Valid item
item := NewItem()
item.Name = "Valid Item"
item.Details.ItemID = 100
result := item.Validate()
if !result.Valid {
t.Errorf("Valid item should pass validation: %v", result.Errors)
}
// Invalid item - no name
invalidItem := NewItem()
invalidItem.Details.ItemID = 100
result = invalidItem.Validate()
if result.Valid {
t.Error("Item without name should fail validation")
}
// Invalid item - negative count
invalidItem2 := NewItem()
invalidItem2.Name = "Invalid Item"
invalidItem2.Details.ItemID = 100
invalidItem2.Details.Count = -1
result = invalidItem2.Validate()
if result.Valid {
t.Error("Item with negative count should fail validation")
}
}
func TestItemStats(t *testing.T) {
item := NewItem()
// Add a stat
stat := &ItemStat{
StatName: "Strength",
StatType: ItemStatStr,
Value: 15,
Level: 1,
}
item.AddStat(stat)
if len(item.ItemStats) != 1 {
t.Errorf("Expected 1 stat, got %d", len(item.ItemStats))
}
// Check if item has stat
if !item.HasStat(0, "Strength") {
t.Error("Item should have Strength stat")
}
if !item.HasStat(uint32(ItemStatStr), "") {
t.Error("Item should have STR stat by ID")
}
if item.HasStat(0, "Nonexistent") {
t.Error("Item should not have nonexistent stat")
}
// Add stat by values
item.AddStatByValues(ItemStatAgi, 0, 10, 1, "Agility")
if len(item.ItemStats) != 2 {
t.Errorf("Expected 2 stats, got %d", len(item.ItemStats))
}
}
func TestItemFlags(t *testing.T) {
item := NewItem()
// Set flags
item.GenericInfo.ItemFlags = Attuned | NoTrade
item.GenericInfo.ItemFlags2 = Heirloom | Ornate
// Test flag checking
if !item.CheckFlag(Attuned) {
t.Error("Item should be attuned")
}
if !item.CheckFlag(NoTrade) {
t.Error("Item should be no-trade")
}
if item.CheckFlag(Lore) {
t.Error("Item should not be lore")
}
if !item.CheckFlag2(Heirloom) {
t.Error("Item should be heirloom")
}
if !item.CheckFlag2(Ornate) {
t.Error("Item should be ornate")
}
if item.CheckFlag2(Refined) {
t.Error("Item should not be refined")
}
}
func TestItemLocking(t *testing.T) {
item := NewItem()
// Item should not be locked initially
if item.IsItemLocked() {
t.Error("New item should not be locked")
}
// Lock for crafting
if !item.TryLockItem(LockReasonCrafting) {
t.Error("Should be able to lock item for crafting")
}
if !item.IsItemLocked() {
t.Error("Item should be locked")
}
if !item.IsItemLockedFor(LockReasonCrafting) {
t.Error("Item should be locked for crafting")
}
if item.IsItemLockedFor(LockReasonHouse) {
t.Error("Item should not be locked for house")
}
// Try to lock for another reason while already locked
if item.TryLockItem(LockReasonHouse) {
t.Error("Should not be able to lock for different reason")
}
// Unlock
if !item.TryUnlockItem(LockReasonCrafting) {
t.Error("Should be able to unlock item")
}
if item.IsItemLocked() {
t.Error("Item should not be locked after unlock")
}
}
func TestItemTypes(t *testing.T) {
item := NewItem()
// Test weapon
item.GenericInfo.ItemType = ItemTypeWeapon
if !item.IsWeapon() {
t.Error("Item should be a weapon")
}
if item.IsArmor() {
t.Error("Item should not be armor")
}
// Test armor
item.GenericInfo.ItemType = ItemTypeArmor
if !item.IsArmor() {
t.Error("Item should be armor")
}
if item.IsWeapon() {
t.Error("Item should not be a weapon")
}
// Test bag
item.GenericInfo.ItemType = ItemTypeBag
if !item.IsBag() {
t.Error("Item should be a bag")
}
// Test food
item.GenericInfo.ItemType = ItemTypeFood
item.FoodInfo = &FoodInfo{Type: 1} // Food
if !item.IsFood() {
t.Error("Item should be food")
}
if !item.IsFoodFood() {
t.Error("Item should be food (not drink)")
}
if item.IsFoodDrink() {
t.Error("Item should not be drink")
}
// Test drink
item.FoodInfo.Type = 0 // Drink
if !item.IsFoodDrink() {
t.Error("Item should be drink")
}
if item.IsFoodFood() {
t.Error("Item should not be food")
}
}
func TestMasterItemList(t *testing.T) {
masterList := NewMasterItemList()
if masterList == nil {
t.Fatal("NewMasterItemList returned nil")
}
// Initial state
if masterList.GetItemCount() != 0 {
t.Error("New master list should be empty")
}
// Add item
item := NewItem()
item.Name = "Test Item"
item.Details.ItemID = 12345
masterList.AddItem(item)
if masterList.GetItemCount() != 1 {
t.Errorf("Expected 1 item, got %d", masterList.GetItemCount())
}
// Get item
retrieved := masterList.GetItem(12345)
if retrieved == nil {
t.Fatal("GetItem returned nil")
}
if retrieved.Name != item.Name {
t.Errorf("Expected name %s, got %s", item.Name, retrieved.Name)
}
// Get by name
byName := masterList.GetItemByName("Test Item")
if byName == nil {
t.Fatal("GetItemByName returned nil")
}
if byName.Details.ItemID != item.Details.ItemID {
t.Errorf("Expected item ID %d, got %d", item.Details.ItemID, byName.Details.ItemID)
}
// Get non-existent item
nonExistent := masterList.GetItem(99999)
if nonExistent != nil {
t.Error("GetItem should return nil for non-existent item")
}
// Test stats
stats := masterList.GetStats()
if stats.TotalItems != 1 {
t.Errorf("Expected 1 total item, got %d", stats.TotalItems)
}
}
func TestMasterItemListStatMapping(t *testing.T) {
masterList := NewMasterItemList()
// Test getting stat ID by name
strID := masterList.GetItemStatIDByName("strength")
if strID == 0 {
t.Error("Should find strength stat ID")
}
// Test getting stat name by ID
strName := masterList.GetItemStatNameByID(ItemStatStr)
if strName == "" {
t.Error("Should find stat name for STR")
}
// Add custom stat mapping
masterList.AddMappedItemStat(9999, "custom stat")
customID := masterList.GetItemStatIDByName("custom stat")
if customID != 9999 {
t.Errorf("Expected custom stat ID 9999, got %d", customID)
}
customName := masterList.GetItemStatNameByID(9999)
if customName != "custom stat" {
t.Errorf("Expected 'custom stat', got '%s'", customName)
}
}
func TestPlayerItemList(t *testing.T) {
playerList := NewPlayerItemList()
if playerList == nil {
t.Fatal("NewPlayerItemList returned nil")
}
// Initial state
if playerList.GetNumberOfItems() != 0 {
t.Error("New player list should be empty")
}
// Create test item
item := NewItem()
item.Name = "Player Item"
item.Details.ItemID = 1001
item.Details.BagID = 0
item.Details.SlotID = 0
// Add item
if !playerList.AddItem(item) {
t.Error("Should be able to add item")
}
if playerList.GetNumberOfItems() != 1 {
t.Errorf("Expected 1 item, got %d", playerList.GetNumberOfItems())
}
// Get item
retrieved := playerList.GetItem(0, 0, BaseEquipment)
if retrieved == nil {
t.Fatal("GetItem returned nil")
}
if retrieved.Name != item.Name {
t.Errorf("Expected name %s, got %s", item.Name, retrieved.Name)
}
// Test HasItem
if !playerList.HasItem(1001, false) {
t.Error("Player should have item 1001")
}
if playerList.HasItem(9999, false) {
t.Error("Player should not have item 9999")
}
// Remove item
playerList.RemoveItem(item, true, true)
if playerList.GetNumberOfItems() != 0 {
t.Errorf("Expected 0 items after removal, got %d", playerList.GetNumberOfItems())
}
}
func TestPlayerItemListOverflow(t *testing.T) {
playerList := NewPlayerItemList()
// Add item to overflow
item := NewItem()
item.Name = "Overflow Item"
item.Details.ItemID = 2001
if !playerList.AddOverflowItem(item) {
t.Error("Should be able to add overflow item")
}
// Check overflow
overflowItem := playerList.GetOverflowItem()
if overflowItem == nil {
t.Fatal("GetOverflowItem returned nil")
}
if overflowItem.Name != item.Name {
t.Errorf("Expected name %s, got %s", item.Name, overflowItem.Name)
}
// Get all overflow items
overflowItems := playerList.GetOverflowItemList()
if len(overflowItems) != 1 {
t.Errorf("Expected 1 overflow item, got %d", len(overflowItems))
}
// Remove overflow item
playerList.RemoveOverflowItem(item)
overflowItems = playerList.GetOverflowItemList()
if len(overflowItems) != 0 {
t.Errorf("Expected 0 overflow items, got %d", len(overflowItems))
}
}
func TestEquipmentItemList(t *testing.T) {
equipment := NewEquipmentItemList()
if equipment == nil {
t.Fatal("NewEquipmentItemList returned nil")
}
// Initial state
if equipment.GetNumberOfItems() != 0 {
t.Error("New equipment list should be empty")
}
// Create test weapon
weapon := NewItem()
weapon.Name = "Test Sword"
weapon.Details.ItemID = 3001
weapon.GenericInfo.ItemType = ItemTypeWeapon
weapon.AddSlot(EQ2PrimarySlot)
// Equip weapon
if !equipment.AddItem(EQ2PrimarySlot, weapon) {
t.Error("Should be able to equip weapon")
}
if equipment.GetNumberOfItems() != 1 {
t.Errorf("Expected 1 equipped item, got %d", equipment.GetNumberOfItems())
}
// Get equipped weapon
equippedWeapon := equipment.GetItem(EQ2PrimarySlot)
if equippedWeapon == nil {
t.Fatal("GetItem returned nil")
}
if equippedWeapon.Name != weapon.Name {
t.Errorf("Expected name %s, got %s", weapon.Name, equippedWeapon.Name)
}
// Test equipment queries
if !equipment.HasItem(3001) {
t.Error("Equipment should have item 3001")
}
if !equipment.HasWeaponEquipped() {
t.Error("Equipment should have weapon equipped")
}
weapons := equipment.GetWeapons()
if len(weapons) != 1 {
t.Errorf("Expected 1 weapon, got %d", len(weapons))
}
// Remove weapon
equipment.RemoveItem(EQ2PrimarySlot, false)
if equipment.GetNumberOfItems() != 0 {
t.Errorf("Expected 0 items after removal, got %d", equipment.GetNumberOfItems())
}
}
func TestEquipmentValidation(t *testing.T) {
equipment := NewEquipmentItemList()
// Create invalid item (no name)
invalidItem := NewItem()
invalidItem.Details.ItemID = 4001
invalidItem.AddSlot(EQ2HeadSlot)
equipment.SetItem(EQ2HeadSlot, invalidItem, false)
result := equipment.ValidateEquipment()
if result.Valid {
t.Error("Equipment with invalid item should fail validation")
}
// Create item that can't be equipped in the slot
wrongSlotItem := NewItem()
wrongSlotItem.Name = "Wrong Slot Item"
wrongSlotItem.Details.ItemID = 4002
wrongSlotItem.AddSlot(EQ2ChestSlot) // Can only go in chest
equipment.SetItem(EQ2HeadSlot, wrongSlotItem, false)
result = equipment.ValidateEquipment()
if result.Valid {
t.Error("Equipment with wrong slot item should fail validation")
}
}
func TestItemSystemAdapter(t *testing.T) {
// Create dependencies
masterList := NewMasterItemList()
spellManager := NewMockSpellManager()
// Add a test spell
spellManager.AddMockSpell(1001, "Test Spell", 100, 1, "A test spell")
adapter := NewItemSystemAdapter(
masterList,
spellManager,
nil, // playerManager
nil, // packetManager
nil, // ruleManager
nil, // databaseService
nil, // questManager
nil, // brokerManager
nil, // craftingManager
nil, // housingManager
nil, // lootManager
)
if adapter == nil {
t.Fatal("NewItemSystemAdapter returned nil")
}
// Test stats
stats := adapter.GetSystemStats()
if stats == nil {
t.Error("GetSystemStats should not return nil")
}
totalTemplates, ok := stats["total_item_templates"].(int32)
if !ok || totalTemplates != 0 {
t.Errorf("Expected 0 total templates, got %v", stats["total_item_templates"])
}
}
func TestItemBrokerChecks(t *testing.T) {
masterList := NewMasterItemList()
// Create weapon
weapon := NewItem()
weapon.Name = "Test Weapon"
weapon.GenericInfo.ItemType = ItemTypeWeapon
weapon.AddSlot(EQ2PrimarySlot)
// Test broker type checks
if !masterList.ShouldAddItemBrokerSlot(weapon, ItemBrokerSlotPrimary) {
t.Error("Weapon should match primary slot broker type")
}
if masterList.ShouldAddItemBrokerSlot(weapon, ItemBrokerSlotHead) {
t.Error("Weapon should not match head slot broker type")
}
// Create armor with stats
armor := NewItem()
armor.Name = "Test Armor"
armor.GenericInfo.ItemType = ItemTypeArmor
armor.AddStat(&ItemStat{
StatName: "Strength",
StatType: ItemStatStr,
Value: 10,
})
if !masterList.ShouldAddItemBrokerStat(armor, ItemBrokerStatTypeStr) {
t.Error("Armor should match STR stat broker type")
}
if masterList.ShouldAddItemBrokerStat(armor, ItemBrokerStatTypeInt) {
t.Error("Armor should not match INT stat broker type")
}
}
func TestItemSearchCriteria(t *testing.T) {
masterList := NewMasterItemList()
// Add test items
sword := NewItem()
sword.Name = "Steel Sword"
sword.Details.ItemID = 5001
sword.Details.Tier = 1
sword.Details.RecommendedLevel = 10
sword.GenericInfo.ItemType = ItemTypeWeapon
sword.BrokerPrice = 1000
armor := NewItem()
armor.Name = "Iron Armor"
armor.Details.ItemID = 5002
armor.Details.Tier = 2
armor.Details.RecommendedLevel = 15
armor.GenericInfo.ItemType = ItemTypeArmor
armor.BrokerPrice = 2000
masterList.AddItem(sword)
masterList.AddItem(armor)
// Search by name
criteria := &ItemSearchCriteria{
Name: "sword",
}
results := masterList.GetItems(criteria)
if len(results) != 1 {
t.Errorf("Expected 1 result for sword search, got %d", len(results))
}
if results[0].Name != sword.Name {
t.Errorf("Expected %s, got %s", sword.Name, results[0].Name)
}
// Search by price range
criteria = &ItemSearchCriteria{
MinPrice: 1500,
MaxPrice: 2500,
}
results = masterList.GetItems(criteria)
if len(results) != 1 {
t.Errorf("Expected 1 result for price search, got %d", len(results))
}
if results[0].Name != armor.Name {
t.Errorf("Expected %s, got %s", armor.Name, results[0].Name)
}
// Search by tier
criteria = &ItemSearchCriteria{
MinTier: 2,
MaxTier: 2,
}
results = masterList.GetItems(criteria)
if len(results) != 1 {
t.Errorf("Expected 1 result for tier search, got %d", len(results))
}
// Search with no matches
criteria = &ItemSearchCriteria{
Name: "nonexistent",
}
results = masterList.GetItems(criteria)
if len(results) != 0 {
t.Errorf("Expected 0 results for nonexistent search, got %d", len(results))
}
}
func TestNextUniqueID(t *testing.T) {
id1 := NextUniqueID()
id2 := NextUniqueID()
if id1 == id2 {
t.Error("NextUniqueID should return different IDs")
}
if id2 != id1+1 {
t.Errorf("Expected ID2 to be ID1+1, got %d and %d", id1, id2)
}
}
func TestItemError(t *testing.T) {
err := NewItemError("test error")
if err == nil {
t.Fatal("NewItemError returned nil")
}
if err.Error() != "test error" {
t.Errorf("Expected 'test error', got '%s'", err.Error())
}
if !IsItemError(err) {
t.Error("Should identify as item error")
}
// Test with non-item error
if IsItemError(fmt.Errorf("not an item error")) {
t.Error("Should not identify as item error")
}
}
func TestConstants(t *testing.T) {
// Test slot constants
if EQ2PrimarySlot != 0 {
t.Errorf("Expected EQ2PrimarySlot to be 0, got %d", EQ2PrimarySlot)
}
if NumSlots != 25 {
t.Errorf("Expected NumSlots to be 25, got %d", NumSlots)
}
// Test item type constants
if ItemTypeWeapon != 1 {
t.Errorf("Expected ItemTypeWeapon to be 1, got %d", ItemTypeWeapon)
}
// Test flag constants
if Attuned != 1 {
t.Errorf("Expected Attuned to be 1, got %d", Attuned)
}
// Test stat constants
if ItemStatStr != 0 {
t.Errorf("Expected ItemStatStr to be 0, got %d", ItemStatStr)
}
}
func BenchmarkItemCreation(b *testing.B) {
for i := 0; i < b.N; i++ {
item := NewItem()
item.Name = "Benchmark Item"
item.Details.ItemID = int32(i)
}
}
func BenchmarkMasterItemListAccess(b *testing.B) {
masterList := NewMasterItemList()
// Add test items
for i := 0; i < 1000; i++ {
item := NewItem()
item.Name = fmt.Sprintf("Item %d", i)
item.Details.ItemID = int32(i + 1000)
masterList.AddItem(item)
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
masterList.GetItem(int32((i % 1000) + 1000))
}
}
func BenchmarkPlayerItemListAdd(b *testing.B) {
playerList := NewPlayerItemList()
b.ResetTimer()
for i := 0; i < b.N; i++ {
item := NewItem()
item.Name = fmt.Sprintf("Item %d", i)
item.Details.ItemID = int32(i)
item.Details.BagID = int32(i % 6)
item.Details.SlotID = int16(i % 20)
playerList.AddItem(item)
}
}
func BenchmarkEquipmentBonusCalculation(b *testing.B) {
equipment := NewEquipmentItemList()
// Add some equipped items with stats
for slot := 0; slot < 10; slot++ {
item := NewItem()
item.Name = fmt.Sprintf("Equipment %d", slot)
item.Details.ItemID = int32(slot + 6000)
item.AddSlot(int8(slot))
// Add some stats
item.AddStat(&ItemStat{StatType: ItemStatStr, Value: 10})
item.AddStat(&ItemStat{StatType: ItemStatAgi, Value: 5})
item.AddStat(&ItemStat{StatType: ItemStatHealth, Value: 100})
equipment.SetItem(int8(slot), item, false)
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
equipment.CalculateEquipmentBonuses()
}
}

View File

@ -0,0 +1,688 @@
package items
import (
"fmt"
"log"
"strings"
"time"
)
// NewMasterItemList creates a new master item list
func NewMasterItemList() *MasterItemList {
mil := &MasterItemList{
items: make(map[int32]*Item),
mappedItemStatsStrings: make(map[string]int32),
mappedItemStatTypeIDs: make(map[int32]string),
brokerItemMap: make(map[*VersionRange]map[int64]int64),
}
// Initialize mapped item stats
mil.initializeMappedStats()
return mil
}
// initializeMappedStats initializes the mapped item stats
func (mil *MasterItemList) initializeMappedStats() {
// Add all the mapped item stats as in the C++ constructor
mil.AddMappedItemStat(ItemStatAdorning, "adorning")
mil.AddMappedItemStat(ItemStatAggression, "aggression")
mil.AddMappedItemStat(ItemStatArtificing, "artificing")
mil.AddMappedItemStat(ItemStatArtistry, "artistry")
mil.AddMappedItemStat(ItemStatChemistry, "chemistry")
mil.AddMappedItemStat(ItemStatCrushing, "crushing")
mil.AddMappedItemStat(ItemStatDefense, "defense")
mil.AddMappedItemStat(ItemStatDeflection, "deflection")
mil.AddMappedItemStat(ItemStatDisruption, "disruption")
mil.AddMappedItemStat(ItemStatFishing, "fishing")
mil.AddMappedItemStat(ItemStatFletching, "fletching")
mil.AddMappedItemStat(ItemStatFocus, "focus")
mil.AddMappedItemStat(ItemStatForesting, "foresting")
mil.AddMappedItemStat(ItemStatGathering, "gathering")
mil.AddMappedItemStat(ItemStatMetalShaping, "metal shaping")
mil.AddMappedItemStat(ItemStatMetalworking, "metalworking")
mil.AddMappedItemStat(ItemStatMining, "mining")
mil.AddMappedItemStat(ItemStatMinistration, "ministration")
mil.AddMappedItemStat(ItemStatOrdination, "ordination")
mil.AddMappedItemStat(ItemStatParry, "parry")
mil.AddMappedItemStat(ItemStatPiercing, "piercing")
mil.AddMappedItemStat(ItemStatRanged, "ranged")
mil.AddMappedItemStat(ItemStatSafeFall, "safe fall")
mil.AddMappedItemStat(ItemStatScribing, "scribing")
mil.AddMappedItemStat(ItemStatSculpting, "sculpting")
mil.AddMappedItemStat(ItemStatSlashing, "slashing")
mil.AddMappedItemStat(ItemStatSubjugation, "subjugation")
mil.AddMappedItemStat(ItemStatSwimming, "swimming")
mil.AddMappedItemStat(ItemStatTailoring, "tailoring")
mil.AddMappedItemStat(ItemStatTinkering, "tinkering")
mil.AddMappedItemStat(ItemStatTransmuting, "transmuting")
mil.AddMappedItemStat(ItemStatTrapping, "trapping")
mil.AddMappedItemStat(ItemStatWeaponSkills, "weapon skills")
mil.AddMappedItemStat(ItemStatPowerCostReduction, "power cost reduction")
mil.AddMappedItemStat(ItemStatSpellAvoidance, "spell avoidance")
}
// AddMappedItemStat adds a mapping between stat ID and name
func (mil *MasterItemList) AddMappedItemStat(id int32, lowerCaseName string) {
mil.mutex.Lock()
defer mil.mutex.Unlock()
mil.mappedItemStatsStrings[lowerCaseName] = id
mil.mappedItemStatTypeIDs[id] = lowerCaseName
}
// GetItemStatIDByName gets the stat ID by name
func (mil *MasterItemList) GetItemStatIDByName(name string) int32 {
mil.mutex.RLock()
defer mil.mutex.RUnlock()
lowerName := strings.ToLower(name)
if id, exists := mil.mappedItemStatsStrings[lowerName]; exists {
return id
}
return 0
}
// GetItemStatNameByID gets the stat name by ID
func (mil *MasterItemList) GetItemStatNameByID(id int32) string {
mil.mutex.RLock()
defer mil.mutex.RUnlock()
if name, exists := mil.mappedItemStatTypeIDs[id]; exists {
return name
}
return ""
}
// AddItem adds an item to the master list
func (mil *MasterItemList) AddItem(item *Item) {
if item == nil {
return
}
mil.mutex.Lock()
defer mil.mutex.Unlock()
mil.items[item.Details.ItemID] = item
log.Printf("Added item %d (%s) to master list", item.Details.ItemID, item.Name)
}
// GetItem retrieves an item by ID
func (mil *MasterItemList) GetItem(itemID int32) *Item {
mil.mutex.RLock()
defer mil.mutex.RUnlock()
if item, exists := mil.items[itemID]; exists {
return item.Copy() // Return a copy to prevent external modifications
}
return nil
}
// GetItemByName retrieves an item by name (case-insensitive)
func (mil *MasterItemList) GetItemByName(name string) *Item {
mil.mutex.RLock()
defer mil.mutex.RUnlock()
lowerName := strings.ToLower(name)
for _, item := range mil.items {
if strings.ToLower(item.Name) == lowerName {
return item.Copy()
}
}
return nil
}
// IsBag checks if an item ID represents a bag
func (mil *MasterItemList) IsBag(itemID int32) bool {
item := mil.GetItem(itemID)
if item == nil {
return false
}
return item.IsBag()
}
// RemoveAll removes all items from the master list
func (mil *MasterItemList) RemoveAll() {
mil.mutex.Lock()
defer mil.mutex.Unlock()
count := len(mil.items)
mil.items = make(map[int32]*Item)
log.Printf("Removed %d items from master list", count)
}
// GetItemCount returns the total number of items
func (mil *MasterItemList) GetItemCount() int {
mil.mutex.RLock()
defer mil.mutex.RUnlock()
return len(mil.items)
}
// CalculateItemBonuses calculates the stat bonuses for an item
func (mil *MasterItemList) CalculateItemBonuses(itemID int32) *ItemStatsValues {
item := mil.GetItem(itemID)
if item == nil {
return nil
}
return mil.CalculateItemBonusesFromItem(item)
}
// CalculateItemBonusesFromItem calculates stat bonuses from an item instance
func (mil *MasterItemList) CalculateItemBonusesFromItem(item *Item) *ItemStatsValues {
if item == nil {
return nil
}
item.mutex.RLock()
defer item.mutex.RUnlock()
values := &ItemStatsValues{}
// Process all item stats
for _, stat := range item.ItemStats {
switch stat.StatType {
case ItemStatStr:
values.Str += int16(stat.Value)
case ItemStatSta:
values.Sta += int16(stat.Value)
case ItemStatAgi:
values.Agi += int16(stat.Value)
case ItemStatWis:
values.Wis += int16(stat.Value)
case ItemStatInt:
values.Int += int16(stat.Value)
case ItemStatVsSlash:
values.VsSlash += int16(stat.Value)
case ItemStatVsCrush:
values.VsCrush += int16(stat.Value)
case ItemStatVsPierce:
values.VsPierce += int16(stat.Value)
case ItemStatVsPhysical:
values.VsPhysical += int16(stat.Value)
case ItemStatVsHeat:
values.VsHeat += int16(stat.Value)
case ItemStatVsCold:
values.VsCold += int16(stat.Value)
case ItemStatVsMagic:
values.VsMagic += int16(stat.Value)
case ItemStatVsMental:
values.VsMental += int16(stat.Value)
case ItemStatVsDivine:
values.VsDivine += int16(stat.Value)
case ItemStatVsDisease:
values.VsDisease += int16(stat.Value)
case ItemStatVsPoison:
values.VsPoison += int16(stat.Value)
case ItemStatHealth:
values.Health += int16(stat.Value)
case ItemStatPower:
values.Power += int16(stat.Value)
case ItemStatConcentration:
values.Concentration += int8(stat.Value)
case ItemStatAbilityModifier:
values.AbilityModifier += int16(stat.Value)
case ItemStatCriticalMitigation:
values.CriticalMitigation += int16(stat.Value)
case ItemStatExtraShieldBlockChance:
values.ExtraShieldBlockChance += int16(stat.Value)
case ItemStatBeneficialCritChance:
values.BeneficialCritChance += int16(stat.Value)
case ItemStatCritBonus:
values.CritBonus += int16(stat.Value)
case ItemStatPotency:
values.Potency += int16(stat.Value)
case ItemStatHateGainMod:
values.HateGainMod += int16(stat.Value)
case ItemStatAbilityReuseSpeed:
values.AbilityReuseSpeed += int16(stat.Value)
case ItemStatAbilityCastingSpeed:
values.AbilityCastingSpeed += int16(stat.Value)
case ItemStatAbilityRecoverySpeed:
values.AbilityRecoverySpeed += int16(stat.Value)
case ItemStatSpellReuseSpeed:
values.SpellReuseSpeed += int16(stat.Value)
case ItemStatSpellMultiAttackChance:
values.SpellMultiAttackChance += int16(stat.Value)
case ItemStatDPS:
values.DPS += int16(stat.Value)
case ItemStatAttackSpeed:
values.AttackSpeed += int16(stat.Value)
case ItemStatMultiattackChance:
values.MultiAttackChance += int16(stat.Value)
case ItemStatFlurry:
values.Flurry += int16(stat.Value)
case ItemStatAEAutoattackChance:
values.AEAutoattackChance += int16(stat.Value)
case ItemStatStrikethrough:
values.Strikethrough += int16(stat.Value)
case ItemStatAccuracy:
values.Accuracy += int16(stat.Value)
case ItemStatOffensiveSpeed:
values.OffensiveSpeed += int16(stat.Value)
case ItemStatUncontestedParry:
values.UncontestedParry += stat.Value
case ItemStatUncontestedBlock:
values.UncontestedBlock += stat.Value
case ItemStatUncontestedDodge:
values.UncontestedDodge += stat.Value
case ItemStatUncontestedRiposte:
values.UncontestedRiposte += stat.Value
case ItemStatSizeMod:
values.SizeMod += stat.Value
}
}
return values
}
// Broker-related methods
// AddBrokerItemMapRange adds a broker item mapping range
func (mil *MasterItemList) AddBrokerItemMapRange(minVersion int32, maxVersion int32, clientBitmask int64, serverBitmask int64) {
mil.mutex.Lock()
defer mil.mutex.Unlock()
// Find existing range
var targetRange *VersionRange
for versionRange := range mil.brokerItemMap {
if versionRange.MinVersion == minVersion && versionRange.MaxVersion == maxVersion {
targetRange = versionRange
break
}
}
// Create new range if not found
if targetRange == nil {
targetRange = &VersionRange{
MinVersion: minVersion,
MaxVersion: maxVersion,
}
mil.brokerItemMap[targetRange] = make(map[int64]int64)
}
mil.brokerItemMap[targetRange][clientBitmask] = serverBitmask
}
// FindBrokerItemMapVersionRange finds a broker item map by version range
func (mil *MasterItemList) FindBrokerItemMapVersionRange(minVersion int32, maxVersion int32) map[int64]int64 {
mil.mutex.RLock()
defer mil.mutex.RUnlock()
for versionRange, mapping := range mil.brokerItemMap {
// Check if min and max version are both in range
if versionRange.MinVersion <= minVersion && maxVersion <= versionRange.MaxVersion {
return mapping
}
// Check if the min version is in range, but max range is 0
if versionRange.MinVersion <= minVersion && versionRange.MaxVersion == 0 {
return mapping
}
// Check if min version is 0 and max_version has a cap
if versionRange.MinVersion == 0 && maxVersion <= versionRange.MaxVersion {
return mapping
}
}
return nil
}
// FindBrokerItemMapByVersion finds a broker item map by specific version
func (mil *MasterItemList) FindBrokerItemMapByVersion(version int32) map[int64]int64 {
mil.mutex.RLock()
defer mil.mutex.RUnlock()
var defaultMapping map[int64]int64
for versionRange, mapping := range mil.brokerItemMap {
// Check for default range (0,0)
if versionRange.MinVersion == 0 && versionRange.MaxVersion == 0 {
defaultMapping = mapping
continue
}
// Check if version is in range
if version >= versionRange.MinVersion && version <= versionRange.MaxVersion {
return mapping
}
}
return defaultMapping
}
// ShouldAddItemBrokerType checks if an item should be added to broker by type
func (mil *MasterItemList) ShouldAddItemBrokerType(item *Item, itemType int64) bool {
if item == nil {
return false
}
switch itemType {
case ItemBrokerTypeAdornment:
return item.IsAdornment()
case ItemBrokerTypeAmmo:
return item.IsAmmo()
case ItemBrokerTypeAttuneable:
return item.CheckFlag(Attuneable)
case ItemBrokerTypeBag:
return item.IsBag()
case ItemBrokerTypeBauble:
return item.IsBauble()
case ItemBrokerTypeBook:
return item.IsBook()
case ItemBrokerTypeChainarmor:
return item.IsChainArmor()
case ItemBrokerTypeCloak:
return item.IsCloak()
case ItemBrokerTypeClotharmor:
return item.IsClothArmor()
case ItemBrokerTypeCollectable:
return item.IsCollectable()
case ItemBrokerTypeCrushweapon:
return item.IsCrushWeapon()
case ItemBrokerTypeDrink:
return item.IsFoodDrink()
case ItemBrokerTypeFood:
return item.IsFoodFood()
case ItemBrokerTypeHouseitem:
return item.IsHouseItem()
case ItemBrokerTypeJewelry:
return item.IsJewelry()
case ItemBrokerTypeLeatherarmor:
return item.IsLeatherArmor()
case ItemBrokerTypeLore:
return item.CheckFlag(Lore)
case ItemBrokerTypeMisc:
return item.IsMisc()
case ItemBrokerTypePierceweapon:
return item.IsPierceWeapon()
case ItemBrokerTypePlatearmor:
return item.IsPlateArmor()
case ItemBrokerTypePoison:
return item.IsPoison()
case ItemBrokerTypePotion:
return item.IsPotion()
case ItemBrokerTypeRecipebook:
return item.IsRecipeBook()
case ItemBrokerTypeSalesdisplay:
return item.IsSalesDisplay()
case ItemBrokerTypeShield:
return item.IsShield()
case ItemBrokerTypeSlashweapon:
return item.IsSlashWeapon()
case ItemBrokerTypeSpellscroll:
return item.IsSpellScroll()
case ItemBrokerTypeTinkered:
return item.IsTinkered()
case ItemBrokerTypeTradeskill:
return item.IsTradeskill()
}
return false
}
// ShouldAddItemBrokerSlot checks if an item should be added to broker by slot
func (mil *MasterItemList) ShouldAddItemBrokerSlot(item *Item, slotType int64) bool {
if item == nil {
return false
}
switch slotType {
case ItemBrokerSlotPrimary:
return item.HasSlot(EQ2PrimarySlot, -1)
case ItemBrokerSlotPrimary2H:
return item.HasSlot(EQ2PrimarySlot, -1) || item.HasSlot(EQ2SecondarySlot, -1)
case ItemBrokerSlotSecondary:
return item.HasSlot(EQ2SecondarySlot, -1)
case ItemBrokerSlotHead:
return item.HasSlot(EQ2HeadSlot, -1)
case ItemBrokerSlotChest:
return item.HasSlot(EQ2ChestSlot, -1)
case ItemBrokerSlotShoulders:
return item.HasSlot(EQ2ShouldersSlot, -1)
case ItemBrokerSlotForearms:
return item.HasSlot(EQ2ForearmsSlot, -1)
case ItemBrokerSlotHands:
return item.HasSlot(EQ2HandsSlot, -1)
case ItemBrokerSlotLegs:
return item.HasSlot(EQ2LegsSlot, -1)
case ItemBrokerSlotFeet:
return item.HasSlot(EQ2FeetSlot, -1)
case ItemBrokerSlotRing:
return item.HasSlot(EQ2LRingSlot, EQ2RRingSlot)
case ItemBrokerSlotEars:
return item.HasSlot(EQ2EarsSlot1, EQ2EarsSlot2)
case ItemBrokerSlotNeck:
return item.HasSlot(EQ2NeckSlot, -1)
case ItemBrokerSlotWrist:
return item.HasSlot(EQ2LWristSlot, EQ2RWristSlot)
case ItemBrokerSlotRangeWeapon:
return item.HasSlot(EQ2RangeSlot, -1)
case ItemBrokerSlotAmmo:
return item.HasSlot(EQ2AmmoSlot, -1)
case ItemBrokerSlotWaist:
return item.HasSlot(EQ2WaistSlot, -1)
case ItemBrokerSlotCloak:
return item.HasSlot(EQ2CloakSlot, -1)
case ItemBrokerSlotCharm:
return item.HasSlot(EQ2CharmSlot1, EQ2CharmSlot2)
case ItemBrokerSlotFood:
return item.HasSlot(EQ2FoodSlot, -1)
case ItemBrokerSlotDrink:
return item.HasSlot(EQ2DrinkSlot, -1)
}
return false
}
// ShouldAddItemBrokerStat checks if an item should be added to broker by stat
func (mil *MasterItemList) ShouldAddItemBrokerStat(item *Item, statType int64) bool {
if item == nil {
return false
}
// Check if the item has the requested stat type
for _, stat := range item.ItemStats {
switch statType {
case ItemBrokerStatTypeStr:
if stat.StatType == ItemStatStr {
return true
}
case ItemBrokerStatTypeSta:
if stat.StatType == ItemStatSta {
return true
}
case ItemBrokerStatTypeAgi:
if stat.StatType == ItemStatAgi {
return true
}
case ItemBrokerStatTypeWis:
if stat.StatType == ItemStatWis {
return true
}
case ItemBrokerStatTypeInt:
if stat.StatType == ItemStatInt {
return true
}
case ItemBrokerStatTypeHealth:
if stat.StatType == ItemStatHealth {
return true
}
case ItemBrokerStatTypePower:
if stat.StatType == ItemStatPower {
return true
}
case ItemBrokerStatTypePotency:
if stat.StatType == ItemStatPotency {
return true
}
case ItemBrokerStatTypeCritical:
if stat.StatType == ItemStatMeleeCritChance || stat.StatType == ItemStatBeneficialCritChance {
return true
}
case ItemBrokerStatTypeAttackspeed:
if stat.StatType == ItemStatAttackSpeed {
return true
}
case ItemBrokerStatTypeDPS:
if stat.StatType == ItemStatDPS {
return true
}
// Add more stat type checks as needed
}
}
return false
}
// GetItems searches for items based on criteria
func (mil *MasterItemList) GetItems(criteria *ItemSearchCriteria) []*Item {
if criteria == nil {
return nil
}
mil.mutex.RLock()
defer mil.mutex.RUnlock()
var results []*Item
for _, item := range mil.items {
if mil.matchesCriteria(item, criteria) {
results = append(results, item.Copy())
}
}
return results
}
// matchesCriteria checks if an item matches the search criteria
func (mil *MasterItemList) matchesCriteria(item *Item, criteria *ItemSearchCriteria) bool {
// Name matching
if criteria.Name != "" {
if !strings.Contains(strings.ToLower(item.Name), strings.ToLower(criteria.Name)) {
return false
}
}
// Price range
if criteria.MinPrice > 0 && item.BrokerPrice < criteria.MinPrice {
return false
}
if criteria.MaxPrice > 0 && item.BrokerPrice > criteria.MaxPrice {
return false
}
// Tier range
if criteria.MinTier > 0 && item.Details.Tier < criteria.MinTier {
return false
}
if criteria.MaxTier > 0 && item.Details.Tier > criteria.MaxTier {
return false
}
// Level range
if criteria.MinLevel > 0 && item.Details.RecommendedLevel < criteria.MinLevel {
return false
}
if criteria.MaxLevel > 0 && item.Details.RecommendedLevel > criteria.MaxLevel {
return false
}
// Item type matching
if criteria.ItemType != 0 {
if !mil.ShouldAddItemBrokerType(item, criteria.ItemType) {
return false
}
}
// Location type matching (slot compatibility)
if criteria.LocationType != 0 {
if !mil.ShouldAddItemBrokerSlot(item, criteria.LocationType) {
return false
}
}
// Broker type matching (stat requirements)
if criteria.BrokerType != 0 {
if !mil.ShouldAddItemBrokerStat(item, criteria.BrokerType) {
return false
}
}
// Seller matching
if criteria.Seller != "" {
if !strings.Contains(strings.ToLower(item.SellerName), strings.ToLower(criteria.Seller)) {
return false
}
}
// Adornment matching
if criteria.Adornment != "" {
if !strings.Contains(strings.ToLower(item.Adornment), strings.ToLower(criteria.Adornment)) {
return false
}
}
return true
}
// GetStats returns statistics about the master item list
func (mil *MasterItemList) GetStats() *ItemManagerStats {
mil.mutex.RLock()
defer mil.mutex.RUnlock()
stats := &ItemManagerStats{
TotalItems: int32(len(mil.items)),
ItemsByType: make(map[int8]int32),
ItemsByTier: make(map[int8]int32),
LastUpdate: time.Now(),
}
// Count items by type and tier
for _, item := range mil.items {
stats.ItemsByType[item.GenericInfo.ItemType]++
stats.ItemsByTier[item.Details.Tier]++
}
return stats
}
// Validate validates the master item list
func (mil *MasterItemList) Validate() *ItemValidationResult {
mil.mutex.RLock()
defer mil.mutex.RUnlock()
result := &ItemValidationResult{Valid: true}
for itemID, item := range mil.items {
itemResult := item.Validate()
if !itemResult.Valid {
result.Valid = false
for _, err := range itemResult.Errors {
result.Errors = append(result.Errors, fmt.Sprintf("Item %d: %s", itemID, err))
}
}
}
return result
}
// Size returns the number of items in the master list
func (mil *MasterItemList) Size() int {
return mil.GetItemCount()
}
// Clear removes all items from the master list
func (mil *MasterItemList) Clear() {
mil.RemoveAll()
}
func init() {
log.Printf("Master item list system initialized")
}

View File

@ -0,0 +1,962 @@
package items
import (
"fmt"
"log"
)
// NewPlayerItemList creates a new player item list
func NewPlayerItemList() *PlayerItemList {
return &PlayerItemList{
indexedItems: make(map[int32]*Item),
items: make(map[int32]map[int8]map[int16]*Item),
overflowItems: make([]*Item, 0),
}
}
// SetMaxItemIndex sets and returns the maximum saved item index
func (pil *PlayerItemList) SetMaxItemIndex() int32 {
pil.mutex.Lock()
defer pil.mutex.Unlock()
maxIndex := int32(0)
for index := range pil.indexedItems {
if index > maxIndex {
maxIndex = index
}
}
pil.maxSavedIndex = maxIndex
return maxIndex
}
// SharedBankAddAllowed checks if an item can be added to shared bank
func (pil *PlayerItemList) SharedBankAddAllowed(item *Item) bool {
if item == nil {
return false
}
// Check item flags that prevent shared bank storage
if item.CheckFlag(NoTrade) || item.CheckFlag(Attuned) || item.CheckFlag(LoreEquip) {
return false
}
// Check heirloom flag
if item.CheckFlag2(Heirloom) {
return true // Heirloom items can go in shared bank
}
return true
}
// GetItemsFromBagID gets all items from a specific bag
func (pil *PlayerItemList) GetItemsFromBagID(bagID int32) []*Item {
pil.mutex.RLock()
defer pil.mutex.RUnlock()
var bagItems []*Item
if bagMap, exists := pil.items[bagID]; exists {
for _, slotMap := range bagMap {
for _, item := range slotMap {
if item != nil {
bagItems = append(bagItems, item)
}
}
}
}
return bagItems
}
// GetItemsInBag gets all items inside a bag item
func (pil *PlayerItemList) GetItemsInBag(bag *Item) []*Item {
if bag == nil || !bag.IsBag() {
return nil
}
return pil.GetItemsFromBagID(bag.Details.BagID)
}
// GetBag gets a bag from an inventory slot
func (pil *PlayerItemList) GetBag(inventorySlot int8, lock bool) *Item {
if lock {
pil.mutex.RLock()
defer pil.mutex.RUnlock()
}
// Check main inventory slots
for bagID := int32(0); bagID < NumInvSlots; bagID++ {
if bagMap, exists := pil.items[bagID]; exists {
if slot0Map, exists := bagMap[0]; exists {
if item, exists := slot0Map[int16(inventorySlot)]; exists && item != nil && item.IsBag() {
return item
}
}
}
}
return nil
}
// HasItem checks if the player has a specific item
func (pil *PlayerItemList) HasItem(itemID int32, includeBank bool) bool {
pil.mutex.RLock()
defer pil.mutex.RUnlock()
for bagID, bagMap := range pil.items {
// Skip bank slots if not including bank
if !includeBank && bagID >= BankSlot1 && bagID <= BankSlot8 {
continue
}
for _, slotMap := range bagMap {
for _, item := range slotMap {
if item != nil && item.Details.ItemID == itemID {
return true
}
}
}
}
return false
}
// GetItemFromIndex gets an item by its index
func (pil *PlayerItemList) GetItemFromIndex(index int32) *Item {
pil.mutex.RLock()
defer pil.mutex.RUnlock()
if item, exists := pil.indexedItems[index]; exists {
return item
}
return nil
}
// MoveItem moves an item to a new location
func (pil *PlayerItemList) MoveItem(item *Item, invSlot int32, slot int16, appearanceType int8, eraseOld bool) {
if item == nil {
return
}
pil.mutex.Lock()
defer pil.mutex.Unlock()
// Remove from old location if requested
if eraseOld {
pil.eraseItemInternal(item)
}
// Update item location
item.Details.InvSlotID = invSlot
item.Details.SlotID = slot
item.Details.AppearanceType = int16(appearanceType)
// Add to new location
pil.addItemToLocationInternal(item, invSlot, appearanceType, slot)
}
// MoveItemByIndex moves an item by index to a new location
func (pil *PlayerItemList) MoveItemByIndex(toBagID int32, fromIndex int16, to int8, appearanceType int8, charges int8) bool {
pil.mutex.Lock()
defer pil.mutex.Unlock()
// Find item by index
var item *Item
for _, bagMap := range pil.items {
for _, slotMap := range bagMap {
for _, foundItem := range slotMap {
if foundItem != nil && foundItem.Details.NewIndex == fromIndex {
item = foundItem
break
}
}
if item != nil {
break
}
}
if item != nil {
break
}
}
if item == nil {
return false
}
// Remove from old location
pil.eraseItemInternal(item)
// Update item properties
item.Details.BagID = toBagID
item.Details.SlotID = int16(to)
item.Details.AppearanceType = int16(appearanceType)
if charges > 0 {
item.Details.Count = int16(charges)
}
// Add to new location
pil.addItemToLocationInternal(item, toBagID, appearanceType, int16(to))
return true
}
// EraseItem removes an item from the inventory
func (pil *PlayerItemList) EraseItem(item *Item) {
if item == nil {
return
}
pil.mutex.Lock()
defer pil.mutex.Unlock()
pil.eraseItemInternal(item)
}
// eraseItemInternal removes an item from internal storage (assumes lock is held)
func (pil *PlayerItemList) eraseItemInternal(item *Item) {
if item == nil {
return
}
// Remove from indexed items
for index, indexedItem := range pil.indexedItems {
if indexedItem == item {
delete(pil.indexedItems, index)
break
}
}
// Remove from location-based storage
if bagMap, exists := pil.items[item.Details.BagID]; exists {
if slotMap, exists := bagMap[int8(item.Details.AppearanceType)]; exists {
delete(slotMap, item.Details.SlotID)
// Clean up empty maps
if len(slotMap) == 0 {
delete(bagMap, int8(item.Details.AppearanceType))
if len(bagMap) == 0 {
delete(pil.items, item.Details.BagID)
}
}
}
}
// Remove from overflow items
for i, overflowItem := range pil.overflowItems {
if overflowItem == item {
pil.overflowItems = append(pil.overflowItems[:i], pil.overflowItems[i+1:]...)
break
}
}
}
// GetItemFromUniqueID gets an item by its unique ID
func (pil *PlayerItemList) GetItemFromUniqueID(uniqueID int32, includeBank bool, lock bool) *Item {
if lock {
pil.mutex.RLock()
defer pil.mutex.RUnlock()
}
for bagID, bagMap := range pil.items {
// Skip bank slots if not including bank
if !includeBank && bagID >= BankSlot1 && bagID <= BankSlot8 {
continue
}
for _, slotMap := range bagMap {
for _, item := range slotMap {
if item != nil && int32(item.Details.UniqueID) == uniqueID {
return item
}
}
}
}
// Check overflow items
for _, item := range pil.overflowItems {
if item != nil && int32(item.Details.UniqueID) == uniqueID {
return item
}
}
return nil
}
// GetItemFromID gets an item by its template ID
func (pil *PlayerItemList) GetItemFromID(itemID int32, count int8, includeBank bool, lock bool) *Item {
if lock {
pil.mutex.RLock()
defer pil.mutex.RUnlock()
}
for bagID, bagMap := range pil.items {
// Skip bank slots if not including bank
if !includeBank && bagID >= BankSlot1 && bagID <= BankSlot8 {
continue
}
for _, slotMap := range bagMap {
for _, item := range slotMap {
if item != nil && item.Details.ItemID == itemID {
if count == 0 || item.Details.Count >= int16(count) {
return item
}
}
}
}
}
return nil
}
// GetAllStackCountItemFromID gets the total count of all stacks of an item
func (pil *PlayerItemList) GetAllStackCountItemFromID(itemID int32, count int8, includeBank bool, lock bool) int32 {
if lock {
pil.mutex.RLock()
defer pil.mutex.RUnlock()
}
totalCount := int32(0)
for bagID, bagMap := range pil.items {
// Skip bank slots if not including bank
if !includeBank && bagID >= BankSlot1 && bagID <= BankSlot8 {
continue
}
for _, slotMap := range bagMap {
for _, item := range slotMap {
if item != nil && item.Details.ItemID == itemID {
totalCount += int32(item.Details.Count)
}
}
}
}
return totalCount
}
// AssignItemToFreeSlot assigns an item to the first available free slot
func (pil *PlayerItemList) AssignItemToFreeSlot(item *Item, inventoryOnly bool) bool {
if item == nil {
return false
}
pil.mutex.Lock()
defer pil.mutex.Unlock()
var bagID int32
var slot int16
if pil.getFirstFreeSlotInternal(&bagID, &slot, inventoryOnly) {
item.Details.BagID = bagID
item.Details.SlotID = slot
item.Details.AppearanceType = BaseEquipment
pil.addItemToLocationInternal(item, bagID, BaseEquipment, slot)
return true
}
return false
}
// GetNumberOfFreeSlots returns the number of free inventory slots
func (pil *PlayerItemList) GetNumberOfFreeSlots() int16 {
pil.mutex.RLock()
defer pil.mutex.RUnlock()
freeSlots := int16(0)
// Check main inventory slots
for bagID := int32(0); bagID < NumInvSlots; bagID++ {
bag := pil.GetBag(int8(bagID), false)
if bag != nil && bag.BagInfo != nil {
// Count free slots in this bag
usedSlots := 0
if bagMap, exists := pil.items[bagID]; exists {
for _, slotMap := range bagMap {
usedSlots += len(slotMap)
}
}
freeSlots += int16(bag.BagInfo.NumSlots) - int16(usedSlots)
}
}
return freeSlots
}
// GetNumberOfItems returns the total number of items in inventory
func (pil *PlayerItemList) GetNumberOfItems() int16 {
pil.mutex.RLock()
defer pil.mutex.RUnlock()
itemCount := int16(0)
for _, bagMap := range pil.items {
for _, slotMap := range bagMap {
itemCount += int16(len(slotMap))
}
}
return itemCount
}
// GetWeight returns the total weight of all items
func (pil *PlayerItemList) GetWeight() int32 {
pil.mutex.RLock()
defer pil.mutex.RUnlock()
totalWeight := int32(0)
for _, bagMap := range pil.items {
for _, slotMap := range bagMap {
for _, item := range slotMap {
if item != nil {
totalWeight += item.GenericInfo.Weight * int32(item.Details.Count)
}
}
}
}
return totalWeight
}
// HasFreeSlot checks if there's at least one free slot
func (pil *PlayerItemList) HasFreeSlot() bool {
return pil.GetNumberOfFreeSlots() > 0
}
// HasFreeBagSlot checks if there's a free bag slot in main inventory
func (pil *PlayerItemList) HasFreeBagSlot() bool {
pil.mutex.RLock()
defer pil.mutex.RUnlock()
// Check main inventory bag slots
for bagSlot := int8(0); bagSlot < NumInvSlots; bagSlot++ {
bag := pil.GetBag(bagSlot, false)
if bag == nil {
return true // Empty bag slot
}
}
return false
}
// DestroyItem destroys an item by index
func (pil *PlayerItemList) DestroyItem(index int16) {
pil.mutex.Lock()
defer pil.mutex.Unlock()
// Find and remove item by index
for _, bagMap := range pil.items {
for _, slotMap := range bagMap {
for _, item := range slotMap {
if item != nil && item.Details.NewIndex == index {
pil.eraseItemInternal(item)
return
}
}
}
}
}
// CanStack checks if an item can be stacked with existing items
func (pil *PlayerItemList) CanStack(item *Item, includeBank bool) *Item {
if item == nil {
return nil
}
pil.mutex.RLock()
defer pil.mutex.RUnlock()
for bagID, bagMap := range pil.items {
// Skip bank slots if not including bank
if !includeBank && bagID >= BankSlot1 && bagID <= BankSlot8 {
continue
}
for _, slotMap := range bagMap {
for _, existingItem := range slotMap {
if existingItem != nil &&
existingItem.Details.ItemID == item.Details.ItemID &&
existingItem.Details.Count < existingItem.StackCount &&
existingItem.Details.UniqueID != item.Details.UniqueID {
return existingItem
}
}
}
}
return nil
}
// GetAllItemsFromID gets all items with a specific ID
func (pil *PlayerItemList) GetAllItemsFromID(itemID int32, includeBank bool, lock bool) []*Item {
if lock {
pil.mutex.RLock()
defer pil.mutex.RUnlock()
}
var matchingItems []*Item
for bagID, bagMap := range pil.items {
// Skip bank slots if not including bank
if !includeBank && bagID >= BankSlot1 && bagID <= BankSlot8 {
continue
}
for _, slotMap := range bagMap {
for _, item := range slotMap {
if item != nil && item.Details.ItemID == itemID {
matchingItems = append(matchingItems, item)
}
}
}
}
return matchingItems
}
// RemoveItem removes an item from inventory
func (pil *PlayerItemList) RemoveItem(item *Item, deleteItem bool, lock bool) {
if item == nil {
return
}
if lock {
pil.mutex.Lock()
defer pil.mutex.Unlock()
}
pil.eraseItemInternal(item)
if deleteItem {
// Mark item for deletion
item.NeedsDeletion = true
}
}
// AddItem adds an item to the inventory
func (pil *PlayerItemList) AddItem(item *Item) bool {
if item == nil {
return false
}
pil.mutex.Lock()
defer pil.mutex.Unlock()
// Try to stack with existing items first
stackableItem := pil.CanStack(item, false)
if stackableItem != nil {
// Stack with existing item
stackableItem.Details.Count += item.Details.Count
if stackableItem.Details.Count > stackableItem.StackCount {
// Handle overflow
overflow := stackableItem.Details.Count - stackableItem.StackCount
stackableItem.Details.Count = stackableItem.StackCount
item.Details.Count = overflow
// Continue to add the overflow as a new item
} else {
return true // Successfully stacked
}
}
// Try to assign to free slot
var bagID int32
var slot int16
if pil.getFirstFreeSlotInternal(&bagID, &slot, true) {
item.Details.BagID = bagID
item.Details.SlotID = slot
item.Details.AppearanceType = BaseEquipment
pil.addItemToLocationInternal(item, bagID, BaseEquipment, slot)
return true
}
// Add to overflow if no free slots
return pil.AddOverflowItem(item)
}
// GetItem gets an item from a specific location
func (pil *PlayerItemList) GetItem(bagSlot int32, slot int16, appearanceType int8) *Item {
pil.mutex.RLock()
defer pil.mutex.RUnlock()
if bagMap, exists := pil.items[bagSlot]; exists {
if slotMap, exists := bagMap[appearanceType]; exists {
if item, exists := slotMap[slot]; exists {
return item
}
}
}
return nil
}
// GetAllItems returns all items in the inventory
func (pil *PlayerItemList) GetAllItems() map[int32]*Item {
pil.mutex.RLock()
defer pil.mutex.RUnlock()
// Return a copy of indexed items
allItems := make(map[int32]*Item)
for index, item := range pil.indexedItems {
allItems[index] = item
}
return allItems
}
// HasFreeBankSlot checks if there's a free bank slot
func (pil *PlayerItemList) HasFreeBankSlot() bool {
pil.mutex.RLock()
defer pil.mutex.RUnlock()
// Check bank bag slots
for bagSlot := int32(BankSlot1); bagSlot <= BankSlot8; bagSlot++ {
if _, exists := pil.items[bagSlot]; !exists {
return true
}
}
return false
}
// FindFreeBankSlot finds the first free bank slot
func (pil *PlayerItemList) FindFreeBankSlot() int8 {
pil.mutex.RLock()
defer pil.mutex.RUnlock()
for bagSlot := int32(BankSlot1); bagSlot <= BankSlot8; bagSlot++ {
if _, exists := pil.items[bagSlot]; !exists {
return int8(bagSlot - BankSlot1)
}
}
return -1
}
// GetFirstFreeSlot gets the first free slot coordinates
func (pil *PlayerItemList) GetFirstFreeSlot(bagID *int32, slot *int16) bool {
pil.mutex.RLock()
defer pil.mutex.RUnlock()
return pil.getFirstFreeSlotInternal(bagID, slot, true)
}
// getFirstFreeSlotInternal gets the first free slot (assumes lock is held)
func (pil *PlayerItemList) getFirstFreeSlotInternal(bagID *int32, slot *int16, inventoryOnly bool) bool {
// Check main inventory bags first
for bagSlotID := int32(0); bagSlotID < NumInvSlots; bagSlotID++ {
bag := pil.GetBag(int8(bagSlotID), false)
if bag != nil && bag.BagInfo != nil {
// Check slots in this bag
bagMap := pil.items[bagSlotID]
if bagMap == nil {
bagMap = make(map[int8]map[int16]*Item)
pil.items[bagSlotID] = bagMap
}
slotMap := bagMap[BaseEquipment]
if slotMap == nil {
slotMap = make(map[int16]*Item)
bagMap[BaseEquipment] = slotMap
}
for slotID := int16(0); slotID < int16(bag.BagInfo.NumSlots); slotID++ {
if _, exists := slotMap[slotID]; !exists {
*bagID = bagSlotID
*slot = slotID
return true
}
}
}
}
// Check bank bags if not inventory only
if !inventoryOnly {
for bagSlotID := int32(BankSlot1); bagSlotID <= BankSlot8; bagSlotID++ {
bag := pil.GetBankBag(int8(bagSlotID-BankSlot1), false)
if bag != nil && bag.BagInfo != nil {
bagMap := pil.items[bagSlotID]
if bagMap == nil {
bagMap = make(map[int8]map[int16]*Item)
pil.items[bagSlotID] = bagMap
}
slotMap := bagMap[BaseEquipment]
if slotMap == nil {
slotMap = make(map[int16]*Item)
bagMap[BaseEquipment] = slotMap
}
for slotID := int16(0); slotID < int16(bag.BagInfo.NumSlots); slotID++ {
if _, exists := slotMap[slotID]; !exists {
*bagID = bagSlotID
*slot = slotID
return true
}
}
}
}
}
return false
}
// GetFirstFreeBankSlot gets the first free bank slot coordinates
func (pil *PlayerItemList) GetFirstFreeBankSlot(bagID *int32, slot *int16) bool {
pil.mutex.RLock()
defer pil.mutex.RUnlock()
return pil.getFirstFreeSlotInternal(bagID, slot, false)
}
// GetBankBag gets a bank bag by slot
func (pil *PlayerItemList) GetBankBag(inventorySlot int8, lock bool) *Item {
if lock {
pil.mutex.RLock()
defer pil.mutex.RUnlock()
}
bagID := int32(BankSlot1) + int32(inventorySlot)
if bagMap, exists := pil.items[bagID]; exists {
if slotMap, exists := bagMap[0]; exists {
if item, exists := slotMap[0]; exists && item != nil && item.IsBag() {
return item
}
}
}
return nil
}
// AddOverflowItem adds an item to overflow storage
func (pil *PlayerItemList) AddOverflowItem(item *Item) bool {
if item == nil {
return false
}
pil.mutex.Lock()
defer pil.mutex.Unlock()
pil.overflowItems = append(pil.overflowItems, item)
return true
}
// GetOverflowItem gets the first overflow item
func (pil *PlayerItemList) GetOverflowItem() *Item {
pil.mutex.RLock()
defer pil.mutex.RUnlock()
if len(pil.overflowItems) > 0 {
return pil.overflowItems[0]
}
return nil
}
// RemoveOverflowItem removes an item from overflow storage
func (pil *PlayerItemList) RemoveOverflowItem(item *Item) {
if item == nil {
return
}
pil.mutex.Lock()
defer pil.mutex.Unlock()
for i, overflowItem := range pil.overflowItems {
if overflowItem == item {
pil.overflowItems = append(pil.overflowItems[:i], pil.overflowItems[i+1:]...)
break
}
}
}
// GetOverflowItemList returns all overflow items
func (pil *PlayerItemList) GetOverflowItemList() []*Item {
pil.mutex.RLock()
defer pil.mutex.RUnlock()
// Return a copy of the overflow list
overflowCopy := make([]*Item, len(pil.overflowItems))
copy(overflowCopy, pil.overflowItems)
return overflowCopy
}
// ResetPackets resets packet data
func (pil *PlayerItemList) ResetPackets() {
pil.mutex.Lock()
defer pil.mutex.Unlock()
pil.xorPacket = nil
pil.origPacket = nil
pil.packetCount = 0
}
// CheckSlotConflict checks for slot conflicts (lore items, etc.)
func (pil *PlayerItemList) CheckSlotConflict(item *Item, checkLoreOnly bool, lockMutex bool, loreStackCount *int16) int32 {
if item == nil {
return 0
}
if lockMutex {
pil.mutex.RLock()
defer pil.mutex.RUnlock()
}
// Check for lore conflicts
if item.CheckFlag(Lore) || item.CheckFlag(LoreEquip) {
stackCount := int16(0)
for _, bagMap := range pil.items {
for _, slotMap := range bagMap {
for _, existingItem := range slotMap {
if existingItem != nil && existingItem.Details.ItemID == item.Details.ItemID {
stackCount++
}
}
}
}
if loreStackCount != nil {
*loreStackCount = stackCount
}
if stackCount > 0 {
return 1 // Lore conflict
}
}
return 0 // No conflict
}
// GetItemCountInBag returns the number of items in a bag
func (pil *PlayerItemList) GetItemCountInBag(bag *Item) int32 {
if bag == nil || !bag.IsBag() {
return 0
}
pil.mutex.RLock()
defer pil.mutex.RUnlock()
count := int32(0)
if bagMap, exists := pil.items[bag.Details.BagID]; exists {
for _, slotMap := range bagMap {
count += int32(len(slotMap))
}
}
return count
}
// GetFirstNewItem gets the index of the first new item
func (pil *PlayerItemList) GetFirstNewItem() int16 {
pil.mutex.RLock()
defer pil.mutex.RUnlock()
for _, bagMap := range pil.items {
for _, slotMap := range bagMap {
for _, item := range slotMap {
if item != nil && item.Details.NewItem {
return item.Details.NewIndex
}
}
}
}
return -1
}
// GetNewItemByIndex gets a new item by its index
func (pil *PlayerItemList) GetNewItemByIndex(index int16) int16 {
pil.mutex.RLock()
defer pil.mutex.RUnlock()
for _, bagMap := range pil.items {
for _, slotMap := range bagMap {
for _, item := range slotMap {
if item != nil && item.Details.NewItem && item.Details.NewIndex == index {
return index
}
}
}
}
return -1
}
// addItemToLocationInternal adds an item to a specific location (assumes lock is held)
func (pil *PlayerItemList) addItemToLocationInternal(item *Item, bagID int32, appearanceType int8, slot int16) {
if item == nil {
return
}
// Ensure bag map exists
if pil.items[bagID] == nil {
pil.items[bagID] = make(map[int8]map[int16]*Item)
}
// Ensure appearance type map exists
if pil.items[bagID][appearanceType] == nil {
pil.items[bagID][appearanceType] = make(map[int16]*Item)
}
// Add item to location
pil.items[bagID][appearanceType][slot] = item
// Add to indexed items
if item.Details.Index > 0 {
pil.indexedItems[int32(item.Details.Index)] = item
}
}
// IsItemInSlotType checks if an item is in a specific slot type
func (pil *PlayerItemList) IsItemInSlotType(item *Item, slotType InventorySlotType, lockItems bool) bool {
if item == nil {
return false
}
if lockItems {
pil.mutex.RLock()
defer pil.mutex.RUnlock()
}
bagID := item.Details.BagID
switch slotType {
case BaseInventory:
return bagID >= 0 && bagID < NumInvSlots
case Bank:
return bagID >= BankSlot1 && bagID <= BankSlot8
case SharedBank:
// TODO: Implement shared bank slot detection
return false
case Overflow:
// Check if item is in overflow list
for _, overflowItem := range pil.overflowItems {
if overflowItem == item {
return true
}
}
return false
}
return false
}
// String returns a string representation of the player item list
func (pil *PlayerItemList) String() string {
pil.mutex.RLock()
defer pil.mutex.RUnlock()
return fmt.Sprintf("PlayerItemList{Items: %d, Overflow: %d, MaxIndex: %d}",
len(pil.indexedItems), len(pil.overflowItems), pil.maxSavedIndex)
}
func init() {
log.Printf("Player item list system initialized")
}

561
internal/items/types.go Normal file
View File

@ -0,0 +1,561 @@
package items
import (
"sync"
"time"
)
// Item effect types
type ItemEffectType int
const (
NoEffectType ItemEffectType = 0
EffectCureTypeTrauma ItemEffectType = 1
EffectCureTypeArcane ItemEffectType = 2
EffectCureTypeNoxious ItemEffectType = 3
EffectCureTypeElemental ItemEffectType = 4
EffectCureTypeCurse ItemEffectType = 5
EffectCureTypeMagic ItemEffectType = 6
EffectCureTypeAll ItemEffectType = 7
)
// Inventory slot types
type InventorySlotType int
const (
HouseVault InventorySlotType = -5
SharedBank InventorySlotType = -4
Bank InventorySlotType = -3
Overflow InventorySlotType = -2
UnknownInvSlotType InventorySlotType = -1
BaseInventory InventorySlotType = 0
)
// Lock reasons for items
type LockReason uint32
const (
LockReasonNone LockReason = 0
LockReasonHouse LockReason = 1 << 0
LockReasonCrafting LockReason = 1 << 1
LockReasonShop LockReason = 1 << 2
)
// Add item types for tracking how items were added
type AddItemType int
const (
NotSet AddItemType = 0
BuyFromBroker AddItemType = 1
GMCommand AddItemType = 2
)
// ItemStatsValues represents the complete stat bonuses from an item
type ItemStatsValues struct {
Str int16 `json:"str"`
Sta int16 `json:"sta"`
Agi int16 `json:"agi"`
Wis int16 `json:"wis"`
Int int16 `json:"int"`
VsSlash int16 `json:"vs_slash"`
VsCrush int16 `json:"vs_crush"`
VsPierce int16 `json:"vs_pierce"`
VsPhysical int16 `json:"vs_physical"`
VsHeat int16 `json:"vs_heat"`
VsCold int16 `json:"vs_cold"`
VsMagic int16 `json:"vs_magic"`
VsMental int16 `json:"vs_mental"`
VsDivine int16 `json:"vs_divine"`
VsDisease int16 `json:"vs_disease"`
VsPoison int16 `json:"vs_poison"`
Health int16 `json:"health"`
Power int16 `json:"power"`
Concentration int8 `json:"concentration"`
AbilityModifier int16 `json:"ability_modifier"`
CriticalMitigation int16 `json:"critical_mitigation"`
ExtraShieldBlockChance int16 `json:"extra_shield_block_chance"`
BeneficialCritChance int16 `json:"beneficial_crit_chance"`
CritBonus int16 `json:"crit_bonus"`
Potency int16 `json:"potency"`
HateGainMod int16 `json:"hate_gain_mod"`
AbilityReuseSpeed int16 `json:"ability_reuse_speed"`
AbilityCastingSpeed int16 `json:"ability_casting_speed"`
AbilityRecoverySpeed int16 `json:"ability_recovery_speed"`
SpellReuseSpeed int16 `json:"spell_reuse_speed"`
SpellMultiAttackChance int16 `json:"spell_multi_attack_chance"`
DPS int16 `json:"dps"`
AttackSpeed int16 `json:"attack_speed"`
MultiAttackChance int16 `json:"multi_attack_chance"`
Flurry int16 `json:"flurry"`
AEAutoattackChance int16 `json:"ae_autoattack_chance"`
Strikethrough int16 `json:"strikethrough"`
Accuracy int16 `json:"accuracy"`
OffensiveSpeed int16 `json:"offensive_speed"`
UncontestedParry float32 `json:"uncontested_parry"`
UncontestedBlock float32 `json:"uncontested_block"`
UncontestedDodge float32 `json:"uncontested_dodge"`
UncontestedRiposte float32 `json:"uncontested_riposte"`
SizeMod float32 `json:"size_mod"`
}
// ItemCore contains the core data for an item instance
type ItemCore struct {
ItemID int32 `json:"item_id"`
SOEId int32 `json:"soe_id"`
BagID int32 `json:"bag_id"`
InvSlotID int32 `json:"inv_slot_id"`
SlotID int16 `json:"slot_id"`
EquipSlotID int16 `json:"equip_slot_id"` // used when a bag is equipped
AppearanceType int16 `json:"appearance_type"` // 0 for combat armor, 1 for appearance armor
Index int8 `json:"index"`
Icon int16 `json:"icon"`
ClassicIcon int16 `json:"classic_icon"`
Count int16 `json:"count"`
Tier int8 `json:"tier"`
NumSlots int8 `json:"num_slots"`
UniqueID int64 `json:"unique_id"`
NumFreeSlots int8 `json:"num_free_slots"`
RecommendedLevel int16 `json:"recommended_level"`
ItemLocked bool `json:"item_locked"`
LockFlags int32 `json:"lock_flags"`
NewItem bool `json:"new_item"`
NewIndex int16 `json:"new_index"`
}
// ItemStat represents a single stat on an item
type ItemStat struct {
StatName string `json:"stat_name"`
StatType int32 `json:"stat_type"`
StatSubtype int16 `json:"stat_subtype"`
StatTypeCombined int16 `json:"stat_type_combined"`
Value float32 `json:"value"`
Level int8 `json:"level"`
}
// ItemSet represents an item set piece
type ItemSet struct {
ItemID int32 `json:"item_id"`
ItemCRC int32 `json:"item_crc"`
ItemIcon int16 `json:"item_icon"`
ItemStackSize int16 `json:"item_stack_size"`
ItemListColor int32 `json:"item_list_color"`
Name string `json:"name"`
Language int8 `json:"language"`
}
// Classifications represents item classifications
type Classifications struct {
ClassificationID int32 `json:"classification_id"`
ClassificationName string `json:"classification_name"`
}
// ItemLevelOverride represents level overrides for specific classes
type ItemLevelOverride struct {
AdventureClass int8 `json:"adventure_class"`
TradeskillClass int8 `json:"tradeskill_class"`
Level int16 `json:"level"`
}
// ItemClass represents class requirements for an item
type ItemClass struct {
AdventureClass int8 `json:"adventure_class"`
TradeskillClass int8 `json:"tradeskill_class"`
Level int16 `json:"level"`
}
// ItemAppearance represents visual appearance data
type ItemAppearance struct {
Type int16 `json:"type"`
Red int8 `json:"red"`
Green int8 `json:"green"`
Blue int8 `json:"blue"`
HighlightRed int8 `json:"highlight_red"`
HighlightGreen int8 `json:"highlight_green"`
HighlightBlue int8 `json:"highlight_blue"`
}
// QuestRewardData represents quest reward information
type QuestRewardData struct {
QuestID int32 `json:"quest_id"`
IsTemporary bool `json:"is_temporary"`
Description string `json:"description"`
IsCollection bool `json:"is_collection"`
HasDisplayed bool `json:"has_displayed"`
TmpCoin int64 `json:"tmp_coin"`
TmpStatus int32 `json:"tmp_status"`
DbSaved bool `json:"db_saved"`
DbIndex int32 `json:"db_index"`
}
// Generic_Info contains general item information
type GenericInfo struct {
ShowName int8 `json:"show_name"`
CreatorFlag int8 `json:"creator_flag"`
ItemFlags int16 `json:"item_flags"`
ItemFlags2 int16 `json:"item_flags2"`
Condition int8 `json:"condition"`
Weight int32 `json:"weight"` // num/10
SkillReq1 int32 `json:"skill_req1"`
SkillReq2 int32 `json:"skill_req2"`
SkillMin int16 `json:"skill_min"`
ItemType int8 `json:"item_type"`
AppearanceID int16 `json:"appearance_id"`
AppearanceRed int8 `json:"appearance_red"`
AppearanceGreen int8 `json:"appearance_green"`
AppearanceBlue int8 `json:"appearance_blue"`
AppearanceHighlightRed int8 `json:"appearance_highlight_red"`
AppearanceHighlightGreen int8 `json:"appearance_highlight_green"`
AppearanceHighlightBlue int8 `json:"appearance_highlight_blue"`
Collectable int8 `json:"collectable"`
OffersQuestID int32 `json:"offers_quest_id"`
PartOfQuestID int32 `json:"part_of_quest_id"`
MaxCharges int16 `json:"max_charges"`
DisplayCharges int8 `json:"display_charges"`
AdventureClasses int64 `json:"adventure_classes"`
TradeskillClasses int64 `json:"tradeskill_classes"`
AdventureDefaultLevel int16 `json:"adventure_default_level"`
TradeskillDefaultLevel int16 `json:"tradeskill_default_level"`
Usable int8 `json:"usable"`
Harvest int8 `json:"harvest"`
BodyDrop int8 `json:"body_drop"`
PvPDescription int8 `json:"pvp_description"`
MercOnly int8 `json:"merc_only"`
MountOnly int8 `json:"mount_only"`
SetID int32 `json:"set_id"`
CollectableUnk int8 `json:"collectable_unk"`
OffersQuestName string `json:"offers_quest_name"`
RequiredByQuestName string `json:"required_by_quest_name"`
TransmutedMaterial int8 `json:"transmuted_material"`
}
// ArmorInfo contains armor-specific information
type ArmorInfo struct {
MitigationLow int16 `json:"mitigation_low"`
MitigationHigh int16 `json:"mitigation_high"`
}
// AdornmentInfo contains adornment-specific information
type AdornmentInfo struct {
Duration float32 `json:"duration"`
ItemTypes int16 `json:"item_types"`
SlotType int16 `json:"slot_type"`
}
// WeaponInfo contains weapon-specific information
type WeaponInfo struct {
WieldType int16 `json:"wield_type"`
DamageLow1 int16 `json:"damage_low1"`
DamageHigh1 int16 `json:"damage_high1"`
DamageLow2 int16 `json:"damage_low2"`
DamageHigh2 int16 `json:"damage_high2"`
DamageLow3 int16 `json:"damage_low3"`
DamageHigh3 int16 `json:"damage_high3"`
Delay int16 `json:"delay"`
Rating float32 `json:"rating"`
}
// ShieldInfo contains shield-specific information
type ShieldInfo struct {
ArmorInfo ArmorInfo `json:"armor_info"`
}
// RangedInfo contains ranged weapon information
type RangedInfo struct {
WeaponInfo WeaponInfo `json:"weapon_info"`
RangeLow int16 `json:"range_low"`
RangeHigh int16 `json:"range_high"`
}
// BagInfo contains bag-specific information
type BagInfo struct {
NumSlots int8 `json:"num_slots"`
WeightReduction int16 `json:"weight_reduction"`
}
// FoodInfo contains food/drink information
type FoodInfo struct {
Type int8 `json:"type"` // 0=water, 1=food
Level int8 `json:"level"`
Duration float32 `json:"duration"`
Satiation int8 `json:"satiation"`
}
// BaubleInfo contains bauble-specific information
type BaubleInfo struct {
Cast int16 `json:"cast"`
Recovery int16 `json:"recovery"`
Duration int32 `json:"duration"`
Recast float32 `json:"recast"`
DisplaySlotOptional int8 `json:"display_slot_optional"`
DisplayCastTime int8 `json:"display_cast_time"`
DisplayBaubleType int8 `json:"display_bauble_type"`
EffectRadius float32 `json:"effect_radius"`
MaxAOETargets int32 `json:"max_aoe_targets"`
DisplayUntilCancelled int8 `json:"display_until_cancelled"`
}
// BookInfo contains book-specific information
type BookInfo struct {
Language int8 `json:"language"`
Author string `json:"author"`
Title string `json:"title"`
}
// BookInfoPages represents a book page
type BookInfoPages struct {
Page int8 `json:"page"`
PageText string `json:"page_text"`
PageTextVAlign int8 `json:"page_text_valign"`
PageTextHAlign int8 `json:"page_text_halign"`
}
// SkillInfo contains skill book information
type SkillInfo struct {
SpellID int32 `json:"spell_id"`
SpellTier int32 `json:"spell_tier"`
}
// HouseItemInfo contains house item information
type HouseItemInfo struct {
StatusRentReduction int32 `json:"status_rent_reduction"`
CoinRentReduction float32 `json:"coin_rent_reduction"`
HouseOnly int8 `json:"house_only"`
HouseLocation int8 `json:"house_location"` // 0 = floor, 1 = ceiling, 2 = wall
}
// HouseContainerInfo contains house container information
type HouseContainerInfo struct {
AllowedTypes int64 `json:"allowed_types"`
NumSlots int8 `json:"num_slots"`
BrokerCommission int8 `json:"broker_commission"`
FenceCommission int8 `json:"fence_commission"`
}
// RecipeBookInfo contains recipe book information
type RecipeBookInfo struct {
Recipes []uint32 `json:"recipes"`
RecipeID int32 `json:"recipe_id"`
Uses int8 `json:"uses"`
}
// ItemSetInfo contains item set information
type ItemSetInfo struct {
ItemID int32 `json:"item_id"`
ItemCRC int32 `json:"item_crc"`
ItemIcon int16 `json:"item_icon"`
ItemStackSize int32 `json:"item_stack_size"`
ItemListColor int32 `json:"item_list_color"`
SOEItemIDUnsigned int32 `json:"soe_item_id_unsigned"`
SOEItemCRCUnsigned int32 `json:"soe_item_crc_unsigned"`
}
// ThrownInfo contains thrown weapon information
type ThrownInfo struct {
Range int32 `json:"range"`
DamageModifier int32 `json:"damage_modifier"`
HitBonus float32 `json:"hit_bonus"`
DamageType int32 `json:"damage_type"`
}
// ItemEffect represents an item effect
type ItemEffect struct {
Effect string `json:"effect"`
Percentage int8 `json:"percentage"`
SubBulletFlag int8 `json:"sub_bullet_flag"`
}
// BookPage represents a book page
type BookPage struct {
Page int8 `json:"page"`
PageText string `json:"page_text"`
VAlign int8 `json:"valign"`
HAlign int8 `json:"halign"`
}
// ItemStatString represents a string-based item stat
type ItemStatString struct {
StatString string `json:"stat_string"`
}
// Item represents a complete item with all its properties
type Item struct {
// Basic item information
LowerName string `json:"lower_name"`
Name string `json:"name"`
Description string `json:"description"`
StackCount int16 `json:"stack_count"`
SellPrice int32 `json:"sell_price"`
SellStatus int32 `json:"sell_status"`
MaxSellValue int32 `json:"max_sell_value"`
BrokerPrice int64 `json:"broker_price"`
// Search and state flags
IsSearchStoreItem bool `json:"is_search_store_item"`
IsSearchInInventory bool `json:"is_search_in_inventory"`
SaveNeeded bool `json:"save_needed"`
NoBuyBack bool `json:"no_buy_back"`
NoSale bool `json:"no_sale"`
NeedsDeletion bool `json:"needs_deletion"`
Crafted bool `json:"crafted"`
Tinkered bool `json:"tinkered"`
// Item metadata
WeaponType int8 `json:"weapon_type"`
Adornment string `json:"adornment"`
Creator string `json:"creator"`
SellerName string `json:"seller_name"`
SellerCharID int32 `json:"seller_char_id"`
SellerHouseID int64 `json:"seller_house_id"`
Created time.Time `json:"created"`
GroupedCharIDs map[int32]bool `json:"grouped_char_ids"`
EffectType ItemEffectType `json:"effect_type"`
BookLanguage int8 `json:"book_language"`
// Adornment slots
Adorn0 int32 `json:"adorn0"`
Adorn1 int32 `json:"adorn1"`
Adorn2 int32 `json:"adorn2"`
// Spell information
SpellID int32 `json:"spell_id"`
SpellTier int8 `json:"spell_tier"`
ItemScript string `json:"item_script"`
// Collections and arrays
Classifications []*Classifications `json:"classifications"`
ItemStats []*ItemStat `json:"item_stats"`
ItemSets []*ItemSet `json:"item_sets"`
ItemStringStats []*ItemStatString `json:"item_string_stats"`
ItemLevelOverrides []*ItemLevelOverride `json:"item_level_overrides"`
ItemEffects []*ItemEffect `json:"item_effects"`
BookPages []*BookPage `json:"book_pages"`
SlotData []int8 `json:"slot_data"`
// Core item data
Details ItemCore `json:"details"`
GenericInfo GenericInfo `json:"generic_info"`
// Type-specific information (pointers to allow nil for unused types)
WeaponInfo *WeaponInfo `json:"weapon_info,omitempty"`
RangedInfo *RangedInfo `json:"ranged_info,omitempty"`
ArmorInfo *ArmorInfo `json:"armor_info,omitempty"`
AdornmentInfo *AdornmentInfo `json:"adornment_info,omitempty"`
BagInfo *BagInfo `json:"bag_info,omitempty"`
FoodInfo *FoodInfo `json:"food_info,omitempty"`
BaubleInfo *BaubleInfo `json:"bauble_info,omitempty"`
BookInfo *BookInfo `json:"book_info,omitempty"`
BookInfoPages *BookInfoPages `json:"book_info_pages,omitempty"`
HouseItemInfo *HouseItemInfo `json:"house_item_info,omitempty"`
HouseContainerInfo *HouseContainerInfo `json:"house_container_info,omitempty"`
SkillInfo *SkillInfo `json:"skill_info,omitempty"`
RecipeBookInfo *RecipeBookInfo `json:"recipe_book_info,omitempty"`
ItemSetInfo *ItemSetInfo `json:"item_set_info,omitempty"`
ThrownInfo *ThrownInfo `json:"thrown_info,omitempty"`
// Thread safety
mutex sync.RWMutex
}
// MasterItemList manages all items in the game
type MasterItemList struct {
items map[int32]*Item `json:"items"`
mappedItemStatsStrings map[string]int32 `json:"mapped_item_stats_strings"`
mappedItemStatTypeIDs map[int32]string `json:"mapped_item_stat_type_ids"`
brokerItemMap map[*VersionRange]map[int64]int64 `json:"-"` // Complex type, exclude from JSON
mutex sync.RWMutex
}
// VersionRange represents a version range for broker item mapping
type VersionRange struct {
MinVersion int32 `json:"min_version"`
MaxVersion int32 `json:"max_version"`
}
// PlayerItemList manages a player's inventory
type PlayerItemList struct {
maxSavedIndex int32 `json:"max_saved_index"`
indexedItems map[int32]*Item `json:"indexed_items"`
items map[int32]map[int8]map[int16]*Item `json:"items"`
overflowItems []*Item `json:"overflow_items"`
packetCount int16 `json:"packet_count"`
xorPacket []byte `json:"-"` // Exclude from JSON
origPacket []byte `json:"-"` // Exclude from JSON
mutex sync.RWMutex
}
// EquipmentItemList manages equipped items for a character
type EquipmentItemList struct {
items [NumSlots]*Item `json:"items"`
appearanceType int8 `json:"appearance_type"` // 0 for normal equip, 1 for appearance
xorPacket []byte `json:"-"` // Exclude from JSON
origPacket []byte `json:"-"` // Exclude from JSON
mutex sync.RWMutex
}
// ItemManagerStats represents statistics about item management
type ItemManagerStats struct {
TotalItems int32 `json:"total_items"`
ItemsByType map[int8]int32 `json:"items_by_type"`
ItemsByTier map[int8]int32 `json:"items_by_tier"`
PlayersWithItems int32 `json:"players_with_items"`
TotalItemInstances int64 `json:"total_item_instances"`
AverageItemsPerPlayer float32 `json:"average_items_per_player"`
LastUpdate time.Time `json:"last_update"`
}
// ItemSearchCriteria represents search criteria for items
type ItemSearchCriteria struct {
Name string `json:"name"`
ItemType int64 `json:"item_type"`
LocationType int64 `json:"location_type"`
BrokerType int64 `json:"broker_type"`
MinPrice int64 `json:"min_price"`
MaxPrice int64 `json:"max_price"`
MinSkill int8 `json:"min_skill"`
MaxSkill int8 `json:"max_skill"`
Seller string `json:"seller"`
Adornment string `json:"adornment"`
MinTier int8 `json:"min_tier"`
MaxTier int8 `json:"max_tier"`
MinLevel int16 `json:"min_level"`
MaxLevel int16 `json:"max_level"`
ItemClass int8 `json:"item_class"`
AdditionalCriteria map[string]string `json:"additional_criteria"`
}
// ItemValidationResult represents the result of item validation
type ItemValidationResult struct {
Valid bool `json:"valid"`
Errors []string `json:"errors,omitempty"`
}
// ItemError represents an item-specific error
type ItemError struct {
message string
}
func (e *ItemError) Error() string {
return e.message
}
// NewItemError creates a new item error
func NewItemError(message string) *ItemError {
return &ItemError{message: message}
}
// IsItemError checks if an error is an ItemError
func IsItemError(err error) bool {
_, ok := err.(*ItemError)
return ok
}
// Common item errors
var (
ErrItemNotFound = NewItemError("item not found")
ErrInvalidItem = NewItemError("invalid item")
ErrItemLocked = NewItemError("item is locked")
ErrInsufficientSpace = NewItemError("insufficient inventory space")
ErrCannotEquip = NewItemError("cannot equip item")
ErrCannotTrade = NewItemError("cannot trade item")
ErrItemExpired = NewItemError("item has expired")
)

View File

@ -4,23 +4,23 @@ package languages
const (
// Maximum language name length
MaxLanguageNameLength = 50
// Special language IDs (common in EQ2)
LanguageIDCommon = 0 // Common tongue (default)
LanguageIDElvish = 1 // Elvish
LanguageIDDwarven = 2 // Dwarven
LanguageIDHalfling = 3 // Halfling
LanguageIDGnomish = 4 // Gnomish
LanguageIDIksar = 5 // Iksar
LanguageIDTrollish = 6 // Trollish
LanguageIDOgrish = 7 // Ogrish
LanguageIDFae = 8 // Fae
LanguageIDArasai = 9 // Arasai
LanguageIDSarnak = 10 // Sarnak
LanguageIDFroglok = 11 // Froglok
LanguageIDCommon = 0 // Common tongue (default)
LanguageIDElvish = 1 // Elvish
LanguageIDDwarven = 2 // Dwarven
LanguageIDHalfling = 3 // Halfling
LanguageIDGnomish = 4 // Gnomish
LanguageIDIksar = 5 // Iksar
LanguageIDTrollish = 6 // Trollish
LanguageIDOgrish = 7 // Ogrish
LanguageIDFae = 8 // Fae
LanguageIDArasai = 9 // Arasai
LanguageIDSarnak = 10 // Sarnak
LanguageIDFroglok = 11 // Froglok
)
// Language validation constants
// Language validation constants
const (
MinLanguageID = 0
MaxLanguageID = 999999 // Reasonable upper bound
@ -34,6 +34,6 @@ const (
// System limits
const (
MaxLanguagesPerPlayer = 100 // Reasonable limit to prevent abuse
MaxLanguagesPerPlayer = 100 // Reasonable limit to prevent abuse
MaxTotalLanguages = 1000 // System-wide language limit
)
)

View File

@ -1,5 +1,7 @@
package languages
import "fmt"
// Database interface for language persistence
type Database interface {
LoadAllLanguages() ([]*Language, error)
@ -69,11 +71,11 @@ type ChatProcessor interface {
// PlayerLanguageAdapter provides language functionality for players
type PlayerLanguageAdapter struct {
player *Player
languages *PlayerLanguagesList
primaryLang int32
manager *Manager
logger Logger
player *Player
languages *PlayerLanguagesList
primaryLang int32
manager *Manager
logger Logger
}
// NewPlayerLanguageAdapter creates a new player language adapter
@ -107,7 +109,7 @@ func (pla *PlayerLanguageAdapter) SetPrimaryLanguage(languageID int32) {
// Only allow setting to a known language
if pla.languages.HasLanguage(languageID) {
pla.primaryLang = languageID
if pla.logger != nil {
lang := pla.manager.GetLanguage(languageID)
langName := "Unknown"
@ -126,7 +128,7 @@ func (pla *PlayerLanguageAdapter) CanUnderstand(languageID int32) bool {
if languageID == LanguageIDCommon {
return true
}
// Check if player knows the language
return pla.languages.HasLanguage(languageID)
}
@ -138,29 +140,29 @@ func (pla *PlayerLanguageAdapter) LearnLanguage(languageID int32) error {
if language == nil {
return fmt.Errorf("language with ID %d does not exist", languageID)
}
// Check if already known
if pla.languages.HasLanguage(languageID) {
return fmt.Errorf("player already knows language %s", language.GetName())
}
// Create a copy for the player
playerLang := language.Copy()
playerLang.SetSaveNeeded(true)
// Add to player's languages
if err := pla.languages.Add(playerLang); err != nil {
return fmt.Errorf("failed to add language to player: %w", err)
}
// Record usage statistics
pla.manager.RecordLanguageUsage(languageID)
if pla.logger != nil {
pla.logger.LogInfo("Player %s learned language %s (%d)",
pla.player.GetName(), language.GetName(), languageID)
}
return nil
}
@ -170,34 +172,34 @@ func (pla *PlayerLanguageAdapter) ForgetLanguage(languageID int32) error {
if languageID == LanguageIDCommon {
return fmt.Errorf("cannot forget common language")
}
// Check if player knows the language
if !pla.languages.HasLanguage(languageID) {
return fmt.Errorf("player does not know language %d", languageID)
}
// Get language name for logging
language := pla.manager.GetLanguage(languageID)
langName := "Unknown"
if language != nil {
langName = language.GetName()
}
// Remove from player's languages
if !pla.languages.RemoveLanguage(languageID) {
return fmt.Errorf("failed to remove language from player")
}
// Reset primary language if this was it
if pla.primaryLang == languageID {
pla.primaryLang = LanguageIDCommon
}
if pla.logger != nil {
pla.logger.LogInfo("Player %s forgot language %s (%d)",
pla.player.GetName(), langName, languageID)
}
return nil
}
@ -206,16 +208,16 @@ func (pla *PlayerLanguageAdapter) LoadPlayerLanguages(database Database) error {
if database == nil {
return fmt.Errorf("database is nil")
}
playerID := pla.player.GetCharacterID()
languages, err := database.LoadPlayerLanguages(playerID)
if err != nil {
return fmt.Errorf("failed to load player languages: %w", err)
}
// Clear current languages
pla.languages.Clear()
// Add loaded languages
for _, lang := range languages {
if err := pla.languages.Add(lang); err != nil && pla.logger != nil {
@ -223,7 +225,7 @@ func (pla *PlayerLanguageAdapter) LoadPlayerLanguages(database Database) error {
lang.GetID(), pla.player.GetName(), err)
}
}
// Ensure player knows common language
if !pla.languages.HasLanguage(LanguageIDCommon) {
commonLang := pla.manager.GetLanguage(LanguageIDCommon)
@ -232,12 +234,12 @@ func (pla *PlayerLanguageAdapter) LoadPlayerLanguages(database Database) error {
pla.languages.Add(playerCommon)
}
}
if pla.logger != nil {
pla.logger.LogDebug("Loaded %d languages for player %s",
len(languages), pla.player.GetName())
}
return nil
}
@ -246,10 +248,10 @@ func (pla *PlayerLanguageAdapter) SavePlayerLanguages(database Database) error {
if database == nil {
return fmt.Errorf("database is nil")
}
playerID := pla.player.GetCharacterID()
languages := pla.languages.GetAllLanguages()
// Save each language that needs saving
for _, lang := range languages {
if lang.GetSaveNeeded() {
@ -259,11 +261,11 @@ func (pla *PlayerLanguageAdapter) SavePlayerLanguages(database Database) error {
lang.SetSaveNeeded(false)
}
}
if pla.logger != nil {
pla.logger.LogDebug("Saved languages for player %s", pla.player.GetName())
}
return nil
}
@ -286,21 +288,21 @@ func (clp *ChatLanguageProcessor) ProcessMessage(speaker *Player, message string
if speaker == nil {
return "", fmt.Errorf("speaker cannot be nil")
}
// Validate language exists
language := clp.manager.GetLanguage(languageID)
if language == nil {
return "", fmt.Errorf("language %d does not exist", languageID)
}
// Check if speaker knows the language
if !speaker.KnowsLanguage(languageID) {
return "", fmt.Errorf("speaker does not know language %s", language.GetName())
}
// Record language usage
clp.manager.RecordLanguageUsage(languageID)
return message, nil
}
@ -309,17 +311,17 @@ func (clp *ChatLanguageProcessor) FilterMessage(listener *Player, message string
if listener == nil {
return message
}
// Common language is always understood
if languageID == LanguageIDCommon {
return message
}
// Check if listener knows the language
if listener.KnowsLanguage(languageID) {
return message
}
// Scramble the message for unknown languages
return clp.GetLanguageSkramble(message, 0.0)
}
@ -329,12 +331,12 @@ func (clp *ChatLanguageProcessor) GetLanguageSkramble(message string, comprehens
if comprehension >= 1.0 {
return message
}
if comprehension <= 0.0 {
// Complete scramble - replace with gibberish
runes := []rune(message)
scrambled := make([]rune, len(runes))
for i, r := range runes {
if r == ' ' {
scrambled[i] = ' '
@ -346,10 +348,10 @@ func (clp *ChatLanguageProcessor) GetLanguageSkramble(message string, comprehens
scrambled[i] = r
}
}
return string(scrambled)
}
// Partial comprehension - scramble some words
// This is a simplified implementation
return message
@ -374,18 +376,18 @@ func (lea *LanguageEventAdapter) ProcessLanguageEvent(eventType string, player *
if lea.handler == nil {
return
}
switch eventType {
case "language_learned":
if err := lea.handler.OnLanguageLearned(player, languageID); err != nil && lea.logger != nil {
lea.logger.LogError("Language learned handler failed: %v", err)
}
case "language_forgotten":
if err := lea.handler.OnLanguageForgotten(player, languageID); err != nil && lea.logger != nil {
lea.logger.LogError("Language forgotten handler failed: %v", err)
}
case "language_used":
if message, ok := data.(string); ok {
if err := lea.handler.OnLanguageUsed(player, languageID, message); err != nil && lea.logger != nil {
@ -393,4 +395,4 @@ func (lea *LanguageEventAdapter) ProcessLanguageEvent(eventType string, player *
}
}
}
}
}

View File

@ -7,24 +7,24 @@ import (
// Manager provides high-level management of the language system
type Manager struct {
masterLanguagesList *MasterLanguagesList
database Database
logger Logger
mutex sync.RWMutex
masterLanguagesList *MasterLanguagesList
database Database
logger Logger
mutex sync.RWMutex
// Statistics
languageLookups int64
playersWithLanguages int64
languageUsageCount map[int32]int64 // Language ID -> usage count
languageLookups int64
playersWithLanguages int64
languageUsageCount map[int32]int64 // Language ID -> usage count
}
// NewManager creates a new language manager
func NewManager(database Database, logger Logger) *Manager {
return &Manager{
masterLanguagesList: NewMasterLanguagesList(),
database: database,
logger: logger,
languageUsageCount: make(map[int32]int64),
masterLanguagesList: NewMasterLanguagesList(),
database: database,
logger: logger,
languageUsageCount: make(map[int32]int64),
}
}
@ -199,11 +199,11 @@ func (m *Manager) GetStatistics() *LanguageStatistics {
}
return &LanguageStatistics{
TotalLanguages: len(allLanguages),
PlayersWithLanguages: int(m.playersWithLanguages),
LanguageUsageCount: usageCount,
LanguageLookups: m.languageLookups,
LanguagesByName: languagesByName,
TotalLanguages: len(allLanguages),
PlayersWithLanguages: int(m.playersWithLanguages),
LanguageUsageCount: usageCount,
LanguageLookups: m.languageLookups,
LanguagesByName: languagesByName,
}
}
@ -491,7 +491,7 @@ func contains(str, substr string) bool {
// Convert to lowercase for case-insensitive comparison
strLower := make([]byte, len(str))
substrLower := make([]byte, len(substr))
for i := 0; i < len(str); i++ {
if str[i] >= 'A' && str[i] <= 'Z' {
strLower[i] = str[i] + 32
@ -499,7 +499,7 @@ func contains(str, substr string) bool {
strLower[i] = str[i]
}
}
for i := 0; i < len(substr); i++ {
if substr[i] >= 'A' && substr[i] <= 'Z' {
substrLower[i] = substr[i] + 32
@ -522,4 +522,4 @@ func contains(str, substr string) bool {
}
return false
}
}

View File

@ -7,9 +7,9 @@ import (
// Language represents a single language that can be learned by players
type Language struct {
id int32 // Unique language identifier
name string // Language name
saveNeeded bool // Whether this language needs to be saved to database
id int32 // Unique language identifier
name string // Language name
saveNeeded bool // Whether this language needs to be saved to database
mutex sync.RWMutex // Thread safety
}
@ -27,10 +27,10 @@ func NewLanguageFromExisting(source *Language) *Language {
if source == nil {
return NewLanguage()
}
source.mutex.RLock()
defer source.mutex.RUnlock()
return &Language{
id: source.id,
name: source.name,
@ -42,7 +42,7 @@ func NewLanguageFromExisting(source *Language) *Language {
func (l *Language) GetID() int32 {
l.mutex.RLock()
defer l.mutex.RUnlock()
return l.id
}
@ -50,7 +50,7 @@ func (l *Language) GetID() int32 {
func (l *Language) SetID(id int32) {
l.mutex.Lock()
defer l.mutex.Unlock()
l.id = id
}
@ -58,7 +58,7 @@ func (l *Language) SetID(id int32) {
func (l *Language) GetName() string {
l.mutex.RLock()
defer l.mutex.RUnlock()
return l.name
}
@ -66,12 +66,12 @@ func (l *Language) GetName() string {
func (l *Language) SetName(name string) {
l.mutex.Lock()
defer l.mutex.Unlock()
// Truncate if too long
if len(name) > MaxLanguageNameLength {
name = name[:MaxLanguageNameLength]
}
l.name = name
}
@ -79,7 +79,7 @@ func (l *Language) SetName(name string) {
func (l *Language) GetSaveNeeded() bool {
l.mutex.RLock()
defer l.mutex.RUnlock()
return l.saveNeeded
}
@ -87,7 +87,7 @@ func (l *Language) GetSaveNeeded() bool {
func (l *Language) SetSaveNeeded(needed bool) {
l.mutex.Lock()
defer l.mutex.Unlock()
l.saveNeeded = needed
}
@ -95,15 +95,15 @@ func (l *Language) SetSaveNeeded(needed bool) {
func (l *Language) IsValid() bool {
l.mutex.RLock()
defer l.mutex.RUnlock()
if l.id < MinLanguageID || l.id > MaxLanguageID {
return false
}
if len(l.name) == 0 || len(l.name) > MaxLanguageNameLength {
return false
}
return true
}
@ -111,7 +111,7 @@ func (l *Language) IsValid() bool {
func (l *Language) String() string {
l.mutex.RLock()
defer l.mutex.RUnlock()
return fmt.Sprintf("Language{ID: %d, Name: %s, SaveNeeded: %v}", l.id, l.name, l.saveNeeded)
}
@ -122,7 +122,7 @@ func (l *Language) Copy() *Language {
// MasterLanguagesList manages the global list of all available languages
type MasterLanguagesList struct {
languages map[int32]*Language // Languages indexed by ID for fast lookup
languages map[int32]*Language // Languages indexed by ID for fast lookup
nameIndex map[string]*Language // Languages indexed by name for name lookups
mutex sync.RWMutex // Thread safety
}
@ -139,7 +139,7 @@ func NewMasterLanguagesList() *MasterLanguagesList {
func (mll *MasterLanguagesList) Clear() {
mll.mutex.Lock()
defer mll.mutex.Unlock()
mll.languages = make(map[int32]*Language)
mll.nameIndex = make(map[string]*Language)
}
@ -148,7 +148,7 @@ func (mll *MasterLanguagesList) Clear() {
func (mll *MasterLanguagesList) Size() int32 {
mll.mutex.RLock()
defer mll.mutex.RUnlock()
return int32(len(mll.languages))
}
@ -157,29 +157,29 @@ func (mll *MasterLanguagesList) AddLanguage(language *Language) error {
if language == nil {
return fmt.Errorf("language cannot be nil")
}
if !language.IsValid() {
return fmt.Errorf("language is not valid: %s", language.String())
}
mll.mutex.Lock()
defer mll.mutex.Unlock()
// Check for duplicate ID
if _, exists := mll.languages[language.GetID()]; exists {
return fmt.Errorf("language with ID %d already exists", language.GetID())
}
// Check for duplicate name
name := language.GetName()
if _, exists := mll.nameIndex[name]; exists {
return fmt.Errorf("language with name '%s' already exists", name)
}
// Add to both indexes
mll.languages[language.GetID()] = language
mll.nameIndex[name] = language
return nil
}
@ -187,7 +187,7 @@ func (mll *MasterLanguagesList) AddLanguage(language *Language) error {
func (mll *MasterLanguagesList) GetLanguage(id int32) *Language {
mll.mutex.RLock()
defer mll.mutex.RUnlock()
return mll.languages[id]
}
@ -195,7 +195,7 @@ func (mll *MasterLanguagesList) GetLanguage(id int32) *Language {
func (mll *MasterLanguagesList) GetLanguageByName(name string) *Language {
mll.mutex.RLock()
defer mll.mutex.RUnlock()
return mll.nameIndex[name]
}
@ -203,12 +203,12 @@ func (mll *MasterLanguagesList) GetLanguageByName(name string) *Language {
func (mll *MasterLanguagesList) GetAllLanguages() []*Language {
mll.mutex.RLock()
defer mll.mutex.RUnlock()
result := make([]*Language, 0, len(mll.languages))
for _, lang := range mll.languages {
result = append(result, lang)
}
return result
}
@ -216,7 +216,7 @@ func (mll *MasterLanguagesList) GetAllLanguages() []*Language {
func (mll *MasterLanguagesList) HasLanguage(id int32) bool {
mll.mutex.RLock()
defer mll.mutex.RUnlock()
_, exists := mll.languages[id]
return exists
}
@ -225,7 +225,7 @@ func (mll *MasterLanguagesList) HasLanguage(id int32) bool {
func (mll *MasterLanguagesList) HasLanguageByName(name string) bool {
mll.mutex.RLock()
defer mll.mutex.RUnlock()
_, exists := mll.nameIndex[name]
return exists
}
@ -234,16 +234,16 @@ func (mll *MasterLanguagesList) HasLanguageByName(name string) bool {
func (mll *MasterLanguagesList) RemoveLanguage(id int32) bool {
mll.mutex.Lock()
defer mll.mutex.Unlock()
language, exists := mll.languages[id]
if !exists {
return false
}
// Remove from both indexes
delete(mll.languages, id)
delete(mll.nameIndex, language.GetName())
return true
}
@ -252,37 +252,37 @@ func (mll *MasterLanguagesList) UpdateLanguage(language *Language) error {
if language == nil {
return fmt.Errorf("language cannot be nil")
}
if !language.IsValid() {
return fmt.Errorf("language is not valid: %s", language.String())
}
mll.mutex.Lock()
defer mll.mutex.Unlock()
id := language.GetID()
oldLanguage, exists := mll.languages[id]
if !exists {
return fmt.Errorf("language with ID %d does not exist", id)
}
// Remove old name index if name changed
oldName := oldLanguage.GetName()
newName := language.GetName()
if oldName != newName {
delete(mll.nameIndex, oldName)
// Check for name conflicts
if _, exists := mll.nameIndex[newName]; exists {
return fmt.Errorf("language with name '%s' already exists", newName)
}
mll.nameIndex[newName] = language
}
// Update language
mll.languages[id] = language
return nil
}
@ -290,18 +290,18 @@ func (mll *MasterLanguagesList) UpdateLanguage(language *Language) error {
func (mll *MasterLanguagesList) GetLanguageNames() []string {
mll.mutex.RLock()
defer mll.mutex.RUnlock()
names := make([]string, 0, len(mll.nameIndex))
for name := range mll.nameIndex {
names = append(names, name)
}
return names
}
// PlayerLanguagesList manages languages known by a specific player
type PlayerLanguagesList struct {
languages map[int32]*Language // Player's languages indexed by ID
languages map[int32]*Language // Player's languages indexed by ID
nameIndex map[string]*Language // Player's languages indexed by name
mutex sync.RWMutex // Thread safety
}
@ -318,7 +318,7 @@ func NewPlayerLanguagesList() *PlayerLanguagesList {
func (pll *PlayerLanguagesList) Clear() {
pll.mutex.Lock()
defer pll.mutex.Unlock()
pll.languages = make(map[int32]*Language)
pll.nameIndex = make(map[string]*Language)
}
@ -328,27 +328,27 @@ func (pll *PlayerLanguagesList) Add(language *Language) error {
if language == nil {
return fmt.Errorf("language cannot be nil")
}
pll.mutex.Lock()
defer pll.mutex.Unlock()
id := language.GetID()
name := language.GetName()
// Check if already known
if _, exists := pll.languages[id]; exists {
return fmt.Errorf("player already knows language with ID %d", id)
}
// Check player language limit
if len(pll.languages) >= MaxLanguagesPerPlayer {
return fmt.Errorf("player has reached maximum language limit (%d)", MaxLanguagesPerPlayer)
}
// Add to both indexes
pll.languages[id] = language
pll.nameIndex[name] = language
return nil
}
@ -356,7 +356,7 @@ func (pll *PlayerLanguagesList) Add(language *Language) error {
func (pll *PlayerLanguagesList) GetLanguage(id int32) *Language {
pll.mutex.RLock()
defer pll.mutex.RUnlock()
return pll.languages[id]
}
@ -364,7 +364,7 @@ func (pll *PlayerLanguagesList) GetLanguage(id int32) *Language {
func (pll *PlayerLanguagesList) GetLanguageByName(name string) *Language {
pll.mutex.RLock()
defer pll.mutex.RUnlock()
return pll.nameIndex[name]
}
@ -372,12 +372,12 @@ func (pll *PlayerLanguagesList) GetLanguageByName(name string) *Language {
func (pll *PlayerLanguagesList) GetAllLanguages() []*Language {
pll.mutex.RLock()
defer pll.mutex.RUnlock()
result := make([]*Language, 0, len(pll.languages))
for _, lang := range pll.languages {
result = append(result, lang)
}
return result
}
@ -385,7 +385,7 @@ func (pll *PlayerLanguagesList) GetAllLanguages() []*Language {
func (pll *PlayerLanguagesList) HasLanguage(id int32) bool {
pll.mutex.RLock()
defer pll.mutex.RUnlock()
_, exists := pll.languages[id]
return exists
}
@ -394,7 +394,7 @@ func (pll *PlayerLanguagesList) HasLanguage(id int32) bool {
func (pll *PlayerLanguagesList) HasLanguageByName(name string) bool {
pll.mutex.RLock()
defer pll.mutex.RUnlock()
_, exists := pll.nameIndex[name]
return exists
}
@ -403,16 +403,16 @@ func (pll *PlayerLanguagesList) HasLanguageByName(name string) bool {
func (pll *PlayerLanguagesList) RemoveLanguage(id int32) bool {
pll.mutex.Lock()
defer pll.mutex.Unlock()
language, exists := pll.languages[id]
if !exists {
return false
}
// Remove from both indexes
delete(pll.languages, id)
delete(pll.nameIndex, language.GetName())
return true
}
@ -420,7 +420,7 @@ func (pll *PlayerLanguagesList) RemoveLanguage(id int32) bool {
func (pll *PlayerLanguagesList) Size() int32 {
pll.mutex.RLock()
defer pll.mutex.RUnlock()
return int32(len(pll.languages))
}
@ -428,33 +428,33 @@ func (pll *PlayerLanguagesList) Size() int32 {
func (pll *PlayerLanguagesList) GetLanguageIDs() []int32 {
pll.mutex.RLock()
defer pll.mutex.RUnlock()
ids := make([]int32, 0, len(pll.languages))
for id := range pll.languages {
ids = append(ids, id)
}
return ids
}
// GetLanguageNames returns all language names the player knows
// GetLanguageNames returns all language names the player knows
func (pll *PlayerLanguagesList) GetLanguageNames() []string {
pll.mutex.RLock()
defer pll.mutex.RUnlock()
names := make([]string, 0, len(pll.nameIndex))
for name := range pll.nameIndex {
names = append(names, name)
}
return names
}
// LanguageStatistics contains language system statistics
type LanguageStatistics struct {
TotalLanguages int `json:"total_languages"`
PlayersWithLanguages int `json:"players_with_languages"`
LanguageUsageCount map[int32]int64 `json:"language_usage_count"`
LanguageLookups int64 `json:"language_lookups"`
LanguagesByName map[string]int32 `json:"languages_by_name"`
}
TotalLanguages int `json:"total_languages"`
PlayersWithLanguages int `json:"players_with_languages"`
LanguageUsageCount map[int32]int64 `json:"language_usage_count"`
LanguageLookups int64 `json:"language_lookups"`
LanguagesByName map[string]int32 `json:"languages_by_name"`
}

View File

@ -11,23 +11,23 @@ type Brain interface {
// Core AI methods
Think() error
GetBrainType() int8
// State management
IsActive() bool
SetActive(bool)
GetState() int32
SetState(int32)
// Timing
GetThinkTick() int32
SetThinkTick(int32)
GetLastThink() int64
SetLastThink(int64)
// Body management
GetBody() NPC
SetBody(NPC)
// Hate management
AddHate(entityID int32, hate int32)
GetHate(entityID int32) int32
@ -36,7 +36,7 @@ type Brain interface {
GetMostHated() int32
GetHatePercentage(entityID int32) int8
GetHateList() map[int32]*HateEntry
// Encounter management
AddToEncounter(entityID, characterID int32, isPlayer, isBot bool) bool
IsEntityInEncounter(entityID int32) bool
@ -45,14 +45,14 @@ type Brain interface {
GetEncounterSize() int
ClearEncounter()
CheckLootAllowed(entityID int32) bool
// Combat methods
ProcessSpell(target Entity, distance float32) bool
ProcessMelee(target Entity, distance float32)
CheckBuffs() bool
HasRecovered() bool
MoveCloser(target Spawn)
// Statistics
GetStatistics() *BrainStatistics
ResetStatistics()
@ -60,14 +60,14 @@ type Brain interface {
// BaseBrain provides the default AI implementation
type BaseBrain struct {
npc NPC // The NPC this brain controls
brainType int8 // Type of brain
state *BrainState // Brain state management
hateList *HateList // Hate management
encounterList *EncounterList // Encounter management
statistics *BrainStatistics // Performance statistics
logger Logger // Logger interface
mutex sync.RWMutex // Thread safety
npc NPC // The NPC this brain controls
brainType int8 // Type of brain
state *BrainState // Brain state management
hateList *HateList // Hate management
encounterList *EncounterList // Encounter management
statistics *BrainStatistics // Performance statistics
logger Logger // Logger interface
mutex sync.RWMutex // Thread safety
}
// NewBaseBrain creates a new base brain
@ -88,7 +88,7 @@ func (bb *BaseBrain) Think() error {
if !bb.IsActive() {
return nil
}
startTime := time.Now()
defer func() {
// Update statistics
@ -98,23 +98,23 @@ func (bb *BaseBrain) Think() error {
bb.statistics.AverageThinkTime = (bb.statistics.AverageThinkTime + thinkTime) / 2.0
bb.statistics.LastThinkTime = time.Now().UnixMilli()
bb.mutex.Unlock()
bb.state.SetLastThink(time.Now().UnixMilli())
}()
if bb.npc == nil {
return fmt.Errorf("brain has no body")
}
// Handle pet ID registration for players
if bb.npc.IsPet() && bb.npc.GetOwner() != nil && bb.npc.GetOwner().IsPlayer() {
// TODO: Register pet ID with player's info struct
}
// Get the most hated target
mostHatedID := bb.hateList.GetMostHated()
var target Entity
if mostHatedID > 0 {
target = bb.getEntityByID(mostHatedID)
// Remove dead targets from hate list
@ -128,29 +128,29 @@ func (bb *BaseBrain) Think() error {
}
}
}
// Skip if mezzed, stunned, or feared
if bb.npc.IsMezzedOrStunned() {
return nil
}
// Get runback distance
runbackDistance := bb.npc.GetRunbackDistance()
if target != nil {
// We have a target to fight
if bb.logger != nil && bb.state.GetDebugLevel() >= DebugLevelDetailed {
bb.logger.LogDebug("NPC %s has target %s", bb.npc.GetName(), target.GetName())
}
// Set target if not already set
if bb.npc.GetTarget() != target {
bb.npc.SetTarget(target)
}
// Face the target
bb.npc.FaceTarget(target, false)
// Enter combat if not already in combat
if !bb.npc.GetInCombat() {
bb.npc.ClearRunningLocations()
@ -158,21 +158,21 @@ func (bb *BaseBrain) Think() error {
bb.npc.SetCastOnAggroCompleted(false)
// TODO: Call spawn script for aggro
}
// Check chase distance and water restrictions
if bb.shouldBreakPursuit(target, runbackDistance) {
// Break pursuit - clear hate and encounter
if bb.logger != nil {
bb.logger.LogDebug("NPC %s breaking pursuit (distance: %.2f)", bb.npc.GetName(), runbackDistance)
}
// TODO: Send encounter break messages to players
bb.hateList.Clear()
bb.encounterList.Clear()
} else {
// Continue combat
distance := bb.npc.GetDistance(target)
// Try to cast spells first, then melee
if !bb.npc.IsCasting() && (!bb.HasRecovered() || !bb.ProcessSpell(target, distance)) {
if bb.logger != nil && bb.state.GetDebugLevel() >= DebugLevelDetailed {
@ -185,19 +185,19 @@ func (bb *BaseBrain) Think() error {
} else {
// No target - handle out of combat behavior
wasInCombat := bb.npc.GetInCombat()
if bb.npc.GetInCombat() {
bb.npc.InCombat(false)
// Restore HP for non-player pets
if !bb.npc.IsPet() || (bb.npc.IsPet() && bb.npc.GetOwner() != nil && !bb.npc.GetOwner().IsPlayer()) {
bb.npc.SetHP(bb.npc.GetTotalHP())
}
}
// Check for buffs when not in combat
bb.CheckBuffs()
// Handle runback if needed
if !bb.npc.GetInCombat() && !bb.npc.IsPauseMovementTimerActive() {
if runbackDistance > RunbackThreshold || (bb.npc.ShouldCallRunback() && !bb.npc.IsFollowing()) {
@ -208,13 +208,13 @@ func (bb *BaseBrain) Think() error {
bb.handleRunbackStages()
}
}
// Clear encounter if any entities remain
if bb.encounterList.Size() > 0 {
bb.encounterList.Clear()
}
}
return nil
}
@ -283,26 +283,26 @@ func (bb *BaseBrain) AddHate(entityID int32, hate int32) {
if bb.npc != nil && bb.npc.IsRunningBack() {
return
}
// Don't add hate if owner is attacking pet
if bb.npc != nil && bb.npc.IsPet() && bb.npc.GetOwner() != nil {
if bb.npc.GetOwner().GetID() == entityID {
return
}
}
// Check for taunt immunity
// TODO: Implement immunity checking
bb.hateList.AddHate(entityID, hate)
// Update statistics
bb.mutex.Lock()
bb.statistics.HateEvents++
bb.mutex.Unlock()
// TODO: Add to entity's HatedBy list
// Add pet owner to hate list if not already present
entity := bb.getEntityByID(entityID)
if entity != nil && entity.IsPet() && entity.GetOwner() != nil {
@ -385,7 +385,7 @@ func (bb *BaseBrain) ClearEncounter() {
// CheckLootAllowed checks if an entity can loot this NPC
func (bb *BaseBrain) CheckLootAllowed(entityID int32) bool {
// TODO: Implement loot method checking, chest timers, etc.
// Basic check - is entity in encounter?
return bb.encounterList.IsEntityInEncounter(entityID)
}
@ -395,29 +395,29 @@ func (bb *BaseBrain) ProcessSpell(target Entity, distance float32) bool {
if bb.npc == nil {
return false
}
// Check cast percentage and conditions
castChance := bb.npc.GetCastPercentage()
if castChance <= 0 {
return false
}
// TODO: Implement random chance checking
// TODO: Check for stifled, feared conditions
// Get next spell to cast
spell := bb.npc.GetNextSpell(target, distance)
if spell == nil {
return false
}
// Determine spell target
var spellTarget Spawn = target
if spell.IsFriendlySpell() {
// TODO: Find best friendly target (lowest HP group member)
spellTarget = bb.npc
}
// Cast the spell
success := bb.castSpell(spell, spellTarget, false)
if success {
@ -425,7 +425,7 @@ func (bb *BaseBrain) ProcessSpell(target Entity, distance float32) bool {
bb.statistics.SpellsCast++
bb.mutex.Unlock()
}
return success
}
@ -434,44 +434,44 @@ func (bb *BaseBrain) ProcessMelee(target Entity, distance float32) {
if bb.npc == nil || target == nil {
return
}
maxCombatRange := bb.getMaxCombatRange()
if distance > maxCombatRange {
bb.MoveCloser(target)
} else {
if bb.logger != nil && bb.state.GetDebugLevel() >= DebugLevelDetailed {
bb.logger.LogDebug("NPC %s is within melee range of %s", bb.npc.GetName(), target.GetName())
}
// Check if attack is allowed
if !bb.npc.AttackAllowed(target) {
return
}
currentTime := time.Now().UnixMilli()
// Primary weapon attack
if bb.npc.PrimaryWeaponReady() && !bb.npc.IsDazed() && !bb.npc.IsFeared() {
if bb.logger != nil && bb.state.GetDebugLevel() >= DebugLevelVerbose {
bb.logger.LogDebug("NPC %s swings primary weapon at %s", bb.npc.GetName(), target.GetName())
}
bb.npc.SetPrimaryLastAttackTime(currentTime)
bb.npc.MeleeAttack(target, distance, true)
bb.mutex.Lock()
bb.statistics.MeleeAttacks++
bb.mutex.Unlock()
// TODO: Call spawn script for auto attack tick
}
// Secondary weapon attack
if bb.npc.SecondaryWeaponReady() && !bb.npc.IsDazed() {
bb.npc.SetSecondaryLastAttackTime(currentTime)
bb.npc.MeleeAttack(target, distance, false)
bb.mutex.Lock()
bb.statistics.MeleeAttacks++
bb.mutex.Unlock()
@ -484,26 +484,26 @@ func (bb *BaseBrain) CheckBuffs() bool {
if bb.npc == nil {
return false
}
// Don't buff in combat, while casting, stunned, etc.
if bb.npc.GetInCombat() || bb.npc.IsCasting() || bb.npc.IsMezzedOrStunned() ||
!bb.npc.IsAlive() || bb.npc.IsStifled() || !bb.HasRecovered() {
return false
}
// Get next buff spell
buffSpell := bb.npc.GetNextBuffSpell(bb.npc)
if buffSpell == nil {
return false
}
// Try to cast on self first
if bb.castSpell(buffSpell, bb.npc, false) {
return true
}
// TODO: Try to buff group members
return false
}
@ -517,13 +517,13 @@ func (bb *BaseBrain) MoveCloser(target Spawn) {
if bb.npc == nil || target == nil {
return
}
maxCombatRange := bb.getMaxCombatRange()
if bb.npc.GetFollowTarget() != target {
bb.npc.SetFollowTarget(target, maxCombatRange)
}
if bb.npc.GetFollowTarget() != nil && !bb.npc.IsFollowing() {
bb.npc.CalculateRunningLocation(true)
bb.npc.SetFollowing(true)
@ -534,7 +534,7 @@ func (bb *BaseBrain) MoveCloser(target Spawn) {
func (bb *BaseBrain) GetStatistics() *BrainStatistics {
bb.mutex.RLock()
defer bb.mutex.RUnlock()
// Return a copy
return &BrainStatistics{
ThinkCycles: bb.statistics.ThinkCycles,
@ -552,7 +552,7 @@ func (bb *BaseBrain) GetStatistics() *BrainStatistics {
func (bb *BaseBrain) ResetStatistics() {
bb.mutex.Lock()
defer bb.mutex.Unlock()
bb.statistics = NewBrainStatistics()
}
@ -563,18 +563,18 @@ func (bb *BaseBrain) shouldBreakPursuit(target Entity, runbackDistance float32)
if target == nil {
return false
}
// Check max chase distance
maxChase := bb.getMaxChaseDistance()
if runbackDistance > maxChase {
return true
}
// Check water creature restrictions
if bb.npc != nil && bb.npc.IsWaterCreature() && !bb.npc.IsFlyingCreature() && !target.InWater() {
return true
}
return false
}
@ -583,21 +583,21 @@ func (bb *BaseBrain) castSpell(spell Spell, target Spawn, calculateRunLoc bool)
if spell == nil || bb.npc == nil {
return false
}
if calculateRunLoc {
bb.npc.CalculateRunningLocation(true)
}
// TODO: Process spell through zone
// bb.npc.GetZone().ProcessSpell(spell, bb.npc, target)
// Set spell recovery time
castTime := spell.GetCastTime() * RecoveryTimeMultiple
recoveryTime := spell.GetRecoveryTime() * RecoveryTimeMultiple
totalRecovery := time.Now().UnixMilli() + int64(castTime) + int64(recoveryTime) + int64(SpellRecoveryBuffer)
bb.state.SetSpellRecovery(totalRecovery)
return true
}
@ -606,12 +606,12 @@ func (bb *BaseBrain) handleRunbackStages() {
if bb.npc == nil {
return
}
runbackLoc := bb.npc.GetRunbackLocation()
if runbackLoc == nil {
return
}
// TODO: Implement runback stage handling
// This would involve movement management and position updates
}
@ -632,4 +632,4 @@ func (bb *BaseBrain) getMaxChaseDistance() float32 {
func (bb *BaseBrain) getMaxCombatRange() float32 {
// TODO: Check zone rules
return MaxCombatRange
}
}

View File

@ -2,36 +2,36 @@ package ai
// AI tick constants
const (
DefaultThinkTick int32 = 250 // Default think tick in milliseconds (1/4 second)
FastThinkTick int32 = 100 // Fast think tick for active AI
SlowThinkTick int32 = 1000 // Slow think tick for idle AI
BlankBrainTick int32 = 50000 // Very slow tick for blank brain
MaxThinkTick int32 = 60000 // Maximum think tick (1 minute)
DefaultThinkTick int32 = 250 // Default think tick in milliseconds (1/4 second)
FastThinkTick int32 = 100 // Fast think tick for active AI
SlowThinkTick int32 = 1000 // Slow think tick for idle AI
BlankBrainTick int32 = 50000 // Very slow tick for blank brain
MaxThinkTick int32 = 60000 // Maximum think tick (1 minute)
)
// Combat constants
const (
MaxChaseDistance float32 = 150.0 // Default max chase distance
MaxCombatRange float32 = 25.0 // Default max combat range
RunbackThreshold float32 = 1.0 // Distance threshold for runback
MaxChaseDistance float32 = 150.0 // Default max chase distance
MaxCombatRange float32 = 25.0 // Default max combat range
RunbackThreshold float32 = 1.0 // Distance threshold for runback
)
// Hate system constants
const (
MinHateValue int32 = 1 // Minimum hate value (0 or negative is invalid)
MaxHateValue int32 = 2147483647 // Maximum hate value (INT_MAX)
DefaultHateValue int32 = 100 // Default hate amount
MaxHateListSize int = 100 // Maximum entities in hate list
MinHateValue int32 = 1 // Minimum hate value (0 or negative is invalid)
MaxHateValue int32 = 2147483647 // Maximum hate value (INT_MAX)
DefaultHateValue int32 = 100 // Default hate amount
MaxHateListSize int = 100 // Maximum entities in hate list
)
// Encounter system constants
const (
MaxEncounterSize int = 50 // Maximum entities in encounter list
MaxEncounterSize int = 50 // Maximum entities in encounter list
)
// Spell recovery constants
const (
SpellRecoveryBuffer int32 = 2000 // Additional recovery time buffer (2 seconds)
SpellRecoveryBuffer int32 = 2000 // Additional recovery time buffer (2 seconds)
)
// Brain type constants for identification
@ -46,9 +46,9 @@ const (
// Pet movement constants
const (
PetMovementFollow int8 = 0
PetMovementStay int8 = 1
PetMovementGuard int8 = 2
PetMovementFollow int8 = 0
PetMovementStay int8 = 1
PetMovementGuard int8 = 2
)
// Encounter state constants
@ -60,31 +60,31 @@ const (
// Combat decision constants
const (
MeleeAttackChance int = 70 // Base chance for melee attack
SpellCastChance int = 30 // Base chance for spell casting
BuffCheckChance int = 50 // Chance to check for buffs
MeleeAttackChance int = 70 // Base chance for melee attack
SpellCastChance int = 30 // Base chance for spell casting
BuffCheckChance int = 50 // Chance to check for buffs
)
// AI state flags
const (
AIStateIdle int32 = 0
AIStateCombat int32 = 1
AIStateFollowing int32 = 2
AIStateRunback int32 = 3
AIStateCasting int32 = 4
AIStateMoving int32 = 5
AIStateIdle int32 = 0
AIStateCombat int32 = 1
AIStateFollowing int32 = 2
AIStateRunback int32 = 3
AIStateCasting int32 = 4
AIStateMoving int32 = 5
)
// Debug levels
const (
DebugLevelNone int8 = 0
DebugLevelBasic int8 = 1
DebugLevelDetailed int8 = 2
DebugLevelVerbose int8 = 3
DebugLevelNone int8 = 0
DebugLevelBasic int8 = 1
DebugLevelDetailed int8 = 2
DebugLevelVerbose int8 = 3
)
// Timer constants
const (
MillisecondsPerSecond int32 = 1000
RecoveryTimeMultiple int32 = 10 // Multiply cast/recovery times by 10
)
RecoveryTimeMultiple int32 = 10 // Multiply cast/recovery times by 10
)

View File

@ -1,6 +1,9 @@
package ai
import "fmt"
import (
"fmt"
"time"
)
// Logger interface for AI logging
type Logger interface {
@ -19,17 +22,17 @@ type NPC interface {
GetTotalHP() int32
SetHP(int32)
IsAlive() bool
// Combat state
GetInCombat() bool
InCombat(bool)
GetTarget() Entity
SetTarget(Entity)
// Pet functionality
IsPet() bool
GetOwner() Entity
// Movement and positioning
GetX() float32
GetY() float32
@ -42,7 +45,7 @@ type NPC interface {
SetFollowTarget(Spawn, float32)
CalculateRunningLocation(bool)
ClearRunningLocations()
// Runback functionality
IsRunningBack() bool
GetRunbackLocation() *MovementLocation
@ -50,7 +53,7 @@ type NPC interface {
Runback(float32)
ShouldCallRunback() bool
SetCallRunback(bool)
// Status effects
IsMezzedOrStunned() bool
IsCasting() bool
@ -60,7 +63,7 @@ type NPC interface {
InWater() bool
IsWaterCreature() bool
IsFlyingCreature() bool
// Combat mechanics
AttackAllowed(Entity) bool
PrimaryWeaponReady() bool
@ -68,23 +71,23 @@ type NPC interface {
SetPrimaryLastAttackTime(int64)
SetSecondaryLastAttackTime(int64)
MeleeAttack(Entity, float32, bool)
// Spell casting
GetCastPercentage() int8
GetNextSpell(Entity, float32) Spell
GetNextBuffSpell(Spawn) Spell
SetCastOnAggroCompleted(bool)
CheckLoS(Entity) bool
// Movement pausing
IsPauseMovementTimerActive() bool
// Encounter state
SetEncounterState(int8)
// Scripts
GetSpawnScript() string
// Utility
KillSpawn(NPC)
}
@ -146,11 +149,11 @@ type Zone interface {
// AIManager provides high-level management of the AI system
type AIManager struct {
brains map[int32]Brain // Map of NPC ID to brain
activeCount int64 // Number of active brains
totalThinks int64 // Total think cycles processed
logger Logger // Logger for AI events
luaInterface LuaInterface // Lua script interface
brains map[int32]Brain // Map of NPC ID to brain
activeCount int64 // Number of active brains
totalThinks int64 // Total think cycles processed
logger Logger // Logger for AI events
luaInterface LuaInterface // Lua script interface
}
// NewAIManager creates a new AI manager
@ -169,20 +172,20 @@ func (am *AIManager) AddBrain(npcID int32, brain Brain) error {
if brain == nil {
return fmt.Errorf("brain cannot be nil")
}
if _, exists := am.brains[npcID]; exists {
return fmt.Errorf("brain already exists for NPC %d", npcID)
}
am.brains[npcID] = brain
if brain.IsActive() {
am.activeCount++
}
if am.logger != nil {
am.logger.LogDebug("Added brain for NPC %d (type: %d)", npcID, brain.GetBrainType())
}
return nil
}
@ -193,7 +196,7 @@ func (am *AIManager) RemoveBrain(npcID int32) {
am.activeCount--
}
delete(am.brains, npcID)
if am.logger != nil {
am.logger.LogDebug("Removed brain for NPC %d", npcID)
}
@ -210,24 +213,24 @@ func (am *AIManager) CreateBrainForNPC(npc NPC, brainType int8, options ...inter
if npc == nil {
return fmt.Errorf("NPC cannot be nil")
}
npcID := npc.GetID()
// Create brain based on type
var brain Brain
switch brainType {
case BrainTypeCombatPet:
brain = NewCombatPetBrain(npc, am.logger)
case BrainTypeNonCombatPet:
brain = NewNonCombatPetBrain(npc, am.logger)
case BrainTypeBlank:
brain = NewBlankBrain(npc, am.logger)
case BrainTypeLua:
brain = NewLuaBrain(npc, am.logger, am.luaInterface)
case BrainTypeDumbFire:
if len(options) >= 2 {
if target, ok := options[0].(Entity); ok {
@ -239,27 +242,27 @@ func (am *AIManager) CreateBrainForNPC(npc NPC, brainType int8, options ...inter
if brain == nil {
return fmt.Errorf("invalid options for dumbfire brain")
}
default:
brain = NewBaseBrain(npc, am.logger)
}
return am.AddBrain(npcID, brain)
}
// ProcessAllBrains runs think cycles for all active brains
func (am *AIManager) ProcessAllBrains() {
currentTime := currentTimeMillis()
for npcID, brain := range am.brains {
if !brain.IsActive() {
continue
}
// Check if it's time to think
lastThink := brain.GetLastThink()
thinkTick := brain.GetThinkTick()
if currentTime-lastThink >= int64(thinkTick) {
if err := brain.Think(); err != nil {
if am.logger != nil {
@ -276,7 +279,7 @@ func (am *AIManager) SetBrainActive(npcID int32, active bool) {
if brain := am.brains[npcID]; brain != nil {
wasActive := brain.IsActive()
brain.SetActive(active)
// Update active count
if wasActive && !active {
am.activeCount--
@ -316,7 +319,7 @@ func (am *AIManager) GetBrainsByType(brainType int8) []Brain {
func (am *AIManager) ClearAllBrains() {
am.brains = make(map[int32]Brain)
am.activeCount = 0
if am.logger != nil {
am.logger.LogInfo("Cleared all AI brains")
}
@ -325,31 +328,31 @@ func (am *AIManager) ClearAllBrains() {
// GetStatistics returns overall AI system statistics
func (am *AIManager) GetStatistics() *AIStatistics {
return &AIStatistics{
TotalBrains: len(am.brains),
ActiveBrains: int(am.activeCount),
TotalThinks: am.totalThinks,
BrainsByType: am.getBrainCountsByType(),
TotalBrains: len(am.brains),
ActiveBrains: int(am.activeCount),
TotalThinks: am.totalThinks,
BrainsByType: am.getBrainCountsByType(),
}
}
// getBrainCountsByType returns counts of brains by type
func (am *AIManager) getBrainCountsByType() map[string]int {
counts := make(map[string]int)
for _, brain := range am.brains {
typeName := getBrainTypeName(brain.GetBrainType())
counts[typeName]++
}
return counts
}
// AIStatistics contains AI system statistics
type AIStatistics struct {
TotalBrains int `json:"total_brains"`
ActiveBrains int `json:"active_brains"`
TotalThinks int64 `json:"total_thinks"`
BrainsByType map[string]int `json:"brains_by_type"`
TotalBrains int `json:"total_brains"`
ActiveBrains int `json:"active_brains"`
TotalThinks int64 `json:"total_thinks"`
BrainsByType map[string]int `json:"brains_by_type"`
}
// AIBrainAdapter provides NPC functionality for brains
@ -376,11 +379,11 @@ func (aba *AIBrainAdapter) ProcessAI(brain Brain) error {
if brain == nil {
return fmt.Errorf("brain is nil")
}
if !brain.IsActive() {
return nil
}
return brain.Think()
}
@ -441,10 +444,10 @@ func (hld *HateListDebugger) PrintHateList(npcName string, hateList map[int32]*H
if hld.logger == nil {
return
}
hld.logger.LogInfo("%s's Hate List", npcName)
hld.logger.LogInfo("-------------------")
if len(hateList) == 0 {
hld.logger.LogInfo("(empty)")
} else {
@ -452,7 +455,7 @@ func (hld *HateListDebugger) PrintHateList(npcName string, hateList map[int32]*H
hld.logger.LogInfo("Entity %d: %d hate", entityID, entry.HateValue)
}
}
hld.logger.LogInfo("-------------------")
}
@ -461,10 +464,10 @@ func (hld *HateListDebugger) PrintEncounterList(npcName string, encounterList ma
if hld.logger == nil {
return
}
hld.logger.LogInfo("%s's Encounter List", npcName)
hld.logger.LogInfo("-------------------")
if len(encounterList) == 0 {
hld.logger.LogInfo("(empty)")
} else {
@ -478,6 +481,6 @@ func (hld *HateListDebugger) PrintEncounterList(npcName string, encounterList ma
hld.logger.LogInfo("Entity %d (%s)", entityID, entryType)
}
}
hld.logger.LogInfo("-------------------")
}
}

View File

@ -7,9 +7,9 @@ import (
// HateEntry represents a single hate entry in the hate list
type HateEntry struct {
EntityID int32 // ID of the hated entity
HateValue int32 // Amount of hate (must be >= 1)
LastUpdated int64 // Timestamp of last hate update
EntityID int32 // ID of the hated entity
HateValue int32 // Amount of hate (must be >= 1)
LastUpdated int64 // Timestamp of last hate update
}
// NewHateEntry creates a new hate entry
@ -17,7 +17,7 @@ func NewHateEntry(entityID, hateValue int32) *HateEntry {
if hateValue < MinHateValue {
hateValue = MinHateValue
}
return &HateEntry{
EntityID: entityID,
HateValue: hateValue,
@ -27,8 +27,8 @@ func NewHateEntry(entityID, hateValue int32) *HateEntry {
// HateList manages the hate list for an NPC brain
type HateList struct {
entries map[int32]*HateEntry // Map of entity ID to hate entry
mutex sync.RWMutex // Thread safety
entries map[int32]*HateEntry // Map of entity ID to hate entry
mutex sync.RWMutex // Thread safety
}
// NewHateList creates a new hate list
@ -42,12 +42,12 @@ func NewHateList() *HateList {
func (hl *HateList) AddHate(entityID, hateValue int32) {
hl.mutex.Lock()
defer hl.mutex.Unlock()
if len(hl.entries) >= MaxHateListSize {
// Remove oldest entry if at capacity
hl.removeOldestEntry()
}
if entry, exists := hl.entries[entityID]; exists {
// Update existing entry
entry.HateValue += hateValue
@ -68,7 +68,7 @@ func (hl *HateList) AddHate(entityID, hateValue int32) {
func (hl *HateList) GetHate(entityID int32) int32 {
hl.mutex.RLock()
defer hl.mutex.RUnlock()
if entry, exists := hl.entries[entityID]; exists {
return entry.HateValue
}
@ -79,7 +79,7 @@ func (hl *HateList) GetHate(entityID int32) int32 {
func (hl *HateList) RemoveHate(entityID int32) {
hl.mutex.Lock()
defer hl.mutex.Unlock()
delete(hl.entries, entityID)
}
@ -87,7 +87,7 @@ func (hl *HateList) RemoveHate(entityID int32) {
func (hl *HateList) Clear() {
hl.mutex.Lock()
defer hl.mutex.Unlock()
hl.entries = make(map[int32]*HateEntry)
}
@ -95,17 +95,17 @@ func (hl *HateList) Clear() {
func (hl *HateList) GetMostHated() int32 {
hl.mutex.RLock()
defer hl.mutex.RUnlock()
var mostHated int32 = 0
var highestHate int32 = 0
for entityID, entry := range hl.entries {
if entry.HateValue > highestHate {
highestHate = entry.HateValue
mostHated = entityID
}
}
return mostHated
}
@ -113,22 +113,22 @@ func (hl *HateList) GetMostHated() int32 {
func (hl *HateList) GetHatePercentage(entityID int32) int8 {
hl.mutex.RLock()
defer hl.mutex.RUnlock()
entry, exists := hl.entries[entityID]
if !exists || entry.HateValue <= 0 {
return 0
}
// Calculate total hate
var totalHate int32 = 0
for _, e := range hl.entries {
totalHate += e.HateValue
}
if totalHate <= 0 {
return 0
}
percentage := float32(entry.HateValue) / float32(totalHate) * 100.0
return int8(percentage)
}
@ -137,7 +137,7 @@ func (hl *HateList) GetHatePercentage(entityID int32) int8 {
func (hl *HateList) GetAllEntries() map[int32]*HateEntry {
hl.mutex.RLock()
defer hl.mutex.RUnlock()
result := make(map[int32]*HateEntry)
for id, entry := range hl.entries {
result[id] = &HateEntry{
@ -153,7 +153,7 @@ func (hl *HateList) GetAllEntries() map[int32]*HateEntry {
func (hl *HateList) Size() int {
hl.mutex.RLock()
defer hl.mutex.RUnlock()
return len(hl.entries)
}
@ -162,17 +162,17 @@ func (hl *HateList) removeOldestEntry() {
if len(hl.entries) == 0 {
return
}
var oldestID int32
var oldestTime int64 = time.Now().UnixMilli()
for id, entry := range hl.entries {
if entry.LastUpdated < oldestTime {
oldestTime = entry.LastUpdated
oldestID = id
}
}
if oldestID != 0 {
delete(hl.entries, oldestID)
}
@ -180,11 +180,11 @@ func (hl *HateList) removeOldestEntry() {
// EncounterEntry represents a single encounter participant
type EncounterEntry struct {
EntityID int32 // ID of the entity
CharacterID int32 // Character ID for players (0 for NPCs)
AddedTime int64 // When entity was added to encounter
IsPlayer bool // Whether this is a player entity
IsBot bool // Whether this is a bot entity
EntityID int32 // ID of the entity
CharacterID int32 // Character ID for players (0 for NPCs)
AddedTime int64 // When entity was added to encounter
IsPlayer bool // Whether this is a player entity
IsBot bool // Whether this is a bot entity
}
// NewEncounterEntry creates a new encounter entry
@ -200,10 +200,10 @@ func NewEncounterEntry(entityID, characterID int32, isPlayer, isBot bool) *Encou
// EncounterList manages the encounter list for an NPC brain
type EncounterList struct {
entries map[int32]*EncounterEntry // Map of entity ID to encounter entry
playerEntries map[int32]int32 // Map of character ID to entity ID
playerInEncounter bool // Whether any player is in encounter
mutex sync.RWMutex // Thread safety
entries map[int32]*EncounterEntry // Map of entity ID to encounter entry
playerEntries map[int32]int32 // Map of character ID to entity ID
playerInEncounter bool // Whether any player is in encounter
mutex sync.RWMutex // Thread safety
}
// NewEncounterList creates a new encounter list
@ -219,26 +219,26 @@ func NewEncounterList() *EncounterList {
func (el *EncounterList) AddEntity(entityID, characterID int32, isPlayer, isBot bool) bool {
el.mutex.Lock()
defer el.mutex.Unlock()
if len(el.entries) >= MaxEncounterSize {
return false
}
// Check if already in encounter
if _, exists := el.entries[entityID]; exists {
return false
}
// Add entry
entry := NewEncounterEntry(entityID, characterID, isPlayer, isBot)
el.entries[entityID] = entry
// Track player entries separately
if isPlayer && characterID > 0 {
el.playerEntries[characterID] = entityID
el.playerInEncounter = true
}
return true
}
@ -246,16 +246,16 @@ func (el *EncounterList) AddEntity(entityID, characterID int32, isPlayer, isBot
func (el *EncounterList) RemoveEntity(entityID int32) {
el.mutex.Lock()
defer el.mutex.Unlock()
if entry, exists := el.entries[entityID]; exists {
// Remove from player entries if it's a player
if entry.IsPlayer && entry.CharacterID > 0 {
delete(el.playerEntries, entry.CharacterID)
}
// Remove main entry
delete(el.entries, entityID)
// Update player in encounter flag
el.updatePlayerInEncounter()
}
@ -265,7 +265,7 @@ func (el *EncounterList) RemoveEntity(entityID int32) {
func (el *EncounterList) Clear() {
el.mutex.Lock()
defer el.mutex.Unlock()
el.entries = make(map[int32]*EncounterEntry)
el.playerEntries = make(map[int32]int32)
el.playerInEncounter = false
@ -275,7 +275,7 @@ func (el *EncounterList) Clear() {
func (el *EncounterList) IsEntityInEncounter(entityID int32) bool {
el.mutex.RLock()
defer el.mutex.RUnlock()
_, exists := el.entries[entityID]
return exists
}
@ -284,7 +284,7 @@ func (el *EncounterList) IsEntityInEncounter(entityID int32) bool {
func (el *EncounterList) IsPlayerInEncounter(characterID int32) bool {
el.mutex.RLock()
defer el.mutex.RUnlock()
_, exists := el.playerEntries[characterID]
return exists
}
@ -293,7 +293,7 @@ func (el *EncounterList) IsPlayerInEncounter(characterID int32) bool {
func (el *EncounterList) HasPlayerInEncounter() bool {
el.mutex.RLock()
defer el.mutex.RUnlock()
return el.playerInEncounter
}
@ -301,7 +301,7 @@ func (el *EncounterList) HasPlayerInEncounter() bool {
func (el *EncounterList) Size() int {
el.mutex.RLock()
defer el.mutex.RUnlock()
return len(el.entries)
}
@ -309,7 +309,7 @@ func (el *EncounterList) Size() int {
func (el *EncounterList) CountPlayerBots() int {
el.mutex.RLock()
defer el.mutex.RUnlock()
count := 0
for _, entry := range el.entries {
if entry.IsPlayer || entry.IsBot {
@ -323,7 +323,7 @@ func (el *EncounterList) CountPlayerBots() int {
func (el *EncounterList) GetAllEntityIDs() []int32 {
el.mutex.RLock()
defer el.mutex.RUnlock()
result := make([]int32, 0, len(el.entries))
for entityID := range el.entries {
result = append(result, entityID)
@ -335,7 +335,7 @@ func (el *EncounterList) GetAllEntityIDs() []int32 {
func (el *EncounterList) GetAllEntries() map[int32]*EncounterEntry {
el.mutex.RLock()
defer el.mutex.RUnlock()
result := make(map[int32]*EncounterEntry)
for id, entry := range el.entries {
result[id] = &EncounterEntry{
@ -356,13 +356,13 @@ func (el *EncounterList) updatePlayerInEncounter() {
// BrainState represents the current state of a brain
type BrainState struct {
State int32 // Current AI state
LastThink int64 // Timestamp of last think cycle
ThinkTick int32 // Time between think cycles in milliseconds
SpellRecovery int64 // Timestamp when spell recovery completes
IsActive bool // Whether the brain is active
DebugLevel int8 // Debug output level
mutex sync.RWMutex
State int32 // Current AI state
LastThink int64 // Timestamp of last think cycle
ThinkTick int32 // Time between think cycles in milliseconds
SpellRecovery int64 // Timestamp when spell recovery completes
IsActive bool // Whether the brain is active
DebugLevel int8 // Debug output level
mutex sync.RWMutex
}
// NewBrainState creates a new brain state
@ -416,13 +416,13 @@ func (bs *BrainState) GetThinkTick() int32 {
func (bs *BrainState) SetThinkTick(tick int32) {
bs.mutex.Lock()
defer bs.mutex.Unlock()
if tick < 1 {
tick = 1
} else if tick > MaxThinkTick {
tick = MaxThinkTick
}
bs.ThinkTick = tick
}
@ -444,7 +444,7 @@ func (bs *BrainState) SetSpellRecovery(timestamp int64) {
func (bs *BrainState) HasRecovered() bool {
bs.mutex.RLock()
defer bs.mutex.RUnlock()
currentTime := time.Now().UnixMilli()
return bs.SpellRecovery <= currentTime
}
@ -479,26 +479,26 @@ func (bs *BrainState) SetDebugLevel(level int8) {
// BrainStatistics contains brain performance statistics
type BrainStatistics struct {
ThinkCycles int64 `json:"think_cycles"`
SpellsCast int64 `json:"spells_cast"`
MeleeAttacks int64 `json:"melee_attacks"`
HateEvents int64 `json:"hate_events"`
EncounterEvents int64 `json:"encounter_events"`
AverageThinkTime float64 `json:"average_think_time_ms"`
LastThinkTime int64 `json:"last_think_time"`
TotalActiveTime int64 `json:"total_active_time_ms"`
ThinkCycles int64 `json:"think_cycles"`
SpellsCast int64 `json:"spells_cast"`
MeleeAttacks int64 `json:"melee_attacks"`
HateEvents int64 `json:"hate_events"`
EncounterEvents int64 `json:"encounter_events"`
AverageThinkTime float64 `json:"average_think_time_ms"`
LastThinkTime int64 `json:"last_think_time"`
TotalActiveTime int64 `json:"total_active_time_ms"`
}
// NewBrainStatistics creates new brain statistics
func NewBrainStatistics() *BrainStatistics {
return &BrainStatistics{
ThinkCycles: 0,
SpellsCast: 0,
MeleeAttacks: 0,
HateEvents: 0,
EncounterEvents: 0,
AverageThinkTime: 0.0,
LastThinkTime: time.Now().UnixMilli(),
TotalActiveTime: 0,
ThinkCycles: 0,
SpellsCast: 0,
MeleeAttacks: 0,
HateEvents: 0,
EncounterEvents: 0,
AverageThinkTime: 0.0,
LastThinkTime: time.Now().UnixMilli(),
TotalActiveTime: 0,
}
}
}

View File

@ -25,16 +25,16 @@ func (cpb *CombatPetBrain) Think() error {
if err := cpb.BaseBrain.Think(); err != nil {
return err
}
// Additional pet-specific logic
if cpb.npc.GetInCombat() || !cpb.npc.IsPet() || cpb.npc.IsMezzedOrStunned() {
return nil
}
if cpb.logger != nil && cpb.state.GetDebugLevel() >= DebugLevelDetailed {
cpb.logger.LogDebug("Combat pet AI for %s", cpb.npc.GetName())
}
// Check if owner has stay command set
owner := cpb.npc.GetOwner()
if owner != nil && owner.IsPlayer() {
@ -43,18 +43,18 @@ func (cpb *CombatPetBrain) Think() error {
// return nil
// }
}
// Follow owner
if owner != nil {
cpb.npc.SetTarget(owner)
distance := cpb.npc.GetDistance(owner)
maxRange := cpb.getMaxCombatRange()
if distance > maxRange {
cpb.MoveCloser(owner)
}
}
return nil
}
@ -78,23 +78,23 @@ func (ncpb *NonCombatPetBrain) Think() error {
if !ncpb.npc.IsPet() || ncpb.npc.IsMezzedOrStunned() {
return nil
}
if ncpb.logger != nil && ncpb.state.GetDebugLevel() >= DebugLevelDetailed {
ncpb.logger.LogDebug("Non-combat pet AI for %s", ncpb.npc.GetName())
}
// Just follow owner
owner := ncpb.npc.GetOwner()
if owner != nil {
ncpb.npc.SetTarget(owner)
distance := ncpb.npc.GetDistance(owner)
maxRange := ncpb.getMaxCombatRange()
if distance > maxRange {
ncpb.MoveCloser(owner)
}
}
return nil
}
@ -140,11 +140,11 @@ func (lb *LuaBrain) Think() error {
if lb.scriptInterface == nil {
return fmt.Errorf("no Lua interface available")
}
if lb.npc == nil {
return fmt.Errorf("brain has no body")
}
script := lb.npc.GetSpawnScript()
if script == "" {
if lb.logger != nil {
@ -152,7 +152,7 @@ func (lb *LuaBrain) Think() error {
}
return fmt.Errorf("no spawn script available")
}
// Call the Lua Think function
target := lb.npc.GetTarget()
err := lb.scriptInterface.RunSpawnScript(script, "Think", lb.npc, target)
@ -162,7 +162,7 @@ func (lb *LuaBrain) Think() error {
}
return fmt.Errorf("Lua Think function failed: %w", err)
}
return nil
}
@ -179,12 +179,12 @@ func NewDumbFirePetBrain(npc NPC, target Entity, expireTimeMS int32, logger Logg
expireTime: time.Now().UnixMilli() + int64(expireTimeMS),
}
brain.brainType = BrainTypeDumbFire
// Add maximum hate for the target
if target != nil {
brain.AddHate(target.GetID(), MaxHateValue)
}
return brain
}
@ -208,7 +208,7 @@ func (dfpb *DumbFirePetBrain) Think() error {
}
return nil
}
// Get target
targetID := dfpb.GetMostHated()
if targetID == 0 {
@ -221,7 +221,7 @@ func (dfpb *DumbFirePetBrain) Think() error {
}
return nil
}
target := dfpb.getEntityByID(targetID)
if target == nil {
// Target no longer exists, kill self
@ -230,39 +230,39 @@ func (dfpb *DumbFirePetBrain) Think() error {
}
return nil
}
// Skip if mezzed or stunned
if dfpb.npc.IsMezzedOrStunned() {
return nil
}
// Set target if not already set
if dfpb.npc.GetTarget() != target {
dfpb.npc.SetTarget(target)
dfpb.npc.FaceTarget(target, false)
}
// Enter combat if not already
if !dfpb.npc.GetInCombat() {
dfpb.npc.CalculateRunningLocation(true)
dfpb.npc.InCombat(true)
}
distance := dfpb.npc.GetDistance(target)
// Try to cast spells if we have line of sight
if dfpb.npc.CheckLoS(target) && !dfpb.npc.IsCasting() &&
if dfpb.npc.CheckLoS(target) && !dfpb.npc.IsCasting() &&
(!dfpb.HasRecovered() || !dfpb.ProcessSpell(target, distance)) {
if dfpb.logger != nil && dfpb.state.GetDebugLevel() >= DebugLevelDetailed {
dfpb.logger.LogDebug("Dumbfire pet %s attempting melee on %s",
dfpb.logger.LogDebug("Dumbfire pet %s attempting melee on %s",
dfpb.npc.GetName(), target.GetName())
}
dfpb.npc.FaceTarget(target, false)
dfpb.ProcessMelee(target, distance)
}
return nil
}
@ -293,13 +293,13 @@ func CreateBrain(npc NPC, brainType int8, logger Logger, options ...interface{})
switch brainType {
case BrainTypeCombatPet:
return NewCombatPetBrain(npc, logger)
case BrainTypeNonCombatPet:
return NewNonCombatPetBrain(npc, logger)
case BrainTypeBlank:
return NewBlankBrain(npc, logger)
case BrainTypeLua:
if len(options) > 0 {
if luaInterface, ok := options[0].(LuaInterface); ok {
@ -307,7 +307,7 @@ func CreateBrain(npc NPC, brainType int8, logger Logger, options ...interface{})
}
}
return NewBaseBrain(npc, logger) // Fallback to default
case BrainTypeDumbFire:
if len(options) >= 2 {
if target, ok := options[0].(Entity); ok {
@ -317,8 +317,8 @@ func CreateBrain(npc NPC, brainType int8, logger Logger, options ...interface{})
}
}
return NewBaseBrain(npc, logger) // Fallback to default
default:
return NewBaseBrain(npc, logger)
}
}
}

View File

@ -2,9 +2,9 @@ package npc
// AI Strategy constants
const (
AIStrategyBalanced int8 = 1
AIStrategyOffensive int8 = 2
AIStrategyDefensive int8 = 3
AIStrategyBalanced int8 = 1
AIStrategyOffensive int8 = 2
AIStrategyDefensive int8 = 3
)
// Randomize Appearances constants
@ -47,26 +47,26 @@ const (
// Cast Type constants
const (
CastOnSpawn int8 = 0
CastOnAggro int8 = 1
CastOnSpawn int8 = 0
CastOnAggro int8 = 1
MaxCastTypes int8 = 2
)
// Default values
const (
DefaultCastPercentage int8 = 25
DefaultAggroRadius float32 = 10.0
DefaultRunbackSpeed float32 = 2.0
MaxSkillBonuses int = 100
MaxNPCSpells int = 50
MaxPauseTime int32 = 300000 // 5 minutes max pause
DefaultCastPercentage int8 = 25
DefaultAggroRadius float32 = 10.0
DefaultRunbackSpeed float32 = 2.0
MaxSkillBonuses int = 100
MaxNPCSpells int = 50
MaxPauseTime int32 = 300000 // 5 minutes max pause
)
// NPC validation constants
const (
MinNPCLevel int8 = 1
MaxNPCLevel int8 = 100
MaxNPCNameLen int = 64
MinNPCLevel int8 = 1
MaxNPCLevel int8 = 100
MaxNPCNameLen int = 64
MinAppearanceID int32 = 0
MaxAppearanceID int32 = 999999
)
@ -90,4 +90,4 @@ const (
const (
DefaultPauseCheckMS int32 = 100
RunbackCheckMS int32 = 250
)
)

View File

@ -214,7 +214,7 @@ func (ea *EntityAdapter) ReceiveNPCCommand(otherNPC *NPC, command string) error
func (ea *EntityAdapter) handleAggroInteraction(otherNPC *NPC) error {
// TODO: Implement aggro logic between NPCs
if ea.logger != nil {
ea.logger.LogDebug("NPC %d received aggro from NPC %d",
ea.logger.LogDebug("NPC %d received aggro from NPC %d",
ea.npc.GetNPCID(), otherNPC.GetNPCID())
}
return nil
@ -224,7 +224,7 @@ func (ea *EntityAdapter) handleAggroInteraction(otherNPC *NPC) error {
func (ea *EntityAdapter) handleAssistInteraction(otherNPC *NPC) error {
// TODO: Implement assist logic between NPCs
if ea.logger != nil {
ea.logger.LogDebug("NPC %d received assist request from NPC %d",
ea.logger.LogDebug("NPC %d received assist request from NPC %d",
ea.npc.GetNPCID(), otherNPC.GetNPCID())
}
return nil
@ -234,7 +234,7 @@ func (ea *EntityAdapter) handleAssistInteraction(otherNPC *NPC) error {
func (ea *EntityAdapter) handleTradeInteraction(otherNPC *NPC) error {
// TODO: Implement trade logic between NPCs
if ea.logger != nil {
ea.logger.LogDebug("NPC %d received trade request from NPC %d",
ea.logger.LogDebug("NPC %d received trade request from NPC %d",
ea.npc.GetNPCID(), otherNPC.GetNPCID())
}
return nil
@ -244,7 +244,7 @@ func (ea *EntityAdapter) handleTradeInteraction(otherNPC *NPC) error {
func (ea *EntityAdapter) handleFollowCommand(otherNPC *NPC) error {
// TODO: Implement follow logic
if ea.logger != nil {
ea.logger.LogDebug("NPC %d received follow command from NPC %d",
ea.logger.LogDebug("NPC %d received follow command from NPC %d",
ea.npc.GetNPCID(), otherNPC.GetNPCID())
}
return nil
@ -254,7 +254,7 @@ func (ea *EntityAdapter) handleFollowCommand(otherNPC *NPC) error {
func (ea *EntityAdapter) handleAttackCommand(otherNPC *NPC) error {
// TODO: Implement attack logic
if ea.logger != nil {
ea.logger.LogDebug("NPC %d received attack command from NPC %d",
ea.logger.LogDebug("NPC %d received attack command from NPC %d",
ea.npc.GetNPCID(), otherNPC.GetNPCID())
}
return nil
@ -264,7 +264,7 @@ func (ea *EntityAdapter) handleAttackCommand(otherNPC *NPC) error {
func (ea *EntityAdapter) handleRetreatCommand(otherNPC *NPC) error {
// TODO: Implement retreat logic
if ea.logger != nil {
ea.logger.LogDebug("NPC %d received retreat command from NPC %d",
ea.logger.LogDebug("NPC %d received retreat command from NPC %d",
ea.npc.GetNPCID(), otherNPC.GetNPCID())
}
return nil
@ -363,7 +363,7 @@ func (sca *SpellCasterAdapter) CastSpell(target interface{}, spell Spell) error
}
if sca.logger != nil {
sca.logger.LogDebug("NPC %d cast spell %s (%d)",
sca.logger.LogDebug("NPC %d cast spell %s (%d)",
sca.npc.GetNPCID(), spell.GetName(), spell.GetSpellID())
}
@ -388,18 +388,18 @@ func (sca *SpellCasterAdapter) getNextCastOnAggroSpell(target interface{}) Spell
// getNextSpellByStrategy selects spells based on AI strategy
func (sca *SpellCasterAdapter) getNextSpellByStrategy(target interface{}, distance float32, strategy int8) Spell {
// TODO: Implement more sophisticated spell selection based on strategy
for _, npcSpell := range sca.npc.spells {
// Check HP ratio requirements
if npcSpell.GetRequiredHPRatio() != 0 {
// TODO: Implement HP ratio checking
}
spell := sca.spellManager.GetSpell(npcSpell.GetSpellID(), npcSpell.GetTier())
if spell == nil {
continue
}
// Check strategy compatibility
if strategy == AIStrategyOffensive && spell.IsFriendlySpell() {
continue
@ -407,14 +407,14 @@ func (sca *SpellCasterAdapter) getNextSpellByStrategy(target interface{}, distan
if strategy == AIStrategyDefensive && !spell.IsFriendlySpell() {
continue
}
// Check range and power requirements
if distance <= spell.GetRange() && distance >= spell.GetMinRange() {
// TODO: Check power requirements
return spell
}
}
return nil
}
@ -425,7 +425,7 @@ func (sca *SpellCasterAdapter) checkCastingConditions(spell Spell) error {
}
// TODO: Implement power checking, cooldown checking, etc.
return nil
}
@ -506,9 +506,9 @@ func (ca *CombatAdapter) ProcessCombat() error {
// MovementAdapter provides movement functionality for NPCs
type MovementAdapter struct {
npc *NPC
movementManager MovementManager
logger Logger
npc *NPC
movementManager MovementManager
logger Logger
}
// NewMovementAdapter creates a new movement adapter
@ -567,4 +567,4 @@ func (ma *MovementAdapter) RunbackToSpawn() error {
}
return ma.MoveToLocation(runbackLocation.X, runbackLocation.Y, runbackLocation.Z)
}
}

View File

@ -10,23 +10,23 @@ import (
// Manager provides high-level management of the NPC system
type Manager struct {
npcs map[int32]*NPC // NPCs indexed by ID
npcsByZone map[int32][]*NPC // NPCs indexed by zone ID
npcsByAppearance map[int32][]*NPC // NPCs indexed by appearance ID
database Database // Database interface
logger Logger // Logger interface
spellManager SpellManager // Spell system interface
skillManager SkillManager // Skill system interface
appearanceManager AppearanceManager // Appearance system interface
mutex sync.RWMutex // Thread safety
npcs map[int32]*NPC // NPCs indexed by ID
npcsByZone map[int32][]*NPC // NPCs indexed by zone ID
npcsByAppearance map[int32][]*NPC // NPCs indexed by appearance ID
database Database // Database interface
logger Logger // Logger interface
spellManager SpellManager // Spell system interface
skillManager SkillManager // Skill system interface
appearanceManager AppearanceManager // Appearance system interface
mutex sync.RWMutex // Thread safety
// Statistics
totalNPCs int64
npcsInCombat int64
spellCastCount int64
skillUsageCount int64
runbackCount int64
aiStrategyCounts map[int8]int64
totalNPCs int64
npcsInCombat int64
spellCastCount int64
skillUsageCount int64
runbackCount int64
aiStrategyCounts map[int8]int64
// Configuration
maxNPCs int32
@ -625,7 +625,7 @@ func (m *Manager) handleSearchCommand(args []string) (string, error) {
}
searchTerm := strings.ToLower(args[0])
m.mutex.RLock()
var results []*NPC
for _, npc := range m.npcs {
@ -668,10 +668,10 @@ func (m *Manager) removeFromZoneIndex(npc *NPC) {
if npc.Entity == nil {
return
}
zoneID := npc.Entity.GetZoneID()
npcs := m.npcsByZone[zoneID]
for i, n := range npcs {
if n == npc {
// Remove from slice
@ -679,7 +679,7 @@ func (m *Manager) removeFromZoneIndex(npc *NPC) {
break
}
}
// Clean up empty slices
if len(m.npcsByZone[zoneID]) == 0 {
delete(m.npcsByZone, zoneID)
@ -689,7 +689,7 @@ func (m *Manager) removeFromZoneIndex(npc *NPC) {
func (m *Manager) removeFromAppearanceIndex(npc *NPC) {
appearanceID := npc.GetAppearanceID()
npcs := m.npcsByAppearance[appearanceID]
for i, n := range npcs {
if n == npc {
// Remove from slice
@ -697,7 +697,7 @@ func (m *Manager) removeFromAppearanceIndex(npc *NPC) {
break
}
}
// Clean up empty slices
if len(m.npcsByAppearance[appearanceID]) == 0 {
delete(m.npcsByAppearance, appearanceID)
@ -708,7 +708,7 @@ func (m *Manager) removeFromAppearanceIndex(npc *NPC) {
func (m *Manager) SetManagers(spellMgr SpellManager, skillMgr SkillManager, appearanceMgr AppearanceManager) {
m.mutex.Lock()
defer m.mutex.Unlock()
m.spellManager = spellMgr
m.skillManager = skillMgr
m.appearanceManager = appearanceMgr
@ -753,10 +753,10 @@ func (m *Manager) Shutdown() {
brain.SetActive(false)
}
}
// Clear all data
m.npcs = make(map[int32]*NPC)
m.npcsByZone = make(map[int32][]*NPC)
m.npcsByAppearance = make(map[int32][]*NPC)
m.mutex.Unlock()
}
}

View File

@ -15,44 +15,44 @@ import (
// NewNPC creates a new NPC with default values
func NewNPC() *NPC {
npc := &NPC{
Entity: entity.NewEntity(),
appearanceID: 0,
npcID: 0,
aiStrategy: AIStrategyBalanced,
attackType: 0,
castPercentage: DefaultCastPercentage,
maxPetLevel: DefaultMaxPetLevel,
aggroRadius: DefaultAggroRadius,
baseAggroRadius: DefaultAggroRadius,
runback: nil,
runningBack: false,
runbackHeadingDir1: 0,
runbackHeadingDir2: 0,
pauseTimer: NewTimer(),
primarySpellList: 0,
secondarySpellList: 0,
primarySkillList: 0,
secondarySkillList: 0,
equipmentListID: 0,
skills: make(map[string]*Skill),
spells: make([]*NPCSpell, 0),
castOnSpells: make(map[int8][]*NPCSpell),
skillBonuses: make(map[int32]*SkillBonus),
hasSpells: false,
castOnAggroCompleted: false,
shardID: 0,
shardCharID: 0,
shardCreatedTimestamp: 0,
callRunback: false,
Entity: entity.NewEntity(),
appearanceID: 0,
npcID: 0,
aiStrategy: AIStrategyBalanced,
attackType: 0,
castPercentage: DefaultCastPercentage,
maxPetLevel: DefaultMaxPetLevel,
aggroRadius: DefaultAggroRadius,
baseAggroRadius: DefaultAggroRadius,
runback: nil,
runningBack: false,
runbackHeadingDir1: 0,
runbackHeadingDir2: 0,
pauseTimer: NewTimer(),
primarySpellList: 0,
secondarySpellList: 0,
primarySkillList: 0,
secondarySkillList: 0,
equipmentListID: 0,
skills: make(map[string]*Skill),
spells: make([]*NPCSpell, 0),
castOnSpells: make(map[int8][]*NPCSpell),
skillBonuses: make(map[int32]*SkillBonus),
hasSpells: false,
castOnAggroCompleted: false,
shardID: 0,
shardCharID: 0,
shardCreatedTimestamp: 0,
callRunback: false,
}
// Initialize cast-on spell arrays
npc.castOnSpells[CastOnSpawn] = make([]*NPCSpell, 0)
npc.castOnSpells[CastOnAggro] = make([]*NPCSpell, 0)
// Create default brain
npc.brain = NewDefaultBrain(npc)
return npc
}
@ -61,9 +61,9 @@ func NewNPCFromExisting(oldNPC *NPC) *NPC {
if oldNPC == nil {
return NewNPC()
}
npc := NewNPC()
// Copy basic properties
npc.npcID = oldNPC.npcID
npc.appearanceID = oldNPC.appearanceID
@ -73,19 +73,19 @@ func NewNPCFromExisting(oldNPC *NPC) *NPC {
npc.maxPetLevel = oldNPC.maxPetLevel
npc.baseAggroRadius = oldNPC.baseAggroRadius
npc.aggroRadius = oldNPC.baseAggroRadius
// Copy spell lists
npc.primarySpellList = oldNPC.primarySpellList
npc.secondarySpellList = oldNPC.secondarySpellList
npc.primarySkillList = oldNPC.primarySkillList
npc.secondarySkillList = oldNPC.secondarySkillList
npc.equipmentListID = oldNPC.equipmentListID
// Copy entity data (stats, appearance, etc.)
if oldNPC.Entity != nil {
npc.Entity = oldNPC.Entity.Copy().(*entity.Entity)
}
// Handle level randomization
if oldNPC.Entity != nil {
minLevel := oldNPC.Entity.GetMinLevel()
@ -95,18 +95,18 @@ func NewNPCFromExisting(oldNPC *NPC) *NPC {
npc.Entity.SetLevel(randomLevel)
}
}
// Copy skills (deep copy)
npc.copySkills(oldNPC)
// Copy spells (deep copy)
npc.copySpells(oldNPC)
// Handle appearance randomization
if oldNPC.Entity != nil && oldNPC.Entity.GetRandomize() > 0 {
npc.randomizeAppearance(oldNPC.Entity.GetRandomize())
}
return npc
}
@ -197,7 +197,7 @@ func (n *NPC) GetAggroRadius() float32 {
func (n *NPC) SetAggroRadius(radius float32, overrideBase bool) {
n.mutex.Lock()
defer n.mutex.Unlock()
if n.baseAggroRadius == 0.0 || overrideBase {
n.baseAggroRadius = radius
}
@ -297,7 +297,7 @@ func (n *NPC) HasSpells() bool {
func (n *NPC) GetSpells() []*NPCSpell {
n.mutex.RLock()
defer n.mutex.RUnlock()
result := make([]*NPCSpell, len(n.spells))
for i, spell := range n.spells {
result[i] = spell.Copy()
@ -309,30 +309,30 @@ func (n *NPC) GetSpells() []*NPCSpell {
func (n *NPC) SetSpells(spells []*NPCSpell) {
n.mutex.Lock()
defer n.mutex.Unlock()
// Clear existing cast-on spells
for i := int8(0); i < MaxCastTypes; i++ {
n.castOnSpells[i] = make([]*NPCSpell, 0)
}
// Clear existing spells
n.spells = make([]*NPCSpell, 0)
if spells == nil || len(spells) == 0 {
n.hasSpells = false
return
}
n.hasSpells = true
// Process spells and separate cast-on types
for _, spell := range spells {
if spell == nil {
continue
}
spellCopy := spell.Copy()
if spellCopy.GetCastOnSpawn() {
n.castOnSpells[CastOnSpawn] = append(n.castOnSpells[CastOnSpawn], spellCopy)
} else if spellCopy.GetCastOnInitialAggro() {
@ -347,17 +347,17 @@ func (n *NPC) SetSpells(spells []*NPCSpell) {
func (n *NPC) GetSkillByName(name string, checkUpdate bool) *Skill {
n.mutex.RLock()
defer n.mutex.RUnlock()
skill, exists := n.skills[name]
if !exists {
return nil
}
// Random skill increase (10% chance)
if checkUpdate && skill.GetCurrentVal() < skill.MaxVal && rand.Intn(100) >= 90 {
skill.IncreaseSkill()
}
return skill
}
@ -372,10 +372,10 @@ func (n *NPC) GetSkillByID(id int32, checkUpdate bool) *Skill {
func (n *NPC) SetSkills(skills map[string]*Skill) {
n.mutex.Lock()
defer n.mutex.Unlock()
// Clear existing skills
n.skills = make(map[string]*Skill)
// Copy skills
if skills != nil {
for name, skill := range skills {
@ -396,20 +396,20 @@ func (n *NPC) AddSkillBonus(spellID, skillID int32, value float32) {
if value == 0 {
return
}
n.mutex.Lock()
defer n.mutex.Unlock()
// Get or create skill bonus
skillBonus, exists := n.skillBonuses[spellID]
if !exists {
skillBonus = NewSkillBonus(spellID)
n.skillBonuses[spellID] = skillBonus
}
// Add the skill bonus
skillBonus.AddSkill(skillID, value)
// Apply bonus to existing skills
for _, skill := range n.skills {
if skill.SkillID == skillID {
@ -423,12 +423,12 @@ func (n *NPC) AddSkillBonus(spellID, skillID int32, value float32) {
func (n *NPC) RemoveSkillBonus(spellID int32) {
n.mutex.Lock()
defer n.mutex.Unlock()
skillBonus, exists := n.skillBonuses[spellID]
if !exists {
return
}
// Remove bonuses from skills
bonuses := skillBonus.GetSkills()
for _, bonus := range bonuses {
@ -439,7 +439,7 @@ func (n *NPC) RemoveSkillBonus(spellID int32) {
}
}
}
// Remove the skill bonus
delete(n.skillBonuses, spellID)
}
@ -448,7 +448,7 @@ func (n *NPC) RemoveSkillBonus(spellID int32) {
func (n *NPC) SetRunbackLocation(x, y, z float32, gridID int32, resetHP bool) {
n.mutex.Lock()
defer n.mutex.Unlock()
n.runback = &MovementLocation{
X: x,
Y: y,
@ -464,7 +464,7 @@ func (n *NPC) SetRunbackLocation(x, y, z float32, gridID int32, resetHP bool) {
func (n *NPC) GetRunbackLocation() *MovementLocation {
n.mutex.RLock()
defer n.mutex.RUnlock()
if n.runback == nil {
return nil
}
@ -474,23 +474,23 @@ func (n *NPC) GetRunbackLocation() *MovementLocation {
func (n *NPC) GetRunbackDistance() float32 {
n.mutex.RLock()
defer n.mutex.RUnlock()
if n.runback == nil || n.Entity == nil {
return 0
}
// Calculate distance using basic distance formula
dx := n.Entity.GetX() - n.runback.X
dy := n.Entity.GetY() - n.runback.Y
dz := n.Entity.GetZ() - n.runback.Z
return float32(math.Sqrt(float64(dx*dx + dy*dy + dz*dz)))
}
func (n *NPC) ClearRunback() {
n.mutex.Lock()
defer n.mutex.Unlock()
n.runback = nil
n.runningBack = false
n.runbackHeadingDir1 = 0
@ -502,14 +502,14 @@ func (n *NPC) StartRunback(resetHP bool) {
if n.GetRunbackLocation() != nil {
return
}
if n.Entity == nil {
return
}
n.mutex.Lock()
defer n.mutex.Unlock()
n.runback = &MovementLocation{
X: n.Entity.GetX(),
Y: n.Entity.GetY(),
@ -520,7 +520,7 @@ func (n *NPC) StartRunback(resetHP bool) {
UseNavPath: false,
Mapped: false,
}
// Store original heading
n.runbackHeadingDir1 = n.Entity.GetHeading()
n.runbackHeadingDir2 = n.Entity.GetHeading() // In C++ these are separate values
@ -531,18 +531,18 @@ func (n *NPC) Runback(distance float32, stopFollowing bool) {
if n.runback == nil {
return
}
if distance == 0.0 {
distance = n.GetRunbackDistance()
}
n.mutex.Lock()
n.runningBack = true
n.mutex.Unlock()
// TODO: Implement actual movement logic
// This would integrate with the movement system
if stopFollowing && n.Entity != nil {
n.Entity.SetFollowing(false)
}
@ -560,15 +560,15 @@ func (n *NPC) PauseMovement(periodMS int32) bool {
if periodMS < 1 {
periodMS = 1
}
if periodMS > MaxPauseTime {
periodMS = MaxPauseTime
}
// TODO: Integrate with movement system to stop movement
// For now, just start the pause timer
n.pauseTimer.Start(periodMS, true)
return true
}
@ -577,7 +577,7 @@ func (n *NPC) IsPauseMovementTimerActive() bool {
n.pauseTimer.Disable()
n.callRunback = true
}
return n.pauseTimer.Enabled()
}
@ -591,13 +591,13 @@ func (n *NPC) GetBrain() Brain {
func (n *NPC) SetBrain(brain Brain) {
n.brainMutex.Lock()
defer n.brainMutex.Unlock()
// Validate brain matches this NPC
if brain != nil && brain.GetBody() != n {
// TODO: Log error
return
}
n.brain = brain
}
@ -643,15 +643,15 @@ func (n *NPC) HandleUse(client Client, commandType string) bool {
if client == nil || len(commandType) == 0 {
return false
}
// Check if NPC shows command icons
if n.Entity == nil {
return false
}
// TODO: Implement entity command processing
// This would integrate with the command system
return false
}
@ -660,31 +660,31 @@ func (n *NPC) InCombat(val bool) {
if n.Entity == nil {
return
}
currentCombat := n.Entity.GetInCombat()
if currentCombat == val {
return
}
n.Entity.SetInCombat(val)
if val {
// Entering combat
if n.GetRunbackLocation() == nil {
n.StartRunback(true)
}
// Set max speed for combat
if n.Entity.GetMaxSpeed() > 0 {
n.Entity.SetSpeed(n.Entity.GetMaxSpeed())
}
// TODO: Add combat icon, call spawn scripts, etc.
} else {
// Leaving combat
// TODO: Remove combat icon, call combat reset scripts, etc.
if n.Entity.GetHP() > 0 {
// TODO: Re-enable action states, stop heroic opportunities
}
@ -702,7 +702,7 @@ func (n *NPC) copySkills(oldNPC *NPC) {
if oldNPC == nil {
return
}
oldNPC.mutex.RLock()
oldSkills := make(map[string]*Skill)
for name, skill := range oldNPC.skills {
@ -716,7 +716,7 @@ func (n *NPC) copySkills(oldNPC *NPC) {
}
}
oldNPC.mutex.RUnlock()
n.SetSkills(oldSkills)
}
@ -724,7 +724,7 @@ func (n *NPC) copySpells(oldNPC *NPC) {
if oldNPC == nil {
return
}
oldNPC.mutex.RLock()
oldSpells := make([]*NPCSpell, len(oldNPC.spells))
for i, spell := range oldNPC.spells {
@ -732,7 +732,7 @@ func (n *NPC) copySpells(oldNPC *NPC) {
oldSpells[i] = spell.Copy()
}
}
// Also copy cast-on spells
for castType, spells := range oldNPC.castOnSpells {
for _, spell := range spells {
@ -742,7 +742,7 @@ func (n *NPC) copySpells(oldNPC *NPC) {
}
}
oldNPC.mutex.RUnlock()
n.SetSpells(oldSpells)
}
@ -750,30 +750,30 @@ func (n *NPC) copySpells(oldNPC *NPC) {
func (n *NPC) randomizeAppearance(flags int32) {
// TODO: Implement full appearance randomization
// This is a complex system that would integrate with the appearance system
// For now, just implement basic randomization
if n.Entity == nil {
return
}
// Random gender
if flags&RandomizeGender != 0 {
gender := int8(rand.Intn(2) + 1) // 1 or 2
n.Entity.SetGender(gender)
}
// Random race (simplified)
if flags&RandomizeRace != 0 {
// TODO: Implement race randomization based on alignment
race := int16(rand.Intn(21)) // 0-20 for basic races
n.Entity.SetRace(race)
}
// Color randomization
if flags&RandomizeSkinColor != 0 {
// TODO: Implement skin color randomization
}
// More randomization options would be implemented here
}
@ -782,16 +782,16 @@ func (n *NPC) IsValid() bool {
if n.Entity == nil {
return false
}
// Basic validation
if n.Entity.GetLevel() < MinNPCLevel || n.Entity.GetLevel() > MaxNPCLevel {
return false
}
if n.appearanceID < MinAppearanceID || n.appearanceID > MaxAppearanceID {
return false
}
return true
}
@ -800,7 +800,7 @@ func (n *NPC) String() string {
if n.Entity == nil {
return fmt.Sprintf("NPC{ID: %d, AppearanceID: %d, Entity: nil}", n.npcID, n.appearanceID)
}
return fmt.Sprintf("NPC{ID: %d, Name: %s, Level: %d, AppearanceID: %d}",
return fmt.Sprintf("NPC{ID: %d, Name: %s, Level: %d, AppearanceID: %d}",
n.npcID, n.Entity.GetName(), n.Entity.GetLevel(), n.appearanceID)
}
}

View File

@ -11,24 +11,24 @@ import (
// NPCSpell represents a spell configuration for NPCs
type NPCSpell struct {
ListID int32 // Spell list identifier
SpellID int32 // Spell ID from master spell list
Tier int8 // Spell tier
CastOnSpawn bool // Cast when NPC spawns
CastOnInitialAggro bool // Cast when first entering combat
RequiredHPRatio int8 // HP ratio requirement for casting (-100 to 100)
mutex sync.RWMutex
ListID int32 // Spell list identifier
SpellID int32 // Spell ID from master spell list
Tier int8 // Spell tier
CastOnSpawn bool // Cast when NPC spawns
CastOnInitialAggro bool // Cast when first entering combat
RequiredHPRatio int8 // HP ratio requirement for casting (-100 to 100)
mutex sync.RWMutex
}
// NewNPCSpell creates a new NPCSpell
func NewNPCSpell() *NPCSpell {
return &NPCSpell{
ListID: 0,
SpellID: 0,
Tier: 1,
CastOnSpawn: false,
CastOnInitialAggro: false,
RequiredHPRatio: 0,
ListID: 0,
SpellID: 0,
Tier: 1,
CastOnSpawn: false,
CastOnInitialAggro: false,
RequiredHPRatio: 0,
}
}
@ -36,14 +36,14 @@ func NewNPCSpell() *NPCSpell {
func (ns *NPCSpell) Copy() *NPCSpell {
ns.mutex.RLock()
defer ns.mutex.RUnlock()
return &NPCSpell{
ListID: ns.ListID,
SpellID: ns.SpellID,
Tier: ns.Tier,
CastOnSpawn: ns.CastOnSpawn,
CastOnInitialAggro: ns.CastOnInitialAggro,
RequiredHPRatio: ns.RequiredHPRatio,
ListID: ns.ListID,
SpellID: ns.SpellID,
Tier: ns.Tier,
CastOnSpawn: ns.CastOnSpawn,
CastOnInitialAggro: ns.CastOnInitialAggro,
RequiredHPRatio: ns.RequiredHPRatio,
}
}
@ -123,8 +123,8 @@ func (ns *NPCSpell) SetRequiredHPRatio(ratio int8) {
// SkillBonus represents a skill bonus from spells
type SkillBonus struct {
SpellID int32 // Spell providing the bonus
Skills map[int32]*SkillBonusValue // Map of skill ID to bonus value
SpellID int32 // Spell providing the bonus
Skills map[int32]*SkillBonusValue // Map of skill ID to bonus value
mutex sync.RWMutex
}
@ -146,7 +146,7 @@ func NewSkillBonus(spellID int32) *SkillBonus {
func (sb *SkillBonus) AddSkill(skillID int32, value float32) {
sb.mutex.Lock()
defer sb.mutex.Unlock()
sb.Skills[skillID] = &SkillBonusValue{
SkillID: skillID,
Value: value,
@ -157,7 +157,7 @@ func (sb *SkillBonus) AddSkill(skillID int32, value float32) {
func (sb *SkillBonus) RemoveSkill(skillID int32) bool {
sb.mutex.Lock()
defer sb.mutex.Unlock()
if _, exists := sb.Skills[skillID]; exists {
delete(sb.Skills, skillID)
return true
@ -169,7 +169,7 @@ func (sb *SkillBonus) RemoveSkill(skillID int32) bool {
func (sb *SkillBonus) GetSkills() map[int32]*SkillBonusValue {
sb.mutex.RLock()
defer sb.mutex.RUnlock()
result := make(map[int32]*SkillBonusValue)
for id, bonus := range sb.Skills {
result[id] = &SkillBonusValue{
@ -183,7 +183,7 @@ func (sb *SkillBonus) GetSkills() map[int32]*SkillBonusValue {
// MovementLocation represents a movement destination for runback
type MovementLocation struct {
X float32 // X coordinate
Y float32 // Y coordinate
Y float32 // Y coordinate
Z float32 // Z coordinate
GridID int32 // Grid location ID
Stage int32 // Movement stage
@ -222,60 +222,60 @@ func (ml *MovementLocation) Copy() *MovementLocation {
// NPC represents a non-player character extending Entity
type NPC struct {
*entity.Entity // Embedded entity for combat capabilities
*entity.Entity // Embedded entity for combat capabilities
// Core NPC properties
appearanceID int32 // Appearance ID for client display
npcID int32 // NPC database ID
aiStrategy int8 // AI strategy (balanced/offensive/defensive)
attackType int8 // Attack type preference
castPercentage int8 // Percentage chance to cast spells
maxPetLevel int8 // Maximum pet level
appearanceID int32 // Appearance ID for client display
npcID int32 // NPC database ID
aiStrategy int8 // AI strategy (balanced/offensive/defensive)
attackType int8 // Attack type preference
castPercentage int8 // Percentage chance to cast spells
maxPetLevel int8 // Maximum pet level
// Combat and movement
aggroRadius float32 // Aggro detection radius
baseAggroRadius float32 // Base aggro radius (for resets)
runback *MovementLocation // Runback location when leaving combat
runningBack bool // Currently running back to spawn point
runbackHeadingDir1 int16 // Original heading direction 1
runbackHeadingDir2 int16 // Original heading direction 2
pauseTimer *Timer // Movement pause timer
// Spell and skill management
primarySpellList int32 // Primary spell list ID
secondarySpellList int32 // Secondary spell list ID
primarySkillList int32 // Primary skill list ID
secondarySkillList int32 // Secondary skill list ID
equipmentListID int32 // Equipment list ID
skills map[string]*Skill // NPC skills by name
spells []*NPCSpell // Available spells
castOnSpells map[int8][]*NPCSpell // Spells to cast by trigger type
skillBonuses map[int32]*SkillBonus // Skill bonuses from spells
hasSpells bool // Whether NPC has any spells
castOnAggroCompleted bool // Whether cast-on-aggro spells are done
aggroRadius float32 // Aggro detection radius
baseAggroRadius float32 // Base aggro radius (for resets)
runback *MovementLocation // Runback location when leaving combat
runningBack bool // Currently running back to spawn point
runbackHeadingDir1 int16 // Original heading direction 1
runbackHeadingDir2 int16 // Original heading direction 2
pauseTimer *Timer // Movement pause timer
// Spell and skill management
primarySpellList int32 // Primary spell list ID
secondarySpellList int32 // Secondary spell list ID
primarySkillList int32 // Primary skill list ID
secondarySkillList int32 // Secondary skill list ID
equipmentListID int32 // Equipment list ID
skills map[string]*Skill // NPC skills by name
spells []*NPCSpell // Available spells
castOnSpells map[int8][]*NPCSpell // Spells to cast by trigger type
skillBonuses map[int32]*SkillBonus // Skill bonuses from spells
hasSpells bool // Whether NPC has any spells
castOnAggroCompleted bool // Whether cast-on-aggro spells are done
// Brain/AI system (placeholder for now)
brain Brain // AI brain for decision making
brain Brain // AI brain for decision making
// Shard system (for cross-server functionality)
shardID int32 // Shard identifier
shardCharID int32 // Character ID on shard
shardCreatedTimestamp int64 // Timestamp when created on shard
shardID int32 // Shard identifier
shardCharID int32 // Character ID on shard
shardCreatedTimestamp int64 // Timestamp when created on shard
// Thread safety
mutex sync.RWMutex // Main NPC mutex
brainMutex sync.RWMutex // Brain-specific mutex
mutex sync.RWMutex // Main NPC mutex
brainMutex sync.RWMutex // Brain-specific mutex
// Atomic flags for thread-safe state management
callRunback bool // Flag to trigger runback
callRunback bool // Flag to trigger runback
}
// Timer represents a simple timer for NPC operations
type Timer struct {
duration time.Duration
startTime time.Time
enabled bool
mutex sync.RWMutex
duration time.Duration
startTime time.Time
enabled bool
mutex sync.RWMutex
}
// NewTimer creates a new timer
@ -289,7 +289,7 @@ func NewTimer() *Timer {
func (t *Timer) Start(durationMS int32, reset bool) {
t.mutex.Lock()
defer t.mutex.Unlock()
if reset || !t.enabled {
t.duration = time.Duration(durationMS) * time.Millisecond
t.startTime = time.Now()
@ -301,11 +301,11 @@ func (t *Timer) Start(durationMS int32, reset bool) {
func (t *Timer) Check() bool {
t.mutex.RLock()
defer t.mutex.RUnlock()
if !t.enabled {
return false
}
return time.Since(t.startTime) >= t.duration
}
@ -325,10 +325,10 @@ func (t *Timer) Disable() {
// Skill represents an NPC skill (simplified from C++ version)
type Skill struct {
SkillID int32 // Skill identifier
Name string // Skill name
CurrentVal int16 // Current skill value
MaxVal int16 // Maximum skill value
SkillID int32 // Skill identifier
Name string // Skill name
CurrentVal int16 // Current skill value
MaxVal int16 // Maximum skill value
mutex sync.RWMutex
}
@ -360,7 +360,7 @@ func (s *Skill) SetCurrentVal(val int16) {
func (s *Skill) IncreaseSkill() bool {
s.mutex.Lock()
defer s.mutex.Unlock()
if s.CurrentVal < s.MaxVal {
s.CurrentVal++
return true
@ -428,13 +428,13 @@ func (b *DefaultBrain) SetActive(active bool) {
// NPCStatistics contains NPC system statistics
type NPCStatistics struct {
TotalNPCs int `json:"total_npcs"`
NPCsInCombat int `json:"npcs_in_combat"`
NPCsWithSpells int `json:"npcs_with_spells"`
NPCsWithSkills int `json:"npcs_with_skills"`
AIStrategyCounts map[string]int `json:"ai_strategy_counts"`
SpellCastCount int64 `json:"spell_cast_count"`
SkillUsageCount int64 `json:"skill_usage_count"`
RunbackCount int64 `json:"runback_count"`
AverageAggroRadius float32 `json:"average_aggro_radius"`
}
TotalNPCs int `json:"total_npcs"`
NPCsInCombat int `json:"npcs_in_combat"`
NPCsWithSpells int `json:"npcs_with_spells"`
NPCsWithSkills int `json:"npcs_with_skills"`
AIStrategyCounts map[string]int `json:"ai_strategy_counts"`
SpellCastCount int64 `json:"spell_cast_count"`
SkillUsageCount int64 `json:"skill_usage_count"`
RunbackCount int64 `json:"runback_count"`
AverageAggroRadius float32 `json:"average_aggro_radius"`
}

View File

@ -4,12 +4,12 @@ package object
const (
// Object spawn type (from C++ constructor)
ObjectSpawnType = 2
// Object appearance defaults (from C++ constructor)
ObjectActivityStatus = 64 // Default activity status
ObjectPosState = 1 // Default position state
ObjectDifficulty = 0 // Default difficulty
ObjectPosState = 1 // Default position state
ObjectDifficulty = 0 // Default difficulty
// Object interaction constants
ObjectShowCommandIcon = 1 // Show command icon when interactable
)
@ -32,4 +32,4 @@ const (
InteractionTypeCommand = 1 // Command-based interaction
InteractionTypeTransport = 2 // Transport/teleport interaction
InteractionTypeDevice = 3 // Device-based interaction
)
)

View File

@ -2,36 +2,35 @@ package object
import (
"fmt"
"eq2emu/internal/spawn"
"eq2emu/internal/common"
)
// ObjectSpawn represents an object that extends spawn functionality
// This properly integrates with the existing spawn system
type ObjectSpawn struct {
*spawn.Spawn // Embed the spawn functionality
// Object-specific properties
clickable bool // Whether the object can be clicked/interacted with
deviceID int8 // Device ID for interactive objects
clickable bool // Whether the object can be clicked/interacted with
deviceID int8 // Device ID for interactive objects
}
// NewObjectSpawn creates a new object spawn with default values
func NewObjectSpawn() *ObjectSpawn {
// Create base spawn
baseSpawn := spawn.NewSpawn()
// Set object-specific spawn properties
baseSpawn.SetSpawnType(ObjectSpawnType)
// Set object appearance defaults
appearance := baseSpawn.GetAppearance()
appearance.ActivityStatus = ObjectActivityStatus
appearance.Pos.State = ObjectPosState
appearance.Difficulty = ObjectDifficulty
baseSpawn.SetAppearance(appearance)
return &ObjectSpawn{
Spawn: baseSpawn,
clickable: false,
@ -68,14 +67,14 @@ func (os *ObjectSpawn) IsObject() bool {
func (os *ObjectSpawn) Copy() *ObjectSpawn {
// Copy base spawn
newSpawn := os.Spawn.Copy()
// Create new object spawn
newObjectSpawn := &ObjectSpawn{
Spawn: newSpawn,
clickable: os.clickable,
deviceID: os.deviceID,
}
return newObjectSpawn
}
@ -83,7 +82,7 @@ func (os *ObjectSpawn) Copy() *ObjectSpawn {
func (os *ObjectSpawn) HandleUse(clientID int32, command string) error {
// Use the base object's HandleUse logic but with spawn integration
object := &Object{}
// Copy relevant properties for handling
object.clickable = os.clickable
object.deviceID = os.deviceID
@ -92,9 +91,9 @@ func (os *ObjectSpawn) HandleUse(clientID int32, command string) error {
if os.GetAppearance().ShowCommandIcon == 1 {
object.appearanceShowCommandIcon = ObjectShowCommandIcon
}
// TODO: Copy command lists when they're integrated with spawn system
return object.HandleUse(clientID, command)
}
@ -117,14 +116,14 @@ func (os *ObjectSpawn) ShowsCommandIcon() bool {
// GetObjectInfo returns comprehensive information about the object spawn
func (os *ObjectSpawn) GetObjectInfo() map[string]interface{} {
info := make(map[string]interface{})
// Add spawn info
info["spawn_id"] = os.GetID()
info["database_id"] = os.GetDatabaseID()
info["zone_name"] = os.GetZoneName()
info["spawn_type"] = os.GetSpawnType()
info["size"] = os.GetSize()
// Add object-specific info
info["clickable"] = os.clickable
info["device_id"] = os.deviceID
@ -132,20 +131,20 @@ func (os *ObjectSpawn) GetObjectInfo() map[string]interface{} {
info["transporter_id"] = os.GetTransporterID()
info["merchant_id"] = os.GetMerchantID()
info["is_collector"] = os.IsCollector()
// Add position info
appearance := os.GetAppearance()
info["x"] = appearance.Pos.X
info["y"] = appearance.Pos.Y
info["z"] = appearance.Pos.Z
info["heading"] = appearance.Pos.Dir1
return info
}
// ObjectSpawnManager manages object spawns specifically
type ObjectSpawnManager struct {
spawnManager *spawn.SpawnManager // Reference to global spawn manager
spawnManager *spawn.SpawnManager // Reference to global spawn manager
objects map[int32]*ObjectSpawn // Object spawns by spawn ID
}
@ -163,10 +162,10 @@ func (osm *ObjectSpawnManager) AddObjectSpawn(objectSpawn *ObjectSpawn) error {
if err := osm.spawnManager.AddSpawn(objectSpawn.Spawn); err != nil {
return err
}
// Add to object tracking
osm.objects[objectSpawn.GetID()] = objectSpawn
return nil
}
@ -174,7 +173,7 @@ func (osm *ObjectSpawnManager) AddObjectSpawn(objectSpawn *ObjectSpawn) error {
func (osm *ObjectSpawnManager) RemoveObjectSpawn(spawnID int32) error {
// Remove from object tracking
delete(osm.objects, spawnID)
// Remove from spawn manager
return osm.spawnManager.RemoveSpawn(spawnID)
}
@ -187,7 +186,7 @@ func (osm *ObjectSpawnManager) GetObjectSpawn(spawnID int32) *ObjectSpawn {
// GetObjectSpawnsByZone returns all object spawns in a zone
func (osm *ObjectSpawnManager) GetObjectSpawnsByZone(zoneName string) []*ObjectSpawn {
result := make([]*ObjectSpawn, 0)
// Get all spawns in zone and filter for objects
spawns := osm.spawnManager.GetSpawnsByZone(zoneName)
for _, spawn := range spawns {
@ -197,20 +196,20 @@ func (osm *ObjectSpawnManager) GetObjectSpawnsByZone(zoneName string) []*ObjectS
}
}
}
return result
}
// GetInteractiveObjectSpawns returns all interactive object spawns
func (osm *ObjectSpawnManager) GetInteractiveObjectSpawns() []*ObjectSpawn {
result := make([]*ObjectSpawn, 0)
for _, objectSpawn := range osm.objects {
if objectSpawn.IsClickable() || objectSpawn.ShowsCommandIcon() {
result = append(result, objectSpawn)
}
}
return result
}
@ -220,7 +219,7 @@ func (osm *ObjectSpawnManager) ProcessObjectInteraction(spawnID, clientID int32,
if objectSpawn == nil {
return fmt.Errorf("object spawn %d not found", spawnID)
}
return objectSpawn.HandleUse(clientID, command)
}
@ -229,19 +228,19 @@ func ConvertSpawnToObject(spawn *spawn.Spawn) *ObjectSpawn {
if spawn.GetSpawnType() != ObjectSpawnType {
return nil
}
objectSpawn := &ObjectSpawn{
Spawn: spawn,
clickable: false, // Default, should be loaded from data
deviceID: DeviceIDNone,
}
// Set clickable based on appearance flags or other indicators
appearance := spawn.GetAppearance()
if appearance.ShowCommandIcon == ObjectShowCommandIcon {
objectSpawn.clickable = true
}
return objectSpawn
}
@ -249,33 +248,33 @@ func ConvertSpawnToObject(spawn *spawn.Spawn) *ObjectSpawn {
// This would be called when loading spawns from the database
func LoadObjectSpawnFromData(spawnData map[string]interface{}) *ObjectSpawn {
objectSpawn := NewObjectSpawn()
// Load basic spawn data
if databaseID, ok := spawnData["database_id"].(int32); ok {
objectSpawn.SetDatabaseID(databaseID)
}
if zoneName, ok := spawnData["zone"].(string); ok {
objectSpawn.SetZoneName(zoneName)
}
// Load object-specific data
if clickable, ok := spawnData["clickable"].(bool); ok {
objectSpawn.SetClickable(clickable)
}
if deviceID, ok := spawnData["device_id"].(int8); ok {
objectSpawn.SetDeviceID(deviceID)
}
// Load position data
if x, ok := spawnData["x"].(float32); ok {
appearance := objectSpawn.GetAppearance()
appearance.Pos.X = x
objectSpawn.SetAppearance(appearance)
}
// TODO: Load other properties as needed
return objectSpawn
}
}

View File

@ -10,27 +10,27 @@ type SpawnInterface interface {
// Basic identification
GetID() int32
GetDatabaseID() int32
// Zone and positioning
GetZoneName() string
SetZoneName(string)
GetX() float32
GetY() float32
GetY() float32
GetZ() float32
GetHeading() float32
// Spawn properties
GetSpawnType() int8
SetSpawnType(int8)
GetSize() int16
SetSize(int16)
// State flags
IsAlive() bool
SetAlive(bool)
IsRunning() bool
SetRunning(bool)
// Entity properties for spell/trade integration
GetFactionID() int32
SetFactionID(int32)
@ -41,19 +41,19 @@ type SpawnInterface interface {
// ObjectInterface defines the interface for interactive objects
type ObjectInterface interface {
SpawnInterface
// Object-specific properties
IsObject() bool
IsClickable() bool
SetClickable(bool)
GetDeviceID() int8
SetDeviceID(int8)
// Interaction
HandleUse(clientID int32, command string) error
ShowsCommandIcon() bool
SetShowCommandIcon(bool)
// Merchant functionality
GetMerchantID() int32
SetMerchantID(int32)
@ -61,11 +61,11 @@ type ObjectInterface interface {
SetMerchantType(int8)
IsCollector() bool
SetCollector(bool)
// Transport functionality
GetTransporterID() int32
SetTransporterID(int32)
// Copying
Copy() ObjectInterface
}
@ -101,7 +101,7 @@ type ObjectSpawnAsEntity struct {
*ObjectSpawn
name string
isPlayer bool
isBot bool
isBot bool
coinsAmount int64
clientVersion int32
}
@ -167,14 +167,14 @@ func (osae *ObjectSpawnAsEntity) SetClientVersion(version int32) {
// ObjectItem represents an item provided by an object (merchants, containers, etc.)
type ObjectItem struct {
id int32
name string
quantity int32
iconID int32
noTrade bool
heirloom bool
attuned bool
creationTime time.Time
id int32
name string
quantity int32
iconID int32
noTrade bool
heirloom bool
attuned bool
creationTime time.Time
groupCharacterIDs []int32
}
@ -285,7 +285,7 @@ func CreateMerchantObjectSpawn(merchantID int32, merchantType int8) *ObjectSpawn
objectSpawn.SetMerchantType(merchantType)
objectSpawn.SetClickable(true)
objectSpawn.SetShowCommandIcon(true)
return objectSpawn
}
@ -295,7 +295,7 @@ func CreateTransportObjectSpawn(transporterID int32) *ObjectSpawn {
objectSpawn.SetTransporterID(transporterID)
objectSpawn.SetClickable(true)
objectSpawn.SetShowCommandIcon(true)
return objectSpawn
}
@ -305,7 +305,7 @@ func CreateDeviceObjectSpawn(deviceID int8) *ObjectSpawn {
objectSpawn.SetDeviceID(deviceID)
objectSpawn.SetClickable(true)
objectSpawn.SetShowCommandIcon(true)
return objectSpawn
}
@ -315,6 +315,6 @@ func CreateCollectorObjectSpawn() *ObjectSpawn {
objectSpawn.SetCollector(true)
objectSpawn.SetClickable(true)
objectSpawn.SetShowCommandIcon(true)
return objectSpawn
}
}

View File

@ -8,16 +8,16 @@ import (
// ObjectManager manages all objects in the game world
type ObjectManager struct {
objects map[int32]*Object // Objects by database ID
// Zone-based indexing
objectsByZone map[string][]*Object // Objects grouped by zone
// Type-based indexing
interactiveObjects []*Object // Objects that can be interacted with
transportObjects []*Object // Objects that provide transport
merchantObjects []*Object // Objects that are merchants
collectorObjects []*Object // Objects that are collectors
// Thread safety
mutex sync.RWMutex
}
@ -39,32 +39,32 @@ func (om *ObjectManager) AddObject(object *Object) error {
if object == nil {
return fmt.Errorf("cannot add nil object")
}
om.mutex.Lock()
defer om.mutex.Unlock()
databaseID := object.GetDatabaseID()
if databaseID == 0 {
return fmt.Errorf("object must have a valid database ID")
}
// Check if object already exists
if _, exists := om.objects[databaseID]; exists {
return fmt.Errorf("object with database ID %d already exists", databaseID)
}
// Add to main collection
om.objects[databaseID] = object
// Add to zone collection
zoneName := object.GetZoneName()
if zoneName != "" {
om.objectsByZone[zoneName] = append(om.objectsByZone[zoneName], object)
}
// Add to type-based collections
om.updateObjectIndices(object, true)
return nil
}
@ -72,15 +72,15 @@ func (om *ObjectManager) AddObject(object *Object) error {
func (om *ObjectManager) RemoveObject(databaseID int32) error {
om.mutex.Lock()
defer om.mutex.Unlock()
object, exists := om.objects[databaseID]
if !exists {
return fmt.Errorf("object with database ID %d not found", databaseID)
}
// Remove from main collection
delete(om.objects, databaseID)
// Remove from zone collection
zoneName := object.GetZoneName()
if zoneName != "" {
@ -91,17 +91,17 @@ func (om *ObjectManager) RemoveObject(databaseID int32) error {
break
}
}
// Clean up empty zone collection
if len(om.objectsByZone[zoneName]) == 0 {
delete(om.objectsByZone, zoneName)
}
}
}
// Remove from type-based collections
om.updateObjectIndices(object, false)
return nil
}
@ -109,7 +109,7 @@ func (om *ObjectManager) RemoveObject(databaseID int32) error {
func (om *ObjectManager) GetObject(databaseID int32) *Object {
om.mutex.RLock()
defer om.mutex.RUnlock()
return om.objects[databaseID]
}
@ -117,14 +117,14 @@ func (om *ObjectManager) GetObject(databaseID int32) *Object {
func (om *ObjectManager) GetObjectsByZone(zoneName string) []*Object {
om.mutex.RLock()
defer om.mutex.RUnlock()
if objects, exists := om.objectsByZone[zoneName]; exists {
// Return a copy to prevent external modification
result := make([]*Object, len(objects))
copy(result, objects)
return result
}
return make([]*Object, 0)
}
@ -132,7 +132,7 @@ func (om *ObjectManager) GetObjectsByZone(zoneName string) []*Object {
func (om *ObjectManager) GetInteractiveObjects() []*Object {
om.mutex.RLock()
defer om.mutex.RUnlock()
result := make([]*Object, len(om.interactiveObjects))
copy(result, om.interactiveObjects)
return result
@ -142,7 +142,7 @@ func (om *ObjectManager) GetInteractiveObjects() []*Object {
func (om *ObjectManager) GetTransportObjects() []*Object {
om.mutex.RLock()
defer om.mutex.RUnlock()
result := make([]*Object, len(om.transportObjects))
copy(result, om.transportObjects)
return result
@ -152,7 +152,7 @@ func (om *ObjectManager) GetTransportObjects() []*Object {
func (om *ObjectManager) GetMerchantObjects() []*Object {
om.mutex.RLock()
defer om.mutex.RUnlock()
result := make([]*Object, len(om.merchantObjects))
copy(result, om.merchantObjects)
return result
@ -162,7 +162,7 @@ func (om *ObjectManager) GetMerchantObjects() []*Object {
func (om *ObjectManager) GetCollectorObjects() []*Object {
om.mutex.RLock()
defer om.mutex.RUnlock()
result := make([]*Object, len(om.collectorObjects))
copy(result, om.collectorObjects)
return result
@ -172,7 +172,7 @@ func (om *ObjectManager) GetCollectorObjects() []*Object {
func (om *ObjectManager) GetObjectCount() int {
om.mutex.RLock()
defer om.mutex.RUnlock()
return len(om.objects)
}
@ -180,7 +180,7 @@ func (om *ObjectManager) GetObjectCount() int {
func (om *ObjectManager) GetZoneCount() int {
om.mutex.RLock()
defer om.mutex.RUnlock()
return len(om.objectsByZone)
}
@ -188,7 +188,7 @@ func (om *ObjectManager) GetZoneCount() int {
func (om *ObjectManager) GetObjectsByType(objectType string) []*Object {
om.mutex.RLock()
defer om.mutex.RUnlock()
switch objectType {
case "interactive":
result := make([]*Object, len(om.interactiveObjects))
@ -215,19 +215,19 @@ func (om *ObjectManager) GetObjectsByType(objectType string) []*Object {
func (om *ObjectManager) FindObjectsInZone(zoneName string, filter func(*Object) bool) []*Object {
om.mutex.RLock()
defer om.mutex.RUnlock()
zoneObjects, exists := om.objectsByZone[zoneName]
if !exists {
return make([]*Object, 0)
}
result := make([]*Object, 0)
for _, obj := range zoneObjects {
if filter == nil || filter(obj) {
result = append(result, obj)
}
}
return result
}
@ -235,7 +235,7 @@ func (om *ObjectManager) FindObjectsInZone(zoneName string, filter func(*Object)
func (om *ObjectManager) FindObjectByName(name string) *Object {
om.mutex.RLock()
defer om.mutex.RUnlock()
// TODO: Implement name searching when spawn name system is integrated
// For now, return nil
return nil
@ -245,18 +245,18 @@ func (om *ObjectManager) FindObjectByName(name string) *Object {
func (om *ObjectManager) UpdateObject(databaseID int32, updateFn func(*Object)) error {
om.mutex.Lock()
defer om.mutex.Unlock()
object, exists := om.objects[databaseID]
if !exists {
return fmt.Errorf("object with database ID %d not found", databaseID)
}
// Store old zone for potential reindexing
oldZone := object.GetZoneName()
// Apply updates
updateFn(object)
// Check if zone changed and reindex if necessary
newZone := object.GetZoneName()
if oldZone != newZone {
@ -269,23 +269,23 @@ func (om *ObjectManager) UpdateObject(databaseID int32, updateFn func(*Object))
break
}
}
// Clean up empty zone collection
if len(om.objectsByZone[oldZone]) == 0 {
delete(om.objectsByZone, oldZone)
}
}
}
// Add to new zone
if newZone != "" {
om.objectsByZone[newZone] = append(om.objectsByZone[newZone], object)
}
}
// Update type-based indices
om.rebuildIndicesForObject(object)
return nil
}
@ -293,24 +293,24 @@ func (om *ObjectManager) UpdateObject(databaseID int32, updateFn func(*Object))
func (om *ObjectManager) ClearZone(zoneName string) int {
om.mutex.Lock()
defer om.mutex.Unlock()
zoneObjects, exists := om.objectsByZone[zoneName]
if !exists {
return 0
}
count := len(zoneObjects)
// Remove objects from main collection and indices
for _, obj := range zoneObjects {
databaseID := obj.GetDatabaseID()
delete(om.objects, databaseID)
om.updateObjectIndices(obj, false)
}
// Clear zone collection
delete(om.objectsByZone, zoneName)
return count
}
@ -318,7 +318,7 @@ func (om *ObjectManager) ClearZone(zoneName string) int {
func (om *ObjectManager) GetStatistics() map[string]interface{} {
om.mutex.RLock()
defer om.mutex.RUnlock()
stats := make(map[string]interface{})
stats["total_objects"] = len(om.objects)
stats["zones_with_objects"] = len(om.objectsByZone)
@ -326,14 +326,14 @@ func (om *ObjectManager) GetStatistics() map[string]interface{} {
stats["transport_objects"] = len(om.transportObjects)
stats["merchant_objects"] = len(om.merchantObjects)
stats["collector_objects"] = len(om.collectorObjects)
// Zone breakdown
zoneStats := make(map[string]int)
for zoneName, objects := range om.objectsByZone {
zoneStats[zoneName] = len(objects)
}
stats["objects_by_zone"] = zoneStats
return stats
}
@ -341,7 +341,7 @@ func (om *ObjectManager) GetStatistics() map[string]interface{} {
func (om *ObjectManager) Shutdown() {
om.mutex.Lock()
defer om.mutex.Unlock()
om.objects = make(map[int32]*Object)
om.objectsByZone = make(map[string][]*Object)
om.interactiveObjects = make([]*Object, 0)
@ -359,22 +359,22 @@ func (om *ObjectManager) updateObjectIndices(object *Object, add bool) {
if object.IsClickable() || len(object.GetPrimaryCommands()) > 0 || len(object.GetSecondaryCommands()) > 0 {
om.interactiveObjects = append(om.interactiveObjects, object)
}
if object.GetTransporterID() > 0 {
om.transportObjects = append(om.transportObjects, object)
}
if object.GetMerchantID() > 0 {
om.merchantObjects = append(om.merchantObjects, object)
}
if object.IsCollector() {
om.collectorObjects = append(om.collectorObjects, object)
}
} else {
// Remove from type-based collections
databaseID := object.GetDatabaseID()
om.interactiveObjects = removeObjectFromSlice(om.interactiveObjects, databaseID)
om.transportObjects = removeObjectFromSlice(om.transportObjects, databaseID)
om.merchantObjects = removeObjectFromSlice(om.merchantObjects, databaseID)
@ -385,13 +385,13 @@ func (om *ObjectManager) updateObjectIndices(object *Object, add bool) {
// rebuildIndicesForObject rebuilds type-based indices for an object (used after updates)
func (om *ObjectManager) rebuildIndicesForObject(object *Object) {
databaseID := object.GetDatabaseID()
// Remove from all type-based collections
om.interactiveObjects = removeObjectFromSlice(om.interactiveObjects, databaseID)
om.transportObjects = removeObjectFromSlice(om.transportObjects, databaseID)
om.merchantObjects = removeObjectFromSlice(om.merchantObjects, databaseID)
om.collectorObjects = removeObjectFromSlice(om.collectorObjects, databaseID)
// Re-add based on current properties
om.updateObjectIndices(object, true)
}
@ -416,4 +416,4 @@ func GetGlobalObjectManager() *ObjectManager {
globalObjectManager = NewObjectManager()
})
return globalObjectManager
}
}

View File

@ -12,46 +12,46 @@ import (
type Object struct {
// Embed spawn functionality - TODO: Use actual spawn.Spawn when integrated
// spawn.Spawn
// Object-specific properties
clickable bool // Whether the object can be clicked/interacted with
zoneName string // Name of the zone this object belongs to
deviceID int8 // Device ID for interactive objects
// Inherited spawn properties (placeholder until spawn integration)
databaseID int32
size int16
sizeOffset int8
merchantID int32
merchantType int8
merchantMinLevel int8
merchantMaxLevel int8
isCollector bool
factionID int32
totalHP int32
totalPower int32
currentHP int32
currentPower int32
transporterID int32
soundsDisabled bool
omittedByDBFlag bool
lootTier int8
lootDropType int8
spawnScript string
spawnScriptSetDB bool
primaryCommandListID int32
secondaryCommandListID int32
databaseID int32
size int16
sizeOffset int8
merchantID int32
merchantType int8
merchantMinLevel int8
merchantMaxLevel int8
isCollector bool
factionID int32
totalHP int32
totalPower int32
currentHP int32
currentPower int32
transporterID int32
soundsDisabled bool
omittedByDBFlag bool
lootTier int8
lootDropType int8
spawnScript string
spawnScriptSetDB bool
primaryCommandListID int32
secondaryCommandListID int32
// Appearance data placeholder - TODO: Use actual appearance struct
appearanceActivityStatus int8
appearancePosState int8
appearanceDifficulty int8
appearanceActivityStatus int8
appearancePosState int8
appearanceDifficulty int8
appearanceShowCommandIcon int8
// Command lists - TODO: Use actual command structures
primaryCommands []string
secondaryCommands []string
// Thread safety
mutex sync.RWMutex
}
@ -60,15 +60,15 @@ type Object struct {
// Converted from C++ Object::Object constructor
func NewObject() *Object {
return &Object{
clickable: false,
zoneName: "",
deviceID: DeviceIDNone,
appearanceActivityStatus: ObjectActivityStatus,
appearancePosState: ObjectPosState,
appearanceDifficulty: ObjectDifficulty,
clickable: false,
zoneName: "",
deviceID: DeviceIDNone,
appearanceActivityStatus: ObjectActivityStatus,
appearancePosState: ObjectPosState,
appearanceDifficulty: ObjectDifficulty,
appearanceShowCommandIcon: 0,
primaryCommands: make([]string, 0),
secondaryCommands: make([]string, 0),
primaryCommands: make([]string, 0),
secondaryCommands: make([]string, 0),
}
}
@ -128,9 +128,9 @@ func (o *Object) IsObject() bool {
func (o *Object) Copy() *Object {
o.mutex.RLock()
defer o.mutex.RUnlock()
newObject := NewObject()
// Copy basic properties
newObject.clickable = o.clickable
newObject.zoneName = o.zoneName
@ -155,13 +155,13 @@ func (o *Object) Copy() *Object {
newObject.spawnScriptSetDB = o.spawnScriptSetDB
newObject.primaryCommandListID = o.primaryCommandListID
newObject.secondaryCommandListID = o.secondaryCommandListID
// Copy appearance data
newObject.appearanceActivityStatus = o.appearanceActivityStatus
newObject.appearancePosState = o.appearancePosState
newObject.appearanceDifficulty = o.appearanceDifficulty
newObject.appearanceShowCommandIcon = o.appearanceShowCommandIcon
// Handle size with random offset (from C++ logic)
if o.sizeOffset > 0 {
offset := o.sizeOffset + 1
@ -175,13 +175,13 @@ func (o *Object) Copy() *Object {
} else {
newObject.size = o.size
}
// Copy command lists
newObject.primaryCommands = make([]string, len(o.primaryCommands))
copy(newObject.primaryCommands, o.primaryCommands)
newObject.secondaryCommands = make([]string, len(o.secondaryCommands))
copy(newObject.secondaryCommands, o.secondaryCommands)
return newObject
}
@ -190,19 +190,19 @@ func (o *Object) Copy() *Object {
func (o *Object) HandleUse(clientID int32, command string) error {
o.mutex.RLock()
defer o.mutex.RUnlock()
// TODO: Implement transport destination handling when zone system is available
// This would check for transporter ID and process teleportation
if o.transporterID > 0 {
// Handle transport logic
return o.handleTransport(clientID)
}
// Handle command-based interaction
if len(command) > 0 && o.appearanceShowCommandIcon == ObjectShowCommandIcon {
return o.handleCommand(clientID, command)
}
return fmt.Errorf("object is not interactive")
}
@ -213,7 +213,7 @@ func (o *Object) handleTransport(clientID int32) error {
// 1. Get transport destinations for this object
// 2. Present options to the client
// 3. Process teleportation request
return fmt.Errorf("transport system not yet implemented")
}
@ -224,22 +224,22 @@ func (o *Object) handleCommand(clientID int32, command string) error {
// 1. Find the entity command by name
// 2. Validate client permissions
// 3. Execute the command
command = strings.TrimSpace(strings.ToLower(command))
// Check if command exists in primary or secondary commands
for _, cmd := range o.primaryCommands {
if strings.ToLower(cmd) == command {
return o.executeCommand(clientID, cmd)
}
}
for _, cmd := range o.secondaryCommands {
if strings.ToLower(cmd) == command {
return o.executeCommand(clientID, cmd)
}
}
return fmt.Errorf("command '%s' not found", command)
}
@ -584,7 +584,7 @@ func (o *Object) ShowsCommandIcon() bool {
func (o *Object) GetObjectInfo() map[string]interface{} {
o.mutex.RLock()
defer o.mutex.RUnlock()
info := make(map[string]interface{})
info["clickable"] = o.clickable
info["zone_name"] = o.zoneName
@ -598,6 +598,6 @@ func (o *Object) GetObjectInfo() map[string]interface{} {
info["primary_commands"] = len(o.primaryCommands)
info["secondary_commands"] = len(o.secondaryCommands)
info["shows_command_icon"] = o.ShowsCommandIcon()
return info
}
}

View File

@ -5,7 +5,7 @@ func (p *Player) SetCharacterFlag(flag int) {
if flag > CF_MAXIMUM_FLAG {
return
}
if flag < 32 {
p.GetInfoStruct().SetFlags(p.GetInfoStruct().GetFlags() | (1 << uint(flag)))
} else {
@ -19,7 +19,7 @@ func (p *Player) ResetCharacterFlag(flag int) {
if flag > CF_MAXIMUM_FLAG {
return
}
if flag < 32 {
p.GetInfoStruct().SetFlags(p.GetInfoStruct().GetFlags() & ^(1 << uint(flag)))
} else {
@ -33,7 +33,7 @@ func (p *Player) ToggleCharacterFlag(flag int) {
if flag > CF_MAXIMUM_FLAG {
return
}
if p.GetCharacterFlag(flag) {
p.ResetCharacterFlag(flag)
} else {
@ -46,7 +46,7 @@ func (p *Player) GetCharacterFlag(flag int) bool {
if flag > CF_MAXIMUM_FLAG {
return false
}
var ret bool
if flag < 32 {
ret = (p.GetInfoStruct().GetFlags() & (1 << uint(flag))) != 0
@ -84,14 +84,14 @@ func NewPlayerControlFlags() PlayerControlFlags {
func (pcf *PlayerControlFlags) SetPlayerControlFlag(param, paramValue int8, isActive bool) {
pcf.controlMutex.Lock()
defer pcf.controlMutex.Unlock()
if pcf.currentFlags[param] == nil {
pcf.currentFlags[param] = make(map[int8]bool)
}
if pcf.currentFlags[param][paramValue] != isActive {
pcf.currentFlags[param][paramValue] = isActive
pcf.changesMutex.Lock()
if pcf.flagChanges[param] == nil {
pcf.flagChanges[param] = make(map[int8]int8)
@ -117,15 +117,15 @@ func (pcf *PlayerControlFlags) ControlFlagsChanged() bool {
func (pcf *PlayerControlFlags) SendControlFlagUpdates(client *Client) {
pcf.changesMutex.Lock()
defer pcf.changesMutex.Unlock()
if !pcf.flagsChanged {
return
}
// TODO: Implement packet sending logic
// For each change in flagChanges, create and send appropriate packets
// Clear changes after sending
pcf.flagChanges = make(map[int8]map[int8]int8)
pcf.flagsChanged = false
}
}

Some files were not shown because too many files have changed in this diff Show More