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

634 lines
18 KiB
Go

package zone
import (
"fmt"
"log"
"sync"
"zombiezen.com/go/sqlite"
"zombiezen.com/go/sqlite/sqlitex"
)
// ZoneDatabase handles all database operations for zones
type ZoneDatabase struct {
conn *sqlite.Conn
mutex sync.RWMutex
}
// NewZoneDatabase creates a new zone database instance
func NewZoneDatabase(conn *sqlite.Conn) *ZoneDatabase {
return &ZoneDatabase{
conn: conn,
}
}
// LoadZoneData loads all zone configuration and spawn data
func (zdb *ZoneDatabase) LoadZoneData(zoneID int32) (*ZoneData, error) {
zoneData := &ZoneData{
ZoneID: zoneID,
}
// Load zone configuration
if err := zdb.loadZoneConfiguration(zoneData); err != nil {
return nil, fmt.Errorf("failed to load zone configuration: %v", err)
}
// Load spawn locations
if err := zdb.loadSpawnLocations(zoneData); err != nil {
return nil, fmt.Errorf("failed to load spawn locations: %v", err)
}
// Load spawn entries
if err := zdb.loadSpawnEntries(zoneData); err != nil {
return nil, fmt.Errorf("failed to load spawn entries: %v", err)
}
// Load NPCs
if err := zdb.loadNPCs(zoneData); err != nil {
return nil, fmt.Errorf("failed to load NPCs: %v", err)
}
// Load objects
if err := zdb.loadObjects(zoneData); err != nil {
return nil, fmt.Errorf("failed to load objects: %v", err)
}
// Load widgets
if err := zdb.loadWidgets(zoneData); err != nil {
return nil, fmt.Errorf("failed to load widgets: %v", err)
}
// Load signs
if err := zdb.loadSigns(zoneData); err != nil {
return nil, fmt.Errorf("failed to load signs: %v", err)
}
// Load ground spawns
if err := zdb.loadGroundSpawns(zoneData); err != nil {
return nil, fmt.Errorf("failed to load ground spawns: %v", err)
}
// Load transporters
if err := zdb.loadTransporters(zoneData); err != nil {
return nil, fmt.Errorf("failed to load transporters: %v", err)
}
// Load location grids
if err := zdb.loadLocationGrids(zoneData); err != nil {
return nil, fmt.Errorf("failed to load location grids: %v", err)
}
// Load revive points
if err := zdb.loadRevivePoints(zoneData); err != nil {
return nil, fmt.Errorf("failed to load revive points: %v", err)
}
log.Printf("%s Loaded zone data for zone %d", LogPrefixZone, zoneID)
return zoneData, nil
}
// SaveZoneConfiguration saves zone configuration to database
func (zdb *ZoneDatabase) SaveZoneConfiguration(config *ZoneConfiguration) error {
zdb.mutex.Lock()
defer zdb.mutex.Unlock()
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 := 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)
}
return nil
}
// LoadSpawnLocation loads a specific spawn location
func (zdb *ZoneDatabase) LoadSpawnLocation(locationID int32) (*SpawnLocation, error) {
zdb.mutex.RLock()
defer zdb.mutex.RUnlock()
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 := 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
}
// SaveSpawnLocation saves a spawn location to database
func (zdb *ZoneDatabase) SaveSpawnLocation(location *SpawnLocation) error {
zdb.mutex.Lock()
defer zdb.mutex.Unlock()
if location.ID == 0 {
// Insert new location
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 := 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
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)
}
}
return nil
}
// DeleteSpawnLocation deletes a spawn location from database
func (zdb *ZoneDatabase) DeleteSpawnLocation(locationID int32) error {
zdb.mutex.Lock()
defer zdb.mutex.Unlock()
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)
}
return nil
}
// LoadSpawnGroups loads spawn group associations for a zone
func (zdb *ZoneDatabase) LoadSpawnGroups(zoneID int32) (map[int32][]int32, error) {
zdb.mutex.RLock()
defer zdb.mutex.RUnlock()
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
},
})
if err != nil {
return nil, fmt.Errorf("failed to load spawn groups: %v", err)
}
return groups, nil
}
// SaveSpawnGroup saves spawn group associations
func (zdb *ZoneDatabase) SaveSpawnGroup(groupID int32, locationIDs []int32) error {
zdb.mutex.Lock()
defer zdb.mutex.Unlock()
// Use transaction for atomic operations
endFn, err := sqlitex.ImmediateTransaction(zdb.conn)
if err != nil {
return fmt.Errorf("failed to start transaction: %v", err)
}
defer endFn(&err)
// Delete existing associations
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
insertQuery := `INSERT INTO spawn_location_group (group_id, location_id) VALUES (?, ?)`
for _, locationID := range locationIDs {
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)
}
}
return nil
}
// Close closes the database connection
func (zdb *ZoneDatabase) Close() error {
return nil // Connection is managed externally
}
// Private helper methods
func (zdb *ZoneDatabase) loadZoneConfiguration(zoneData *ZoneData) error {
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{}
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 {
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
},
})
if err != nil {
return fmt.Errorf("failed to load spawn locations: %v", err)
}
zoneData.SpawnLocations = locations
return nil
}
func (zdb *ZoneDatabase) loadSpawnEntries(zoneData *ZoneData) error {
// Placeholder implementation - initialize empty map
zoneData.SpawnEntries = make(map[int32]*SpawnEntry)
return nil
}
func (zdb *ZoneDatabase) loadNPCs(zoneData *ZoneData) error {
// Placeholder implementation - initialize empty map
zoneData.NPCs = make(map[int32]*NPCTemplate)
return nil
}
func (zdb *ZoneDatabase) loadObjects(zoneData *ZoneData) error {
// Placeholder implementation - initialize empty map
zoneData.Objects = make(map[int32]*ObjectTemplate)
return nil
}
func (zdb *ZoneDatabase) loadWidgets(zoneData *ZoneData) error {
// Placeholder implementation - initialize empty map
zoneData.Widgets = make(map[int32]*WidgetTemplate)
return nil
}
func (zdb *ZoneDatabase) loadSigns(zoneData *ZoneData) error {
// Placeholder implementation - initialize empty map
zoneData.Signs = make(map[int32]*SignTemplate)
return nil
}
func (zdb *ZoneDatabase) loadGroundSpawns(zoneData *ZoneData) error {
// Placeholder implementation - initialize empty map
zoneData.GroundSpawns = make(map[int32]*GroundSpawnTemplate)
return nil
}
func (zdb *ZoneDatabase) loadTransporters(zoneData *ZoneData) error {
// Placeholder implementation - initialize empty map
zoneData.Transporters = make(map[int32]*TransportDestination)
return nil
}
func (zdb *ZoneDatabase) loadLocationGrids(zoneData *ZoneData) error {
// Placeholder implementation - initialize empty map
zoneData.LocationGrids = make(map[int32]*LocationGrid)
return nil
}
func (zdb *ZoneDatabase) loadRevivePoints(zoneData *ZoneData) error {
// Placeholder implementation - initialize empty map
zoneData.RevivePoints = make(map[int32]*RevivePoint)
return nil
}
// ZoneData represents all data loaded for a zone
type ZoneData struct {
ZoneID int32
Configuration *ZoneConfiguration
SpawnLocations map[int32]*SpawnLocation
SpawnEntries map[int32]*SpawnEntry
NPCs map[int32]*NPCTemplate
Objects map[int32]*ObjectTemplate
Widgets map[int32]*WidgetTemplate
Signs map[int32]*SignTemplate
GroundSpawns map[int32]*GroundSpawnTemplate
Transporters map[int32]*TransportDestination
LocationGrids map[int32]*LocationGrid
RevivePoints map[int32]*RevivePoint
SpawnGroups map[int32][]int32
}
// ZoneConfiguration represents zone configuration from database
type ZoneConfiguration struct {
ZoneID int32
Name string
File string
Description string
SafeX float32
SafeY float32
SafeZ float32
SafeHeading float32
Underworld float32
MinLevel int16
MaxLevel int16
MinStatus int16
MinVersion int16
InstanceType int16
MaxPlayers int32
DefaultLockoutTime int32
DefaultReenterTime int32
DefaultResetTime int32
GroupZoneOption int8
ExpansionFlag int32
HolidayFlag int32
CanBind bool
CanGate bool
CanEvac bool
CityZone bool
AlwaysLoaded bool
WeatherAllowed bool
}
// Template types for database-loaded spawn data
type NPCTemplate struct {
ID int32
SpawnEntryID int32
Name string
Level int16
EncounterLevel int16
Model string
Size float32
HP int32
Power int32
Heroic int8
Gender int8
Race int16
AdventureClass int16
TradeskillClass int16
AttackType int8
MinLevel int16
MaxLevel int16
EncounterType int8
ShowName int8
Targetable int8
ShowLevel int8
LootTier int8
MinGold int32
MaxGold int32
AggroRadius float32
CastPercentage int8
Randomize bool
}
type ObjectTemplate struct {
ID int32
SpawnEntryID int32
Name string
Model string
Size float32
DeviceID int32
Icon int32
SoundName string
}
type WidgetTemplate struct {
ID int32
SpawnEntryID int32
Name string
Model string
WidgetType int8
OpenType int8
OpenTime int32
CloseTime int32
OpenSound string
CloseSound string
OpenGraphic string
CloseGraphic string
LinkedSpawnID int32
ActionSpawnID int32
HouseID int32
IncludeLocation bool
IncludeHeading bool
}
type SignTemplate struct {
ID int32
SpawnEntryID int32
Name string
Model string
SignType int8
ZoneIDDestination int32
WidgetID int32
Title string
Description string
ZoneX float32
ZoneY float32
ZoneZ float32
ZoneHeading float32
IncludeLocation bool
IncludeHeading bool
}
type GroundSpawnTemplate struct {
ID int32
SpawnEntryID int32
Name string
Model string
HarvestType string
NumberHarvests int8
MaxNumberHarvests int8
CollectionSkill string
RespawnTimer int32
}
// Types are already defined in interfaces.go
// All types are defined in interfaces.go