diff --git a/Robust.Client/ClientIoC.cs b/Robust.Client/ClientIoC.cs index ba7ed9dab..d7ba40e3f 100644 --- a/Robust.Client/ClientIoC.cs +++ b/Robust.Client/ClientIoC.cs @@ -14,6 +14,7 @@ using Robust.Client.Player; using Robust.Client.Profiling; using Robust.Client.Prototypes; using Robust.Client.Reflection; +using Robust.Client.Replays; using Robust.Client.ResourceManagement; using Robust.Client.State; using Robust.Client.Timing; @@ -32,6 +33,7 @@ using Robust.Shared.Physics; using Robust.Shared.Players; using Robust.Shared.Prototypes; using Robust.Shared.Reflection; +using Robust.Shared.Replays; using Robust.Shared.Timing; using Robust.Shared.ViewVariables; @@ -69,6 +71,7 @@ namespace Robust.Client deps.Register(); deps.Register(); deps.Register(); + deps.Register(); deps.Register(); deps.Register(); deps.Register(); diff --git a/Robust.Client/GameObjects/ClientEntityManager.cs b/Robust.Client/GameObjects/ClientEntityManager.cs index 7147f15cd..5a4a877b8 100644 --- a/Robust.Client/GameObjects/ClientEntityManager.cs +++ b/Robust.Client/GameObjects/ClientEntityManager.cs @@ -21,6 +21,7 @@ namespace Robust.Client.GameObjects [Dependency] private readonly IClientNetManager _networkManager = default!; [Dependency] private readonly IClientGameTiming _gameTiming = default!; [Dependency] private readonly IClientGameStateManager _stateMan = default!; + [Dependency] private readonly IBaseClient _client = default!; protected override int NextEntityUid { get; set; } = EntityUid.ClientUid + 1; @@ -93,7 +94,7 @@ namespace Robust.Client.GameObjects if (!_stateMan.IsPredictionEnabled) return; - DebugTools.Assert(_gameTiming.InPrediction && _gameTiming.IsFirstTimePredicted); + DebugTools.Assert(_gameTiming.InPrediction && _gameTiming.IsFirstTimePredicted || _client.RunLevel != ClientRunLevel.Connected); var eventArgs = new EntitySessionEventArgs(localPlayer!.Session); EventBus.RaiseEvent(EventSource.Local, msg); @@ -132,7 +133,7 @@ namespace Robust.Client.GameObjects } /// - public void SendSystemNetworkMessage(EntityEventArgs message) + public void SendSystemNetworkMessage(EntityEventArgs message, bool recordReplay = true) { SendSystemNetworkMessage(message, default(uint)); } diff --git a/Robust.Client/GameObjects/EntitySystems/AudioSystem.cs b/Robust.Client/GameObjects/EntitySystems/AudioSystem.cs index a67e93332..5532f9b8f 100644 --- a/Robust.Client/GameObjects/EntitySystems/AudioSystem.cs +++ b/Robust.Client/GameObjects/EntitySystems/AudioSystem.cs @@ -14,6 +14,7 @@ using Robust.Shared.Map; using Robust.Shared.Physics; using Robust.Shared.Physics.Systems; using Robust.Shared.Player; +using Robust.Shared.Players; using Robust.Shared.Random; using Robust.Shared.Timing; using Robust.Shared.Utility; @@ -423,7 +424,7 @@ public sealed class AudioSystem : SharedAudioSystem AudioParams? audioParams = null) { if (_timing.IsFirstTimePredicted || sound == null) - return Play(sound, Filter.Local(), source, audioParams); + return Play(sound, Filter.Local(), source, false, audioParams); return null; // uhh Lets hope predicted audio never needs to somehow store the playing audio.... } @@ -503,14 +504,13 @@ public sealed class AudioSystem : SharedAudioSystem } /// - public override IPlayingAudioStream? PlayGlobal(string filename, Filter playerFilter, AudioParams? audioParams = null) + public override IPlayingAudioStream? PlayGlobal(string filename, Filter playerFilter, bool recordReplay, AudioParams? audioParams = null) { return Play(filename, audioParams); } /// - public override IPlayingAudioStream? Play(string filename, Filter playerFilter, EntityUid entity, - AudioParams? audioParams = null) + public override IPlayingAudioStream? Play(string filename, Filter playerFilter, EntityUid entity, bool recordReplay, AudioParams? audioParams = null) { if (_resourceCache.TryGetResource(new ResourcePath(filename), out var audio)) { @@ -522,9 +522,53 @@ public sealed class AudioSystem : SharedAudioSystem } /// - public override IPlayingAudioStream? Play(string filename, Filter playerFilter, EntityCoordinates coordinates, - AudioParams? audioParams = null) + public override IPlayingAudioStream? Play(string filename, Filter playerFilter, EntityCoordinates coordinates, bool recordReplay, AudioParams? audioParams = null) { return Play(filename, coordinates, GetFallbackCoordinates(coordinates.ToMap(EntityManager)), audioParams); } -} \ No newline at end of file + + + /// + public override IPlayingAudioStream? PlayGlobal(string filename, ICommonSession recipient, AudioParams? audioParams = null) + { + return Play(filename, audioParams); + } + + /// + public override IPlayingAudioStream? PlayGlobal(string filename, EntityUid recipient, AudioParams? audioParams = null) + { + return Play(filename, audioParams); + } + + /// + public override IPlayingAudioStream? PlayEntity(string filename, ICommonSession recipient, EntityUid uid, AudioParams? audioParams = null) + { + if (_resourceCache.TryGetResource(new ResourcePath(filename), out var audio)) + { + return Play(audio, uid, null, audioParams); + } + return null; + } + + /// + public override IPlayingAudioStream? PlayEntity(string filename, EntityUid recipient, EntityUid uid, AudioParams? audioParams = null) + { + if (_resourceCache.TryGetResource(new ResourcePath(filename), out var audio)) + { + return Play(audio, uid, null, audioParams); + } + return null; + } + + /// + public override IPlayingAudioStream? PlayStatic(string filename, ICommonSession recipient, EntityCoordinates coordinates, AudioParams? audioParams = null) + { + return Play(filename, coordinates, GetFallbackCoordinates(coordinates.ToMap(EntityManager)), audioParams); + } + + /// + public override IPlayingAudioStream? PlayStatic(string filename, EntityUid recipient, EntityCoordinates coordinates, AudioParams? audioParams = null) + { + return Play(filename, coordinates, GetFallbackCoordinates(coordinates.ToMap(EntityManager)), audioParams); + } +} diff --git a/Robust.Client/Player/PlayerManager.cs b/Robust.Client/Player/PlayerManager.cs index c1106749d..5286e1e4c 100644 --- a/Robust.Client/Player/PlayerManager.cs +++ b/Robust.Client/Player/PlayerManager.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using Robust.Shared.Enums; using Robust.Shared.GameObjects; @@ -255,5 +256,17 @@ namespace Robust.Client.Player PlayerListUpdated?.Invoke(this, EventArgs.Empty); } + + public bool TryGetSessionByEntity(EntityUid uid, [NotNullWhen(true)] out ICommonSession? session) + { + if (LocalPlayer?.ControlledEntity == uid) + { + session = LocalPlayer.Session; + return true; + } + + session = null; + return false; + } } } diff --git a/Robust.Client/Replays/ReplayRecordingManager.cs b/Robust.Client/Replays/ReplayRecordingManager.cs new file mode 100644 index 000000000..39e207932 --- /dev/null +++ b/Robust.Client/Replays/ReplayRecordingManager.cs @@ -0,0 +1,14 @@ +using Robust.Shared.Replays; + +namespace Robust.Client.Replays; + +/// +/// Dummy class so that can be used in shared code. +/// +public sealed class ReplayRecordingManager : IReplayRecordingManager +{ + /// + public void QueueReplayMessage(object args) { } + + public bool Recording => false; +} diff --git a/Robust.Server/GameObjects/EntitySystems/AudioSystem.cs b/Robust.Server/GameObjects/EntitySystems/AudioSystem.cs index 68a76c929..ae723117d 100644 --- a/Robust.Server/GameObjects/EntitySystems/AudioSystem.cs +++ b/Robust.Server/GameObjects/EntitySystems/AudioSystem.cs @@ -55,7 +55,7 @@ public sealed class AudioSystem : SharedAudioSystem } /// - public override IPlayingAudioStream? PlayGlobal(string filename, Filter playerFilter, AudioParams? audioParams = null) + public override IPlayingAudioStream? PlayGlobal(string filename, Filter playerFilter, bool recordReplay, AudioParams? audioParams = null) { var id = CacheIdentifier(); var msg = new PlayAudioGlobalMessage @@ -65,12 +65,12 @@ public sealed class AudioSystem : SharedAudioSystem Identifier = id }; - RaiseNetworkEvent(msg, playerFilter); + RaiseNetworkEvent(msg, playerFilter, recordReplay); return new AudioSourceServer(this, id, playerFilter.Recipients.ToArray()); } - public override IPlayingAudioStream? Play(string filename, Filter playerFilter, EntityUid uid, AudioParams? audioParams = null) + public override IPlayingAudioStream? Play(string filename, Filter playerFilter, EntityUid uid, bool recordReplay, AudioParams? audioParams = null) { if(!EntityManager.TryGetComponent(uid, out var transform)) return null; @@ -89,13 +89,13 @@ public sealed class AudioSystem : SharedAudioSystem Identifier = id, }; - RaiseNetworkEvent(msg, playerFilter); + RaiseNetworkEvent(msg, playerFilter, recordReplay); return new AudioSourceServer(this, id, playerFilter.Recipients.ToArray()); } /// - public override IPlayingAudioStream? Play(string filename, Filter playerFilter, EntityCoordinates coordinates, AudioParams? audioParams = null) + public override IPlayingAudioStream? Play(string filename, Filter playerFilter, EntityCoordinates coordinates, bool recordReplay, AudioParams? audioParams = null) { var id = CacheIdentifier(); @@ -110,7 +110,7 @@ public sealed class AudioSystem : SharedAudioSystem Identifier = id }; - RaiseNetworkEvent(msg, playerFilter); + RaiseNetworkEvent(msg, playerFilter, recordReplay); return new AudioSourceServer(this, id, playerFilter.Recipients.ToArray()); } @@ -122,6 +122,42 @@ public sealed class AudioSystem : SharedAudioSystem return null; var filter = Filter.Pvs(source, entityManager: EntityManager, playerManager: PlayerManager, cfgManager: CfgManager).RemoveWhereAttachedEntity(e => e == user); - return Play(sound, filter, source, audioParams); + return Play(sound, filter, source, true, audioParams); + } + + public override IPlayingAudioStream? PlayGlobal(string filename, ICommonSession recipient, AudioParams? audioParams = null) + { + return PlayGlobal(filename, Filter.SinglePlayer(recipient), false, audioParams); + } + + public override IPlayingAudioStream? PlayGlobal(string filename, EntityUid recipient, AudioParams? audioParams = null) + { + if (TryComp(recipient, out ActorComponent? actor)) + return PlayGlobal(filename, actor.PlayerSession, audioParams); + return null; + } + + public override IPlayingAudioStream? PlayEntity(string filename, ICommonSession recipient, EntityUid uid, AudioParams? audioParams = null) + { + return Play(filename, Filter.SinglePlayer(recipient), uid, false, audioParams); + } + + public override IPlayingAudioStream? PlayEntity(string filename, EntityUid recipient, EntityUid uid, AudioParams? audioParams = null) + { + if (TryComp(recipient, out ActorComponent? actor)) + return PlayEntity(filename, actor.PlayerSession, uid, audioParams); + return null; + } + + public override IPlayingAudioStream? PlayStatic(string filename, ICommonSession recipient, EntityCoordinates coordinates, AudioParams? audioParams = null) + { + return Play(filename, Filter.SinglePlayer(recipient), coordinates, false, audioParams); + } + + public override IPlayingAudioStream? PlayStatic(string filename, EntityUid recipient, EntityCoordinates coordinates, AudioParams? audioParams = null) + { + if (TryComp(recipient, out ActorComponent? actor)) + return PlayStatic(filename, actor.PlayerSession, coordinates, audioParams); + return null; } } diff --git a/Robust.Server/GameObjects/ServerEntityManager.cs b/Robust.Server/GameObjects/ServerEntityManager.cs index d7d31fc2d..a1f3463e5 100644 --- a/Robust.Server/GameObjects/ServerEntityManager.cs +++ b/Robust.Server/GameObjects/ServerEntityManager.cs @@ -15,6 +15,7 @@ using Robust.Shared.Log; using Robust.Shared.Network; using Robust.Shared.Network.Messages; using Robust.Shared.Prototypes; +using Robust.Shared.Replays; using Robust.Shared.Timing; using Robust.Shared.Utility; @@ -30,6 +31,7 @@ namespace Robust.Server.GameObjects "robust_entities_count", "Amount of alive entities."); + [Dependency] private readonly IReplayRecordingManager _replay = default!; [Dependency] private readonly IServerNetManager _networkManager = default!; [Dependency] private readonly IGameTiming _gameTiming = default!; [Dependency] private readonly IPlayerManager _playerManager = default!; @@ -164,13 +166,16 @@ namespace Robust.Server.GameObjects } /// - public void SendSystemNetworkMessage(EntityEventArgs message) + public void SendSystemNetworkMessage(EntityEventArgs message, bool recordReplay = true) { var newMsg = new MsgEntity(); newMsg.Type = EntityMessageType.SystemMessage; newMsg.SystemMessage = message; newMsg.SourceTick = _gameTiming.CurTick; + if (recordReplay) + _replay.QueueReplayMessage(message); + _networkManager.ServerSendToAll(newMsg); } diff --git a/Robust.Server/Player/PlayerManager.cs b/Robust.Server/Player/PlayerManager.cs index 9aa3a4567..dcd00bdfc 100644 --- a/Robust.Server/Player/PlayerManager.cs +++ b/Robust.Server/Player/PlayerManager.cs @@ -5,6 +5,7 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; using Prometheus; +using Robust.Server.GameObjects; using Robust.Shared.Configuration; using Robust.Shared.Enums; using Robust.Shared.GameObjects; @@ -516,6 +517,18 @@ namespace Robust.Server.Player { return _playerData.ContainsKey(userId); } + + public bool TryGetSessionByEntity(EntityUid uid, [NotNullWhen(true)] out ICommonSession? session) + { + if (!_entityManager.TryGetComponent(uid, out ActorComponent? actor)) + { + session = null; + return false; + } + + session = actor.PlayerSession; + return true; + } } public sealed class SessionStatusEventArgs : EventArgs diff --git a/Robust.Server/Replays/IInternalReplayManager.cs b/Robust.Server/Replays/IInternalReplayManager.cs new file mode 100644 index 000000000..05db59701 --- /dev/null +++ b/Robust.Server/Replays/IInternalReplayManager.cs @@ -0,0 +1,21 @@ +using Robust.Shared.IoC; +using System.Threading; + +namespace Robust.Server.Replays; + +internal interface IInternalReplayRecordingManager : IServerReplayRecordingManager +{ + /// + /// Initializes the replay manager. + /// + void Initialize(); + + /// + /// Saves the replay data for the current tick. Does nothing if is false. + /// + /// + /// This is intended to be called by PVS in parallel with other game-state networking. + /// + void SaveReplayData(Thread mainThread, IDependencyCollection parentDeps); +} diff --git a/Robust.Server/Replays/IServerReplayRecordingManager.cs b/Robust.Server/Replays/IServerReplayRecordingManager.cs new file mode 100644 index 000000000..ee39b61fa --- /dev/null +++ b/Robust.Server/Replays/IServerReplayRecordingManager.cs @@ -0,0 +1,14 @@ +using Robust.Shared.Replays; + +namespace Robust.Server.Replays; + +public interface IServerReplayRecordingManager : IReplayRecordingManager +{ + /// + /// Starts or stops a replay recording. The first tick will contain all game state data that would be sent to a + /// new client with PVS disabled. Old messages queued with are NOT included. Those messages are only saved + /// once recording has started. + /// + void ToggleRecording(); +} diff --git a/Robust.Server/Replays/ReplayRecordingManager.cs b/Robust.Server/Replays/ReplayRecordingManager.cs new file mode 100644 index 000000000..119b0d5ae --- /dev/null +++ b/Robust.Server/Replays/ReplayRecordingManager.cs @@ -0,0 +1,22 @@ +using Robust.Shared.IoC; +using System.Threading; + +namespace Robust.Server.Replays; + +public sealed class ReplayRecordingManager : IInternalReplayRecordingManager +{ + /// + public bool Recording => false; + + /// + public void Initialize() { } + + /// + public void ToggleRecording() { } + + /// + public void QueueReplayMessage(object obj) { } + + /// + public void SaveReplayData(Thread mainThread, IDependencyCollection parentDeps) { } +} diff --git a/Robust.Server/ServerIoC.cs b/Robust.Server/ServerIoC.cs index 32e3214e6..c31b3229f 100644 --- a/Robust.Server/ServerIoC.cs +++ b/Robust.Server/ServerIoC.cs @@ -9,6 +9,7 @@ using Robust.Server.Placement; using Robust.Server.Player; using Robust.Server.Prototypes; using Robust.Server.Reflection; +using Robust.Server.Replays; using Robust.Server.Scripting; using Robust.Server.ServerHub; using Robust.Server.ServerStatus; @@ -24,6 +25,7 @@ using Robust.Shared.Physics; using Robust.Shared.Players; using Robust.Shared.Prototypes; using Robust.Shared.Reflection; +using Robust.Shared.Replays; using Robust.Shared.Timing; using Robust.Shared.ViewVariables; @@ -63,6 +65,7 @@ namespace Robust.Server deps.Register(); deps.Register(); deps.Register(); + deps.Register(); deps.Register(); deps.Register(); deps.Register(); diff --git a/Robust.Shared/Audio/SoundSystem.cs b/Robust.Shared/Audio/SoundSystem.cs index 1de1fc3c0..dcb5dc058 100644 --- a/Robust.Shared/Audio/SoundSystem.cs +++ b/Robust.Shared/Audio/SoundSystem.cs @@ -11,6 +11,9 @@ namespace Robust.Shared.Audio /// public static class SoundSystem { + // These functions are obsolete and I CBF adding new arguments to them. + private static bool _recordReplay = false; + /// /// Play an audio file globally, without position. /// @@ -24,7 +27,7 @@ namespace Robust.Shared.Audio // Some timers try to play audio after the system has shut down? entSystMan.TryGetEntitySystem(out SharedAudioSystem? audio); - return audio?.PlayGlobal(filename, playerFilter, audioParams); + return audio?.PlayGlobal(filename, playerFilter, _recordReplay, audioParams); } /// @@ -42,7 +45,7 @@ namespace Robust.Shared.Audio // Some timers try to play audio after the system has shut down? entSystMan.TryGetEntitySystem(out SharedAudioSystem? audio); - return audio?.Play(filename, playerFilter, uid, audioParams); + return audio?.Play(filename, playerFilter, uid, _recordReplay, audioParams); } /// @@ -60,7 +63,7 @@ namespace Robust.Shared.Audio // Some timers try to play audio after the system has shut down? entSystMan.TryGetEntitySystem(out SharedAudioSystem? audio); - return audio?.Play(filename, playerFilter, coordinates, audioParams); + return audio?.Play(filename, playerFilter, coordinates, _recordReplay, audioParams); } } } diff --git a/Robust.Shared/GameObjects/EntitySystem.cs b/Robust.Shared/GameObjects/EntitySystem.cs index e85644852..cd599dd83 100644 --- a/Robust.Shared/GameObjects/EntitySystem.cs +++ b/Robust.Shared/GameObjects/EntitySystem.cs @@ -7,7 +7,9 @@ using JetBrains.Annotations; using Robust.Shared.IoC; using Robust.Shared.Network; using Robust.Shared.Player; +using Robust.Shared.Players; using Robust.Shared.Reflection; +using Robust.Shared.Replays; namespace Robust.Shared.GameObjects { @@ -21,6 +23,8 @@ namespace Robust.Shared.GameObjects public abstract partial class EntitySystem : IEntitySystem { [Dependency] protected readonly EntityManager EntityManager; + [Dependency] private readonly ISharedPlayerManager _playerMan = default!; + [Dependency] private readonly IReplayRecordingManager _replayMan = default!; protected internal List UpdatesAfter { get; } = new(); protected internal List UpdatesBefore { get; } = new(); @@ -90,14 +94,34 @@ namespace Robust.Shared.GameObjects EntityManager.EntityNetManager?.SendSystemNetworkMessage(message, channel); } - protected void RaiseNetworkEvent(EntityEventArgs message, Filter filter) + protected void RaiseNetworkEvent(EntityEventArgs message, ICommonSession session) { + EntityManager.EntityNetManager?.SendSystemNetworkMessage(message, session.ConnectedClient); + } + + /// + /// Raises a networked event with some filter. + /// + /// The event to send + /// The filter that specifies recipients + /// Optional bool specifying whether or not to save this event to replays. + protected void RaiseNetworkEvent(EntityEventArgs message, Filter filter, bool recordReplay = true) + { + if (recordReplay) + _replayMan.QueueReplayMessage(message); + foreach (var session in filter.Recipients) { EntityManager.EntityNetManager?.SendSystemNetworkMessage(message, session.ConnectedClient); } } + protected void RaiseNetworkEvent(EntityEventArgs message, EntityUid recipient) + { + if (_playerMan.TryGetSessionByEntity(recipient, out var session)) + EntityManager.EntityNetManager?.SendSystemNetworkMessage(message, session.ConnectedClient); + } + protected void RaiseLocalEvent(EntityUid uid, TEvent args, bool broadcast = false) where TEvent : notnull { diff --git a/Robust.Shared/GameObjects/IEntityNetworkManager.cs b/Robust.Shared/GameObjects/IEntityNetworkManager.cs index 2cd68e5e4..e0daa06f6 100644 --- a/Robust.Shared/GameObjects/IEntityNetworkManager.cs +++ b/Robust.Shared/GameObjects/IEntityNetworkManager.cs @@ -25,7 +25,8 @@ namespace Robust.Shared.GameObjects /// Server: Use the alternative overload to send to a single client. /// /// Message that should be sent. - void SendSystemNetworkMessage(EntityEventArgs message); + /// Whether or not this message should be saved to replays. + void SendSystemNetworkMessage(EntityEventArgs message, bool recordReplay = true); void SendSystemNetworkMessage(EntityEventArgs message, uint sequence) { diff --git a/Robust.Shared/GameObjects/Systems/SharedAudioSystem.cs b/Robust.Shared/GameObjects/Systems/SharedAudioSystem.cs index 52ae2057b..1ec0fe39b 100644 --- a/Robust.Shared/GameObjects/Systems/SharedAudioSystem.cs +++ b/Robust.Shared/GameObjects/Systems/SharedAudioSystem.cs @@ -1,12 +1,11 @@ using Robust.Shared.Audio; +using Robust.Shared.Configuration; using Robust.Shared.IoC; using Robust.Shared.Map; using Robust.Shared.Player; +using Robust.Shared.Players; using Robust.Shared.Prototypes; using Robust.Shared.Random; -using System; -using Robust.Shared.Configuration; -using Robust.Shared.Players; namespace Robust.Shared.GameObjects; public abstract class SharedAudioSystem : EntitySystem @@ -53,13 +52,59 @@ public abstract class SharedAudioSystem : EntitySystem /// The resource path to the OGG Vorbis file to play. /// The set of players that will hear the sound. /// Audio parameters to apply when playing the sound. - public abstract IPlayingAudioStream? PlayGlobal(string filename, Filter playerFilter, AudioParams? audioParams = null); + public abstract IPlayingAudioStream? PlayGlobal(string filename, Filter playerFilter, bool recordReplay, AudioParams? audioParams = null); - public IPlayingAudioStream? PlayGlobal(SoundSpecifier? sound, Filter playerFilter, AudioParams? audioParams = null) + + /// + /// Play an audio file globally, without position. + /// + /// The sound specifier that points the audio file(s) that should be played. + /// The set of players that will hear the sound. + /// Audio parameters to apply when playing the sound. + public IPlayingAudioStream? PlayGlobal(SoundSpecifier? sound, Filter playerFilter, bool recordReplay, AudioParams? audioParams = null) { - return sound == null ? null : PlayGlobal(GetSound(sound), playerFilter, audioParams ?? sound.Params); + return sound == null ? null : PlayGlobal(GetSound(sound), playerFilter, recordReplay, audioParams ?? sound.Params); } + /// + /// Play an audio file globally, without position. + /// + /// The resource path to the OGG Vorbis file to play. + /// The player that will hear the sound. + /// Audio parameters to apply when playing the sound. + public abstract IPlayingAudioStream? PlayGlobal(string filename, ICommonSession recipient, AudioParams? audioParams = null); + + /// + /// Play an audio file globally, without position. + /// + /// The sound specifier that points the audio file(s) that should be played. + /// The player that will hear the sound. + /// Audio parameters to apply when playing the sound. + public IPlayingAudioStream? PlayGlobal(SoundSpecifier? sound, ICommonSession recipient, AudioParams? audioParams = null) + { + return sound == null ? null : PlayGlobal(GetSound(sound), recipient, audioParams ?? sound.Params); + } + + /// + /// Play an audio file globally, without position. + /// + /// The resource path to the OGG Vorbis file to play. + /// The player that will hear the sound. + /// Audio parameters to apply when playing the sound. + public abstract IPlayingAudioStream? PlayGlobal(string filename, EntityUid recipient, AudioParams? audioParams = null); + + /// + /// Play an audio file globally, without position. + /// + /// The sound specifier that points the audio file(s) that should be played. + /// The player that will hear the sound. + /// Audio parameters to apply when playing the sound. + public IPlayingAudioStream? PlayGlobal(SoundSpecifier? sound, EntityUid recipient, AudioParams? audioParams = null) + { + return sound == null ? null : PlayGlobal(GetSound(sound), recipient, audioParams ?? sound.Params); + } + + // TODO rename to PlayEntity /// /// Play an audio file following an entity. /// @@ -67,8 +112,27 @@ public abstract class SharedAudioSystem : EntitySystem /// The set of players that will hear the sound. /// The UID of the entity "emitting" the audio. /// Audio parameters to apply when playing the sound. - public abstract IPlayingAudioStream? Play(string filename, Filter playerFilter, EntityUid uid, AudioParams? audioParams = null); + public abstract IPlayingAudioStream? Play(string filename, Filter playerFilter, EntityUid uid, bool recordReplay, AudioParams? audioParams = null); + /// + /// Play an audio file following an entity. + /// + /// The resource path to the OGG Vorbis file to play. + /// The player that will hear the sound. + /// The UID of the entity "emitting" the audio. + /// Audio parameters to apply when playing the sound. + public abstract IPlayingAudioStream? PlayEntity(string filename, ICommonSession recipient, EntityUid uid, AudioParams? audioParams = null); + + /// + /// Play an audio file following an entity. + /// + /// The resource path to the OGG Vorbis file to play. + /// The player that will hear the sound. + /// The UID of the entity "emitting" the audio. + /// Audio parameters to apply when playing the sound. + public abstract IPlayingAudioStream? PlayEntity(string filename, EntityUid recipient, EntityUid uid, AudioParams? audioParams = null); + + // TODO rename to PlayEntity /// /// Play an audio file following an entity. /// @@ -76,9 +140,33 @@ public abstract class SharedAudioSystem : EntitySystem /// The set of players that will hear the sound. /// The UID of the entity "emitting" the audio. /// Audio parameters to apply when playing the sound. Defaults to using the sound specifier's parameters - public IPlayingAudioStream? Play(SoundSpecifier? sound, Filter playerFilter, EntityUid uid, AudioParams? audioParams = null) + public IPlayingAudioStream? Play(SoundSpecifier? sound, Filter playerFilter, EntityUid uid, bool recordReplay, AudioParams? audioParams = null) { - return sound == null ? null : Play(GetSound(sound), playerFilter, uid, audioParams ?? sound.Params); + return sound == null ? null : Play(GetSound(sound), playerFilter, uid, recordReplay, audioParams ?? sound.Params); + } + + /// + /// Play an audio file following an entity. + /// + /// The sound specifier that points the audio file(s) that should be played. + /// The player that will hear the sound. + /// The UID of the entity "emitting" the audio. + /// Audio parameters to apply when playing the sound. Defaults to using the sound specifier's parameters + public IPlayingAudioStream? PlayEntity(SoundSpecifier? sound, ICommonSession recipient, EntityUid uid, AudioParams? audioParams = null) + { + return sound == null ? null : PlayEntity(GetSound(sound), recipient, uid, audioParams ?? sound.Params); + } + + /// + /// Play an audio file following an entity. + /// + /// The sound specifier that points the audio file(s) that should be played. + /// The player that will hear the sound. + /// The UID of the entity "emitting" the audio. + /// Audio parameters to apply when playing the sound. Defaults to using the sound specifier's parameters + public IPlayingAudioStream? PlayEntity(SoundSpecifier? sound, EntityUid recipient, EntityUid uid, AudioParams? audioParams = null) + { + return sound == null ? null : PlayEntity(GetSound(sound), recipient, uid, audioParams ?? sound.Params); } /// @@ -89,7 +177,18 @@ public abstract class SharedAudioSystem : EntitySystem /// Audio parameters to apply when playing the sound. Defaults to using the sound specifier's parameters public IPlayingAudioStream? PlayPvs(SoundSpecifier? sound, EntityUid uid, AudioParams? audioParams = null) { - return sound == null ? null : Play(sound, Filter.Pvs(uid, entityManager: EntityManager), uid, audioParams); + return sound == null ? null : PlayPvs(GetSound(sound), uid, audioParams); + } + + /// + /// Play an audio file following an entity for every entity in PVS range. + /// + /// The resource path to the OGG Vorbis file to play. + /// The UID of the entity "emitting" the audio. + /// Audio parameters to apply when playing the sound. Defaults to using the sound specifier's parameters + public IPlayingAudioStream? PlayPvs(string filename, EntityUid uid, AudioParams? audioParams = null) + { + return Play(filename, Filter.Pvs(uid, entityManager: EntityManager), uid, true, audioParams); } /// @@ -103,6 +202,7 @@ public abstract class SharedAudioSystem : EntitySystem /// Audio parameters to apply when playing the sound. Defaults to using the sound specifier's parameters public abstract IPlayingAudioStream? PlayPredicted(SoundSpecifier? sound, EntityUid source, EntityUid? user, AudioParams? audioParams = null); + // TODO rename to play static /// /// Play an audio file at a static position. /// @@ -110,8 +210,27 @@ public abstract class SharedAudioSystem : EntitySystem /// The set of players that will hear the sound. /// The coordinates at which to play the audio. /// Audio parameters to apply when playing the sound. - public abstract IPlayingAudioStream? Play(string filename, Filter playerFilter, EntityCoordinates coordinates, AudioParams? audioParams = null); + public abstract IPlayingAudioStream? Play(string filename, Filter playerFilter, EntityCoordinates coordinates, bool recordReplay, AudioParams? audioParams = null); + /// + /// Play an audio file at a static position. + /// + /// The resource path to the OGG Vorbis file to play. + /// The player that will hear the sound. + /// The coordinates at which to play the audio. + /// Audio parameters to apply when playing the sound. + public abstract IPlayingAudioStream? PlayStatic(string filename, ICommonSession recipient, EntityCoordinates coordinates, AudioParams? audioParams = null); + + /// + /// Play an audio file at a static position. + /// + /// The resource path to the OGG Vorbis file to play. + /// The player that will hear the sound. + /// The coordinates at which to play the audio. + /// Audio parameters to apply when playing the sound. + public abstract IPlayingAudioStream? PlayStatic(string filename, EntityUid recipient, EntityCoordinates coordinates, AudioParams? audioParams = null); + + // TODO rename to play static /// /// Play an audio file at a static position. /// @@ -119,9 +238,33 @@ public abstract class SharedAudioSystem : EntitySystem /// The set of players that will hear the sound. /// The coordinates at which to play the audio. /// Audio parameters to apply when playing the sound. - public IPlayingAudioStream? Play(SoundSpecifier? sound, Filter playerFilter, EntityCoordinates coordinates, AudioParams? audioParams = null) + public IPlayingAudioStream? Play(SoundSpecifier? sound, Filter playerFilter, EntityCoordinates coordinates, bool recordReplay, AudioParams? audioParams = null) { - return sound == null ? null : Play(GetSound(sound), playerFilter, coordinates, audioParams ?? sound.Params); + return sound == null ? null : Play(GetSound(sound), playerFilter, coordinates, recordReplay, audioParams ?? sound.Params); + } + + /// + /// Play an audio file at a static position. + /// + /// The sound specifier that points the audio file(s) that should be played. + /// The player that will hear the sound. + /// The coordinates at which to play the audio. + /// Audio parameters to apply when playing the sound. + public IPlayingAudioStream? PlayStatic(SoundSpecifier? sound, ICommonSession recipient, EntityCoordinates coordinates, AudioParams? audioParams = null) + { + return sound == null ? null : PlayStatic(GetSound(sound), recipient, coordinates, audioParams ?? sound.Params); + } + + /// + /// Play an audio file at a static position. + /// + /// The sound specifier that points the audio file(s) that should be played. + /// The player that will hear the sound. + /// The coordinates at which to play the audio. + /// Audio parameters to apply when playing the sound. + public IPlayingAudioStream? PlayStatic(SoundSpecifier? sound, EntityUid recipient, EntityCoordinates coordinates, AudioParams? audioParams = null) + { + return sound == null ? null : PlayStatic(GetSound(sound), recipient, coordinates, audioParams ?? sound.Params); } protected EntityCoordinates GetFallbackCoordinates(MapCoordinates mapCoordinates) diff --git a/Robust.Shared/Players/ISharedPlayerManager.cs b/Robust.Shared/Players/ISharedPlayerManager.cs index 967819e30..163c322f0 100644 --- a/Robust.Shared/Players/ISharedPlayerManager.cs +++ b/Robust.Shared/Players/ISharedPlayerManager.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using Robust.Shared.GameObjects; using Robust.Shared.Network; namespace Robust.Shared.Players @@ -13,6 +14,8 @@ namespace Robust.Shared.Players IEnumerable Sessions { get; } + bool TryGetSessionByEntity(EntityUid uid, [NotNullWhen(true)] out ICommonSession? session); + /// /// Number of players currently connected to this server. /// diff --git a/Robust.Shared/Replays/IReplayRecordingManager.cs b/Robust.Shared/Replays/IReplayRecordingManager.cs new file mode 100644 index 000000000..5501f8fc5 --- /dev/null +++ b/Robust.Shared/Replays/IReplayRecordingManager.cs @@ -0,0 +1,23 @@ +using Robust.Shared.GameObjects; + +namespace Robust.Shared.Replays; + +public interface IReplayRecordingManager +{ + /// + /// Queues some net-serializable data to be saved for replaying + /// + /// + /// The queued object is typically something like an , so that replays can + /// simulate receiving networked messages. However, this can really be any serializable data and could be used + /// for saving server-exclusive data like power net or atmos pipe-net data for replaying. Alternatively, those + /// could also just use networked component states on entities that are in null space and hidden from all + /// players (but still saved to replays). + /// + void QueueReplayMessage(object args); + + /// + /// Whether the server is currently recording replay data. + /// + bool Recording { get; } +} diff --git a/Robust.UnitTesting/Server/RobustServerSimulation.cs b/Robust.UnitTesting/Server/RobustServerSimulation.cs index 841f67659..6eb881835 100644 --- a/Robust.UnitTesting/Server/RobustServerSimulation.cs +++ b/Robust.UnitTesting/Server/RobustServerSimulation.cs @@ -38,6 +38,9 @@ using Robust.Shared.Serialization.Manager; using Robust.Shared.Serialization.Manager.Attributes; using Robust.Shared.Threading; using Robust.Shared.Timing; +using Robust.Server.Replays; +using Robust.Shared.Replays; +using Robust.Shared.Players; namespace Robust.UnitTesting.Server { @@ -232,7 +235,11 @@ namespace Robust.UnitTesting.Server container.Register(); // god help you if you actually need to test pvs functions container.RegisterInstance(new Mock().Object); + container.RegisterInstance(new Mock().Object); container.RegisterInstance(new Mock().Object); + container.RegisterInstance(new Mock().Object); + container.RegisterInstance(new Mock().Object); + container.RegisterInstance(new Mock().Object); _diFactory?.Invoke(container); container.BuildGraph();