package housing import ( "context" "fmt" "time" "zombiezen.com/go/sqlite" "zombiezen.com/go/sqlite/sqlitex" ) // DatabaseHousingManager implements HousingDatabase interface using zombiezen.com/go/sqlite type DatabaseHousingManager struct { pool *sqlitex.Pool } // NewDatabaseHousingManager creates a new database housing manager func NewDatabaseHousingManager(pool *sqlitex.Pool) *DatabaseHousingManager { return &DatabaseHousingManager{ pool: pool, } } // LoadHouseZones retrieves all available house types from database func (dhm *DatabaseHousingManager) LoadHouseZones(ctx context.Context) ([]HouseZoneData, error) { conn, err := dhm.pool.Take(ctx) if err != nil { return nil, fmt.Errorf("failed to get database connection: %w", err) } defer dhm.pool.Put(conn) query := `SELECT id, name, zone_id, cost_coin, cost_status, upkeep_coin, upkeep_status, alignment, guild_level, vault_slots, max_items, max_visitors, upkeep_period, description FROM houses ORDER BY id` var zones []HouseZoneData err = sqlitex.Execute(conn, query, &sqlitex.ExecOptions{ ResultFunc: func(stmt *sqlite.Stmt) error { zone := HouseZoneData{ ID: int32(stmt.ColumnInt64(0)), Name: stmt.ColumnText(1), ZoneID: int32(stmt.ColumnInt64(2)), CostCoin: stmt.ColumnInt64(3), CostStatus: stmt.ColumnInt64(4), UpkeepCoin: stmt.ColumnInt64(5), UpkeepStatus: stmt.ColumnInt64(6), Alignment: int8(stmt.ColumnInt64(7)), GuildLevel: int8(stmt.ColumnInt64(8)), VaultSlots: int(stmt.ColumnInt64(9)), MaxItems: int(stmt.ColumnInt64(10)), MaxVisitors: int(stmt.ColumnInt64(11)), UpkeepPeriod: int32(stmt.ColumnInt64(12)), } // Handle nullable description field if stmt.ColumnType(13) != sqlite.TypeNull { zone.Description = stmt.ColumnText(13) } zones = append(zones, zone) return nil }, }) if err != nil { return nil, fmt.Errorf("failed to query house zones: %w", err) } return zones, nil } // LoadHouseZone retrieves a specific house zone from database func (dhm *DatabaseHousingManager) LoadHouseZone(ctx context.Context, houseID int32) (*HouseZoneData, error) { conn, err := dhm.pool.Take(ctx) if err != nil { return nil, fmt.Errorf("failed to get database connection: %w", err) } defer dhm.pool.Put(conn) query := `SELECT id, name, zone_id, cost_coin, cost_status, upkeep_coin, upkeep_status, alignment, guild_level, vault_slots, max_items, max_visitors, upkeep_period, description FROM houses WHERE id = ?` var zone HouseZoneData found := false err = sqlitex.Execute(conn, query, &sqlitex.ExecOptions{ Args: []any{houseID}, ResultFunc: func(stmt *sqlite.Stmt) error { found = true zone = HouseZoneData{ ID: int32(stmt.ColumnInt64(0)), Name: stmt.ColumnText(1), ZoneID: int32(stmt.ColumnInt64(2)), CostCoin: stmt.ColumnInt64(3), CostStatus: stmt.ColumnInt64(4), UpkeepCoin: stmt.ColumnInt64(5), UpkeepStatus: stmt.ColumnInt64(6), Alignment: int8(stmt.ColumnInt64(7)), GuildLevel: int8(stmt.ColumnInt64(8)), VaultSlots: int(stmt.ColumnInt64(9)), MaxItems: int(stmt.ColumnInt64(10)), MaxVisitors: int(stmt.ColumnInt64(11)), UpkeepPeriod: int32(stmt.ColumnInt64(12)), } // Handle nullable description field if stmt.ColumnType(13) != sqlite.TypeNull { zone.Description = stmt.ColumnText(13) } return nil }, }) if err != nil { return nil, fmt.Errorf("failed to load house zone %d: %w", houseID, err) } if !found { return nil, fmt.Errorf("house zone %d not found", houseID) } return &zone, nil } // SaveHouseZone saves a house zone to database func (dhm *DatabaseHousingManager) SaveHouseZone(ctx context.Context, zone *HouseZone) error { conn, err := dhm.pool.Take(ctx) if err != nil { return fmt.Errorf("failed to get database connection: %w", err) } defer dhm.pool.Put(conn) query := `INSERT OR REPLACE INTO houses (id, name, zone_id, cost_coin, cost_status, upkeep_coin, upkeep_status, alignment, guild_level, vault_slots, max_items, max_visitors, upkeep_period, description) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)` err = sqlitex.Execute(conn, query, &sqlitex.ExecOptions{ Args: []any{ zone.ID, zone.Name, zone.ZoneID, zone.CostCoin, zone.CostStatus, zone.UpkeepCoin, zone.UpkeepStatus, zone.Alignment, zone.GuildLevel, zone.VaultSlots, zone.MaxItems, zone.MaxVisitors, zone.UpkeepPeriod, zone.Description, }, }) if err != nil { return fmt.Errorf("failed to save house zone %d: %w", zone.ID, err) } return nil } // DeleteHouseZone removes a house zone from database func (dhm *DatabaseHousingManager) DeleteHouseZone(ctx context.Context, houseID int32) error { conn, err := dhm.pool.Take(ctx) if err != nil { return fmt.Errorf("failed to get database connection: %w", err) } defer dhm.pool.Put(conn) err = sqlitex.Execute(conn, "DELETE FROM houses WHERE id = ?", &sqlitex.ExecOptions{ Args: []any{houseID}, }) if err != nil { return fmt.Errorf("failed to delete house zone %d: %w", houseID, err) } return nil } // LoadPlayerHouses retrieves all houses owned by a character func (dhm *DatabaseHousingManager) LoadPlayerHouses(ctx context.Context, characterID int32) ([]PlayerHouseData, error) { conn, err := dhm.pool.Take(ctx) if err != nil { return nil, fmt.Errorf("failed to get database connection: %w", err) } defer dhm.pool.Put(conn) query := `SELECT unique_id, char_id, house_id, instance_id, upkeep_due, escrow_coins, escrow_status, status, house_name, visit_permission, public_note, private_note, allow_friends, allow_guild, require_approval, show_on_directory, allow_decoration, tax_exempt FROM character_houses WHERE char_id = ? ORDER BY unique_id` var houses []PlayerHouseData err = sqlitex.Execute(conn, query, &sqlitex.ExecOptions{ Args: []any{characterID}, ResultFunc: func(stmt *sqlite.Stmt) error { house := PlayerHouseData{ UniqueID: stmt.ColumnInt64(0), CharacterID: int32(stmt.ColumnInt64(1)), HouseID: int32(stmt.ColumnInt64(2)), InstanceID: int32(stmt.ColumnInt64(3)), UpkeepDue: time.Unix(stmt.ColumnInt64(4), 0), EscrowCoins: stmt.ColumnInt64(5), EscrowStatus: stmt.ColumnInt64(6), Status: int8(stmt.ColumnInt64(7)), VisitPermission: int8(stmt.ColumnInt64(9)), AllowFriends: stmt.ColumnInt64(12) != 0, AllowGuild: stmt.ColumnInt64(13) != 0, RequireApproval: stmt.ColumnInt64(14) != 0, ShowOnDirectory: stmt.ColumnInt64(15) != 0, AllowDecoration: stmt.ColumnInt64(16) != 0, TaxExempt: stmt.ColumnInt64(17) != 0, } // Handle nullable fields if stmt.ColumnType(8) != sqlite.TypeNull { house.HouseName = stmt.ColumnText(8) } if stmt.ColumnType(10) != sqlite.TypeNull { house.PublicNote = stmt.ColumnText(10) } if stmt.ColumnType(11) != sqlite.TypeNull { house.PrivateNote = stmt.ColumnText(11) } houses = append(houses, house) return nil }, }) if err != nil { return nil, fmt.Errorf("failed to query player houses for character %d: %w", characterID, err) } return houses, nil } // LoadPlayerHouse retrieves a specific player house func (dhm *DatabaseHousingManager) LoadPlayerHouse(ctx context.Context, uniqueID int64) (*PlayerHouseData, error) { conn, err := dhm.pool.Take(ctx) if err != nil { return nil, fmt.Errorf("failed to get database connection: %w", err) } defer dhm.pool.Put(conn) query := `SELECT unique_id, char_id, house_id, instance_id, upkeep_due, escrow_coins, escrow_status, status, house_name, visit_permission, public_note, private_note, allow_friends, allow_guild, require_approval, show_on_directory, allow_decoration, tax_exempt FROM character_houses WHERE unique_id = ?` var house PlayerHouseData found := false err = sqlitex.Execute(conn, query, &sqlitex.ExecOptions{ Args: []any{uniqueID}, ResultFunc: func(stmt *sqlite.Stmt) error { found = true house = PlayerHouseData{ UniqueID: stmt.ColumnInt64(0), CharacterID: int32(stmt.ColumnInt64(1)), HouseID: int32(stmt.ColumnInt64(2)), InstanceID: int32(stmt.ColumnInt64(3)), UpkeepDue: time.Unix(stmt.ColumnInt64(4), 0), EscrowCoins: stmt.ColumnInt64(5), EscrowStatus: stmt.ColumnInt64(6), Status: int8(stmt.ColumnInt64(7)), VisitPermission: int8(stmt.ColumnInt64(9)), AllowFriends: stmt.ColumnInt64(12) != 0, AllowGuild: stmt.ColumnInt64(13) != 0, RequireApproval: stmt.ColumnInt64(14) != 0, ShowOnDirectory: stmt.ColumnInt64(15) != 0, AllowDecoration: stmt.ColumnInt64(16) != 0, TaxExempt: stmt.ColumnInt64(17) != 0, } // Handle nullable fields if stmt.ColumnType(8) != sqlite.TypeNull { house.HouseName = stmt.ColumnText(8) } if stmt.ColumnType(10) != sqlite.TypeNull { house.PublicNote = stmt.ColumnText(10) } if stmt.ColumnType(11) != sqlite.TypeNull { house.PrivateNote = stmt.ColumnText(11) } return nil }, }) if err != nil { return nil, fmt.Errorf("failed to load player house %d: %w", uniqueID, err) } if !found { return nil, fmt.Errorf("player house %d not found", uniqueID) } return &house, nil } // SavePlayerHouse saves a player house to database func (dhm *DatabaseHousingManager) SavePlayerHouse(ctx context.Context, house *PlayerHouse) error { conn, err := dhm.pool.Take(ctx) if err != nil { return fmt.Errorf("failed to get database connection: %w", err) } defer dhm.pool.Put(conn) query := `INSERT OR REPLACE INTO character_houses (unique_id, char_id, house_id, instance_id, upkeep_due, escrow_coins, escrow_status, status, house_name, visit_permission, public_note, private_note, allow_friends, allow_guild, require_approval, show_on_directory, allow_decoration, tax_exempt) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)` upkeepDueTimestamp := house.UpkeepDue.Unix() err = sqlitex.Execute(conn, query, &sqlitex.ExecOptions{ Args: []any{ house.UniqueID, house.CharacterID, house.HouseID, house.InstanceID, upkeepDueTimestamp, house.EscrowCoins, house.EscrowStatus, house.Status, house.Settings.HouseName, house.Settings.VisitPermission, house.Settings.PublicNote, house.Settings.PrivateNote, boolToInt(house.Settings.AllowFriends), boolToInt(house.Settings.AllowGuild), boolToInt(house.Settings.RequireApproval), boolToInt(house.Settings.ShowOnDirectory), boolToInt(house.Settings.AllowDecoration), boolToInt(house.Settings.TaxExempt), }, }) if err != nil { return fmt.Errorf("failed to save player house %d: %w", house.UniqueID, err) } return nil } // DeletePlayerHouse removes a player house from database func (dhm *DatabaseHousingManager) DeletePlayerHouse(ctx context.Context, uniqueID int64) error { conn, err := dhm.pool.Take(ctx) if err != nil { return fmt.Errorf("failed to get database connection: %w", err) } defer dhm.pool.Put(conn) // Use a savepoint for nested transaction support defer sqlitex.Save(conn)(&err) // Delete related data first tables := []string{ "character_house_deposits", "character_house_history", "character_house_access", "character_house_amenities", "character_house_items", } for _, table := range tables { query := fmt.Sprintf("DELETE FROM %s WHERE house_id = ?", table) err = sqlitex.Execute(conn, query, &sqlitex.ExecOptions{ Args: []any{uniqueID}, }) if err != nil { return fmt.Errorf("failed to delete from %s: %w", table, err) } } // Delete the house itself err = sqlitex.Execute(conn, "DELETE FROM character_houses WHERE unique_id = ?", &sqlitex.ExecOptions{ Args: []any{uniqueID}, }) if err != nil { return fmt.Errorf("failed to delete player house: %w", err) } return nil } // AddPlayerHouse creates a new player house entry func (dhm *DatabaseHousingManager) AddPlayerHouse(ctx context.Context, houseData PlayerHouseData) (int64, error) { conn, err := dhm.pool.Take(ctx) if err != nil { return 0, fmt.Errorf("failed to get database connection: %w", err) } defer dhm.pool.Put(conn) query := `INSERT INTO character_houses (char_id, house_id, instance_id, upkeep_due, escrow_coins, escrow_status, status, house_name, visit_permission, public_note, private_note, allow_friends, allow_guild, require_approval, show_on_directory, allow_decoration, tax_exempt) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)` upkeepDueTimestamp := houseData.UpkeepDue.Unix() err = sqlitex.Execute(conn, query, &sqlitex.ExecOptions{ Args: []any{ houseData.CharacterID, houseData.HouseID, houseData.InstanceID, upkeepDueTimestamp, houseData.EscrowCoins, houseData.EscrowStatus, houseData.Status, houseData.HouseName, houseData.VisitPermission, houseData.PublicNote, houseData.PrivateNote, boolToInt(houseData.AllowFriends), boolToInt(houseData.AllowGuild), boolToInt(houseData.RequireApproval), boolToInt(houseData.ShowOnDirectory), boolToInt(houseData.AllowDecoration), boolToInt(houseData.TaxExempt), }, }) if err != nil { return 0, fmt.Errorf("failed to create player house: %w", err) } // Get the last inserted ID return conn.LastInsertRowID(), nil } // LoadDeposits retrieves deposit history for a house func (dhm *DatabaseHousingManager) LoadDeposits(ctx context.Context, houseID int64) ([]HouseDepositData, error) { conn, err := dhm.pool.Take(ctx) if err != nil { return nil, fmt.Errorf("failed to get database connection: %w", err) } defer dhm.pool.Put(conn) query := `SELECT house_id, timestamp, amount, last_amount, status, last_status, name, character_id FROM character_house_deposits WHERE house_id = ? ORDER BY timestamp DESC LIMIT ?` var deposits []HouseDepositData err = sqlitex.Execute(conn, query, &sqlitex.ExecOptions{ Args: []any{houseID, MaxDepositHistory}, ResultFunc: func(stmt *sqlite.Stmt) error { deposit := HouseDepositData{ HouseID: stmt.ColumnInt64(0), Timestamp: time.Unix(stmt.ColumnInt64(1), 0), Amount: stmt.ColumnInt64(2), LastAmount: stmt.ColumnInt64(3), Status: stmt.ColumnInt64(4), LastStatus: stmt.ColumnInt64(5), Name: stmt.ColumnText(6), CharacterID: int32(stmt.ColumnInt64(7)), } deposits = append(deposits, deposit) return nil }, }) if err != nil { return nil, fmt.Errorf("failed to query house deposits for house %d: %w", houseID, err) } return deposits, nil } // SaveDeposit saves a deposit record func (dhm *DatabaseHousingManager) SaveDeposit(ctx context.Context, houseID int64, deposit HouseDeposit) error { conn, err := dhm.pool.Take(ctx) if err != nil { return fmt.Errorf("failed to get database connection: %w", err) } defer dhm.pool.Put(conn) query := `INSERT INTO character_house_deposits (house_id, timestamp, amount, last_amount, status, last_status, name, character_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?)` timestampUnix := deposit.Timestamp.Unix() err = sqlitex.Execute(conn, query, &sqlitex.ExecOptions{ Args: []any{ houseID, timestampUnix, deposit.Amount, deposit.LastAmount, deposit.Status, deposit.LastStatus, deposit.Name, deposit.CharacterID, }, }) if err != nil { return fmt.Errorf("failed to save house deposit: %w", err) } return nil } // LoadHistory retrieves transaction history for a house func (dhm *DatabaseHousingManager) LoadHistory(ctx context.Context, houseID int64) ([]HouseHistoryData, error) { conn, err := dhm.pool.Take(ctx) if err != nil { return nil, fmt.Errorf("failed to get database connection: %w", err) } defer dhm.pool.Put(conn) query := `SELECT house_id, timestamp, amount, status, reason, name, character_id, pos_flag, type FROM character_house_history WHERE house_id = ? ORDER BY timestamp DESC LIMIT ?` var history []HouseHistoryData err = sqlitex.Execute(conn, query, &sqlitex.ExecOptions{ Args: []any{houseID, MaxTransactionHistory}, ResultFunc: func(stmt *sqlite.Stmt) error { entry := HouseHistoryData{ HouseID: stmt.ColumnInt64(0), Timestamp: time.Unix(stmt.ColumnInt64(1), 0), Amount: stmt.ColumnInt64(2), Status: stmt.ColumnInt64(3), Reason: stmt.ColumnText(4), Name: stmt.ColumnText(5), CharacterID: int32(stmt.ColumnInt64(6)), PosFlag: int8(stmt.ColumnInt64(7)), Type: int(stmt.ColumnInt64(8)), } history = append(history, entry) return nil }, }) if err != nil { return nil, fmt.Errorf("failed to query house history for house %d: %w", houseID, err) } return history, nil } // AddHistory adds a new history entry func (dhm *DatabaseHousingManager) AddHistory(ctx context.Context, houseID int64, history HouseHistory) error { conn, err := dhm.pool.Take(ctx) if err != nil { return fmt.Errorf("failed to get database connection: %w", err) } defer dhm.pool.Put(conn) query := `INSERT INTO character_house_history (house_id, timestamp, amount, status, reason, name, character_id, pos_flag, type) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)` timestampUnix := history.Timestamp.Unix() err = sqlitex.Execute(conn, query, &sqlitex.ExecOptions{ Args: []any{ houseID, timestampUnix, history.Amount, history.Status, history.Reason, history.Name, history.CharacterID, history.PosFlag, history.Type, }, }) if err != nil { return fmt.Errorf("failed to add house history: %w", err) } return nil } // LoadHouseAccess retrieves access permissions for a house func (dhm *DatabaseHousingManager) LoadHouseAccess(ctx context.Context, houseID int64) ([]HouseAccessData, error) { conn, err := dhm.pool.Take(ctx) if err != nil { return nil, fmt.Errorf("failed to get database connection: %w", err) } defer dhm.pool.Put(conn) query := `SELECT house_id, character_id, player_name, access_level, permissions, granted_by, granted_date, expires_date, notes FROM character_house_access WHERE house_id = ?` var accessList []HouseAccessData err = sqlitex.Execute(conn, query, &sqlitex.ExecOptions{ Args: []any{houseID}, ResultFunc: func(stmt *sqlite.Stmt) error { access := HouseAccessData{ HouseID: stmt.ColumnInt64(0), CharacterID: int32(stmt.ColumnInt64(1)), PlayerName: stmt.ColumnText(2), AccessLevel: int8(stmt.ColumnInt64(3)), Permissions: int32(stmt.ColumnInt64(4)), GrantedBy: int32(stmt.ColumnInt64(5)), GrantedDate: time.Unix(stmt.ColumnInt64(6), 0), ExpiresDate: time.Unix(stmt.ColumnInt64(7), 0), } if stmt.ColumnType(8) != sqlite.TypeNull { access.Notes = stmt.ColumnText(8) } accessList = append(accessList, access) return nil }, }) if err != nil { return nil, fmt.Errorf("failed to query house access for house %d: %w", houseID, err) } return accessList, nil } // SaveHouseAccess saves access permissions for a house func (dhm *DatabaseHousingManager) SaveHouseAccess(ctx context.Context, houseID int64, accessList []HouseAccess) error { conn, err := dhm.pool.Take(ctx) if err != nil { return fmt.Errorf("failed to get database connection: %w", err) } defer dhm.pool.Put(conn) // Use a savepoint for nested transaction support defer sqlitex.Save(conn)(&err) // Delete existing access for this house err = sqlitex.Execute(conn, "DELETE FROM character_house_access WHERE house_id = ?", &sqlitex.ExecOptions{ Args: []any{houseID}, }) if err != nil { return fmt.Errorf("failed to delete existing house access: %w", err) } // Insert all access entries insertQuery := `INSERT INTO character_house_access (house_id, character_id, player_name, access_level, permissions, granted_by, granted_date, expires_date, notes) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)` for _, access := range accessList { grantedDateUnix := access.GrantedDate.Unix() expiresDateUnix := access.ExpiresDate.Unix() err = sqlitex.Execute(conn, insertQuery, &sqlitex.ExecOptions{ Args: []any{ houseID, access.CharacterID, access.PlayerName, access.AccessLevel, access.Permissions, access.GrantedBy, grantedDateUnix, expiresDateUnix, access.Notes, }, }) if err != nil { return fmt.Errorf("failed to insert house access for character %d: %w", access.CharacterID, err) } } return nil } // DeleteHouseAccess removes access for a specific character func (dhm *DatabaseHousingManager) DeleteHouseAccess(ctx context.Context, houseID int64, characterID int32) error { conn, err := dhm.pool.Take(ctx) if err != nil { return fmt.Errorf("failed to get database connection: %w", err) } defer dhm.pool.Put(conn) err = sqlitex.Execute(conn, "DELETE FROM character_house_access WHERE house_id = ? AND character_id = ?", &sqlitex.ExecOptions{ Args: []any{houseID, characterID}, }) if err != nil { return fmt.Errorf("failed to delete house access: %w", err) } return nil } // LoadHouseAmenities retrieves amenities for a house func (dhm *DatabaseHousingManager) LoadHouseAmenities(ctx context.Context, houseID int64) ([]HouseAmenityData, error) { conn, err := dhm.pool.Take(ctx) if err != nil { return nil, fmt.Errorf("failed to get database connection: %w", err) } defer dhm.pool.Put(conn) query := `SELECT house_id, id, type, name, cost, status_cost, purchase_date, x, y, z, heading, is_active FROM character_house_amenities WHERE house_id = ?` var amenities []HouseAmenityData err = sqlitex.Execute(conn, query, &sqlitex.ExecOptions{ Args: []any{houseID}, ResultFunc: func(stmt *sqlite.Stmt) error { amenity := HouseAmenityData{ HouseID: stmt.ColumnInt64(0), ID: int32(stmt.ColumnInt64(1)), Type: int(stmt.ColumnInt64(2)), Name: stmt.ColumnText(3), Cost: stmt.ColumnInt64(4), StatusCost: stmt.ColumnInt64(5), PurchaseDate: time.Unix(stmt.ColumnInt64(6), 0), X: float32(stmt.ColumnFloat(7)), Y: float32(stmt.ColumnFloat(8)), Z: float32(stmt.ColumnFloat(9)), Heading: float32(stmt.ColumnFloat(10)), IsActive: stmt.ColumnInt64(11) != 0, } amenities = append(amenities, amenity) return nil }, }) if err != nil { return nil, fmt.Errorf("failed to query house amenities for house %d: %w", houseID, err) } return amenities, nil } // SaveHouseAmenity saves a house amenity func (dhm *DatabaseHousingManager) SaveHouseAmenity(ctx context.Context, houseID int64, amenity HouseAmenity) error { conn, err := dhm.pool.Take(ctx) if err != nil { return fmt.Errorf("failed to get database connection: %w", err) } defer dhm.pool.Put(conn) query := `INSERT OR REPLACE INTO character_house_amenities (house_id, id, type, name, cost, status_cost, purchase_date, x, y, z, heading, is_active) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)` purchaseDateUnix := amenity.PurchaseDate.Unix() err = sqlitex.Execute(conn, query, &sqlitex.ExecOptions{ Args: []any{ houseID, amenity.ID, amenity.Type, amenity.Name, amenity.Cost, amenity.StatusCost, purchaseDateUnix, amenity.X, amenity.Y, amenity.Z, amenity.Heading, boolToInt(amenity.IsActive), }, }) if err != nil { return fmt.Errorf("failed to save house amenity: %w", err) } return nil } // DeleteHouseAmenity removes a house amenity func (dhm *DatabaseHousingManager) DeleteHouseAmenity(ctx context.Context, houseID int64, amenityID int32) error { conn, err := dhm.pool.Take(ctx) if err != nil { return fmt.Errorf("failed to get database connection: %w", err) } defer dhm.pool.Put(conn) err = sqlitex.Execute(conn, "DELETE FROM character_house_amenities WHERE house_id = ? AND id = ?", &sqlitex.ExecOptions{ Args: []any{houseID, amenityID}, }) if err != nil { return fmt.Errorf("failed to delete house amenity: %w", err) } return nil } // LoadHouseItems retrieves items placed in a house func (dhm *DatabaseHousingManager) LoadHouseItems(ctx context.Context, houseID int64) ([]HouseItemData, error) { conn, err := dhm.pool.Take(ctx) if err != nil { return nil, fmt.Errorf("failed to get database connection: %w", err) } defer dhm.pool.Put(conn) query := `SELECT house_id, id, item_id, character_id, x, y, z, heading, pitch_x, pitch_y, roll_x, roll_y, placed_date, quantity, condition, house FROM character_house_items WHERE house_id = ?` var items []HouseItemData err = sqlitex.Execute(conn, query, &sqlitex.ExecOptions{ Args: []any{houseID}, ResultFunc: func(stmt *sqlite.Stmt) error { item := HouseItemData{ HouseID: stmt.ColumnInt64(0), ID: stmt.ColumnInt64(1), ItemID: int32(stmt.ColumnInt64(2)), CharacterID: int32(stmt.ColumnInt64(3)), X: float32(stmt.ColumnFloat(4)), Y: float32(stmt.ColumnFloat(5)), Z: float32(stmt.ColumnFloat(6)), Heading: float32(stmt.ColumnFloat(7)), PitchX: float32(stmt.ColumnFloat(8)), PitchY: float32(stmt.ColumnFloat(9)), RollX: float32(stmt.ColumnFloat(10)), RollY: float32(stmt.ColumnFloat(11)), PlacedDate: time.Unix(stmt.ColumnInt64(12), 0), Quantity: int32(stmt.ColumnInt64(13)), Condition: int8(stmt.ColumnInt64(14)), House: stmt.ColumnText(15), } items = append(items, item) return nil }, }) if err != nil { return nil, fmt.Errorf("failed to query house items for house %d: %w", houseID, err) } return items, nil } // SaveHouseItem saves a house item func (dhm *DatabaseHousingManager) SaveHouseItem(ctx context.Context, houseID int64, item HouseItem) error { conn, err := dhm.pool.Take(ctx) if err != nil { return fmt.Errorf("failed to get database connection: %w", err) } defer dhm.pool.Put(conn) query := `INSERT OR REPLACE INTO character_house_items (house_id, id, item_id, character_id, x, y, z, heading, pitch_x, pitch_y, roll_x, roll_y, placed_date, quantity, condition, house) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)` placedDateUnix := item.PlacedDate.Unix() err = sqlitex.Execute(conn, query, &sqlitex.ExecOptions{ Args: []any{ houseID, item.ID, item.ItemID, item.CharacterID, item.X, item.Y, item.Z, item.Heading, item.PitchX, item.PitchY, item.RollX, item.RollY, placedDateUnix, item.Quantity, item.Condition, item.House, }, }) if err != nil { return fmt.Errorf("failed to save house item: %w", err) } return nil } // DeleteHouseItem removes a house item func (dhm *DatabaseHousingManager) DeleteHouseItem(ctx context.Context, houseID int64, itemID int64) error { conn, err := dhm.pool.Take(ctx) if err != nil { return fmt.Errorf("failed to get database connection: %w", err) } defer dhm.pool.Put(conn) err = sqlitex.Execute(conn, "DELETE FROM character_house_items WHERE house_id = ? AND id = ?", &sqlitex.ExecOptions{ Args: []any{houseID, itemID}, }) if err != nil { return fmt.Errorf("failed to delete house item: %w", err) } return nil } // GetNextHouseID returns the next available house unique ID func (dhm *DatabaseHousingManager) GetNextHouseID(ctx context.Context) (int64, error) { conn, err := dhm.pool.Take(ctx) if err != nil { return 0, fmt.Errorf("failed to get database connection: %w", err) } defer dhm.pool.Put(conn) query := "SELECT COALESCE(MAX(unique_id), 0) + 1 FROM character_houses" var nextID int64 err = sqlitex.Execute(conn, query, &sqlitex.ExecOptions{ ResultFunc: func(stmt *sqlite.Stmt) error { nextID = stmt.ColumnInt64(0) return nil }, }) if err != nil { return 0, fmt.Errorf("failed to get next house ID: %w", err) } return nextID, nil } // GetHouseByInstance finds a house by instance ID func (dhm *DatabaseHousingManager) GetHouseByInstance(ctx context.Context, instanceID int32) (*PlayerHouseData, error) { conn, err := dhm.pool.Take(ctx) if err != nil { return nil, fmt.Errorf("failed to get database connection: %w", err) } defer dhm.pool.Put(conn) query := `SELECT unique_id, char_id, house_id, instance_id, upkeep_due, escrow_coins, escrow_status, status, house_name, visit_permission, public_note, private_note, allow_friends, allow_guild, require_approval, show_on_directory, allow_decoration, tax_exempt FROM character_houses WHERE instance_id = ?` var house PlayerHouseData found := false err = sqlitex.Execute(conn, query, &sqlitex.ExecOptions{ Args: []any{instanceID}, ResultFunc: func(stmt *sqlite.Stmt) error { found = true house = PlayerHouseData{ UniqueID: stmt.ColumnInt64(0), CharacterID: int32(stmt.ColumnInt64(1)), HouseID: int32(stmt.ColumnInt64(2)), InstanceID: int32(stmt.ColumnInt64(3)), UpkeepDue: time.Unix(stmt.ColumnInt64(4), 0), EscrowCoins: stmt.ColumnInt64(5), EscrowStatus: stmt.ColumnInt64(6), Status: int8(stmt.ColumnInt64(7)), VisitPermission: int8(stmt.ColumnInt64(9)), AllowFriends: stmt.ColumnInt64(12) != 0, AllowGuild: stmt.ColumnInt64(13) != 0, RequireApproval: stmt.ColumnInt64(14) != 0, ShowOnDirectory: stmt.ColumnInt64(15) != 0, AllowDecoration: stmt.ColumnInt64(16) != 0, TaxExempt: stmt.ColumnInt64(17) != 0, } // Handle nullable fields if stmt.ColumnType(8) != sqlite.TypeNull { house.HouseName = stmt.ColumnText(8) } if stmt.ColumnType(10) != sqlite.TypeNull { house.PublicNote = stmt.ColumnText(10) } if stmt.ColumnType(11) != sqlite.TypeNull { house.PrivateNote = stmt.ColumnText(11) } return nil }, }) if err != nil { return nil, fmt.Errorf("failed to load house by instance %d: %w", instanceID, err) } if !found { return nil, fmt.Errorf("house with instance %d not found", instanceID) } return &house, nil } // UpdateHouseUpkeepDue updates the upkeep due date for a house func (dhm *DatabaseHousingManager) UpdateHouseUpkeepDue(ctx context.Context, houseID int64, upkeepDue time.Time) error { conn, err := dhm.pool.Take(ctx) if err != nil { return fmt.Errorf("failed to get database connection: %w", err) } defer dhm.pool.Put(conn) query := "UPDATE character_houses SET upkeep_due = ? WHERE unique_id = ?" upkeepDueTimestamp := upkeepDue.Unix() err = sqlitex.Execute(conn, query, &sqlitex.ExecOptions{ Args: []any{upkeepDueTimestamp, houseID}, }) if err != nil { return fmt.Errorf("failed to update house upkeep due: %w", err) } return nil } // UpdateHouseEscrow updates the escrow balances for a house func (dhm *DatabaseHousingManager) UpdateHouseEscrow(ctx context.Context, houseID int64, coins, status int64) error { conn, err := dhm.pool.Take(ctx) if err != nil { return fmt.Errorf("failed to get database connection: %w", err) } defer dhm.pool.Put(conn) query := "UPDATE character_houses SET escrow_coins = ?, escrow_status = ? WHERE unique_id = ?" err = sqlitex.Execute(conn, query, &sqlitex.ExecOptions{ Args: []any{coins, status, houseID}, }) if err != nil { return fmt.Errorf("failed to update house escrow: %w", err) } return nil } // GetHousesForUpkeep returns houses that need upkeep processing func (dhm *DatabaseHousingManager) GetHousesForUpkeep(ctx context.Context, cutoffTime time.Time) ([]PlayerHouseData, error) { conn, err := dhm.pool.Take(ctx) if err != nil { return nil, fmt.Errorf("failed to get database connection: %w", err) } defer dhm.pool.Put(conn) query := `SELECT unique_id, char_id, house_id, instance_id, upkeep_due, escrow_coins, escrow_status, status, house_name, visit_permission, public_note, private_note, allow_friends, allow_guild, require_approval, show_on_directory, allow_decoration, tax_exempt FROM character_houses WHERE upkeep_due <= ? AND status = ?` cutoffTimestamp := cutoffTime.Unix() var houses []PlayerHouseData err = sqlitex.Execute(conn, query, &sqlitex.ExecOptions{ Args: []any{cutoffTimestamp, HouseStatusActive}, ResultFunc: func(stmt *sqlite.Stmt) error { house := PlayerHouseData{ UniqueID: stmt.ColumnInt64(0), CharacterID: int32(stmt.ColumnInt64(1)), HouseID: int32(stmt.ColumnInt64(2)), InstanceID: int32(stmt.ColumnInt64(3)), UpkeepDue: time.Unix(stmt.ColumnInt64(4), 0), EscrowCoins: stmt.ColumnInt64(5), EscrowStatus: stmt.ColumnInt64(6), Status: int8(stmt.ColumnInt64(7)), VisitPermission: int8(stmt.ColumnInt64(9)), AllowFriends: stmt.ColumnInt64(12) != 0, AllowGuild: stmt.ColumnInt64(13) != 0, RequireApproval: stmt.ColumnInt64(14) != 0, ShowOnDirectory: stmt.ColumnInt64(15) != 0, AllowDecoration: stmt.ColumnInt64(16) != 0, TaxExempt: stmt.ColumnInt64(17) != 0, } // Handle nullable fields if stmt.ColumnType(8) != sqlite.TypeNull { house.HouseName = stmt.ColumnText(8) } if stmt.ColumnType(10) != sqlite.TypeNull { house.PublicNote = stmt.ColumnText(10) } if stmt.ColumnType(11) != sqlite.TypeNull { house.PrivateNote = stmt.ColumnText(11) } houses = append(houses, house) return nil }, }) if err != nil { return nil, fmt.Errorf("failed to query houses for upkeep: %w", err) } return houses, nil } // GetHouseStatistics returns housing system statistics func (dhm *DatabaseHousingManager) GetHouseStatistics(ctx context.Context) (*HousingStatistics, error) { conn, err := dhm.pool.Take(ctx) if err != nil { return nil, fmt.Errorf("failed to get database connection: %w", err) } defer dhm.pool.Put(conn) stats := &HousingStatistics{ HousesByType: make(map[int32]int64), HousesByAlignment: make(map[int8]int64), RevenueByType: make(map[int]int64), TopDepositors: make([]PlayerDeposits, 0), } // Get basic counts queries := []struct { query string target *int64 }{ {"SELECT COUNT(*) FROM character_houses", &stats.TotalHouses}, {"SELECT COUNT(*) FROM character_houses WHERE status = 0", &stats.ActiveHouses}, {"SELECT COUNT(*) FROM character_houses WHERE status = 2", &stats.ForelosedHouses}, {"SELECT COUNT(*) FROM character_house_deposits", &stats.TotalDeposits}, {"SELECT COUNT(*) FROM character_house_history WHERE pos_flag = 0", &stats.TotalWithdrawals}, } for _, q := range queries { err = sqlitex.Execute(conn, q.query, &sqlitex.ExecOptions{ ResultFunc: func(stmt *sqlite.Stmt) error { *q.target = stmt.ColumnInt64(0) return nil }, }) if err != nil { return nil, fmt.Errorf("failed to get statistics: %w", err) } } return stats, nil } // EnsureHousingTables creates the housing tables if they don't exist func (dhm *DatabaseHousingManager) EnsureHousingTables(ctx context.Context) error { conn, err := dhm.pool.Take(ctx) if err != nil { return fmt.Errorf("failed to get database connection: %w", err) } defer dhm.pool.Put(conn) queries := []string{ `CREATE TABLE IF NOT EXISTS houses ( id INTEGER PRIMARY KEY, name TEXT NOT NULL, zone_id INTEGER NOT NULL, cost_coin INTEGER NOT NULL DEFAULT 0, cost_status INTEGER NOT NULL DEFAULT 0, upkeep_coin INTEGER NOT NULL DEFAULT 0, upkeep_status INTEGER NOT NULL DEFAULT 0, alignment INTEGER NOT NULL DEFAULT 0, guild_level INTEGER NOT NULL DEFAULT 0, vault_slots INTEGER NOT NULL DEFAULT 4, max_items INTEGER NOT NULL DEFAULT 100, max_visitors INTEGER NOT NULL DEFAULT 50, upkeep_period INTEGER NOT NULL DEFAULT 604800, description TEXT DEFAULT '', created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP )`, `CREATE TABLE IF NOT EXISTS character_houses ( unique_id INTEGER PRIMARY KEY AUTOINCREMENT, char_id INTEGER NOT NULL, house_id INTEGER NOT NULL, instance_id INTEGER NOT NULL, upkeep_due INTEGER NOT NULL, escrow_coins INTEGER NOT NULL DEFAULT 0, escrow_status INTEGER NOT NULL DEFAULT 0, status INTEGER NOT NULL DEFAULT 0, house_name TEXT DEFAULT '', visit_permission INTEGER NOT NULL DEFAULT 0, public_note TEXT DEFAULT '', private_note TEXT DEFAULT '', allow_friends INTEGER NOT NULL DEFAULT 1, allow_guild INTEGER NOT NULL DEFAULT 0, require_approval INTEGER NOT NULL DEFAULT 0, show_on_directory INTEGER NOT NULL DEFAULT 1, allow_decoration INTEGER NOT NULL DEFAULT 0, tax_exempt INTEGER NOT NULL DEFAULT 0, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (house_id) REFERENCES houses(id) )`, `CREATE TABLE IF NOT EXISTS character_house_deposits ( house_id INTEGER NOT NULL, timestamp INTEGER NOT NULL, amount INTEGER NOT NULL, last_amount INTEGER NOT NULL, status INTEGER NOT NULL, last_status INTEGER NOT NULL, name TEXT NOT NULL, character_id INTEGER NOT NULL, FOREIGN KEY (house_id) REFERENCES character_houses(unique_id) ON DELETE CASCADE )`, `CREATE TABLE IF NOT EXISTS character_house_history ( house_id INTEGER NOT NULL, timestamp INTEGER NOT NULL, amount INTEGER NOT NULL, status INTEGER NOT NULL, reason TEXT NOT NULL, name TEXT NOT NULL, character_id INTEGER NOT NULL, pos_flag INTEGER NOT NULL, type INTEGER NOT NULL, FOREIGN KEY (house_id) REFERENCES character_houses(unique_id) ON DELETE CASCADE )`, `CREATE TABLE IF NOT EXISTS character_house_access ( house_id INTEGER NOT NULL, character_id INTEGER NOT NULL, player_name TEXT NOT NULL, access_level INTEGER NOT NULL, permissions INTEGER NOT NULL, granted_by INTEGER NOT NULL, granted_date INTEGER NOT NULL, expires_date INTEGER NOT NULL, notes TEXT DEFAULT '', PRIMARY KEY (house_id, character_id), FOREIGN KEY (house_id) REFERENCES character_houses(unique_id) ON DELETE CASCADE )`, `CREATE TABLE IF NOT EXISTS character_house_amenities ( house_id INTEGER NOT NULL, id INTEGER NOT NULL, type INTEGER NOT NULL, name TEXT NOT NULL, cost INTEGER NOT NULL, status_cost INTEGER NOT NULL, purchase_date INTEGER NOT NULL, x REAL NOT NULL, y REAL NOT NULL, z REAL NOT NULL, heading REAL NOT NULL, is_active INTEGER NOT NULL DEFAULT 1, PRIMARY KEY (house_id, id), FOREIGN KEY (house_id) REFERENCES character_houses(unique_id) ON DELETE CASCADE )`, `CREATE TABLE IF NOT EXISTS character_house_items ( house_id INTEGER NOT NULL, id INTEGER NOT NULL, item_id INTEGER NOT NULL, character_id INTEGER NOT NULL, x REAL NOT NULL, y REAL NOT NULL, z REAL NOT NULL, heading REAL NOT NULL, pitch_x REAL NOT NULL DEFAULT 0, pitch_y REAL NOT NULL DEFAULT 0, roll_x REAL NOT NULL DEFAULT 0, roll_y REAL NOT NULL DEFAULT 0, placed_date INTEGER NOT NULL, quantity INTEGER NOT NULL, condition INTEGER NOT NULL, house TEXT NOT NULL, PRIMARY KEY (house_id, id), FOREIGN KEY (house_id) REFERENCES character_houses(unique_id) ON DELETE CASCADE )`, } for i, query := range queries { if err := sqlitex.Execute(conn, query, nil); err != nil { return fmt.Errorf("failed to create housing table %d: %w", i+1, err) } } // Create indexes for better performance indexes := []string{ `CREATE INDEX IF NOT EXISTS idx_character_houses_char_id ON character_houses(char_id)`, `CREATE INDEX IF NOT EXISTS idx_character_houses_instance_id ON character_houses(instance_id)`, `CREATE INDEX IF NOT EXISTS idx_character_houses_upkeep_due ON character_houses(upkeep_due)`, `CREATE INDEX IF NOT EXISTS idx_character_houses_status ON character_houses(status)`, `CREATE INDEX IF NOT EXISTS idx_house_deposits_house_id ON character_house_deposits(house_id)`, `CREATE INDEX IF NOT EXISTS idx_house_deposits_timestamp ON character_house_deposits(timestamp)`, `CREATE INDEX IF NOT EXISTS idx_house_history_house_id ON character_house_history(house_id)`, `CREATE INDEX IF NOT EXISTS idx_house_history_timestamp ON character_house_history(timestamp)`, `CREATE INDEX IF NOT EXISTS idx_house_access_character_id ON character_house_access(character_id)`, `CREATE INDEX IF NOT EXISTS idx_house_items_item_id ON character_house_items(item_id)`, `CREATE INDEX IF NOT EXISTS idx_houses_zone_id ON houses(zone_id)`, `CREATE INDEX IF NOT EXISTS idx_houses_alignment ON houses(alignment)`, } for i, query := range indexes { if err := sqlitex.Execute(conn, query, nil); err != nil { return fmt.Errorf("failed to create housing index %d: %w", i+1, err) } } return nil } // Helper function to convert bool to int for SQLite func boolToInt(b bool) int { if b { return 1 } return 0 }