Add support for client-side replays (#17168)
This commit is contained in:
@@ -11,6 +11,7 @@ using Robust.Shared.Map;
|
|||||||
using Robust.Shared.Player;
|
using Robust.Shared.Player;
|
||||||
using Robust.Shared.Players;
|
using Robust.Shared.Players;
|
||||||
using Robust.Shared.Prototypes;
|
using Robust.Shared.Prototypes;
|
||||||
|
using Robust.Shared.Replays;
|
||||||
using Robust.Shared.Timing;
|
using Robust.Shared.Timing;
|
||||||
using Robust.Shared.Utility;
|
using Robust.Shared.Utility;
|
||||||
|
|
||||||
@@ -26,6 +27,7 @@ namespace Content.Client.Popups
|
|||||||
[Dependency] private readonly IResourceCache _resource = default!;
|
[Dependency] private readonly IResourceCache _resource = default!;
|
||||||
[Dependency] private readonly IGameTiming _timing = default!;
|
[Dependency] private readonly IGameTiming _timing = default!;
|
||||||
[Dependency] private readonly IUserInterfaceManager _uiManager = default!;
|
[Dependency] private readonly IUserInterfaceManager _uiManager = default!;
|
||||||
|
[Dependency] private readonly IReplayRecordingManager _replayRecording = default!;
|
||||||
|
|
||||||
public IReadOnlyList<WorldPopupLabel> WorldLabels => _aliveWorldLabels;
|
public IReadOnlyList<WorldPopupLabel> WorldLabels => _aliveWorldLabels;
|
||||||
public IReadOnlyList<CursorPopupLabel> CursorLabels => _aliveCursorLabels;
|
public IReadOnlyList<CursorPopupLabel> CursorLabels => _aliveCursorLabels;
|
||||||
@@ -52,8 +54,16 @@ namespace Content.Client.Popups
|
|||||||
.RemoveOverlay<PopupOverlay>();
|
.RemoveOverlay<PopupOverlay>();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void PopupMessage(string message, PopupType type, EntityCoordinates coordinates, EntityUid? entity = null)
|
private void PopupMessage(string message, PopupType type, EntityCoordinates coordinates, EntityUid? entity, bool recordReplay)
|
||||||
{
|
{
|
||||||
|
if (recordReplay && _replayRecording.IsRecording)
|
||||||
|
{
|
||||||
|
if (entity != null)
|
||||||
|
_replayRecording.RecordClientMessage(new PopupEntityEvent(message, type, entity.Value));
|
||||||
|
else
|
||||||
|
_replayRecording.RecordClientMessage(new PopupCoordinatesEvent(message, type, coordinates));
|
||||||
|
}
|
||||||
|
|
||||||
var label = new WorldPopupLabel(coordinates)
|
var label = new WorldPopupLabel(coordinates)
|
||||||
{
|
{
|
||||||
Text = message,
|
Text = message,
|
||||||
@@ -66,23 +76,26 @@ namespace Content.Client.Popups
|
|||||||
#region Abstract Method Implementations
|
#region Abstract Method Implementations
|
||||||
public override void PopupCoordinates(string message, EntityCoordinates coordinates, PopupType type = PopupType.Small)
|
public override void PopupCoordinates(string message, EntityCoordinates coordinates, PopupType type = PopupType.Small)
|
||||||
{
|
{
|
||||||
PopupMessage(message, type, coordinates, null);
|
PopupMessage(message, type, coordinates, null, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void PopupCoordinates(string message, EntityCoordinates coordinates, ICommonSession recipient, PopupType type = PopupType.Small)
|
public override void PopupCoordinates(string message, EntityCoordinates coordinates, ICommonSession recipient, PopupType type = PopupType.Small)
|
||||||
{
|
{
|
||||||
if (_playerManager.LocalPlayer?.Session == recipient)
|
if (_playerManager.LocalPlayer?.Session == recipient)
|
||||||
PopupMessage(message, type, coordinates, null);
|
PopupMessage(message, type, coordinates, null, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void PopupCoordinates(string message, EntityCoordinates coordinates, EntityUid recipient, PopupType type = PopupType.Small)
|
public override void PopupCoordinates(string message, EntityCoordinates coordinates, EntityUid recipient, PopupType type = PopupType.Small)
|
||||||
{
|
{
|
||||||
if (_playerManager.LocalPlayer?.ControlledEntity == recipient)
|
if (_playerManager.LocalPlayer?.ControlledEntity == recipient)
|
||||||
PopupMessage(message, type, coordinates, null);
|
PopupMessage(message, type, coordinates, null, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void PopupCursor(string message, PopupType type = PopupType.Small)
|
private void PopupCursorInternal(string message, PopupType type, bool recordReplay)
|
||||||
{
|
{
|
||||||
|
if (recordReplay && _replayRecording.IsRecording)
|
||||||
|
_replayRecording.RecordClientMessage(new PopupCursorEvent(message, type));
|
||||||
|
|
||||||
var label = new CursorPopupLabel(_inputManager.MouseScreenPosition)
|
var label = new CursorPopupLabel(_inputManager.MouseScreenPosition)
|
||||||
{
|
{
|
||||||
Text = message,
|
Text = message,
|
||||||
@@ -92,6 +105,9 @@ namespace Content.Client.Popups
|
|||||||
_aliveCursorLabels.Add(label);
|
_aliveCursorLabels.Add(label);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public override void PopupCursor(string message, PopupType type = PopupType.Small)
|
||||||
|
=> PopupCursorInternal(message, type, true);
|
||||||
|
|
||||||
public override void PopupCursor(string message, ICommonSession recipient, PopupType type = PopupType.Small)
|
public override void PopupCursor(string message, ICommonSession recipient, PopupType type = PopupType.Small)
|
||||||
{
|
{
|
||||||
if (_playerManager.LocalPlayer?.Session == recipient)
|
if (_playerManager.LocalPlayer?.Session == recipient)
|
||||||
@@ -137,12 +153,8 @@ namespace Content.Client.Popups
|
|||||||
|
|
||||||
public override void PopupEntity(string message, EntityUid uid, PopupType type = PopupType.Small)
|
public override void PopupEntity(string message, EntityUid uid, PopupType type = PopupType.Small)
|
||||||
{
|
{
|
||||||
if (!EntityManager.EntityExists(uid))
|
if (TryComp(uid, out TransformComponent? transform))
|
||||||
return;
|
PopupMessage(message, type, transform.Coordinates, uid, true);
|
||||||
|
|
||||||
var transform = EntityManager.GetComponent<TransformComponent>(uid);
|
|
||||||
|
|
||||||
PopupMessage(message, type, transform.Coordinates, uid);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
@@ -151,17 +163,18 @@ namespace Content.Client.Popups
|
|||||||
|
|
||||||
private void OnPopupCursorEvent(PopupCursorEvent ev)
|
private void OnPopupCursorEvent(PopupCursorEvent ev)
|
||||||
{
|
{
|
||||||
PopupCursor(ev.Message, ev.Type);
|
PopupCursorInternal(ev.Message, ev.Type, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnPopupCoordinatesEvent(PopupCoordinatesEvent ev)
|
private void OnPopupCoordinatesEvent(PopupCoordinatesEvent ev)
|
||||||
{
|
{
|
||||||
PopupCoordinates(ev.Message, ev.Coordinates, ev.Type);
|
PopupMessage(ev.Message, ev.Type, ev.Coordinates, null, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnPopupEntityEvent(PopupEntityEvent ev)
|
private void OnPopupEntityEvent(PopupEntityEvent ev)
|
||||||
{
|
{
|
||||||
PopupEntity(ev.Message, ev.Uid, ev.Type);
|
if (TryComp(ev.Uid, out TransformComponent? transform))
|
||||||
|
PopupMessage(ev.Message, ev.Type, transform.Coordinates, ev.Uid, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnRoundRestart(RoundRestartCleanupEvent ev)
|
private void OnRoundRestart(RoundRestartCleanupEvent ev)
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ using Robust.Client.State;
|
|||||||
using Robust.Client.Timing;
|
using Robust.Client.Timing;
|
||||||
using Robust.Client.UserInterface;
|
using Robust.Client.UserInterface;
|
||||||
using Robust.Shared.ContentPack;
|
using Robust.Shared.ContentPack;
|
||||||
|
using Robust.Shared.Serialization.Markdown.Mapping;
|
||||||
using Robust.Shared.Utility;
|
using Robust.Shared.Utility;
|
||||||
|
|
||||||
namespace Content.Client.Replay;
|
namespace Content.Client.Replay;
|
||||||
@@ -99,6 +100,8 @@ public sealed class ContentReplayPlaybackManager
|
|||||||
{
|
{
|
||||||
switch (message)
|
switch (message)
|
||||||
{
|
{
|
||||||
|
case BoundUserInterfaceMessage:
|
||||||
|
break; // TODO REPLAYS refactor BUIs
|
||||||
case ChatMessage chat:
|
case ChatMessage chat:
|
||||||
// Just pass on the chat message to the UI controller, but skip speech-bubbles if we are fast-forwarding.
|
// Just pass on the chat message to the UI controller, but skip speech-bubbles if we are fast-forwarding.
|
||||||
_uiMan.GetUIController<ChatUIController>().ProcessChatMessage(chat, speechBubble: !skipEffects);
|
_uiMan.GetUIController<ChatUIController>().ProcessChatMessage(chat, speechBubble: !skipEffects);
|
||||||
@@ -129,8 +132,7 @@ public sealed class ContentReplayPlaybackManager
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void OnReplayPlaybackStarted(MappingDataNode metadata, List<object> objects)
|
||||||
private void OnReplayPlaybackStarted()
|
|
||||||
{
|
{
|
||||||
_conGrp.Implementation = new ReplayConGroup();
|
_conGrp.Implementation = new ReplayConGroup();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ using Content.Shared.Movement.Components;
|
|||||||
using Robust.Client.GameObjects;
|
using Robust.Client.GameObjects;
|
||||||
using Robust.Shared.Map;
|
using Robust.Shared.Map;
|
||||||
using Robust.Shared.Map.Components;
|
using Robust.Shared.Map.Components;
|
||||||
|
using Robust.Shared.Network;
|
||||||
|
|
||||||
namespace Content.Client.Replay.Spectator;
|
namespace Content.Client.Replay.Spectator;
|
||||||
|
|
||||||
@@ -13,61 +14,99 @@ public sealed partial class ReplaySpectatorSystem
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Simple struct containing position & rotation data for maintaining a persistent view when jumping around in time.
|
/// Simple struct containing position & rotation data for maintaining a persistent view when jumping around in time.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public struct SpectatorPosition
|
public struct SpectatorData
|
||||||
{
|
{
|
||||||
// TODO REPLAYS handle ghost-following.
|
// TODO REPLAYS handle ghost-following.
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The current entity being spectated.
|
||||||
|
/// </summary>
|
||||||
public EntityUid Entity;
|
public EntityUid Entity;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The player that was originally controlling <see cref="Entity"/>
|
||||||
|
/// </summary>
|
||||||
|
public NetUserId? Controller;
|
||||||
|
|
||||||
public (EntityCoordinates Coords, Angle Rot)? Local;
|
public (EntityCoordinates Coords, Angle Rot)? Local;
|
||||||
public (EntityCoordinates Coords, Angle Rot)? World;
|
public (EntityCoordinates Coords, Angle Rot)? World;
|
||||||
public (EntityUid? Ent, Angle Rot)? Eye;
|
public (EntityUid? Ent, Angle Rot)? Eye;
|
||||||
}
|
}
|
||||||
|
|
||||||
public SpectatorPosition GetSpectatorPosition()
|
public SpectatorData GetSpectatorData()
|
||||||
{
|
{
|
||||||
var obs = new SpectatorPosition();
|
var data = new SpectatorData();
|
||||||
if (_player.LocalPlayer?.ControlledEntity is { } player && TryComp(player, out TransformComponent? xform) && xform.MapUid != null)
|
|
||||||
|
if (_player.LocalPlayer?.ControlledEntity is not { } player)
|
||||||
|
return data;
|
||||||
|
|
||||||
|
foreach (var session in _player.Sessions)
|
||||||
{
|
{
|
||||||
obs.Local = (xform.Coordinates, xform.LocalRotation);
|
if (session.UserId == _player.LocalPlayer?.UserId)
|
||||||
obs.World = (new(xform.MapUid.Value, xform.WorldPosition), xform.WorldRotation);
|
continue;
|
||||||
|
|
||||||
if (TryComp(player, out InputMoverComponent? mover))
|
if (session.AttachedEntity == player)
|
||||||
obs.Eye = (mover.RelativeEntity, mover.TargetRelativeRotation);
|
{
|
||||||
|
data.Controller = session.UserId;
|
||||||
obs.Entity = player;
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return obs;
|
if (!TryComp(player, out TransformComponent? xform) || xform.MapUid == null)
|
||||||
|
return data;
|
||||||
|
|
||||||
|
data.Local = (xform.Coordinates, xform.LocalRotation);
|
||||||
|
data.World = (new(xform.MapUid.Value, xform.WorldPosition), xform.WorldRotation);
|
||||||
|
|
||||||
|
if (TryComp(player, out InputMoverComponent? mover))
|
||||||
|
data.Eye = (mover.RelativeEntity, mover.TargetRelativeRotation);
|
||||||
|
|
||||||
|
data.Entity = player;
|
||||||
|
|
||||||
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnBeforeSetTick()
|
private void OnBeforeSetTick()
|
||||||
{
|
{
|
||||||
_oldPosition = GetSpectatorPosition();
|
_spectatorData = GetSpectatorData();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnAfterSetTick()
|
private void OnAfterSetTick()
|
||||||
{
|
{
|
||||||
if (_oldPosition != null)
|
if (_spectatorData != null)
|
||||||
SetSpectatorPosition(_oldPosition.Value);
|
SetSpectatorPosition(_spectatorData.Value);
|
||||||
_oldPosition = null;
|
_spectatorData = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void SetSpectatorPosition(SpectatorPosition spectatorPosition)
|
public void SetSpectatorPosition(SpectatorData data)
|
||||||
{
|
{
|
||||||
if (Exists(spectatorPosition.Entity) && Transform(spectatorPosition.Entity).MapID != MapId.Nullspace)
|
if (_player.LocalPlayer == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (data.Controller != null
|
||||||
|
&& _player.SessionsDict.TryGetValue(data.Controller.Value, out var session)
|
||||||
|
&& Exists(session.AttachedEntity)
|
||||||
|
&& Transform(session.AttachedEntity.Value).MapID != MapId.Nullspace)
|
||||||
{
|
{
|
||||||
_player.LocalPlayer!.AttachEntity(spectatorPosition.Entity, EntityManager, _client);
|
_player.LocalPlayer.AttachEntity(session.AttachedEntity.Value, EntityManager, _client);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (spectatorPosition.Local != null && spectatorPosition.Local.Value.Coords.IsValid(EntityManager))
|
if (Exists(data.Entity) && Transform(data.Entity).MapID != MapId.Nullspace)
|
||||||
{
|
{
|
||||||
var newXform = SpawnSpectatorGhost(spectatorPosition.Local.Value.Coords, false);
|
_player.LocalPlayer.AttachEntity(data.Entity, EntityManager, _client);
|
||||||
newXform.LocalRotation = spectatorPosition.Local.Value.Rot;
|
return;
|
||||||
}
|
}
|
||||||
else if (spectatorPosition.World != null && spectatorPosition.World.Value.Coords.IsValid(EntityManager))
|
|
||||||
|
if (data.Local != null && data.Local.Value.Coords.IsValid(EntityManager))
|
||||||
{
|
{
|
||||||
var newXform = SpawnSpectatorGhost(spectatorPosition.World.Value.Coords, true);
|
var newXform = SpawnSpectatorGhost(data.Local.Value.Coords, false);
|
||||||
newXform.LocalRotation = spectatorPosition.World.Value.Rot;
|
newXform.LocalRotation = data.Local.Value.Rot;
|
||||||
|
}
|
||||||
|
else if (data.World != null && data.World.Value.Coords.IsValid(EntityManager))
|
||||||
|
{
|
||||||
|
var newXform = SpawnSpectatorGhost(data.World.Value.Coords, true);
|
||||||
|
newXform.LocalRotation = data.World.Value.Rot;
|
||||||
}
|
}
|
||||||
else if (TryFindFallbackSpawn(out var coords))
|
else if (TryFindFallbackSpawn(out var coords))
|
||||||
{
|
{
|
||||||
@@ -80,15 +119,21 @@ public sealed partial class ReplaySpectatorSystem
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (spectatorPosition.Eye != null && TryComp(_player.LocalPlayer?.ControlledEntity, out InputMoverComponent? newMover))
|
if (data.Eye != null && TryComp(_player.LocalPlayer.ControlledEntity, out InputMoverComponent? newMover))
|
||||||
{
|
{
|
||||||
newMover.RelativeEntity = spectatorPosition.Eye.Value.Ent;
|
newMover.RelativeEntity = data.Eye.Value.Ent;
|
||||||
newMover.TargetRelativeRotation = newMover.RelativeRotation = spectatorPosition.Eye.Value.Rot;
|
newMover.TargetRelativeRotation = newMover.RelativeRotation = data.Eye.Value.Rot;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool TryFindFallbackSpawn(out EntityCoordinates coords)
|
private bool TryFindFallbackSpawn(out EntityCoordinates coords)
|
||||||
{
|
{
|
||||||
|
if (_replayPlayback.TryGetRecorderEntity(out var recorder))
|
||||||
|
{
|
||||||
|
coords = new EntityCoordinates(recorder.Value, default);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
var uid = EntityQuery<MapGridComponent>()
|
var uid = EntityQuery<MapGridComponent>()
|
||||||
.OrderByDescending(x => x.LocalAABB.Size.LengthSquared)
|
.OrderByDescending(x => x.LocalAABB.Size.LengthSquared)
|
||||||
.FirstOrDefault()?.Owner;
|
.FirstOrDefault()?.Owner;
|
||||||
|
|||||||
@@ -84,6 +84,7 @@ public sealed partial class ReplaySpectatorSystem
|
|||||||
|
|
||||||
_stateMan.RequestStateChange<ReplayGhostState>();
|
_stateMan.RequestStateChange<ReplayGhostState>();
|
||||||
|
|
||||||
|
_spectatorData = GetSpectatorData();
|
||||||
return xform;
|
return xform;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ using Robust.Client.Player;
|
|||||||
using Robust.Client.Replays.Playback;
|
using Robust.Client.Replays.Playback;
|
||||||
using Robust.Client.State;
|
using Robust.Client.State;
|
||||||
using Robust.Shared.Console;
|
using Robust.Shared.Console;
|
||||||
|
using Robust.Shared.Network;
|
||||||
|
using Robust.Shared.Serialization.Markdown.Mapping;
|
||||||
|
|
||||||
namespace Content.Client.Replay.Spectator;
|
namespace Content.Client.Replay.Spectator;
|
||||||
|
|
||||||
@@ -29,7 +31,7 @@ public sealed partial class ReplaySpectatorSystem : EntitySystem
|
|||||||
[Dependency] private readonly SharedContentEyeSystem _eye = default!;
|
[Dependency] private readonly SharedContentEyeSystem _eye = default!;
|
||||||
[Dependency] private readonly IReplayPlaybackManager _replayPlayback = default!;
|
[Dependency] private readonly IReplayPlaybackManager _replayPlayback = default!;
|
||||||
|
|
||||||
private SpectatorPosition? _oldPosition;
|
private SpectatorData? _spectatorData;
|
||||||
public const string SpectateCmd = "replay_spectate";
|
public const string SpectateCmd = "replay_spectate";
|
||||||
|
|
||||||
public override void Initialize()
|
public override void Initialize()
|
||||||
@@ -58,15 +60,19 @@ public sealed partial class ReplaySpectatorSystem : EntitySystem
|
|||||||
_replayPlayback.ReplayPlaybackStopped -= OnPlaybackStopped;
|
_replayPlayback.ReplayPlaybackStopped -= OnPlaybackStopped;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnPlaybackStarted()
|
private void OnPlaybackStarted(MappingDataNode yamlMappingNode, List<object> objects)
|
||||||
{
|
{
|
||||||
InitializeMovement();
|
InitializeMovement();
|
||||||
SetSpectatorPosition(default);
|
|
||||||
_conHost.RegisterCommand(SpectateCmd,
|
_conHost.RegisterCommand(SpectateCmd,
|
||||||
Loc.GetString("cmd-replay-spectate-desc"),
|
Loc.GetString("cmd-replay-spectate-desc"),
|
||||||
Loc.GetString("cmd-replay-spectate-help"),
|
Loc.GetString("cmd-replay-spectate-help"),
|
||||||
SpectateCommand,
|
SpectateCommand,
|
||||||
SpectateCompletions);
|
SpectateCompletions);
|
||||||
|
|
||||||
|
if (_replayPlayback.TryGetRecorderEntity(out var recorder))
|
||||||
|
SpectateEntity(recorder.Value);
|
||||||
|
else
|
||||||
|
SetSpectatorPosition(default);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnPlaybackStopped()
|
private void OnPlaybackStopped()
|
||||||
|
|||||||
@@ -66,9 +66,10 @@ public sealed class SubFloorHideSystem : SharedSubFloorHideSystem
|
|||||||
|
|
||||||
private void UpdateAll()
|
private void UpdateAll()
|
||||||
{
|
{
|
||||||
foreach (var (_, appearance) in EntityManager.EntityQuery<SubFloorHideComponent, AppearanceComponent>(true))
|
var query = AllEntityQuery<SubFloorHideComponent, AppearanceComponent>();
|
||||||
|
while (query.MoveNext(out var uid, out _, out var appearance))
|
||||||
{
|
{
|
||||||
_appearance.MarkDirty(appearance, true);
|
_appearance.QueueUpdate(uid, appearance);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ using Robust.Shared.Configuration;
|
|||||||
using Robust.Shared.Input.Binding;
|
using Robust.Shared.Input.Binding;
|
||||||
using Robust.Shared.Map;
|
using Robust.Shared.Map;
|
||||||
using Robust.Shared.Network;
|
using Robust.Shared.Network;
|
||||||
|
using Robust.Shared.Replays;
|
||||||
using Robust.Shared.Timing;
|
using Robust.Shared.Timing;
|
||||||
using Robust.Shared.Utility;
|
using Robust.Shared.Utility;
|
||||||
|
|
||||||
@@ -46,6 +47,8 @@ public sealed class ChatUIController : UIController
|
|||||||
[Dependency] private readonly IPlayerManager _player = default!;
|
[Dependency] private readonly IPlayerManager _player = default!;
|
||||||
[Dependency] private readonly IStateManager _state = default!;
|
[Dependency] private readonly IStateManager _state = default!;
|
||||||
[Dependency] private readonly IGameTiming _timing = default!;
|
[Dependency] private readonly IGameTiming _timing = default!;
|
||||||
|
[Dependency] private readonly IReplayRecordingManager _replayRecording = default!;
|
||||||
|
[Dependency] private readonly IConfigurationManager _cfg = default!;
|
||||||
|
|
||||||
[UISystemDependency] private readonly ExamineSystem? _examine = default;
|
[UISystemDependency] private readonly ExamineSystem? _examine = default;
|
||||||
[UISystemDependency] private readonly GhostSystem? _ghost = default;
|
[UISystemDependency] private readonly GhostSystem? _ghost = default;
|
||||||
@@ -758,7 +761,17 @@ public sealed class ChatUIController : UIController
|
|||||||
_manager.SendMessage(text, prefixChannel == 0 ? channel : prefixChannel);
|
_manager.SendMessage(text, prefixChannel == 0 ? channel : prefixChannel);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnChatMessage(MsgChatMessage message) => ProcessChatMessage(message.Message);
|
private void OnChatMessage(MsgChatMessage message)
|
||||||
|
{
|
||||||
|
var msg = message.Message;
|
||||||
|
ProcessChatMessage(msg);
|
||||||
|
|
||||||
|
if ((msg.Channel & ChatChannel.AdminRelated) == 0 ||
|
||||||
|
_cfg.GetCVar(CCVars.ReplayRecordAdminChat))
|
||||||
|
{
|
||||||
|
_replayRecording.RecordClientMessage(msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public void ProcessChatMessage(ChatMessage msg, bool speechBubble = true)
|
public void ProcessChatMessage(ChatMessage msg, bool speechBubble = true)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -10,11 +10,10 @@ using Robust.Client.UserInterface;
|
|||||||
using Robust.Client.UserInterface.Controls;
|
using Robust.Client.UserInterface.Controls;
|
||||||
using Robust.Shared;
|
using Robust.Shared;
|
||||||
using Robust.Shared.Configuration;
|
using Robust.Shared.Configuration;
|
||||||
|
using Robust.Shared.ContentPack;
|
||||||
using Robust.Shared.Serialization.Markdown.Value;
|
using Robust.Shared.Serialization.Markdown.Value;
|
||||||
using Robust.Shared.Utility;
|
using Robust.Shared.Utility;
|
||||||
using TerraFX.Interop.Windows;
|
|
||||||
using static Robust.Shared.Replays.IReplayRecordingManager;
|
using static Robust.Shared.Replays.IReplayRecordingManager;
|
||||||
using IResourceManager = Robust.Shared.ContentPack.IResourceManager;
|
|
||||||
|
|
||||||
namespace Content.Replay.Menu;
|
namespace Content.Replay.Menu;
|
||||||
|
|
||||||
@@ -94,7 +93,8 @@ public sealed class ReplayMainScreen : State
|
|||||||
var forkId = string.Empty;
|
var forkId = string.Empty;
|
||||||
if (data.TryGet<ValueDataNode>(Fork, out var forkNode))
|
if (data.TryGet<ValueDataNode>(Fork, out var forkNode))
|
||||||
{
|
{
|
||||||
// TODO REPLAYS somehow distribute and load from build.json?
|
// TODO Replay client build info.
|
||||||
|
// When distributing the client we need to distribute a build.json or provide these cvars some other way?
|
||||||
var clientFork = _cfg.GetCVar(CVars.BuildForkId);
|
var clientFork = _cfg.GetCVar(CVars.BuildForkId);
|
||||||
if (string.IsNullOrWhiteSpace(clientFork))
|
if (string.IsNullOrWhiteSpace(clientFork))
|
||||||
forkId = forkNode.Value;
|
forkId = forkNode.Value;
|
||||||
@@ -181,6 +181,7 @@ public sealed class ReplayMainScreen : State
|
|||||||
|
|
||||||
info.HorizontalAlignment = Control.HAlignment.Left;
|
info.HorizontalAlignment = Control.HAlignment.Left;
|
||||||
info.VerticalAlignment = Control.VAlignment.Top;
|
info.VerticalAlignment = Control.VAlignment.Top;
|
||||||
|
|
||||||
info.SetMarkup(Loc.GetString(
|
info.SetMarkup(Loc.GetString(
|
||||||
"replay-info-info",
|
"replay-info-info",
|
||||||
("file", file),
|
("file", file),
|
||||||
|
|||||||
@@ -65,7 +65,6 @@ public sealed class PlayGlobalSoundCommand : IConsoleCommand
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// TODO REPLAYS uhhh.. what to do with this?
|
|
||||||
replay = false;
|
replay = false;
|
||||||
|
|
||||||
filter = Filter.Empty();
|
filter = Filter.Empty();
|
||||||
|
|||||||
@@ -253,7 +253,7 @@ namespace Content.Server.Chat.Managers
|
|||||||
if ((channel & ChatChannel.AdminRelated) == 0 ||
|
if ((channel & ChatChannel.AdminRelated) == 0 ||
|
||||||
_configurationManager.GetCVar(CCVars.ReplayRecordAdminChat))
|
_configurationManager.GetCVar(CCVars.ReplayRecordAdminChat))
|
||||||
{
|
{
|
||||||
_replay.QueueReplayMessage(msg);
|
_replay.RecordServerMessage(msg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -271,7 +271,7 @@ namespace Content.Server.Chat.Managers
|
|||||||
if ((channel & ChatChannel.AdminRelated) == 0 ||
|
if ((channel & ChatChannel.AdminRelated) == 0 ||
|
||||||
_configurationManager.GetCVar(CCVars.ReplayRecordAdminChat))
|
_configurationManager.GetCVar(CCVars.ReplayRecordAdminChat))
|
||||||
{
|
{
|
||||||
_replay.QueueReplayMessage(msg);
|
_replay.RecordServerMessage(msg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -301,7 +301,7 @@ namespace Content.Server.Chat.Managers
|
|||||||
if ((channel & ChatChannel.AdminRelated) == 0 ||
|
if ((channel & ChatChannel.AdminRelated) == 0 ||
|
||||||
_configurationManager.GetCVar(CCVars.ReplayRecordAdminChat))
|
_configurationManager.GetCVar(CCVars.ReplayRecordAdminChat))
|
||||||
{
|
{
|
||||||
_replay.QueueReplayMessage(msg);
|
_replay.RecordServerMessage(msg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -397,7 +397,7 @@ public sealed partial class ChatSystem : SharedChatSystem
|
|||||||
_chatManager.ChatMessageToOne(ChatChannel.Whisper, obfuscatedMessage, wrappedobfuscatedMessage, source, false, session.ConnectedClient);
|
_chatManager.ChatMessageToOne(ChatChannel.Whisper, obfuscatedMessage, wrappedobfuscatedMessage, source, false, session.ConnectedClient);
|
||||||
}
|
}
|
||||||
|
|
||||||
_replay.QueueReplayMessage(new ChatMessage(ChatChannel.Whisper, message, wrappedMessage, source, MessageRangeHideChatForReplay(range)));
|
_replay.RecordServerMessage(new ChatMessage(ChatChannel.Whisper, message, wrappedMessage, source, MessageRangeHideChatForReplay(range)));
|
||||||
|
|
||||||
var ev = new EntitySpokeEvent(source, message, channel, obfuscatedMessage);
|
var ev = new EntitySpokeEvent(source, message, channel, obfuscatedMessage);
|
||||||
RaiseLocalEvent(source, ev, true);
|
RaiseLocalEvent(source, ev, true);
|
||||||
@@ -548,7 +548,7 @@ public sealed partial class ChatSystem : SharedChatSystem
|
|||||||
_chatManager.ChatMessageToOne(channel, message, wrappedMessage, source, entHideChat, session.ConnectedClient);
|
_chatManager.ChatMessageToOne(channel, message, wrappedMessage, source, entHideChat, session.ConnectedClient);
|
||||||
}
|
}
|
||||||
|
|
||||||
_replay.QueueReplayMessage(new ChatMessage(channel, message, wrappedMessage, source, MessageRangeHideChatForReplay(range)));
|
_replay.RecordServerMessage(new ChatMessage(channel, message, wrappedMessage, source, MessageRangeHideChatForReplay(range)));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ public sealed class TypingIndicatorSystem : SharedTypingIndicatorSystem
|
|||||||
// when player poses entity we want to make sure that there is typing indicator
|
// when player poses entity we want to make sure that there is typing indicator
|
||||||
EnsureComp<TypingIndicatorComponent>(ev.Entity);
|
EnsureComp<TypingIndicatorComponent>(ev.Entity);
|
||||||
// we also need appearance component to sync visual state
|
// we also need appearance component to sync visual state
|
||||||
EnsureComp<ServerAppearanceComponent>(ev.Entity);
|
EnsureComp<AppearanceComponent>(ev.Entity);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnPlayerDetached(EntityUid uid, TypingIndicatorComponent component, PlayerDetachedEvent args)
|
private void OnPlayerDetached(EntityUid uid, TypingIndicatorComponent component, PlayerDetachedEvent args)
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ public sealed partial class ExplosionSystem : EntitySystem
|
|||||||
// restricted to something like the same map, but whatever.
|
// restricted to something like the same map, but whatever.
|
||||||
_pvsSys.AddGlobalOverride(explosionEntity);
|
_pvsSys.AddGlobalOverride(explosionEntity);
|
||||||
|
|
||||||
var appearance = AddComp<ServerAppearanceComponent>(explosionEntity);
|
var appearance = AddComp<AppearanceComponent>(explosionEntity);
|
||||||
_appearance.SetData(explosionEntity, ExplosionAppearanceData.Progress, 1, appearance);
|
_appearance.SetData(explosionEntity, ExplosionAppearanceData.Progress, 1, appearance);
|
||||||
|
|
||||||
return explosionEntity;
|
return explosionEntity;
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ using Content.Server.Ghost;
|
|||||||
using Content.Server.Maps;
|
using Content.Server.Maps;
|
||||||
using Content.Server.Mind;
|
using Content.Server.Mind;
|
||||||
using Content.Server.Players;
|
using Content.Server.Players;
|
||||||
using Content.Shared.CCVar;
|
|
||||||
using Content.Shared.GameTicking;
|
using Content.Shared.GameTicking;
|
||||||
using Content.Shared.Preferences;
|
using Content.Shared.Preferences;
|
||||||
using JetBrains.Annotations;
|
using JetBrains.Annotations;
|
||||||
@@ -18,7 +17,6 @@ using Robust.Shared.Player;
|
|||||||
using Robust.Shared.Random;
|
using Robust.Shared.Random;
|
||||||
using Robust.Shared.Utility;
|
using Robust.Shared.Utility;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Content.Shared.Database;
|
using Content.Shared.Database;
|
||||||
using Robust.Shared.Asynchronous;
|
using Robust.Shared.Asynchronous;
|
||||||
|
|
||||||
@@ -66,9 +64,6 @@ namespace Content.Server.GameTicking
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[ViewVariables]
|
|
||||||
public int RoundId { get; private set; }
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns true if the round's map is eligible to be updated.
|
/// Returns true if the round's map is eligible to be updated.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
using Content.Server.Administration.Logs;
|
using Content.Server.Administration.Logs;
|
||||||
using Content.Server.Administration.Managers;
|
using Content.Server.Administration.Managers;
|
||||||
using Content.Server.Chat;
|
|
||||||
using Content.Server.Chat.Managers;
|
using Content.Server.Chat.Managers;
|
||||||
using Content.Server.Chat.Systems;
|
using Content.Server.Chat.Systems;
|
||||||
using Content.Server.Database;
|
using Content.Server.Database;
|
||||||
@@ -14,23 +13,17 @@ using Content.Server.Station.Systems;
|
|||||||
using Content.Shared.Chat;
|
using Content.Shared.Chat;
|
||||||
using Content.Shared.Damage;
|
using Content.Shared.Damage;
|
||||||
using Content.Shared.GameTicking;
|
using Content.Shared.GameTicking;
|
||||||
using Content.Shared.Mobs.Systems;
|
|
||||||
using Content.Shared.Roles;
|
using Content.Shared.Roles;
|
||||||
using Robust.Server;
|
using Robust.Server;
|
||||||
using Robust.Server.GameObjects;
|
using Robust.Server.GameObjects;
|
||||||
using Robust.Server.Maps;
|
|
||||||
using Robust.Shared.Configuration;
|
using Robust.Shared.Configuration;
|
||||||
using Robust.Shared.Console;
|
using Robust.Shared.Console;
|
||||||
#if EXCEPTION_TOLERANCE
|
#if EXCEPTION_TOLERANCE
|
||||||
using Robust.Shared.Exceptions;
|
using Robust.Shared.Exceptions;
|
||||||
#endif
|
#endif
|
||||||
using Robust.Shared.Map;
|
using Robust.Shared.Map;
|
||||||
using Robust.Shared.Network;
|
|
||||||
using Robust.Shared.Prototypes;
|
using Robust.Shared.Prototypes;
|
||||||
using Robust.Shared.Random;
|
using Robust.Shared.Random;
|
||||||
using Robust.Shared.Replays;
|
|
||||||
using Robust.Shared.Serialization.Markdown.Mapping;
|
|
||||||
using Robust.Shared.Serialization.Markdown.Value;
|
|
||||||
using Robust.Shared.Timing;
|
using Robust.Shared.Timing;
|
||||||
using Robust.Shared.Utility;
|
using Robust.Shared.Utility;
|
||||||
|
|
||||||
@@ -68,7 +61,6 @@ namespace Content.Server.GameTicking
|
|||||||
DebugTools.Assert(_prototypeManager.Index<JobPrototype>(FallbackOverflowJob).Name == FallbackOverflowJobName,
|
DebugTools.Assert(_prototypeManager.Index<JobPrototype>(FallbackOverflowJob).Name == FallbackOverflowJobName,
|
||||||
"Overflow role does not have the correct name!");
|
"Overflow role does not have the correct name!");
|
||||||
InitializeGameRules();
|
InitializeGameRules();
|
||||||
_replay.OnRecordingStarted += OnRecordingStart;
|
|
||||||
_initialized = true;
|
_initialized = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -88,12 +80,6 @@ namespace Content.Server.GameTicking
|
|||||||
base.Shutdown();
|
base.Shutdown();
|
||||||
|
|
||||||
ShutdownGameRules();
|
ShutdownGameRules();
|
||||||
_replay.OnRecordingStarted -= OnRecordingStart;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnRecordingStart((MappingDataNode, List<object>) data)
|
|
||||||
{
|
|
||||||
data.Item1["roundId"] = new ValueDataNode(RoundId.ToString());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void SendServerMessage(string message)
|
private void SendServerMessage(string message)
|
||||||
@@ -123,7 +109,6 @@ namespace Content.Server.GameTicking
|
|||||||
#if EXCEPTION_TOLERANCE
|
#if EXCEPTION_TOLERANCE
|
||||||
[Dependency] private readonly IRuntimeLog _runtimeLog = default!;
|
[Dependency] private readonly IRuntimeLog _runtimeLog = default!;
|
||||||
#endif
|
#endif
|
||||||
[Dependency] private readonly StationSystem _stationSystem = default!;
|
|
||||||
[Dependency] private readonly StationSpawningSystem _stationSpawning = default!;
|
[Dependency] private readonly StationSpawningSystem _stationSpawning = default!;
|
||||||
[Dependency] private readonly StationJobsSystem _stationJobs = default!;
|
[Dependency] private readonly StationJobsSystem _stationJobs = default!;
|
||||||
[Dependency] private readonly DamageableSystem _damageable = default!;
|
[Dependency] private readonly DamageableSystem _damageable = default!;
|
||||||
@@ -133,6 +118,5 @@ namespace Content.Server.GameTicking
|
|||||||
[Dependency] private readonly ServerUpdateManager _serverUpdates = default!;
|
[Dependency] private readonly ServerUpdateManager _serverUpdates = default!;
|
||||||
[Dependency] private readonly PlayTimeTrackingSystem _playTimeTrackings = default!;
|
[Dependency] private readonly PlayTimeTrackingSystem _playTimeTrackings = default!;
|
||||||
[Dependency] private readonly UserDbDataManager _userDb = default!;
|
[Dependency] private readonly UserDbDataManager _userDb = default!;
|
||||||
[Dependency] private readonly IReplayRecordingManager _replay = default!;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ namespace Content.Server.Pointing.EntitySystems
|
|||||||
RaiseNetworkEvent(new PopupEntityEvent(message, PopupType.Small, source), viewerEntity);
|
RaiseNetworkEvent(new PopupEntityEvent(message, PopupType.Small, source), viewerEntity);
|
||||||
}
|
}
|
||||||
|
|
||||||
_replay.QueueReplayMessage(new PopupEntityEvent(viewerMessage, PopupType.Small, source));
|
_replay.RecordServerMessage(new PopupEntityEvent(viewerMessage, PopupType.Small, source));
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool InRange(EntityUid pointer, EntityCoordinates coordinates)
|
public bool InRange(EntityUid pointer, EntityCoordinates coordinates)
|
||||||
|
|||||||
@@ -122,7 +122,7 @@ public sealed class RadioSystem : EntitySystem
|
|||||||
else
|
else
|
||||||
_adminLogger.Add(LogType.Chat, LogImpact.Low, $"Radio message from {ToPrettyString(messageSource):user} on {channel.LocalizedName}: {message}");
|
_adminLogger.Add(LogType.Chat, LogImpact.Low, $"Radio message from {ToPrettyString(messageSource):user} on {channel.LocalizedName}: {message}");
|
||||||
|
|
||||||
_replay.QueueReplayMessage(chat);
|
_replay.RecordServerMessage(chat);
|
||||||
_messages.Remove(message);
|
_messages.Remove(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1596,6 +1596,6 @@ namespace Content.Shared.CCVar
|
|||||||
/// false.
|
/// false.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static readonly CVarDef<bool> ReplayRecordAdminChat =
|
public static readonly CVarDef<bool> ReplayRecordAdminChat =
|
||||||
CVarDef.Create("replay.record_admin_chat", false, CVar.SERVERONLY);
|
CVarDef.Create("replay.record_admin_chat", false, CVar.ARCHIVE);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,41 @@
|
|||||||
using Robust.Shared.Network;
|
using Robust.Shared.Network;
|
||||||
|
using Robust.Shared.Replays;
|
||||||
using Robust.Shared.Serialization;
|
using Robust.Shared.Serialization;
|
||||||
using Robust.Shared.Utility;
|
using Robust.Shared.Serialization.Markdown.Mapping;
|
||||||
|
using Robust.Shared.Serialization.Markdown.Value;
|
||||||
|
|
||||||
namespace Content.Shared.GameTicking
|
namespace Content.Shared.GameTicking
|
||||||
{
|
{
|
||||||
public abstract class SharedGameTicker : EntitySystem
|
public abstract class SharedGameTicker : EntitySystem
|
||||||
{
|
{
|
||||||
|
[Dependency] private readonly IReplayRecordingManager _replay = default!;
|
||||||
|
|
||||||
// See ideally these would be pulled from the job definition or something.
|
// See ideally these would be pulled from the job definition or something.
|
||||||
// But this is easier, and at least it isn't hardcoded.
|
// But this is easier, and at least it isn't hardcoded.
|
||||||
//TODO: Move these, they really belong in StationJobsSystem or a cvar.
|
//TODO: Move these, they really belong in StationJobsSystem or a cvar.
|
||||||
public const string FallbackOverflowJob = "Passenger";
|
public const string FallbackOverflowJob = "Passenger";
|
||||||
public const string FallbackOverflowJobName = "job-name-passenger";
|
public const string FallbackOverflowJobName = "job-name-passenger";
|
||||||
|
|
||||||
|
// TODO network.
|
||||||
|
// Probably most useful for replays, round end info, and probably things like lobby menus.
|
||||||
|
[ViewVariables]
|
||||||
|
public int RoundId { get; protected set; }
|
||||||
|
|
||||||
|
public override void Initialize()
|
||||||
|
{
|
||||||
|
base.Initialize();
|
||||||
|
_replay.RecordingStarted += OnRecordingStart;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Shutdown()
|
||||||
|
{
|
||||||
|
_replay.RecordingStarted -= OnRecordingStart;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnRecordingStart(MappingDataNode metadata, List<object> events)
|
||||||
|
{
|
||||||
|
metadata["roundId"] = new ValueDataNode(RoundId.ToString());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[Serializable, NetSerializable]
|
[Serializable, NetSerializable]
|
||||||
|
|||||||
@@ -228,12 +228,6 @@
|
|||||||
layer:
|
layer:
|
||||||
- MobLayer
|
- MobLayer
|
||||||
- type: Appearance
|
- type: Appearance
|
||||||
rotate: true
|
|
||||||
states:
|
|
||||||
Alive:
|
|
||||||
Base: onestar_boss
|
|
||||||
Dead:
|
|
||||||
Base: onestar_boss_wrecked
|
|
||||||
- type: CombatMode
|
- type: CombatMode
|
||||||
- type: Tag
|
- type: Tag
|
||||||
tags:
|
tags:
|
||||||
|
|||||||
@@ -31,6 +31,16 @@
|
|||||||
- cvar
|
- cvar
|
||||||
- fuckrules
|
- fuckrules
|
||||||
- midipanic
|
- midipanic
|
||||||
|
- replay_recording_start
|
||||||
|
- replay_recording_stop
|
||||||
|
- replay_recording_stats
|
||||||
|
- replay_play
|
||||||
|
- replay_pause
|
||||||
|
- replay_toggle
|
||||||
|
- replay_skip
|
||||||
|
- replay_set_time
|
||||||
|
- replay_stop
|
||||||
|
- replay_load
|
||||||
|
|
||||||
- Flags: DEBUG
|
- Flags: DEBUG
|
||||||
Commands:
|
Commands:
|
||||||
|
|||||||
@@ -64,6 +64,7 @@
|
|||||||
- tilelookup
|
- tilelookup
|
||||||
- net_entityreport
|
- net_entityreport
|
||||||
- scene
|
- scene
|
||||||
|
- replay_recording_stats
|
||||||
|
|
||||||
|
|
||||||
- Flags: MAPPING
|
- Flags: MAPPING
|
||||||
@@ -120,6 +121,8 @@
|
|||||||
- gcf
|
- gcf
|
||||||
- getcomponentregistration
|
- getcomponentregistration
|
||||||
- fuck
|
- fuck
|
||||||
|
- replay_recording_start
|
||||||
|
- replay_recording_stop
|
||||||
|
|
||||||
- Flags: QUERY
|
- Flags: QUERY
|
||||||
Commands:
|
Commands:
|
||||||
|
|||||||
Reference in New Issue
Block a user