eq2go/internal/housing/database.go

1301 lines
40 KiB
Go

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