package housing import ( "context" "eq2emu/internal/database" "eq2emu/internal/packets" "fmt" "sync" "time" ) // House represents a purchasable house type/zone type House struct { mu sync.RWMutex ID int32 Name string CostCoins int64 CostStatus int64 UpkeepCoins int64 UpkeepStatus int64 VaultSlots int8 Alignment int8 GuildLevel int8 ZoneID int32 ExitZoneID int32 ExitX float32 ExitY float32 ExitZ float32 ExitHeading float32 } // CharacterHouse represents a house owned by a character type CharacterHouse struct { mu sync.RWMutex UniqueID int64 CharacterID int32 HouseID int32 InstanceID int32 UpkeepDue time.Time EscrowCoins int64 EscrowStatus int64 Status int8 Settings CharacterHouseSettings AccessList map[int32]HouseAccess Items []HouseItem History []HouseHistory } // CharacterHouseSettings represents house configuration type CharacterHouseSettings struct { HouseName string VisitPermission int8 PublicNote string PrivateNote string AllowFriends bool AllowGuild bool RequireApproval bool ShowOnDirectory bool AllowDecoration bool } // HouseAccess represents access permissions for a character type HouseAccess struct { CharacterID int32 PlayerName string AccessLevel int8 Permissions int32 GrantedBy int32 GrantedDate time.Time ExpiresDate time.Time Notes string } // HouseItem represents an item placed in a house type HouseItem struct { ID int64 ItemID int32 CharacterID int32 X float32 Y float32 Z float32 Heading float32 PitchX float32 PitchY float32 RollX float32 RollY float32 PlacedDate time.Time Quantity int32 Condition int8 } // HouseHistory represents a transaction history entry type HouseHistory struct { Timestamp time.Time Amount int64 Status int64 Reason string Name string CharacterID int32 Type int } // HousingManager manages the housing system type HousingManager struct { mu sync.RWMutex db *database.Database houses map[int32]*House // Available house types characterHouses map[int64]*CharacterHouse // All character houses by unique ID characterIndex map[int32][]*CharacterHouse // Character houses by character ID logger Logger config HousingConfig } // Logger interface for housing system logging type Logger interface { LogInfo(system, format string, args ...interface{}) LogError(system, format string, args ...interface{}) LogDebug(system, format string, args ...interface{}) LogWarning(system, format string, args ...interface{}) } // PlayerManager interface for player operations type PlayerManager interface { CanPlayerAffordHouse(characterID int32, coinCost, statusCost int64) (bool, error) DeductPlayerCoins(characterID int32, amount int64) error DeductPlayerStatus(characterID int32, amount int64) error GetPlayerAlignment(characterID int32) (int8, error) GetPlayerGuildLevel(characterID int32) (int8, error) } // HousingConfig contains housing system configuration type HousingConfig struct { EnableUpkeep bool EnableForeclosure bool UpkeepGracePeriod int32 MaxHousesPerPlayer int EnableStatistics bool } // NewHousingManager creates a new housing manager func NewHousingManager(db *database.Database, logger Logger, config HousingConfig) *HousingManager { return &HousingManager{ db: db, houses: make(map[int32]*House), characterHouses: make(map[int64]*CharacterHouse), characterIndex: make(map[int32][]*CharacterHouse), logger: logger, config: config, } } // Initialize loads house data and starts background processes func (hm *HousingManager) Initialize(ctx context.Context) error { hm.mu.Lock() defer hm.mu.Unlock() // Load available house types from database houseData, err := hm.loadHousesFromDB(ctx) if err != nil { return fmt.Errorf("failed to load houses: %w", err) } for _, house := range houseData { hm.houses[house.ID] = house } hm.logger.LogInfo("housing", "Loaded %d house types", len(hm.houses)) // Start upkeep processing if enabled if hm.config.EnableUpkeep { go hm.processUpkeepLoop(ctx) } return nil } // GetHouse returns a house type by ID func (hm *HousingManager) GetHouse(houseID int32) (*House, bool) { hm.mu.RLock() defer hm.mu.RUnlock() house, exists := hm.houses[houseID] return house, exists } // GetAvailableHouses returns all available house types func (hm *HousingManager) GetAvailableHouses() []*House { hm.mu.RLock() defer hm.mu.RUnlock() houses := make([]*House, 0, len(hm.houses)) for _, house := range hm.houses { houses = append(houses, house) } return houses } // GetCharacterHouses returns all houses owned by a character func (hm *HousingManager) GetCharacterHouses(characterID int32) ([]*CharacterHouse, error) { hm.mu.RLock() houses, exists := hm.characterIndex[characterID] hm.mu.RUnlock() if !exists { // If no database, return empty list if hm.db == nil { return []*CharacterHouse{}, nil } // Load from database houses, err := hm.loadCharacterHousesFromDB(context.Background(), characterID) if err != nil { return nil, fmt.Errorf("failed to load character houses: %w", err) } hm.mu.Lock() hm.characterIndex[characterID] = houses for _, house := range houses { hm.characterHouses[house.UniqueID] = house } hm.mu.Unlock() return houses, nil } return houses, nil } // PurchaseHouse handles house purchase requests func (hm *HousingManager) PurchaseHouse(ctx context.Context, characterID int32, houseID int32, playerManager PlayerManager) (*CharacterHouse, error) { // Get house type house, exists := hm.GetHouse(houseID) if !exists { return nil, fmt.Errorf("house type %d not found", houseID) } // Check if player can afford it canAfford, err := playerManager.CanPlayerAffordHouse(characterID, house.CostCoins, house.CostStatus) if err != nil { return nil, fmt.Errorf("failed to check affordability: %w", err) } if !canAfford { return nil, fmt.Errorf("insufficient funds") } // Check alignment requirement if house.Alignment != AlignmentAny { alignment, err := playerManager.GetPlayerAlignment(characterID) if err != nil { return nil, fmt.Errorf("failed to get player alignment: %w", err) } if alignment != house.Alignment { return nil, fmt.Errorf("alignment requirement not met") } } // Check guild level requirement if house.GuildLevel > 0 { guildLevel, err := playerManager.GetPlayerGuildLevel(characterID) if err != nil { return nil, fmt.Errorf("failed to get guild level: %w", err) } if guildLevel < house.GuildLevel { return nil, fmt.Errorf("guild level requirement not met") } } // Check max houses limit currentHouses, err := hm.GetCharacterHouses(characterID) if err != nil { return nil, fmt.Errorf("failed to get current houses: %w", err) } if len(currentHouses) >= hm.config.MaxHousesPerPlayer { return nil, fmt.Errorf("maximum number of houses reached") } // Deduct costs if err := playerManager.DeductPlayerCoins(characterID, house.CostCoins); err != nil { return nil, fmt.Errorf("failed to deduct coins: %w", err) } if err := playerManager.DeductPlayerStatus(characterID, house.CostStatus); err != nil { return nil, fmt.Errorf("failed to deduct status: %w", err) } // Create character house characterHouse := &CharacterHouse{ CharacterID: characterID, HouseID: houseID, InstanceID: hm.generateInstanceID(), UpkeepDue: time.Now().Add(7 * 24 * time.Hour), // Weekly upkeep EscrowCoins: 0, EscrowStatus: 0, Status: HouseStatusActive, Settings: CharacterHouseSettings{ VisitPermission: VisitPermissionFriends, AllowFriends: true, ShowOnDirectory: true, }, AccessList: make(map[int32]HouseAccess), Items: []HouseItem{}, History: []HouseHistory{{ Timestamp: time.Now(), Amount: house.CostCoins, Status: house.CostStatus, Reason: "House Purchase", CharacterID: characterID, Type: TransactionPurchase, }}, } // Save to database if database is available if hm.db != nil { if err := hm.saveCharacterHouseToDBInternal(ctx, characterHouse); err != nil { return nil, fmt.Errorf("failed to save house: %w", err) } } // Add to memory hm.mu.Lock() hm.characterHouses[characterHouse.UniqueID] = characterHouse hm.characterIndex[characterID] = append(hm.characterIndex[characterID], characterHouse) hm.mu.Unlock() hm.logger.LogInfo("housing", "Character %d purchased house type %d", characterID, houseID) return characterHouse, nil } // SendHousePurchasePacket sends a house purchase packet to a client func (hm *HousingManager) SendHousePurchasePacket(characterID int32, clientVersion int32, house *House) error { def, exists := packets.GetPacket("PlayerHousePurchase") if !exists { return fmt.Errorf("PlayerHousePurchase packet definition not found") } builder := packets.NewPacketBuilder(def, uint32(clientVersion), 0) packetData := map[string]any{ "house_name": house.Name, "house_id": uint64(house.ID), "spawn_id": uint32(0), "purchase_coins": house.CostCoins, "purchase_status": house.CostStatus, "upkeep_coins": house.UpkeepCoins, "upkeep_status": house.UpkeepStatus, "vendor_vault_slots": house.VaultSlots, "additional_reqs": fmt.Sprintf("Alignment: %s", AlignmentNames[house.Alignment]), "enable_buy": 1, } packet, err := builder.Build(packetData) if err != nil { return fmt.Errorf("failed to build packet: %w", err) } // TODO: Send packet to client when client interface is available _ = packet hm.logger.LogDebug("housing", "Built house purchase packet for character %d", characterID) return nil } // SendCharacterHousesPacket sends a character's house list to client func (hm *HousingManager) SendCharacterHousesPacket(characterID int32, clientVersion int32) error { houses, err := hm.GetCharacterHouses(characterID) if err != nil { return fmt.Errorf("failed to get character houses: %w", err) } def, exists := packets.GetPacket("CharacterHousingList") if !exists { return fmt.Errorf("CharacterHousingList packet definition not found") } builder := packets.NewPacketBuilder(def, uint32(clientVersion), 0) houseArray := make([]map[string]any, len(houses)) for i, house := range houses { houseType, exists := hm.GetHouse(house.HouseID) houseName := "Unknown" if exists { houseName = houseType.Name } houseArray[i] = map[string]any{ "house_id": uint64(house.UniqueID), "zone": house.HouseID, "house_city": "Qeynos", // Default city "house_address": house.Settings.HouseName, "house_description": fmt.Sprintf("Upkeep due: %s", house.UpkeepDue.Format("2006-01-02")), "index": i, } if house.Settings.HouseName == "" { houseArray[i]["house_address"] = houseName } } packetData := map[string]any{ "num_houses": len(houses), "house_array": houseArray, } packet, err := builder.Build(packetData) if err != nil { return fmt.Errorf("failed to build packet: %w", err) } // TODO: Send packet to client when client interface is available _ = packet hm.logger.LogDebug("housing", "Built housing list packet for character %d (%d houses)", characterID, len(houses)) return nil } // PayUpkeep handles upkeep payment for a house func (hm *HousingManager) PayUpkeep(ctx context.Context, houseUniqueID int64, playerManager PlayerManager) error { hm.mu.RLock() characterHouse, exists := hm.characterHouses[houseUniqueID] hm.mu.RUnlock() if !exists { return fmt.Errorf("character house %d not found", houseUniqueID) } house, exists := hm.GetHouse(characterHouse.HouseID) if !exists { return fmt.Errorf("house type %d not found", characterHouse.HouseID) } // Check if player can afford upkeep canAfford, err := playerManager.CanPlayerAffordHouse(characterHouse.CharacterID, house.UpkeepCoins, house.UpkeepStatus) if err != nil { return fmt.Errorf("failed to check affordability: %w", err) } if !canAfford { return fmt.Errorf("insufficient funds for upkeep") } // Deduct upkeep costs if err := playerManager.DeductPlayerCoins(characterHouse.CharacterID, house.UpkeepCoins); err != nil { return fmt.Errorf("failed to deduct upkeep coins: %w", err) } if err := playerManager.DeductPlayerStatus(characterHouse.CharacterID, house.UpkeepStatus); err != nil { return fmt.Errorf("failed to deduct upkeep status: %w", err) } // Update upkeep due date characterHouse.mu.Lock() characterHouse.UpkeepDue = time.Now().Add(7 * 24 * time.Hour) // Next week characterHouse.History = append(characterHouse.History, HouseHistory{ Timestamp: time.Now(), Amount: house.UpkeepCoins, Status: house.UpkeepStatus, Reason: "Upkeep Payment", CharacterID: characterHouse.CharacterID, Type: TransactionUpkeep, }) characterHouse.mu.Unlock() // Save to database if database is available if hm.db != nil { if err := hm.saveCharacterHouseToDBInternal(ctx, characterHouse); err != nil { return fmt.Errorf("failed to save upkeep payment: %w", err) } } hm.logger.LogInfo("housing", "Paid upkeep for house %d (coins: %d, status: %d)", houseUniqueID, house.UpkeepCoins, house.UpkeepStatus) return nil } // processUpkeepLoop runs upkeep processing in background func (hm *HousingManager) processUpkeepLoop(ctx context.Context) { ticker := time.NewTicker(1 * time.Hour) defer ticker.Stop() for { select { case <-ctx.Done(): hm.logger.LogInfo("housing", "Stopping upkeep processing") return case <-ticker.C: if err := hm.processUpkeep(ctx); err != nil { hm.logger.LogError("housing", "Failed to process upkeep: %v", err) } } } } // processUpkeep processes houses due for upkeep func (hm *HousingManager) processUpkeep(ctx context.Context) error { hm.mu.RLock() houses := make([]*CharacterHouse, 0) now := time.Now() cutoff := now.Add(24 * time.Hour) // Houses due in next 24 hours for _, house := range hm.characterHouses { if house.UpkeepDue.Before(cutoff) && house.Status == HouseStatusActive { houses = append(houses, house) } } hm.mu.RUnlock() hm.logger.LogInfo("housing", "Processing upkeep for %d houses", len(houses)) for _, house := range houses { if house.UpkeepDue.Before(now) { // Mark as upkeep due house.mu.Lock() house.Status = HouseStatusUpkeepDue house.mu.Unlock() hm.logger.LogWarning("housing", "House %d upkeep is overdue", house.UniqueID) // If enabled, could handle foreclosure here if hm.config.EnableForeclosure { gracePeriod := time.Duration(hm.config.UpkeepGracePeriod) * time.Second if house.UpkeepDue.Add(gracePeriod).Before(now) { house.mu.Lock() house.Status = HouseStatusForeclosed house.mu.Unlock() hm.logger.LogInfo("housing", "House %d foreclosed due to unpaid upkeep", house.UniqueID) } } } } return nil } // generateInstanceID generates a unique instance ID for houses func (hm *HousingManager) generateInstanceID() int32 { // Simple implementation - in production would use proper ID generation return int32(time.Now().Unix()) } // Database operations (internal) func (hm *HousingManager) loadHousesFromDB(ctx context.Context) ([]*House, error) { query := ` SELECT id, name, cost_coins, cost_status, upkeep_coins, upkeep_status, vault_slots, alignment, guild_level, zone_id, exit_zone_id, exit_x, exit_y, exit_z, exit_heading FROM character_house_zones ORDER BY id ` rows, err := hm.db.Query(query) if err != nil { return nil, fmt.Errorf("failed to query houses: %w", err) } defer rows.Close() houses := make([]*House, 0) for rows.Next() { house := &House{} err := rows.Scan( &house.ID, &house.Name, &house.CostCoins, &house.CostStatus, &house.UpkeepCoins, &house.UpkeepStatus, &house.VaultSlots, &house.Alignment, &house.GuildLevel, &house.ZoneID, &house.ExitZoneID, &house.ExitX, &house.ExitY, &house.ExitZ, &house.ExitHeading, ) if err != nil { return nil, fmt.Errorf("failed to scan house: %w", err) } houses = append(houses, house) } return houses, nil } func (hm *HousingManager) loadCharacterHousesFromDB(ctx context.Context, characterID int32) ([]*CharacterHouse, 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 FROM character_houses WHERE char_id = ? ORDER BY unique_id ` rows, err := hm.db.Query(query, characterID) if err != nil { return nil, fmt.Errorf("failed to query character houses: %w", err) } defer rows.Close() houses := make([]*CharacterHouse, 0) for rows.Next() { house := &CharacterHouse{ AccessList: make(map[int32]HouseAccess), Items: []HouseItem{}, History: []HouseHistory{}, } err := rows.Scan( &house.UniqueID, &house.CharacterID, &house.HouseID, &house.InstanceID, &house.UpkeepDue, &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, ) if err != nil { return nil, fmt.Errorf("failed to scan character house: %w", err) } houses = append(houses, house) } return houses, nil } func (hm *HousingManager) saveCharacterHouseToDBInternal(ctx context.Context, house *CharacterHouse) error { if house.UniqueID == 0 { // Insert new house 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 ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ` result, err := hm.db.Exec(query, house.CharacterID, house.HouseID, house.InstanceID, house.UpkeepDue, 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, ) if err != nil { return fmt.Errorf("failed to insert character house: %w", err) } id, err := result.LastInsertId() if err != nil { return fmt.Errorf("failed to get insert ID: %w", err) } house.UniqueID = id } else { // Update existing house query := ` UPDATE character_houses SET 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 = ? WHERE unique_id = ? ` _, err := hm.db.Exec(query, house.UpkeepDue, 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.UniqueID, ) if err != nil { return fmt.Errorf("failed to update character house: %w", err) } } return nil } // Shutdown gracefully shuts down the housing manager func (hm *HousingManager) Shutdown(ctx context.Context) error { hm.logger.LogInfo("housing", "Shutting down housing manager") // Any cleanup would go here return nil } // Utility functions // FormatUpkeepDue formats upkeep due date for display func FormatUpkeepDue(upkeepDue time.Time) string { now := time.Now() if upkeepDue.Before(now) { duration := now.Sub(upkeepDue) days := int(duration.Hours() / 24) if days == 0 { return "Overdue (today)" } return fmt.Sprintf("Overdue (%d days)", days) } else { duration := upkeepDue.Sub(now) days := int(duration.Hours() / 24) if days == 0 { return "Due today" } return fmt.Sprintf("Due in %d days", days) } } // FormatCurrency formats currency amounts for display func FormatCurrency(amount int64) string { if amount < 0 { return fmt.Sprintf("-%s", FormatCurrency(-amount)) } if amount >= 10000 { // 1 gold = 10000 copper gold := amount / 10000 remainder := amount % 10000 if remainder == 0 { return fmt.Sprintf("%dg", gold) } else { silver := remainder / 100 copper := remainder % 100 if copper == 0 { return fmt.Sprintf("%dg %ds", gold, silver) } else { return fmt.Sprintf("%dg %ds %dc", gold, silver, copper) } } } else if amount >= 100 { silver := amount / 100 copper := amount % 100 if copper == 0 { return fmt.Sprintf("%ds", silver) } else { return fmt.Sprintf("%ds %dc", silver, copper) } } else { return fmt.Sprintf("%dc", amount) } }