eq2go/internal/zone/database.go

778 lines
21 KiB
Go

package zone
import (
"database/sql"
"fmt"
"log"
"sync"
)
// ZoneDatabase handles all database operations for zones
type ZoneDatabase struct {
db *sql.DB
queries map[string]*sql.Stmt
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),
}
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
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()
stmt := zdb.queries["updateZoneConfig"]
if stmt == nil {
return fmt.Errorf("update zone config statement not prepared")
}
_, 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,
)
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()
stmt := zdb.queries["selectSpawnLocation"]
if stmt == nil {
return nil, fmt.Errorf("select spawn location statement not prepared")
}
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,
)
if err != nil {
return nil, fmt.Errorf("failed to load spawn location %d: %v", locationID, err)
}
return location, nil
}
// SaveSpawnLocation saves a spawn location to database
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")
}
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)
} else {
// Update existing location
stmt = zdb.queries["updateSpawnLocation"]
if stmt == nil {
return fmt.Errorf("update spawn location statement not prepared")
}
_, 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
}
// DeleteSpawnLocation deletes a spawn location from database
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)
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()
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()
groups := make(map[int32][]int32)
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)
}
return groups, nil
}
// SaveSpawnGroup saves spawn group associations
func (zdb *ZoneDatabase) SaveSpawnGroup(groupID int32, locationIDs []int32) error {
zdb.mutex.Lock()
defer zdb.mutex.Unlock()
// Start transaction
tx, err := zdb.db.Begin()
if err != nil {
return fmt.Errorf("failed to begin transaction: %v", err)
}
defer tx.Rollback()
// 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)
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")
}
for _, locationID := range locationIDs {
_, err = tx.Stmt(insertStmt).Exec(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
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
}
// 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")
}
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,
)
if err != nil {
return fmt.Errorf("failed to load zone configuration: %v", err)
}
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()
locations := make(map[int32]*SpawnLocation)
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)
}
zoneData.SpawnLocations = locations
return nil
}
func (zdb *ZoneDatabase) loadSpawnEntries(zoneData *ZoneData) error {
// Similar implementation for spawn entries
zoneData.SpawnEntries = make(map[int32]*SpawnEntry)
return nil // TODO: Implement
}
func (zdb *ZoneDatabase) loadNPCs(zoneData *ZoneData) error {
// Similar implementation for NPCs
zoneData.NPCs = make(map[int32]*NPCTemplate)
return nil // TODO: Implement
}
func (zdb *ZoneDatabase) loadObjects(zoneData *ZoneData) error {
// Similar implementation for objects
zoneData.Objects = make(map[int32]*ObjectTemplate)
return nil // TODO: Implement
}
func (zdb *ZoneDatabase) loadWidgets(zoneData *ZoneData) error {
// Similar implementation for widgets
zoneData.Widgets = make(map[int32]*WidgetTemplate)
return nil // TODO: Implement
}
func (zdb *ZoneDatabase) loadSigns(zoneData *ZoneData) error {
// Similar implementation for signs
zoneData.Signs = make(map[int32]*SignTemplate)
return nil // TODO: Implement
}
func (zdb *ZoneDatabase) loadGroundSpawns(zoneData *ZoneData) error {
// Similar implementation for ground spawns
zoneData.GroundSpawns = make(map[int32]*GroundSpawnTemplate)
return nil // TODO: Implement
}
func (zdb *ZoneDatabase) loadTransporters(zoneData *ZoneData) error {
// Similar implementation for transporters
zoneData.Transporters = make(map[int32]*TransportDestination)
return nil // TODO: Implement
}
func (zdb *ZoneDatabase) loadLocationGrids(zoneData *ZoneData) error {
// Similar implementation for location grids
zoneData.LocationGrids = make(map[int32]*LocationGrid)
return nil // TODO: Implement
}
func (zdb *ZoneDatabase) loadRevivePoints(zoneData *ZoneData) error {
// Similar implementation for revive points
zoneData.RevivePoints = make(map[int32]*RevivePoint)
return nil // TODO: Implement
}
// 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
}