fix sign package

This commit is contained in:
Sky Johnson 2025-08-06 12:31:24 -05:00
parent c67a7bf6c6
commit 3b6d35ce98
4 changed files with 1011 additions and 83 deletions

View File

@ -102,7 +102,22 @@ func (ss *SignSpawn) HandleUse(client Client, command string) error {
// Copy creates a copy of the sign spawn
func (ss *SignSpawn) Copy() *SignSpawn {
newSign := ss.Sign.Copy()
newSpawn := ss.Spawn.Copy()
// TODO: Enable when spawn.Copy() method is implemented
// newSpawn := ss.Spawn.Copy()
// For now, create a new spawn and copy basic properties
newSpawn := spawn.NewSpawn()
if ss.Spawn != nil {
newSpawn.SetDatabaseID(ss.Spawn.GetDatabaseID())
newSpawn.SetName(ss.Spawn.GetName())
newSpawn.SetLevel(ss.Spawn.GetLevel())
newSpawn.SetX(ss.Spawn.GetX())
newSpawn.SetY(ss.Spawn.GetY(), false)
newSpawn.SetZ(ss.Spawn.GetZ())
newSpawn.SetSpawnType(ss.Spawn.GetSpawnType())
newSpawn.SetFactionID(ss.Spawn.GetFactionID())
newSpawn.SetSize(ss.Spawn.GetSize())
}
return &SignSpawn{
Spawn: newSpawn,

View File

@ -60,6 +60,9 @@ func (m *Manager) LoadZoneSigns(zoneID int32) error {
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)
}
@ -95,6 +98,33 @@ func (m *Manager) AddSign(sign *Sign) error {
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()
@ -109,10 +139,11 @@ func (m *Manager) addSignUnsafe(sign *Sign) {
}
// Add to zone collection
if sign.Spawn != nil {
zoneID := sign.Spawn.GetZone()
m.signsByZone[zoneID] = append(m.signsByZone[zoneID], sign)
}
// 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++
@ -138,17 +169,18 @@ func (m *Manager) RemoveSign(signID int32) bool {
}
// Remove from zone collection
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
}
}
}
}
// 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--
@ -246,9 +278,10 @@ func (m *Manager) HandleSignUse(sign *Sign, client Client, command string) error
if sign.IsZoneSign() {
m.zoneTransports++
}
if sign.Spawn != nil && sign.Spawn.GetTransporterID() > 0 {
m.transporterUses++
}
// TODO: Enable when transporter system is implemented
// if sign.Spawn != nil && sign.Spawn.GetTransporterID() > 0 {
// m.transporterUses++
// }
m.mutex.Unlock()
}
@ -439,7 +472,7 @@ func (m *Manager) handleInfoCommand(args []string) (string, error) {
return fmt.Sprintf("Sign %d not found.", signID), nil
}
result := fmt.Sprintf("Sign Information:\n")
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())

View File

