896 lines
26 KiB
Go
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
|
|
} |