/// <summary> /// Processes all inbound GameAction messages.<para /> /// Dispatches all outgoing messages.<para /> /// Removes dead sessions. /// </summary> public static int DoSessionWork() { int sessionCount; sessionLock.EnterUpgradeableReadLock(); try { sessionCount = sessions.Count; // The session tick inbound processes all inbound GameAction messages ServerPerformanceMonitor.RegisterEventStart(ServerPerformanceMonitor.MonitorType.DoSessionWork_TickInbound); foreach (var s in sessions) { s.TickInbound(); } ServerPerformanceMonitor.RegisterEventEnd(ServerPerformanceMonitor.MonitorType.DoSessionWork_TickInbound); // Do not combine the above and below loops. All inbound messages should be processed first and then all outbound messages should be processed second. // The session tick outbound processes pending actions and handles outgoing messages ServerPerformanceMonitor.RegisterEventStart(ServerPerformanceMonitor.MonitorType.DoSessionWork_TickOutbound); foreach (var s in sessions) { s.TickOutbound(); } ServerPerformanceMonitor.RegisterEventEnd(ServerPerformanceMonitor.MonitorType.DoSessionWork_TickOutbound); // Removes sessions in the NetworkTimeout state, including sessions that have reached a timeout limit. ServerPerformanceMonitor.RegisterEventStart(ServerPerformanceMonitor.MonitorType.DoSessionWork_RemoveSessions); for (int i = sessions.Count - 1; i >= 0; i--) { var sesh = sessions[i]; switch (sesh.State) { case SessionState.NetworkTimeout: sesh.DropSession(string.IsNullOrEmpty(sesh.BootSessionReason) ? "Network Timeout" : sesh.BootSessionReason); break; case SessionState.ClientConnectionFailure: // needs to send the client the "git outa here" message or client will zombie out and appear to the player like it's still in game. // TO-DO: see if PacketHeaderFlags.NetErrorDisconnect will work for this sesh.BootSession("Client connection failure", new GameMessageBootAccount(sesh)); break; case SessionState.ClientSentNetErrorDisconnect: sesh.DropSession(string.IsNullOrEmpty(sesh.BootSessionReason) ? "client sent network error disconnect" : sesh.BootSessionReason); break; } } ServerPerformanceMonitor.RegisterEventEnd(ServerPerformanceMonitor.MonitorType.DoSessionWork_RemoveSessions); } finally { sessionLock.ExitUpgradeableReadLock(); } return(sessionCount); }
/// <summary> /// Projected to run at a reasonable rate for gameplay (30-60fps) /// </summary> public static bool UpdateGameWorld() { if (updateGameWorldRateLimiter.GetSecondsToWaitBeforeNextEvent() > 0) { return(false); } updateGameWorldRateLimiter.RegisterEvent(); ServerPerformanceMonitor.RegisterEventStart(ServerPerformanceMonitor.MonitorType.UpdateGameWorld_Entire); // update positions through physics engine ServerPerformanceMonitor.RegisterEventStart(ServerPerformanceMonitor.MonitorType.UpdateGameWorld_HandlePhysics); var movedObjects = HandlePhysics(Timers.PortalYearTicks); ServerPerformanceMonitor.RegisterEventEnd(ServerPerformanceMonitor.MonitorType.UpdateGameWorld_HandlePhysics); // iterate through objects that have changed landblocks ServerPerformanceMonitor.RegisterEventStart(ServerPerformanceMonitor.MonitorType.UpdateGameWorld_RelocateObjectForPhysics); 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); } ServerPerformanceMonitor.RegisterEventEnd(ServerPerformanceMonitor.MonitorType.UpdateGameWorld_RelocateObjectForPhysics); // Tick all of our Landblocks and WorldObjects ServerPerformanceMonitor.RegisterEventStart(ServerPerformanceMonitor.MonitorType.UpdateGameWorld_landblock_Tick); var loadedLandblocks = LandblockManager.GetLoadedLandblocks(); foreach (var landblock in loadedLandblocks) { landblock.Tick(Time.GetUnixTime()); } ServerPerformanceMonitor.RegisterEventEnd(ServerPerformanceMonitor.MonitorType.UpdateGameWorld_landblock_Tick); // clean up inactive landblocks LandblockManager.UnloadLandblocks(); HouseManager.Tick(); ServerPerformanceMonitor.RegisterEventEnd(ServerPerformanceMonitor.MonitorType.UpdateGameWorld_Entire); return(true); }
public static void Tick(double portalYearTicks) { // update positions through physics engine ServerPerformanceMonitor.RestartEvent(ServerPerformanceMonitor.MonitorType.LandblockManager_TickPhysics); TickPhysics(portalYearTicks); ServerPerformanceMonitor.RegisterEventEnd(ServerPerformanceMonitor.MonitorType.LandblockManager_TickPhysics); // Tick all of our Landblocks and WorldObjects ServerPerformanceMonitor.RestartEvent(ServerPerformanceMonitor.MonitorType.LandblockManager_Tick); Tick(); ServerPerformanceMonitor.RegisterEventEnd(ServerPerformanceMonitor.MonitorType.LandblockManager_Tick); // clean up inactive landblocks UnloadLandblocks(); }
/// <summary> /// Projected to run at a reasonable rate for gameplay (30-60fps) /// </summary> public static bool UpdateGameWorld() { if (updateGameWorldRateLimiter.GetSecondsToWaitBeforeNextEvent() > 0) { return(false); } updateGameWorldRateLimiter.RegisterEvent(); ServerPerformanceMonitor.RegisterEventStart(ServerPerformanceMonitor.MonitorType.UpdateGameWorld_Entire); LandblockManager.Tick(Timers.PortalYearTicks); HouseManager.Tick(); ServerPerformanceMonitor.RegisterEventEnd(ServerPerformanceMonitor.MonitorType.UpdateGameWorld_Entire); return(true); }
public static void Tick(double portalYearTicks) { // update positions through physics engine ServerPerformanceMonitor.RestartEvent(ServerPerformanceMonitor.MonitorType.LandblockManager_TickPhysics); TickPhysics(portalYearTicks); ServerPerformanceMonitor.RegisterEventEnd(ServerPerformanceMonitor.MonitorType.LandblockManager_TickPhysics); // Tick all of our Landblocks and WorldObjects (Work that can be multi-threaded) ServerPerformanceMonitor.RestartEvent(ServerPerformanceMonitor.MonitorType.LandblockManager_TickMultiThreadedWork); TickMultiThreadedWork(); ServerPerformanceMonitor.RegisterEventEnd(ServerPerformanceMonitor.MonitorType.LandblockManager_TickMultiThreadedWork); // Tick all of our Landblocks and WorldObjects (Work that must be single threaded) ServerPerformanceMonitor.RestartEvent(ServerPerformanceMonitor.MonitorType.LandblockManager_TickSingleThreadedWork); TickSingleThreadedWork(); ServerPerformanceMonitor.RegisterEventEnd(ServerPerformanceMonitor.MonitorType.LandblockManager_TickSingleThreadedWork); // clean up inactive landblocks UnloadLandblocks(); }
/// <summary> /// Processes all inbound GameAction messages.<para /> /// Dispatches all outgoing messages.<para /> /// Removes dead sessions. /// </summary> public static int DoSessionWork() { int sessionCount = 0; sessionLock.EnterUpgradeableReadLock(); try { // The session tick outbound processes pending actions and handles outgoing messages ServerPerformanceMonitor.RegisterEventStart(ServerPerformanceMonitor.MonitorType.DoSessionWork_TickOutbound); foreach (var s in sessionMap) { s?.TickOutbound(); } ServerPerformanceMonitor.RegisterEventEnd(ServerPerformanceMonitor.MonitorType.DoSessionWork_TickOutbound); // Removes sessions in the NetworkTimeout state, including sessions that have reached a timeout limit. ServerPerformanceMonitor.RegisterEventStart(ServerPerformanceMonitor.MonitorType.DoSessionWork_RemoveSessions); foreach (var session in sessionMap.Where(k => !Equals(null, k))) { var pendingTerm = session.PendingTermination; if (session.PendingTermination != null && session.PendingTermination.TerminationStatus == SessionTerminationPhase.SessionWorkCompleted) { session.DropSession(); session.PendingTermination.TerminationStatus = SessionTerminationPhase.WorldManagerWorkCompleted; } sessionCount++; } ServerPerformanceMonitor.RegisterEventEnd(ServerPerformanceMonitor.MonitorType.DoSessionWork_RemoveSessions); } finally { sessionLock.ExitUpgradeableReadLock(); } return(sessionCount); }
/// <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 ProcessPacket(ClientPacket packet, IPEndPoint endPoint, IPEndPoint listenerEndpoint) { if (listenerEndpoint.Port == ConfigManager.Config.Server.Network.Port + 1) { ServerPerformanceMonitor.RegisterEventStart(ServerPerformanceMonitor.MonitorType.ProcessPacket_1); if (packet.Header.Flags.HasFlag(PacketHeaderFlags.ConnectResponse)) { packetLog.Debug($"{packet}, {endPoint}"); PacketInboundConnectResponse connectResponse = new PacketInboundConnectResponse(packet); // This should be set on the second packet to the server from the client. // This completes the three-way handshake. sessionLock.EnterReadLock(); Session session = null; try { session = (from k in sessionMap where k != null && k.State == SessionState.AuthConnectResponse && k.Network.ConnectionData.ConnectionCookie == connectResponse.Check && k.EndPoint.Address.Equals(endPoint.Address) select k).FirstOrDefault(); } finally { sessionLock.ExitReadLock(); } if (session != null) { session.State = SessionState.AuthConnected; session.Network.sendResync = true; AuthenticationHandler.HandleConnectResponse(session); } } else if (packet.Header.Id == 0 && packet.Header.HasFlag(PacketHeaderFlags.CICMDCommand)) { // TODO: Not sure what to do with these packets yet } else { log.ErrorFormat("Packet from {0} rejected. Packet sent to listener 1 and is not a ConnectResponse or CICMDCommand", endPoint); } ServerPerformanceMonitor.RegisterEventEnd(ServerPerformanceMonitor.MonitorType.ProcessPacket_1); } else // ConfigManager.Config.Server.Network.Port + 0 { ServerPerformanceMonitor.RegisterEventStart(ServerPerformanceMonitor.MonitorType.ProcessPacket_0); if (packet.Header.HasFlag(PacketHeaderFlags.LoginRequest)) { packetLog.Debug($"{packet}, {endPoint}"); if (GetSessionCount() >= ConfigManager.Config.Server.Network.MaximumAllowedSessions) { log.InfoFormat("Login Request from {0} rejected. Server full.", endPoint); SendLoginRequestReject(endPoint, CharacterError.LogonServerFull); } else if (ServerManager.ShutdownInitiated && (ServerManager.ShutdownTime - DateTime.UtcNow).TotalMinutes < 2) { log.InfoFormat("Login Request from {0} rejected. Server shutting down in less than 2 minutes.", endPoint); SendLoginRequestReject(endPoint, CharacterError.ServerCrash1); } else { log.DebugFormat("Login Request from {0}", endPoint); var session = FindOrCreateSession(endPoint); if (session != null) { if (session.State == SessionState.AuthConnectResponse) { // connect request packet sent to the client was corrupted in transit and session entered an unspecified state. // ignore the request and remove the broken session and the client will start a new session. RemoveSession(session); log.Warn($"Bad handshake from {endPoint}, aborting session."); } session.ProcessPacket(packet); } else { log.InfoFormat("Login Request from {0} rejected. Failed to find or create session.", endPoint); SendLoginRequestReject(endPoint, CharacterError.LogonServerFull); } } } else if (sessionMap.Length > packet.Header.Id) { var session = sessionMap[packet.Header.Id]; if (session != null) { if (session.EndPoint.Equals(endPoint)) { session.ProcessPacket(packet); } else { log.WarnFormat("Session for Id {0} has IP {1} but packet has IP {2}", packet.Header.Id, session.EndPoint, endPoint); } } else { log.DebugFormat("Unsolicited Packet from {0} with Id {1}", endPoint, packet.Header.Id); } } else { log.DebugFormat("Unsolicited Packet from {0} with Id {1}", endPoint, packet.Header.Id); } ServerPerformanceMonitor.RegisterEventEnd(ServerPerformanceMonitor.MonitorType.ProcessPacket_0); } }