/// <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; }
/// <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> /// 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> /// 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; }