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

View File

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

View File

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

View File

@ -3,7 +3,6 @@ package alt_advancement
import ( import (
"fmt" "fmt"
"sort" "sort"
"sync"
"time" "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 // GetTreeIDForTab returns the tree ID for a specific tab and class
func (manl *MasterAANodeList) GetTreeIDForTab(classID int32, tab int8) int32 { func (manl *MasterAANodeList) GetTreeIDForTab(classID int32, tab int8) int32 {
nodes := manl.GetTreeNodesByClass(classID) nodes := manl.GetTreeNodesByClass(classID)
if int(tab) < len(nodes) { if int(tab) < len(nodes) {
return nodes[tab].TreeID return nodes[tab].TreeID
} }
return 0 return 0
} }

View File

@ -8,287 +8,287 @@ import (
// AltAdvanceData represents an Alternate Advancement node // AltAdvanceData represents an Alternate Advancement node
type AltAdvanceData struct { type AltAdvanceData struct {
// Core identification // Core identification
SpellID int32 `json:"spell_id" db:"spell_id"` SpellID int32 `json:"spell_id" db:"spell_id"`
NodeID int32 `json:"node_id" db:"node_id"` NodeID int32 `json:"node_id" db:"node_id"`
SpellCRC int32 `json:"spell_crc" db:"spell_crc"` SpellCRC int32 `json:"spell_crc" db:"spell_crc"`
// Display information // Display information
Name string `json:"name" db:"name"` Name string `json:"name" db:"name"`
Description string `json:"description" db:"description"` Description string `json:"description" db:"description"`
// Tree organization // Tree organization
Group int8 `json:"group" db:"group"` // AA tab (AA_CLASS, AA_SUBCLASS, etc.) Group int8 `json:"group" db:"group"` // AA tab (AA_CLASS, AA_SUBCLASS, etc.)
Col int8 `json:"col" db:"col"` // Column position in tree Col int8 `json:"col" db:"col"` // Column position in tree
Row int8 `json:"row" db:"row"` // Row position in tree Row int8 `json:"row" db:"row"` // Row position in tree
// Visual representation // Visual representation
Icon int16 `json:"icon" db:"icon"` // Primary icon ID Icon int16 `json:"icon" db:"icon"` // Primary icon ID
Icon2 int16 `json:"icon2" db:"icon2"` // Secondary icon ID Icon2 int16 `json:"icon2" db:"icon2"` // Secondary icon ID
// Ranking system // Ranking system
RankCost int8 `json:"rank_cost" db:"rank_cost"` // Cost per rank RankCost int8 `json:"rank_cost" db:"rank_cost"` // Cost per rank
MaxRank int8 `json:"max_rank" db:"max_rank"` // Maximum achievable rank MaxRank int8 `json:"max_rank" db:"max_rank"` // Maximum achievable rank
// Prerequisites // Prerequisites
MinLevel int8 `json:"min_level" db:"min_level"` // Minimum character level 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 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 RankPrereq int8 `json:"rank_prereq" db:"rank_prereq"` // Required rank in prerequisite
ClassReq int8 `json:"class_req" db:"class_req"` // Required class ClassReq int8 `json:"class_req" db:"class_req"` // Required class
Tier int8 `json:"tier" db:"tier"` // AA tier Tier int8 `json:"tier" db:"tier"` // AA tier
ReqPoints int8 `json:"req_points" db:"req_points"` // Required points in classification 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 ReqTreePoints int16 `json:"req_tree_points" db:"req_tree_points"` // Required points in entire tree
// Display classification // Display classification
ClassName string `json:"class_name" db:"class_name"` // Class name for display ClassName string `json:"class_name" db:"class_name"` // Class name for display
SubclassName string `json:"subclass_name" db:"subclass_name"` // Subclass 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 LineTitle string `json:"line_title" db:"line_title"` // AA line title
TitleLevel int8 `json:"title_level" db:"title_level"` // Title level requirement TitleLevel int8 `json:"title_level" db:"title_level"` // Title level requirement
// Metadata // Metadata
CreatedAt time.Time `json:"created_at" db:"created_at"` CreatedAt time.Time `json:"created_at" db:"created_at"`
UpdatedAt time.Time `json:"updated_at" db:"updated_at"` UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
} }
// TreeNodeData represents class-specific AA tree node configuration // TreeNodeData represents class-specific AA tree node configuration
type TreeNodeData struct { type TreeNodeData struct {
ClassID int32 `json:"class_id" db:"class_id"` // Character class ID ClassID int32 `json:"class_id" db:"class_id"` // Character class ID
TreeID int32 `json:"tree_id" db:"tree_id"` // Tree node identifier 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 AATreeID int32 `json:"aa_tree_id" db:"aa_tree_id"` // AA tree classification ID
} }
// AAEntry represents a player's AA entry in a template // AAEntry represents a player's AA entry in a template
type AAEntry struct { type AAEntry struct {
TemplateID int8 `json:"template_id" db:"template_id"` // Template identifier (1-8) TemplateID int8 `json:"template_id" db:"template_id"` // Template identifier (1-8)
TabID int8 `json:"tab_id" db:"tab_id"` // Tab identifier TabID int8 `json:"tab_id" db:"tab_id"` // Tab identifier
AAID int32 `json:"aa_id" db:"aa_id"` // AA node ID AAID int32 `json:"aa_id" db:"aa_id"` // AA node ID
Order int16 `json:"order" db:"order"` // Display order Order int16 `json:"order" db:"order"` // Display order
TreeID int8 `json:"tree_id" db:"tree_id"` // Tree identifier TreeID int8 `json:"tree_id" db:"tree_id"` // Tree identifier
} }
// PlayerAAData represents a player's AA progression // PlayerAAData represents a player's AA progression
type PlayerAAData struct { type PlayerAAData struct {
// Player identification // Player identification
CharacterID int32 `json:"character_id" db:"character_id"` CharacterID int32 `json:"character_id" db:"character_id"`
// AA progression // AA progression
NodeID int32 `json:"node_id" db:"node_id"` // AA node ID NodeID int32 `json:"node_id" db:"node_id"` // AA node ID
CurrentRank int8 `json:"current_rank" db:"current_rank"` // Current rank in this AA 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 PointsSpent int32 `json:"points_spent" db:"points_spent"` // Total points spent on this AA
// Template assignment // Template assignment
TemplateID int8 `json:"template_id" db:"template_id"` // Template this AA belongs to 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 TabID int8 `json:"tab_id" db:"tab_id"` // Tab this AA belongs to
Order int16 `json:"order" db:"order"` // Display order Order int16 `json:"order" db:"order"` // Display order
// Timestamps // Timestamps
PurchasedAt time.Time `json:"purchased_at" db:"purchased_at"` PurchasedAt time.Time `json:"purchased_at" db:"purchased_at"`
UpdatedAt time.Time `json:"updated_at" db:"updated_at"` UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
} }
// AATemplate represents an AA template configuration // AATemplate represents an AA template configuration
type AATemplate struct { type AATemplate struct {
// Template identification // Template identification
TemplateID int8 `json:"template_id"` TemplateID int8 `json:"template_id"`
Name string `json:"name"` Name string `json:"name"`
Description string `json:"description"` Description string `json:"description"`
IsPersonal bool `json:"is_personal"` // True for personal templates (1-3) IsPersonal bool `json:"is_personal"` // True for personal templates (1-3)
IsServer bool `json:"is_server"` // True for server templates (4-6) IsServer bool `json:"is_server"` // True for server templates (4-6)
IsCurrent bool `json:"is_current"` // True for current active template IsCurrent bool `json:"is_current"` // True for current active template
// Template data // Template data
Entries []*AAEntry `json:"entries"` // AA entries in this template Entries []*AAEntry `json:"entries"` // AA entries in this template
// Metadata // Metadata
CreatedAt time.Time `json:"created_at"` CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"` UpdatedAt time.Time `json:"updated_at"`
} }
// AATab represents an AA tab with its associated data // AATab represents an AA tab with its associated data
type AATab struct { type AATab struct {
// Tab identification // Tab identification
TabID int8 `json:"tab_id"` TabID int8 `json:"tab_id"`
Group int8 `json:"group"` Group int8 `json:"group"`
Name string `json:"name"` Name string `json:"name"`
// Tab configuration // Tab configuration
MaxAA int32 `json:"max_aa"` // Maximum AA points for this tab MaxAA int32 `json:"max_aa"` // Maximum AA points for this tab
ClassID int32 `json:"class_id"` // Associated class ID ClassID int32 `json:"class_id"` // Associated class ID
ExpansionReq int8 `json:"expansion_req"` // Required expansion flags ExpansionReq int8 `json:"expansion_req"` // Required expansion flags
// Current state // 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 PointsAvailable int32 `json:"points_available"` // Available points for spending
// AA nodes in this tab // AA nodes in this tab
Nodes []*AltAdvanceData `json:"nodes"` Nodes []*AltAdvanceData `json:"nodes"`
// Metadata // Metadata
LastUpdate time.Time `json:"last_update"` LastUpdate time.Time `json:"last_update"`
} }
// AAPlayerState represents a player's complete AA state // AAPlayerState represents a player's complete AA state
type AAPlayerState struct { type AAPlayerState struct {
// Player identification // Player identification
CharacterID int32 `json:"character_id"` CharacterID int32 `json:"character_id"`
// AA points // AA points
TotalPoints int32 `json:"total_points"` // Total AA points earned TotalPoints int32 `json:"total_points"` // Total AA points earned
SpentPoints int32 `json:"spent_points"` // Total AA points spent SpentPoints int32 `json:"spent_points"` // Total AA points spent
AvailablePoints int32 `json:"available_points"` // Available AA points 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 // Templates
ActiveTemplate int8 `json:"active_template"` // Currently active template ActiveTemplate int8 `json:"active_template"` // Currently active template
Templates map[int8]*AATemplate `json:"templates"` // All templates Templates map[int8]*AATemplate `json:"templates"` // All templates
// Tab states // Tab states
Tabs map[int8]*AATab `json:"tabs"` // Tab states Tabs map[int8]*AATab `json:"tabs"` // Tab states
// Player AA progression // 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 // Caching and synchronization
mutex sync.RWMutex `json:"-"` mutex sync.RWMutex `json:"-"`
lastUpdate time.Time `json:"last_update"` lastUpdate time.Time `json:"last_update"`
needsSync bool `json:"-"` needsSync bool `json:"-"`
} }
// MasterAAList manages all AA definitions // MasterAAList manages all AA definitions
type MasterAAList struct { type MasterAAList struct {
// AA storage // AA storage
aaList []*AltAdvanceData `json:"aa_list"` aaList []*AltAdvanceData `json:"aa_list"`
aaBySpellID map[int32]*AltAdvanceData `json:"-"` // Fast lookup by spell ID aaBySpellID map[int32]*AltAdvanceData `json:"-"` // Fast lookup by spell ID
aaByNodeID map[int32]*AltAdvanceData `json:"-"` // Fast lookup by node ID aaByNodeID map[int32]*AltAdvanceData `json:"-"` // Fast lookup by node ID
aaByGroup map[int8][]*AltAdvanceData `json:"-"` // Fast lookup by group/tab aaByGroup map[int8][]*AltAdvanceData `json:"-"` // Fast lookup by group/tab
// Synchronization // Synchronization
mutex sync.RWMutex `json:"-"` mutex sync.RWMutex `json:"-"`
// Statistics // Statistics
totalLoaded int64 `json:"total_loaded"` totalLoaded int64 `json:"total_loaded"`
lastLoadTime time.Time `json:"last_load_time"` lastLoadTime time.Time `json:"last_load_time"`
} }
// MasterAANodeList manages tree node configurations // MasterAANodeList manages tree node configurations
type MasterAANodeList struct { type MasterAANodeList struct {
// Node storage // Node storage
nodeList []*TreeNodeData `json:"node_list"` nodeList []*TreeNodeData `json:"node_list"`
nodesByClass map[int32][]*TreeNodeData `json:"-"` // Fast lookup by class ID nodesByClass map[int32][]*TreeNodeData `json:"-"` // Fast lookup by class ID
nodesByTree map[int32]*TreeNodeData `json:"-"` // Fast lookup by tree ID nodesByTree map[int32]*TreeNodeData `json:"-"` // Fast lookup by tree ID
// Synchronization // Synchronization
mutex sync.RWMutex `json:"-"` mutex sync.RWMutex `json:"-"`
// Statistics // Statistics
totalLoaded int64 `json:"total_loaded"` totalLoaded int64 `json:"total_loaded"`
lastLoadTime time.Time `json:"last_load_time"` lastLoadTime time.Time `json:"last_load_time"`
} }
// AAManager manages the entire AA system // AAManager manages the entire AA system
type AAManager struct { type AAManager struct {
// Core lists // Core lists
masterAAList *MasterAAList `json:"master_aa_list"` masterAAList *MasterAAList `json:"master_aa_list"`
masterNodeList *MasterAANodeList `json:"master_node_list"` masterNodeList *MasterAANodeList `json:"master_node_list"`
// Player states // Player states
playerStates map[int32]*AAPlayerState `json:"-"` // Player AA states by character ID playerStates map[int32]*AAPlayerState `json:"-"` // Player AA states by character ID
statesMutex sync.RWMutex `json:"-"` statesMutex sync.RWMutex `json:"-"`
// Configuration // Configuration
config AAManagerConfig `json:"config"` config AAManagerConfig `json:"config"`
// Database interface // Database interface
database AADatabase `json:"-"` database AADatabase `json:"-"`
// Packet handler // Packet handler
packetHandler AAPacketHandler `json:"-"` packetHandler AAPacketHandler `json:"-"`
// Event handlers // Event handlers
eventHandlers []AAEventHandler `json:"-"` eventHandlers []AAEventHandler `json:"-"`
eventMutex sync.RWMutex `json:"-"` eventMutex sync.RWMutex `json:"-"`
// Statistics // Statistics
stats AAManagerStats `json:"stats"` stats AAManagerStats `json:"stats"`
statsMutex sync.RWMutex `json:"-"` statsMutex sync.RWMutex `json:"-"`
// Background processing // Background processing
stopChan chan struct{} `json:"-"` stopChan chan struct{} `json:"-"`
wg sync.WaitGroup `json:"-"` wg sync.WaitGroup `json:"-"`
} }
// AAManagerConfig holds configuration for the AA manager // AAManagerConfig holds configuration for the AA manager
type AAManagerConfig struct { type AAManagerConfig struct {
// System settings // System settings
EnableAASystem bool `json:"enable_aa_system"` EnableAASystem bool `json:"enable_aa_system"`
EnableCaching bool `json:"enable_caching"` EnableCaching bool `json:"enable_caching"`
EnableValidation bool `json:"enable_validation"` EnableValidation bool `json:"enable_validation"`
EnableLogging bool `json:"enable_logging"` EnableLogging bool `json:"enable_logging"`
// Player settings // Player settings
AAPointsPerLevel int32 `json:"aa_points_per_level"` AAPointsPerLevel int32 `json:"aa_points_per_level"`
MaxBankedPoints int32 `json:"max_banked_points"` MaxBankedPoints int32 `json:"max_banked_points"`
EnableAABanking bool `json:"enable_aa_banking"` EnableAABanking bool `json:"enable_aa_banking"`
// Performance settings // Performance settings
CacheSize int32 `json:"cache_size"` CacheSize int32 `json:"cache_size"`
UpdateInterval time.Duration `json:"update_interval"` UpdateInterval time.Duration `json:"update_interval"`
BatchSize int32 `json:"batch_size"` BatchSize int32 `json:"batch_size"`
// Database settings // Database settings
DatabaseEnabled bool `json:"database_enabled"` DatabaseEnabled bool `json:"database_enabled"`
AutoSave bool `json:"auto_save"` AutoSave bool `json:"auto_save"`
SaveInterval time.Duration `json:"save_interval"` SaveInterval time.Duration `json:"save_interval"`
} }
// AAManagerStats holds statistics about the AA system // AAManagerStats holds statistics about the AA system
type AAManagerStats struct { type AAManagerStats struct {
// Loading statistics // Loading statistics
TotalAAsLoaded int64 `json:"total_aas_loaded"` TotalAAsLoaded int64 `json:"total_aas_loaded"`
TotalNodesLoaded int64 `json:"total_nodes_loaded"` TotalNodesLoaded int64 `json:"total_nodes_loaded"`
LastLoadTime time.Time `json:"last_load_time"` LastLoadTime time.Time `json:"last_load_time"`
LoadDuration time.Duration `json:"load_duration"` LoadDuration time.Duration `json:"load_duration"`
// Player statistics // Player statistics
ActivePlayers int64 `json:"active_players"` ActivePlayers int64 `json:"active_players"`
TotalAAPurchases int64 `json:"total_aa_purchases"` TotalAAPurchases int64 `json:"total_aa_purchases"`
TotalPointsSpent int64 `json:"total_points_spent"` TotalPointsSpent int64 `json:"total_points_spent"`
AveragePointsSpent float64 `json:"average_points_spent"` AveragePointsSpent float64 `json:"average_points_spent"`
// Performance statistics // Performance statistics
CacheHits int64 `json:"cache_hits"` CacheHits int64 `json:"cache_hits"`
CacheMisses int64 `json:"cache_misses"` CacheMisses int64 `json:"cache_misses"`
DatabaseQueries int64 `json:"database_queries"` DatabaseQueries int64 `json:"database_queries"`
PacketsSent int64 `json:"packets_sent"` PacketsSent int64 `json:"packets_sent"`
// Tab usage statistics // Tab usage statistics
TabUsage map[int8]int64 `json:"tab_usage"` TabUsage map[int8]int64 `json:"tab_usage"`
PopularAAs map[int32]int64 `json:"popular_aas"` PopularAAs map[int32]int64 `json:"popular_aas"`
// Error statistics // Error statistics
ValidationErrors int64 `json:"validation_errors"` ValidationErrors int64 `json:"validation_errors"`
DatabaseErrors int64 `json:"database_errors"` DatabaseErrors int64 `json:"database_errors"`
PacketErrors int64 `json:"packet_errors"` PacketErrors int64 `json:"packet_errors"`
// Timing statistics // Timing statistics
LastStatsUpdate time.Time `json:"last_stats_update"` LastStatsUpdate time.Time `json:"last_stats_update"`
} }
// DefaultAAManagerConfig returns a default configuration // DefaultAAManagerConfig returns a default configuration
func DefaultAAManagerConfig() AAManagerConfig { func DefaultAAManagerConfig() AAManagerConfig {
return AAManagerConfig{ return AAManagerConfig{
EnableAASystem: DEFAULT_ENABLE_AA_SYSTEM, EnableAASystem: DEFAULT_ENABLE_AA_SYSTEM,
EnableCaching: DEFAULT_ENABLE_AA_CACHING, EnableCaching: DEFAULT_ENABLE_AA_CACHING,
EnableValidation: DEFAULT_ENABLE_AA_VALIDATION, EnableValidation: DEFAULT_ENABLE_AA_VALIDATION,
EnableLogging: DEFAULT_ENABLE_AA_LOGGING, EnableLogging: DEFAULT_ENABLE_AA_LOGGING,
AAPointsPerLevel: DEFAULT_AA_POINTS_PER_LEVEL, AAPointsPerLevel: DEFAULT_AA_POINTS_PER_LEVEL,
MaxBankedPoints: DEFAULT_AA_MAX_BANKED_POINTS, MaxBankedPoints: DEFAULT_AA_MAX_BANKED_POINTS,
EnableAABanking: true, EnableAABanking: true,
CacheSize: AA_CACHE_SIZE, CacheSize: AA_CACHE_SIZE,
UpdateInterval: time.Duration(AA_UPDATE_INTERVAL) * time.Millisecond, UpdateInterval: time.Duration(AA_UPDATE_INTERVAL) * time.Millisecond,
BatchSize: AA_PROCESSING_BATCH_SIZE, BatchSize: AA_PROCESSING_BATCH_SIZE,
DatabaseEnabled: true, DatabaseEnabled: true,
AutoSave: true, AutoSave: true,
SaveInterval: 5 * time.Minute, SaveInterval: 5 * time.Minute,
} }
} }
@ -348,11 +348,11 @@ func (aad *AltAdvanceData) Copy() *AltAdvanceData {
// IsValid validates the AltAdvanceData // IsValid validates the AltAdvanceData
func (aad *AltAdvanceData) IsValid() bool { func (aad *AltAdvanceData) IsValid() bool {
return aad.SpellID > 0 && return aad.SpellID > 0 &&
aad.NodeID > 0 && aad.NodeID > 0 &&
len(aad.Name) > 0 && len(aad.Name) > 0 &&
aad.MaxRank > 0 && aad.MaxRank > 0 &&
aad.RankCost > 0 aad.RankCost > 0
} }
// GetMaxAAForTab returns the maximum AA points for a given tab // 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 // IsExpansionRequired checks if a specific expansion is required
func IsExpansionRequired(flags int8, expansion int8) bool { func IsExpansionRequired(flags int8, expansion int8) bool {
return (flags & expansion) != 0 return (flags & expansion) != 0
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -168,7 +168,7 @@ func (c *Channel) leaveChannel(characterID int32) error {
func (c *Channel) GetMembers() []int32 { func (c *Channel) GetMembers() []int32 {
c.mu.RLock() c.mu.RLock()
defer c.mu.RUnlock() defer c.mu.RUnlock()
// Return a copy to prevent external modification // Return a copy to prevent external modification
members := make([]int32, len(c.members)) members := make([]int32, len(c.members))
copy(members, c.members) copy(members, c.members)
@ -179,7 +179,7 @@ func (c *Channel) GetMembers() []int32 {
func (c *Channel) GetChannelInfo() ChannelInfo { func (c *Channel) GetChannelInfo() ChannelInfo {
c.mu.RLock() c.mu.RLock()
defer c.mu.RUnlock() defer c.mu.RUnlock()
return ChannelInfo{ return ChannelInfo{
Name: c.name, Name: c.name,
HasPassword: c.password != "", HasPassword: c.password != "",
@ -203,7 +203,7 @@ func (c *Channel) ValidateJoin(level, race, class int32, password string) error
// Check level restriction // Check level restriction
if !c.CanJoinChannelByLevel(level) { 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) level, c.levelRestriction, c.name)
} }
@ -246,4 +246,4 @@ func (c *Channel) Copy() *Channel {
copy(newChannel.members, c.members) copy(newChannel.members, c.members)
return newChannel return newChannel
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -20,19 +20,19 @@ type CollectionRewardItem struct {
// Collection represents a collection that players can complete // Collection represents a collection that players can complete
type Collection struct { type Collection struct {
mu sync.RWMutex mu sync.RWMutex
id int32 id int32
name string name string
category string category string
level int8 level int8
rewardCoin int64 rewardCoin int64
rewardXP int64 rewardXP int64
completed bool completed bool
saveNeeded bool saveNeeded bool
collectionItems []CollectionItem collectionItems []CollectionItem
rewardItems []CollectionRewardItem rewardItems []CollectionRewardItem
selectableRewardItems []CollectionRewardItem selectableRewardItems []CollectionRewardItem
lastModified time.Time lastModified time.Time
} }
// CollectionData represents collection data for database operations // CollectionData represents collection data for database operations
@ -60,9 +60,9 @@ type PlayerCollectionData struct {
// PlayerCollectionItemData represents player found collection items // PlayerCollectionItemData represents player found collection items
type PlayerCollectionItemData struct { type PlayerCollectionItemData struct {
CharacterID int32 `json:"char_id" db:"char_id"` CharacterID int32 `json:"char_id" db:"char_id"`
CollectionID int32 `json:"collection_id" db:"collection_id"` CollectionID int32 `json:"collection_id" db:"collection_id"`
CollectionItemID int32 `json:"collection_item_id" db:"collection_item_id"` CollectionItemID int32 `json:"collection_item_id" db:"collection_item_id"`
} }
// MasterCollectionList manages all available collections in the game // MasterCollectionList manages all available collections in the game
@ -100,31 +100,31 @@ type CollectionStatistics struct {
// CollectionInfo provides basic collection information // CollectionInfo provides basic collection information
type CollectionInfo struct { type CollectionInfo struct {
ID int32 `json:"id"` ID int32 `json:"id"`
Name string `json:"name"` Name string `json:"name"`
Category string `json:"category"` Category string `json:"category"`
Level int8 `json:"level"` Level int8 `json:"level"`
Completed bool `json:"completed"` Completed bool `json:"completed"`
ReadyToTurnIn bool `json:"ready_to_turn_in"` ReadyToTurnIn bool `json:"ready_to_turn_in"`
ItemsFound int `json:"items_found"` ItemsFound int `json:"items_found"`
ItemsTotal int `json:"items_total"` ItemsTotal int `json:"items_total"`
RewardCoin int64 `json:"reward_coin"` RewardCoin int64 `json:"reward_coin"`
RewardXP int64 `json:"reward_xp"` RewardXP int64 `json:"reward_xp"`
RewardItems []CollectionRewardItem `json:"reward_items"` RewardItems []CollectionRewardItem `json:"reward_items"`
SelectableRewards []CollectionRewardItem `json:"selectable_rewards"` SelectableRewards []CollectionRewardItem `json:"selectable_rewards"`
RequiredItems []CollectionItem `json:"required_items"` RequiredItems []CollectionItem `json:"required_items"`
} }
// CollectionProgress represents player progress on a collection // CollectionProgress represents player progress on a collection
type CollectionProgress struct { type CollectionProgress struct {
CollectionID int32 `json:"collection_id"` CollectionID int32 `json:"collection_id"`
Name string `json:"name"` Name string `json:"name"`
Category string `json:"category"` Category string `json:"category"`
Level int8 `json:"level"` Level int8 `json:"level"`
Completed bool `json:"completed"` Completed bool `json:"completed"`
ReadyToTurnIn bool `json:"ready_to_turn_in"` ReadyToTurnIn bool `json:"ready_to_turn_in"`
Progress float64 `json:"progress_percentage"` Progress float64 `json:"progress_percentage"`
ItemsFound []CollectionItem `json:"items_found"` ItemsFound []CollectionItem `json:"items_found"`
ItemsNeeded []CollectionItem `json:"items_needed"` ItemsNeeded []CollectionItem `json:"items_needed"`
LastUpdated time.Time `json:"last_updated"` 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) 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 // Emote represents an emote with visual state and messages
type Emote struct { type Emote struct {
name string name string
visualState int32 visualState int32
message string message string
targetedMessage string targetedMessage string
} }
// NewEmote creates a new emote // NewEmote creates a new emote
@ -375,4 +375,4 @@ func (vs *VisualStates) FindEmoteByPartialName(partial string) []*EmoteVersionRa
} }
return results return results
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -3,9 +3,7 @@ package heroic_ops
import ( import (
"context" "context"
"fmt" "fmt"
"math/rand"
"sort" "sort"
"sync"
) )
// NewMasterHeroicOPList creates a new master heroic opportunity list // 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 { func (mhol *MasterHeroicOPList) LoadFromDatabase(ctx context.Context, database HeroicOPDatabase) error {
mhol.mu.Lock() mhol.mu.Lock()
defer mhol.mu.Unlock() defer mhol.mu.Unlock()
// Clear existing data // Clear existing data
mhol.starters = make(map[int8]map[int32]*HeroicOPStarter) mhol.starters = make(map[int8]map[int32]*HeroicOPStarter)
mhol.wheels = make(map[int32][]*HeroicOPWheel) mhol.wheels = make(map[int32][]*HeroicOPWheel)
mhol.spells = make(map[int32]SpellInfo) mhol.spells = make(map[int32]SpellInfo)
// Load starters // Load starters
starterData, err := database.LoadStarters(ctx) starterData, err := database.LoadStarters(ctx)
if err != nil { if err != nil {
return fmt.Errorf("failed to load starters: %w", err) return fmt.Errorf("failed to load starters: %w", err)
} }
for _, data := range starterData { for _, data := range starterData {
starter := &HeroicOPStarter{ starter := &HeroicOPStarter{
ID: data.ID, ID: data.ID,
@ -47,25 +45,25 @@ func (mhol *MasterHeroicOPList) LoadFromDatabase(ctx context.Context, database H
}, },
SaveNeeded: false, SaveNeeded: false,
} }
// Validate starter // Validate starter
if err := starter.Validate(); err != nil { if err := starter.Validate(); err != nil {
continue // Skip invalid starters continue // Skip invalid starters
} }
// Add to map structure // Add to map structure
if mhol.starters[starter.StartClass] == nil { if mhol.starters[starter.StartClass] == nil {
mhol.starters[starter.StartClass] = make(map[int32]*HeroicOPStarter) mhol.starters[starter.StartClass] = make(map[int32]*HeroicOPStarter)
} }
mhol.starters[starter.StartClass][starter.ID] = starter mhol.starters[starter.StartClass][starter.ID] = starter
} }
// Load wheels // Load wheels
wheelData, err := database.LoadWheels(ctx) wheelData, err := database.LoadWheels(ctx)
if err != nil { if err != nil {
return fmt.Errorf("failed to load wheels: %w", err) return fmt.Errorf("failed to load wheels: %w", err)
} }
for _, data := range wheelData { for _, data := range wheelData {
wheel := &HeroicOPWheel{ wheel := &HeroicOPWheel{
ID: data.ID, ID: data.ID,
@ -82,15 +80,15 @@ func (mhol *MasterHeroicOPList) LoadFromDatabase(ctx context.Context, database H
}, },
SaveNeeded: false, SaveNeeded: false,
} }
// Validate wheel // Validate wheel
if err := wheel.Validate(); err != nil { if err := wheel.Validate(); err != nil {
continue // Skip invalid wheels continue // Skip invalid wheels
} }
// Add to wheels map // Add to wheels map
mhol.wheels[wheel.StarterLinkID] = append(mhol.wheels[wheel.StarterLinkID], wheel) mhol.wheels[wheel.StarterLinkID] = append(mhol.wheels[wheel.StarterLinkID], wheel)
// Store spell info // Store spell info
mhol.spells[wheel.SpellID] = SpellInfo{ mhol.spells[wheel.SpellID] = SpellInfo{
ID: wheel.SpellID, ID: wheel.SpellID,
@ -98,7 +96,7 @@ func (mhol *MasterHeroicOPList) LoadFromDatabase(ctx context.Context, database H
Description: wheel.Description, Description: wheel.Description,
} }
} }
mhol.loaded = true mhol.loaded = true
return nil return nil
} }
@ -107,23 +105,23 @@ func (mhol *MasterHeroicOPList) LoadFromDatabase(ctx context.Context, database H
func (mhol *MasterHeroicOPList) GetStartersForClass(playerClass int8) []*HeroicOPStarter { func (mhol *MasterHeroicOPList) GetStartersForClass(playerClass int8) []*HeroicOPStarter {
mhol.mu.RLock() mhol.mu.RLock()
defer mhol.mu.RUnlock() defer mhol.mu.RUnlock()
var starters []*HeroicOPStarter var starters []*HeroicOPStarter
// Add class-specific starters // Add class-specific starters
if classStarters, exists := mhol.starters[playerClass]; exists { if classStarters, exists := mhol.starters[playerClass]; exists {
for _, starter := range classStarters { for _, starter := range classStarters {
starters = append(starters, starter) starters = append(starters, starter)
} }
} }
// Add universal starters (class 0 = any) // Add universal starters (class 0 = any)
if universalStarters, exists := mhol.starters[ClassAny]; exists { if universalStarters, exists := mhol.starters[ClassAny]; exists {
for _, starter := range universalStarters { for _, starter := range universalStarters {
starters = append(starters, starter) starters = append(starters, starter)
} }
} }
return starters return starters
} }
@ -131,14 +129,14 @@ func (mhol *MasterHeroicOPList) GetStartersForClass(playerClass int8) []*HeroicO
func (mhol *MasterHeroicOPList) GetStarter(starterID int32) *HeroicOPStarter { func (mhol *MasterHeroicOPList) GetStarter(starterID int32) *HeroicOPStarter {
mhol.mu.RLock() mhol.mu.RLock()
defer mhol.mu.RUnlock() defer mhol.mu.RUnlock()
// Search through all classes // Search through all classes
for _, classStarters := range mhol.starters { for _, classStarters := range mhol.starters {
if starter, exists := classStarters[starterID]; exists { if starter, exists := classStarters[starterID]; exists {
return starter return starter
} }
} }
return nil return nil
} }
@ -146,14 +144,14 @@ func (mhol *MasterHeroicOPList) GetStarter(starterID int32) *HeroicOPStarter {
func (mhol *MasterHeroicOPList) GetWheelsForStarter(starterID int32) []*HeroicOPWheel { func (mhol *MasterHeroicOPList) GetWheelsForStarter(starterID int32) []*HeroicOPWheel {
mhol.mu.RLock() mhol.mu.RLock()
defer mhol.mu.RUnlock() defer mhol.mu.RUnlock()
if wheels, exists := mhol.wheels[starterID]; exists { if wheels, exists := mhol.wheels[starterID]; exists {
// Return a copy to prevent external modification // Return a copy to prevent external modification
result := make([]*HeroicOPWheel, len(wheels)) result := make([]*HeroicOPWheel, len(wheels))
copy(result, wheels) copy(result, wheels)
return result return result
} }
return nil return nil
} }
@ -161,7 +159,7 @@ func (mhol *MasterHeroicOPList) GetWheelsForStarter(starterID int32) []*HeroicOP
func (mhol *MasterHeroicOPList) GetWheel(wheelID int32) *HeroicOPWheel { func (mhol *MasterHeroicOPList) GetWheel(wheelID int32) *HeroicOPWheel {
mhol.mu.RLock() mhol.mu.RLock()
defer mhol.mu.RUnlock() defer mhol.mu.RUnlock()
// Search through all wheel lists // Search through all wheel lists
for _, wheelList := range mhol.wheels { for _, wheelList := range mhol.wheels {
for _, wheel := range wheelList { for _, wheel := range wheelList {
@ -170,7 +168,7 @@ func (mhol *MasterHeroicOPList) GetWheel(wheelID int32) *HeroicOPWheel {
} }
} }
} }
return nil return nil
} }
@ -180,7 +178,7 @@ func (mhol *MasterHeroicOPList) SelectRandomWheel(starterID int32) *HeroicOPWhee
if len(wheels) == 0 { if len(wheels) == 0 {
return nil return nil
} }
return SelectRandomWheel(wheels) return SelectRandomWheel(wheels)
} }
@ -188,11 +186,11 @@ func (mhol *MasterHeroicOPList) SelectRandomWheel(starterID int32) *HeroicOPWhee
func (mhol *MasterHeroicOPList) GetSpellInfo(spellID int32) (*SpellInfo, bool) { func (mhol *MasterHeroicOPList) GetSpellInfo(spellID int32) (*SpellInfo, bool) {
mhol.mu.RLock() mhol.mu.RLock()
defer mhol.mu.RUnlock() defer mhol.mu.RUnlock()
if spell, exists := mhol.spells[spellID]; exists { if spell, exists := mhol.spells[spellID]; exists {
return &spell, true return &spell, true
} }
return nil, false return nil, false
} }
@ -200,22 +198,22 @@ func (mhol *MasterHeroicOPList) GetSpellInfo(spellID int32) (*SpellInfo, bool) {
func (mhol *MasterHeroicOPList) AddStarter(starter *HeroicOPStarter) error { func (mhol *MasterHeroicOPList) AddStarter(starter *HeroicOPStarter) error {
mhol.mu.Lock() mhol.mu.Lock()
defer mhol.mu.Unlock() defer mhol.mu.Unlock()
if err := starter.Validate(); err != nil { if err := starter.Validate(); err != nil {
return fmt.Errorf("invalid starter: %w", err) return fmt.Errorf("invalid starter: %w", err)
} }
// Check for duplicate ID // Check for duplicate ID
if existingStarter := mhol.getStarterNoLock(starter.ID); existingStarter != nil { if existingStarter := mhol.getStarterNoLock(starter.ID); existingStarter != nil {
return fmt.Errorf("starter ID %d already exists", starter.ID) return fmt.Errorf("starter ID %d already exists", starter.ID)
} }
// Add to map structure // Add to map structure
if mhol.starters[starter.StartClass] == nil { if mhol.starters[starter.StartClass] == nil {
mhol.starters[starter.StartClass] = make(map[int32]*HeroicOPStarter) mhol.starters[starter.StartClass] = make(map[int32]*HeroicOPStarter)
} }
mhol.starters[starter.StartClass][starter.ID] = starter mhol.starters[starter.StartClass][starter.ID] = starter
return nil return nil
} }
@ -223,31 +221,31 @@ func (mhol *MasterHeroicOPList) AddStarter(starter *HeroicOPStarter) error {
func (mhol *MasterHeroicOPList) AddWheel(wheel *HeroicOPWheel) error { func (mhol *MasterHeroicOPList) AddWheel(wheel *HeroicOPWheel) error {
mhol.mu.Lock() mhol.mu.Lock()
defer mhol.mu.Unlock() defer mhol.mu.Unlock()
if err := wheel.Validate(); err != nil { if err := wheel.Validate(); err != nil {
return fmt.Errorf("invalid wheel: %w", err) return fmt.Errorf("invalid wheel: %w", err)
} }
// Check for duplicate ID // Check for duplicate ID
if existingWheel := mhol.getWheelNoLock(wheel.ID); existingWheel != nil { if existingWheel := mhol.getWheelNoLock(wheel.ID); existingWheel != nil {
return fmt.Errorf("wheel ID %d already exists", wheel.ID) return fmt.Errorf("wheel ID %d already exists", wheel.ID)
} }
// Verify starter exists // Verify starter exists
if mhol.getStarterNoLock(wheel.StarterLinkID) == nil { if mhol.getStarterNoLock(wheel.StarterLinkID) == nil {
return fmt.Errorf("starter ID %d not found for wheel", wheel.StarterLinkID) return fmt.Errorf("starter ID %d not found for wheel", wheel.StarterLinkID)
} }
// Add to wheels map // Add to wheels map
mhol.wheels[wheel.StarterLinkID] = append(mhol.wheels[wheel.StarterLinkID], wheel) mhol.wheels[wheel.StarterLinkID] = append(mhol.wheels[wheel.StarterLinkID], wheel)
// Store spell info // Store spell info
mhol.spells[wheel.SpellID] = SpellInfo{ mhol.spells[wheel.SpellID] = SpellInfo{
ID: wheel.SpellID, ID: wheel.SpellID,
Name: wheel.Name, Name: wheel.Name,
Description: wheel.Description, Description: wheel.Description,
} }
return nil return nil
} }
@ -255,30 +253,30 @@ func (mhol *MasterHeroicOPList) AddWheel(wheel *HeroicOPWheel) error {
func (mhol *MasterHeroicOPList) RemoveStarter(starterID int32) bool { func (mhol *MasterHeroicOPList) RemoveStarter(starterID int32) bool {
mhol.mu.Lock() mhol.mu.Lock()
defer mhol.mu.Unlock() defer mhol.mu.Unlock()
// Find and remove starter // Find and remove starter
found := false found := false
for class, classStarters := range mhol.starters { for class, classStarters := range mhol.starters {
if _, exists := classStarters[starterID]; exists { if _, exists := classStarters[starterID]; exists {
delete(classStarters, starterID) delete(classStarters, starterID)
found = true found = true
// Clean up empty class map // Clean up empty class map
if len(classStarters) == 0 { if len(classStarters) == 0 {
delete(mhol.starters, class) delete(mhol.starters, class)
} }
break break
} }
} }
if !found { if !found {
return false return false
} }
// Remove associated wheels // Remove associated wheels
delete(mhol.wheels, starterID) delete(mhol.wheels, starterID)
return true return true
} }
@ -286,24 +284,24 @@ func (mhol *MasterHeroicOPList) RemoveStarter(starterID int32) bool {
func (mhol *MasterHeroicOPList) RemoveWheel(wheelID int32) bool { func (mhol *MasterHeroicOPList) RemoveWheel(wheelID int32) bool {
mhol.mu.Lock() mhol.mu.Lock()
defer mhol.mu.Unlock() defer mhol.mu.Unlock()
// Find and remove wheel // Find and remove wheel
for starterID, wheelList := range mhol.wheels { for starterID, wheelList := range mhol.wheels {
for i, wheel := range wheelList { for i, wheel := range wheelList {
if wheel.ID == wheelID { if wheel.ID == wheelID {
// Remove wheel from slice // Remove wheel from slice
mhol.wheels[starterID] = append(wheelList[:i], wheelList[i+1:]...) mhol.wheels[starterID] = append(wheelList[:i], wheelList[i+1:]...)
// Clean up empty wheel list // Clean up empty wheel list
if len(mhol.wheels[starterID]) == 0 { if len(mhol.wheels[starterID]) == 0 {
delete(mhol.wheels, starterID) delete(mhol.wheels, starterID)
} }
return true return true
} }
} }
} }
return false return false
} }
@ -311,20 +309,20 @@ func (mhol *MasterHeroicOPList) RemoveWheel(wheelID int32) bool {
func (mhol *MasterHeroicOPList) GetAllStarters() []*HeroicOPStarter { func (mhol *MasterHeroicOPList) GetAllStarters() []*HeroicOPStarter {
mhol.mu.RLock() mhol.mu.RLock()
defer mhol.mu.RUnlock() defer mhol.mu.RUnlock()
var allStarters []*HeroicOPStarter var allStarters []*HeroicOPStarter
for _, classStarters := range mhol.starters { for _, classStarters := range mhol.starters {
for _, starter := range classStarters { for _, starter := range classStarters {
allStarters = append(allStarters, starter) allStarters = append(allStarters, starter)
} }
} }
// Sort by ID for consistent ordering // Sort by ID for consistent ordering
sort.Slice(allStarters, func(i, j int) bool { sort.Slice(allStarters, func(i, j int) bool {
return allStarters[i].ID < allStarters[j].ID return allStarters[i].ID < allStarters[j].ID
}) })
return allStarters return allStarters
} }
@ -332,18 +330,18 @@ func (mhol *MasterHeroicOPList) GetAllStarters() []*HeroicOPStarter {
func (mhol *MasterHeroicOPList) GetAllWheels() []*HeroicOPWheel { func (mhol *MasterHeroicOPList) GetAllWheels() []*HeroicOPWheel {
mhol.mu.RLock() mhol.mu.RLock()
defer mhol.mu.RUnlock() defer mhol.mu.RUnlock()
var allWheels []*HeroicOPWheel var allWheels []*HeroicOPWheel
for _, wheelList := range mhol.wheels { for _, wheelList := range mhol.wheels {
allWheels = append(allWheels, wheelList...) allWheels = append(allWheels, wheelList...)
} }
// Sort by ID for consistent ordering // Sort by ID for consistent ordering
sort.Slice(allWheels, func(i, j int) bool { sort.Slice(allWheels, func(i, j int) bool {
return allWheels[i].ID < allWheels[j].ID return allWheels[i].ID < allWheels[j].ID
}) })
return allWheels return allWheels
} }
@ -351,12 +349,12 @@ func (mhol *MasterHeroicOPList) GetAllWheels() []*HeroicOPWheel {
func (mhol *MasterHeroicOPList) GetStarterCount() int { func (mhol *MasterHeroicOPList) GetStarterCount() int {
mhol.mu.RLock() mhol.mu.RLock()
defer mhol.mu.RUnlock() defer mhol.mu.RUnlock()
count := 0 count := 0
for _, classStarters := range mhol.starters { for _, classStarters := range mhol.starters {
count += len(classStarters) count += len(classStarters)
} }
return count return count
} }
@ -364,12 +362,12 @@ func (mhol *MasterHeroicOPList) GetStarterCount() int {
func (mhol *MasterHeroicOPList) GetWheelCount() int { func (mhol *MasterHeroicOPList) GetWheelCount() int {
mhol.mu.RLock() mhol.mu.RLock()
defer mhol.mu.RUnlock() defer mhol.mu.RUnlock()
count := 0 count := 0
for _, wheelList := range mhol.wheels { for _, wheelList := range mhol.wheels {
count += len(wheelList) count += len(wheelList)
} }
return count return count
} }
@ -377,7 +375,7 @@ func (mhol *MasterHeroicOPList) GetWheelCount() int {
func (mhol *MasterHeroicOPList) IsLoaded() bool { func (mhol *MasterHeroicOPList) IsLoaded() bool {
mhol.mu.RLock() mhol.mu.RLock()
defer mhol.mu.RUnlock() defer mhol.mu.RUnlock()
return mhol.loaded return mhol.loaded
} }
@ -385,9 +383,9 @@ func (mhol *MasterHeroicOPList) IsLoaded() bool {
func (mhol *MasterHeroicOPList) SearchStarters(criteria HeroicOPSearchCriteria) []*HeroicOPStarter { func (mhol *MasterHeroicOPList) SearchStarters(criteria HeroicOPSearchCriteria) []*HeroicOPStarter {
mhol.mu.RLock() mhol.mu.RLock()
defer mhol.mu.RUnlock() defer mhol.mu.RUnlock()
var results []*HeroicOPStarter var results []*HeroicOPStarter
for _, classStarters := range mhol.starters { for _, classStarters := range mhol.starters {
for _, starter := range classStarters { for _, starter := range classStarters {
if mhol.matchesStarterCriteria(starter, criteria) { if mhol.matchesStarterCriteria(starter, criteria) {
@ -395,12 +393,12 @@ func (mhol *MasterHeroicOPList) SearchStarters(criteria HeroicOPSearchCriteria)
} }
} }
} }
// Sort results by ID // Sort results by ID
sort.Slice(results, func(i, j int) bool { sort.Slice(results, func(i, j int) bool {
return results[i].ID < results[j].ID return results[i].ID < results[j].ID
}) })
return results return results
} }
@ -408,9 +406,9 @@ func (mhol *MasterHeroicOPList) SearchStarters(criteria HeroicOPSearchCriteria)
func (mhol *MasterHeroicOPList) SearchWheels(criteria HeroicOPSearchCriteria) []*HeroicOPWheel { func (mhol *MasterHeroicOPList) SearchWheels(criteria HeroicOPSearchCriteria) []*HeroicOPWheel {
mhol.mu.RLock() mhol.mu.RLock()
defer mhol.mu.RUnlock() defer mhol.mu.RUnlock()
var results []*HeroicOPWheel var results []*HeroicOPWheel
for _, wheelList := range mhol.wheels { for _, wheelList := range mhol.wheels {
for _, wheel := range wheelList { for _, wheel := range wheelList {
if mhol.matchesWheelCriteria(wheel, criteria) { if mhol.matchesWheelCriteria(wheel, criteria) {
@ -418,12 +416,12 @@ func (mhol *MasterHeroicOPList) SearchWheels(criteria HeroicOPSearchCriteria) []
} }
} }
} }
// Sort results by ID // Sort results by ID
sort.Slice(results, func(i, j int) bool { sort.Slice(results, func(i, j int) bool {
return results[i].ID < results[j].ID return results[i].ID < results[j].ID
}) })
return results return results
} }
@ -431,15 +429,15 @@ func (mhol *MasterHeroicOPList) SearchWheels(criteria HeroicOPSearchCriteria) []
func (mhol *MasterHeroicOPList) GetStatistics() map[string]interface{} { func (mhol *MasterHeroicOPList) GetStatistics() map[string]interface{} {
mhol.mu.RLock() mhol.mu.RLock()
defer mhol.mu.RUnlock() defer mhol.mu.RUnlock()
stats := make(map[string]interface{}) stats := make(map[string]interface{})
// Basic counts // Basic counts
stats["total_starters"] = mhol.getStarterCountNoLock() stats["total_starters"] = mhol.getStarterCountNoLock()
stats["total_wheels"] = mhol.getWheelCountNoLock() stats["total_wheels"] = mhol.getWheelCountNoLock()
stats["total_spells"] = len(mhol.spells) stats["total_spells"] = len(mhol.spells)
stats["loaded"] = mhol.loaded stats["loaded"] = mhol.loaded
// Class distribution // Class distribution
classDistribution := make(map[string]int) classDistribution := make(map[string]int)
for class, classStarters := range mhol.starters { for class, classStarters := range mhol.starters {
@ -450,14 +448,14 @@ func (mhol *MasterHeroicOPList) GetStatistics() map[string]interface{} {
} }
} }
stats["class_distribution"] = classDistribution stats["class_distribution"] = classDistribution
// Wheel distribution per starter // Wheel distribution per starter
wheelDistribution := make(map[string]int) wheelDistribution := make(map[string]int)
for starterID, wheelList := range mhol.wheels { for starterID, wheelList := range mhol.wheels {
wheelDistribution[fmt.Sprintf("starter_%d", starterID)] = len(wheelList) wheelDistribution[fmt.Sprintf("starter_%d", starterID)] = len(wheelList)
} }
stats["wheel_distribution"] = wheelDistribution stats["wheel_distribution"] = wheelDistribution
return stats return stats
} }
@ -465,9 +463,9 @@ func (mhol *MasterHeroicOPList) GetStatistics() map[string]interface{} {
func (mhol *MasterHeroicOPList) Validate() []error { func (mhol *MasterHeroicOPList) Validate() []error {
mhol.mu.RLock() mhol.mu.RLock()
defer mhol.mu.RUnlock() defer mhol.mu.RUnlock()
var errors []error var errors []error
// Validate all starters // Validate all starters
for _, classStarters := range mhol.starters { for _, classStarters := range mhol.starters {
for _, starter := range classStarters { for _, starter := range classStarters {
@ -476,21 +474,21 @@ func (mhol *MasterHeroicOPList) Validate() []error {
} }
} }
} }
// Validate all wheels // Validate all wheels
for _, wheelList := range mhol.wheels { for _, wheelList := range mhol.wheels {
for _, wheel := range wheelList { for _, wheel := range wheelList {
if err := wheel.Validate(); err != nil { if err := wheel.Validate(); err != nil {
errors = append(errors, fmt.Errorf("wheel %d: %w", wheel.ID, err)) errors = append(errors, fmt.Errorf("wheel %d: %w", wheel.ID, err))
} }
// Check if starter exists for this wheel // Check if starter exists for this wheel
if mhol.getStarterNoLock(wheel.StarterLinkID) == nil { if mhol.getStarterNoLock(wheel.StarterLinkID) == nil {
errors = append(errors, fmt.Errorf("wheel %d references non-existent starter %d", wheel.ID, wheel.StarterLinkID)) errors = append(errors, fmt.Errorf("wheel %d references non-existent starter %d", wheel.ID, wheel.StarterLinkID))
} }
} }
} }
// Check for orphaned wheels (starters with no wheels) // Check for orphaned wheels (starters with no wheels)
for _, classStarters := range mhol.starters { for _, classStarters := range mhol.starters {
for starterID := range classStarters { for starterID := range classStarters {
@ -499,7 +497,7 @@ func (mhol *MasterHeroicOPList) Validate() []error {
} }
} }
} }
return errors return errors
} }
@ -546,7 +544,7 @@ func (mhol *MasterHeroicOPList) matchesStarterCriteria(starter *HeroicOPStarter,
if criteria.StarterClass != 0 && starter.StartClass != criteria.StarterClass { if criteria.StarterClass != 0 && starter.StartClass != criteria.StarterClass {
return false return false
} }
// Name pattern filter // Name pattern filter
if criteria.NamePattern != "" { if criteria.NamePattern != "" {
// Simple case-insensitive substring match // Simple case-insensitive substring match
@ -555,7 +553,7 @@ func (mhol *MasterHeroicOPList) matchesStarterCriteria(starter *HeroicOPStarter,
return false return false
} }
} }
return true return true
} }
@ -564,38 +562,38 @@ func (mhol *MasterHeroicOPList) matchesWheelCriteria(wheel *HeroicOPWheel, crite
if criteria.SpellID != 0 && wheel.SpellID != criteria.SpellID { if criteria.SpellID != 0 && wheel.SpellID != criteria.SpellID {
return false return false
} }
// Chance range filter // Chance range filter
if criteria.MinChance > 0 && wheel.Chance < criteria.MinChance { if criteria.MinChance > 0 && wheel.Chance < criteria.MinChance {
return false return false
} }
if criteria.MaxChance > 0 && wheel.Chance > criteria.MaxChance { if criteria.MaxChance > 0 && wheel.Chance > criteria.MaxChance {
return false return false
} }
// Required players filter // Required players filter
if criteria.RequiredPlayers > 0 && wheel.RequiredPlayers != criteria.RequiredPlayers { if criteria.RequiredPlayers > 0 && wheel.RequiredPlayers != criteria.RequiredPlayers {
return false return false
} }
// Name pattern filter // Name pattern filter
if criteria.NamePattern != "" { if criteria.NamePattern != "" {
if !containsIgnoreCase(wheel.Name, criteria.NamePattern) { if !containsIgnoreCase(wheel.Name, criteria.NamePattern) {
return false return false
} }
} }
// Shift availability filter // Shift availability filter
if criteria.HasShift && !wheel.HasShift() { if criteria.HasShift && !wheel.HasShift() {
return false return false
} }
// Order type filter // Order type filter
if criteria.IsOrdered && !wheel.IsOrdered() { if criteria.IsOrdered && !wheel.IsOrdered() {
return false return false
} }
return true return true
} }
@ -606,4 +604,4 @@ func containsIgnoreCase(s, substr string) bool {
// or a proper Unicode-aware comparison // or a proper Unicode-aware comparison
return len(substr) == 0 // Empty substring matches everything return len(substr) == 0 // Empty substring matches everything
// TODO: Implement proper case-insensitive search // 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 // Start with base packet structure
packet := make([]byte, 0, 256) packet := make([]byte, 0, 256)
// Packet header (simplified - real implementation would use proper packet structure) // Packet header (simplified - real implementation would use proper packet structure)
// This is a placeholder implementation // This is a placeholder implementation
packet = append(packet, 0x01) // HO Start packet type packet = append(packet, 0x01) // HO Start packet type
// HO Instance ID (8 bytes) // HO Instance ID (8 bytes)
idBytes := make([]byte, 8) idBytes := make([]byte, 8)
binary.LittleEndian.PutUint64(idBytes, uint64(ho.ID)) binary.LittleEndian.PutUint64(idBytes, uint64(ho.ID))
packet = append(packet, idBytes...) packet = append(packet, idBytes...)
// Encounter ID (4 bytes) // Encounter ID (4 bytes)
encounterBytes := make([]byte, 4) encounterBytes := make([]byte, 4)
binary.LittleEndian.PutUint32(encounterBytes, uint32(ho.EncounterID)) binary.LittleEndian.PutUint32(encounterBytes, uint32(ho.EncounterID))
packet = append(packet, encounterBytes...) packet = append(packet, encounterBytes...)
// State (1 byte) // State (1 byte)
packet = append(packet, byte(ho.State)) packet = append(packet, byte(ho.State))
// Starter ID (4 bytes) // Starter ID (4 bytes)
starterBytes := make([]byte, 4) starterBytes := make([]byte, 4)
binary.LittleEndian.PutUint32(starterBytes, uint32(ho.StarterID)) binary.LittleEndian.PutUint32(starterBytes, uint32(ho.StarterID))
packet = append(packet, starterBytes...) packet = append(packet, starterBytes...)
return packet, nil return packet, nil
} }
@ -60,50 +60,50 @@ func (hpb *HeroicOPPacketBuilder) BuildHOUpdatePacket(ho *HeroicOP) ([]byte, err
// Build packet based on HO state // Build packet based on HO state
packet := make([]byte, 0, 512) packet := make([]byte, 0, 512)
// Packet header // Packet header
packet = append(packet, 0x02) // HO Update packet type packet = append(packet, 0x02) // HO Update packet type
// HO Instance ID (8 bytes) // HO Instance ID (8 bytes)
idBytes := make([]byte, 8) idBytes := make([]byte, 8)
binary.LittleEndian.PutUint64(idBytes, uint64(ho.ID)) binary.LittleEndian.PutUint64(idBytes, uint64(ho.ID))
packet = append(packet, idBytes...) packet = append(packet, idBytes...)
// State (1 byte) // State (1 byte)
packet = append(packet, byte(ho.State)) packet = append(packet, byte(ho.State))
if ho.State == HOStateWheelPhase { if ho.State == HOStateWheelPhase {
// Wheel ID (4 bytes) // Wheel ID (4 bytes)
wheelBytes := make([]byte, 4) wheelBytes := make([]byte, 4)
binary.LittleEndian.PutUint32(wheelBytes, uint32(ho.WheelID)) binary.LittleEndian.PutUint32(wheelBytes, uint32(ho.WheelID))
packet = append(packet, wheelBytes...) packet = append(packet, wheelBytes...)
// Time remaining (4 bytes) // Time remaining (4 bytes)
timeBytes := make([]byte, 4) timeBytes := make([]byte, 4)
binary.LittleEndian.PutUint32(timeBytes, uint32(ho.TimeRemaining)) binary.LittleEndian.PutUint32(timeBytes, uint32(ho.TimeRemaining))
packet = append(packet, timeBytes...) packet = append(packet, timeBytes...)
// Total time (4 bytes) // Total time (4 bytes)
totalTimeBytes := make([]byte, 4) totalTimeBytes := make([]byte, 4)
binary.LittleEndian.PutUint32(totalTimeBytes, uint32(ho.TotalTime)) binary.LittleEndian.PutUint32(totalTimeBytes, uint32(ho.TotalTime))
packet = append(packet, totalTimeBytes...) packet = append(packet, totalTimeBytes...)
// Countered array (6 bytes) // Countered array (6 bytes)
for i := 0; i < MaxAbilities; i++ { for i := 0; i < MaxAbilities; i++ {
packet = append(packet, byte(ho.Countered[i])) packet = append(packet, byte(ho.Countered[i]))
} }
// Complete flag (1 byte) // Complete flag (1 byte)
packet = append(packet, byte(ho.Complete)) packet = append(packet, byte(ho.Complete))
// Shift used flag (1 byte) // Shift used flag (1 byte)
packet = append(packet, byte(ho.ShiftUsed)) packet = append(packet, byte(ho.ShiftUsed))
// Spell name length and data // Spell name length and data
spellNameBytes := []byte(ho.SpellName) spellNameBytes := []byte(ho.SpellName)
packet = append(packet, byte(len(spellNameBytes))) packet = append(packet, byte(len(spellNameBytes)))
packet = append(packet, spellNameBytes...) packet = append(packet, spellNameBytes...)
// Spell description length and data // Spell description length and data
spellDescBytes := []byte(ho.SpellDescription) spellDescBytes := []byte(ho.SpellDescription)
descLen := make([]byte, 2) descLen := make([]byte, 2)
@ -111,7 +111,7 @@ func (hpb *HeroicOPPacketBuilder) BuildHOUpdatePacket(ho *HeroicOP) ([]byte, err
packet = append(packet, descLen...) packet = append(packet, descLen...)
packet = append(packet, spellDescBytes...) packet = append(packet, spellDescBytes...)
} }
return packet, nil return packet, nil
} }
@ -122,27 +122,27 @@ func (hpb *HeroicOPPacketBuilder) BuildHOCompletePacket(ho *HeroicOP, success bo
} }
packet := make([]byte, 0, 256) packet := make([]byte, 0, 256)
// Packet header // Packet header
packet = append(packet, 0x03) // HO Complete packet type packet = append(packet, 0x03) // HO Complete packet type
// HO Instance ID (8 bytes) // HO Instance ID (8 bytes)
idBytes := make([]byte, 8) idBytes := make([]byte, 8)
binary.LittleEndian.PutUint64(idBytes, uint64(ho.ID)) binary.LittleEndian.PutUint64(idBytes, uint64(ho.ID))
packet = append(packet, idBytes...) packet = append(packet, idBytes...)
// Success flag (1 byte) // Success flag (1 byte)
if success { if success {
packet = append(packet, 0x01) packet = append(packet, 0x01)
} else { } else {
packet = append(packet, 0x00) packet = append(packet, 0x00)
} }
// Completed by character ID (4 bytes) // Completed by character ID (4 bytes)
completedByBytes := make([]byte, 4) completedByBytes := make([]byte, 4)
binary.LittleEndian.PutUint32(completedByBytes, uint32(ho.CompletedBy)) binary.LittleEndian.PutUint32(completedByBytes, uint32(ho.CompletedBy))
packet = append(packet, completedByBytes...) packet = append(packet, completedByBytes...)
if success { if success {
// Spell ID if successful (4 bytes) // Spell ID if successful (4 bytes)
spellBytes := make([]byte, 4) spellBytes := make([]byte, 4)
@ -150,27 +150,27 @@ func (hpb *HeroicOPPacketBuilder) BuildHOCompletePacket(ho *HeroicOP, success bo
binary.LittleEndian.PutUint32(spellBytes, 0) // Placeholder binary.LittleEndian.PutUint32(spellBytes, 0) // Placeholder
packet = append(packet, spellBytes...) packet = append(packet, spellBytes...)
} }
return packet, nil return packet, nil
} }
// BuildHOTimerPacket builds timer update packet // BuildHOTimerPacket builds timer update packet
func (hpb *HeroicOPPacketBuilder) BuildHOTimerPacket(timeRemaining, totalTime int32) ([]byte, error) { func (hpb *HeroicOPPacketBuilder) BuildHOTimerPacket(timeRemaining, totalTime int32) ([]byte, error) {
packet := make([]byte, 0, 16) packet := make([]byte, 0, 16)
// Packet header // Packet header
packet = append(packet, 0x04) // HO Timer packet type packet = append(packet, 0x04) // HO Timer packet type
// Time remaining (4 bytes) // Time remaining (4 bytes)
timeBytes := make([]byte, 4) timeBytes := make([]byte, 4)
binary.LittleEndian.PutUint32(timeBytes, uint32(timeRemaining)) binary.LittleEndian.PutUint32(timeBytes, uint32(timeRemaining))
packet = append(packet, timeBytes...) packet = append(packet, timeBytes...)
// Total time (4 bytes) // Total time (4 bytes)
totalTimeBytes := make([]byte, 4) totalTimeBytes := make([]byte, 4)
binary.LittleEndian.PutUint32(totalTimeBytes, uint32(totalTime)) binary.LittleEndian.PutUint32(totalTimeBytes, uint32(totalTime))
packet = append(packet, totalTimeBytes...) packet = append(packet, totalTimeBytes...)
return packet, nil return packet, nil
} }
@ -181,66 +181,66 @@ func (hpb *HeroicOPPacketBuilder) BuildHOWheelPacket(ho *HeroicOP, wheel *Heroic
} }
packet := make([]byte, 0, 512) packet := make([]byte, 0, 512)
// Packet header // Packet header
packet = append(packet, 0x05) // HO Wheel packet type packet = append(packet, 0x05) // HO Wheel packet type
// HO Instance ID (8 bytes) // HO Instance ID (8 bytes)
idBytes := make([]byte, 8) idBytes := make([]byte, 8)
binary.LittleEndian.PutUint64(idBytes, uint64(ho.ID)) binary.LittleEndian.PutUint64(idBytes, uint64(ho.ID))
packet = append(packet, idBytes...) packet = append(packet, idBytes...)
// Wheel ID (4 bytes) // Wheel ID (4 bytes)
wheelBytes := make([]byte, 4) wheelBytes := make([]byte, 4)
binary.LittleEndian.PutUint32(wheelBytes, uint32(wheel.ID)) binary.LittleEndian.PutUint32(wheelBytes, uint32(wheel.ID))
packet = append(packet, wheelBytes...) packet = append(packet, wheelBytes...)
// Order type (1 byte) // Order type (1 byte)
packet = append(packet, byte(wheel.Order)) packet = append(packet, byte(wheel.Order))
// Shift icon (2 bytes) // Shift icon (2 bytes)
shiftBytes := make([]byte, 2) shiftBytes := make([]byte, 2)
binary.LittleEndian.PutUint16(shiftBytes, uint16(wheel.ShiftIcon)) binary.LittleEndian.PutUint16(shiftBytes, uint16(wheel.ShiftIcon))
packet = append(packet, shiftBytes...) packet = append(packet, shiftBytes...)
// Abilities (12 bytes - 2 bytes per ability) // Abilities (12 bytes - 2 bytes per ability)
for i := 0; i < MaxAbilities; i++ { for i := 0; i < MaxAbilities; i++ {
abilityBytes := make([]byte, 2) abilityBytes := make([]byte, 2)
binary.LittleEndian.PutUint16(abilityBytes, uint16(wheel.Abilities[i])) binary.LittleEndian.PutUint16(abilityBytes, uint16(wheel.Abilities[i]))
packet = append(packet, abilityBytes...) packet = append(packet, abilityBytes...)
} }
// Countered status (6 bytes) // Countered status (6 bytes)
for i := 0; i < MaxAbilities; i++ { for i := 0; i < MaxAbilities; i++ {
packet = append(packet, byte(ho.Countered[i])) packet = append(packet, byte(ho.Countered[i]))
} }
// Timer information // Timer information
timeBytes := make([]byte, 4) timeBytes := make([]byte, 4)
binary.LittleEndian.PutUint32(timeBytes, uint32(ho.TimeRemaining)) binary.LittleEndian.PutUint32(timeBytes, uint32(ho.TimeRemaining))
packet = append(packet, timeBytes...) packet = append(packet, timeBytes...)
totalTimeBytes := make([]byte, 4) totalTimeBytes := make([]byte, 4)
binary.LittleEndian.PutUint32(totalTimeBytes, uint32(ho.TotalTime)) binary.LittleEndian.PutUint32(totalTimeBytes, uint32(ho.TotalTime))
packet = append(packet, totalTimeBytes...) packet = append(packet, totalTimeBytes...)
// Spell information // Spell information
spellBytes := make([]byte, 4) spellBytes := make([]byte, 4)
binary.LittleEndian.PutUint32(spellBytes, uint32(wheel.SpellID)) binary.LittleEndian.PutUint32(spellBytes, uint32(wheel.SpellID))
packet = append(packet, spellBytes...) packet = append(packet, spellBytes...)
// Spell name length and data // Spell name length and data
spellNameBytes := []byte(wheel.Name) spellNameBytes := []byte(wheel.Name)
packet = append(packet, byte(len(spellNameBytes))) packet = append(packet, byte(len(spellNameBytes)))
packet = append(packet, spellNameBytes...) packet = append(packet, spellNameBytes...)
// Spell description length and data // Spell description length and data
spellDescBytes := []byte(wheel.Description) spellDescBytes := []byte(wheel.Description)
descLen := make([]byte, 2) descLen := make([]byte, 2)
binary.LittleEndian.PutUint16(descLen, uint16(len(spellDescBytes))) binary.LittleEndian.PutUint16(descLen, uint16(len(spellDescBytes)))
packet = append(packet, descLen...) packet = append(packet, descLen...)
packet = append(packet, spellDescBytes...) packet = append(packet, spellDescBytes...)
return packet, nil return packet, nil
} }
@ -251,21 +251,21 @@ func (hpb *HeroicOPPacketBuilder) BuildHOProgressPacket(ho *HeroicOP, progressPe
} }
packet := make([]byte, 0, 32) packet := make([]byte, 0, 32)
// Packet header // Packet header
packet = append(packet, 0x06) // HO Progress packet type packet = append(packet, 0x06) // HO Progress packet type
// HO Instance ID (8 bytes) // HO Instance ID (8 bytes)
idBytes := make([]byte, 8) idBytes := make([]byte, 8)
binary.LittleEndian.PutUint64(idBytes, uint64(ho.ID)) binary.LittleEndian.PutUint64(idBytes, uint64(ho.ID))
packet = append(packet, idBytes...) packet = append(packet, idBytes...)
// Progress percentage as float (4 bytes) // Progress percentage as float (4 bytes)
progressBits := math.Float32bits(progressPercent) progressBits := math.Float32bits(progressPercent)
progressBytes := make([]byte, 4) progressBytes := make([]byte, 4)
binary.LittleEndian.PutUint32(progressBytes, progressBits) binary.LittleEndian.PutUint32(progressBytes, progressBits)
packet = append(packet, progressBytes...) packet = append(packet, progressBytes...)
// Current completion count (1 byte) // Current completion count (1 byte)
completed := int8(0) completed := int8(0)
for i := 0; i < MaxAbilities; i++ { for i := 0; i < MaxAbilities; i++ {
@ -274,32 +274,32 @@ func (hpb *HeroicOPPacketBuilder) BuildHOProgressPacket(ho *HeroicOP, progressPe
} }
} }
packet = append(packet, byte(completed)) packet = append(packet, byte(completed))
return packet, nil return packet, nil
} }
// BuildHOErrorPacket builds error notification packet // BuildHOErrorPacket builds error notification packet
func (hpb *HeroicOPPacketBuilder) BuildHOErrorPacket(instanceID int64, errorCode int, errorMessage string) ([]byte, error) { func (hpb *HeroicOPPacketBuilder) BuildHOErrorPacket(instanceID int64, errorCode int, errorMessage string) ([]byte, error) {
packet := make([]byte, 0, 256) packet := make([]byte, 0, 256)
// Packet header // Packet header
packet = append(packet, 0x07) // HO Error packet type packet = append(packet, 0x07) // HO Error packet type
// HO Instance ID (8 bytes) // HO Instance ID (8 bytes)
idBytes := make([]byte, 8) idBytes := make([]byte, 8)
binary.LittleEndian.PutUint64(idBytes, uint64(instanceID)) binary.LittleEndian.PutUint64(idBytes, uint64(instanceID))
packet = append(packet, idBytes...) packet = append(packet, idBytes...)
// Error code (2 bytes) // Error code (2 bytes)
errorBytes := make([]byte, 2) errorBytes := make([]byte, 2)
binary.LittleEndian.PutUint16(errorBytes, uint16(errorCode)) binary.LittleEndian.PutUint16(errorBytes, uint16(errorCode))
packet = append(packet, errorBytes...) packet = append(packet, errorBytes...)
// Error message length and data // Error message length and data
messageBytes := []byte(errorMessage) messageBytes := []byte(errorMessage)
packet = append(packet, byte(len(messageBytes))) packet = append(packet, byte(len(messageBytes)))
packet = append(packet, messageBytes...) packet = append(packet, messageBytes...)
return packet, nil return packet, nil
} }
@ -310,25 +310,25 @@ func (hpb *HeroicOPPacketBuilder) BuildHOShiftPacket(ho *HeroicOP, oldWheelID, n
} }
packet := make([]byte, 0, 32) packet := make([]byte, 0, 32)
// Packet header // Packet header
packet = append(packet, 0x08) // HO Shift packet type packet = append(packet, 0x08) // HO Shift packet type
// HO Instance ID (8 bytes) // HO Instance ID (8 bytes)
idBytes := make([]byte, 8) idBytes := make([]byte, 8)
binary.LittleEndian.PutUint64(idBytes, uint64(ho.ID)) binary.LittleEndian.PutUint64(idBytes, uint64(ho.ID))
packet = append(packet, idBytes...) packet = append(packet, idBytes...)
// Old wheel ID (4 bytes) // Old wheel ID (4 bytes)
oldWheelBytes := make([]byte, 4) oldWheelBytes := make([]byte, 4)
binary.LittleEndian.PutUint32(oldWheelBytes, uint32(oldWheelID)) binary.LittleEndian.PutUint32(oldWheelBytes, uint32(oldWheelID))
packet = append(packet, oldWheelBytes...) packet = append(packet, oldWheelBytes...)
// New wheel ID (4 bytes) // New wheel ID (4 bytes)
newWheelBytes := make([]byte, 4) newWheelBytes := make([]byte, 4)
binary.LittleEndian.PutUint32(newWheelBytes, uint32(newWheelID)) binary.LittleEndian.PutUint32(newWheelBytes, uint32(newWheelID))
packet = append(packet, newWheelBytes...) packet = append(packet, newWheelBytes...)
return packet, nil return packet, nil
} }
@ -343,7 +343,7 @@ func (hpb *HeroicOPPacketBuilder) ToPacketData(ho *HeroicOP, wheel *HeroicOPWhee
State: ho.State, State: ho.State,
Countered: ho.Countered, Countered: ho.Countered,
} }
if wheel != nil { if wheel != nil {
data.SpellName = wheel.Name data.SpellName = wheel.Name
data.SpellDescription = wheel.Description data.SpellDescription = wheel.Description
@ -355,7 +355,7 @@ func (hpb *HeroicOPPacketBuilder) ToPacketData(ho *HeroicOP, wheel *HeroicOPWhee
data.SpellDescription = ho.SpellDescription data.SpellDescription = ho.SpellDescription
// Abilities will be zero-initialized // Abilities will be zero-initialized
} }
return data return data
} }
@ -364,11 +364,11 @@ func (hpb *HeroicOPPacketBuilder) ToPacketData(ho *HeroicOP, wheel *HeroicOPWhee
// ValidatePacketSize checks if packet size is within acceptable limits // ValidatePacketSize checks if packet size is within acceptable limits
func (hpb *HeroicOPPacketBuilder) ValidatePacketSize(packet []byte) error { func (hpb *HeroicOPPacketBuilder) ValidatePacketSize(packet []byte) error {
const maxPacketSize = 1024 // 1KB limit for HO packets const maxPacketSize = 1024 // 1KB limit for HO packets
if len(packet) > maxPacketSize { if len(packet) > maxPacketSize {
return fmt.Errorf("packet size %d exceeds maximum %d", len(packet), maxPacketSize) return fmt.Errorf("packet size %d exceeds maximum %d", len(packet), maxPacketSize)
} }
return nil return nil
} }
@ -455,4 +455,4 @@ func GetErrorMessage(errorCode int) string {
default: default:
return "Unknown error" return "Unknown error"
} }
} }

