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