eq2go/internal/sign/manager.go
2025-08-06 12:31:24 -05:00

505 lines
12 KiB
Go

package sign
import (
"fmt"
"sync"
)
// Manager provides high-level management of the sign system
type Manager struct {
signs map[int32]*Sign // Signs by ID
signsByZone map[int32][]*Sign // Signs by zone ID
signsByWidget map[int32]*Sign // Signs by widget ID
database Database
logger Logger
mutex sync.RWMutex
// Statistics
totalSigns int64
signsByType map[int8]int64 // Sign type -> count
signInteractions int64
zoneTransports int64
transporterUses int64
}
// NewManager creates a new sign manager
func NewManager(database Database, logger Logger) *Manager {
return &Manager{
signs: make(map[int32]*Sign),
signsByZone: make(map[int32][]*Sign),
signsByWidget: make(map[int32]*Sign),
database: database,
logger: logger,
signsByType: make(map[int8]int64),
}
}
// Initialize loads signs from database
func (m *Manager) Initialize() error {
if m.logger != nil {
m.logger.LogInfo("Initializing sign manager...")
}
// TODO: Load all signs from database when database system is integrated
// This would typically iterate through all zones and load their signs
return nil
}
// LoadZoneSigns loads signs for a specific zone
func (m *Manager) LoadZoneSigns(zoneID int32) error {
if m.database == nil {
return fmt.Errorf("database is nil")
}
signs, err := m.database.LoadSigns(zoneID)
if err != nil {
return fmt.Errorf("failed to load signs for zone %d: %w", zoneID, err)
}
m.mutex.Lock()
defer m.mutex.Unlock()
// Store signs by zone ID for now since spawn doesn't have zone support yet
m.signsByZone[zoneID] = signs
for _, sign := range signs {
m.addSignUnsafe(sign)
}
if m.logger != nil {
m.logger.LogInfo("Loaded %d signs for zone %d", len(signs), zoneID)
}
return nil
}
// AddSign adds a sign to the manager
func (m *Manager) AddSign(sign *Sign) error {
if sign == nil {
return fmt.Errorf("sign is nil")
}
// Validate the sign
if issues := sign.Validate(); len(issues) > 0 {
return fmt.Errorf("sign validation failed: %v", issues)
}
m.mutex.Lock()
defer m.mutex.Unlock()
m.addSignUnsafe(sign)
if m.logger != nil {
m.logger.LogInfo("Added sign %d (widget %d) of type %d",
sign.Spawn.GetDatabaseID(), sign.GetWidgetID(), sign.GetSignType())
}
return nil
}
// AddSignToZone adds a sign to the manager for a specific zone
func (m *Manager) AddSignToZone(sign *Sign, zoneID int32) error {
if sign == nil {
return fmt.Errorf("sign is nil")
}
// Validate the sign
if issues := sign.Validate(); len(issues) > 0 {
return fmt.Errorf("sign validation failed: %v", issues)
}
m.mutex.Lock()
defer m.mutex.Unlock()
// Add to zone collection
m.signsByZone[zoneID] = append(m.signsByZone[zoneID], sign)
m.addSignUnsafe(sign)
if m.logger != nil {
m.logger.LogInfo("Added sign %d (widget %d) of type %d to zone %d",
sign.Spawn.GetDatabaseID(), sign.GetWidgetID(), sign.GetSignType(), zoneID)
}
return nil
}
// addSignUnsafe adds a sign without locking (internal use)
func (m *Manager) addSignUnsafe(sign *Sign) {
signID := sign.Spawn.GetDatabaseID()
widgetID := sign.GetWidgetID()
// Add to main collection
m.signs[signID] = sign
// Add to widget collection
if widgetID > 0 {
m.signsByWidget[widgetID] = sign
}
// Add to zone collection
// TODO: Enable when spawn system has zone support
// if sign.Spawn != nil {
// zoneID := sign.Spawn.GetZone()
// m.signsByZone[zoneID] = append(m.signsByZone[zoneID], sign)
// }
// Update statistics
m.totalSigns++
m.signsByType[sign.GetSignType()]++
}
// RemoveSign removes a sign from the manager
func (m *Manager) RemoveSign(signID int32) bool {
m.mutex.Lock()
defer m.mutex.Unlock()
sign, exists := m.signs[signID]
if !exists {
return false
}
// Remove from main collection
delete(m.signs, signID)
// Remove from widget collection
if sign.GetWidgetID() > 0 {
delete(m.signsByWidget, sign.GetWidgetID())
}
// Remove from zone collection
// TODO: Enable when spawn system has zone support
// if sign.Spawn != nil {
// zoneID := sign.Spawn.GetZone()
// if zoneSigns, exists := m.signsByZone[zoneID]; exists {
// for i, zoneSign := range zoneSigns {
// if zoneSign == sign {
// m.signsByZone[zoneID] = append(zoneSigns[:i], zoneSigns[i+1:]...)
// break
// }
// }
// }
// }
// Update statistics
m.totalSigns--
m.signsByType[sign.GetSignType()]--
if m.logger != nil {
m.logger.LogInfo("Removed sign %d (widget %d)", signID, sign.GetWidgetID())
}
return true
}
// GetSign returns a sign by ID
func (m *Manager) GetSign(signID int32) *Sign {
m.mutex.RLock()
defer m.mutex.RUnlock()
return m.signs[signID]
}
// GetSignByWidget returns a sign by widget ID
func (m *Manager) GetSignByWidget(widgetID int32) *Sign {
m.mutex.RLock()
defer m.mutex.RUnlock()
return m.signsByWidget[widgetID]
}
// GetZoneSigns returns all signs in a zone
func (m *Manager) GetZoneSigns(zoneID int32) []*Sign {
m.mutex.RLock()
defer m.mutex.RUnlock()
signs := m.signsByZone[zoneID]
// Return a copy to prevent external modification
result := make([]*Sign, len(signs))
copy(result, signs)
return result
}
// GetSignsByType returns all signs of a specific type
func (m *Manager) GetSignsByType(signType int8) []*Sign {
m.mutex.RLock()
defer m.mutex.RUnlock()
var result []*Sign
for _, sign := range m.signs {
if sign.GetSignType() == signType {
result = append(result, sign)
}
}
return result
}
// SaveSign saves a sign to database
func (m *Manager) SaveSign(sign *Sign) error {
if m.database == nil {
return fmt.Errorf("database is nil")
}
if sign == nil {
return fmt.Errorf("sign is nil")
}
err := m.database.SaveSign(sign)
if err != nil {
return fmt.Errorf("failed to save sign: %w", err)
}
if m.logger != nil {
m.logger.LogDebug("Saved sign %d to database", sign.Spawn.GetDatabaseID())
}
return nil
}
// HandleSignUse processes sign usage and records statistics
func (m *Manager) HandleSignUse(sign *Sign, client Client, command string) error {
if sign == nil {
return fmt.Errorf("sign is nil")
}
m.mutex.Lock()
m.signInteractions++
m.mutex.Unlock()
err := sign.HandleUse(client, command)
// Record specific interaction types for statistics
if err == nil {
m.mutex.Lock()
if sign.IsZoneSign() {
m.zoneTransports++
}
// TODO: Enable when transporter system is implemented
// if sign.Spawn != nil && sign.Spawn.GetTransporterID() > 0 {
// m.transporterUses++
// }
m.mutex.Unlock()
}
if m.logger != nil {
if err != nil {
m.logger.LogError("Sign %d use failed: %v", sign.Spawn.GetDatabaseID(), err)
} else {
m.logger.LogDebug("Sign %d used successfully by character %d",
sign.Spawn.GetDatabaseID(), client.GetCharacterID())
}
}
return err
}
// GetStatistics returns sign system statistics
func (m *Manager) GetStatistics() map[string]any {
m.mutex.RLock()
defer m.mutex.RUnlock()
stats := make(map[string]any)
stats["total_signs"] = m.totalSigns
stats["sign_interactions"] = m.signInteractions
stats["zone_transports"] = m.zoneTransports
stats["transporter_uses"] = m.transporterUses
// Copy sign type statistics
typeStats := make(map[int8]int64)
for signType, count := range m.signsByType {
typeStats[signType] = count
}
stats["signs_by_type"] = typeStats
// Zone statistics
zoneStats := make(map[int32]int)
for zoneID, signs := range m.signsByZone {
zoneStats[zoneID] = len(signs)
}
stats["signs_by_zone"] = zoneStats
return stats
}
// ResetStatistics resets all statistics
func (m *Manager) ResetStatistics() {
m.mutex.Lock()
defer m.mutex.Unlock()
m.signInteractions = 0
m.zoneTransports = 0
m.transporterUses = 0
}
// ValidateAllSigns validates all signs in the system
func (m *Manager) ValidateAllSigns() map[int32][]string {
m.mutex.RLock()
defer m.mutex.RUnlock()
issues := make(map[int32][]string)
for signID, sign := range m.signs {
if signIssues := sign.Validate(); len(signIssues) > 0 {
issues[signID] = signIssues
}
}
return issues
}
// GetSignCount returns the total number of signs
func (m *Manager) GetSignCount() int64 {
m.mutex.RLock()
defer m.mutex.RUnlock()
return m.totalSigns
}
// GetSignTypeCount returns the number of signs of a specific type
func (m *Manager) GetSignTypeCount(signType int8) int64 {
m.mutex.RLock()
defer m.mutex.RUnlock()
return m.signsByType[signType]
}
// ProcessCommand handles sign-related commands
func (m *Manager) ProcessCommand(command string, args []string) (string, error) {
switch command {
case "stats":
return m.handleStatsCommand(args)
case "validate":
return m.handleValidateCommand(args)
case "list":
return m.handleListCommand(args)
case "info":
return m.handleInfoCommand(args)
default:
return "", fmt.Errorf("unknown sign command: %s", command)
}
}
// handleStatsCommand shows sign system statistics
func (m *Manager) handleStatsCommand(args []string) (string, error) {
stats := m.GetStatistics()
result := "Sign System Statistics:\n"
result += fmt.Sprintf("Total Signs: %d\n", stats["total_signs"])
result += fmt.Sprintf("Sign Interactions: %d\n", stats["sign_interactions"])
result += fmt.Sprintf("Zone Transports: %d\n", stats["zone_transports"])
result += fmt.Sprintf("Transporter Uses: %d\n", stats["transporter_uses"])
typeStats := stats["signs_by_type"].(map[int8]int64)
result += fmt.Sprintf("Generic Signs: %d\n", typeStats[SignTypeGeneric])
result += fmt.Sprintf("Zone Signs: %d\n", typeStats[SignTypeZone])
return result, nil
}
// handleValidateCommand validates all signs
func (m *Manager) handleValidateCommand(args []string) (string, error) {
issues := m.ValidateAllSigns()
if len(issues) == 0 {
return "All signs are valid.", nil
}
result := fmt.Sprintf("Found issues with %d signs:\n", len(issues))
count := 0
for signID, signIssues := range issues {
if count >= 10 { // Limit output
result += "... (and more)\n"
break
}
result += fmt.Sprintf("Sign %d:\n", signID)
for _, issue := range signIssues {
result += fmt.Sprintf(" - %s\n", issue)
}
count++
}
return result, nil
}
// handleListCommand lists signs
func (m *Manager) handleListCommand(args []string) (string, error) {
m.mutex.RLock()
defer m.mutex.RUnlock()
if len(m.signs) == 0 {
return "No signs loaded.", nil
}
result := fmt.Sprintf("Signs (%d):\n", len(m.signs))
count := 0
for signID, sign := range m.signs {
if count >= 20 { // Limit output
result += "... (and more)\n"
break
}
typeName := "Generic"
if sign.GetSignType() == SignTypeZone {
typeName = "Zone"
}
result += fmt.Sprintf(" %d: %s (%s, Widget: %d)\n",
signID, sign.GetSignTitle(), typeName, sign.GetWidgetID())
count++
}
return result, nil
}
// handleInfoCommand shows information about a specific sign
func (m *Manager) handleInfoCommand(args []string) (string, error) {
if len(args) == 0 {
return "", fmt.Errorf("sign ID required")
}
// Parse sign ID
var signID int32
if _, err := fmt.Sscanf(args[0], "%d", &signID); err != nil {
return "", fmt.Errorf("invalid sign ID: %s", args[0])
}
sign := m.GetSign(signID)
if sign == nil {
return fmt.Sprintf("Sign %d not found.", signID), nil
}
result := "Sign Information:\n"
result += fmt.Sprintf("ID: %d\n", signID)
result += fmt.Sprintf("Widget ID: %d\n", sign.GetWidgetID())
result += fmt.Sprintf("Type: %d\n", sign.GetSignType())
result += fmt.Sprintf("Title: %s\n", sign.GetSignTitle())
result += fmt.Sprintf("Description: %s\n", sign.GetSignDescription())
result += fmt.Sprintf("Language: %d\n", sign.GetLanguage())
if sign.IsZoneSign() {
result += fmt.Sprintf("Zone ID: %d\n", sign.GetSignZoneID())
result += fmt.Sprintf("Zone Coords: (%.2f, %.2f, %.2f)\n",
sign.GetSignZoneX(), sign.GetSignZoneY(), sign.GetSignZoneZ())
result += fmt.Sprintf("Zone Heading: %.2f\n", sign.GetSignZoneHeading())
result += fmt.Sprintf("Distance: %.2f\n", sign.GetSignDistance())
}
result += fmt.Sprintf("Include Location: %t\n", sign.GetIncludeLocation())
result += fmt.Sprintf("Include Heading: %t\n", sign.GetIncludeHeading())
return result, nil
}
// Shutdown gracefully shuts down the manager
func (m *Manager) Shutdown() {
if m.logger != nil {
m.logger.LogInfo("Shutting down sign manager...")
}
// Nothing to clean up currently, but placeholder for future cleanup
}