package housing import ( "context" "fmt" "time" "eq2emu/internal/database" ) // DatabaseHousingManager implements HousingDatabase interface using the existing database wrapper type DatabaseHousingManager struct { db *database.DB } // NewDatabaseHousingManager creates a new database housing manager func NewDatabaseHousingManager(db *database.DB) *DatabaseHousingManager { return &DatabaseHousingManager{ db: db, } } // LoadHouseZones retrieves all available house types from database func (dhm *DatabaseHousingManager) LoadHouseZones(ctx context.Context) ([]HouseZoneData, error) { 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` rows, err := dhm.db.QueryContext(ctx, query) if err != nil { return nil, fmt.Errorf("failed to query house zones: %w", err) } defer rows.Close() var zones []HouseZoneData for rows.Next() { var zone HouseZoneData var description *string err := rows.Scan( &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, &description, ) if err != nil { return nil, fmt.Errorf("failed to scan house zone row: %w", err) } // Handle nullable description field if description != nil { zone.Description = *description } zones = append(zones, zone) } if err := rows.Err(); err != nil { return nil, fmt.Errorf("error iterating house zone rows: %w", err) } return zones, nil } // LoadHouseZone retrieves a specific house zone from database func (dhm *DatabaseHousingManager) LoadHouseZone(ctx context.Context, houseID int32) (*HouseZoneData, error) { 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 var description *string err := dhm.db.QueryRowContext(ctx, query, houseID).Scan( &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, &description, ) if err != nil { return nil, fmt.Errorf("failed to load house zone %d: %w", houseID, err) } // Handle nullable description field if description != nil { zone.Description = *description } return &zone, nil } // SaveHouseZone saves a house zone to database func (dhm *DatabaseHousingManager) SaveHouseZone(ctx context.Context, zone *HouseZone) error { 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 := dhm.db.ExecContext(ctx, query, 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 { _, err := dhm.db.ExecContext(ctx, "DELETE FROM houses WHERE id = ?", 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) { 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` rows, err := dhm.db.QueryContext(ctx, query, characterID) if err != nil { return nil, fmt.Errorf("failed to query player houses for character %d: %w", characterID, err) } defer rows.Close() var houses []PlayerHouseData for rows.Next() { var house PlayerHouseData var upkeepDueTimestamp int64 var houseName, publicNote, privateNote *string err := rows.Scan( &house.UniqueID, &house.CharacterID, &house.HouseID, &house.InstanceID, &upkeepDueTimestamp, &house.EscrowCoins, &house.EscrowStatus, &house.Status, &houseName, &house.VisitPermission, &publicNote, &privateNote, &house.AllowFriends, &house.AllowGuild, &house.RequireApproval, &house.ShowOnDirectory, &house.AllowDecoration, &house.TaxExempt, ) if err != nil { return nil, fmt.Errorf("failed to scan player house row: %w", err) } // Convert timestamp to time house.UpkeepDue = time.Unix(upkeepDueTimestamp, 0) // Handle nullable fields if houseName != nil { house.HouseName = *houseName } if publicNote != nil { house.PublicNote = *publicNote } if privateNote != nil { house.PrivateNote = *privateNote } houses = append(houses, house) } if err := rows.Err(); err != nil { return nil, fmt.Errorf("error iterating player house rows: %w", err) } return houses, nil } // LoadPlayerHouse retrieves a specific player house func (dhm *DatabaseHousingManager) LoadPlayerHouse(ctx context.Context, uniqueID int64) (*PlayerHouseData, error) { 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 var upkeepDueTimestamp int64 var houseName, publicNote, privateNote *string err := dhm.db.QueryRowContext(ctx, query, uniqueID).Scan( &house.UniqueID, &house.CharacterID, &house.HouseID, &house.InstanceID, &upkeepDueTimestamp, &house.EscrowCoins, &house.EscrowStatus, &house.Status, &houseName, &house.VisitPermission, &publicNote, &privateNote, &house.AllowFriends, &house.AllowGuild, &house.RequireApproval, &house.ShowOnDirectory, &house.AllowDecoration, &house.TaxExempt, ) if err != nil { return nil, fmt.Errorf("failed to load player house %d: %w", uniqueID, err) } // Convert timestamp to time house.UpkeepDue = time.Unix(upkeepDueTimestamp, 0) // Handle nullable fields if houseName != nil { house.HouseName = *houseName } if publicNote != nil { house.PublicNote = *publicNote } if privateNote != nil { house.PrivateNote = *privateNote } return &house, nil } // SavePlayerHouse saves a player house to database func (dhm *DatabaseHousingManager) SavePlayerHouse(ctx context.Context, house *PlayerHouse) error { 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 := dhm.db.ExecContext(ctx, query, 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, house.Settings.AllowFriends, house.Settings.AllowGuild, house.Settings.RequireApproval, house.Settings.ShowOnDirectory, house.Settings.AllowDecoration, 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 { // Use a transaction to delete house and all related data tx, err := dhm.db.BeginTx(ctx, nil) if err != nil { return fmt.Errorf("failed to begin transaction: %w", err) } defer tx.Rollback() // 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 = tx.ExecContext(ctx, query, uniqueID) if err != nil { return fmt.Errorf("failed to delete from %s: %w", table, err) } } // Delete the house itself _, err = tx.ExecContext(ctx, "DELETE FROM character_houses WHERE unique_id = ?", uniqueID) if err != nil { return fmt.Errorf("failed to delete player house: %w", err) } if err := tx.Commit(); err != nil { return fmt.Errorf("failed to commit transaction: %w", err) } return nil } // AddPlayerHouse creates a new player house entry func (dhm *DatabaseHousingManager) AddPlayerHouse(ctx context.Context, houseData PlayerHouseData) (int64, error) { 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() result, err := dhm.db.ExecContext(ctx, query, houseData.CharacterID, houseData.HouseID, houseData.InstanceID, upkeepDueTimestamp, houseData.EscrowCoins, houseData.EscrowStatus, houseData.Status, houseData.HouseName, houseData.VisitPermission, houseData.PublicNote, houseData.PrivateNote, houseData.AllowFriends, houseData.AllowGuild, houseData.RequireApproval, houseData.ShowOnDirectory, houseData.AllowDecoration, houseData.TaxExempt, ) if err != nil { return 0, fmt.Errorf("failed to create player house: %w", err) } id, err := result.LastInsertId() if err != nil { return 0, fmt.Errorf("failed to get new house ID: %w", err) } return id, nil } // LoadDeposits retrieves deposit history for a house func (dhm *DatabaseHousingManager) LoadDeposits(ctx context.Context, houseID int64) ([]HouseDepositData, error) { 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 ?` rows, err := dhm.db.QueryContext(ctx, query, houseID, MaxDepositHistory) if err != nil { return nil, fmt.Errorf("failed to query house deposits for house %d: %w", houseID, err) } defer rows.Close() var deposits []HouseDepositData for rows.Next() { var deposit HouseDepositData var timestampUnix int64 err := rows.Scan( &deposit.HouseID, ×tampUnix, &deposit.Amount, &deposit.LastAmount, &deposit.Status, &deposit.LastStatus, &deposit.Name, &deposit.CharacterID, ) if err != nil { return nil, fmt.Errorf("failed to scan deposit row: %w", err) } deposit.Timestamp = time.Unix(timestampUnix, 0) deposits = append(deposits, deposit) } if err := rows.Err(); err != nil { return nil, fmt.Errorf("error iterating deposit rows: %w", err) } return deposits, nil } // SaveDeposit saves a deposit record func (dhm *DatabaseHousingManager) SaveDeposit(ctx context.Context, houseID int64, deposit HouseDeposit) error { query := `INSERT INTO character_house_deposits (house_id, timestamp, amount, last_amount, status, last_status, name, character_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?)` timestampUnix := deposit.Timestamp.Unix() _, err := dhm.db.ExecContext(ctx, query, 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) { 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 ?` rows, err := dhm.db.QueryContext(ctx, query, houseID, MaxTransactionHistory) if err != nil { return nil, fmt.Errorf("failed to query house history for house %d: %w", houseID, err) } defer rows.Close() var history []HouseHistoryData for rows.Next() { var entry HouseHistoryData var timestampUnix int64 err := rows.Scan( &entry.HouseID, ×tampUnix, &entry.Amount, &entry.Status, &entry.Reason, &entry.Name, &entry.CharacterID, &entry.PosFlag, &entry.Type, ) if err != nil { return nil, fmt.Errorf("failed to scan history row: %w", err) } entry.Timestamp = time.Unix(timestampUnix, 0) history = append(history, entry) } if err := rows.Err(); err != nil { return nil, fmt.Errorf("error iterating history rows: %w", err) } return history, nil } // AddHistory adds a new history entry func (dhm *DatabaseHousingManager) AddHistory(ctx context.Context, houseID int64, history HouseHistory) error { query := `INSERT INTO character_house_history (house_id, timestamp, amount, status, reason, name, character_id, pos_flag, type) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)` timestampUnix := history.Timestamp.Unix() _, err := dhm.db.ExecContext(ctx, query, 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) { 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 = ?` rows, err := dhm.db.QueryContext(ctx, query, houseID) if err != nil { return nil, fmt.Errorf("failed to query house access for house %d: %w", houseID, err) } defer rows.Close() var accessList []HouseAccessData for rows.Next() { var access HouseAccessData var grantedDateUnix, expiresDateUnix int64 var notes *string err := rows.Scan( &access.HouseID, &access.CharacterID, &access.PlayerName, &access.AccessLevel, &access.Permissions, &access.GrantedBy, &grantedDateUnix, &expiresDateUnix, ¬es, ) if err != nil { return nil, fmt.Errorf("failed to scan access row: %w", err) } access.GrantedDate = time.Unix(grantedDateUnix, 0) access.ExpiresDate = time.Unix(expiresDateUnix, 0) if notes != nil { access.Notes = *notes } accessList = append(accessList, access) } if err := rows.Err(); err != nil { return nil, fmt.Errorf("error iterating access rows: %w", err) } return accessList, nil } // SaveHouseAccess saves access permissions for a house func (dhm *DatabaseHousingManager) SaveHouseAccess(ctx context.Context, houseID int64, accessList []HouseAccess) error { // Use a transaction for atomic updates tx, err := dhm.db.BeginTx(ctx, nil) if err != nil { return fmt.Errorf("failed to begin transaction: %w", err) } defer tx.Rollback() // Delete existing access for this house _, err = tx.ExecContext(ctx, "DELETE FROM character_house_access WHERE house_id = ?", 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 = tx.ExecContext(ctx, insertQuery, 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) } } if err := tx.Commit(); err != nil { return fmt.Errorf("failed to commit transaction: %w", err) } return nil } // DeleteHouseAccess removes access for a specific character func (dhm *DatabaseHousingManager) DeleteHouseAccess(ctx context.Context, houseID int64, characterID int32) error { _, err := dhm.db.ExecContext(ctx, "DELETE FROM character_house_access WHERE house_id = ? AND character_id = ?", houseID, characterID) if err != nil { return fmt.Errorf("failed to delete house access: %w", err) } return nil } // Additional database methods continued in next part due to length... // LoadHouseAmenities retrieves amenities for a house func (dhm *DatabaseHousingManager) LoadHouseAmenities(ctx context.Context, houseID int64) ([]HouseAmenityData, error) { 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 = ?` rows, err := dhm.db.QueryContext(ctx, query, houseID) if err != nil { return nil, fmt.Errorf("failed to query house amenities for house %d: %w", houseID, err) } defer rows.Close() var amenities []HouseAmenityData for rows.Next() { var amenity HouseAmenityData var purchaseDateUnix int64 err := rows.Scan( &amenity.HouseID, &amenity.ID, &amenity.Type, &amenity.Name, &amenity.Cost, &amenity.StatusCost, &purchaseDateUnix, &amenity.X, &amenity.Y, &amenity.Z, &amenity.Heading, &amenity.IsActive, ) if err != nil { return nil, fmt.Errorf("failed to scan amenity row: %w", err) } amenity.PurchaseDate = time.Unix(purchaseDateUnix, 0) amenities = append(amenities, amenity) } if err := rows.Err(); err != nil { return nil, fmt.Errorf("error iterating amenity rows: %w", err) } return amenities, nil } // SaveHouseAmenity saves a house amenity func (dhm *DatabaseHousingManager) SaveHouseAmenity(ctx context.Context, houseID int64, amenity HouseAmenity) error { 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 := dhm.db.ExecContext(ctx, query, houseID, amenity.ID, amenity.Type, amenity.Name, amenity.Cost, amenity.StatusCost, purchaseDateUnix, amenity.X, amenity.Y, amenity.Z, amenity.Heading, 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 { _, err := dhm.db.ExecContext(ctx, "DELETE FROM character_house_amenities WHERE house_id = ? AND id = ?", 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) { 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 = ?` rows, err := dhm.db.QueryContext(ctx, query, houseID) if err != nil { return nil, fmt.Errorf("failed to query house items for house %d: %w", houseID, err) } defer rows.Close() var items []HouseItemData for rows.Next() { var item HouseItemData var placedDateUnix int64 err := rows.Scan( &item.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 nil, fmt.Errorf("failed to scan item row: %w", err) } item.PlacedDate = time.Unix(placedDateUnix, 0) items = append(items, item) } if err := rows.Err(); err != nil { return nil, fmt.Errorf("error iterating item rows: %w", err) } return items, nil } // SaveHouseItem saves a house item func (dhm *DatabaseHousingManager) SaveHouseItem(ctx context.Context, houseID int64, item HouseItem) error { 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 := dhm.db.ExecContext(ctx, query, 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 { _, err := dhm.db.ExecContext(ctx, "DELETE FROM character_house_items WHERE house_id = ? AND id = ?", houseID, itemID) if err != nil { return fmt.Errorf("failed to delete house item: %w", err) } return nil } // Utility operations // GetNextHouseID returns the next available house unique ID func (dhm *DatabaseHousingManager) GetNextHouseID(ctx context.Context) (int64, error) { query := "SELECT COALESCE(MAX(unique_id), 0) + 1 FROM character_houses" var nextID int64 err := dhm.db.QueryRowContext(ctx, query).Scan(&nextID) 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) { 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 var upkeepDueTimestamp int64 var houseName, publicNote, privateNote *string err := dhm.db.QueryRowContext(ctx, query, instanceID).Scan( &house.UniqueID, &house.CharacterID, &house.HouseID, &house.InstanceID, &upkeepDueTimestamp, &house.EscrowCoins, &house.EscrowStatus, &house.Status, &houseName, &house.VisitPermission, &publicNote, &privateNote, &house.AllowFriends, &house.AllowGuild, &house.RequireApproval, &house.ShowOnDirectory, &house.AllowDecoration, &house.TaxExempt, ) if err != nil { return nil, fmt.Errorf("failed to load house by instance %d: %w", instanceID, err) } // Convert timestamp and handle nullable fields house.UpkeepDue = time.Unix(upkeepDueTimestamp, 0) if houseName != nil { house.HouseName = *houseName } if publicNote != nil { house.PublicNote = *publicNote } if privateNote != nil { house.PrivateNote = *privateNote } 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 { query := "UPDATE character_houses SET upkeep_due = ? WHERE unique_id = ?" upkeepDueTimestamp := upkeepDue.Unix() _, err := dhm.db.ExecContext(ctx, query, 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 { query := "UPDATE character_houses SET escrow_coins = ?, escrow_status = ? WHERE unique_id = ?" _, err := dhm.db.ExecContext(ctx, query, 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) { 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() rows, err := dhm.db.QueryContext(ctx, query, cutoffTimestamp, HouseStatusActive) if err != nil { return nil, fmt.Errorf("failed to query houses for upkeep: %w", err) } defer rows.Close() var houses []PlayerHouseData for rows.Next() { var house PlayerHouseData var upkeepDueTimestamp int64 var houseName, publicNote, privateNote *string err := rows.Scan( &house.UniqueID, &house.CharacterID, &house.HouseID, &house.InstanceID, &upkeepDueTimestamp, &house.EscrowCoins, &house.EscrowStatus, &house.Status, &houseName, &house.VisitPermission, &publicNote, &privateNote, &house.AllowFriends, &house.AllowGuild, &house.RequireApproval, &house.ShowOnDirectory, &house.AllowDecoration, &house.TaxExempt, ) if err != nil { return nil, fmt.Errorf("failed to scan upkeep house row: %w", err) } house.UpkeepDue = time.Unix(upkeepDueTimestamp, 0) if houseName != nil { house.HouseName = *houseName } if publicNote != nil { house.PublicNote = *publicNote } if privateNote != nil { house.PrivateNote = *privateNote } houses = append(houses, house) } if err := rows.Err(); err != nil { return nil, fmt.Errorf("error iterating upkeep house rows: %w", err) } return houses, nil } // GetHouseStatistics returns housing system statistics func (dhm *DatabaseHousingManager) GetHouseStatistics(ctx context.Context) (*HousingStatistics, error) { 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 := map[string]*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 query, target := range queries { err := dhm.db.QueryRowContext(ctx, query).Scan(target) 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 { 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 { _, err := dhm.db.ExecContext(ctx, query) if 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 { _, err := dhm.db.ExecContext(ctx, query) if err != nil { return fmt.Errorf("failed to create housing index %d: %w", i+1, err) } } return nil }