private static void DoPlayerEnterWorld(Session session, Character character, Biota playerBiota, PossessedBiotas possessedBiotas) { Player player; if (playerBiota.WeenieType == (int)WeenieType.Admin) { player = new Admin(playerBiota, possessedBiotas.Inventory, possessedBiotas.WieldedItems, character, session); } else if (playerBiota.WeenieType == (int)WeenieType.Sentinel) { player = new Sentinel(playerBiota, possessedBiotas.Inventory, possessedBiotas.WieldedItems, character, session); } else { player = new Player(playerBiota, possessedBiotas.Inventory, possessedBiotas.WieldedItems, character, session); } session.SetPlayer(player); session.Player.PlayerEnterWorld(); if (character.TotalLogins <= 1 || PropertyManager.GetBool("alwaysshowwelcome").Item) { // check the value of the welcome message. Only display it if it is not empty string welcomeHeader = !string.IsNullOrEmpty(ConfigManager.Config.Server.Welcome) ? ConfigManager.Config.Server.Welcome : "Welcome to Asheron's Call!"; string msg = "To begin your training, speak to the Society Greeter. Walk up to the Society Greeter using the 'W' key, then double-click on her to initiate a conversation."; session.Network.EnqueueSend(new GameEventPopupString(session, $"{welcomeHeader}\n{msg}")); } LandblockManager.AddObject(session.Player, true); var motdString = PropertyManager.GetString("motd_string").Item; session.Network.EnqueueSend(new GameMessageSystemChat(motdString, ChatMessageType.Broadcast)); }
private static void DoPlayerEnterWorld(Session session, Character character, Biota playerBiota, PossessedBiotas possessedBiotas) { Player player; Player.HandleNoLogLandblock(playerBiota, out var playerLoggedInOnNoLogLandblock); var stripAdminProperties = false; var addAdminProperties = false; var addSentinelProperties = false; if (ConfigManager.Config.Server.Accounts.OverrideCharacterPermissions) { if (session.AccessLevel <= AccessLevel.Advocate) // check for elevated characters { if (playerBiota.WeenieType == WeenieType.Admin || playerBiota.WeenieType == WeenieType.Sentinel) // Downgrade weenie { character.IsPlussed = false; playerBiota.WeenieType = WeenieType.Creature; stripAdminProperties = true; } } else if (session.AccessLevel >= AccessLevel.Sentinel && session.AccessLevel <= AccessLevel.Envoy) { if (playerBiota.WeenieType == WeenieType.Creature || playerBiota.WeenieType == WeenieType.Admin) // Up/downgrade weenie { character.IsPlussed = true; playerBiota.WeenieType = WeenieType.Sentinel; addSentinelProperties = true; } } else // Developers and Admins { if (playerBiota.WeenieType == WeenieType.Creature || playerBiota.WeenieType == WeenieType.Sentinel) // Up/downgrade weenie { character.IsPlussed = true; playerBiota.WeenieType = WeenieType.Admin; addAdminProperties = true; } } } if (playerBiota.WeenieType == WeenieType.Admin) { player = new Admin(playerBiota, possessedBiotas.Inventory, possessedBiotas.WieldedItems, character, session); } else if (playerBiota.WeenieType == WeenieType.Sentinel) { player = new Sentinel(playerBiota, possessedBiotas.Inventory, possessedBiotas.WieldedItems, character, session); } else { player = new Player(playerBiota, possessedBiotas.Inventory, possessedBiotas.WieldedItems, character, session); } session.SetPlayer(player); if (stripAdminProperties) // continue stripping properties { player.CloakStatus = CloakStatus.Undef; player.Attackable = true; player.SetProperty(PropertyBool.DamagedByCollisions, true); player.AdvocateLevel = null; player.ChannelsActive = null; player.ChannelsAllowed = null; player.Invincible = false; player.Cloaked = null; player.IgnoreHouseBarriers = false; player.IgnorePortalRestrictions = false; player.SafeSpellComponents = false; player.ReportCollisions = true; player.ChangesDetected = true; player.CharacterChangesDetected = true; } if (addSentinelProperties || addAdminProperties) // continue restoring properties to default { WorldObject weenie; if (addAdminProperties) { weenie = Factories.WorldObjectFactory.CreateWorldObject(DatabaseManager.World.GetCachedWeenie("admin"), new ACE.Entity.ObjectGuid(ACE.Entity.ObjectGuid.Invalid.Full)); } else { weenie = Factories.WorldObjectFactory.CreateWorldObject(DatabaseManager.World.GetCachedWeenie("sentinel"), new ACE.Entity.ObjectGuid(ACE.Entity.ObjectGuid.Invalid.Full)); } if (weenie != null) { player.CloakStatus = CloakStatus.Off; player.Attackable = weenie.Attackable; player.SetProperty(PropertyBool.DamagedByCollisions, false); player.AdvocateLevel = weenie.GetProperty(PropertyInt.AdvocateLevel); player.ChannelsActive = (Channel?)weenie.GetProperty(PropertyInt.ChannelsActive); player.ChannelsAllowed = (Channel?)weenie.GetProperty(PropertyInt.ChannelsAllowed); player.Invincible = false; player.Cloaked = false; player.ChangesDetected = true; player.CharacterChangesDetected = true; } } // If the client is missing a location, we start them off in the starter town they chose if (session.Player.Location == null) { if (session.Player.Instantiation != null) { session.Player.Location = new Position(session.Player.Instantiation); } else { session.Player.Location = new Position(0xA9B40019, 84, 7.1f, 94, 0, 0, -0.0784591f, 0.996917f); // ultimate fallback } } var olthoiPlayerReturnedToLifestone = session.Player.IsOlthoiPlayer && character.TotalLogins >= 1 && session.Player.LoginAtLifestone; if (olthoiPlayerReturnedToLifestone) { session.Player.Location = new Position(session.Player.Sanctuary); } session.Player.PlayerEnterWorld(); var success = LandblockManager.AddObject(session.Player, true); if (!success) { // send to lifestone, or fallback location var fixLoc = session.Player.Sanctuary ?? new Position(0xA9B40019, 84, 7.1f, 94, 0, 0, -0.0784591f, 0.996917f); log.Error($"WorldManager.DoPlayerEnterWorld: failed to spawn {session.Player.Name}, relocating to {fixLoc.ToLOCString()}"); session.Player.Location = new Position(fixLoc); LandblockManager.AddObject(session.Player, true); var actionChain = new ActionChain(); actionChain.AddDelaySeconds(5.0f); actionChain.AddAction(session.Player, () => { if (session != null && session.Player != null) { session.Player.Teleport(fixLoc); } }); actionChain.EnqueueChain(); } // These warnings are set by DDD_InterrogationResponse if ((session.DatWarnCell || session.DatWarnLanguage || session.DatWarnPortal) && PropertyManager.GetBool("show_dat_warning").Item) { var msg = PropertyManager.GetString("dat_warning_msg").Item; var chatMsg = new GameMessageSystemChat(msg, ChatMessageType.System); session.Network.EnqueueSend(chatMsg); } var popup_header = PropertyManager.GetString("popup_header").Item; var popup_motd = PropertyManager.GetString("popup_motd").Item; var popup_welcome = player.IsOlthoiPlayer ? PropertyManager.GetString("popup_welcome_olthoi").Item : PropertyManager.GetString("popup_welcome").Item; if (character.TotalLogins <= 1) { if (player.IsOlthoiPlayer) { session.Network.EnqueueSend(new GameEventPopupString(session, AppendLines(popup_welcome, popup_motd))); } else { session.Network.EnqueueSend(new GameEventPopupString(session, AppendLines(popup_header, popup_motd, popup_welcome))); } } else if (!string.IsNullOrEmpty(popup_motd)) { session.Network.EnqueueSend(new GameEventPopupString(session, AppendLines(popup_header, popup_motd))); } var info = "Welcome to Asheron's Call\n powered by ACEmulator\n\nFor more information on commands supported by this server, type @acehelp\n"; session.Network.EnqueueSend(new GameMessageSystemChat(info, ChatMessageType.Broadcast)); var server_motd = PropertyManager.GetString("server_motd").Item; if (!string.IsNullOrEmpty(server_motd)) { session.Network.EnqueueSend(new GameMessageSystemChat($"{server_motd}\n", ChatMessageType.Broadcast)); } if (olthoiPlayerReturnedToLifestone) { session.Network.EnqueueSend(new GameMessageSystemChat("You have returned to the Olthoi Queen to serve the hive.", ChatMessageType.Broadcast)); } else if (playerLoggedInOnNoLogLandblock) // see http://acpedia.org/wiki/Mount_Elyrii_Hive { session.Network.EnqueueSend(new GameMessageSystemChat("The currents of portal space cannot return you from whence you came. Your previous location forbids login.", ChatMessageType.Broadcast)); } }
private static void DoPlayerEnterWorld(Session session, Character character, Biota playerBiota, PossessedBiotas possessedBiotas) { Player player; Player.HandleNoLogLandblock(playerBiota); var stripAdminProperties = false; var addAdminProperties = false; var addSentinelProperties = false; if (ConfigManager.Config.Server.Accounts.OverrideCharacterPermissions) { if (session.AccessLevel <= AccessLevel.Advocate) // check for elevated characters { if (playerBiota.WeenieType == (int)WeenieType.Admin || playerBiota.WeenieType == (int)WeenieType.Sentinel) // Downgrade weenie { character.IsPlussed = false; playerBiota.WeenieType = (int)WeenieType.Creature; stripAdminProperties = true; } } else if (session.AccessLevel >= AccessLevel.Sentinel && session.AccessLevel <= AccessLevel.Envoy) { if (playerBiota.WeenieType == (int)WeenieType.Creature || playerBiota.WeenieType == (int)WeenieType.Admin) // Up/downgrade weenie { character.IsPlussed = true; playerBiota.WeenieType = (int)WeenieType.Sentinel; addSentinelProperties = true; } } else // Developers and Admins { if (playerBiota.WeenieType == (int)WeenieType.Creature || playerBiota.WeenieType == (int)WeenieType.Sentinel) // Up/downgrade weenie { character.IsPlussed = true; playerBiota.WeenieType = (int)WeenieType.Admin; addAdminProperties = true; } } } if (playerBiota.WeenieType == (int)WeenieType.Admin) { player = new Admin(playerBiota, possessedBiotas.Inventory, possessedBiotas.WieldedItems, character, session); } else if (playerBiota.WeenieType == (int)WeenieType.Sentinel) { player = new Sentinel(playerBiota, possessedBiotas.Inventory, possessedBiotas.WieldedItems, character, session); } else { player = new Player(playerBiota, possessedBiotas.Inventory, possessedBiotas.WieldedItems, character, session); } session.SetPlayer(player); if (stripAdminProperties) // continue stripping properties { player.CloakStatus = null; player.Attackable = true; player.SetProperty(ACE.Entity.Enum.Properties.PropertyBool.DamagedByCollisions, true); player.AdvocateLevel = null; player.ChannelsActive = null; player.ChannelsAllowed = null; player.Invincible = false; player.Cloaked = null; player.IgnoreHouseBarriers = false; player.IgnorePortalRestrictions = false; player.SafeSpellComponents = false; player.ChangesDetected = true; player.CharacterChangesDetected = true; } if (addSentinelProperties || addAdminProperties) // continue restoring properties to default { WorldObject weenie; if (addAdminProperties) { weenie = Factories.WorldObjectFactory.CreateWorldObject(DatabaseManager.World.GetCachedWeenie("admin"), new ACE.Entity.ObjectGuid(ACE.Entity.ObjectGuid.Invalid.Full)); } else { weenie = Factories.WorldObjectFactory.CreateWorldObject(DatabaseManager.World.GetCachedWeenie("sentinel"), new ACE.Entity.ObjectGuid(ACE.Entity.ObjectGuid.Invalid.Full)); } if (weenie != null) { player.CloakStatus = CloakStatus.Off; player.Attackable = weenie.Attackable; player.SetProperty(ACE.Entity.Enum.Properties.PropertyBool.DamagedByCollisions, false); player.AdvocateLevel = weenie.GetProperty(ACE.Entity.Enum.Properties.PropertyInt.AdvocateLevel); player.ChannelsActive = (Channel?)weenie.GetProperty(ACE.Entity.Enum.Properties.PropertyInt.ChannelsActive); player.ChannelsAllowed = (Channel?)weenie.GetProperty(ACE.Entity.Enum.Properties.PropertyInt.ChannelsAllowed); player.Invincible = false; player.Cloaked = false; player.ChangesDetected = true; player.CharacterChangesDetected = true; } } // If the client is missing a location, we start them off in the starter town they chose if (session.Player.Location == null) { if (session.Player.Instantiation != null) { session.Player.Location = new Position(session.Player.Instantiation); } else { session.Player.Location = new Position(0xA9B40019, 84, 7.1f, 94, 0, 0, -0.0784591f, 0.996917f); // ultimate fallback; } } session.Player.PlayerEnterWorld(); var success = LandblockManager.AddObject(session.Player, true); if (!success) { // send to lifestone, or fallback location var fixLoc = session.Player.Sanctuary ?? new Position(0xA9B40019, 84, 7.1f, 94, 0, 0, -0.0784591f, 0.996917f); log.Error($"WorldManager.DoPlayerEnterWorld: failed to spawn {session.Player.Name}, relocating to {fixLoc.ToLOCString()}"); session.Player.Location = new Position(fixLoc); LandblockManager.AddObject(session.Player, true); var actionChain = new ActionChain(); actionChain.AddDelaySeconds(5.0f); actionChain.AddAction(session.Player, () => { if (session != null && session.Player != null) { session.Player.Teleport(fixLoc); } }); actionChain.EnqueueChain(); } var popup_header = PropertyManager.GetString("popup_header").Item; var popup_motd = PropertyManager.GetString("popup_motd").Item; var popup_welcome = PropertyManager.GetString("popup_welcome").Item; if (character.TotalLogins <= 1) { session.Network.EnqueueSend(new GameEventPopupString(session, AppendLines(popup_header, popup_motd, popup_welcome))); } else if (!string.IsNullOrEmpty(popup_motd)) { session.Network.EnqueueSend(new GameEventPopupString(session, AppendLines(popup_header, popup_motd))); } var info = "Welcome to Asheron's Call\n powered by ACEmulator\n\nFor more information on commands supported by this server, type @acehelp\n"; session.Network.EnqueueSend(new GameMessageSystemChat(info, ChatMessageType.Broadcast)); var server_motd = PropertyManager.GetString("server_motd").Item; if (!string.IsNullOrEmpty(server_motd)) { session.Network.EnqueueSend(new GameMessageSystemChat($"{server_motd}\n", ChatMessageType.Broadcast)); } }
/// <summary> /// Manages updating all entities on the world. /// - Server-side command-line commands are handled in their own thread. /// - Network commands come from their own listener threads, and are queued in world objects /// - This thread does the rest of the work! /// </summary> private static void UpdateWorld() { log.DebugFormat("Starting UpdateWorld thread"); double lastTick = 0d; WorldActive = true; var worldTickTimer = new Stopwatch(); while (!pendingWorldStop) { worldTickTimer.Restart(); // Handle time-based timeouts DelayManager.RunActions(); // Sequences of update thread: // Update positions based on new tick // TODO(ddevec): Physics here IEnumerable <WorldObject> movedObjects = FakePhysics(PortalYearTicks); // Do any pre-calculated landblock transfers -- foreach (WorldObject wo in movedObjects) { // If it was picked up, or moved // NOTE: The object's Location can now be null, if a player logs out, or an item is picked up if (wo.Location != null && wo.Location.LandblockId != wo.CurrentLandblock.Id) { // NOTE: We are moving the objects on behalf of the physics LandblockManager.RelocateObjectForPhysics(wo); } } // FIXME(ddevec): This O(n^2) tracking loop is a remenant of the old structure -- we should probably come up with a more efficient tracking scheme Parallel.ForEach(movedObjects, mo => { // detect all world objects in ghost range List <WorldObject> woproxghost = new List <WorldObject>(); woproxghost.AddRange(mo.CurrentLandblock.GetWorldObjectsInRangeForPhysics(mo, Landblock.MaxObjectGhostRange)); // for all objects in range of this moving object or in ghost range of moving object update them. Parallel.ForEach(woproxghost, gwo => { if (mo.Guid.IsPlayer()) { // if world object is in active zone then. if (gwo.Location.SquaredDistanceTo(mo.Location) <= Landblock.MaxObjectRange * Landblock.MaxObjectRange) { // if world object is in active zone. if (!(mo as Player).GetTrackedObjectGuids().Contains(gwo.Guid)) { (mo as Player).TrackObject(gwo); } } // if world object is in ghost zone and outside of active zone else { if ((mo as Player).GetTrackedObjectGuids().Contains(gwo.Guid)) { (mo as Player).StopTrackingObject(gwo, false); } } } }); }); // Process between landblock object motions sequentially // Currently only used for picking items up off a landblock MotionQueue.RunActions(); // Now, update actions within landblocks // This is responsible for updating all "actors" residing within the landblock. // Objects and landblocks are "actors" // "actors" decide if they want to read/modify their own state (set desired velocity), move-to positions, move items, read vitals, etc // N.B. -- Broadcasts are enqueued for sending at the end of the landblock's action time // FIXME(ddevec): Goal is to eventually migrate to an "Act" function of the LandblockManager ActiveLandblocks // Inactive landblocks will be put on TimeoutManager queue for timeout killing ActionQueue.RunActions(); // Handles sending out all per-landblock broadcasts -- This may rework when we rework tracking -- tbd BroadcastQueue.RunActions(); // XXX(ddevec): Should this be its own step in world-update thread? sessionLock.EnterReadLock(); try { // Send the current time ticks to allow sessions to declare themselves bad Parallel.ForEach(sessions, s => s.Update(lastTick, DateTime.UtcNow.Ticks)); } finally { sessionLock.ExitReadLock(); } // Removes sessions in the NetworkTimeout state, incuding sessions that have reached a timeout limit. var deadSessions = sessions.FindAll(s => s.State == Network.Enum.SessionState.NetworkTimeout); if (deadSessions.Count > 0) { Parallel.ForEach(deadSessions, RemoveSession); } Thread.Sleep(1); lastTick = (double)worldTickTimer.ElapsedTicks / Stopwatch.Frequency; PortalYearTicks += lastTick; } // World has finished operations and concedes the thread to garbage collection WorldActive = false; }
/// <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); } PropertyManager.ResyncVariables(); PropertyManager.StopUpdating(); log.Debug("Logging off all players..."); // logout each player foreach (var player in PlayerManager.GetAllOnline()) { player.Session.LogOffPlayer(); } log.Info("Waiting for all players to log off..."); // wait 10 seconds for log-off while (PlayerManager.GetAllOnline().Count > 0) { 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(); log.Info("Waiting for all active landblocks to unload..."); while (LandblockManager.GetLoadedLandblocks().Count > 0) { Thread.Sleep(10); } log.Debug("Stopping world..."); // Disabled thread update loop WorldManager.StopWorld(); log.Info("Waiting for world to stop..."); // Wait for world to end while (WorldManager.WorldActive) { Thread.Sleep(10); } //log.Info("Saving OfflinePlayers that have unsaved changes..."); //PlayerManager.SaveOfflinePlayersWithChanges(); log.Info("Waiting for database queue to empty..."); // Wait for the database queue to empty while (DatabaseManager.Shard.QueueCount > 0) { Thread.Sleep(10); } // Write exit to console/log log.Info($"Exiting at {DateTime.UtcNow}"); // System exit Environment.Exit(Environment.ExitCode); }
/// <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"); double lastTickDuration = 0d; WorldActive = true; var worldTickTimer = new Stopwatch(); while (!pendingWorldStop) { worldTickTimer.Restart(); /* * 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 * * TODO: We need a thread safe way to handle object transitions between distant landblocks */ InboundClientMessageQueue.RunActions(); playerEnterWorldQueue.RunActions(); DelayManager.RunActions(); // update positions through physics engine var movedObjects = HandlePhysics(PortalYearTicks); // iterate through objects that have changed landblocks foreach (var movedObject in movedObjects) { // NOTE: The object's Location can now be null, if a player logs out, or an item is picked up if (movedObject.Location == null) { continue; } // assume adjacency move here? LandblockManager.RelocateObjectForPhysics(movedObject, true); } // Tick all of our Landblocks and WorldObjects var activeLandblocks = LandblockManager.GetActiveLandblocks(); foreach (var landblock in activeLandblocks) { landblock.Tick(lastTickDuration, Time.GetUnixTime()); } // clean up inactive landblocks LandblockManager.UnloadLandblocks(); // Session Maintenance int sessionCount; sessionLock.EnterUpgradeableReadLock(); try { sessionCount = sessions.Count; // The session tick processes all inbound GameAction messages foreach (var s in sessions) { s.Tick(lastTickDuration); } // Send the current time ticks to allow sessions to declare themselves bad Parallel.ForEach(sessions, s => s.TickInParallel(lastTickDuration)); // Removes sessions in the NetworkTimeout state, incuding sessions that have reached a timeout limit. var deadSessions = sessions.FindAll(s => s.State == Network.Enum.SessionState.NetworkTimeout); foreach (var session in deadSessions) { log.Info($"client {session.Account} dropped"); RemoveSession(session); } } finally { sessionLock.ExitUpgradeableReadLock(); } Thread.Sleep(sessionCount == 0 ? 10 : 1); // Relax the CPU if no sessions are connected lastTickDuration = worldTickTimer.Elapsed.TotalSeconds; PortalYearTicks += lastTickDuration; } // World has finished operations and concedes the thread to garbage collection WorldActive = false; }
/// <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(); 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); } 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); }
/// <summary> /// Manages updating all entities on the world. /// - Server-side command-line commands are handled in their own thread. /// - Network commands come from their own listener threads, and are queued in world objects /// - This thread does the rest of the work! /// </summary> private static void UpdateWorld() { log.DebugFormat("Starting UpdateWorld thread"); double lastTick = 0d; WorldActive = true; var worldTickTimer = new Stopwatch(); while (!pendingWorldStop) { worldTickTimer.Restart(); // handle time-based actions DelayManager.RunActions(); // update positions through physics engine var movedObjects = HandlePhysics(PortalYearTicks); // iterate through objects that have changed landblocks foreach (var movedObject in movedObjects) { // NOTE: The object's Location can now be null, if a player logs out, or an item is picked up if (movedObject.Location == null) { continue; } // assume adjacency move here? LandblockManager.RelocateObjectForPhysics(movedObject, true); } InboundMessageQueue.RunActions(); // Process between landblock object motions sequentially // Currently only used for picking items up off a landblock LandblockMotionQueue.RunActions(); // Now, update actions within landblocks // This is responsible for updating all "actors" residing within the landblock. // Objects and landblocks are "actors" // "actors" decide if they want to read/modify their own state (set desired velocity), move-to positions, move items, read vitals, etc // N.B. -- Broadcasts are enqueued for sending at the end of the landblock's action time // FIXME(ddevec): Goal is to eventually migrate to an "Act" function of the LandblockManager ActiveLandblocks // Inactive landblocks will be put on TimeoutManager queue for timeout killing LandblockActionQueue.RunActions(); // Handles sending out all per-landblock broadcasts -- This may rework when we rework tracking -- tbd LandblockBroadcastQueue.RunActions(); // XXX(ddevec): Should this be its own step in world-update thread? sessionLock.EnterReadLock(); try { // Send the current time ticks to allow sessions to declare themselves bad Parallel.ForEach(sessions, s => s.Update(lastTick, DateTime.UtcNow.Ticks)); } finally { sessionLock.ExitReadLock(); } // Removes sessions in the NetworkTimeout state, incuding sessions that have reached a timeout limit. var deadSessions = sessions.FindAll(s => s.State == Network.Enum.SessionState.NetworkTimeout); if (deadSessions.Count > 0) { Parallel.ForEach(deadSessions, RemoveSession); } Thread.Sleep(1); lastTick = (double)worldTickTimer.ElapsedTicks / Stopwatch.Frequency; PortalYearTicks += lastTick; // clean up inactive landblocks LandblockManager.UnloadLandblocks(); } // World has finished operations and concedes the thread to garbage collection WorldActive = false; }
private static void DoPlayerEnterWorld(Session session, Character character, Biota playerBiota, PossessedBiotas possessedBiotas) { Player player; Player.HandleNoLogLandblock(playerBiota); var stripAdminProperties = false; var addAdminProperties = false; var addSentinelProperties = false; if (ConfigManager.Config.Server.Accounts.OverrideCharacterPermissions) { if (session.AccessLevel <= AccessLevel.Advocate) // check for elevated characters { if (playerBiota.WeenieType == (int)WeenieType.Admin || playerBiota.WeenieType == (int)WeenieType.Sentinel) // Downgrade weenie { character.IsPlussed = false; playerBiota.WeenieType = (int)WeenieType.Creature; stripAdminProperties = true; } } else if (session.AccessLevel >= AccessLevel.Sentinel && session.AccessLevel <= AccessLevel.Envoy) { if (playerBiota.WeenieType == (int)WeenieType.Creature || playerBiota.WeenieType == (int)WeenieType.Admin) // Up/downgrade weenie { character.IsPlussed = true; playerBiota.WeenieType = (int)WeenieType.Sentinel; addSentinelProperties = true; } } else // Developers and Admins { if (playerBiota.WeenieType == (int)WeenieType.Creature || playerBiota.WeenieType == (int)WeenieType.Sentinel) // Up/downgrade weenie { character.IsPlussed = true; playerBiota.WeenieType = (int)WeenieType.Admin; addAdminProperties = true; } } } if (playerBiota.WeenieType == (int)WeenieType.Admin) { player = new Admin(playerBiota, possessedBiotas.Inventory, possessedBiotas.WieldedItems, character, session); } else if (playerBiota.WeenieType == (int)WeenieType.Sentinel) { player = new Sentinel(playerBiota, possessedBiotas.Inventory, possessedBiotas.WieldedItems, character, session); } else { player = new Player(playerBiota, possessedBiotas.Inventory, possessedBiotas.WieldedItems, character, session); } session.SetPlayer(player); if (stripAdminProperties) // continue stripping properties { player.CloakStatus = null; player.Attackable = true; player.SetProperty(ACE.Entity.Enum.Properties.PropertyBool.DamagedByCollisions, true); player.AdvocateLevel = null; player.ChannelsActive = null; player.ChannelsAllowed = null; player.Invincible = null; player.Cloaked = null; player.ChangesDetected = true; player.CharacterChangesDetected = true; } if (addSentinelProperties || addAdminProperties) // continue restoring properties to default { WorldObject weenie; if (addAdminProperties) { weenie = Factories.WorldObjectFactory.CreateWorldObject(DatabaseManager.World.GetCachedWeenie("admin"), new ACE.Entity.ObjectGuid(ACE.Entity.ObjectGuid.Invalid.Full)) as Admin; } else { weenie = Factories.WorldObjectFactory.CreateWorldObject(DatabaseManager.World.GetCachedWeenie("sentinel"), new ACE.Entity.ObjectGuid(ACE.Entity.ObjectGuid.Invalid.Full)) as Sentinel; } if (weenie != null) { player.CloakStatus = CloakStatus.Off; player.Attackable = weenie.Attackable; player.SetProperty(ACE.Entity.Enum.Properties.PropertyBool.DamagedByCollisions, false); player.AdvocateLevel = weenie.GetProperty(ACE.Entity.Enum.Properties.PropertyInt.AdvocateLevel); player.ChannelsActive = (Channel?)weenie.GetProperty(ACE.Entity.Enum.Properties.PropertyInt.ChannelsActive); player.ChannelsAllowed = (Channel?)weenie.GetProperty(ACE.Entity.Enum.Properties.PropertyInt.ChannelsAllowed); player.Invincible = false; player.Cloaked = false; player.ChangesDetected = true; player.CharacterChangesDetected = true; } } // If the client is missing a location, we start them off in the starter dungeon if (session.Player.Location == null) { if (session.Player.Instantiation != null) { session.Player.Location = new Position(session.Player.Instantiation); } else { session.Player.Location = new Position(2349072813, 12.3199f, -28.482f, 0.0049999995f, 0.0f, 0.0f, -0.9408059f, -0.3389459f); } } session.Player.PlayerEnterWorld(); LandblockManager.AddObject(session.Player, true); var popup_header = PropertyManager.GetString("popup_header").Item; var popup_motd = PropertyManager.GetString("popup_motd").Item; var popup_welcome = PropertyManager.GetString("popup_welcome").Item; if (character.TotalLogins <= 1) { session.Network.EnqueueSend(new GameEventPopupString(session, AppendLines(popup_header, popup_motd, popup_welcome))); } else if (!string.IsNullOrEmpty(popup_motd)) { session.Network.EnqueueSend(new GameEventPopupString(session, AppendLines(popup_header, popup_motd))); } var info = "Welcome to Asheron's Call\n powered by ACEmulator\n\nFor more information on commands supported by this server, type @acehelp\n"; session.Network.EnqueueSend(new GameMessageSystemChat(info, ChatMessageType.Broadcast)); var server_motd = PropertyManager.GetString("server_motd").Item; if (!string.IsNullOrEmpty(server_motd)) { session.Network.EnqueueSend(new GameMessageSystemChat($"{server_motd}\n", ChatMessageType.Broadcast)); } }
/// <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 shut down has been cancelled @ {DateTime.Now} ({DateTime.UtcNow} UTC)"; log.Info(shutdownText); // special text foreach (var player in PlayerManager.GetAllOnline()) { player.Session.WorldBroadcast($"Broadcast from System> ATTENTION - This Asheron's Call Server shut down has been cancelled."); } // 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; var playerLogoffStart = DateTime.UtcNow; while ((playerCount = PlayerManager.GetOnlineCount()) > 0) { logUpdateTS = LogStatusUpdate(logUpdateTS, $"Waiting for {playerCount} player{(playerCount > 1 ? "s" : "")} to log off..."); Thread.Sleep(10); if (playerCount > 0 && DateTime.UtcNow - playerLogoffStart > TimeSpan.FromMinutes(5)) { playerLogoffStart = DateTime.UtcNow; log.Warn($"5 minute log off failsafe reached and there are {playerCount} player{(playerCount > 1 ? "s" : "")} still online."); foreach (var player in PlayerManager.GetAllOnline()) { log.Warn($"Player {player.Name} (0x{player.Guid}) appears to be stuck in world and unable to log off normally. Requesting Forced Logoff..."); player.ForcedLogOffRequested = true; player.ForceLogoff(); } } } 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.GetAuthenticatedSessionCount()) > 0) { logUpdateTS = LogStatusUpdate(logUpdateTS, $"Waiting for {sessionCount} authenticated 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); }