using System; using System.Collections.Generic; using System.Linq; using System.Text.RegularExpressions; using log4net; using ACE.Common.Performance; using ACE.Database; using ACE.Database.Models.Shard; using ACE.Database.Models.World; using ACE.Entity; using ACE.Entity.Enum; using ACE.Entity.Enum.Properties; using ACE.Server.Entity; using ACE.Server.Entity.Actions; using ACE.Server.Factories; using ACE.Server.Network.GameMessages.Messages; using ACE.Server.Network.Structure; using ACE.Server.WorldObjects; namespace ACE.Server.Managers { public static class HouseManager { private static readonly ILog log = LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType); /// /// A lookup table of HouseId => HouseGuid /// private static Dictionary> HouseIdToGuid { get; set; } /// /// A list of all player-owned houses on the server, /// sorted by RentDue times /// private static SortedSet RentQueue; /// /// HouseManager actions to run when slumlord inventory has completed loading /// private static Dictionary> SlumlordCallbacks = new Dictionary>(); /// /// The rate at which HouseManager.Tick() executes /// private static readonly RateLimiter updateHouseManagerRateLimiter = new RateLimiter(1, TimeSpan.FromMinutes(1)); public static void Initialize() { TotalOwnedHousingByType[HouseType.Apartment] = 0; TotalOwnedHousingByType[HouseType.Cottage] = 0; TotalOwnedHousingByType[HouseType.Villa] = 0; TotalOwnedHousingByType[HouseType.Mansion] = 0; BuildHouseIdToGuid(); BuildRentQueue(); } /// /// Builds the lookup table for HouseId => HouseGuid /// private static void BuildHouseIdToGuid() { using (var ctx = new WorldDbContext()) { var query = from weenie in ctx.Weenie join inst in ctx.LandblockInstance on weenie.ClassId equals inst.WeenieClassId where weenie.Type == (int)WeenieType.House select new { Weenie = weenie, Instance = inst }; var results = query.ToList(); HouseIdToGuid = new Dictionary>(); foreach (var result in results) { var classname = result.Weenie.ClassName; var guid = result.Instance.Guid; if (!uint.TryParse(Regex.Match(classname, @"\d+").Value, out var houseId)) { log.Error($"[HOUSE] HouseManager.BuildHouseIdToGuid(): couldn't parse {classname}"); continue; } if (!HouseIdToGuid.TryGetValue(houseId, out var houseGuids)) { houseGuids = new List(); HouseIdToGuid.Add(houseId, houseGuids); } houseGuids.Add(guid); } //log.Info($"BuildHouseIdToGuid: {HouseIdToGuid.Count}"); } } /// /// Builds a list of all the player-owned houses on the server, sorted by RentDue timestamp /// public static void BuildRentQueue() { RentQueue = new SortedSet(); //var allPlayers = PlayerManager.GetAllPlayers(); //var houseOwners = allPlayers.Where(i => i.HouseInstance != null); //foreach (var houseOwner in houseOwners) //AddRentQueue(houseOwner); var slumlordBiotas = DatabaseManager.Shard.BaseDatabase.GetBiotasByType(WeenieType.SlumLord); foreach (var slumlord in slumlordBiotas) AddRentQueue(slumlord); //log.Info($"Loaded {RentQueue.Count} active houses."); QueryMultiHouse(); } /// /// Adds a player-owned house to the rent queue /// private static void AddRentQueue(Biota slumlord) { var biotaOwner = slumlord.BiotaPropertiesIID.FirstOrDefault(i => i.Type == (ushort)PropertyInstanceId.HouseOwner); if (biotaOwner == null) { // this is fine. this is just a house that was purchased, and then later abandoned //Console.WriteLine($"HouseManager.AddRentQueue(): couldn't find owner for house {slumlord.Id:X8}"); return; } var owner = PlayerManager.FindByGuid(biotaOwner.Value); if (owner == null) { log.Error($"[HOUSE] HouseManager.AddRentQueue(): couldn't find owner {biotaOwner.Value:X8}"); return; } var houseId = slumlord.BiotaPropertiesDID.FirstOrDefault(i => i.Type == (ushort)PropertyDataId.HouseId); if (houseId == null) { log.Error($"[HOUSE] HouseManager.AddRentQueue(): couldn't find house id for {slumlord.Id:X8}"); return; } if (!HouseIdToGuid.TryGetValue(houseId.Value, out var houseGuids)) { log.Error($"[HOUSE] HouseManager.AddRentQueue(): couldn't find house instance for {slumlord.Id:X8}"); return; } var houseInstance = GetHouseGuid(slumlord.Id, houseGuids); if (houseInstance == 0) { log.Error($"[HOUSE] HouseManager.AddRentQueue(): couldn't find house guid for {slumlord.Id:X8}"); return; } if (RentQueueContainsHouse(houseInstance)) { log.Error($"[HOUSE] HouseManager.AddRentQueue(): rent queue already contains house {houseInstance}"); return; } AddRentQueue(owner, houseInstance); } /// /// Returns TRUE if houseInstance is contained in the RentQueue /// private static bool RentQueueContainsHouse(uint houseInstance) { return RentQueue.Any(i => i.House.HouseInstance == houseInstance); } /// /// Adds a player-owned house to the rent queue /// public static void AddRentQueue(IPlayer player, uint houseGuid) { //Console.WriteLine($"AddRentQueue({player.Name}, {houseGuid:X8})"); var house = House.Load(houseGuid); if (house == null) // this can happen for basement dungeons return; var purchaseTime = (uint)(player.HousePurchaseTimestamp ?? 0); if (player.HouseRentTimestamp == null) { log.WarnFormat("[HOUSE] HouseManager.AddRentQueue({0}, {1:X8}): player has null HouseRentTimestamp", player.Name, houseGuid); player.HouseRentTimestamp = (int)house.GetRentDue(purchaseTime); //return; } AddRentQueue(player, house); } /// /// Adds a player-owned house to the rent queue /// private static void AddRentQueue(IPlayer player, House house) { var playerHouse = new PlayerHouse(player, house); RentQueue.Add(playerHouse); TotalOwnedHousingByType[playerHouse.House.HouseType]++; } /// /// Called when a player abandons a house /// public static void RemoveRentQueue(uint houseGuid) { RentQueue.RemoveWhere(i => i.House.Guid.Full == houseGuid); } /// /// Queries the status of multi-house owners on the server /// private static void QueryMultiHouse() { var slumlordBiotas = DatabaseManager.Shard.BaseDatabase.GetBiotasByType(WeenieType.SlumLord); var playerHouses = new Dictionary>(); var accountHouses = new Dictionary>(); foreach (var slumlord in slumlordBiotas) { var biotaOwner = slumlord.BiotaPropertiesIID.FirstOrDefault(i => i.Type == (ushort)PropertyInstanceId.HouseOwner); if (biotaOwner == null) { // this is fine. this is just a house that was purchased, and then later abandoned //Console.WriteLine($"HouseManager.QueryMultiHouse(): couldn't find owner for house {slumlord.Id:X8}"); continue; } var owner = PlayerManager.FindByGuid(biotaOwner.Value); if (owner == null) { Console.WriteLine($"HouseManager.QueryMultiHouse(): couldn't find owner {biotaOwner.Value:X8}"); continue; } if (!playerHouses.TryGetValue(owner, out var houses)) { houses = new List(); playerHouses.Add(owner, houses); } houses.Add(slumlord); var accountName = owner.Account != null ? owner.Account.AccountName : "NULL"; if (!accountHouses.TryGetValue(accountName, out var aHouses)) { aHouses = new List(); accountHouses.Add(accountName, aHouses); } aHouses.Add(slumlord); } if (PropertyManager.GetBool("house_per_char").Item) { var results = playerHouses.Where(i => i.Value.Count() > 1).OrderByDescending(i => i.Value.Count()); if (results.Count() > 0) Console.WriteLine("Multi-house owners:"); foreach (var playerHouse in results) { Console.WriteLine($"{playerHouse.Key.Name}: {playerHouse.Value.Count}"); for (var i = 0; i < playerHouse.Value.Count; i++) Console.WriteLine($"{i + 1}. {GetCoords(playerHouse.Value[i])}"); } } else { var results = accountHouses.Where(i => i.Value.Count() > 1).OrderByDescending(i => i.Value.Count()); if (results.Count() > 0) Console.WriteLine("Multi-house owners:"); foreach (var accountHouse in results) { Console.WriteLine($"{accountHouse.Key}: {accountHouse.Value.Count}"); for (var i = 0; i < accountHouse.Value.Count; i++) Console.WriteLine($"{i + 1}. {GetCoords(accountHouse.Value[i])}"); } } } /// /// Returns a friendly string for the position of a biota /// private static string GetCoords(Biota biota) { var p = biota.BiotaPropertiesPosition.FirstOrDefault(i => i.PositionType == (ushort)PositionType.Location); return GetCoords(new Position(p.ObjCellId, p.OriginX, p.OriginY, p.OriginZ, p.AnglesX, p.AnglesY, p.AnglesZ, p.AnglesW)); } /// /// Returns a friendly string a house / slumlord position /// public static string GetCoords(Position position) { var coords = position.GetMapCoordStr(); if (coords == null) { // apartment slumlord? if (ApartmentBlocks.TryGetValue(position.Landblock, out var apartmentBlock)) coords = $"{apartmentBlock} - "; else log.Error($"[HOUSE] HouseManager.GetCoords({position}) - couldn't find apartment block"); coords += position; } return coords; } /// /// Runs every ~1 minute /// public static void Tick() { if (updateHouseManagerRateLimiter.GetSecondsToWaitBeforeNextEvent() > 0) return; updateHouseManagerRateLimiter.RegisterEvent(); //log.Info($"HouseManager.Tick({RentQueue.Count})"); var nextEntry = RentQueue.FirstOrDefault(); if (nextEntry == null) return; var currentTime = DateTime.UtcNow; while (currentTime > nextEntry.RentDue) { RentQueue.Remove(nextEntry); DecrementTotalOwnedHousingByType(nextEntry.House.HouseType); ProcessRent(nextEntry); nextEntry = RentQueue.FirstOrDefault(); if (nextEntry == null) return; currentTime = DateTime.UtcNow; } } /// /// Called when the RentDue timestamp has been reached for a player house /// Determines if rent has been paid, and if the owner currently meets the requirements for owning the dwelling, /// and then calls either HandleRentPaid or HandleEviction /// private static void ProcessRent(PlayerHouse playerHouse) { // load the most up-to-date copy of the house data GetHouse(playerHouse.House.Guid.Full, (house) => { playerHouse.House = house; var isInActiveOrDisabled = playerHouse.House.HouseStatus <= HouseStatus.InActive; var isPaid = IsRentPaid(playerHouse); var hasRequirements = HasRequirements(playerHouse); log.InfoFormat("[HOUSE] {0}.ProcessRent(): isPaid = {1} | HasRequirements = {2} | MaintenanceFree = {3}", playerHouse.PlayerName, isPaid, hasRequirements, (house.HouseStatus == HouseStatus.InActive)); if (isInActiveOrDisabled || (isPaid && hasRequirements)) HandleRentPaid(playerHouse); else HandleEviction(playerHouse); }); } /// /// Handles a successful rent payment /// Updates the next rent due timestamp, and clears the slumlord inventory /// private static void HandleRentPaid(PlayerHouse playerHouse) { var player = PlayerManager.FindByGuid(playerHouse.PlayerGuid); if (player == null) { log.WarnFormat("[HOUSE] HouseManager.HandleRentPaid({0}): couldn't find player", playerHouse.PlayerName); return; } var purchaseTime = (uint)(player.HousePurchaseTimestamp ?? 0); var rentTime = (uint)(player.HouseRentTimestamp ?? 0); var nextRentTime = playerHouse.House.GetRentDue(purchaseTime); if (nextRentTime <= rentTime) { log.WarnFormat("[HOUSE] HouseManager.HandleRentPaid({0}): nextRentTime {1} <= rentTime {2}", playerHouse.PlayerName, nextRentTime, rentTime); return; } player.HouseRentTimestamp = (int)nextRentTime; player.SaveBiotaToDatabase(); var clearedInventoryStatus = ""; if (playerHouse.House.HouseStatus == HouseStatus.Active) { // clear out slumlord inventory var slumlord = playerHouse.House.SlumLord; slumlord.ClearInventory(); slumlord.SaveBiotaToDatabase(); clearedInventoryStatus = "and cleared "; } log.InfoFormat($"[HOUSE] HouseManager.HandleRentPaid({0}): rent payment successfully collected {clearedInventoryStatus}from SlumLord!", playerHouse.PlayerName); // re-add item to queue AddRentQueue(player, playerHouse.House); var onlinePlayer = PlayerManager.GetOnlinePlayer(playerHouse.PlayerGuid); if (onlinePlayer != null) { var actionChain = new ActionChain(); actionChain.AddDelaySeconds(3.0f); // wait for slumlord inventory biotas above to save actionChain.AddAction(onlinePlayer, onlinePlayer.HandleActionQueryHouse); actionChain.EnqueueChain(); } } /// /// Handles the eviction process for a player house /// private static void HandleEviction(PlayerHouse playerHouse, bool force = false) { HandleEviction(playerHouse.House, playerHouse.PlayerGuid, false, force); } /// /// Handles the eviction process for a player house /// public static void HandleEviction(House house, uint playerGuid, bool multihouse = false, bool force = false) { // clear out slumlord inventory var slumlord = house.SlumLord; slumlord.ClearInventory(); var player = PlayerManager.FindByGuid(playerGuid, out bool isOnline); if (!PropertyManager.GetBool("house_rent_enabled", true).Item && !multihouse && !force) { // rent disabled, push forward var purchaseTime = (uint)(player.HousePurchaseTimestamp ?? 0); var nextRentTime = house.GetRentDue(purchaseTime); player.HouseRentTimestamp = (int)nextRentTime; log.InfoFormat("[HOUSE] HouseManager.HandleEviction({0}): house rent disabled via config", player.Name); // re-add item to queue AddRentQueue(player, house); return; } // handle eviction house.HouseOwner = null; house.MonarchId = null; house.HouseOwnerName = null; house.ClearPermissions(); house.SaveBiotaToDatabase(); // relink house.UpdateLinks(); if (house.HasDungeon) { var dungeonHouse = house.GetDungeonHouse(); if (dungeonHouse != null) dungeonHouse.UpdateLinks(); } // player slumlord 'off' animation slumlord.Off(); // reset slumlord name slumlord.SetAndBroadcastName(); slumlord.SaveBiotaToDatabase(); HouseList.AddToAvailable(slumlord, house); // if evicting a multihouse owner's previous house, // no update for player properties if (player.HouseInstance == house.Guid.Full) { player.HouseId = null; player.HouseInstance = null; //player.HousePurchaseTimestamp = null; player.HouseRentTimestamp = null; } else log.WarnFormat("[HOUSE] HouseManager.HandleRentEviction({0}, {1}, {2}): house guids don't match {3}", house.Guid, player.Name, multihouse, player.HouseInstance); house.ClearRestrictions(); log.InfoFormat("[HOUSE] HouseManager.HandleRentEviction({0})", player.Name); if (multihouse) { RemoveRentQueue(house.Guid.Full); DecrementTotalOwnedHousingByType(house.HouseType); player.SaveBiotaToDatabase(); return; } if (!isOnline) { // inform player of eviction when they log in var offlinePlayer = PlayerManager.GetOfflinePlayer(playerGuid); if (offlinePlayer == null) { log.WarnFormat("[HOUSE] {0}.HandleEviction(): couldn't find offline player", player.Name); return; } offlinePlayer.SetProperty(PropertyBool.HouseEvicted, true); offlinePlayer.SaveBiotaToDatabase(); return; } var onlinePlayer = PlayerManager.GetOnlinePlayer(playerGuid); onlinePlayer.House = null; // send text message onlinePlayer.Session.Network.EnqueueSend(new GameMessageSystemChat("Your house has reverted due to non-payment of the maintenance costs. All items stored in the house have been lost.", ChatMessageType.Broadcast)); onlinePlayer.RemoveDeed(); onlinePlayer.SaveBiotaToDatabase(); // clear house panel for online player var actionChain = new ActionChain(); actionChain.AddDelaySeconds(3.0f); // wait for slumlord inventory biotas above to save actionChain.AddAction(onlinePlayer, onlinePlayer.HandleActionQueryHouse); actionChain.EnqueueChain(); } /// /// Returns TRUE if the slumlord contains all of the required items for rent payment /// private static bool IsRentPaid(PlayerHouse playerHouse) { var houseData = GetHouseData(playerHouse.House); foreach (var rentItem in houseData.Rent) { if (rentItem.Paid < rentItem.Num) { log.Info($"[HOUSE] {playerHouse.PlayerName}.IsRentPaid() - required {rentItem.Num:N0}x {(rentItem.Num > 1 ? $"{rentItem.PluralName}" : $"{rentItem.Name}")} ({rentItem.WeenieID}), found {rentItem.Paid:N0}"); return false; } } return true; } /// /// Returns TRUE if a player currently meets all of the requirements for owning their house (allegiance rank) /// private static bool HasRequirements(PlayerHouse playerHouse) { if (!PropertyManager.GetBool("house_purchase_requirements").Item) return true; var slumlord = playerHouse.House.SlumLord; if (slumlord.AllegianceMinLevel == null) return true; var allegianceMinLevel = PropertyManager.GetLong("mansion_min_rank", -1).Item; if (allegianceMinLevel == -1) allegianceMinLevel = slumlord.AllegianceMinLevel.Value; var player = PlayerManager.FindByGuid(playerHouse.PlayerGuid); if (player == null) { log.WarnFormat("[HOUSE] {0}.HasRequirements() - couldn't find player", playerHouse.PlayerName); return false; } // ensure allegiance is loaded var allegiance = AllegianceManager.GetAllegiance(player); AllegianceNode allegianceNode = null; if (allegiance != null) allegiance.Members.TryGetValue(player.Guid, out allegianceNode); var rank = allegianceNode != null ? allegianceNode.Rank : 0; if (allegianceMinLevel > 0 && (allegiance == null || rank < allegianceMinLevel)) { log.InfoFormat("[HOUSE] {0}.HasRequirements() - allegiance rank {1} < {2}", playerHouse.PlayerName, rank, allegianceMinLevel); return false; } return true; } /// /// Returns the HouseData structure for a House (rent and paid items) /// private static HouseData GetHouseData(House house) { var houseData = new HouseData(); houseData.SetRentItems(house.SlumLord.GetRentItems()); houseData.SetPaidItems(house.SlumLord); if (house.HouseStatus == HouseStatus.InActive) houseData.MaintenanceFree = true; return houseData; } // This function is called from a database callback. // We must add thread safety to prevent HouseManager corruption public static void HandlePlayerDelete(uint playerGuid) { WorldManager.EnqueueAction(new ActionEventDelegate(() => DoHandlePlayerDelete(playerGuid))); } /// /// Called on character delete, evicts from house /// private static void DoHandlePlayerDelete(uint playerGuid) { var player = PlayerManager.FindByGuid(playerGuid); if (player == null) { Console.WriteLine($"HouseManager.HandlePlayerDelete({playerGuid:X8}): couldn't find player guid"); return; } if (player.HouseInstance == null) return; var playerHouse = FindPlayerHouse(playerGuid); if (playerHouse == null) return; // load the most up-to-date copy of house data GetHouse(playerHouse.House.Guid.Full, (house) => { playerHouse.House = house; HandleEviction(playerHouse, true); RemoveRentQueue(house.Guid.Full); DecrementTotalOwnedHousingByType(house.HouseType); }); } /// /// Returns the house in the rent queue for a player guid, if exists /// private static PlayerHouse FindPlayerHouse(uint playerGuid) { return RentQueue.FirstOrDefault(i => i.PlayerGuid == playerGuid); } /// /// Returns all of the houses in the rent queue for a house id /// public static List GetHouseById(uint houseId) { return RentQueue.Where(i => i.House.HouseId == houseId).Select(i => i.House).ToList(); } /// /// Returns all of the houses in the rent queue for an account /// public static List GetAccountHouses(uint accountId) { return RentQueue.Where(i => i.AccountId == accountId).Select(i => i.House).ToList(); } /// /// Returns all of the houses in the rent queue for a character /// public static List GetCharacterHouses(uint playerGuid) { return RentQueue.Where(i => i.PlayerGuid == playerGuid).Select(i => i.House).ToList(); } /// /// Returns the house guid for a slumlord guid /// private static uint GetHouseGuid(uint slumlord_guid, List house_guids) { var slumlord_prefix = slumlord_guid >> 12; return house_guids.FirstOrDefault(i => slumlord_prefix == (i >> 12)); } /// /// If the landblock is loaded, return a reference to the current House object /// else return a copy of the House biota from the latest info in the db /// /// called when the slumlord inventory is fully loaded public static void GetHouse(uint houseGuid, Action callback) { var landblock = (ushort)((houseGuid >> 12) & 0xFFFF); var landblockId = new LandblockId((uint)(landblock << 16 | 0xFFFF)); var isLoaded = LandblockManager.IsLoaded(landblockId); if (!isLoaded) { // landblock is unloaded // return a copy of the House biota from the latest info in the db var houseBiota = House.Load(houseGuid); RegisterCallback(houseBiota, callback); return; } // landblock is loaded, return a reference to the current House object var loaded = LandblockManager.GetLandblock(landblockId, false); var house = loaded.GetObject(new ObjectGuid(houseGuid)) as House; if (house != null && house.SlumLord != null) { if (!house.SlumLord.InventoryLoaded) RegisterCallback(house, callback); else callback(house); } else if (!loaded.CreateWorldObjectsCompleted) { var houseBiota = House.Load(houseGuid); RegisterCallback(houseBiota, callback); } else log.ErrorFormat("[HOUSE] HouseManager.GetHouse({0:X8}): couldn't find house on loaded landblock", houseGuid); } /// /// Registers a callback to run when the slumlord inventory has been loaded /// public static void RegisterCallback(House house, Action callback) { if (!SlumlordCallbacks.TryGetValue(house.SlumLord.Guid.Full, out var callbacks)) { callbacks = new List(); SlumlordCallbacks.Add(house.SlumLord.Guid.Full, callbacks); } callbacks.Add(new HouseCallback(house, callback)); } /// /// Runs any pending HouseManager callbacks for this slumlord house /// public static void OnInitialInventoryLoadCompleted(SlumLord slumlord) { //Console.WriteLine($"HouseManager.OnInitialInventoryLoadCompleted({slumlord.Name})"); if (SlumlordCallbacks.TryGetValue(slumlord.Guid.Full, out var callbacks)) { foreach (var callback in callbacks) callback.Run(); SlumlordCallbacks.Remove(slumlord.Guid.Full); } } /// /// A mapping of apartment landblocks => apartment complex names /// public static Dictionary ApartmentBlocks = new Dictionary() { // currently used for apartment deeds { 0x5360, "Sanctum Residential Halls - Alvan Court" }, { 0x5361, "Sanctum Residential Halls - Caerna Dwellings" }, { 0x5362, "Sanctum Residential Halls - Illsin Veranda" }, { 0x5363, "Sanctum Residential Halls - Marin Court" }, { 0x5364, "Sanctum Residential Halls - Ruadnar Court" }, { 0x5365, "Sanctum Residential Halls - Senmai Court" }, { 0x5366, "Sanctum Residential Halls - Sigil Veranda" }, { 0x5367, "Sanctum Residential Halls - Sorveya Court" }, { 0x5368, "Sanctum Residential Halls - Sylvan Dwellings" }, { 0x5369, "Sanctum Residential Halls - Treyval Veranda" }, { 0x7200, "Atrium Residential Halls - Winthur Gate" }, { 0x7300, "Atrium Residential Halls - Larkspur Gardens" }, { 0x7400, "Atrium Residential Halls - Mellas Court" }, { 0x7500, "Atrium Residential Halls - Vesper Gate" }, { 0x7600, "Atrium Residential Halls - Gajin Dwellings" }, { 0x7700, "Atrium Residential Halls - Valorya Gate" }, { 0x7800, "Atrium Residential Halls - Heartland Yard" }, { 0x7900, "Atrium Residential Halls - Ivory Gate" }, { 0x7A00, "Atrium Residential Halls - Alphas Court" }, { 0x7B00, "Atrium Residential Halls - Hasina Gardens" }, { 0x7C00, "Oriel Residential Halls - Sorac Gate" }, { 0x7D00, "Oriel Residential Halls - Maru Veranda" }, { 0x7E00, "Oriel Residential Halls - Forsythian Gardens" }, { 0x7F00, "Oriel Residential Halls - Vindalan Dwellings" }, { 0x8000, "Oriel Residential Halls - Syrah Dwellings" }, { 0x8100, "Oriel Residential Halls - Allain Court" }, { 0x8200, "Oriel Residential Halls - White Lotus Gate" }, { 0x8300, "Oriel Residential Halls - Autumn Moon Gardens" }, { 0x8400, "Oriel Residential Halls - Trellyn Gardens" }, { 0x8500, "Oriel Residential Halls - Endara Gate" }, { 0x8600, "Haven Residential Halls - Celcynd Grotto" }, { 0x8700, "Haven Residential Halls - Trothyr Hollow" }, { 0x8800, "Haven Residential Halls - Jojii Gardens" }, { 0x8900, "Haven Residential Halls - Cedraic Court" }, { 0x8A00, "Haven Residential Halls - Ben Ten Lodge" }, { 0x8B00, "Haven Residential Halls - Dulok Court" }, { 0x8C00, "Haven Residential Halls - Crescent Moon Veranda" }, { 0x8D00, "Haven Residential Halls - Jade Gate" }, { 0x8E00, "Haven Residential Halls - Ispar Yard" }, { 0x8F00, "Haven Residential Halls - Xao Wu Gardens" }, { 0x9000, "Victory Residential Halls - Accord Veranda" }, { 0x9100, "Victory Residential Halls - Candeth Court" }, { 0x9200, "Victory Residential Halls - Celdiseth Court" }, { 0x9300, "Victory Residential Halls - Festivus Court" }, { 0x9400, "Victory Residential Halls - Hibiscus Gardens" }, { 0x9500, "Victory Residential Halls - Meditation Gardens" }, { 0x9600, "Victory Residential Halls - Setera Gardens" }, { 0x9700, "Victory Residential Halls - Spirit Gate" }, { 0x9800, "Victory Residential Halls - Triumphal Gardens" }, { 0x9900, "Victory Residential Halls - Wilamil Court" }, }; /// /// Pay rent for a house /// private static void PayRent(PlayerHouse playerHouse) { // load the most up-to-date copy of the house data GetHouse(playerHouse.House.Guid.Full, (house) => { playerHouse.House = house; var isPaid = IsRentPaid(playerHouse) || playerHouse.House.HouseStatus <= HouseStatus.InActive; if (!isPaid) { var houseData = GetHouseData(playerHouse.House); foreach (var rentItem in houseData.Rent) { if (rentItem.Paid < rentItem.Num) { var amountLeftToPay = rentItem.Num - rentItem.Paid; while (amountLeftToPay > 0) { var payment = WorldObjectFactory.CreateNewWorldObject(rentItem.WeenieID); if (payment == null) { log.Error($"[HOUSE] HouseManager.PayRent({house.Guid}): couldn't create payment for WCID {rentItem.WeenieID}"); return; } payment.SetStackSize(Math.Min(amountLeftToPay, payment.MaxStackSize ?? 1)); if (!house.SlumLord.TryAddToInventory(payment)) { log.Error($"[HOUSE] HouseManager.PayRent({house.Guid}): couldn't place {payment.Name} (0x{payment.Guid}) in SlumLord's Inventory"); return; } amountLeftToPay -= (payment.StackSize ?? 1); } } } house.SlumLord.MergeAllStackables(); foreach (var item in house.SlumLord.Inventory.Values) item.SaveBiotaToDatabase(); house.SlumLord.SaveBiotaToDatabase(); var onlinePlayer = PlayerManager.GetOnlinePlayer(playerHouse.PlayerGuid); if (onlinePlayer != null) { var actionChain = new ActionChain(); actionChain.AddDelaySeconds(3.0f); // wait for slumlord inventory biotas above to save actionChain.AddAction(onlinePlayer, onlinePlayer.HandleActionQueryHouse); actionChain.EnqueueChain(); } log.InfoFormat("[HOUSE] HouseManager.PayRent({0}): fully paid rent into SlumLord.", house.Guid); } }); } /// /// Pay rent for a house /// public static bool PayRent(House house) { var foundHouse = RentQueue.FirstOrDefault(h => h.PlayerGuid == (house.HouseOwner ?? 0)); if (foundHouse == null) return false; PayRent(foundHouse); return true; } /// /// Pay rent for all owned housing /// public static void PayAllRent() { foreach (var house in RentQueue) { PayRent(house); } } public static int TotalOwnedHousing => RentQueue?.Count ?? 0; public static Dictionary TotalOwnedHousingByType = new Dictionary(); public static void IncrementTotalOwnedHousingByType(HouseType houseType) => TotalOwnedHousingByType[houseType]++; public static void DecrementTotalOwnedHousingByType(HouseType houseType) => TotalOwnedHousingByType[houseType]--; } }