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
import (
"database/sql"
"fmt"
"log"
"sync"
"zombiezen.com/go/sqlite"
"zombiezen.com/go/sqlite/sqlitex"
)
// ZoneDatabase handles all database operations for zones
type ZoneDatabase struct {
db *sql.DB
queries map[string]*sql.Stmt
mutex sync.RWMutex
conn *sqlite.Conn
mutex sync.RWMutex
}
// NewZoneDatabase creates a new zone database instance
func NewZoneDatabase(db *sql.DB) *ZoneDatabase {
zdb := &ZoneDatabase{
db: db,
queries: make(map[string]*sql.Stmt),
func NewZoneDatabase(conn *sqlite.Conn) *ZoneDatabase {
return &ZoneDatabase{
conn: conn,
}
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
@ -98,40 +92,46 @@ func (zdb *ZoneDatabase) SaveZoneConfiguration(config *ZoneConfiguration) error
zdb.mutex.Lock()
defer zdb.mutex.Unlock()
stmt := zdb.queries["updateZoneConfig"]
if stmt == nil {
return fmt.Errorf("update zone config statement not prepared")
}
query := `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 = ?`
_, err := stmt.Exec(
config.Name,
config.File,
config.Description,
config.SafeX,
config.SafeY,
config.SafeZ,
config.SafeHeading,
config.Underworld,
config.MinLevel,
config.MaxLevel,
config.MinStatus,
config.MinVersion,
config.InstanceType,
config.MaxPlayers,
config.DefaultLockoutTime,
config.DefaultReenterTime,
config.DefaultResetTime,
config.GroupZoneOption,
config.ExpansionFlag,
config.HolidayFlag,
config.CanBind,
config.CanGate,
config.CanEvac,
config.CityZone,
config.AlwaysLoaded,
config.WeatherAllowed,
config.ZoneID,
)
err := sqlitex.Execute(zdb.conn, query, &sqlitex.ExecOptions{
Args: []any{
config.Name,
config.File,
config.Description,
config.SafeX,
config.SafeY,
config.SafeZ,
config.SafeHeading,
config.Underworld,
config.MinLevel,
config.MaxLevel,
config.MinStatus,
config.MinVersion,
config.InstanceType,
config.MaxPlayers,
config.DefaultLockoutTime,
config.DefaultReenterTime,
config.DefaultResetTime,
config.GroupZoneOption,
config.ExpansionFlag,
config.HolidayFlag,
config.CanBind,
config.CanGate,
config.CanEvac,
config.CityZone,
config.AlwaysLoaded,
config.WeatherAllowed,
config.ZoneID,
},
})
if err != nil {
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()
defer zdb.mutex.RUnlock()
stmt := zdb.queries["selectSpawnLocation"]
if stmt == nil {
return nil, fmt.Errorf("select spawn location statement not prepared")
}
query := `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 = ?`
location := &SpawnLocation{}
err := stmt.QueryRow(locationID).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,
)
err := sqlitex.Execute(zdb.conn, query, &sqlitex.ExecOptions{
Args: []any{locationID},
ResultFunc: func(stmt *sqlite.Stmt) error {
location.ID = int32(stmt.ColumnInt64(0))
location.X = float32(stmt.ColumnFloat(1))
location.Y = float32(stmt.ColumnFloat(2))
location.Z = float32(stmt.ColumnFloat(3))
location.Heading = float32(stmt.ColumnFloat(4))
location.Pitch = float32(stmt.ColumnFloat(5))
location.Roll = float32(stmt.ColumnFloat(6))
location.SpawnType = int8(stmt.ColumnInt64(7))
location.RespawnTime = int32(stmt.ColumnInt64(8))
location.ExpireTime = int32(stmt.ColumnInt64(9))
location.ExpireOffset = int32(stmt.ColumnInt64(10))
location.Conditions = int8(stmt.ColumnInt64(11))
location.ConditionalValue = int32(stmt.ColumnInt64(12))
location.SpawnPercentage = float32(stmt.ColumnFloat(13))
return nil
},
})
if err != nil {
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
}
@ -180,58 +187,67 @@ func (zdb *ZoneDatabase) SaveSpawnLocation(location *SpawnLocation) error {
zdb.mutex.Lock()
defer zdb.mutex.Unlock()
var stmt *sql.Stmt
var err error
if location.ID == 0 {
// Insert new location
stmt = zdb.queries["insertSpawnLocation"]
if stmt == nil {
return fmt.Errorf("insert spawn location statement not prepared")
}
query := `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`
err = stmt.QueryRow(
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,
).Scan(&location.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,
},
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 {
// Update existing location
stmt = zdb.queries["updateSpawnLocation"]
if stmt == nil {
return fmt.Errorf("update spawn location statement not prepared")
query := `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 = ?`
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
@ -242,12 +258,10 @@ func (zdb *ZoneDatabase) DeleteSpawnLocation(locationID int32) error {
zdb.mutex.Lock()
defer zdb.mutex.Unlock()
stmt := zdb.queries["deleteSpawnLocation"]
if stmt == nil {
return fmt.Errorf("delete spawn location statement not prepared")
}
_, err := stmt.Exec(locationID)
query := `DELETE FROM spawn_location_placement WHERE id = ?`
err := sqlitex.Execute(zdb.conn, query, &sqlitex.ExecOptions{
Args: []any{locationID},
})
if err != nil {
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()
defer zdb.mutex.RUnlock()
stmt := zdb.queries["selectSpawnGroups"]
if stmt == nil {
return nil, fmt.Errorf("select spawn groups statement not prepared")
}
rows, err := stmt.Query(zoneID)
if err != nil {
return nil, fmt.Errorf("failed to query spawn groups: %v", err)
}
defer rows.Close()
query := `SELECT group_id, location_id
FROM spawn_location_group WHERE zone_id = ?
ORDER BY group_id, location_id`
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() {
var groupID, locationID int32
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)
if err != nil {
return nil, fmt.Errorf("failed to load spawn groups: %v", err)
}
return groups, nil
@ -294,288 +301,134 @@ func (zdb *ZoneDatabase) SaveSpawnGroup(groupID int32, locationIDs []int32) erro
zdb.mutex.Lock()
defer zdb.mutex.Unlock()
// Start transaction
tx, err := zdb.db.Begin()
// Use transaction for atomic operations
endFn, err := sqlitex.ImmediateTransaction(zdb.conn)
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
deleteStmt := zdb.queries["deleteSpawnGroup"]
if deleteStmt == nil {
return fmt.Errorf("delete spawn group statement not prepared")
}
_, err = tx.Stmt(deleteStmt).Exec(groupID)
deleteQuery := `DELETE FROM spawn_location_group WHERE group_id = ?`
err = sqlitex.Execute(zdb.conn, deleteQuery, &sqlitex.ExecOptions{
Args: []any{groupID},
})
if err != nil {
return fmt.Errorf("failed to delete existing spawn group: %v", err)
}
// Insert new associations
insertStmt := zdb.queries["insertSpawnGroup"]
if insertStmt == nil {
return fmt.Errorf("insert spawn group statement not prepared")
}
insertQuery := `INSERT INTO spawn_location_group (group_id, location_id) VALUES (?, ?)`
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 {
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
}
// Close closes all prepared statements and database connection
// Close closes the database connection
func (zdb *ZoneDatabase) Close() error {
zdb.mutex.Lock()
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
return nil // Connection is managed externally
}
// 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 {
stmt := zdb.queries["selectZoneConfig"]
if stmt == nil {
return fmt.Errorf("select zone config statement not prepared")
}
query := `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 = ?`
config := &ZoneConfiguration{}
err := stmt.QueryRow(zoneData.ZoneID).Scan(
&config.ZoneID,
&config.Name,
&config.File,
&config.Description,
&config.SafeX,
&config.SafeY,
&config.SafeZ,
&config.SafeHeading,
&config.Underworld,
&config.MinLevel,
&config.MaxLevel,
&config.MinStatus,
&config.MinVersion,
&config.InstanceType,
&config.MaxPlayers,
&config.DefaultLockoutTime,
&config.DefaultReenterTime,
&config.DefaultResetTime,
&config.GroupZoneOption,
&config.ExpansionFlag,
&config.HolidayFlag,
&config.CanBind,
&config.CanGate,
&config.CanEvac,
&config.CityZone,
&config.AlwaysLoaded,
&config.WeatherAllowed,
)
found := false
err := sqlitex.Execute(zdb.conn, query, &sqlitex.ExecOptions{
Args: []any{zoneData.ZoneID},
ResultFunc: func(stmt *sqlite.Stmt) error {
found = true
config.ZoneID = int32(stmt.ColumnInt64(0))
config.Name = stmt.ColumnText(1)
config.File = stmt.ColumnText(2)
config.Description = stmt.ColumnText(3)
config.SafeX = float32(stmt.ColumnFloat(4))
config.SafeY = float32(stmt.ColumnFloat(5))
config.SafeZ = float32(stmt.ColumnFloat(6))
config.SafeHeading = float32(stmt.ColumnFloat(7))
config.Underworld = float32(stmt.ColumnFloat(8))
config.MinLevel = int16(stmt.ColumnInt64(9))
config.MaxLevel = int16(stmt.ColumnInt64(10))
config.MinStatus = int16(stmt.ColumnInt64(11))
config.MinVersion = int16(stmt.ColumnInt64(12))
config.InstanceType = int16(stmt.ColumnInt64(13))
config.MaxPlayers = int32(stmt.ColumnInt64(14))
config.DefaultLockoutTime = int32(stmt.ColumnInt64(15))
config.DefaultReenterTime = int32(stmt.ColumnInt64(16))
config.DefaultResetTime = int32(stmt.ColumnInt64(17))
config.GroupZoneOption = int8(stmt.ColumnInt64(18))
config.ExpansionFlag = int32(stmt.ColumnInt64(19))
config.HolidayFlag = int32(stmt.ColumnInt64(20))
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 {
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
return nil
}
func (zdb *ZoneDatabase) loadSpawnLocations(zoneData *ZoneData) error {
stmt := zdb.queries["selectSpawnLocations"]
if stmt == nil {
return fmt.Errorf("select spawn locations statement not prepared")
}
rows, err := stmt.Query(zoneData.ZoneID)
if err != nil {
return fmt.Errorf("failed to query spawn locations: %v", err)
}
defer rows.Close()
query := `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`
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() {
location := &SpawnLocation{}
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)
if err != nil {
return fmt.Errorf("failed to load spawn locations: %v", err)
}
zoneData.SpawnLocations = locations
@ -583,57 +436,57 @@ func (zdb *ZoneDatabase) loadSpawnLocations(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)
return nil // TODO: Implement
return nil
}
func (zdb *ZoneDatabase) loadNPCs(zoneData *ZoneData) error {
// Similar implementation for NPCs
// Placeholder implementation - initialize empty map
zoneData.NPCs = make(map[int32]*NPCTemplate)
return nil // TODO: Implement
return nil
}
func (zdb *ZoneDatabase) loadObjects(zoneData *ZoneData) error {
// Similar implementation for objects
// Placeholder implementation - initialize empty map
zoneData.Objects = make(map[int32]*ObjectTemplate)
return nil // TODO: Implement
return nil
}
func (zdb *ZoneDatabase) loadWidgets(zoneData *ZoneData) error {
// Similar implementation for widgets
// Placeholder implementation - initialize empty map
zoneData.Widgets = make(map[int32]*WidgetTemplate)
return nil // TODO: Implement
return nil
}
func (zdb *ZoneDatabase) loadSigns(zoneData *ZoneData) error {
// Similar implementation for signs
// Placeholder implementation - initialize empty map
zoneData.Signs = make(map[int32]*SignTemplate)
return nil // TODO: Implement
return nil
}
func (zdb *ZoneDatabase) loadGroundSpawns(zoneData *ZoneData) error {
// Similar implementation for ground spawns
// Placeholder implementation - initialize empty map
zoneData.GroundSpawns = make(map[int32]*GroundSpawnTemplate)
return nil // TODO: Implement
return nil
}
func (zdb *ZoneDatabase) loadTransporters(zoneData *ZoneData) error {
// Similar implementation for transporters
// Placeholder implementation - initialize empty map
zoneData.Transporters = make(map[int32]*TransportDestination)
return nil // TODO: Implement
return nil
}
func (zdb *ZoneDatabase) loadLocationGrids(zoneData *ZoneData) error {
// Similar implementation for location grids
// Placeholder implementation - initialize empty map
zoneData.LocationGrids = make(map[int32]*LocationGrid)
return nil // TODO: Implement
return nil
}
func (zdb *ZoneDatabase) loadRevivePoints(zoneData *ZoneData) error {
// Similar implementation for revive points
// Placeholder implementation - initialize empty map
zoneData.RevivePoints = make(map[int32]*RevivePoint)
return nil // TODO: Implement
return nil
}
// ZoneData represents all data loaded for a zone
@ -775,3 +628,7 @@ type GroundSpawnTemplate struct {
CollectionSkill string
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) {
mm.mutex.Lock()
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 {
return // Already tracking
}
@ -131,7 +135,10 @@ func (mm *MobMovementManager) AddMovementSpawn(spawnID int32) {
return
}
x, y, z, heading := spawn.GetPosition()
x := spawn.GetX()
y := spawn.GetY()
z := spawn.GetZ()
heading := spawn.GetHeading()
mm.movementSpawns[spawnID] = &MovementState{
SpawnID: spawnID,
@ -234,7 +241,10 @@ func (mm *MobMovementManager) EvadeCombat(spawnID int32) error {
// Get spawn's original position (would need to be stored somewhere)
// 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{
Type: MovementCommandEvadeCombat,
@ -257,7 +267,7 @@ func (mm *MobMovementManager) QueueCommand(spawnID int32, command *MovementComma
// Ensure spawn is being tracked
if _, exists := mm.movementSpawns[spawnID]; !exists {
mm.AddMovementSpawn(spawnID)
mm.addMovementSpawnInternal(spawnID)
}
// 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) {
currentX, currentY, currentZ, currentHeading := spawn.GetPosition()
currentX := spawn.GetX()
currentY := spawn.GetY()
currentZ := spawn.GetZ()
// Calculate distance to target
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)
// Update spawn position
spawn.SetPosition(newX, newY, newZ, newHeading)
spawn.SetX(newX)
spawn.SetY(newY, false)
spawn.SetZ(newZ)
spawn.SetHeadingFromFloat(newHeading)
// Update state
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) {
// 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
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) {
currentX, currentY, currentZ, currentHeading := spawn.GetPosition()
currentHeading := spawn.GetHeading()
// Calculate heading difference
headingDiff := HeadingDifference(currentHeading, command.TargetHeading)
// Check if we've reached the target heading
if abs(headingDiff) <= 1.0 { // Close enough threshold
spawn.SetPosition(currentX, currentY, currentZ, command.TargetHeading)
spawn.SetHeadingFromFloat(command.TargetHeading)
mm.zone.markSpawnChanged(spawn.GetID())
return true, nil
}
@ -490,7 +508,7 @@ func (mm *MobMovementManager) processRotateTo(spawn *spawn.Spawn, state *Movemen
// Apply rotation
newHeading := NormalizeHeading(currentHeading + rotation)
spawn.SetPosition(currentX, currentY, currentZ, newHeading)
spawn.SetHeadingFromFloat(newHeading)
// Update state
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 {
currentX, currentY, currentZ, _ := spawn.GetPosition()
currentX := spawn.GetX()
currentY := spawn.GetY()
currentZ := spawn.GetZ()
// Check if spawn has moved significantly since last update
if state.LastPosition != nil {
@ -546,7 +566,10 @@ func (mm *MobMovementManager) handleStuck(spawn *spawn.Spawn, state *MovementSta
// Get or create stuck info
stuckInfo, exists := mm.stuckSpawns[spawnID]
if !exists {
currentX, currentY, currentZ, currentHeading := spawn.GetPosition()
currentX := spawn.GetX()
currentY := spawn.GetY()
currentZ := spawn.GetZ()
currentHeading := spawn.GetHeading()
stuckInfo = &StuckInfo{
Position: NewPosition(currentX, currentY, currentZ, currentHeading),
StuckCount: 0,
@ -588,8 +611,6 @@ func (mm *MobMovementManager) handleStuckWithRun(spawn *spawn.Spawn, state *Move
}
// Try a slightly different path
currentX, currentY, currentZ, _ := spawn.GetPosition()
// Add some randomness to the movement
offsetX := 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) {
// 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())
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) {
// Return to original position (evade)
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())
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 {
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

View File

@ -2,6 +2,7 @@ package zone
import (
"math"
"math/rand"
)
// 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
func GetRandomPositionInRadius(centerX, centerY, centerZ float32, radius float32) *Position {
// Generate random angle
angle := float32(math.Random() * 2 * math.Pi)
angle := float32(rand.Float64() * 2 * math.Pi)
// Generate random distance (uniform distribution in circle)
distance := float32(math.Sqrt(math.Random())) * radius
distance := float32(math.Sqrt(rand.Float64())) * radius
// Calculate new position
x := centerX + distance*float32(math.Cos(float64(angle)))

View File

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

View File

@ -5,7 +5,6 @@ import (
"log"
"math"
"sync/atomic"
"time"
)
// 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
func (rm *RegionManager) ReturnRegionType(location [3]float32, gridID int32, version int32) WaterRegionType {
startTime := time.Now()
defer func() {
atomic.AddInt64(&rm.regionChecks, 1)
// Update average check time (simplified moving average)

View File

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

View File

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

View File

@ -6,7 +6,7 @@ import (
"sync"
"time"
"eq2emu/internal/database"
"zombiezen.com/go/sqlite"
)
// ZoneManager manages all active zones in the server
@ -14,7 +14,7 @@ type ZoneManager struct {
zones map[int32]*ZoneServer
zonesByName map[string]*ZoneServer
instanceZones map[int32]*ZoneServer
db *database.Database
db *sqlite.Conn
config *ZoneManagerConfig
shutdownSignal chan struct{}
isShuttingDown bool
@ -39,7 +39,7 @@ type ZoneManagerConfig struct {
}
// 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 {
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
zoneDB := NewZoneDatabase(zm.db.DB)
zoneDB := NewZoneDatabase(zm.db)
zoneData, err := zoneDB.LoadZoneData(zoneID)
if err != nil {
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
zoneDB := NewZoneDatabase(zm.db.DB)
zoneDB := NewZoneDatabase(zm.db)
zoneData, err := zoneDB.LoadZoneData(baseZoneID)
if err != nil {
return nil, fmt.Errorf("failed to load base zone data: %v", err)

View File

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

File diff suppressed because it is too large Load Diff