View File

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

View File

@ -8,78 +8,78 @@ const (
AccessLevelVisitor AccessLevelVisitor
AccessLevelGuildMember AccessLevelGuildMember
AccessLevelBanned AccessLevelBanned
// House alignment requirements // House alignment requirements
AlignmentAny = 0 AlignmentAny = 0
AlignmentGood = 1 AlignmentGood = 1
AlignmentEvil = 2 AlignmentEvil = 2
AlignmentNeutral = 3 AlignmentNeutral = 3
// Transaction types for house history // Transaction types for house history
TransactionPurchase = 1 TransactionPurchase = 1
TransactionUpkeep = 2 TransactionUpkeep = 2
TransactionDeposit = 3 TransactionDeposit = 3
TransactionWithdrawal = 4 TransactionWithdrawal = 4
TransactionAmenity = 5 TransactionAmenity = 5
TransactionVaultExpansion = 6 TransactionVaultExpansion = 6
TransactionRent = 7 TransactionRent = 7
TransactionForeclosure = 8 TransactionForeclosure = 8
TransactionTransfer = 9 TransactionTransfer = 9
TransactionRepair = 10 TransactionRepair = 10
// History position flags // History position flags
HistoryFlagPositive = 1 HistoryFlagPositive = 1
HistoryFlagNegative = 0 HistoryFlagNegative = 0
// Upkeep periods (in seconds) // Upkeep periods (in seconds)
UpkeepPeriodWeekly = 604800 // 7 days UpkeepPeriodWeekly = 604800 // 7 days
UpkeepPeriodMonthly = 2592000 // 30 days UpkeepPeriodMonthly = 2592000 // 30 days
UpkeepGracePeriod = 259200 // 3 days UpkeepGracePeriod = 259200 // 3 days
// House status flags // House status flags
HouseStatusActive = 0 HouseStatusActive = 0
HouseStatusUpkeepDue = 1 HouseStatusUpkeepDue = 1
HouseStatusForeclosed = 2 HouseStatusForeclosed = 2
HouseStatusAbandoned = 3 HouseStatusAbandoned = 3
// Maximum values // Maximum values
MaxHouseName = 64 MaxHouseName = 64
MaxReasonLength = 255 MaxReasonLength = 255
MaxDepositHistory = 100 MaxDepositHistory = 100
MaxTransactionHistory = 500 MaxTransactionHistory = 500
MaxVaultSlots = 200 MaxVaultSlots = 200
MaxAmenities = 50 MaxAmenities = 50
MaxAccessEntries = 100 MaxAccessEntries = 100
// Database retry settings // Database retry settings
MaxDatabaseRetries = 3 MaxDatabaseRetries = 3
DatabaseTimeout = 30 // seconds DatabaseTimeout = 30 // seconds
// Escrow limits // Escrow limits
MaxEscrowCoins = 1000000000 // 1 billion copper MaxEscrowCoins = 1000000000 // 1 billion copper
MaxEscrowStatus = 10000000 // 10 million status MaxEscrowStatus = 10000000 // 10 million status
// Visit permissions // Visit permissions
VisitPermissionPublic = 0 VisitPermissionPublic = 0
VisitPermissionFriends = 1 VisitPermissionFriends = 1
VisitPermissionGuild = 2 VisitPermissionGuild = 2
VisitPermissionInviteOnly = 3 VisitPermissionInviteOnly = 3
VisitPermissionPrivate = 4 VisitPermissionPrivate = 4
// Housing opcodes/packet types // Housing opcodes/packet types
OpHousePurchase = "PlayerHousePurchase" OpHousePurchase = "PlayerHousePurchase"
OpHousingList = "CharacterHousingList" OpHousingList = "CharacterHousingList"
OpBaseHouseWindow = "PlayerHouseBaseScreen" OpBaseHouseWindow = "PlayerHouseBaseScreen"
OpHouseVisitWindow = "PlayerHouseVisit" OpHouseVisitWindow = "PlayerHouseVisit"
OpBuyHouse = "BuyHouse" OpBuyHouse = "BuyHouse"
OpEnterHouse = "EnterHouse" OpEnterHouse = "EnterHouse"
OpUpdateHouseAccess = "UpdateHouseAccessDataMsg" OpUpdateHouseAccess = "UpdateHouseAccessDataMsg"
OpHouseDeposit = "HouseDeposit" OpHouseDeposit = "HouseDeposit"
OpHouseWithdrawal = "HouseWithdrawal" OpHouseWithdrawal = "HouseWithdrawal"
OpPlaceItem = "PlaceItemInHouse" OpPlaceItem = "PlaceItemInHouse"
OpRemoveItem = "RemoveItemFromHouse" OpRemoveItem = "RemoveItemFromHouse"
OpUpdateAmenities = "UpdateHouseAmenities" OpUpdateAmenities = "UpdateHouseAmenities"
// Error messages // Error messages
ErrHouseNotFound = "house not found" ErrHouseNotFound = "house not found"
ErrInsufficientFunds = "insufficient funds" ErrInsufficientFunds = "insufficient funds"
@ -93,20 +93,20 @@ const (
ErrInvalidHouseType = "invalid house type" ErrInvalidHouseType = "invalid house type"
ErrDuplicateHouse = "player already owns this house type" ErrDuplicateHouse = "player already owns this house type"
ErrMaxHousesReached = "maximum number of houses reached" ErrMaxHousesReached = "maximum number of houses reached"
// Default upkeep costs (can be overridden per house type) // Default upkeep costs (can be overridden per house type)
DefaultUpkeepCoins = 10000 // 1 gold DefaultUpkeepCoins = 10000 // 1 gold
DefaultUpkeepStatus = 100 DefaultUpkeepStatus = 100
// Item placement constants // Item placement constants
MaxItemsPerHouse = 1000 MaxItemsPerHouse = 1000
MaxItemStackSize = 100 MaxItemStackSize = 100
ItemPlacementRadius = 50.0 // Maximum distance from spawn point ItemPlacementRadius = 50.0 // Maximum distance from spawn point
// House zones configuration // House zones configuration
DefaultInstanceLifetime = 3600 // 1 hour in seconds DefaultInstanceLifetime = 3600 // 1 hour in seconds
MaxHouseVisitors = 50 // Maximum concurrent visitors MaxHouseVisitors = 50 // Maximum concurrent visitors
// Amenity types // Amenity types
AmenityVaultExpansion = 1 AmenityVaultExpansion = 1
AmenityPortal = 2 AmenityPortal = 2
@ -116,15 +116,15 @@ const (
AmenityBanker = 6 AmenityBanker = 6
AmenityManagedItems = 7 AmenityManagedItems = 7
AmenityTeleporter = 8 AmenityTeleporter = 8
// Foreclosure settings // Foreclosure settings
ForeclosureWarningDays = 7 // Days before foreclosure ForeclosureWarningDays = 7 // Days before foreclosure
ForeclosureNoticeDays = 3 // Days of final notice ForeclosureNoticeDays = 3 // Days of final notice
// Deposit limits // Deposit limits
MinDepositAmount = 1 MinDepositAmount = 1
MaxDepositAmount = 10000000 // 1000 gold MaxDepositAmount = 10000000 // 1000 gold
// Search and filtering // Search and filtering
MaxSearchResults = 100 MaxSearchResults = 100
SearchTimeout = 5 // seconds SearchTimeout = 5 // seconds
@ -132,43 +132,43 @@ const (
// House type constants for common house types // House type constants for common house types
const ( const (
HouseTypeInn = 1 HouseTypeInn = 1
HouseTypeCottage = 2 HouseTypeCottage = 2
HouseTypeApartment = 3 HouseTypeApartment = 3
HouseTypeHouse = 4 HouseTypeHouse = 4
HouseTypeMansion = 5 HouseTypeMansion = 5
HouseTypeKeep = 6 HouseTypeKeep = 6
HouseTypeGuildHall = 7 HouseTypeGuildHall = 7
HouseTypePrestigeHome = 8 HouseTypePrestigeHome = 8
) )
// Access permission flags (bitwise) // Access permission flags (bitwise)
const ( const (
PermissionEnter = 1 << iota // Can enter the house PermissionEnter = 1 << iota // Can enter the house
PermissionPlace = 1 << iota // Can place items PermissionPlace = 1 << iota // Can place items
PermissionRemove = 1 << iota // Can remove items PermissionRemove = 1 << iota // Can remove items
PermissionMove = 1 << iota // Can move items PermissionMove = 1 << iota // Can move items
PermissionVault = 1 << iota // Can access vault PermissionVault = 1 << iota // Can access vault
PermissionDeposit = 1 << iota // Can make deposits PermissionDeposit = 1 << iota // Can make deposits
PermissionWithdraw = 1 << iota // Can make withdrawals PermissionWithdraw = 1 << iota // Can make withdrawals
PermissionInvite = 1 << iota // Can invite others PermissionInvite = 1 << iota // Can invite others
PermissionKick = 1 << iota // Can kick visitors PermissionKick = 1 << iota // Can kick visitors
PermissionAdmin = 1 << iota // Full administrative access PermissionAdmin = 1 << iota // Full administrative access
) )
// Default permission sets // Default permission sets
const ( const (
PermissionsOwner = PermissionEnter | PermissionPlace | PermissionRemove | PermissionsOwner = PermissionEnter | PermissionPlace | PermissionRemove |
PermissionMove | PermissionVault | PermissionDeposit | PermissionMove | PermissionVault | PermissionDeposit |
PermissionWithdraw | PermissionInvite | PermissionKick | PermissionAdmin PermissionWithdraw | PermissionInvite | PermissionKick | PermissionAdmin
PermissionsFriend = PermissionEnter | PermissionPlace | PermissionMove | PermissionsFriend = PermissionEnter | PermissionPlace | PermissionMove |
PermissionVault | PermissionDeposit PermissionVault | PermissionDeposit
PermissionsVisitor = PermissionEnter PermissionsVisitor = PermissionEnter
PermissionsGuildMember = PermissionEnter | PermissionPlace | PermissionDeposit PermissionsGuildMember = PermissionEnter | PermissionPlace | PermissionDeposit
PermissionsBanned = 0 PermissionsBanned = 0
) )
@ -176,14 +176,14 @@ const (
var AlignmentNames = map[int8]string{ var AlignmentNames = map[int8]string{
AlignmentAny: "Any", AlignmentAny: "Any",
AlignmentGood: "Good", AlignmentGood: "Good",
AlignmentEvil: "Evil", AlignmentEvil: "Evil",
AlignmentNeutral: "Neutral", AlignmentNeutral: "Neutral",
} }
// Transaction reason descriptions // Transaction reason descriptions
var TransactionReasons = map[int]string{ var TransactionReasons = map[int]string{
TransactionPurchase: "House Purchase", TransactionPurchase: "House Purchase",
TransactionUpkeep: "Upkeep Payment", TransactionUpkeep: "Upkeep Payment",
TransactionDeposit: "Escrow Deposit", TransactionDeposit: "Escrow Deposit",
TransactionWithdrawal: "Escrow Withdrawal", TransactionWithdrawal: "Escrow Withdrawal",
TransactionAmenity: "Amenity Purchase", TransactionAmenity: "Amenity Purchase",
@ -199,7 +199,7 @@ var AmenityNames = map[int]string{
AmenityVaultExpansion: "Vault Expansion", AmenityVaultExpansion: "Vault Expansion",
AmenityPortal: "Portal", AmenityPortal: "Portal",
AmenityMerchant: "Merchant", AmenityMerchant: "Merchant",
AmenityRepairNPC: "Repair NPC", AmenityRepairNPC: "Repair NPC",
AmenityBroker: "Broker", AmenityBroker: "Broker",
AmenityBanker: "Banker", AmenityBanker: "Banker",
AmenityManagedItems: "Managed Items", AmenityManagedItems: "Managed Items",
@ -220,13 +220,13 @@ var HouseTypeNames = map[int]string{
// Default costs for house types (in copper coins) // Default costs for house types (in copper coins)
var DefaultHouseCosts = map[int]int64{ var DefaultHouseCosts = map[int]int64{
HouseTypeInn: 50000, // 5 gold HouseTypeInn: 50000, // 5 gold
HouseTypeCottage: 200000, // 20 gold HouseTypeCottage: 200000, // 20 gold
HouseTypeApartment: 500000, // 50 gold HouseTypeApartment: 500000, // 50 gold
HouseTypeHouse: 1000000, // 100 gold HouseTypeHouse: 1000000, // 100 gold
HouseTypeMansion: 5000000, // 500 gold HouseTypeMansion: 5000000, // 500 gold
HouseTypeKeep: 10000000, // 1000 gold HouseTypeKeep: 10000000, // 1000 gold
HouseTypeGuildHall: 50000000, // 5000 gold HouseTypeGuildHall: 50000000, // 5000 gold
HouseTypePrestigeHome: 100000000, // 10000 gold HouseTypePrestigeHome: 100000000, // 10000 gold
} }
@ -244,13 +244,13 @@ var DefaultHouseStatusCosts = map[int]int64{
// Default upkeep costs (in copper coins per week) // Default upkeep costs (in copper coins per week)
var DefaultHouseUpkeepCosts = map[int]int64{ var DefaultHouseUpkeepCosts = map[int]int64{
HouseTypeInn: 5000, // 50 silver HouseTypeInn: 5000, // 50 silver
HouseTypeCottage: 10000, // 1 gold HouseTypeCottage: 10000, // 1 gold
HouseTypeApartment: 25000, // 2.5 gold HouseTypeApartment: 25000, // 2.5 gold
HouseTypeHouse: 50000, // 5 gold HouseTypeHouse: 50000, // 5 gold
HouseTypeMansion: 100000, // 10 gold HouseTypeMansion: 100000, // 10 gold
HouseTypeKeep: 200000, // 20 gold HouseTypeKeep: 200000, // 20 gold
HouseTypeGuildHall: 500000, // 50 gold HouseTypeGuildHall: 500000, // 50 gold
HouseTypePrestigeHome: 1000000, // 100 gold HouseTypePrestigeHome: 1000000, // 100 gold
} }
@ -264,4 +264,4 @@ var DefaultVaultSlots = map[int]int{
HouseTypeKeep: 20, HouseTypeKeep: 20,
HouseTypeGuildHall: 24, HouseTypeGuildHall: 24,
HouseTypePrestigeHome: 32, 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, 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 alignment, guild_level, vault_slots, max_items, max_visitors, upkeep_period, description
FROM houses ORDER BY id` FROM houses ORDER BY id`
rows, err := dhm.db.QueryContext(ctx, query) rows, err := dhm.db.QueryContext(ctx, query)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to query house zones: %w", err) 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, 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 alignment, guild_level, vault_slots, max_items, max_visitors, upkeep_period, description
FROM houses WHERE id = ?` FROM houses WHERE id = ?`
var zone HouseZoneData var zone HouseZoneData
var description *string 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, status, house_name, visit_permission, public_note, private_note, allow_friends, allow_guild,
require_approval, show_on_directory, allow_decoration, tax_exempt require_approval, show_on_directory, allow_decoration, tax_exempt
FROM character_houses WHERE char_id = ? ORDER BY unique_id` FROM character_houses WHERE char_id = ? ORDER BY unique_id`
rows, err := dhm.db.QueryContext(ctx, query, characterID) rows, err := dhm.db.QueryContext(ctx, query, characterID)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to query player houses for character %d: %w", characterID, err) 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, status, house_name, visit_permission, public_note, private_note, allow_friends, allow_guild,
require_approval, show_on_directory, allow_decoration, tax_exempt require_approval, show_on_directory, allow_decoration, tax_exempt
FROM character_houses WHERE unique_id = ?` FROM character_houses WHERE unique_id = ?`
var house PlayerHouseData var house PlayerHouseData
var upkeepDueTimestamp int64 var upkeepDueTimestamp int64
var houseName, publicNote, privateNote *string var houseName, publicNote, privateNote *string
@ -317,7 +317,7 @@ func (dhm *DatabaseHousingManager) DeletePlayerHouse(ctx context.Context, unique
// Delete related data first // Delete related data first
tables := []string{ tables := []string{
"character_house_deposits", "character_house_deposits",
"character_house_history", "character_house_history",
"character_house_access", "character_house_access",
"character_house_amenities", "character_house_amenities",
"character_house_items", "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 query := `SELECT house_id, timestamp, amount, last_amount, status, last_status, name, character_id
FROM character_house_deposits WHERE house_id = ? FROM character_house_deposits WHERE house_id = ?
ORDER BY timestamp DESC LIMIT ?` ORDER BY timestamp DESC LIMIT ?`
rows, err := dhm.db.QueryContext(ctx, query, houseID, MaxDepositHistory) rows, err := dhm.db.QueryContext(ctx, query, houseID, MaxDepositHistory)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to query house deposits for house %d: %w", houseID, err) 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 query := `SELECT house_id, timestamp, amount, status, reason, name, character_id, pos_flag, type
FROM character_house_history WHERE house_id = ? FROM character_house_history WHERE house_id = ?
ORDER BY timestamp DESC LIMIT ?` ORDER BY timestamp DESC LIMIT ?`
rows, err := dhm.db.QueryContext(ctx, query, houseID, MaxTransactionHistory) rows, err := dhm.db.QueryContext(ctx, query, houseID, MaxTransactionHistory)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to query house history for house %d: %w", houseID, err) 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 // DeleteHouseAccess removes access for a specific character
func (dhm *DatabaseHousingManager) DeleteHouseAccess(ctx context.Context, houseID int64, characterID int32) error { func (dhm *DatabaseHousingManager) DeleteHouseAccess(ctx context.Context, houseID int64, characterID int32) error {
_, err := dhm.db.ExecContext(ctx, _, err := dhm.db.ExecContext(ctx,
"DELETE FROM character_house_access WHERE house_id = ? AND character_id = ?", "DELETE FROM character_house_access WHERE house_id = ? AND character_id = ?",
houseID, characterID) houseID, characterID)
if err != nil { if err != nil {
return fmt.Errorf("failed to delete house access: %w", err) 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 // DeleteHouseAmenity removes a house amenity
func (dhm *DatabaseHousingManager) DeleteHouseAmenity(ctx context.Context, houseID int64, amenityID int32) error { func (dhm *DatabaseHousingManager) DeleteHouseAmenity(ctx context.Context, houseID int64, amenityID int32) error {
_, err := dhm.db.ExecContext(ctx, _, err := dhm.db.ExecContext(ctx,
"DELETE FROM character_house_amenities WHERE house_id = ? AND id = ?", "DELETE FROM character_house_amenities WHERE house_id = ? AND id = ?",
houseID, amenityID) houseID, amenityID)
if err != nil { if err != nil {
return fmt.Errorf("failed to delete house amenity: %w", err) 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 // DeleteHouseItem removes a house item
func (dhm *DatabaseHousingManager) DeleteHouseItem(ctx context.Context, houseID int64, itemID int64) error { func (dhm *DatabaseHousingManager) DeleteHouseItem(ctx context.Context, houseID int64, itemID int64) error {
_, err := dhm.db.ExecContext(ctx, _, err := dhm.db.ExecContext(ctx,
"DELETE FROM character_house_items WHERE house_id = ? AND id = ?", "DELETE FROM character_house_items WHERE house_id = ? AND id = ?",
houseID, itemID) houseID, itemID)
if err != nil { if err != nil {
return fmt.Errorf("failed to delete house item: %w", err) 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 // GetNextHouseID returns the next available house unique ID
func (dhm *DatabaseHousingManager) GetNextHouseID(ctx context.Context) (int64, error) { func (dhm *DatabaseHousingManager) GetNextHouseID(ctx context.Context) (int64, error) {
query := "SELECT COALESCE(MAX(unique_id), 0) + 1 FROM character_houses" query := "SELECT COALESCE(MAX(unique_id), 0) + 1 FROM character_houses"
var nextID int64 var nextID int64
err := dhm.db.QueryRowContext(ctx, query).Scan(&nextID) err := dhm.db.QueryRowContext(ctx, query).Scan(&nextID)
if err != nil { 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, status, house_name, visit_permission, public_note, private_note, allow_friends, allow_guild,
require_approval, show_on_directory, allow_decoration, tax_exempt require_approval, show_on_directory, allow_decoration, tax_exempt
FROM character_houses WHERE instance_id = ?` FROM character_houses WHERE instance_id = ?`
var house PlayerHouseData var house PlayerHouseData
var upkeepDueTimestamp int64 var upkeepDueTimestamp int64
var houseName, publicNote, privateNote *string 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 = ?` FROM character_houses WHERE upkeep_due <= ? AND status = ?`
cutoffTimestamp := cutoffTime.Unix() cutoffTimestamp := cutoffTime.Unix()
rows, err := dhm.db.QueryContext(ctx, query, cutoffTimestamp, HouseStatusActive) rows, err := dhm.db.QueryContext(ctx, query, cutoffTimestamp, HouseStatusActive)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to query houses for upkeep: %w", err) 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 // Get basic counts
queries := map[string]*int64{ queries := map[string]*int64{
"SELECT COUNT(*) FROM character_houses": &stats.TotalHouses, "SELECT COUNT(*) FROM character_houses": &stats.TotalHouses,
"SELECT COUNT(*) FROM character_houses WHERE status = 0": &stats.ActiveHouses, "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 = 2": &stats.ForelosedHouses,
"SELECT COUNT(*) FROM character_house_deposits": &stats.TotalDeposits, "SELECT COUNT(*) FROM character_house_deposits": &stats.TotalDeposits,
"SELECT COUNT(*) FROM character_house_history WHERE pos_flag = 0": &stats.TotalWithdrawals, "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 return nil
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -2,36 +2,36 @@ package ai
// AI tick constants // AI tick constants
const ( const (
DefaultThinkTick int32 = 250 // Default think tick in milliseconds (1/4 second) DefaultThinkTick int32 = 250 // Default think tick in milliseconds (1/4 second)
FastThinkTick int32 = 100 // Fast think tick for active AI FastThinkTick int32 = 100 // Fast think tick for active AI
SlowThinkTick int32 = 1000 // Slow think tick for idle AI SlowThinkTick int32 = 1000 // Slow think tick for idle AI
BlankBrainTick int32 = 50000 // Very slow tick for blank brain BlankBrainTick int32 = 50000 // Very slow tick for blank brain
MaxThinkTick int32 = 60000 // Maximum think tick (1 minute) MaxThinkTick int32 = 60000 // Maximum think tick (1 minute)
) )
// Combat constants // Combat constants
const ( const (
MaxChaseDistance float32 = 150.0 // Default max chase distance MaxChaseDistance float32 = 150.0 // Default max chase distance
MaxCombatRange float32 = 25.0 // Default max combat range MaxCombatRange float32 = 25.0 // Default max combat range
RunbackThreshold float32 = 1.0 // Distance threshold for runback RunbackThreshold float32 = 1.0 // Distance threshold for runback
) )
// Hate system constants // Hate system constants
const ( const (
MinHateValue int32 = 1 // Minimum hate value (0 or negative is invalid) MinHateValue int32 = 1 // Minimum hate value (0 or negative is invalid)
MaxHateValue int32 = 2147483647 // Maximum hate value (INT_MAX) MaxHateValue int32 = 2147483647 // Maximum hate value (INT_MAX)
DefaultHateValue int32 = 100 // Default hate amount DefaultHateValue int32 = 100 // Default hate amount
MaxHateListSize int = 100 // Maximum entities in hate list MaxHateListSize int = 100 // Maximum entities in hate list
) )
// Encounter system constants // Encounter system constants
const ( const (
MaxEncounterSize int = 50 // Maximum entities in encounter list MaxEncounterSize int = 50 // Maximum entities in encounter list
) )
// Spell recovery constants // Spell recovery constants
const ( const (
SpellRecoveryBuffer int32 = 2000 // Additional recovery time buffer (2 seconds) SpellRecoveryBuffer int32 = 2000 // Additional recovery time buffer (2 seconds)
) )
// Brain type constants for identification // Brain type constants for identification
@ -46,9 +46,9 @@ const (
// Pet movement constants // Pet movement constants
const ( const (
PetMovementFollow int8 = 0 PetMovementFollow int8 = 0
PetMovementStay int8 = 1 PetMovementStay int8 = 1
PetMovementGuard int8 = 2 PetMovementGuard int8 = 2
) )
// Encounter state constants // Encounter state constants
@ -60,31 +60,31 @@ const (
// Combat decision constants // Combat decision constants
const ( const (
MeleeAttackChance int = 70 // Base chance for melee attack MeleeAttackChance int = 70 // Base chance for melee attack
SpellCastChance int = 30 // Base chance for spell casting SpellCastChance int = 30 // Base chance for spell casting
BuffCheckChance int = 50 // Chance to check for buffs BuffCheckChance int = 50 // Chance to check for buffs
) )
// AI state flags // AI state flags
const ( const (
AIStateIdle int32 = 0 AIStateIdle int32 = 0
AIStateCombat int32 = 1 AIStateCombat int32 = 1
AIStateFollowing int32 = 2 AIStateFollowing int32 = 2
AIStateRunback int32 = 3 AIStateRunback int32 = 3
AIStateCasting int32 = 4 AIStateCasting int32 = 4
AIStateMoving int32 = 5 AIStateMoving int32 = 5
) )
// Debug levels // Debug levels
const ( const (
DebugLevelNone int8 = 0 DebugLevelNone int8 = 0
DebugLevelBasic int8 = 1 DebugLevelBasic int8 = 1
DebugLevelDetailed int8 = 2 DebugLevelDetailed int8 = 2
DebugLevelVerbose int8 = 3 DebugLevelVerbose int8 = 3
) )
// Timer constants // Timer constants
const ( const (
MillisecondsPerSecond int32 = 1000 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 package ai
import "fmt" import (
"fmt"
"time"
)
// Logger interface for AI logging // Logger interface for AI logging
type Logger interface { type Logger interface {
@ -19,17 +22,17 @@ type NPC interface {
GetTotalHP() int32 GetTotalHP() int32
SetHP(int32) SetHP(int32)
IsAlive() bool IsAlive() bool
// Combat state // Combat state
GetInCombat() bool GetInCombat() bool
InCombat(bool) InCombat(bool)
GetTarget() Entity GetTarget() Entity
SetTarget(Entity) SetTarget(Entity)
// Pet functionality // Pet functionality
IsPet() bool IsPet() bool
GetOwner() Entity GetOwner() Entity
// Movement and positioning // Movement and positioning
GetX() float32 GetX() float32
GetY() float32 GetY() float32
@ -42,7 +45,7 @@ type NPC interface {
SetFollowTarget(Spawn, float32) SetFollowTarget(Spawn, float32)
CalculateRunningLocation(bool) CalculateRunningLocation(bool)
ClearRunningLocations() ClearRunningLocations()
// Runback functionality // Runback functionality
IsRunningBack() bool IsRunningBack() bool
GetRunbackLocation() *MovementLocation GetRunbackLocation() *MovementLocation
@ -50,7 +53,7 @@ type NPC interface {
Runback(float32) Runback(float32)
ShouldCallRunback() bool ShouldCallRunback() bool
SetCallRunback(bool) SetCallRunback(bool)
// Status effects // Status effects
IsMezzedOrStunned() bool IsMezzedOrStunned() bool
IsCasting() bool IsCasting() bool
@ -60,7 +63,7 @@ type NPC interface {
InWater() bool InWater() bool
IsWaterCreature() bool IsWaterCreature() bool
IsFlyingCreature() bool IsFlyingCreature() bool
// Combat mechanics // Combat mechanics
AttackAllowed(Entity) bool AttackAllowed(Entity) bool
PrimaryWeaponReady() bool PrimaryWeaponReady() bool
@ -68,23 +71,23 @@ type NPC interface {
SetPrimaryLastAttackTime(int64) SetPrimaryLastAttackTime(int64)
SetSecondaryLastAttackTime(int64) SetSecondaryLastAttackTime(int64)
MeleeAttack(Entity, float32, bool) MeleeAttack(Entity, float32, bool)
// Spell casting // Spell casting
GetCastPercentage() int8 GetCastPercentage() int8
GetNextSpell(Entity, float32) Spell GetNextSpell(Entity, float32) Spell
GetNextBuffSpell(Spawn) Spell GetNextBuffSpell(Spawn) Spell
SetCastOnAggroCompleted(bool) SetCastOnAggroCompleted(bool)
CheckLoS(Entity) bool CheckLoS(Entity) bool
// Movement pausing // Movement pausing
IsPauseMovementTimerActive() bool IsPauseMovementTimerActive() bool
// Encounter state // Encounter state
SetEncounterState(int8) SetEncounterState(int8)
// Scripts // Scripts
GetSpawnScript() string GetSpawnScript() string
// Utility // Utility
KillSpawn(NPC) KillSpawn(NPC)
} }
@ -146,11 +149,11 @@ type Zone interface {
// AIManager provides high-level management of the AI system // AIManager provides high-level management of the AI system
type AIManager struct { type AIManager struct {
brains map[int32]Brain // Map of NPC ID to brain brains map[int32]Brain // Map of NPC ID to brain
activeCount int64 // Number of active brains activeCount int64 // Number of active brains
totalThinks int64 // Total think cycles processed totalThinks int64 // Total think cycles processed
logger Logger // Logger for AI events logger Logger // Logger for AI events
luaInterface LuaInterface // Lua script interface luaInterface LuaInterface // Lua script interface
} }
// NewAIManager creates a new AI manager // NewAIManager creates a new AI manager
@ -169,20 +172,20 @@ func (am *AIManager) AddBrain(npcID int32, brain Brain) error {
if brain == nil { if brain == nil {
return fmt.Errorf("brain cannot be nil") return fmt.Errorf("brain cannot be nil")
} }
if _, exists := am.brains[npcID]; exists { if _, exists := am.brains[npcID]; exists {
return fmt.Errorf("brain already exists for NPC %d", npcID) return fmt.Errorf("brain already exists for NPC %d", npcID)
} }
am.brains[npcID] = brain am.brains[npcID] = brain
if brain.IsActive() { if brain.IsActive() {
am.activeCount++ am.activeCount++
} }
if am.logger != nil { if am.logger != nil {
am.logger.LogDebug("Added brain for NPC %d (type: %d)", npcID, brain.GetBrainType()) am.logger.LogDebug("Added brain for NPC %d (type: %d)", npcID, brain.GetBrainType())
} }
return nil return nil
} }
@ -193,7 +196,7 @@ func (am *AIManager) RemoveBrain(npcID int32) {
am.activeCount-- am.activeCount--
} }
delete(am.brains, npcID) delete(am.brains, npcID)
if am.logger != nil { if am.logger != nil {
am.logger.LogDebug("Removed brain for NPC %d", npcID) 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 { if npc == nil {
return fmt.Errorf("NPC cannot be nil") return fmt.Errorf("NPC cannot be nil")
} }
npcID := npc.GetID() npcID := npc.GetID()
// Create brain based on type // Create brain based on type
var brain Brain var brain Brain
switch brainType { switch brainType {
case BrainTypeCombatPet: case BrainTypeCombatPet:
brain = NewCombatPetBrain(npc, am.logger) brain = NewCombatPetBrain(npc, am.logger)
case BrainTypeNonCombatPet: case BrainTypeNonCombatPet:
brain = NewNonCombatPetBrain(npc, am.logger) brain = NewNonCombatPetBrain(npc, am.logger)
case BrainTypeBlank: case BrainTypeBlank:
brain = NewBlankBrain(npc, am.logger) brain = NewBlankBrain(npc, am.logger)
case BrainTypeLua: case BrainTypeLua:
brain = NewLuaBrain(npc, am.logger, am.luaInterface) brain = NewLuaBrain(npc, am.logger, am.luaInterface)
case BrainTypeDumbFire: case BrainTypeDumbFire:
if len(options) >= 2 { if len(options) >= 2 {
if target, ok := options[0].(Entity); ok { if target, ok := options[0].(Entity); ok {
@ -239,27 +242,27 @@ func (am *AIManager) CreateBrainForNPC(npc NPC, brainType int8, options ...inter
if brain == nil { if brain == nil {
return fmt.Errorf("invalid options for dumbfire brain") return fmt.Errorf("invalid options for dumbfire brain")
} }
default: default:
brain = NewBaseBrain(npc, am.logger) brain = NewBaseBrain(npc, am.logger)
} }
return am.AddBrain(npcID, brain) return am.AddBrain(npcID, brain)
} }
// ProcessAllBrains runs think cycles for all active brains // ProcessAllBrains runs think cycles for all active brains
func (am *AIManager) ProcessAllBrains() { func (am *AIManager) ProcessAllBrains() {
currentTime := currentTimeMillis() currentTime := currentTimeMillis()
for npcID, brain := range am.brains { for npcID, brain := range am.brains {
if !brain.IsActive() { if !brain.IsActive() {
continue continue
} }
// Check if it's time to think // Check if it's time to think
lastThink := brain.GetLastThink() lastThink := brain.GetLastThink()
thinkTick := brain.GetThinkTick() thinkTick := brain.GetThinkTick()
if currentTime-lastThink >= int64(thinkTick) { if currentTime-lastThink >= int64(thinkTick) {
if err := brain.Think(); err != nil { if err := brain.Think(); err != nil {
if am.logger != nil { if am.logger != nil {
@ -276,7 +279,7 @@ func (am *AIManager) SetBrainActive(npcID int32, active bool) {
if brain := am.brains[npcID]; brain != nil { if brain := am.brains[npcID]; brain != nil {
wasActive := brain.IsActive() wasActive := brain.IsActive()
brain.SetActive(active) brain.SetActive(active)
// Update active count // Update active count
if wasActive && !active { if wasActive && !active {
am.activeCount-- am.activeCount--
@ -316,7 +319,7 @@ func (am *AIManager) GetBrainsByType(brainType int8) []Brain {
func (am *AIManager) ClearAllBrains() { func (am *AIManager) ClearAllBrains() {
am.brains = make(map[int32]Brain) am.brains = make(map[int32]Brain)
am.activeCount = 0 am.activeCount = 0
if am.logger != nil { if am.logger != nil {
am.logger.LogInfo("Cleared all AI brains") am.logger.LogInfo("Cleared all AI brains")
} }
@ -325,31 +328,31 @@ func (am *AIManager) ClearAllBrains() {
// GetStatistics returns overall AI system statistics // GetStatistics returns overall AI system statistics
func (am *AIManager) GetStatistics() *AIStatistics { func (am *AIManager) GetStatistics() *AIStatistics {
return &AIStatistics{ return &AIStatistics{
TotalBrains: len(am.brains), TotalBrains: len(am.brains),
ActiveBrains: int(am.activeCount), ActiveBrains: int(am.activeCount),
TotalThinks: am.totalThinks, TotalThinks: am.totalThinks,
BrainsByType: am.getBrainCountsByType(), BrainsByType: am.getBrainCountsByType(),
} }
} }
// getBrainCountsByType returns counts of brains by type // getBrainCountsByType returns counts of brains by type
func (am *AIManager) getBrainCountsByType() map[string]int { func (am *AIManager) getBrainCountsByType() map[string]int {
counts := make(map[string]int) counts := make(map[string]int)
for _, brain := range am.brains { for _, brain := range am.brains {
typeName := getBrainTypeName(brain.GetBrainType()) typeName := getBrainTypeName(brain.GetBrainType())
counts[typeName]++ counts[typeName]++
} }
return counts return counts
} }
// AIStatistics contains AI system statistics // AIStatistics contains AI system statistics
type AIStatistics struct { type AIStatistics struct {
TotalBrains int `json:"total_brains"` TotalBrains int `json:"total_brains"`
ActiveBrains int `json:"active_brains"` ActiveBrains int `json:"active_brains"`
TotalThinks int64 `json:"total_thinks"` TotalThinks int64 `json:"total_thinks"`
BrainsByType map[string]int `json:"brains_by_type"` BrainsByType map[string]int `json:"brains_by_type"`
} }
// AIBrainAdapter provides NPC functionality for brains // AIBrainAdapter provides NPC functionality for brains
@ -376,11 +379,11 @@ func (aba *AIBrainAdapter) ProcessAI(brain Brain) error {
if brain == nil { if brain == nil {
return fmt.Errorf("brain is nil") return fmt.Errorf("brain is nil")
} }
if !brain.IsActive() { if !brain.IsActive() {
return nil return nil
} }
return brain.Think() return brain.Think()
} }
@ -441,10 +444,10 @@ func (hld *HateListDebugger) PrintHateList(npcName string, hateList map[int32]*H
if hld.logger == nil { if hld.logger == nil {
return return
} }
hld.logger.LogInfo("%s's Hate List", npcName) hld.logger.LogInfo("%s's Hate List", npcName)
hld.logger.LogInfo("-------------------") hld.logger.LogInfo("-------------------")
if len(hateList) == 0 { if len(hateList) == 0 {
hld.logger.LogInfo("(empty)") hld.logger.LogInfo("(empty)")
} else { } 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("Entity %d: %d hate", entityID, entry.HateValue)
} }
} }
hld.logger.LogInfo("-------------------") hld.logger.LogInfo("-------------------")
} }
@ -461,10 +464,10 @@ func (hld *HateListDebugger) PrintEncounterList(npcName string, encounterList ma
if hld.logger == nil { if hld.logger == nil {
return return
} }
hld.logger.LogInfo("%s's Encounter List", npcName) hld.logger.LogInfo("%s's Encounter List", npcName)
hld.logger.LogInfo("-------------------") hld.logger.LogInfo("-------------------")
if len(encounterList) == 0 { if len(encounterList) == 0 {
hld.logger.LogInfo("(empty)") hld.logger.LogInfo("(empty)")
} else { } else {
@ -478,6 +481,6 @@ func (hld *HateListDebugger) PrintEncounterList(npcName string, encounterList ma
hld.logger.LogInfo("Entity %d (%s)", entityID, entryType) hld.logger.LogInfo("Entity %d (%s)", entityID, entryType)
} }
} }
hld.logger.LogInfo("-------------------") hld.logger.LogInfo("-------------------")
} }

View File

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

View File

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

View File

@ -2,9 +2,9 @@ package npc
// AI Strategy constants // AI Strategy constants
const ( const (
AIStrategyBalanced int8 = 1 AIStrategyBalanced int8 = 1
AIStrategyOffensive int8 = 2 AIStrategyOffensive int8 = 2
AIStrategyDefensive int8 = 3 AIStrategyDefensive int8 = 3
) )
// Randomize Appearances constants // Randomize Appearances constants
@ -47,26 +47,26 @@ const (
// Cast Type constants // Cast Type constants
const ( const (
CastOnSpawn int8 = 0 CastOnSpawn int8 = 0
CastOnAggro int8 = 1 CastOnAggro int8 = 1
MaxCastTypes int8 = 2 MaxCastTypes int8 = 2
) )
// Default values // Default values
const ( const (
DefaultCastPercentage int8 = 25 DefaultCastPercentage int8 = 25
DefaultAggroRadius float32 = 10.0 DefaultAggroRadius float32 = 10.0
DefaultRunbackSpeed float32 = 2.0 DefaultRunbackSpeed float32 = 2.0
MaxSkillBonuses int = 100 MaxSkillBonuses int = 100
MaxNPCSpells int = 50 MaxNPCSpells int = 50
MaxPauseTime int32 = 300000 // 5 minutes max pause MaxPauseTime int32 = 300000 // 5 minutes max pause
) )
// NPC validation constants // NPC validation constants
const ( const (
MinNPCLevel int8 = 1 MinNPCLevel int8 = 1
MaxNPCLevel int8 = 100 MaxNPCLevel int8 = 100
MaxNPCNameLen int = 64 MaxNPCNameLen int = 64
MinAppearanceID int32 = 0 MinAppearanceID int32 = 0
MaxAppearanceID int32 = 999999 MaxAppearanceID int32 = 999999
) )
@ -90,4 +90,4 @@ const (
const ( const (
DefaultPauseCheckMS int32 = 100 DefaultPauseCheckMS int32 = 100
RunbackCheckMS int32 = 250 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 { func (ea *EntityAdapter) handleAggroInteraction(otherNPC *NPC) error {
// TODO: Implement aggro logic between NPCs // TODO: Implement aggro logic between NPCs
if ea.logger != nil { 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()) ea.npc.GetNPCID(), otherNPC.GetNPCID())
} }
return nil return nil
@ -224,7 +224,7 @@ func (ea *EntityAdapter) handleAggroInteraction(otherNPC *NPC) error {
func (ea *EntityAdapter) handleAssistInteraction(otherNPC *NPC) error { func (ea *EntityAdapter) handleAssistInteraction(otherNPC *NPC) error {
// TODO: Implement assist logic between NPCs // TODO: Implement assist logic between NPCs
if ea.logger != nil { 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()) ea.npc.GetNPCID(), otherNPC.GetNPCID())
} }
return nil return nil
@ -234,7 +234,7 @@ func (ea *EntityAdapter) handleAssistInteraction(otherNPC *NPC) error {
func (ea *EntityAdapter) handleTradeInteraction(otherNPC *NPC) error { func (ea *EntityAdapter) handleTradeInteraction(otherNPC *NPC) error {
// TODO: Implement trade logic between NPCs // TODO: Implement trade logic between NPCs
if ea.logger != nil { 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()) ea.npc.GetNPCID(), otherNPC.GetNPCID())
} }
return nil return nil
@ -244,7 +244,7 @@ func (ea *EntityAdapter) handleTradeInteraction(otherNPC *NPC) error {
func (ea *EntityAdapter) handleFollowCommand(otherNPC *NPC) error { func (ea *EntityAdapter) handleFollowCommand(otherNPC *NPC) error {
// TODO: Implement follow logic // TODO: Implement follow logic
if ea.logger != nil { 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()) ea.npc.GetNPCID(), otherNPC.GetNPCID())
} }
return nil return nil
@ -254,7 +254,7 @@ func (ea *EntityAdapter) handleFollowCommand(otherNPC *NPC) error {
func (ea *EntityAdapter) handleAttackCommand(otherNPC *NPC) error { func (ea *EntityAdapter) handleAttackCommand(otherNPC *NPC) error {
// TODO: Implement attack logic // TODO: Implement attack logic
if ea.logger != nil { 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()) ea.npc.GetNPCID(), otherNPC.GetNPCID())
} }
return nil return nil
@ -264,7 +264,7 @@ func (ea *EntityAdapter) handleAttackCommand(otherNPC *NPC) error {
func (ea *EntityAdapter) handleRetreatCommand(otherNPC *NPC) error { func (ea *EntityAdapter) handleRetreatCommand(otherNPC *NPC) error {
// TODO: Implement retreat logic // TODO: Implement retreat logic
if ea.logger != nil { 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()) ea.npc.GetNPCID(), otherNPC.GetNPCID())
} }
return nil return nil
@ -363,7 +363,7 @@ func (sca *SpellCasterAdapter) CastSpell(target interface{}, spell Spell) error
} }
if sca.logger != nil { 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()) 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 // getNextSpellByStrategy selects spells based on AI strategy
func (sca *SpellCasterAdapter) getNextSpellByStrategy(target interface{}, distance float32, strategy int8) Spell { func (sca *SpellCasterAdapter) getNextSpellByStrategy(target interface{}, distance float32, strategy int8) Spell {
// TODO: Implement more sophisticated spell selection based on strategy // TODO: Implement more sophisticated spell selection based on strategy
for _, npcSpell := range sca.npc.spells { for _, npcSpell := range sca.npc.spells {
// Check HP ratio requirements // Check HP ratio requirements
if npcSpell.GetRequiredHPRatio() != 0 { if npcSpell.GetRequiredHPRatio() != 0 {
// TODO: Implement HP ratio checking // TODO: Implement HP ratio checking
} }
spell := sca.spellManager.GetSpell(npcSpell.GetSpellID(), npcSpell.GetTier()) spell := sca.spellManager.GetSpell(npcSpell.GetSpellID(), npcSpell.GetTier())
if spell == nil { if spell == nil {
continue continue
} }
// Check strategy compatibility // Check strategy compatibility
if strategy == AIStrategyOffensive && spell.IsFriendlySpell() { if strategy == AIStrategyOffensive && spell.IsFriendlySpell() {
continue continue
@ -407,14 +407,14 @@ func (sca *SpellCasterAdapter) getNextSpellByStrategy(target interface{}, distan
if strategy == AIStrategyDefensive && !spell.IsFriendlySpell() { if strategy == AIStrategyDefensive && !spell.IsFriendlySpell() {
continue continue
} }
// Check range and power requirements // Check range and power requirements
if distance <= spell.GetRange() && distance >= spell.GetMinRange() { if distance <= spell.GetRange() && distance >= spell.GetMinRange() {
// TODO: Check power requirements // TODO: Check power requirements
return spell return spell
} }
} }
return nil return nil
} }
@ -425,7 +425,7 @@ func (sca *SpellCasterAdapter) checkCastingConditions(spell Spell) error {
} }
// TODO: Implement power checking, cooldown checking, etc. // TODO: Implement power checking, cooldown checking, etc.
return nil return nil
} }
@ -506,9 +506,9 @@ func (ca *CombatAdapter) ProcessCombat() error {
// MovementAdapter provides movement functionality for NPCs // MovementAdapter provides movement functionality for NPCs
type MovementAdapter struct { type MovementAdapter struct {
npc *NPC npc *NPC
movementManager MovementManager movementManager MovementManager
logger Logger logger Logger
} }
// NewMovementAdapter creates a new movement adapter // NewMovementAdapter creates a new movement adapter
@ -567,4 +567,4 @@ func (ma *MovementAdapter) RunbackToSpawn() error {
} }
return ma.MoveToLocation(runbackLocation.X, runbackLocation.Y, runbackLocation.Z) 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 // Manager provides high-level management of the NPC system
type Manager struct { type Manager struct {
npcs map[int32]*NPC // NPCs indexed by ID npcs map[int32]*NPC // NPCs indexed by ID
npcsByZone map[int32][]*NPC // NPCs indexed by zone ID npcsByZone map[int32][]*NPC // NPCs indexed by zone ID
npcsByAppearance map[int32][]*NPC // NPCs indexed by appearance ID npcsByAppearance map[int32][]*NPC // NPCs indexed by appearance ID
database Database // Database interface database Database // Database interface
logger Logger // Logger interface logger Logger // Logger interface
spellManager SpellManager // Spell system interface spellManager SpellManager // Spell system interface
skillManager SkillManager // Skill system interface skillManager SkillManager // Skill system interface
appearanceManager AppearanceManager // Appearance system interface appearanceManager AppearanceManager // Appearance system interface
mutex sync.RWMutex // Thread safety mutex sync.RWMutex // Thread safety
// Statistics // Statistics
totalNPCs int64 totalNPCs int64
npcsInCombat int64 npcsInCombat int64
spellCastCount int64 spellCastCount int64
skillUsageCount int64 skillUsageCount int64
runbackCount int64 runbackCount int64
aiStrategyCounts map[int8]int64 aiStrategyCounts map[int8]int64
// Configuration // Configuration
maxNPCs int32 maxNPCs int32
@ -625,7 +625,7 @@ func (m *Manager) handleSearchCommand(args []string) (string, error) {
} }
searchTerm := strings.ToLower(args[0]) searchTerm := strings.ToLower(args[0])
m.mutex.RLock() m.mutex.RLock()
var results []*NPC var results []*NPC
for _, npc := range m.npcs { for _, npc := range m.npcs {
@ -668,10 +668,10 @@ func (m *Manager) removeFromZoneIndex(npc *NPC) {
if npc.Entity == nil { if npc.Entity == nil {
return return
} }
zoneID := npc.Entity.GetZoneID() zoneID := npc.Entity.GetZoneID()
npcs := m.npcsByZone[zoneID] npcs := m.npcsByZone[zoneID]
for i, n := range npcs { for i, n := range npcs {
if n == npc { if n == npc {
// Remove from slice // Remove from slice
@ -679,7 +679,7 @@ func (m *Manager) removeFromZoneIndex(npc *NPC) {
break break
} }
} }
// Clean up empty slices // Clean up empty slices
if len(m.npcsByZone[zoneID]) == 0 { if len(m.npcsByZone[zoneID]) == 0 {
delete(m.npcsByZone, zoneID) delete(m.npcsByZone, zoneID)
@ -689,7 +689,7 @@ func (m *Manager) removeFromZoneIndex(npc *NPC) {
func (m *Manager) removeFromAppearanceIndex(npc *NPC) { func (m *Manager) removeFromAppearanceIndex(npc *NPC) {
appearanceID := npc.GetAppearanceID() appearanceID := npc.GetAppearanceID()
npcs := m.npcsByAppearance[appearanceID] npcs := m.npcsByAppearance[appearanceID]
for i, n := range npcs { for i, n := range npcs {
if n == npc { if n == npc {
// Remove from slice // Remove from slice
@ -697,7 +697,7 @@ func (m *Manager) removeFromAppearanceIndex(npc *NPC) {
break break
} }
} }
// Clean up empty slices // Clean up empty slices
if len(m.npcsByAppearance[appearanceID]) == 0 { if len(m.npcsByAppearance[appearanceID]) == 0 {
delete(m.npcsByAppearance, appearanceID) delete(m.npcsByAppearance, appearanceID)
@ -708,7 +708,7 @@ func (m *Manager) removeFromAppearanceIndex(npc *NPC) {
func (m *Manager) SetManagers(spellMgr SpellManager, skillMgr SkillManager, appearanceMgr AppearanceManager) { func (m *Manager) SetManagers(spellMgr SpellManager, skillMgr SkillManager, appearanceMgr AppearanceManager) {
m.mutex.Lock() m.mutex.Lock()
defer m.mutex.Unlock() defer m.mutex.Unlock()
m.spellManager = spellMgr m.spellManager = spellMgr
m.skillManager = skillMgr m.skillManager = skillMgr
m.appearanceManager = appearanceMgr m.appearanceManager = appearanceMgr
@ -753,10 +753,10 @@ func (m *Manager) Shutdown() {
brain.SetActive(false) brain.SetActive(false)
} }
} }
// Clear all data // Clear all data
m.npcs = make(map[int32]*NPC) m.npcs = make(map[int32]*NPC)
m.npcsByZone = make(map[int32][]*NPC) m.npcsByZone = make(map[int32][]*NPC)
m.npcsByAppearance = make(map[int32][]*NPC) m.npcsByAppearance = make(map[int32][]*NPC)
m.mutex.Unlock() m.mutex.Unlock()
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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