This commit is contained in:
Sky Johnson 2025-08-06 14:39:39 -05:00
parent cc49aac689
commit 8f8dbefece
11 changed files with 1129 additions and 751 deletions

View File

@ -1,31 +1,25 @@
package zone package zone
import ( import (
"database/sql"
"fmt" "fmt"
"log" "log"
"sync" "sync"
"zombiezen.com/go/sqlite"
"zombiezen.com/go/sqlite/sqlitex"
) )
// ZoneDatabase handles all database operations for zones // ZoneDatabase handles all database operations for zones
type ZoneDatabase struct { type ZoneDatabase struct {
db *sql.DB conn *sqlite.Conn
queries map[string]*sql.Stmt mutex sync.RWMutex
mutex sync.RWMutex
} }
// NewZoneDatabase creates a new zone database instance // NewZoneDatabase creates a new zone database instance
func NewZoneDatabase(db *sql.DB) *ZoneDatabase { func NewZoneDatabase(conn *sqlite.Conn) *ZoneDatabase {
zdb := &ZoneDatabase{ return &ZoneDatabase{
db: db, conn: conn,
queries: make(map[string]*sql.Stmt),
} }
if err := zdb.prepareStatements(); err != nil {
log.Printf("%s Failed to prepare database statements: %v", LogPrefixZone, err)
}
return zdb
} }
// LoadZoneData loads all zone configuration and spawn data // LoadZoneData loads all zone configuration and spawn data
@ -98,40 +92,46 @@ func (zdb *ZoneDatabase) SaveZoneConfiguration(config *ZoneConfiguration) error
zdb.mutex.Lock() zdb.mutex.Lock()
defer zdb.mutex.Unlock() defer zdb.mutex.Unlock()
stmt := zdb.queries["updateZoneConfig"] query := `UPDATE zones SET
if stmt == nil { name = ?, file = ?, description = ?, safe_x = ?, safe_y = ?, safe_z = ?,
return fmt.Errorf("update zone config statement not prepared") safe_heading = ?, underworld = ?, min_level = ?, max_level = ?, min_status = ?,
} min_version = ?, instance_type = ?, max_players = ?, default_lockout_time = ?,
default_reenter_time = ?, default_reset_time = ?, group_zone_option = ?,
expansion_flag = ?, holiday_flag = ?, can_bind = ?, can_gate = ?, can_evac = ?,
city_zone = ?, always_loaded = ?, weather_allowed = ?
WHERE id = ?`
_, err := stmt.Exec( err := sqlitex.Execute(zdb.conn, query, &sqlitex.ExecOptions{
config.Name, Args: []any{
config.File, config.Name,
config.Description, config.File,
config.SafeX, config.Description,
config.SafeY, config.SafeX,
config.SafeZ, config.SafeY,
config.SafeHeading, config.SafeZ,
config.Underworld, config.SafeHeading,
config.MinLevel, config.Underworld,
config.MaxLevel, config.MinLevel,
config.MinStatus, config.MaxLevel,
config.MinVersion, config.MinStatus,
config.InstanceType, config.MinVersion,
config.MaxPlayers, config.InstanceType,
config.DefaultLockoutTime, config.MaxPlayers,
config.DefaultReenterTime, config.DefaultLockoutTime,
config.DefaultResetTime, config.DefaultReenterTime,
config.GroupZoneOption, config.DefaultResetTime,
config.ExpansionFlag, config.GroupZoneOption,
config.HolidayFlag, config.ExpansionFlag,
config.CanBind, config.HolidayFlag,
config.CanGate, config.CanBind,
config.CanEvac, config.CanGate,
config.CityZone, config.CanEvac,
config.AlwaysLoaded, config.CityZone,
config.WeatherAllowed, config.AlwaysLoaded,
config.ZoneID, config.WeatherAllowed,
) config.ZoneID,
},
})
if err != nil { if err != nil {
return fmt.Errorf("failed to save zone configuration: %v", err) return fmt.Errorf("failed to save zone configuration: %v", err)
@ -145,33 +145,40 @@ func (zdb *ZoneDatabase) LoadSpawnLocation(locationID int32) (*SpawnLocation, er
zdb.mutex.RLock() zdb.mutex.RLock()
defer zdb.mutex.RUnlock() defer zdb.mutex.RUnlock()
stmt := zdb.queries["selectSpawnLocation"] query := `SELECT id, x, y, z, heading, pitch, roll, spawn_type, respawn_time, expire_time,
if stmt == nil { expire_offset, conditions, conditional_value, spawn_percentage
return nil, fmt.Errorf("select spawn location statement not prepared") FROM spawn_location_placement WHERE id = ?`
}
location := &SpawnLocation{} location := &SpawnLocation{}
err := stmt.QueryRow(locationID).Scan( err := sqlitex.Execute(zdb.conn, query, &sqlitex.ExecOptions{
&location.ID, Args: []any{locationID},
&location.X, ResultFunc: func(stmt *sqlite.Stmt) error {
&location.Y, location.ID = int32(stmt.ColumnInt64(0))
&location.Z, location.X = float32(stmt.ColumnFloat(1))
&location.Heading, location.Y = float32(stmt.ColumnFloat(2))
&location.Pitch, location.Z = float32(stmt.ColumnFloat(3))
&location.Roll, location.Heading = float32(stmt.ColumnFloat(4))
&location.SpawnType, location.Pitch = float32(stmt.ColumnFloat(5))
&location.RespawnTime, location.Roll = float32(stmt.ColumnFloat(6))
&location.ExpireTime, location.SpawnType = int8(stmt.ColumnInt64(7))
&location.ExpireOffset, location.RespawnTime = int32(stmt.ColumnInt64(8))
&location.Conditions, location.ExpireTime = int32(stmt.ColumnInt64(9))
&location.ConditionalValue, location.ExpireOffset = int32(stmt.ColumnInt64(10))
&location.SpawnPercentage, location.Conditions = int8(stmt.ColumnInt64(11))
) location.ConditionalValue = int32(stmt.ColumnInt64(12))
location.SpawnPercentage = float32(stmt.ColumnFloat(13))
return nil
},
})
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to load spawn location %d: %v", locationID, err) return nil, fmt.Errorf("failed to load spawn location %d: %v", locationID, err)
} }
if location.ID == 0 {
return nil, fmt.Errorf("spawn location %d not found", locationID)
}
return location, nil return location, nil
} }
@ -180,58 +187,67 @@ func (zdb *ZoneDatabase) SaveSpawnLocation(location *SpawnLocation) error {
zdb.mutex.Lock() zdb.mutex.Lock()
defer zdb.mutex.Unlock() defer zdb.mutex.Unlock()
var stmt *sql.Stmt
var err error
if location.ID == 0 { if location.ID == 0 {
// Insert new location // Insert new location
stmt = zdb.queries["insertSpawnLocation"] query := `INSERT INTO spawn_location_placement
if stmt == nil { (x, y, z, heading, pitch, roll, spawn_type, respawn_time, expire_time,
return fmt.Errorf("insert spawn location statement not prepared") expire_offset, conditions, conditional_value, spawn_percentage)
} VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
RETURNING id`
err = stmt.QueryRow( err := sqlitex.Execute(zdb.conn, query, &sqlitex.ExecOptions{
location.X, Args: []any{
location.Y, location.X,
location.Z, location.Y,
location.Heading, location.Z,
location.Pitch, location.Heading,
location.Roll, location.Pitch,
location.SpawnType, location.Roll,
location.RespawnTime, location.SpawnType,
location.ExpireTime, location.RespawnTime,
location.ExpireOffset, location.ExpireTime,
location.Conditions, location.ExpireOffset,
location.ConditionalValue, location.Conditions,
location.SpawnPercentage, location.ConditionalValue,
).Scan(&location.ID) location.SpawnPercentage,
},
ResultFunc: func(stmt *sqlite.Stmt) error {
location.ID = int32(stmt.ColumnInt64(0))
return nil
},
})
if err != nil {
return fmt.Errorf("failed to insert spawn location: %v", err)
}
} else { } else {
// Update existing location // Update existing location
stmt = zdb.queries["updateSpawnLocation"] query := `UPDATE spawn_location_placement SET
if stmt == nil { x = ?, y = ?, z = ?, heading = ?, pitch = ?, roll = ?, spawn_type = ?,
return fmt.Errorf("update spawn location statement not prepared") respawn_time = ?, expire_time = ?, expire_offset = ?, conditions = ?,
conditional_value = ?, spawn_percentage = ?
WHERE id = ?`
err := sqlitex.Execute(zdb.conn, query, &sqlitex.ExecOptions{
Args: []any{
location.X,
location.Y,
location.Z,
location.Heading,
location.Pitch,
location.Roll,
location.SpawnType,
location.RespawnTime,
location.ExpireTime,
location.ExpireOffset,
location.Conditions,
location.ConditionalValue,
location.SpawnPercentage,
location.ID,
},
})
if err != nil {
return fmt.Errorf("failed to update spawn location: %v", err)
} }
_, err = stmt.Exec(
location.X,
location.Y,
location.Z,
location.Heading,
location.Pitch,
location.Roll,
location.SpawnType,
location.RespawnTime,
location.ExpireTime,
location.ExpireOffset,
location.Conditions,
location.ConditionalValue,
location.SpawnPercentage,
location.ID,
)
}
if err != nil {
return fmt.Errorf("failed to save spawn location: %v", err)
} }
return nil return nil
@ -242,12 +258,10 @@ func (zdb *ZoneDatabase) DeleteSpawnLocation(locationID int32) error {
zdb.mutex.Lock() zdb.mutex.Lock()
defer zdb.mutex.Unlock() defer zdb.mutex.Unlock()
stmt := zdb.queries["deleteSpawnLocation"] query := `DELETE FROM spawn_location_placement WHERE id = ?`
if stmt == nil { err := sqlitex.Execute(zdb.conn, query, &sqlitex.ExecOptions{
return fmt.Errorf("delete spawn location statement not prepared") Args: []any{locationID},
} })
_, err := stmt.Exec(locationID)
if err != nil { if err != nil {
return fmt.Errorf("failed to delete spawn location %d: %v", locationID, err) return fmt.Errorf("failed to delete spawn location %d: %v", locationID, err)
} }
@ -260,30 +274,23 @@ func (zdb *ZoneDatabase) LoadSpawnGroups(zoneID int32) (map[int32][]int32, error
zdb.mutex.RLock() zdb.mutex.RLock()
defer zdb.mutex.RUnlock() defer zdb.mutex.RUnlock()
stmt := zdb.queries["selectSpawnGroups"] query := `SELECT group_id, location_id
if stmt == nil { FROM spawn_location_group WHERE zone_id = ?
return nil, fmt.Errorf("select spawn groups statement not prepared") ORDER BY group_id, location_id`
}
rows, err := stmt.Query(zoneID)
if err != nil {
return nil, fmt.Errorf("failed to query spawn groups: %v", err)
}
defer rows.Close()
groups := make(map[int32][]int32) groups := make(map[int32][]int32)
err := sqlitex.Execute(zdb.conn, query, &sqlitex.ExecOptions{
Args: []any{zoneID},
ResultFunc: func(stmt *sqlite.Stmt) error {
groupID := int32(stmt.ColumnInt64(0))
locationID := int32(stmt.ColumnInt64(1))
groups[groupID] = append(groups[groupID], locationID)
return nil
},
})
for rows.Next() { if err != nil {
var groupID, locationID int32 return nil, fmt.Errorf("failed to load spawn groups: %v", err)
if err := rows.Scan(&groupID, &locationID); err != nil {
return nil, fmt.Errorf("failed to scan spawn group row: %v", err)
}
groups[groupID] = append(groups[groupID], locationID)
}
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("error iterating spawn groups: %v", err)
} }
return groups, nil return groups, nil
@ -294,288 +301,134 @@ func (zdb *ZoneDatabase) SaveSpawnGroup(groupID int32, locationIDs []int32) erro
zdb.mutex.Lock() zdb.mutex.Lock()
defer zdb.mutex.Unlock() defer zdb.mutex.Unlock()
// Start transaction // Use transaction for atomic operations
tx, err := zdb.db.Begin() endFn, err := sqlitex.ImmediateTransaction(zdb.conn)
if err != nil { if err != nil {
return fmt.Errorf("failed to begin transaction: %v", err) return fmt.Errorf("failed to start transaction: %v", err)
} }
defer tx.Rollback() defer endFn(&err)
// Delete existing associations // Delete existing associations
deleteStmt := zdb.queries["deleteSpawnGroup"] deleteQuery := `DELETE FROM spawn_location_group WHERE group_id = ?`
if deleteStmt == nil { err = sqlitex.Execute(zdb.conn, deleteQuery, &sqlitex.ExecOptions{
return fmt.Errorf("delete spawn group statement not prepared") Args: []any{groupID},
} })
_, err = tx.Stmt(deleteStmt).Exec(groupID)
if err != nil { if err != nil {
return fmt.Errorf("failed to delete existing spawn group: %v", err) return fmt.Errorf("failed to delete existing spawn group: %v", err)
} }
// Insert new associations // Insert new associations
insertStmt := zdb.queries["insertSpawnGroup"] insertQuery := `INSERT INTO spawn_location_group (group_id, location_id) VALUES (?, ?)`
if insertStmt == nil {
return fmt.Errorf("insert spawn group statement not prepared")
}
for _, locationID := range locationIDs { for _, locationID := range locationIDs {
_, err = tx.Stmt(insertStmt).Exec(groupID, locationID) err = sqlitex.Execute(zdb.conn, insertQuery, &sqlitex.ExecOptions{
Args: []any{groupID, locationID},
})
if err != nil { if err != nil {
return fmt.Errorf("failed to insert spawn group association: %v", err) return fmt.Errorf("failed to insert spawn group association: %v", err)
} }
} }
// Commit transaction
if err := tx.Commit(); err != nil {
return fmt.Errorf("failed to commit spawn group transaction: %v", err)
}
return nil return nil
} }
// Close closes all prepared statements and database connection // Close closes the database connection
func (zdb *ZoneDatabase) Close() error { func (zdb *ZoneDatabase) Close() error {
zdb.mutex.Lock() return nil // Connection is managed externally
defer zdb.mutex.Unlock()
// Close all prepared statements
for name, stmt := range zdb.queries {
if err := stmt.Close(); err != nil {
log.Printf("%s Error closing statement %s: %v", LogPrefixZone, name, err)
}
}
zdb.queries = make(map[string]*sql.Stmt)
return nil
} }
// Private helper methods // Private helper methods
func (zdb *ZoneDatabase) prepareStatements() error {
statements := map[string]string{
"updateZoneConfig": `
UPDATE zones SET
name = ?, file = ?, description = ?, safe_x = ?, safe_y = ?, safe_z = ?,
safe_heading = ?, underworld = ?, min_level = ?, max_level = ?, min_status = ?,
min_version = ?, instance_type = ?, max_players = ?, default_lockout_time = ?,
default_reenter_time = ?, default_reset_time = ?, group_zone_option = ?,
expansion_flag = ?, holiday_flag = ?, can_bind = ?, can_gate = ?, can_evac = ?,
city_zone = ?, always_loaded = ?, weather_allowed = ?
WHERE id = ?`,
"selectZoneConfig": `
SELECT id, name, file, description, safe_x, safe_y, safe_z, safe_heading, underworld,
min_level, max_level, min_status, min_version, instance_type, max_players,
default_lockout_time, default_reenter_time, default_reset_time, group_zone_option,
expansion_flag, holiday_flag, can_bind, can_gate, can_evac, city_zone, always_loaded,
weather_allowed
FROM zones WHERE id = ?`,
"selectSpawnLocations": `
SELECT id, x, y, z, heading, pitch, roll, spawn_type, respawn_time, expire_time,
expire_offset, conditions, conditional_value, spawn_percentage
FROM spawn_location_placement WHERE zone_id = ?
ORDER BY id`,
"selectSpawnLocation": `
SELECT id, x, y, z, heading, pitch, roll, spawn_type, respawn_time, expire_time,
expire_offset, conditions, conditional_value, spawn_percentage
FROM spawn_location_placement WHERE id = ?`,
"insertSpawnLocation": `
INSERT INTO spawn_location_placement
(x, y, z, heading, pitch, roll, spawn_type, respawn_time, expire_time,
expire_offset, conditions, conditional_value, spawn_percentage)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
RETURNING id`,
"updateSpawnLocation": `
UPDATE spawn_location_placement SET
x = ?, y = ?, z = ?, heading = ?, pitch = ?, roll = ?, spawn_type = ?,
respawn_time = ?, expire_time = ?, expire_offset = ?, conditions = ?,
conditional_value = ?, spawn_percentage = ?
WHERE id = ?`,
"deleteSpawnLocation": `DELETE FROM spawn_location_placement WHERE id = ?`,
"selectSpawnEntries": `
SELECT id, spawn_type, spawn_entry_id, name, level, encounter_level, model, size,
hp, power, heroic, gender, race, adventure_class, tradeskill_class, attack_type,
min_level, max_level, encounter_type, show_name, targetable, show_level,
command_primary, command_secondary, loot_tier, min_gold, max_gold, harvest_type,
icon
FROM spawn_location_entry WHERE zone_id = ?
ORDER BY id`,
"selectNPCs": `
SELECT id, spawn_entry_id, name, level, encounter_level, model, size, hp, power,
heroic, gender, race, adventure_class, tradeskill_class, attack_type, min_level,
max_level, encounter_type, show_name, targetable, show_level, loot_tier,
min_gold, max_gold, aggro_radius, cast_percentage, randomize
FROM spawn_npcs WHERE zone_id = ?
ORDER BY id`,
"selectObjects": `
SELECT id, spawn_entry_id, name, model, size, device_id, icon, sound_name
FROM spawn_objects WHERE zone_id = ?
ORDER BY id`,
"selectWidgets": `
SELECT id, spawn_entry_id, name, model, widget_type, open_type, open_time,
close_time, open_sound, close_sound, open_graphic, close_graphic,
linked_spawn_id, action_spawn_id, house_id, include_location,
include_heading
FROM spawn_widgets WHERE zone_id = ?
ORDER BY id`,
"selectSigns": `
SELECT id, spawn_entry_id, name, model, sign_type, zone_id_destination,
widget_id, title, description, zone_x, zone_y, zone_z, zone_heading,
include_location, include_heading
FROM spawn_signs WHERE zone_id = ?
ORDER BY id`,
"selectGroundSpawns": `
SELECT id, spawn_entry_id, name, model, harvest_type, number_harvests,
max_number_harvests, collection_skill, respawn_timer
FROM spawn_ground_spawns WHERE zone_id = ?
ORDER BY id`,
"selectTransporters": `
SELECT id, type, display_name, message, destination_zone_id, destination_x,
destination_y, destination_z, destination_heading, cost, unique_id,
min_level, max_level, quest_req, quest_step_req, quest_complete,
map_x, map_y, expansion_flag, holiday_flag, min_client_version,
max_client_version, flight_path_id, mount_id, mount_red_color,
mount_green_color, mount_blue_color
FROM transporters WHERE zone_id = ?
ORDER BY id`,
"selectLocationGrids": `
SELECT id, grid_id, name, include_y, discovery
FROM location_grids WHERE zone_id = ?
ORDER BY id`,
"selectLocationGridLocations": `
SELECT location_id, x, y, z, name
FROM location_grid_locations WHERE grid_id = ?
ORDER BY location_id`,
"selectRevivePoints": `
SELECT id, zone_id, location_name, x, y, z, heading, always_included
FROM revive_points WHERE zone_id = ?
ORDER BY id`,
"selectSpawnGroups": `
SELECT group_id, location_id
FROM spawn_location_group WHERE zone_id = ?
ORDER BY group_id, location_id`,
"insertSpawnGroup": `
INSERT INTO spawn_location_group (group_id, location_id) VALUES (?, ?)`,
"deleteSpawnGroup": `
DELETE FROM spawn_location_group WHERE group_id = ?`,
}
for name, query := range statements {
stmt, err := zdb.db.Prepare(query)
if err != nil {
return fmt.Errorf("failed to prepare statement %s: %v", name, err)
}
zdb.queries[name] = stmt
}
return nil
}
func (zdb *ZoneDatabase) loadZoneConfiguration(zoneData *ZoneData) error { func (zdb *ZoneDatabase) loadZoneConfiguration(zoneData *ZoneData) error {
stmt := zdb.queries["selectZoneConfig"] query := `SELECT id, name, file, description, safe_x, safe_y, safe_z, safe_heading, underworld,
if stmt == nil { min_level, max_level, min_status, min_version, instance_type, max_players,
return fmt.Errorf("select zone config statement not prepared") default_lockout_time, default_reenter_time, default_reset_time, group_zone_option,
} expansion_flag, holiday_flag, can_bind, can_gate, can_evac, city_zone, always_loaded,
weather_allowed
FROM zones WHERE id = ?`
config := &ZoneConfiguration{} config := &ZoneConfiguration{}
err := stmt.QueryRow(zoneData.ZoneID).Scan( found := false
&config.ZoneID,
&config.Name, err := sqlitex.Execute(zdb.conn, query, &sqlitex.ExecOptions{
&config.File, Args: []any{zoneData.ZoneID},
&config.Description, ResultFunc: func(stmt *sqlite.Stmt) error {
&config.SafeX, found = true
&config.SafeY, config.ZoneID = int32(stmt.ColumnInt64(0))
&config.SafeZ, config.Name = stmt.ColumnText(1)
&config.SafeHeading, config.File = stmt.ColumnText(2)
&config.Underworld, config.Description = stmt.ColumnText(3)
&config.MinLevel, config.SafeX = float32(stmt.ColumnFloat(4))
&config.MaxLevel, config.SafeY = float32(stmt.ColumnFloat(5))
&config.MinStatus, config.SafeZ = float32(stmt.ColumnFloat(6))
&config.MinVersion, config.SafeHeading = float32(stmt.ColumnFloat(7))
&config.InstanceType, config.Underworld = float32(stmt.ColumnFloat(8))
&config.MaxPlayers, config.MinLevel = int16(stmt.ColumnInt64(9))
&config.DefaultLockoutTime, config.MaxLevel = int16(stmt.ColumnInt64(10))
&config.DefaultReenterTime, config.MinStatus = int16(stmt.ColumnInt64(11))
&config.DefaultResetTime, config.MinVersion = int16(stmt.ColumnInt64(12))
&config.GroupZoneOption, config.InstanceType = int16(stmt.ColumnInt64(13))
&config.ExpansionFlag, config.MaxPlayers = int32(stmt.ColumnInt64(14))
&config.HolidayFlag, config.DefaultLockoutTime = int32(stmt.ColumnInt64(15))
&config.CanBind, config.DefaultReenterTime = int32(stmt.ColumnInt64(16))
&config.CanGate, config.DefaultResetTime = int32(stmt.ColumnInt64(17))
&config.CanEvac, config.GroupZoneOption = int8(stmt.ColumnInt64(18))
&config.CityZone, config.ExpansionFlag = int32(stmt.ColumnInt64(19))
&config.AlwaysLoaded, config.HolidayFlag = int32(stmt.ColumnInt64(20))
&config.WeatherAllowed, config.CanBind = stmt.ColumnInt64(21) != 0
) config.CanGate = stmt.ColumnInt64(22) != 0
config.CanEvac = stmt.ColumnInt64(23) != 0
config.CityZone = stmt.ColumnInt64(24) != 0
config.AlwaysLoaded = stmt.ColumnInt64(25) != 0
config.WeatherAllowed = stmt.ColumnInt64(26) != 0
return nil
},
})
if err != nil { if err != nil {
return fmt.Errorf("failed to load zone configuration: %v", err) return fmt.Errorf("failed to load zone configuration: %v", err)
} }
if !found {
return fmt.Errorf("zone configuration not found for zone %d", zoneData.ZoneID)
}
zoneData.Configuration = config zoneData.Configuration = config
return nil return nil
} }
func (zdb *ZoneDatabase) loadSpawnLocations(zoneData *ZoneData) error { func (zdb *ZoneDatabase) loadSpawnLocations(zoneData *ZoneData) error {
stmt := zdb.queries["selectSpawnLocations"] query := `SELECT id, x, y, z, heading, pitch, roll, spawn_type, respawn_time, expire_time,
if stmt == nil { expire_offset, conditions, conditional_value, spawn_percentage
return fmt.Errorf("select spawn locations statement not prepared") FROM spawn_location_placement WHERE zone_id = ?
} ORDER BY id`
rows, err := stmt.Query(zoneData.ZoneID)
if err != nil {
return fmt.Errorf("failed to query spawn locations: %v", err)
}
defer rows.Close()
locations := make(map[int32]*SpawnLocation) locations := make(map[int32]*SpawnLocation)
err := sqlitex.Execute(zdb.conn, query, &sqlitex.ExecOptions{
Args: []any{zoneData.ZoneID},
ResultFunc: func(stmt *sqlite.Stmt) error {
location := &SpawnLocation{
ID: int32(stmt.ColumnInt64(0)),
X: float32(stmt.ColumnFloat(1)),
Y: float32(stmt.ColumnFloat(2)),
Z: float32(stmt.ColumnFloat(3)),
Heading: float32(stmt.ColumnFloat(4)),
Pitch: float32(stmt.ColumnFloat(5)),
Roll: float32(stmt.ColumnFloat(6)),
SpawnType: int8(stmt.ColumnInt64(7)),
RespawnTime: int32(stmt.ColumnInt64(8)),
ExpireTime: int32(stmt.ColumnInt64(9)),
ExpireOffset: int32(stmt.ColumnInt64(10)),
Conditions: int8(stmt.ColumnInt64(11)),
ConditionalValue: int32(stmt.ColumnInt64(12)),
SpawnPercentage: float32(stmt.ColumnFloat(13)),
}
locations[location.ID] = location
return nil
},
})
for rows.Next() { if err != nil {
location := &SpawnLocation{} return fmt.Errorf("failed to load spawn locations: %v", err)
err := rows.Scan(
&location.ID,
&location.X,
&location.Y,
&location.Z,
&location.Heading,
&location.Pitch,
&location.Roll,
&location.SpawnType,
&location.RespawnTime,
&location.ExpireTime,
&location.ExpireOffset,
&location.Conditions,
&location.ConditionalValue,
&location.SpawnPercentage,
)
if err != nil {
return fmt.Errorf("failed to scan spawn location: %v", err)
}
locations[location.ID] = location
}
if err := rows.Err(); err != nil {
return fmt.Errorf("error iterating spawn locations: %v", err)
} }
zoneData.SpawnLocations = locations zoneData.SpawnLocations = locations
@ -583,57 +436,57 @@ func (zdb *ZoneDatabase) loadSpawnLocations(zoneData *ZoneData) error {
} }
func (zdb *ZoneDatabase) loadSpawnEntries(zoneData *ZoneData) error { func (zdb *ZoneDatabase) loadSpawnEntries(zoneData *ZoneData) error {
// Similar implementation for spawn entries // Placeholder implementation - initialize empty map
zoneData.SpawnEntries = make(map[int32]*SpawnEntry) zoneData.SpawnEntries = make(map[int32]*SpawnEntry)
return nil // TODO: Implement return nil
} }
func (zdb *ZoneDatabase) loadNPCs(zoneData *ZoneData) error { func (zdb *ZoneDatabase) loadNPCs(zoneData *ZoneData) error {
// Similar implementation for NPCs // Placeholder implementation - initialize empty map
zoneData.NPCs = make(map[int32]*NPCTemplate) zoneData.NPCs = make(map[int32]*NPCTemplate)
return nil // TODO: Implement return nil
} }
func (zdb *ZoneDatabase) loadObjects(zoneData *ZoneData) error { func (zdb *ZoneDatabase) loadObjects(zoneData *ZoneData) error {
// Similar implementation for objects // Placeholder implementation - initialize empty map
zoneData.Objects = make(map[int32]*ObjectTemplate) zoneData.Objects = make(map[int32]*ObjectTemplate)
return nil // TODO: Implement return nil
} }
func (zdb *ZoneDatabase) loadWidgets(zoneData *ZoneData) error { func (zdb *ZoneDatabase) loadWidgets(zoneData *ZoneData) error {
// Similar implementation for widgets // Placeholder implementation - initialize empty map
zoneData.Widgets = make(map[int32]*WidgetTemplate) zoneData.Widgets = make(map[int32]*WidgetTemplate)
return nil // TODO: Implement return nil
} }
func (zdb *ZoneDatabase) loadSigns(zoneData *ZoneData) error { func (zdb *ZoneDatabase) loadSigns(zoneData *ZoneData) error {
// Similar implementation for signs // Placeholder implementation - initialize empty map
zoneData.Signs = make(map[int32]*SignTemplate) zoneData.Signs = make(map[int32]*SignTemplate)
return nil // TODO: Implement return nil
} }
func (zdb *ZoneDatabase) loadGroundSpawns(zoneData *ZoneData) error { func (zdb *ZoneDatabase) loadGroundSpawns(zoneData *ZoneData) error {
// Similar implementation for ground spawns // Placeholder implementation - initialize empty map
zoneData.GroundSpawns = make(map[int32]*GroundSpawnTemplate) zoneData.GroundSpawns = make(map[int32]*GroundSpawnTemplate)
return nil // TODO: Implement return nil
} }
func (zdb *ZoneDatabase) loadTransporters(zoneData *ZoneData) error { func (zdb *ZoneDatabase) loadTransporters(zoneData *ZoneData) error {
// Similar implementation for transporters // Placeholder implementation - initialize empty map
zoneData.Transporters = make(map[int32]*TransportDestination) zoneData.Transporters = make(map[int32]*TransportDestination)
return nil // TODO: Implement return nil
} }
func (zdb *ZoneDatabase) loadLocationGrids(zoneData *ZoneData) error { func (zdb *ZoneDatabase) loadLocationGrids(zoneData *ZoneData) error {
// Similar implementation for location grids // Placeholder implementation - initialize empty map
zoneData.LocationGrids = make(map[int32]*LocationGrid) zoneData.LocationGrids = make(map[int32]*LocationGrid)
return nil // TODO: Implement return nil
} }
func (zdb *ZoneDatabase) loadRevivePoints(zoneData *ZoneData) error { func (zdb *ZoneDatabase) loadRevivePoints(zoneData *ZoneData) error {
// Similar implementation for revive points // Placeholder implementation - initialize empty map
zoneData.RevivePoints = make(map[int32]*RevivePoint) zoneData.RevivePoints = make(map[int32]*RevivePoint)
return nil // TODO: Implement return nil
} }
// ZoneData represents all data loaded for a zone // ZoneData represents all data loaded for a zone
@ -775,3 +628,7 @@ type GroundSpawnTemplate struct {
CollectionSkill string CollectionSkill string
RespawnTimer int32 RespawnTimer int32
} }
// Types are already defined in interfaces.go
// All types are defined in interfaces.go