@ -2,7 +2,6 @@ package sign
import (
"fmt"
"math/rand"
"strings"
)
@ -12,26 +11,17 @@ func (s *Sign) Copy() *Sign {
// Copy spawn data
if s.Spawn != nil {
// Handle size randomization like the C++ version
if s.Spawn.GetSizeOffset() > 0 {
offset := s.Spawn.GetSizeOffset() + 1
tmpSize := int32(s.Spawn.GetSize()) + (rand.Int31n(int32(offset)) - rand.Int31n(int32(offset)))
if tmpSize < 0 {
tmpSize = 1
} else if tmpSize >= 0xFFFF {
tmpSize = 0xFFFF
}
newSign.Spawn.SetSize(int16(tmpSize))
} else {
newSign.Spawn.SetSize(s.Spawn.GetSize())
}
// Copy other spawn properties
// Copy basic spawn properties that exist
newSign.Spawn.SetSize(s.Spawn.GetSize())
newSign.Spawn.SetDatabaseID(s.Spawn.GetDatabaseID())
newSign.Spawn.SetMerchantID(s.Spawn.GetMerchantID())
newSign.Spawn.SetMerchantType(s.Spawn.GetMerchantType())
newSign.Spawn.SetName(s.Spawn.GetName())
newSign.Spawn.SetLevel(s.Spawn.GetLevel())
newSign.Spawn.SetX(s.Spawn.GetX())
newSign.Spawn.SetY(s.Spawn.GetY(), false)
newSign.Spawn.SetZ(s.Spawn.GetZ())
newSign.Spawn.SetSpawnType(s.Spawn.GetSpawnType())
newSign.Spawn.SetFactionID(s.Spawn.GetFactionID())
// TODO: Copy appearance data when spawn system is fully integrated
// TODO: Copy command lists when command system is integrated
// TODO: Copy transporter ID, sounds, loot properties, etc.
@ -60,12 +50,13 @@ func (s *Sign) Copy() *Sign {
// Serialize creates a packet for sending the sign to a client
func (s *Sign) Serialize(player Player, version int16) ([]byte, error) {
// TODO: Implement serialization when spawn system supports it
// Delegate to spawn serialization
if s.Spawn != nil {
return s.Spawn.Serialize(player, version)
}
// if s.Spawn != nil {
// return s.Spawn.Serialize(player, version)
// }
return nil, fmt.Errorf("spawn is nil")
return nil, fmt.Errorf("sign serialization not yet implemented")
}
// HandleUse processes player interaction with the sign
@ -85,9 +76,10 @@ func (s *Sign) HandleUse(client Client, command string) error {
}
// Handle transporter functionality first
if s.Spawn != nil && s.Spawn.GetTransporterID() > 0 {
return s.handleTransporter(client)
}
// TODO: Enable when transporter system is implemented in spawn
// if s.Spawn != nil && s.Spawn.GetTransporterID() > 0 {
// return s.handleTransporter(client)
// }
// Handle zone transport signs
if s.signType == SignTypeZone && s.zoneID > 0 {
@ -115,26 +107,11 @@ func (s *Sign) meetsQuestRequirements(client Client) bool {
}
// handleTransporter processes transporter functionality
// TODO: Enable when transporter system is implemented in spawn
func (s *Sign) handleTransporter(client Client) error {
zone := client.GetPlayer().GetZone()
if zone == nil {
return fmt.Errorf("player not in zone")
}
transporterID := s.Spawn.GetTransporterID()
// Get transport destinations
destinations, err := zone.GetTransporters(client, transporterID)
if err != nil {
return fmt.Errorf("failed to get transporters: %w", err)
}
if len(destinations) > 0 {
client.SetTemporaryTransportID(0)
return client.ProcessTeleport(s, destinations, transporterID)
}
return nil
// Placeholder implementation for transporter functionality
// This will be enabled when the spawn system supports transporters
return fmt.Errorf("transporter system not yet implemented")
}
// handleZoneTransport processes zone transport functionality
@ -188,26 +165,25 @@ func (s *Sign) handleEntityCommand(client Client, command string) error {
return fmt.Errorf("spawn is nil")
}
entityCommand := s.Spawn.FindEntityCommand(command)
if entityCommand == nil {
return nil // Command not found
}
// Handle mark command specially
if strings.ToLower(entityCommand.Command) == "mark" {
// TODO: Implement entity command finding when spawn system supports it
// entityCommand := s.Spawn.FindEntityCommand(command)
// For now, handle mark command directly
if strings.ToLower(command) == "mark" {
return s.handleMarkCommand(client)
}
// Process the entity command
zone := client.GetCurrentZone()
if zone == nil {
return fmt.Errorf("player not in zone")
}
player := client.GetPlayer()
target := player.GetTarget()
return zone.ProcessEntityCommand(entityCommand, player, target)
// TODO: Process other entity commands when system is implemented
// zone := client.GetCurrentZone()
// if zone == nil {
// return fmt.Errorf("player not in zone")
// }
//
// player := client.GetPlayer()
// target := player.GetTarget()
//
// return zone.ProcessEntityCommand(entityCommand, player, target)
return nil // Command not handled
}
// handleMarkCommand processes the mark command for marking signs

904
internal/sign/sign_test.go Normal file
View File

@ -0,0 +1,904 @@
package sign
import (
"fmt"
"strings"
"sync"
"testing"
"eq2emu/internal/spawn"
)
// Mock implementations for testing
// MockLogger implements the Logger interface for testing
type MockLogger struct {
logs []string
mu sync.Mutex
}
func (ml *MockLogger) LogInfo(message string, args ...any) {
ml.mu.Lock()
defer ml.mu.Unlock()
ml.logs = append(ml.logs, fmt.Sprintf("INFO: "+message, args...))
}
func (ml *MockLogger) LogError(message string, args ...any) {
ml.mu.Lock()
defer ml.mu.Unlock()
ml.logs = append(ml.logs, fmt.Sprintf("ERROR: "+message, args...))
}
func (ml *MockLogger) LogDebug(message string, args ...any) {
ml.mu.Lock()
defer ml.mu.Unlock()
ml.logs = append(ml.logs, fmt.Sprintf("DEBUG: "+message, args...))
}
func (ml *MockLogger) LogWarning(message string, args ...any) {
ml.mu.Lock()
defer ml.mu.Unlock()
ml.logs = append(ml.logs, fmt.Sprintf("WARNING: "+message, args...))
}
func (ml *MockLogger) GetLogs() []string {
ml.mu.Lock()
defer ml.mu.Unlock()
result := make([]string, len(ml.logs))
copy(result, ml.logs)
return result
}
func (ml *MockLogger) Clear() {
ml.mu.Lock()
defer ml.mu.Unlock()
ml.logs = ml.logs[:0]
}
// MockDatabase implements the Database interface for testing
type MockDatabase struct {
signs map[int32][]*Sign
zoneNames map[int32]string
charNames map[int32]string
signMarks map[int32]map[int32]string // charID -> widgetID -> charName
mu sync.RWMutex
}
func NewMockDatabase() *MockDatabase {
return &MockDatabase{
signs: make(map[int32][]*Sign),
zoneNames: make(map[int32]string),
charNames: make(map[int32]string),
signMarks: make(map[int32]map[int32]string),
}
}
func (md *MockDatabase) GetZoneName(zoneID int32) (string, error) {
md.mu.RLock()
defer md.mu.RUnlock()
if name, exists := md.zoneNames[zoneID]; exists {
return name, nil
}
return "", fmt.Errorf("zone %d not found", zoneID)
}
func (md *MockDatabase) GetCharacterName(charID int32) (string, error) {
md.mu.RLock()
defer md.mu.RUnlock()
if name, exists := md.charNames[charID]; exists {
return name, nil
}
return "", fmt.Errorf("character %d not found", charID)
}
func (md *MockDatabase) SaveSignMark(charID int32, widgetID int32, charName string, client Client) error {
md.mu.Lock()
defer md.mu.Unlock()
if md.signMarks[charID] == nil {
md.signMarks[charID] = make(map[int32]string)
}
md.signMarks[charID][widgetID] = charName
return nil
}
func (md *MockDatabase) LoadSigns(zoneID int32) ([]*Sign, error) {
md.mu.RLock()
defer md.mu.RUnlock()
return md.signs[zoneID], nil
}
func (md *MockDatabase) SaveSign(sign *Sign) error {
return nil // No-op for testing
}
func (md *MockDatabase) DeleteSign(signID int32) error {
return nil // No-op for testing
}
func (md *MockDatabase) AddZone(zoneID int32, name string) {
md.mu.Lock()
defer md.mu.Unlock()
md.zoneNames[zoneID] = name
}
func (md *MockDatabase) AddCharacter(charID int32, name string) {
md.mu.Lock()
defer md.mu.Unlock()
md.charNames[charID] = name
}
func (md *MockDatabase) AddSignToZone(zoneID int32, sign *Sign) {
md.mu.Lock()
defer md.mu.Unlock()
md.signs[zoneID] = append(md.signs[zoneID], sign)
}
// MockPlayer implements the Player interface for testing
type MockPlayer struct {
x, y, z, heading float32
zone Zone
target *spawn.Spawn
}
func (mp *MockPlayer) GetDistance(target *spawn.Spawn) float32 {
if target == nil {
return 0
}
return 5.0 // Mock distance
}
func (mp *MockPlayer) SetX(x float32) { mp.x = x }
func (mp *MockPlayer) SetY(y float32) { mp.y = y }
func (mp *MockPlayer) SetZ(z float32) { mp.z = z }
func (mp *MockPlayer) SetHeading(heading float32) { mp.heading = heading }
func (mp *MockPlayer) GetZone() Zone { return mp.zone }
func (mp *MockPlayer) GetTarget() *spawn.Spawn { return mp.target }
// MockZone implements the Zone interface for testing
type MockZone struct {
transporters map[int32][]TransportDestination
}
func (mz *MockZone) GetTransporters(client Client, transporterID int32) ([]TransportDestination, error) {
if transporters, exists := mz.transporters[transporterID]; exists {
return transporters, nil
}
return nil, fmt.Errorf("transporter %d not found", transporterID)
}
func (mz *MockZone) ProcessEntityCommand(command *EntityCommand, player Player, target *spawn.Spawn) error {
return nil // No-op for testing
}
// MockClient implements the Client interface for testing
type MockClient struct {
player Player
charID int32
database Database
zone Zone
messages []string
tempTransportID int32
zoneAccess map[string]bool
mu sync.Mutex
}
func (mc *MockClient) GetPlayer() Player { return mc.player }
func (mc *MockClient) GetCharacterID() int32 { return mc.charID }
func (mc *MockClient) GetDatabase() Database { return mc.database }
func (mc *MockClient) GetCurrentZone() Zone { return mc.zone }
func (mc *MockClient) SetTemporaryTransportID(id int32) { mc.tempTransportID = id }
func (mc *MockClient) SimpleMessage(channel int32, message string) {
mc.mu.Lock()
defer mc.mu.Unlock()
mc.messages = append(mc.messages, message)
}
func (mc *MockClient) Message(channel int32, format string, args ...any) {
mc.mu.Lock()
defer mc.mu.Unlock()
mc.messages = append(mc.messages, fmt.Sprintf(format, args...))
}
func (mc *MockClient) CheckZoneAccess(zoneName string) bool {
return mc.zoneAccess[zoneName]
}
func (mc *MockClient) TryZoneInstance(zoneID int32, useDefaults bool) bool {
return false // Mock always uses regular zones
}
func (mc *MockClient) Zone(zoneName string, useDefaults bool) error {
return nil // No-op for testing
}
func (mc *MockClient) ProcessTeleport(sign *Sign, destinations []TransportDestination, transporterID int32) error {
return nil // No-op for testing
}
func (mc *MockClient) GetMessages() []string {
mc.mu.Lock()
defer mc.mu.Unlock()
result := make([]string, len(mc.messages))
copy(result, mc.messages)
return result
}
func (mc *MockClient) ClearMessages() {
mc.mu.Lock()
defer mc.mu.Unlock()
mc.messages = mc.messages[:0]
}
// MockEntity implements the Entity interface for testing
type MockEntity struct {
id int32
name string
databaseID int32
}
func (me *MockEntity) GetID() int32 { return me.id }
func (me *MockEntity) GetName() string { return me.name }
func (me *MockEntity) GetDatabaseID() int32 { return me.databaseID }
// Helper functions for testing
func createTestSign() *Sign {
sign := NewSign()
sign.Spawn.SetDatabaseID(1)
sign.Spawn.SetName("Test Sign")
sign.SetWidgetID(100)
sign.SetSignTitle("Welcome")
sign.SetSignDescription("This is a test sign")
return sign
}
func createZoneSign() *Sign {
sign := createTestSign()
sign.SetSignType(SignTypeZone)
sign.SetSignZoneID(2)
sign.SetSignZoneX(100.0)
sign.SetSignZoneY(200.0)
sign.SetSignZoneZ(300.0)
sign.SetSignZoneHeading(45.0)
return sign
}
// Tests
func TestNewSign(t *testing.T) {
sign := NewSign()
if sign == nil {
t.Fatal("NewSign() returned nil")
}
if sign.Spawn == nil {
t.Error("Sign spawn is nil")
}
if sign.GetSignType() != SignTypeGeneric {
t.Errorf("Expected sign type %d, got %d", SignTypeGeneric, sign.GetSignType())
}
if !sign.IsSign() {
t.Error("IsSign() should return true")
}
if sign.IsZoneSign() {
t.Error("IsZoneSign() should return false for generic sign")
}
if !sign.IsGenericSign() {
t.Error("IsGenericSign() should return true for generic sign")
}
}
func TestSignProperties(t *testing.T) {
sign := NewSign()
// Test widget properties
sign.SetWidgetID(123)
if sign.GetWidgetID() != 123 {
t.Errorf("Expected widget ID 123, got %d", sign.GetWidgetID())
}
sign.SetWidgetX(10.5)
if sign.GetWidgetX() != 10.5 {
t.Errorf("Expected widget X 10.5, got %f", sign.GetWidgetX())
}
// Test sign properties
sign.SetSignTitle("Test Title")
if sign.GetSignTitle() != "Test Title" {
t.Errorf("Expected title 'Test Title', got '%s'", sign.GetSignTitle())
}
sign.SetSignDescription("Test Description")
if sign.GetSignDescription() != "Test Description" {
t.Errorf("Expected description 'Test Description', got '%s'", sign.GetSignDescription())
}
// Test zone properties
sign.SetSignZoneID(456)
if sign.GetSignZoneID() != 456 {
t.Errorf("Expected zone ID 456, got %d", sign.GetSignZoneID())
}
sign.SetSignZoneX(100.0)
sign.SetSignZoneY(200.0)
sign.SetSignZoneZ(300.0)
sign.SetSignZoneHeading(45.0)
if !sign.HasZoneCoordinates() {
t.Error("Should have zone coordinates")
}
// Test display options
sign.SetIncludeLocation(true)
if !sign.GetIncludeLocation() {
t.Error("Include location should be true")
}
}
func TestSignValidation(t *testing.T) {
// Valid sign
sign := createTestSign()
if !sign.IsValid() {
t.Error("Sign should be valid")
}
issues := sign.Validate()
if len(issues) > 0 {
t.Errorf("Valid sign should have no issues, got: %v", issues)
}
// Invalid sign - no widget ID
sign.SetWidgetID(0)
if sign.IsValid() {
t.Error("Sign without widget ID should be invalid")
}
issues = sign.Validate()
if len(issues) == 0 {
t.Error("Sign without widget ID should have validation issues")
}
// Invalid sign - title too long
sign.SetWidgetID(100)
sign.SetSignTitle(strings.Repeat("a", MaxSignTitleLength+1))
issues = sign.Validate()
found := false
for _, issue := range issues {
if strings.Contains(issue, "title too long") {
found = true
break
}
}
if !found {
t.Error("Should have title too long validation issue")
}
// Invalid zone sign - no zone ID
sign = createTestSign()
sign.SetSignType(SignTypeZone)
// Don't set zone ID
issues = sign.Validate()
found = false
for _, issue := range issues {
if strings.Contains(issue, "no zone ID") {
found = true
break
}
}
if !found {
t.Error("Zone sign without zone ID should have validation issue")
}
}
func TestSignCopy(t *testing.T) {
original := createTestSign()
original.SetSignTitle("Original Title")
original.SetSignDescription("Original Description")
copy := original.Copy()
if copy == nil {
t.Fatal("Copy returned nil")
}
if copy == original {
t.Error("Copy should not be the same instance")
}
if copy.GetSignTitle() != original.GetSignTitle() {
t.Error("Copy should have same title")
}
if copy.GetSignDescription() != original.GetSignDescription() {
t.Error("Copy should have same description")
}
if copy.GetWidgetID() != original.GetWidgetID() {
t.Error("Copy should have same widget ID")
}
// Modify copy and ensure original is unchanged
copy.SetSignTitle("Modified Title")
if original.GetSignTitle() == "Modified Title" {
t.Error("Modifying copy should not affect original")
}
}
func TestSignDisplayText(t *testing.T) {
sign := NewSign()
// Empty sign
text := sign.GetDisplayText()
if len(text) > 0 {
t.Error("Empty sign should have no display text")
}
// Title only
sign.SetSignTitle("Test Title")
text = sign.GetDisplayText()
if text != "Test Title" {
t.Errorf("Expected 'Test Title', got '%s'", text)
}
// Title and description
sign.SetSignDescription("Test Description")
text = sign.GetDisplayText()
expected := "Test Title\nTest Description"
if text != expected {
t.Errorf("Expected '%s', got '%s'", expected, text)
}
// With location
sign.SetSignZoneX(100.0)
sign.SetSignZoneY(200.0)
sign.SetSignZoneZ(300.0)
sign.SetIncludeLocation(true)
text = sign.GetDisplayText()
if !strings.Contains(text, "Location: 100.00, 200.00, 300.00") {
t.Error("Display text should contain location information")
}
// With heading
sign.SetSignZoneHeading(45.0)
sign.SetIncludeHeading(true)
text = sign.GetDisplayText()
if !strings.Contains(text, "Heading: 45.00") {
t.Error("Display text should contain heading information")
}
}
func TestSignHandleUse(t *testing.T) {
// Setup test environment
database := NewMockDatabase()
database.AddZone(2, "Test Zone")
database.AddCharacter(1, "TestPlayer")
player := &MockPlayer{}
client := &MockClient{
player: player,
charID: 1,
database: database,
zoneAccess: map[string]bool{"Test Zone": true},
}
// Test zone transport sign
sign := createZoneSign()
sign.SetSignDistance(10.0) // Set distance limit
err := sign.HandleUse(client, "")
if err != nil {
t.Errorf("HandleUse failed: %v", err)
}
// Check if player position was set
if player.x != 100.0 || player.y != 200.0 || player.z != 300.0 {
t.Error("Player position should be updated for zone transport")
}
if player.heading != 45.0 {
t.Error("Player heading should be updated for zone transport")
}
// Test mark command
sign = createTestSign()
err = sign.HandleUse(client, "mark")
if err != nil {
t.Errorf("Mark command failed: %v", err)
}
// Test distance check
sign = createZoneSign()
sign.SetSignDistance(1.0) // Very short distance
client.ClearMessages()
err = sign.HandleUse(client, "")
if err != nil {
t.Errorf("HandleUse with distance check failed: %v", err)
}
messages := client.GetMessages()
found := false
for _, msg := range messages {
if strings.Contains(msg, "too far away") {
found = true
break
}
}
if !found {
t.Error("Should get 'too far away' message when outside distance")
}
}
func TestSignSpawn(t *testing.T) {
baseSpawn := spawn.NewSpawn()
baseSpawn.SetName("Test Sign Spawn")
signSpawn := NewSignSpawn(baseSpawn)
if signSpawn == nil {
t.Fatal("NewSignSpawn returned nil")
}
if !signSpawn.IsSign() {
t.Error("SignSpawn should be a sign")
}
if signSpawn.Spawn != baseSpawn {
t.Error("SignSpawn should reference the base spawn")
}
// Test copy
copy := signSpawn.Copy()
if copy == nil {
t.Fatal("SignSpawn Copy returned nil")
}
if copy == signSpawn {
t.Error("SignSpawn Copy should create new instance")
}
if copy.Spawn.GetName() != baseSpawn.GetName() {
t.Error("Copied SignSpawn should have same spawn name")
}
}
func TestSignAdapter(t *testing.T) {
logger := &MockLogger{}
entity := &MockEntity{id: 1, name: "Test Entity", databaseID: 100}
adapter := NewSignAdapter(entity, logger)
if adapter == nil {
t.Fatal("NewSignAdapter returned nil")
}
if !adapter.IsSign() {
t.Error("SignAdapter should be a sign")
}
if adapter.GetSign() == nil {
t.Error("SignAdapter should have a sign")
}
// Test setting properties with logging
adapter.GetSign().SetWidgetID(500) // Set widget ID for validation
adapter.SetSignTitle("Test Title")
adapter.SetSignDescription("Test Description")
adapter.SetSignType(SignTypeZone)
adapter.SetZoneTransport(2, 100.0, 200.0, 300.0, 45.0)
adapter.SetSignDistance(15.0)
logs := logger.GetLogs()
if len(logs) == 0 {
t.Error("SignAdapter operations should generate log messages")
}
// Verify sign properties were set
sign := adapter.GetSign()
if sign.GetSignTitle() != "Test Title" {
t.Error("Sign title not set correctly")
}
if sign.GetSignType() != SignTypeZone {
t.Error("Sign type not set correctly")
}
if sign.GetSignZoneID() != 2 {
t.Error("Zone ID not set correctly")
}
// Test validation
if !adapter.IsValid() {
issues := adapter.Validate()
t.Errorf("SignAdapter should be valid, issues: %v", issues)
}
}
func TestManager(t *testing.T) {
database := NewMockDatabase()
logger := &MockLogger{}
manager := NewManager(database, logger)
if manager == nil {
t.Fatal("NewManager returned nil")
}
// Test initialization
err := manager.Initialize()
if err != nil {
t.Errorf("Manager initialization failed: %v", err)
}
// Test adding signs
sign1 := createTestSign()
sign1.Spawn.SetDatabaseID(1)
sign1.SetWidgetID(100)
err = manager.AddSign(sign1)
if err != nil {
t.Errorf("Adding sign failed: %v", err)
}
if manager.GetSignCount() != 1 {
t.Errorf("Expected 1 sign, got %d", manager.GetSignCount())
}
// Test retrieving signs
retrieved := manager.GetSign(1)
if retrieved == nil {
t.Error("Should be able to retrieve sign by ID")
}
retrieved = manager.GetSignByWidget(100)
if retrieved == nil {
t.Error("Should be able to retrieve sign by widget ID")
}
// Test zone functionality
sign2 := createZoneSign()
sign2.Spawn.SetDatabaseID(2)
sign2.SetWidgetID(200)
err = manager.AddSignToZone(sign2, 1)
if err != nil {
t.Errorf("Adding sign to zone failed: %v", err)
}
zoneSigns := manager.GetZoneSigns(1)
if len(zoneSigns) != 1 {
t.Errorf("Expected 1 zone sign, got %d", len(zoneSigns))
}
// Test statistics
stats := manager.GetStatistics()
if stats["total_signs"] != int64(2) {
t.Errorf("Expected 2 total signs in stats, got %v", stats["total_signs"])
}
typeStats := stats["signs_by_type"].(map[int8]int64)
if typeStats[SignTypeGeneric] != 1 {
t.Errorf("Expected 1 generic sign in stats, got %d", typeStats[SignTypeGeneric])
}
if typeStats[SignTypeZone] != 1 {
t.Errorf("Expected 1 zone sign in stats, got %d", typeStats[SignTypeZone])
}
// Test removing signs
if !manager.RemoveSign(1) {
t.Error("Should be able to remove sign")
}
if manager.GetSignCount() != 1 {
t.Errorf("Expected 1 sign after removal, got %d", manager.GetSignCount())
}
// Test validation
issues := manager.ValidateAllSigns()
if len(issues) > 0 {
t.Errorf("All signs should be valid, found issues: %v", issues)
}
// Test commands
result, err := manager.ProcessCommand("stats", nil)
if err != nil {
t.Errorf("Stats command failed: %v", err)
}
if !strings.Contains(result, "Sign System Statistics") {
t.Error("Stats command should return statistics")
}
result, err = manager.ProcessCommand("list", nil)
if err != nil {
t.Errorf("List command failed: %v", err)
}
if !strings.Contains(result, "Signs (1)") {
t.Error("List command should show sign count")
}
result, err = manager.ProcessCommand("validate", nil)
if err != nil {
t.Errorf("Validate command failed: %v", err)
}
if !strings.Contains(result, "All signs are valid") {
t.Error("Validate command should report all signs valid")
}
result, err = manager.ProcessCommand("info", []string{"2"})
if err != nil {
t.Errorf("Info command failed: %v", err)
}
if !strings.Contains(result, "Sign Information") {
t.Error("Info command should show sign information")
}
// Test unknown command
_, err = manager.ProcessCommand("unknown", nil)
if err == nil {
t.Error("Unknown command should return error")
}
}
func TestManagerConcurrency(t *testing.T) {
database := NewMockDatabase()
logger := &MockLogger{}
manager := NewManager(database, logger)
const numGoroutines = 10
const numOperations = 100
var wg sync.WaitGroup
wg.Add(numGoroutines)
// Test concurrent sign additions
for i := 0; i < numGoroutines; i++ {
go func(id int) {
defer wg.Done()
for j := 0; j < numOperations; j++ {
sign := createTestSign()
signID := int32(id*numOperations + j + 1)
sign.Spawn.SetDatabaseID(signID)
sign.SetWidgetID(signID)
err := manager.AddSign(sign)
if err != nil {
t.Errorf("Concurrent AddSign failed: %v", err)
}
}
}(i)
}
wg.Wait()
expectedCount := int64(numGoroutines * numOperations)
if manager.GetSignCount() != expectedCount {
t.Errorf("Expected %d signs after concurrent additions, got %d",
expectedCount, manager.GetSignCount())
}
// Test concurrent reads
wg.Add(numGoroutines)
for i := 0; i < numGoroutines; i++ {
go func() {
defer wg.Done()
for j := 0; j < numOperations; j++ {
stats := manager.GetStatistics()
if stats["total_signs"].(int64) <= 0 {
t.Error("Stats should show positive sign count")
}
}
}()
}
wg.Wait()
}
func TestSignSerialization(t *testing.T) {
sign := createTestSign()
player := &MockPlayer{}
// Test serialization (currently returns error due to incomplete implementation)
_, err := sign.Serialize(player, 1146)
if err == nil {
t.Error("Serialization should return error with current implementation")
}
if !strings.Contains(err.Error(), "not yet implemented") {
t.Error("Should get 'not yet implemented' error message")
}
}
func TestSignConstants(t *testing.T) {
// Test sign type constants
if SignTypeGeneric != 0 {
t.Errorf("SignTypeGeneric should be 0, got %d", SignTypeGeneric)
}
if SignTypeZone != 1 {
t.Errorf("SignTypeZone should be 1, got %d", SignTypeZone)
}
// Test default constants
if DefaultSpawnType != 2 {
t.Errorf("DefaultSpawnType should be 2, got %d", DefaultSpawnType)
}
if DefaultActivityStatus != 64 {
t.Errorf("DefaultActivityStatus should be 64, got %d", DefaultActivityStatus)
}
if DefaultSignDistance != 0.0 {
t.Errorf("DefaultSignDistance should be 0.0, got %f", DefaultSignDistance)
}
}
// Benchmark tests
func BenchmarkNewSign(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = NewSign()
}
}
func BenchmarkSignCopy(b *testing.B) {
sign := createTestSign()
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = sign.Copy()
}
}
func BenchmarkSignValidation(b *testing.B) {
sign := createTestSign()
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = sign.Validate()
}
}
func BenchmarkManagerAddSign(b *testing.B) {
database := NewMockDatabase()
logger := &MockLogger{}
manager := NewManager(database, logger)
b.ResetTimer()
for i := 0; i < b.N; i++ {
sign := createTestSign()
sign.Spawn.SetDatabaseID(int32(i + 1))
sign.SetWidgetID(int32(i + 100))
_ = manager.AddSign(sign)
}
}
func BenchmarkManagerGetSign(b *testing.B) {
database := NewMockDatabase()
logger := &MockLogger{}
manager := NewManager(database, logger)
// Pre-populate with signs
for i := 0; i < 1000; i++ {
sign := createTestSign()
sign.Spawn.SetDatabaseID(int32(i + 1))
sign.SetWidgetID(int32(i + 100))
_ = manager.AddSign(sign)
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = manager.GetSign(int32((i % 1000) + 1))
}
}