1153 lines
33 KiB
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,
|
|
×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
|
|
}
|