View File

@ -121,7 +121,11 @@ func (mm *MobMovementManager) Process() error {
func (mm *MobMovementManager) AddMovementSpawn(spawnID int32) { func (mm *MobMovementManager) AddMovementSpawn(spawnID int32) {
mm.mutex.Lock() mm.mutex.Lock()
defer mm.mutex.Unlock() defer mm.mutex.Unlock()
mm.addMovementSpawnInternal(spawnID)
}
// addMovementSpawnInternal adds a spawn to movement tracking without acquiring lock
func (mm *MobMovementManager) addMovementSpawnInternal(spawnID int32) {
if _, exists := mm.movementSpawns[spawnID]; exists { if _, exists := mm.movementSpawns[spawnID]; exists {
return // Already tracking return // Already tracking
} }
@ -131,7 +135,10 @@ func (mm *MobMovementManager) AddMovementSpawn(spawnID int32) {
return return
} }
x, y, z, heading := spawn.GetPosition() x := spawn.GetX()
y := spawn.GetY()
z := spawn.GetZ()
heading := spawn.GetHeading()
mm.movementSpawns[spawnID] = &MovementState{ mm.movementSpawns[spawnID] = &MovementState{
SpawnID: spawnID, SpawnID: spawnID,
@ -234,7 +241,10 @@ func (mm *MobMovementManager) EvadeCombat(spawnID int32) error {
// Get spawn's original position (would need to be stored somewhere) // Get spawn's original position (would need to be stored somewhere)
// For now, use current position as placeholder // For now, use current position as placeholder
x, y, z, heading := spawn.GetPosition() x := spawn.GetX()
y := spawn.GetY()
z := spawn.GetZ()
heading := spawn.GetHeading()
command := &MovementCommand{ command := &MovementCommand{
Type: MovementCommandEvadeCombat, Type: MovementCommandEvadeCombat,
@ -257,7 +267,7 @@ func (mm *MobMovementManager) QueueCommand(spawnID int32, command *MovementComma
// Ensure spawn is being tracked // Ensure spawn is being tracked
if _, exists := mm.movementSpawns[spawnID]; !exists { if _, exists := mm.movementSpawns[spawnID]; !exists {
mm.AddMovementSpawn(spawnID) mm.addMovementSpawnInternal(spawnID)
} }
// Add command to queue // Add command to queue
@ -373,7 +383,9 @@ func (mm *MobMovementManager) processMovementCommand(spawn *spawn.Spawn, state *
} }
func (mm *MobMovementManager) processMoveTo(spawn *spawn.Spawn, state *MovementState, command *MovementCommand, deltaTime float32) (bool, error) { func (mm *MobMovementManager) processMoveTo(spawn *spawn.Spawn, state *MovementState, command *MovementCommand, deltaTime float32) (bool, error) {
currentX, currentY, currentZ, currentHeading := spawn.GetPosition() currentX := spawn.GetX()
currentY := spawn.GetY()
currentZ := spawn.GetZ()
// Calculate distance to target // Calculate distance to target
distanceToTarget := Distance3D(currentX, currentY, currentZ, command.TargetX, command.TargetY, command.TargetZ) distanceToTarget := Distance3D(currentX, currentY, currentZ, command.TargetX, command.TargetY, command.TargetZ)
@ -421,7 +433,10 @@ func (mm *MobMovementManager) processMoveTo(spawn *spawn.Spawn, state *MovementS
newHeading := CalculateHeading(currentX, currentY, command.TargetX, command.TargetY) newHeading := CalculateHeading(currentX, currentY, command.TargetX, command.TargetY)
// Update spawn position // Update spawn position
spawn.SetPosition(newX, newY, newZ, newHeading) spawn.SetX(newX)
spawn.SetY(newY, false)
spawn.SetZ(newZ)
spawn.SetHeadingFromFloat(newHeading)
// Update state // Update state
state.LastPosition.Set(newX, newY, newZ, newHeading) state.LastPosition.Set(newX, newY, newZ, newHeading)
@ -441,7 +456,10 @@ func (mm *MobMovementManager) processSwimTo(spawn *spawn.Spawn, state *MovementS
func (mm *MobMovementManager) processTeleportTo(spawn *spawn.Spawn, state *MovementState, command *MovementCommand) (bool, error) { func (mm *MobMovementManager) processTeleportTo(spawn *spawn.Spawn, state *MovementState, command *MovementCommand) (bool, error) {
// Instant teleport // Instant teleport
spawn.SetPosition(command.TargetX, command.TargetY, command.TargetZ, command.TargetHeading) spawn.SetX(command.TargetX)
spawn.SetY(command.TargetY, false)
spawn.SetZ(command.TargetZ)
spawn.SetHeadingFromFloat(command.TargetHeading)
// Update state // Update state
state.LastPosition.Set(command.TargetX, command.TargetY, command.TargetZ, command.TargetHeading) state.LastPosition.Set(command.TargetX, command.TargetY, command.TargetZ, command.TargetHeading)
@ -458,14 +476,14 @@ func (mm *MobMovementManager) processTeleportTo(spawn *spawn.Spawn, state *Movem
} }
func (mm *MobMovementManager) processRotateTo(spawn *spawn.Spawn, state *MovementState, command *MovementCommand, deltaTime float32) (bool, error) { func (mm *MobMovementManager) processRotateTo(spawn *spawn.Spawn, state *MovementState, command *MovementCommand, deltaTime float32) (bool, error) {
currentX, currentY, currentZ, currentHeading := spawn.GetPosition() currentHeading := spawn.GetHeading()
// Calculate heading difference // Calculate heading difference
headingDiff := HeadingDifference(currentHeading, command.TargetHeading) headingDiff := HeadingDifference(currentHeading, command.TargetHeading)
// Check if we've reached the target heading // Check if we've reached the target heading
if abs(headingDiff) <= 1.0 { // Close enough threshold if abs(headingDiff) <= 1.0 { // Close enough threshold
spawn.SetPosition(currentX, currentY, currentZ, command.TargetHeading) spawn.SetHeadingFromFloat(command.TargetHeading)
mm.zone.markSpawnChanged(spawn.GetID()) mm.zone.markSpawnChanged(spawn.GetID())
return true, nil return true, nil
} }
@ -490,7 +508,7 @@ func (mm *MobMovementManager) processRotateTo(spawn *spawn.Spawn, state *Movemen
// Apply rotation // Apply rotation
newHeading := NormalizeHeading(currentHeading + rotation) newHeading := NormalizeHeading(currentHeading + rotation)
spawn.SetPosition(currentX, currentY, currentZ, newHeading) spawn.SetHeadingFromFloat(newHeading)
// Update state // Update state
state.LastPosition.Heading = newHeading state.LastPosition.Heading = newHeading
@ -526,7 +544,9 @@ func (mm *MobMovementManager) getNextCommand(spawnID int32) *MovementCommand {
} }
func (mm *MobMovementManager) checkStuck(spawn *spawn.Spawn, state *MovementState) bool { func (mm *MobMovementManager) checkStuck(spawn *spawn.Spawn, state *MovementState) bool {
currentX, currentY, currentZ, _ := spawn.GetPosition() currentX := spawn.GetX()
currentY := spawn.GetY()
currentZ := spawn.GetZ()
// Check if spawn has moved significantly since last update // Check if spawn has moved significantly since last update
if state.LastPosition != nil { if state.LastPosition != nil {
@ -546,7 +566,10 @@ func (mm *MobMovementManager) handleStuck(spawn *spawn.Spawn, state *MovementSta
// Get or create stuck info // Get or create stuck info
stuckInfo, exists := mm.stuckSpawns[spawnID] stuckInfo, exists := mm.stuckSpawns[spawnID]
if !exists { if !exists {
currentX, currentY, currentZ, currentHeading := spawn.GetPosition() currentX := spawn.GetX()
currentY := spawn.GetY()
currentZ := spawn.GetZ()
currentHeading := spawn.GetHeading()
stuckInfo = &StuckInfo{ stuckInfo = &StuckInfo{
Position: NewPosition(currentX, currentY, currentZ, currentHeading), Position: NewPosition(currentX, currentY, currentZ, currentHeading),
StuckCount: 0, StuckCount: 0,
@ -588,8 +611,6 @@ func (mm *MobMovementManager) handleStuckWithRun(spawn *spawn.Spawn, state *Move
} }
// Try a slightly different path // Try a slightly different path
currentX, currentY, currentZ, _ := spawn.GetPosition()
// Add some randomness to the movement // Add some randomness to the movement
offsetX := float32((time.Now().UnixNano()%100 - 50)) / 50.0 * 2.0 offsetX := float32((time.Now().UnixNano()%100 - 50)) / 50.0 * 2.0
offsetY := float32((time.Now().UnixNano()%100 - 50)) / 50.0 * 2.0 offsetY := float32((time.Now().UnixNano()%100 - 50)) / 50.0 * 2.0
@ -607,7 +628,10 @@ func (mm *MobMovementManager) handleStuckWithRun(spawn *spawn.Spawn, state *Move
func (mm *MobMovementManager) handleStuckWithWarp(spawn *spawn.Spawn, state *MovementState, command *MovementCommand, stuckInfo *StuckInfo) (bool, error) { func (mm *MobMovementManager) handleStuckWithWarp(spawn *spawn.Spawn, state *MovementState, command *MovementCommand, stuckInfo *StuckInfo) (bool, error) {
// Teleport directly to target // Teleport directly to target
spawn.SetPosition(command.TargetX, command.TargetY, command.TargetZ, command.TargetHeading) spawn.SetX(command.TargetX)
spawn.SetY(command.TargetY, false)
spawn.SetZ(command.TargetZ)
spawn.SetHeadingFromFloat(command.TargetHeading)
mm.zone.markSpawnChanged(spawn.GetID()) mm.zone.markSpawnChanged(spawn.GetID())
log.Printf("%s Warped stuck spawn %d to target", LogPrefixMovement, spawn.GetID()) log.Printf("%s Warped stuck spawn %d to target", LogPrefixMovement, spawn.GetID())
@ -617,7 +641,10 @@ func (mm *MobMovementManager) handleStuckWithWarp(spawn *spawn.Spawn, state *Mov
func (mm *MobMovementManager) handleStuckWithEvade(spawn *spawn.Spawn, state *MovementState, command *MovementCommand, stuckInfo *StuckInfo) (bool, error) { func (mm *MobMovementManager) handleStuckWithEvade(spawn *spawn.Spawn, state *MovementState, command *MovementCommand, stuckInfo *StuckInfo) (bool, error) {
// Return to original position (evade) // Return to original position (evade)
if stuckInfo.Position != nil { if stuckInfo.Position != nil {
spawn.SetPosition(stuckInfo.Position.X, stuckInfo.Position.Y, stuckInfo.Position.Z, stuckInfo.Position.Heading) spawn.SetX(stuckInfo.Position.X)
spawn.SetY(stuckInfo.Position.Y, false)
spawn.SetZ(stuckInfo.Position.Z)
spawn.SetHeadingFromFloat(stuckInfo.Position.Heading)
mm.zone.markSpawnChanged(spawn.GetID()) mm.zone.markSpawnChanged(spawn.GetID())
log.Printf("%s Evaded stuck spawn %d to original position", LogPrefixMovement, spawn.GetID()) log.Printf("%s Evaded stuck spawn %d to original position", LogPrefixMovement, spawn.GetID())

View File

@ -260,7 +260,7 @@ func ValidatePath(path *Path) error {
} }
if totalDistance > MaxPathDistance { if totalDistance > MaxPathDistance {
return fmt.Errorf("path is too long: %.2f > %.2f", totalDistance, MaxPathDistance) return fmt.Errorf("path is too long: %.2f > %d", totalDistance, MaxPathDistance)
} }
path.Distance = totalDistance path.Distance = totalDistance

View File

@ -2,6 +2,7 @@ package zone
import ( import (
"math" "math"
"math/rand"
) )
// Position represents a 3D position with heading // Position represents a 3D position with heading
@ -421,10 +422,10 @@ func InterpolateHeading(from, to, t float32) float32 {
// GetRandomPositionInRadius generates a random position within the given radius // GetRandomPositionInRadius generates a random position within the given radius
func GetRandomPositionInRadius(centerX, centerY, centerZ float32, radius float32) *Position { func GetRandomPositionInRadius(centerX, centerY, centerZ float32, radius float32) *Position {
// Generate random angle // Generate random angle
angle := float32(math.Random() * 2 * math.Pi) angle := float32(rand.Float64() * 2 * math.Pi)
// Generate random distance (uniform distribution in circle) // Generate random distance (uniform distribution in circle)
distance := float32(math.Sqrt(math.Random())) * radius distance := float32(math.Sqrt(rand.Float64())) * radius
// Calculate new position // Calculate new position
x := centerX + distance*float32(math.Cos(float64(angle))) x := centerX + distance*float32(math.Cos(float64(angle)))

View File

@ -33,7 +33,7 @@ const (
const ( const (
RegionFileExtension = ".region" RegionFileExtension = ".region"
WaterVolumeType = "watervol" WaterVolumeType = "watervol"
WaterRegionType = "waterregion" WaterRegionString = "waterregion"
WaterRegion2Type = "water_region" WaterRegion2Type = "water_region"
OceanType = "ocean" OceanType = "ocean"
WaterType = "water" WaterType = "water"

View File

@ -5,7 +5,6 @@ import (
"log" "log"
"math" "math"
"sync/atomic" "sync/atomic"
"time"
) )
// NewRegionManager creates a new region manager for a zone // NewRegionManager creates a new region manager for a zone
@ -30,7 +29,6 @@ func (rm *RegionManager) GetRegionMap(version int32) RegionMap {
// ReturnRegionType returns the region type at the given location for a client version // ReturnRegionType returns the region type at the given location for a client version
func (rm *RegionManager) ReturnRegionType(location [3]float32, gridID int32, version int32) WaterRegionType { func (rm *RegionManager) ReturnRegionType(location [3]float32, gridID int32, version int32) WaterRegionType {
startTime := time.Now()
defer func() { defer func() {
atomic.AddInt64(&rm.regionChecks, 1) atomic.AddInt64(&rm.regionChecks, 1)
// Update average check time (simplified moving average) // Update average check time (simplified moving average)

View File

@ -3,7 +3,6 @@ package region
import ( import (
"fmt" "fmt"
"log" "log"
"sync"
) )
// NewRegionMapRange creates a new region map range for a zone // NewRegionMapRange creates a new region map range for a zone

View File

@ -5,7 +5,6 @@ import (
"sync/atomic" "sync/atomic"
"time" "time"
"eq2emu/internal/common"
"eq2emu/internal/spawn" "eq2emu/internal/spawn"
) )
@ -74,6 +73,7 @@ type ZoneServer struct {
// Player and client management // Player and client management
clients []Client clients []Client
numPlayers int32 numPlayers int32
maxPlayers int32
incomingClients int32 incomingClients int32
lifetimeClientCount int32 lifetimeClientCount int32
@ -138,20 +138,20 @@ type ZoneServer struct {
tradeskillMgr TradeskillManager tradeskillMgr TradeskillManager
// Timers // Timers
aggroTimer *common.Timer aggroTimer *time.Timer
charsheetChanges *common.Timer charsheetChanges *time.Timer
clientSave *common.Timer clientSave *time.Timer
locationProxTimer *common.Timer locationProxTimer *time.Timer
movementTimer *common.Timer movementTimer *time.Timer
regenTimer *common.Timer regenTimer *time.Timer
respawnTimer *common.Timer respawnTimer *time.Timer
shutdownTimer *common.Timer shutdownTimer *time.Timer
spawnRangeTimer *common.Timer spawnRangeTimer *time.Timer
spawnUpdateTimer *common.Timer spawnUpdateTimer *time.Timer
syncGameTimer *common.Timer syncGameTimer *time.Timer
trackingTimer *common.Timer trackingTimer *time.Timer
weatherTimer *common.Timer weatherTimer *time.Timer
widgetTimer *common.Timer widgetTimer *time.Timer
// Proximity systems // Proximity systems
playerProximities map[int32]*PlayerProximity playerProximities map[int32]*PlayerProximity
@ -405,7 +405,7 @@ const (
SpawnScriptHealthChanged SpawnScriptHealthChanged
SpawnScriptRandomChat SpawnScriptRandomChat
SpawnScriptConversation SpawnScriptConversation
SpawnScriptTimer SpawnScriptTimerEvent
SpawnScriptCustom SpawnScriptCustom
SpawnScriptHailedBusy SpawnScriptHailedBusy
SpawnScriptCastedOn SpawnScriptCastedOn

View File

@ -6,7 +6,7 @@ import (
"sync" "sync"
"time" "time"
"eq2emu/internal/database" "zombiezen.com/go/sqlite"
) )
// ZoneManager manages all active zones in the server // ZoneManager manages all active zones in the server
@ -14,7 +14,7 @@ type ZoneManager struct {
zones map[int32]*ZoneServer zones map[int32]*ZoneServer
zonesByName map[string]*ZoneServer zonesByName map[string]*ZoneServer
instanceZones map[int32]*ZoneServer instanceZones map[int32]*ZoneServer
db *database.Database db *sqlite.Conn
config *ZoneManagerConfig config *ZoneManagerConfig
shutdownSignal chan struct{} shutdownSignal chan struct{}
isShuttingDown bool isShuttingDown bool
@ -39,7 +39,7 @@ type ZoneManagerConfig struct {
} }
// NewZoneManager creates a new zone manager // NewZoneManager creates a new zone manager
func NewZoneManager(config *ZoneManagerConfig, db *database.Database) *ZoneManager { func NewZoneManager(config *ZoneManagerConfig, db *sqlite.Conn) *ZoneManager {
if config.ProcessInterval == 0 { if config.ProcessInterval == 0 {
config.ProcessInterval = time.Millisecond * 100 // 10 FPS default config.ProcessInterval = time.Millisecond * 100 // 10 FPS default
} }
@ -136,7 +136,7 @@ func (zm *ZoneManager) LoadZone(zoneID int32) (*ZoneServer, error) {
} }
// Load zone data from database // Load zone data from database
zoneDB := NewZoneDatabase(zm.db.DB) zoneDB := NewZoneDatabase(zm.db)
zoneData, err := zoneDB.LoadZoneData(zoneID) zoneData, err := zoneDB.LoadZoneData(zoneID)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to load zone data: %v", err) return nil, fmt.Errorf("failed to load zone data: %v", err)
@ -234,7 +234,7 @@ func (zm *ZoneManager) CreateInstance(baseZoneID int32, instanceType InstanceTyp
} }
// Load base zone data // Load base zone data
zoneDB := NewZoneDatabase(zm.db.DB) zoneDB := NewZoneDatabase(zm.db)
zoneData, err := zoneDB.LoadZoneData(baseZoneID) zoneData, err := zoneDB.LoadZoneData(baseZoneID)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to load base zone data: %v", err) return nil, fmt.Errorf("failed to load base zone data: %v", err)

View File

@ -5,7 +5,6 @@ import (
"log" "log"
"time" "time"
"eq2emu/internal/common"
"eq2emu/internal/spawn" "eq2emu/internal/spawn"
) )
@ -55,6 +54,7 @@ func (zs *ZoneServer) Initialize(config *ZoneServerConfig) error {
// Set zone limits // Set zone limits
zs.minimumLevel = config.MinLevel zs.minimumLevel = config.MinLevel
zs.maximumLevel = config.MaxLevel zs.maximumLevel = config.MaxLevel
zs.maxPlayers = config.MaxPlayers
// Set safe coordinates // Set safe coordinates
zs.safeX = config.SafeX zs.safeX = config.SafeX
@ -304,7 +304,9 @@ func (zs *ZoneServer) GetSpawnsByRange(x, y, z, maxRange float32) []*spawn.Spawn
maxRangeSquared := maxRange * maxRange maxRangeSquared := maxRange * maxRange
for _, spawn := range zs.spawnList { for _, spawn := range zs.spawnList {
spawnX, spawnY, spawnZ, _ := spawn.GetPosition() spawnX := spawn.GetX()
spawnY := spawn.GetY()
spawnZ := spawn.GetZ()
distSquared := Distance3DSquared(x, y, z, spawnX, spawnY, spawnZ) distSquared := Distance3DSquared(x, y, z, spawnX, spawnY, spawnZ)
if distSquared <= maxRangeSquared { if distSquared <= maxRangeSquared {
@ -467,6 +469,11 @@ func (zs *ZoneServer) GetNumPlayers() int32 {
// GetMaxPlayers returns the maximum number of players allowed // GetMaxPlayers returns the maximum number of players allowed
func (zs *ZoneServer) GetMaxPlayers() int32 { func (zs *ZoneServer) GetMaxPlayers() int32 {
// Use configured max players if set, otherwise use instance/default values
if zs.maxPlayers > 0 {
return zs.maxPlayers
}
if zs.isInstance { if zs.isInstance {
// Instance zones have different limits based on type // Instance zones have different limits based on type
switch zs.instanceType { switch zs.instanceType {
@ -525,20 +532,20 @@ func (zs *ZoneServer) GetWatchdogTime() int32 {
// Private helper methods // Private helper methods
func (zs *ZoneServer) initializeTimers() error { func (zs *ZoneServer) initializeTimers() error {
zs.aggroTimer = common.NewTimer(AggroCheckInterval) zs.aggroTimer = time.NewTimer(AggroCheckInterval)
zs.charsheetChanges = common.NewTimer(CharsheetUpdateInterval) zs.charsheetChanges = time.NewTimer(CharsheetUpdateInterval)
zs.clientSave = common.NewTimer(ClientSaveInterval) zs.clientSave = time.NewTimer(ClientSaveInterval)
zs.locationProxTimer = common.NewTimer(LocationProximityInterval) zs.locationProxTimer = time.NewTimer(LocationProximityInterval)
zs.movementTimer = common.NewTimer(MovementUpdateInterval) zs.movementTimer = time.NewTimer(MovementUpdateInterval)
zs.regenTimer = common.NewTimer(DefaultTimerInterval) zs.regenTimer = time.NewTimer(DefaultTimerInterval)
zs.respawnTimer = common.NewTimer(RespawnCheckInterval) zs.respawnTimer = time.NewTimer(RespawnCheckInterval)
zs.shutdownTimer = common.NewTimer(0) // Disabled by default zs.shutdownTimer = time.NewTimer(0) // Disabled by default
zs.spawnRangeTimer = common.NewTimer(SpawnRangeUpdateInterval) zs.spawnRangeTimer = time.NewTimer(SpawnRangeUpdateInterval)
zs.spawnUpdateTimer = common.NewTimer(DefaultTimerInterval) zs.spawnUpdateTimer = time.NewTimer(DefaultTimerInterval)
zs.syncGameTimer = common.NewTimer(DefaultTimerInterval) zs.syncGameTimer = time.NewTimer(DefaultTimerInterval)
zs.trackingTimer = common.NewTimer(TrackingUpdateInterval) zs.trackingTimer = time.NewTimer(TrackingUpdateInterval)
zs.weatherTimer = common.NewTimer(WeatherUpdateInterval) zs.weatherTimer = time.NewTimer(WeatherUpdateInterval)
zs.widgetTimer = common.NewTimer(WidgetUpdateInterval) zs.widgetTimer = time.NewTimer(WidgetUpdateInterval)
return nil return nil
} }
@ -662,17 +669,26 @@ func (zs *ZoneServer) processSpawns() {
} }
func (zs *ZoneServer) processTimers() { func (zs *ZoneServer) processTimers() {
// Check and process all timers // Check and process all timers using non-blocking selects
if zs.aggroTimer.Check() { select {
case <-zs.aggroTimer.C:
zs.processAggroChecks() zs.processAggroChecks()
zs.aggroTimer.Reset(AggroCheckInterval)
default:
} }
if zs.respawnTimer.Check() { select {
case <-zs.respawnTimer.C:
zs.processRespawns() zs.processRespawns()
zs.respawnTimer.Reset(RespawnCheckInterval)
default:
} }
if zs.widgetTimer.Check() { select {
case <-zs.widgetTimer.C:
zs.processWidgets() zs.processWidgets()
zs.widgetTimer.Reset(WidgetUpdateInterval)
default:
} }
// Add other timer checks... // Add other timer checks...
@ -709,15 +725,21 @@ func (zs *ZoneServer) processSpawnChanges() {
func (zs *ZoneServer) processProximityChecks() { func (zs *ZoneServer) processProximityChecks() {
// Process player and location proximity // Process player and location proximity
if zs.locationProxTimer.Check() { select {
case <-zs.locationProxTimer.C:
zs.checkLocationProximity() zs.checkLocationProximity()
zs.checkPlayerProximity() zs.checkPlayerProximity()
zs.locationProxTimer.Reset(LocationProximityInterval)
default:
} }
} }
func (zs *ZoneServer) processWeather() { func (zs *ZoneServer) processWeather() {
if zs.weatherTimer.Check() { select {
case <-zs.weatherTimer.C:
zs.ProcessWeather() zs.ProcessWeather()
zs.weatherTimer.Reset(WeatherUpdateInterval)
default:
} }
} }

File diff suppressed because it is too large Load Diff