diff --git a/Robust.Client/GameController/GameController.cs b/Robust.Client/GameController/GameController.cs index 54aaeef39..20ec6bd67 100644 --- a/Robust.Client/GameController/GameController.cs +++ b/Robust.Client/GameController/GameController.cs @@ -518,9 +518,16 @@ namespace Robust.Client { using (_prof.Group("Entity")) { - // The last real tick is the current tick! This way we won't be in "prediction" mode. - _gameTiming.LastRealTick = _gameTiming.LastProcessedTick = _gameTiming.CurTick; - _entityManager.TickUpdate(frameEventArgs.DeltaSeconds, noPredictions: false); + if (ContentEntityTickUpdate != null) + { + ContentEntityTickUpdate.Invoke(frameEventArgs); + } + else + { + // The last real tick is the current tick! This way we won't be in "prediction" mode. + _gameTiming.LastRealTick = _gameTiming.LastProcessedTick = _gameTiming.CurTick; + _entityManager.TickUpdate(frameEventArgs.DeltaSeconds, noPredictions: false); + } } } @@ -687,5 +694,7 @@ namespace Robust.Client string? SplashLogo, bool AutoConnect ); + + public event Action? ContentEntityTickUpdate; } } diff --git a/Robust.Client/GameObjects/ClientEntityManager.cs b/Robust.Client/GameObjects/ClientEntityManager.cs index 80f016311..7147f15cd 100644 --- a/Robust.Client/GameObjects/ClientEntityManager.cs +++ b/Robust.Client/GameObjects/ClientEntityManager.cs @@ -124,7 +124,7 @@ namespace Robust.Client.GameObjects { var (_, msg) = _queue.Take(); // Logger.DebugS("net.ent", "Dispatching: {0}: {1}", seq, msg); - DispatchMsgEntity(msg); + DispatchReceivedNetworkMsg(msg); } } @@ -158,7 +158,7 @@ namespace Robust.Client.GameObjects { if (message.SourceTick <= _gameTiming.LastRealTick) { - DispatchMsgEntity(message); + DispatchReceivedNetworkMsg(message); return; } @@ -168,20 +168,24 @@ namespace Robust.Client.GameObjects _queue.Add((++_incomingMsgSequence, message)); } - private void DispatchMsgEntity(MsgEntity message) + private void DispatchReceivedNetworkMsg(MsgEntity message) { switch (message.Type) { case EntityMessageType.SystemMessage: - var msg = message.SystemMessage; - var sessionType = typeof(EntitySessionMessage<>).MakeGenericType(msg.GetType()); - var sessionMsg = Activator.CreateInstance(sessionType, new EntitySessionEventArgs(_playerManager.LocalPlayer!.Session), msg)!; - ReceivedSystemMessage?.Invoke(this, msg); - ReceivedSystemMessage?.Invoke(this, sessionMsg); + DispatchReceivedNetworkMsg(message.SystemMessage); return; } } + public void DispatchReceivedNetworkMsg(EntityEventArgs msg) + { + var sessionType = typeof(EntitySessionMessage<>).MakeGenericType(msg.GetType()); + var sessionMsg = Activator.CreateInstance(sessionType, new EntitySessionEventArgs(_playerManager.LocalPlayer!.Session), msg)!; + ReceivedSystemMessage?.Invoke(this, msg); + ReceivedSystemMessage?.Invoke(this, sessionMsg); + } + private sealed class MessageTickComparer : IComparer<(uint seq, MsgEntity msg)> { public int Compare((uint seq, MsgEntity msg) x, (uint seq, MsgEntity msg) y) diff --git a/Robust.Client/GameObjects/IClientEntityManager.cs b/Robust.Client/GameObjects/IClientEntityManager.cs index d648f792a..82051a89d 100644 --- a/Robust.Client/GameObjects/IClientEntityManager.cs +++ b/Robust.Client/GameObjects/IClientEntityManager.cs @@ -4,5 +4,9 @@ namespace Robust.Client.GameObjects { public interface IClientEntityManager : IEntityManager, IEntityNetworkManager { + /// + /// Raises a networked message as if it had arrived from the sever. + /// + public void DispatchReceivedNetworkMsg(EntityEventArgs msg); } } diff --git a/Robust.Client/GameStates/ClientDirtySystem.cs b/Robust.Client/GameStates/ClientDirtySystem.cs index 471f79e9d..a7bf599e3 100644 --- a/Robust.Client/GameStates/ClientDirtySystem.cs +++ b/Robust.Client/GameStates/ClientDirtySystem.cs @@ -11,7 +11,7 @@ namespace Robust.Client.GameStates; /// /// Tracks dirty entities on the client for the purposes of gamestatemanager. /// -internal sealed class ClientDirtySystem : EntitySystem +public sealed class ClientDirtySystem : EntitySystem { [Dependency] private readonly IClientGameTiming _timing = default!; [Dependency] private readonly IComponentFactory _compFact = default!; @@ -65,7 +65,7 @@ internal sealed class ClientDirtySystem : EntitySystem RemovedComponents.GetOrNew(comp.Owner).Add(netId.Value); } - internal void Reset() + public void Reset() { DirtyEntities.Clear(); RemovedComponents.Clear(); diff --git a/Robust.Client/GameStates/ClientGameStateManager.cs b/Robust.Client/GameStates/ClientGameStateManager.cs index 6d97c4c3a..2534ffafa 100644 --- a/Robust.Client/GameStates/ClientGameStateManager.cs +++ b/Robust.Client/GameStates/ClientGameStateManager.cs @@ -192,6 +192,8 @@ namespace Robust.Client.GameStates AckGameState(message.State.ToSequence); } + public void UpdateFullRep(GameState state) => _processor.UpdateFullRep(state); + private void HandlePvsLeaveMessage(MsgStateLeavePvs message) { _processor.AddLeavePvsMessage(message); @@ -272,7 +274,7 @@ namespace Robust.Client.GameStates // Update the cached server state. using (_prof.Group("FullRep")) { - _processor.UpdateFullRep(curState, _entities); + _processor.UpdateFullRep(curState); } IEnumerable createdEntities; @@ -440,7 +442,7 @@ namespace Robust.Client.GameStates } } - private void ResetPredictedEntities() + public void ResetPredictedEntities() { PredictionNeedsResetting = false; @@ -587,7 +589,7 @@ namespace Robust.Client.GameStates _network.ClientSendMessage(new MsgStateAck() { Sequence = sequence }); } - private IEnumerable ApplyGameState(GameState curState, GameState? nextState) + public IEnumerable ApplyGameState(GameState curState, GameState? nextState) { using var _ = _timing.StartStateApplicationArea(); diff --git a/Robust.Client/GameStates/GameStateProcessor.cs b/Robust.Client/GameStates/GameStateProcessor.cs index 4ed5673cb..57418cfa1 100644 --- a/Robust.Client/GameStates/GameStateProcessor.cs +++ b/Robust.Client/GameStates/GameStateProcessor.cs @@ -152,7 +152,7 @@ namespace Robust.Client.GameStates return applyNextState; } - public void UpdateFullRep(GameState state, IEntityManager entMan) + public void UpdateFullRep(GameState state) { // Note: the most recently received server state currently doesn't include pvs-leave messages (detaching // transform to null-space). This is because a client should never predict an entity being moved back from diff --git a/Robust.Client/GameStates/IClientGameStateManager.cs b/Robust.Client/GameStates/IClientGameStateManager.cs index 4c5f354f7..6f31ea4ff 100644 --- a/Robust.Client/GameStates/IClientGameStateManager.cs +++ b/Robust.Client/GameStates/IClientGameStateManager.cs @@ -1,6 +1,8 @@ using System; +using System.Collections.Generic; using Robust.Shared; using Robust.Shared.GameObjects; +using Robust.Shared.GameStates; using Robust.Shared.Input; using Robust.Shared.Network.Messages; using Robust.Shared.Timing; @@ -70,6 +72,16 @@ namespace Robust.Client.GameStates /// void ApplyGameState(); + /// + /// Applies a given set of game states. + /// + IEnumerable ApplyGameState(GameState curState, GameState? nextState); + + /// + /// Resets any entities that have changed while predicting future ticks. + /// + void ResetPredictedEntities(); + /// /// An input command has been dispatched. /// @@ -82,5 +94,7 @@ namespace Robust.Client.GameStates public void RequestFullState(EntityUid? missingEntity = null); uint SystemMessageDispatched(T message) where T : EntityEventArgs; + + void UpdateFullRep(GameState state); } } diff --git a/Robust.Client/IGameController.cs b/Robust.Client/IGameController.cs index 3ea9e30b8..7fb13e360 100644 --- a/Robust.Client/IGameController.cs +++ b/Robust.Client/IGameController.cs @@ -1,4 +1,8 @@ -namespace Robust.Client; +using System; +using Robust.Shared.GameObjects; +using Robust.Shared.Timing; + +namespace Robust.Client; public interface IGameController { @@ -15,5 +19,12 @@ public interface IGameController /// The server address, such as "ss14://localhost:1212/". /// Informational text on the cause of the reconnect. Empty or null gives a default reason. void Redial(string address, string? text = null); + + /// + /// This event gets invoked prior to performing entity tick update logic. If this is null the game + /// controller will simply call . + /// This exists to give content module more control over tick updating. + /// + event Action? ContentEntityTickUpdate; } diff --git a/Robust.Client/Player/PlayerManager.cs b/Robust.Client/Player/PlayerManager.cs index 77745d8ff..c1106749d 100644 --- a/Robust.Client/Player/PlayerManager.cs +++ b/Robust.Client/Player/PlayerManager.cs @@ -111,7 +111,8 @@ namespace Robust.Client.Player // This happens when the server says "nothing changed!" return; } - DebugTools.Assert(_network.IsConnected, "Received player state without being connected?"); + DebugTools.Assert(_network.IsConnected || _client.RunLevel == ClientRunLevel.SinglePlayerGame // replays use state application. + , "Received player state without being connected?"); DebugTools.Assert(LocalPlayer != null, "Call Startup()"); DebugTools.Assert(LocalPlayer!.Session != null, "Received player state before Session finished setup."); @@ -213,7 +214,8 @@ namespace Robust.Client.Player // clear slot, player left if (!hitSet.Contains(existing)) { - DebugTools.Assert(LocalPlayer!.UserId != existing, "I'm still connected to the server, but i left?"); + DebugTools.Assert(LocalPlayer!.UserId != existing || _client.RunLevel == ClientRunLevel.SinglePlayerGame, // replays apply player states. + "I'm still connected to the server, but i left?"); _sessions.Remove(existing); dirty = true; } diff --git a/Robust.UnitTesting/GameControllerDummy.cs b/Robust.UnitTesting/GameControllerDummy.cs index 41d5eb8eb..5c4370bea 100644 --- a/Robust.UnitTesting/GameControllerDummy.cs +++ b/Robust.UnitTesting/GameControllerDummy.cs @@ -12,6 +12,8 @@ namespace Robust.UnitTesting public GameControllerOptions Options { get; } = new(); public bool ContentStart { get; set; } + public event Action? ContentEntityTickUpdate; + public void Shutdown(string? reason = null) { }