eq2go/internal/housing/database.go

1153 lines
33 KiB
Go

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,
&timestampUnix,
&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,
&timestampUnix,
&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,
&notes,
)
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
}