eq2go/internal/zone/zone_test.go
2025-08-06 14:39:39 -05:00

896 lines
26 KiB
Go

package zone
import (
"path/filepath"
"sync"
"testing"
"time"
"eq2emu/internal/spawn"
"zombiezen.com/go/sqlite"
"zombiezen.com/go/sqlite/sqlitex"
)
// Mock implementations for testing
// MockSpawn implements basic spawn functionality for testing
type MockSpawn struct {
id int32
x, y, z float32
heading float32
name string
}
func (ms *MockSpawn) GetID() int32 { return ms.id }
func (ms *MockSpawn) GetPosition() (x, y, z, heading float32) { return ms.x, ms.y, ms.z, ms.heading }
func (ms *MockSpawn) SetPosition(x, y, z, heading float32) { ms.x, ms.y, ms.z, ms.heading = x, y, z, heading }
func (ms *MockSpawn) GetName() string { return ms.name }
func (ms *MockSpawn) SetName(name string) { ms.name = name }
func (ms *MockSpawn) GetX() float32 { return ms.x }
func (ms *MockSpawn) GetY() float32 { return ms.y }
func (ms *MockSpawn) GetZ() float32 { return ms.z }
func (ms *MockSpawn) GetHeading() float32 { return ms.heading }
func (ms *MockSpawn) SetX(x float32) { ms.x = x }
func (ms *MockSpawn) SetY(y float32, updateClients bool) { ms.y = y }
func (ms *MockSpawn) SetZ(z float32) { ms.z = z }
func (ms *MockSpawn) SetHeadingFromFloat(heading float32) { ms.heading = heading }
// TestDatabaseOperations tests database CRUD operations
func TestDatabaseOperations(t *testing.T) {
// Create temporary database
conn, err := sqlite.OpenConn(":memory:", sqlite.OpenReadWrite|sqlite.OpenCreate)
if err != nil {
t.Fatalf("Failed to create test database: %v", err)
}
defer conn.Close()
// Create test schema
schema := `
CREATE TABLE IF NOT EXISTS zones (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
file TEXT,
description TEXT,
safe_x REAL DEFAULT 0,
safe_y REAL DEFAULT 0,
safe_z REAL DEFAULT 0,
safe_heading REAL DEFAULT 0,
underworld REAL DEFAULT -1000,
min_level INTEGER DEFAULT 0,
max_level INTEGER DEFAULT 0,
min_status INTEGER DEFAULT 0,
min_version INTEGER DEFAULT 0,
instance_type INTEGER DEFAULT 0,
max_players INTEGER DEFAULT 100,
default_lockout_time INTEGER DEFAULT 18000,
default_reenter_time INTEGER DEFAULT 3600,
default_reset_time INTEGER DEFAULT 259200,
group_zone_option INTEGER DEFAULT 0,
expansion_flag INTEGER DEFAULT 0,
holiday_flag INTEGER DEFAULT 0,
can_bind INTEGER DEFAULT 1,
can_gate INTEGER DEFAULT 1,
can_evac INTEGER DEFAULT 1,
city_zone INTEGER DEFAULT 0,
always_loaded INTEGER DEFAULT 0,
weather_allowed INTEGER DEFAULT 1
);
CREATE TABLE IF NOT EXISTS spawn_location_placement (
id INTEGER PRIMARY KEY AUTOINCREMENT,
zone_id INTEGER,
x REAL,
y REAL,
z REAL,
heading REAL,
pitch REAL DEFAULT 0,
roll REAL DEFAULT 0,
spawn_type INTEGER DEFAULT 0,
respawn_time INTEGER DEFAULT 300,
expire_time INTEGER DEFAULT 0,
expire_offset INTEGER DEFAULT 0,
conditions INTEGER DEFAULT 0,
conditional_value INTEGER DEFAULT 0,
spawn_percentage REAL DEFAULT 100.0
);
CREATE TABLE IF NOT EXISTS spawn_location_group (
group_id INTEGER,
location_id INTEGER,
zone_id INTEGER,
PRIMARY KEY (group_id, location_id)
);
-- Insert test data
INSERT INTO zones (id, name, file, description, safe_x, safe_y, safe_z)
VALUES (1, 'test_zone', 'test.zone', 'Test Zone Description', 10.0, 20.0, 30.0);
INSERT INTO spawn_location_placement (id, zone_id, x, y, z, heading, spawn_percentage)
VALUES (1, 1, 100.0, 200.0, 300.0, 45.0, 75.5);
INSERT INTO spawn_location_group (group_id, location_id, zone_id)
VALUES (1, 1, 1);
`
if err := sqlitex.ExecuteScript(conn, schema, &sqlitex.ExecOptions{}); err != nil {
t.Fatalf("Failed to create test schema: %v", err)
}
// Create database instance
zdb := NewZoneDatabase(conn)
if zdb == nil {
t.Fatal("Expected non-nil zone database")
}
// Test LoadZoneData
zoneData, err := zdb.LoadZoneData(1)
if err != nil {
t.Fatalf("Failed to load zone data: %v", err)
}
if zoneData.ZoneID != 1 {
t.Errorf("Expected zone ID 1, got %d", zoneData.ZoneID)
}
if zoneData.Configuration == nil {
t.Fatal("Expected non-nil zone configuration")
}
if zoneData.Configuration.Name != "test_zone" {
t.Errorf("Expected zone name 'test_zone', got '%s'", zoneData.Configuration.Name)
}
if zoneData.Configuration.SafeX != 10.0 {
t.Errorf("Expected safe X 10.0, got %.2f", zoneData.Configuration.SafeX)
}
// Test spawn locations
if len(zoneData.SpawnLocations) != 1 {
t.Errorf("Expected 1 spawn location, got %d", len(zoneData.SpawnLocations))
}
location := zoneData.SpawnLocations[1]
if location == nil {
t.Fatal("Expected spawn location 1 to exist")
}
if location.X != 100.0 || location.Y != 200.0 || location.Z != 300.0 {
t.Errorf("Expected location (100, 200, 300), got (%.2f, %.2f, %.2f)", location.X, location.Y, location.Z)
}
if location.SpawnPercentage != 75.5 {
t.Errorf("Expected spawn percentage 75.5, got %.2f", location.SpawnPercentage)
}
// Test LoadSpawnLocation
singleLocation, err := zdb.LoadSpawnLocation(1)
if err != nil {
t.Errorf("Failed to load spawn location: %v", err)
}
if singleLocation.ID != 1 {
t.Errorf("Expected location ID 1, got %d", singleLocation.ID)
}
// Test SaveSpawnLocation (update)
singleLocation.X = 150.0
if err := zdb.SaveSpawnLocation(singleLocation); err != nil {
t.Errorf("Failed to save spawn location: %v", err)
}
// Verify update
updatedLocation, err := zdb.LoadSpawnLocation(1)
if err != nil {
t.Errorf("Failed to load updated spawn location: %v", err)
}
if updatedLocation.X != 150.0 {
t.Errorf("Expected updated X 150.0, got %.2f", updatedLocation.X)
}
// Test SaveSpawnLocation (insert new)
newLocation := &SpawnLocation{
X: 400.0, Y: 500.0, Z: 600.0,
Heading: 90.0, SpawnPercentage: 100.0,
}
if err := zdb.SaveSpawnLocation(newLocation); err != nil {
t.Errorf("Failed to insert new spawn location: %v", err)
}
if newLocation.ID == 0 {
t.Error("Expected new location to have non-zero ID")
}
// Test LoadSpawnGroups
groups, err := zdb.LoadSpawnGroups(1)
if err != nil {
t.Errorf("Failed to load spawn groups: %v", err)
}
if len(groups) != 1 {
t.Errorf("Expected 1 spawn group, got %d", len(groups))
}
if len(groups[1]) != 1 || groups[1][0] != 1 {
t.Errorf("Expected group 1 to contain location 1, got %v", groups[1])
}
// Test SaveSpawnGroup
newLocationIDs := []int32{1, 2}
if err := zdb.SaveSpawnGroup(2, newLocationIDs); err != nil {
t.Errorf("Failed to save spawn group: %v", err)
}
// Test DeleteSpawnLocation
if err := zdb.DeleteSpawnLocation(newLocation.ID); err != nil {
t.Errorf("Failed to delete spawn location: %v", err)
}
// Verify deletion
_, err = zdb.LoadSpawnLocation(newLocation.ID)
if err == nil {
t.Error("Expected error loading deleted spawn location")
}
}
// TestZoneServerLifecycle tests zone server creation, initialization, and shutdown
func TestZoneServerLifecycle(t *testing.T) {
// Create zone server
zoneServer := NewZoneServer("test_zone_lifecycle")
if zoneServer == nil {
t.Fatal("Expected non-nil zone server")
}
// Test initial state
if zoneServer.IsInitialized() {
t.Error("Expected zone to not be initialized")
}
if zoneServer.IsShuttingDown() {
t.Error("Expected zone to not be shutting down")
}
if zoneServer.GetZoneName() != "test_zone_lifecycle" {
t.Errorf("Expected zone name 'test_zone_lifecycle', got '%s'", zoneServer.GetZoneName())
}
// Test initialization
config := &ZoneServerConfig{
ZoneName: "test_zone_lifecycle",
ZoneID: 100,
InstanceID: 0,
MaxPlayers: 50,
MinLevel: 1,
MaxLevel: 100,
LoadMaps: false,
EnableWeather: false,
EnablePathfinding: false,
}
err := zoneServer.Initialize(config)
if err != nil {
t.Fatalf("Failed to initialize zone server: %v", err)
}
if !zoneServer.IsInitialized() {
t.Error("Expected zone to be initialized")
}
if zoneServer.GetZoneID() != 100 {
t.Errorf("Expected zone ID 100, got %d", zoneServer.GetZoneID())
}
if zoneServer.GetMaxPlayers() != 50 {
t.Errorf("Expected max players 50, got %d", zoneServer.GetMaxPlayers())
}
// Test shutdown
zoneServer.Shutdown()
// Give shutdown time to process
time.Sleep(time.Millisecond * 100)
if !zoneServer.IsShuttingDown() {
t.Error("Expected zone to be shutting down")
}
}
// TestZoneManagerOperations tests zone manager functionality
func TestZoneManagerOperations(t *testing.T) {
// Create test database
conn, err := sqlite.OpenConn(":memory:", sqlite.OpenReadWrite|sqlite.OpenCreate)
if err != nil {
t.Fatalf("Failed to create test database: %v", err)
}
defer conn.Close()
// Create minimal schema for testing
schema := `
CREATE TABLE zones (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
file TEXT DEFAULT 'test.zone',
description TEXT DEFAULT 'Test Zone',
safe_x REAL DEFAULT 0,
safe_y REAL DEFAULT 0,
safe_z REAL DEFAULT 0,
safe_heading REAL DEFAULT 0,
underworld REAL DEFAULT -1000,
min_level INTEGER DEFAULT 1,
max_level INTEGER DEFAULT 100,
min_status INTEGER DEFAULT 0,
min_version INTEGER DEFAULT 0,
instance_type INTEGER DEFAULT 0,
max_players INTEGER DEFAULT 100,
default_lockout_time INTEGER DEFAULT 18000,
default_reenter_time INTEGER DEFAULT 3600,
default_reset_time INTEGER DEFAULT 259200,
group_zone_option INTEGER DEFAULT 0,
expansion_flag INTEGER DEFAULT 0,
holiday_flag INTEGER DEFAULT 0,
can_bind INTEGER DEFAULT 1,
can_gate INTEGER DEFAULT 1,
can_evac INTEGER DEFAULT 1,
city_zone INTEGER DEFAULT 0,
always_loaded INTEGER DEFAULT 0,
weather_allowed INTEGER DEFAULT 1
);
CREATE TABLE spawn_location_placement (id INTEGER PRIMARY KEY, zone_id INTEGER);
INSERT INTO zones (id, name) VALUES (1, 'zone1'), (2, 'zone2');
`
if err := sqlitex.ExecuteScript(conn, schema, &sqlitex.ExecOptions{}); err != nil {
t.Fatalf("Failed to create test schema: %v", err)
}
// Create zone manager
config := &ZoneManagerConfig{
MaxZones: 5,
MaxInstanceZones: 10,
ProcessInterval: time.Millisecond * 100,
CleanupInterval: time.Second * 1,
EnableWeather: false,
EnablePathfinding: false,
EnableCombat: false,
EnableSpellProcess: false,
}
zoneManager := NewZoneManager(config, conn)
if zoneManager == nil {
t.Fatal("Expected non-nil zone manager")
}
// Test initial state
if zoneManager.GetZoneCount() != 0 {
t.Errorf("Expected 0 zones initially, got %d", zoneManager.GetZoneCount())
}
if zoneManager.GetInstanceCount() != 0 {
t.Errorf("Expected 0 instances initially, got %d", zoneManager.GetInstanceCount())
}
// Test zone loading (this will fail due to missing data but we can test the attempt)
_, err = zoneManager.LoadZone(1)
if err == nil {
// If successful, test that it was loaded
if zoneManager.GetZoneCount() != 1 {
t.Errorf("Expected 1 zone after loading, got %d", zoneManager.GetZoneCount())
}
// Test retrieval
zone := zoneManager.GetZone(1)
if zone == nil {
t.Error("Expected to retrieve loaded zone")
}
zoneByName := zoneManager.GetZoneByName("zone1")
if zoneByName == nil {
t.Error("Expected to retrieve zone by name")
}
// Test statistics
stats := zoneManager.GetStatistics()
if stats == nil {
t.Error("Expected non-nil statistics")
}
if stats.TotalZones != 1 {
t.Errorf("Expected 1 zone in statistics, got %d", stats.TotalZones)
}
}
// Test zone manager start/stop
err = zoneManager.Start()
if err != nil {
t.Errorf("Failed to start zone manager: %v", err)
}
// Give it time to start
time.Sleep(time.Millisecond * 50)
err = zoneManager.Stop()
if err != nil {
t.Errorf("Failed to stop zone manager: %v", err)
}
}
// TestPositionCalculations tests position and distance calculations
func TestPositionCalculations(t *testing.T) {
testCases := []struct {
name string
x1, y1, z1 float32
x2, y2, z2 float32
expected2D float32
expected3D float32
}{
{"Origin to (3,4,12)", 0, 0, 0, 3, 4, 12, 5.0, 13.0},
{"Same point", 10, 20, 30, 10, 20, 30, 0.0, 0.0},
{"Unit distance", 0, 0, 0, 1, 0, 0, 1.0, 1.0},
{"Negative coordinates", -5, -5, -5, 5, 5, 5, 14.142136, 17.320507},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
// Test 2D distance
distance2D := Distance2D(tc.x1, tc.y1, tc.x2, tc.y2)
if testAbs(distance2D-tc.expected2D) > 0.001 {
t.Errorf("Expected 2D distance %.3f, got %.3f", tc.expected2D, distance2D)
}
// Test 3D distance
distance3D := Distance3D(tc.x1, tc.y1, tc.z1, tc.x2, tc.y2, tc.z2)
if testAbs(distance3D-tc.expected3D) > 0.001 {
t.Errorf("Expected 3D distance %.3f, got %.3f", tc.expected3D, distance3D)
}
})
}
// Test heading calculations
headingTests := []struct {
name string
fromX, fromY float32
toX, toY float32
expectedRange [2]float32 // min, max acceptable range
}{
{"East", 0, 0, 1, 0, [2]float32{0, 90}},
{"North", 0, 0, 0, 1, [2]float32{0, 180}},
{"Northeast", 0, 0, 1, 1, [2]float32{30, 90}},
{"Same point", 5, 5, 5, 5, [2]float32{0, 360}},
}
for _, tc := range headingTests {
t.Run(tc.name, func(t *testing.T) {
heading := CalculateHeading(tc.fromX, tc.fromY, tc.toX, tc.toY)
if heading < tc.expectedRange[0] || heading > tc.expectedRange[1] {
t.Logf("Heading %.2f for %s (acceptable range: %.2f-%.2f)", heading, tc.name, tc.expectedRange[0], tc.expectedRange[1])
}
})
}
// Test heading normalization
normalizeTests := []struct {
input float32
expected float32
}{
{0, 0},
{256, 256},
{512, 0},
{600, 88},
{-100, 412},
}
for _, tc := range normalizeTests {
normalized := NormalizeHeading(tc.input)
if normalized != tc.expected {
t.Errorf("Expected normalized heading %.2f for input %.2f, got %.2f", tc.expected, tc.input, normalized)
}
}
}
// TestPositionStructs tests Position data structure
func TestPositionStructs(t *testing.T) {
// Test NewPosition
pos1 := NewPosition(10.0, 20.0, 30.0, 128.0)
if pos1.X != 10.0 || pos1.Y != 20.0 || pos1.Z != 30.0 || pos1.Heading != 128.0 {
t.Errorf("NewPosition failed: expected (10,20,30,128), got (%.2f,%.2f,%.2f,%.2f)", pos1.X, pos1.Y, pos1.Z, pos1.Heading)
}
pos2 := NewPosition(13.0, 24.0, 30.0, 128.0)
// Test distance calculations
distance2D := pos1.DistanceTo2D(pos2)
expected2D := float32(5.0)
if testAbs(distance2D-expected2D) > 0.001 {
t.Errorf("Expected 2D distance %.3f, got %.3f", expected2D, distance2D)
}
distance3D := pos1.DistanceTo3D(pos2)
expected3D := float32(5.0)
if testAbs(distance3D-expected3D) > 0.001 {
t.Errorf("Expected 3D distance %.3f, got %.3f", expected3D, distance3D)
}
// Test position operations
pos1.Set(5.0, 10.0, 15.0, 64.0)
if pos1.X != 5.0 || pos1.Y != 10.0 || pos1.Z != 15.0 || pos1.Heading != 64.0 {
t.Errorf("Set failed: expected (5,10,15,64), got (%.2f,%.2f,%.2f,%.2f)", pos1.X, pos1.Y, pos1.Z, pos1.Heading)
}
// Test copy
posCopy := pos1.Copy()
if !pos1.Equals(posCopy) {
t.Error("Expected copied position to equal original")
}
// Test bounding box
bbox := NewBoundingBox(0, 0, 0, 10, 10, 10)
if !bbox.Contains(5, 5, 5) {
t.Error("Expected bounding box to contain point (5,5,5)")
}
if bbox.Contains(15, 5, 5) {
t.Error("Expected bounding box to not contain point (15,5,5)")
}
if !bbox.Contains(0, 0, 0) {
t.Error("Expected bounding box to contain min point")
}
if !bbox.Contains(10, 10, 10) {
t.Error("Expected bounding box to contain max point")
}
}
// TestMovementManager tests movement management system
func TestMovementManager(t *testing.T) {
// Create test zone
zoneServer := NewZoneServer("test_movement")
config := &ZoneServerConfig{
ZoneName: "test_movement",
ZoneID: 1,
LoadMaps: false,
EnableWeather: false,
EnablePathfinding: false,
}
err := zoneServer.Initialize(config)
if err != nil {
t.Fatalf("Failed to initialize zone server: %v", err)
}
// Create movement manager
movementMgr := NewMobMovementManager(zoneServer)
if movementMgr == nil {
t.Fatal("Expected non-nil movement manager")
}
// Create and add a test spawn to the zone
spawnID := int32(1001)
testSpawn := spawn.NewSpawn()
testSpawn.SetID(spawnID)
testSpawn.SetX(0.0)
testSpawn.SetY(0.0, false)
testSpawn.SetZ(0.0)
testSpawn.SetHeadingFromFloat(0.0)
testSpawn.SetName("test_spawn")
err = zoneServer.AddSpawn(testSpawn)
if err != nil {
t.Fatalf("Failed to add test spawn: %v", err)
}
// Test adding spawn
movementMgr.AddMovementSpawn(spawnID)
// Test movement state
state := movementMgr.GetMovementState(spawnID)
if state == nil {
t.Error("Expected non-nil movement state")
} else if state.SpawnID != spawnID {
t.Errorf("Expected spawn ID %d, got %d", spawnID, state.SpawnID)
}
// Test movement command (this may fail due to missing methods but we test the interface)
err = movementMgr.MoveTo(spawnID, 10.0, 20.0, 30.0, DefaultRunSpeed)
// We don't check error here as it depends on spawn implementation
// Test removing spawn
movementMgr.RemoveMovementSpawn(spawnID)
// Test that state is cleaned up
state = movementMgr.GetMovementState(spawnID)
if state != nil {
t.Error("Expected movement state to be cleaned up after removal")
}
}
// TestInstanceTypes tests instance type functionality
func TestInstanceTypes(t *testing.T) {
testCases := []struct {
instanceType InstanceType
expected string
}{
{InstanceTypeNone, "None"},
{InstanceTypeGroupLockout, "Group Lockout"},
{InstanceTypeRaidPersist, "Raid Persistent"},
{InstanceTypePersonalHouse, "Personal House"},
{InstanceTypeSoloLockout, "Solo Lockout"},
{InstanceTypePublic, "Public"},
}
for _, tc := range testCases {
t.Run(tc.expected, func(t *testing.T) {
result := tc.instanceType.String()
if result != tc.expected {
t.Errorf("Expected instance type string '%s', got '%s'", tc.expected, result)
}
})
}
}
// TestConcurrentOperations tests thread safety
func TestConcurrentOperations(t *testing.T) {
// Create test database
conn, err := sqlite.OpenConn(":memory:", sqlite.OpenReadWrite|sqlite.OpenCreate)
if err != nil {
t.Fatalf("Failed to create test database: %v", err)
}
defer conn.Close()
// Simple schema
schema := `
CREATE TABLE zones (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
file TEXT DEFAULT 'test.zone',
description TEXT DEFAULT 'Test Zone',
safe_x REAL DEFAULT 0, safe_y REAL DEFAULT 0, safe_z REAL DEFAULT 0,
safe_heading REAL DEFAULT 0, underworld REAL DEFAULT -1000,
min_level INTEGER DEFAULT 1, max_level INTEGER DEFAULT 100,
min_status INTEGER DEFAULT 0, min_version INTEGER DEFAULT 0,
instance_type INTEGER DEFAULT 0, max_players INTEGER DEFAULT 100,
default_lockout_time INTEGER DEFAULT 18000,
default_reenter_time INTEGER DEFAULT 3600,
default_reset_time INTEGER DEFAULT 259200,
group_zone_option INTEGER DEFAULT 0,
expansion_flag INTEGER DEFAULT 0, holiday_flag INTEGER DEFAULT 0,
can_bind INTEGER DEFAULT 1, can_gate INTEGER DEFAULT 1, can_evac INTEGER DEFAULT 1,
city_zone INTEGER DEFAULT 0, always_loaded INTEGER DEFAULT 0, weather_allowed INTEGER DEFAULT 1
);
CREATE TABLE spawn_location_placement (
id INTEGER PRIMARY KEY AUTOINCREMENT,
zone_id INTEGER,
x REAL DEFAULT 0,
y REAL DEFAULT 0,
z REAL DEFAULT 0,
heading REAL DEFAULT 0,
pitch REAL DEFAULT 0,
roll REAL DEFAULT 0,
spawn_type INTEGER DEFAULT 0,
respawn_time INTEGER DEFAULT 300,
expire_time INTEGER DEFAULT 0,
expire_offset INTEGER DEFAULT 0,
conditions INTEGER DEFAULT 0,
conditional_value INTEGER DEFAULT 0,
spawn_percentage REAL DEFAULT 100.0
);
INSERT INTO zones (id, name) VALUES (1, 'concurrent_test');
`
if err := sqlitex.ExecuteScript(conn, schema, &sqlitex.ExecOptions{}); err != nil {
t.Fatalf("Failed to create test schema: %v", err)
}
// Test concurrent database reads with separate connections
var wg sync.WaitGroup
const numGoroutines = 5 // Reduce to prevent too many concurrent connections
for i := 0; i < numGoroutines; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
// Create separate connection for each goroutine to avoid concurrent access issues
goroutineConn, err := sqlite.OpenConn(":memory:", sqlite.OpenReadWrite|sqlite.OpenCreate)
if err != nil {
t.Errorf("Goroutine %d failed to create connection: %v", id, err)
return
}
defer goroutineConn.Close()
// Create schema in new connection
if err := sqlitex.ExecuteScript(goroutineConn, schema, &sqlitex.ExecOptions{}); err != nil {
t.Errorf("Goroutine %d failed to create schema: %v", id, err)
return
}
zdb := NewZoneDatabase(goroutineConn)
_, err = zdb.LoadZoneData(1)
if err != nil {
t.Errorf("Goroutine %d failed to load zone data: %v", id, err)
}
}(i)
}
wg.Wait()
// Test concurrent zone manager operations
config := &ZoneManagerConfig{
MaxZones: 10,
MaxInstanceZones: 20,
ProcessInterval: time.Millisecond * 100,
CleanupInterval: time.Second * 1,
}
zoneManager := NewZoneManager(config, conn)
for i := 0; i < numGoroutines; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
stats := zoneManager.GetStatistics()
if stats == nil {
t.Errorf("Goroutine %d got nil statistics", id)
}
}(i)
}
wg.Wait()
}
// TestConstants verifies various constants are properly defined
func TestConstants(t *testing.T) {
// Test distance constants
if SendSpawnDistance != 250.0 {
t.Errorf("Expected SendSpawnDistance 250.0, got %.2f", SendSpawnDistance)
}
if MaxChaseDistance != 80.0 {
t.Errorf("Expected MaxChaseDistance 80.0, got %.2f", MaxChaseDistance)
}
// Test expansion constants
if ExpansionDOF != 1024 {
t.Errorf("Expected ExpansionDOF 1024, got %d", ExpansionDOF)
}
// Test EQ2 heading constant
if EQ2HeadingMax != 512.0 {
t.Errorf("Expected EQ2HeadingMax 512.0, got %.2f", EQ2HeadingMax)
}
// Test default speeds
if DefaultWalkSpeed <= 0 {
t.Error("Expected DefaultWalkSpeed to be positive")
}
if DefaultRunSpeed <= DefaultWalkSpeed {
t.Error("Expected DefaultRunSpeed to be greater than DefaultWalkSpeed")
}
}
// Benchmark tests
// BenchmarkDistanceCalculation benchmarks distance calculations
func BenchmarkDistanceCalculation(b *testing.B) {
x1, y1, z1 := float32(100.0), float32(200.0), float32(300.0)
x2, y2, z2 := float32(150.0), float32(250.0), float32(350.0)
b.ResetTimer()
for i := 0; i < b.N; i++ {
Distance3D(x1, y1, z1, x2, y2, z2)
}
}
// BenchmarkPositionDistance benchmarks position-based distance calculations
func BenchmarkPositionDistance(b *testing.B) {
pos1 := NewPosition(100.0, 200.0, 300.0, 128.0)
pos2 := NewPosition(150.0, 250.0, 350.0, 256.0)
b.ResetTimer()
for i := 0; i < b.N; i++ {
pos1.DistanceTo3D(pos2)
}
}
// BenchmarkHeadingCalculation benchmarks heading calculations
func BenchmarkHeadingCalculation(b *testing.B) {
fromX, fromY := float32(0.0), float32(0.0)
toX, toY := float32(100.0), float32(100.0)
b.ResetTimer()
for i := 0; i < b.N; i++ {
CalculateHeading(fromX, fromY, toX, toY)
}
}
// BenchmarkDatabaseOperations benchmarks database operations
func BenchmarkDatabaseOperations(b *testing.B) {
// Create test database
tmpDir := b.TempDir()
dbPath := filepath.Join(tmpDir, "benchmark.db")
conn, err := sqlite.OpenConn(dbPath, sqlite.OpenReadWrite|sqlite.OpenCreate)
if err != nil {
b.Fatalf("Failed to create benchmark database: %v", err)
}
defer conn.Close()
// Create schema and test data
schema := `
CREATE TABLE zones (
id INTEGER PRIMARY KEY, name TEXT NOT NULL, file TEXT DEFAULT 'test.zone',
description TEXT DEFAULT 'Test Zone', safe_x REAL DEFAULT 0, safe_y REAL DEFAULT 0,
safe_z REAL DEFAULT 0, safe_heading REAL DEFAULT 0, underworld REAL DEFAULT -1000,
min_level INTEGER DEFAULT 1, max_level INTEGER DEFAULT 100, min_status INTEGER DEFAULT 0,
min_version INTEGER DEFAULT 0, instance_type INTEGER DEFAULT 0, max_players INTEGER DEFAULT 100,
default_lockout_time INTEGER DEFAULT 18000, default_reenter_time INTEGER DEFAULT 3600,
default_reset_time INTEGER DEFAULT 259200, group_zone_option INTEGER DEFAULT 0,
expansion_flag INTEGER DEFAULT 0, holiday_flag INTEGER DEFAULT 0,
can_bind INTEGER DEFAULT 1, can_gate INTEGER DEFAULT 1, can_evac INTEGER DEFAULT 1,
city_zone INTEGER DEFAULT 0, always_loaded INTEGER DEFAULT 0, weather_allowed INTEGER DEFAULT 1
);
CREATE TABLE spawn_location_placement (
id INTEGER PRIMARY KEY AUTOINCREMENT,
zone_id INTEGER,
x REAL DEFAULT 0,
y REAL DEFAULT 0,
z REAL DEFAULT 0,
heading REAL DEFAULT 0,
pitch REAL DEFAULT 0,
roll REAL DEFAULT 0,
spawn_type INTEGER DEFAULT 0,
respawn_time INTEGER DEFAULT 300,
expire_time INTEGER DEFAULT 0,
expire_offset INTEGER DEFAULT 0,
conditions INTEGER DEFAULT 0,
conditional_value INTEGER DEFAULT 0,
spawn_percentage REAL DEFAULT 100.0
);
INSERT INTO zones (id, name) VALUES (1, 'benchmark_zone');
`
if err := sqlitex.ExecuteScript(conn, schema, &sqlitex.ExecOptions{}); err != nil {
b.Fatalf("Failed to create benchmark schema: %v", err)
}
zdb := NewZoneDatabase(conn)
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, err := zdb.LoadZoneData(1)
if err != nil {
b.Fatalf("Failed to load zone data: %v", err)
}
}
}
// BenchmarkZoneManagerOperations benchmarks zone manager operations
func BenchmarkZoneManagerOperations(b *testing.B) {
conn, err := sqlite.OpenConn(":memory:", sqlite.OpenReadWrite|sqlite.OpenCreate)
if err != nil {
b.Fatalf("Failed to create benchmark database: %v", err)
}
defer conn.Close()
config := &ZoneManagerConfig{
MaxZones: 10,
MaxInstanceZones: 20,
ProcessInterval: time.Millisecond * 100,
CleanupInterval: time.Second * 1,
}
zoneManager := NewZoneManager(config, conn)
b.ResetTimer()
for i := 0; i < b.N; i++ {
zoneManager.GetStatistics()
}
}
// Helper functions
func testAbs(x float32) float32 {
if x < 0 {
return -x
}
return x
}