Skip to content

Commit

Permalink
Port TapeRecorder (space-syndicate#129)
Browse files Browse the repository at this point in the history
  • Loading branch information
Vonsant authored Dec 6, 2024
1 parent 5120fc5 commit 306bd79
Show file tree
Hide file tree
Showing 40 changed files with 1,401 additions and 3 deletions.
24 changes: 24 additions & 0 deletions Content.Client/TapeRecorder/TapeRecorderSystem.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
using Content.Shared.TapeRecorder;

namespace Content.Client.TapeRecorder;

/// <summary>
/// Required for client side prediction stuff
/// </summary>
public sealed class TapeRecorderSystem : SharedTapeRecorderSystem
{
private TimeSpan _lastTickTime = TimeSpan.Zero;

public override void Update(float frameTime)
{
if (!Timing.IsFirstTimePredicted)
return;

//We need to know the exact time period that has passed since the last update to ensure the tape position is sync'd with the server
//Since the client can skip frames when lagging, we cannot use frameTime
var realTime = (float) (Timing.CurTime - _lastTickTime).TotalSeconds;
_lastTickTime = Timing.CurTime;

base.Update(realTime);
}
}
64 changes: 64 additions & 0 deletions Content.Client/TapeRecorder/Ui/TapeRecorderBoundUserInterface.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
using Content.Shared.TapeRecorder.Components;
using Content.Shared.TapeRecorder.Events;
using Robust.Shared.Prototypes;
using Robust.Shared.Timing;

namespace Content.Client.TapeRecorder.Ui;

public sealed class TapeRecorderBoundUserInterface(EntityUid owner, Enum uiKey) : BoundUserInterface(owner, uiKey)
{
[Dependency] private readonly IEntityManager _entMan = default!;

[ViewVariables]
private TapeRecorderWindow? _window;

[ViewVariables]
private TimeSpan _printCooldown;

protected override void Open()
{
base.Open();

_window = new(_entMan, Owner);
_window.OnClose += Close;
_window.OnModeChanged += ChangeMode;
_window.OnPrintTranscript += PrintTranscript;
_window.OpenCentered();
}

private void ChangeMode(TapeRecorderMode mode)
{
SendMessage(new ChangeModeTapeRecorderMessage(mode));
}

private void PrintTranscript()
{
SendMessage(new PrintTapeRecorderMessage());

_window?.UpdatePrint(true);

Timer.Spawn(_printCooldown, () =>
{
_window?.UpdatePrint(false);
});
}

protected override void UpdateState(BoundUserInterfaceState state)
{
base.UpdateState(state);

if (state is not TapeRecorderState cast)
return;

_printCooldown = cast.PrintCooldown;

_window?.UpdateState(cast);
}

protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
if (disposing)
_window?.Dispose();
}
}
23 changes: 23 additions & 0 deletions Content.Client/TapeRecorder/Ui/TapeRecorderWindow.xaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<controls:FancyWindow
xmlns="https://spacestation14.io"
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
MinSize="440 220"
SetSize="440 220"
Title="{Loc 'tape-recorder-menu-title'}"
Resizable="False">
<BoxContainer Margin = "10 5" Orientation="Vertical" SeparationOverride="5">
<BoxContainer Orientation="Vertical">
<Label Margin = "5 0" Name="CassetteLabel" Text="{Loc 'tape-recorder-menu-no-cassette-label'}" Align="Left" StyleClasses="StatusFieldTitle" />
<Slider Name="PlaybackSlider" HorizontalExpand="True" />
</BoxContainer>
<BoxContainer Name ="Test" Margin = "0 5 0 0" Orientation="Horizontal" VerticalExpand = "True">
<BoxContainer Orientation="Vertical" HorizontalExpand = "True">
<Label Text="{Loc 'tape-recorder-menu-controls-label'}" Align="Center" />
<BoxContainer Name="Buttons" Orientation="Horizontal" VerticalExpand="True" Align="Center"/> <!-- Populated in constructor -->
</BoxContainer>
</BoxContainer>
<BoxContainer Margin = "0 2 0 0" Orientation="Horizontal">
<Button Name="PrintButton" Text="{Loc 'tape-recorder-menu-print-button'}" TextAlign="Center" HorizontalExpand ="True"/>
</BoxContainer>
</BoxContainer>
</controls:FancyWindow>
133 changes: 133 additions & 0 deletions Content.Client/TapeRecorder/Ui/TapeRecorderWindow.xaml.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
using Content.Client.UserInterface.Controls;
using Content.Shared.Shuttles.Components;
using Content.Shared.TapeRecorder.Components;
using Content.Shared.TapeRecorder.Events;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Timing;

