internal static void Open(Player player) { WorldStatus = WorldStatusState.Open; PlayerManager.BroadcastToAuditChannel(player, "World is now open"); }
/// <summary> /// Threaded task created when performing a server shutdown /// </summary> private static void ShutdownServer() { var shutdownTime = DateTime.UtcNow.AddSeconds(ShutdownInterval); ShutdownTime = shutdownTime; var lastNoticeTime = DateTime.UtcNow; // wait for shutdown interval to expire while (shutdownTime != DateTime.MinValue && shutdownTime >= DateTime.UtcNow) { // this allows the server shutdown to be canceled if (!ShutdownInitiated) { // reset shutdown details string shutdownText = $"The server has canceled the shutdown procedure @ {DateTime.UtcNow} UTC"; log.Info(shutdownText); // special text foreach (var player in PlayerManager.GetAllOnline()) { player.Session.WorldBroadcast(shutdownText); } // break function return; } lastNoticeTime = NotifyPlayersOfPendingShutdown(lastNoticeTime, shutdownTime.AddSeconds(1)); Thread.Sleep(10); } ShutdownInProgress = true; PropertyManager.ResyncVariables(); PropertyManager.StopUpdating(); WorldManager.EnqueueAction(new ActionEventDelegate(() => { log.Debug("Logging off all players..."); // logout each player foreach (var player in PlayerManager.GetAllOnline()) { player.Session.LogOffPlayer(true); } })); // Wait for all players to log out var logUpdateTS = DateTime.MinValue; int playerCount; while ((playerCount = PlayerManager.GetOnlineCount()) > 0) { logUpdateTS = LogStatusUpdate(logUpdateTS, $"Waiting for {playerCount} player{(playerCount > 1 ? "s" : "")} to log off..."); Thread.Sleep(10); } WorldManager.EnqueueAction(new ActionEventDelegate(() => { log.Debug("Disconnecting all sessions..."); // disconnect each session NetworkManager.DisconnectAllSessionsForShutdown(); })); // Wait for all sessions to drop out logUpdateTS = DateTime.MinValue; int sessionCount; while ((sessionCount = NetworkManager.GetSessionCount()) > 0) { logUpdateTS = LogStatusUpdate(logUpdateTS, $"Waiting for {sessionCount} session{(sessionCount > 1 ? "s" : "")} to disconnect..."); Thread.Sleep(10); } log.Debug("Adding all landblocks to destruction queue..."); // Queue unloading of all the landblocks // The actual unloading will happen in WorldManager.UpdateGameWorld LandblockManager.AddAllActiveLandblocksToDestructionQueue(); // Wait for all landblocks to unload logUpdateTS = DateTime.MinValue; int landblockCount; while ((landblockCount = LandblockManager.GetLoadedLandblocks().Count) > 0) { logUpdateTS = LogStatusUpdate(logUpdateTS, $"Waiting for {landblockCount} loaded landblock{(landblockCount > 1 ? "s" : "")} to unload..."); Thread.Sleep(10); } log.Debug("Stopping world..."); // Disabled thread update loop WorldManager.StopWorld(); // Wait for world to end logUpdateTS = DateTime.MinValue; while (WorldManager.WorldActive) { logUpdateTS = LogStatusUpdate(logUpdateTS, "Waiting for world to stop..."); Thread.Sleep(10); } log.Info("Saving OfflinePlayers that have unsaved changes..."); PlayerManager.SaveOfflinePlayersWithChanges(); // Wait for the database queue to empty logUpdateTS = DateTime.MinValue; int shardQueueCount; while ((shardQueueCount = DatabaseManager.Shard.QueueCount) > 0) { logUpdateTS = LogStatusUpdate(logUpdateTS, $"Waiting for database queue ({shardQueueCount}) to empty..."); Thread.Sleep(10); } // Write exit to console/log log.Info($"Exiting at {DateTime.UtcNow}"); // System exit Environment.Exit(Environment.ExitCode); }
public static void HandlePlayerDelete(uint playerGuid) { var player = PlayerManager.FindByGuid(playerGuid); if (player == null) { Console.WriteLine($"AllegianceManager.HandlePlayerDelete({playerGuid:X8}): couldn't find player guid"); return; } var allegiance = GetAllegiance(player); if (allegiance == null) { return; } allegiance.Members.TryGetValue(player.Guid, out var allegianceNode); var players = new List <IPlayer>() { player }; if (player.PatronId != null) { var patron = PlayerManager.FindByGuid(player.PatronId.Value); if (patron != null) { players.Add(patron); } } player.PatronId = null; player.MonarchId = null; // vassals now become monarchs... foreach (var vassal in allegianceNode.Vassals.Values) { var vassalPlayer = PlayerManager.FindByGuid(vassal.PlayerGuid, out bool isOnline); if (vassalPlayer == null) { continue; } vassalPlayer.PatronId = null; vassalPlayer.MonarchId = null; players.Add(vassal.Player); } RemoveCache(allegiance); // rebuild for those directly involved foreach (var p in players) { Rebuild(GetAllegiance(p)); } foreach (var p in players) { LoadPlayer(p); } foreach (var p in players) { HandleNoAllegiance(p); } // save immediately? foreach (var p in players) { var offline = PlayerManager.GetOfflinePlayer(p.Guid); if (offline != null) { offline.SaveBiotaToDatabase(); } else { var online = PlayerManager.GetOnlinePlayer(p.Guid); if (online != null) { online.SaveBiotaToDatabase(); } } } }
/// <summary> /// Manages updating all entities on the world. /// - Server-side command-line commands are handled in their own thread. /// - Database I/O is handled in its own thread. /// - Network commands come from their own listener threads, and are queued for each sessions which are then processed here. /// - This thread does the rest of the work! /// </summary> private static void UpdateWorld() { log.DebugFormat("Starting UpdateWorld thread"); WorldActive = true; var worldTickTimer = new Stopwatch(); while (!pendingWorldStop) { /* * When it comes to thread safety for Landblocks and WorldObjects, ACE makes the following assumptions: * * Inbound ClientMessages and GameActions are handled on the main UpdateWorld thread. * - These actions may load Landblocks and modify other WorldObjects safely. * * PlayerEnterWorld queue is run on the main UpdateWorld thread. * - These actions may load Landblocks and modify other WorldObjects safely. * * Landblock Groups (calculated by LandblockManager) can be processed in parallel. * * Adjacent Landblocks will always be run on the same thread. * * Non-adjacent landblocks might be run on different threads. * - If two non-adjacent landblocks both touch the same landblock, and that landblock is active, they will be run on the same thread. * * Database results are returned from a task spawned in SerializedShardDatabase (via callback). * - Minimal processing should be done from the callback. Return as quickly as possible to let the database thread do database work. * - The processing of these results should be queued to an ActionQueue * * The only cases where it's acceptable for to create a new Task, Thread or Parallel loop are the following: * - Every scenario must be one where you don't care about breaking ACE * - DeveloperCommand Handlers */ worldTickTimer.Restart(); ServerPerformanceMonitor.RestartEvent(ServerPerformanceMonitor.MonitorType.PlayerManager_Tick); PlayerManager.Tick(); ServerPerformanceMonitor.RegisterEventEnd(ServerPerformanceMonitor.MonitorType.PlayerManager_Tick); ServerPerformanceMonitor.RestartEvent(ServerPerformanceMonitor.MonitorType.NetworkManager_InboundClientMessageQueueRun); NetworkManager.InboundMessageQueue.RunActions(); ServerPerformanceMonitor.RegisterEventEnd(ServerPerformanceMonitor.MonitorType.NetworkManager_InboundClientMessageQueueRun); // This will consist of PlayerEnterWorld actions, as well as other game world actions that require thread safety ServerPerformanceMonitor.RestartEvent(ServerPerformanceMonitor.MonitorType.actionQueue_RunActions); actionQueue.RunActions(); ServerPerformanceMonitor.RegisterEventEnd(ServerPerformanceMonitor.MonitorType.actionQueue_RunActions); ServerPerformanceMonitor.RestartEvent(ServerPerformanceMonitor.MonitorType.DelayManager_RunActions); DelayManager.RunActions(); ServerPerformanceMonitor.RegisterEventEnd(ServerPerformanceMonitor.MonitorType.DelayManager_RunActions); ServerPerformanceMonitor.RestartEvent(ServerPerformanceMonitor.MonitorType.UpdateGameWorld); var gameWorldUpdated = UpdateGameWorld(); ServerPerformanceMonitor.RegisterEventEnd(ServerPerformanceMonitor.MonitorType.UpdateGameWorld); ServerPerformanceMonitor.RestartEvent(ServerPerformanceMonitor.MonitorType.NetworkManager_DoSessionWork); int sessionCount = NetworkManager.DoSessionWork(); ServerPerformanceMonitor.RegisterEventEnd(ServerPerformanceMonitor.MonitorType.NetworkManager_DoSessionWork); ServerPerformanceMonitor.Tick(); // We only relax the CPU if our game world is able to update at the target rate. // We do not sleep if our game world just updated. This is to prevent the scenario where our game world can't keep up. We don't want to add further delays. // If our game world is able to keep up, it will not be updated on most ticks. It's on those ticks (between updates) that we will relax the CPU. if (!gameWorldUpdated) { Thread.Sleep(sessionCount == 0 ? 10 : 1); // Relax the CPU more if no sessions are connected } Timers.PortalYearTicks += worldTickTimer.Elapsed.TotalSeconds; } // World has finished operations and concedes the thread to garbage collection WorldActive = false; }
public static void PassXP(AllegianceNode vassalNode, ulong amount, bool direct) { // http://asheron.wikia.com/wiki/Allegiance_Experience // Pre-patch: // Vassal-to-patron pass-up has no effective cap, but patron-to-grandpatron pass-up caps with an effective Loyalty of 175. // If you're sworn for 10 days to the same patron, the base Loyalty required to maximize the pass-up from your vassal through you to your patron is only 88. // Take into account level 7 enchantments, and you can see how there's practically no need to spend XP on the skill, due to the ease in reaching the cap. // Leadership is arguably worse off. In theory, you need to train Leadership and spend XP on it in order to get maximum results. // However, effective Leadership is multiplied by two factors: how many vassals you have, and how long they have been sworn to you, with the emphasis on the number of vassals you have. // The effect of Leadership on pass-up caps at around 165 effective Leadership, or 83 base Leadership before the modifier. // The end result of this is that if you have two active vassals and you can get 10 mules sworn underneath you for an average of 5 in-game days, // you never need to raise your Leadership beyond 83. Again, take into account level 7 enchantments and you can see why few people even bother training the skill. It's just too easy to reach the cap. // Post-patch: // - Leadership and Loyalty are not based on Self attribute // - Effective Loyalty is still modified by the time you have spent sworn to your patron // - Effective Leadership is still modified by number of vassals and average time that they have been sworn to you, // but the emphasis is on the "time sworn" side and not on the "number of vassals" side. In fact, the vassals // required to achieve the maximum benefit has been decreased from 12 to 4. This is to reduce the incentive of having non-playing vassals. // - For both Loyalty and Leadership, the time sworn modifier will now be a factor of both in-game time and real time. // - Most importantly, Leadership and Loyalty no longer "cap" // XP pass-up: // - New minimums and maximums // - Vassal-to-patron pass-up will have a minimum of 25% of earned XP, and a maximum of 90% of earned XP. // Under the old system, the minimum was about 9% of earned XP, and the effective maximum was somewhere near 44% of earned XP. // - Patron-to-grandpatron pass-up will have a minimum of 0% of XP passed-up by the patron's vassal, and a maximum of 10% of passed-up XP. // Under the old system, the minimum was about 30% and the maximum was about 94%. // Original system: up to January 12, 2004 // Follow-up: all XP instead of just kill XP: October 2009 // Formulas: // http://asheron.wikia.com/wiki/XP_Passup // Thanks for Xerxes of Thistledown, who verified accuracy over four months of testing and research! // Generated % - Percentage of XP passed to the patron through the vassal's earned XP (hunting and most quests). // Received % - Percentage of XP that patron will receive from his vassal's Generated XP. // Passup % - Percentage of XP actually received by patron from vassal's earned XP (hunting and most quests). // Generated % = 50.0 + 22.5 * (loyalty / 291) * (1.0 + (RT/730) * (IG/720)) // Received % = 50.0 + 22.5 * (leadership / 291) * (1.0 + V * (RT2/730) * (IG2/720)) // Passup % = Generated% * Received% / 100.0 // Where: // Loyalty = Buffed Loyalty (291 max) // Leadership = Buffed Leadership (291 max) // RT = actual real time sworn to patron in days (730 max) // IG = actual in-game time sworn to patron in hours (720 max) // RT2 = average real time sworn to patron for all vassals in days (730 max) // IG2 = average in-game time sworn to patron for all vassals in hours (720 max) // V = vassal factor(1 = 0.25, 2 = 0.50, 3 = 0.75, 4 + = 1.00) (1.0 max) var patronNode = vassalNode.Patron; if (patronNode == null) { return; } var vassal = vassalNode.Player; var patron = patronNode.Player; var loyalty = Math.Min(vassal.GetCurrentLoyalty(), SkillCap); var leadership = Math.Min(patron.GetCurrentLeadership(), SkillCap); var timeReal = Math.Min(RealCap, RealCap); var timeGame = Math.Min(GameCap, GameCap); var timeRealAvg = Math.Min(RealCap, RealCap); var timeGameAvg = Math.Min(GameCap, GameCap); var vassalFactor = Math.Min(0.25f * patronNode.TotalVassals, 1.0f); var factor1 = direct ? 50.0f : 16.0f; var factor2 = direct ? 22.5f : 8.0f; var generated = (factor1 + factor2 * (loyalty / SkillCap) * (1.0f + (timeReal / RealCap) * (timeGame / GameCap))) * 0.01f; var received = (factor1 + factor2 * (leadership / SkillCap) * (1.0f + vassalFactor * (timeRealAvg / RealCap) * (timeGameAvg / GameCap))) * 0.01f; var passup = generated * received; var generatedAmount = (uint)(amount * generated); var passupAmount = (uint)(amount * passup); /*Console.WriteLine("---"); * Console.WriteLine("AllegianceManager.PassXP(" + amount + ")"); * Console.WriteLine("Vassal: " + vassal.Name); * Console.WriteLine("Patron: " + patron.Name); * * Console.WriteLine("Generated: " + Math.Round(generated * 100, 2) + "%"); * Console.WriteLine("Received: " + Math.Round(received * 100, 2) + "%"); * Console.WriteLine("Passup: " + Math.Round(passup * 100, 2) + "%"); * * Console.WriteLine("Generated amount: " + generatedAmount); * Console.WriteLine("Passup amount: " + passupAmount);*/ if (passupAmount > 0) { //vassal.CPTithed += generatedAmount; //patron.CPCached += passupAmount; //patron.CPPoolToUnload += passupAmount; vassal.AllegianceXPGenerated += generatedAmount; patron.AllegianceXPCached += passupAmount; var onlinePatron = PlayerManager.GetOnlinePlayer(patron.Guid); if (onlinePatron != null) { onlinePatron.AddAllegianceXP(); } // call recursively PassXP(patronNode, passupAmount, false); } }
/// <summary> /// Returns a list of all players under a monarch /// </summary> public static List <IPlayer> FindAllPlayers(ObjectGuid monarchGuid) { return(PlayerManager.FindAllByMonarch(monarchGuid)); }
/// <summary> /// Handles the eviction process for a player house /// </summary> public static void HandleEviction(House house, uint playerGuid, bool multihouse = false) { // clear out slumlord inventory var slumlord = house.SlumLord; slumlord.ClearInventory(true); var player = PlayerManager.FindByGuid(playerGuid, out bool isOnline); if (!PropertyManager.GetBool("house_rent_enabled", true).Item&& !multihouse) { // rent disabled, push forward var purchaseTime = (uint)(player.HousePurchaseTimestamp ?? 0); var nextRentTime = house.GetRentDue(purchaseTime); player.HouseRentTimestamp = (int)nextRentTime; log.Info($"HouseManager.HandleRentPaid({player.Name}): house rent disabled via config"); // 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(); // player slumlord 'off' animation var off = new Motion(MotionStance.Invalid, MotionCommand.Off); slumlord.CurrentMotionState = off; slumlord.EnqueueBroadcastMotion(off); // reset slumlord name var weenie = DatabaseManager.World.GetCachedWeenie(slumlord.WeenieClassId); var wo = WorldObjectFactory.CreateWorldObject(weenie, ObjectGuid.Invalid); slumlord.Name = wo.Name; slumlord.EnqueueBroadcast(new GameMessagePublicUpdatePropertyString(slumlord, PropertyString.Name, wo.Name)); slumlord.SaveBiotaToDatabase(); // 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.Warn($"HouseManager.HandleRentEviction({house.Guid}, {player.Name}, {multihouse}): house guids don't match {player.HouseInstance}"); } house.ClearRestrictions(); log.Info($"HouseManager.HandleRentEviction({player.Name})"); if (multihouse) { RemoveRentQueue(house.Guid.Full); player.SaveBiotaToDatabase(); return; } if (!isOnline) { // inform player of eviction when they log in var offlinePlayer = PlayerManager.GetOfflinePlayer(playerGuid); if (offlinePlayer == null) { log.Warn($"{player.Name}.HandleEviction(): couldn't find offline player"); 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("You abandon your house!", 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(); }
/// <summary> /// Queries the status of multi-house owners on the server /// </summary> private static void QueryMultiHouse() { var slumlordBiotas = DatabaseManager.Shard.GetBiotasByType(WeenieType.SlumLord); var playerHouses = new Dictionary <IPlayer, List <Biota> >(); var accountHouses = new Dictionary <string, List <Biota> >(); 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 <Biota>(); 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 <Biota>(); 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])}"); } } } }
public static async void HandleEviction(PlayerHouse playerHouse) { // todo: copied from Player_House.HandleActionAbandonHouse, move to House.Abandon() // todo: get online copy of house var house = playerHouse.House; // clear out slumlord inventory // todo: get online copy of house var slumlord = house.SlumLord; slumlord.ClearInventory(true); var player = PlayerManager.FindByGuid(playerHouse.PlayerGuid, out bool isOnline); if (!PropertyManager.GetBool("house_rent_enabled", true).Item) { // rent disabled, push forward var purchaseTime = (uint)(player.HousePurchaseTimestamp ?? 0); var nextRentTime = house.GetRentDue(purchaseTime); player.HouseRentTimestamp = (int)nextRentTime; log.Info($"HouseManager.HandleRentPaid({playerHouse.PlayerName}): house rent disabled via config"); BuildRentQueue(); return; } house.HouseOwner = null; house.MonarchId = null; house.HouseOwnerName = null; house.ClearPermissions(); house.SaveBiotaToDatabase(); // relink house.UpdateLinks(); // player slumlord 'off' animation var off = new Motion(MotionStance.Invalid, MotionCommand.Off); slumlord.CurrentMotionState = off; slumlord.EnqueueBroadcastMotion(off); // reset slumlord name var weenie = DatabaseManager.World.GetCachedWeenie(slumlord.WeenieClassId); var wo = WorldObjectFactory.CreateWorldObject(weenie, ObjectGuid.Invalid); slumlord.Name = wo.Name; slumlord.EnqueueBroadcast(new GameMessagePublicUpdatePropertyString(slumlord, PropertyString.Name, wo.Name)); slumlord.SaveBiotaToDatabase(); player.HouseId = null; player.HouseInstance = null; //player.HousePurchaseTimestamp = null; player.HouseRentTimestamp = null; house.ClearRestrictions(); log.Info($"HouseManager.HandleRentEviction({playerHouse.PlayerName})"); BuildRentQueue(); if (!isOnline) { // inform player of eviction when they log in var offlinePlayer = PlayerManager.GetOfflinePlayer(playerHouse.PlayerGuid); if (offlinePlayer == null) { log.Warn($"{playerHouse.PlayerName}.HandleEviction(): couldn't find offline player"); return; } offlinePlayer.SetProperty(PropertyBool.HouseEvicted, true); offlinePlayer.SaveBiotaToDatabase(); return; } var onlinePlayer = PlayerManager.GetOnlinePlayer(playerHouse.PlayerGuid); onlinePlayer.House = null; // send text message onlinePlayer.Session.Network.EnqueueSend(new GameMessageSystemChat("You abandon your house!", ChatMessageType.Broadcast)); onlinePlayer.RemoveDeed(); await Task.Delay(3000); // wait for slumlord inventory biotas above to save onlinePlayer.HandleActionQueryHouse(); }