namespace Content.Client.TapeRecorder.Ui;

[GenerateTypedNameReferences]
public sealed partial class TapeRecorderWindow : FancyWindow
{
private IEntityManager _entMan;

private EntityUid _owner;
private bool _onCooldown;
private bool _hasCasette;
private TapeRecorderMode _mode = TapeRecorderMode.Stopped;

private RadioOptions<TapeRecorderMode> _options = default!;
private bool _updating;

public Action<TapeRecorderMode>? OnModeChanged;
public Action? OnPrintTranscript;

public TapeRecorderWindow(IEntityManager entMan, EntityUid owner)
{
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);

_entMan = entMan;

_owner = owner;

_options = new RadioOptions<TapeRecorderMode>(RadioOptionsLayout.Horizontal);
Buttons.AddChild(_options);
_options.FirstButtonStyle = "OpenRight";
_options.LastButtonStyle = "OpenLeft";
_options.ButtonStyle = "OpenBoth";
foreach (var mode in Enum.GetValues<TapeRecorderMode>())
{
var name = mode.ToString().ToLower();
_options.AddItem(Loc.GetString($"tape-recorder-menu-{name}-button"), mode);
}

_options.OnItemSelected += args =>
{
if (_updating) // don't tell server to change mode to the mode it told us
return;
args.Button.Select(args.Id);
var mode = args.Button.SelectedValue;
OnModeChanged?.Invoke(mode);
};

PrintButton.OnPressed += _ => OnPrintTranscript?.Invoke();

SetEnabled(TapeRecorderMode.Recording, false);
SetEnabled(TapeRecorderMode.Playing, false);
SetEnabled(TapeRecorderMode.Rewinding, false);
}

private void SetSlider(float maxTime, float currentTime)
{
PlaybackSlider.Disabled = true;
PlaybackSlider.MaxValue = maxTime;
PlaybackSlider.Value = currentTime;
}

public void UpdatePrint(bool disabled)
{
PrintButton.Disabled = disabled;
_onCooldown = disabled;
}

public void UpdateState(TapeRecorderState state)
{
if (!_entMan.TryGetComponent<TapeRecorderComponent>(_owner, out var comp))
return;

_mode = comp.Mode; // TODO: update UI on handling state instead of adding UpdateUI to everything
_hasCasette = state.HasCasette;

_updating = true;

CassetteLabel.Text = _hasCasette
? Loc.GetString("tape-recorder-menu-cassette-label", ("cassetteName", state.CassetteName))
: Loc.GetString("tape-recorder-menu-no-cassette-label");

// Select the currently used mode
_options.SelectByValue(_mode);

// When tape is ejected or a button can't be used, disable it
// Server will change to paused once a tape is inactive
var tapeLeft = state.CurrentTime < state.MaxTime;
SetEnabled(TapeRecorderMode.Recording, tapeLeft);
SetEnabled(TapeRecorderMode.Playing, tapeLeft);
SetEnabled(TapeRecorderMode.Rewinding, state.CurrentTime > float.Epsilon);

if (state.HasCasette)
SetSlider(state.MaxTime, state.CurrentTime);

_updating = false;
}

private void SetEnabled(TapeRecorderMode mode, bool condition)
{
_options.SetItemDisabled((int) mode, !(_hasCasette && condition));
}

protected override void FrameUpdate(FrameEventArgs args)
{
base.FrameUpdate(args);

if (!_entMan.HasComponent<ActiveTapeRecorderComponent>(_owner))
return;

if (!_entMan.TryGetComponent<TapeRecorderComponent>(_owner, out var comp))
return;

if (_mode != comp.Mode)
{
_mode = comp.Mode;
_options.SelectByValue(_mode);
}

var speed = _mode == TapeRecorderMode.Rewinding
? -comp.RewindSpeed
: 1f;
PlaybackSlider.Value += args.DeltaSeconds * speed;
}
}
132 changes: 132 additions & 0 deletions Content.Server/TapeRecorder/TapeRecorderSystem.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
using Content.Server.Chat.Systems;
using Content.Server.Hands.Systems;
using Content.Server.Speech;
using Content.Server.Speech.Components;
using Content.Shared.Chat;
using Content.Shared.Paper;
using Content.Shared.Speech;
using Content.Shared.TapeRecorder;
using Content.Shared.TapeRecorder.Components;
using Content.Shared.TapeRecorder.Events;
using Robust.Server.Audio;
using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
using System.Text;

namespace Content.Server.TapeRecorder;

public sealed class TapeRecorderSystem : SharedTapeRecorderSystem
{
[Dependency] private readonly ChatSystem _chat = default!;
[Dependency] private readonly HandsSystem _hands = default!;
[Dependency] private readonly IPrototypeManager _proto = default!;
[Dependency] private readonly PaperSystem _paper = default!;

public override void Initialize()
{
base.Initialize();

SubscribeLocalEvent<TapeRecorderComponent, ListenEvent>(OnListen);
SubscribeLocalEvent<TapeRecorderComponent, PrintTapeRecorderMessage>(OnPrintMessage);
}

/// <summary>
/// Given a time range, play all messages on a tape within said range, [start, end).
/// Split into this system as shared does not have ChatSystem access
/// </summary>
protected override void ReplayMessagesInSegment(Entity<TapeRecorderComponent> ent, TapeCassetteComponent tape, float segmentStart, float segmentEnd)
{
var voice = EnsureComp<VoiceOverrideComponent>(ent);
var speech = EnsureComp<SpeechComponent>(ent);

foreach (var message in tape.RecordedData)
{
if (message.Timestamp < tape.CurrentPosition || message.Timestamp >= segmentEnd)
continue;

//Change the voice to match the speaker
voice.NameOverride = message.Name ?? ent.Comp.DefaultName;
// TODO: mimic the exact string chosen when the message was recorded
var verb = message.Verb ?? SharedChatSystem.DefaultSpeechVerb;
speech.SpeechVerb = _proto.Index<SpeechVerbPrototype>(verb);
//Play the message
_chat.TrySendInGameICMessage(ent, message.Message, InGameICChatType.Speak, false);
}
}

/// <summary>
/// Whenever someone speaks within listening range, record it to tape
/// </summary>
private void OnListen(Entity<TapeRecorderComponent> ent, ref ListenEvent args)
{
// mode should never be set when it isn't active but whatever
if (ent.Comp.Mode != TapeRecorderMode.Recording || !HasComp<ActiveTapeRecorderComponent>(ent))
return;

// No feedback loops
if (args.Source == ent.Owner)
return;

if (!TryGetTapeCassette(ent, out var cassette))
return;

// TODO: Handle "Someone" when whispering from far away, needs chat refactor

//Handle someone using a voice changer
var nameEv = new TransformSpeakerNameEvent(args.Source, Name(args.Source));
RaiseLocalEvent(args.Source, nameEv);

//Add a new entry to the tape
var verb = _chat.GetSpeechVerb(args.Source, args.Message);
var name = nameEv.VoiceName;
cassette.Comp.Buffer.Add(new TapeCassetteRecordedMessage(cassette.Comp.CurrentPosition, name, verb, args.Message));
}

private void OnPrintMessage(Entity<TapeRecorderComponent> ent, ref PrintTapeRecorderMessage args)
{
var (uid, comp) = ent;

if (comp.CooldownEndTime > Timing.CurTime)
return;

if (!TryGetTapeCassette(ent, out var cassette))
return;

var text = new StringBuilder();
var paper = Spawn(comp.PaperPrototype, Transform(ent).Coordinates);

// Sorting list by time for overwrite order
// TODO: why is this needed? why wouldn't it be stored in order
var data = cassette.Comp.RecordedData;
data.Sort((x,y) => x.Timestamp.CompareTo(y.Timestamp));

// Looking if player's entity exists to give paper in its hand
var player = args.Actor;
if (Exists(player))
_hands.PickupOrDrop(player, paper, checkActionBlocker: false);

if (!TryComp<PaperComponent>(paper, out var paperComp))
return;

Audio.PlayPvs(comp.PrintSound, ent);

text.AppendLine(Loc.GetString("tape-recorder-print-start-text"));
text.AppendLine();
foreach (var message in cassette.Comp.RecordedData)
{
var name = message.Name ?? ent.Comp.DefaultName;
var time = TimeSpan.FromSeconds((double) message.Timestamp);

text.AppendLine(Loc.GetString("tape-recorder-print-message-text",
("time", time.ToString(@"hh\:mm\:ss")),
("source", name),
("message", message.Message)));
}
text.AppendLine();
text.Append(Loc.GetString("tape-recorder-print-end-text"));

_paper.SetContent((paper, paperComp), text.ToString());

comp.CooldownEndTime = Timing.CurTime + comp.PrintCooldown;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using Robust.Shared.GameStates;

namespace Content.Shared.TapeRecorder.Components;

/// <summary>
/// Added to tape records that are updating, winding or rewinding the tape.
/// </summary>
[RegisterComponent, NetworkedComponent]
public sealed partial class ActiveTapeRecorderComponent : Component;
Loading

0 comments on commit 306bd79

Please sign in to comment.