Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add tape recorder #2498

Merged
merged 4 commits into from
Dec 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions Content.Client/DeltaV/TapeRecorder/TapeRecorderSystem.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
using Content.Shared.DeltaV.TapeRecorder.Systems;

namespace Content.Client.DeltaV.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);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
using Content.Shared.DeltaV.TapeRecorder;
using Robust.Client.UserInterface;
using Robust.Shared.Prototypes;
using Robust.Shared.Timing;

namespace Content.Client.DeltaV.TapeRecorder.UI;

public sealed class TapeRecorderBoundUserInterface(EntityUid owner, Enum uiKey) : BoundUserInterface(owner, uiKey)
{
[ViewVariables]
private TapeRecorderWindow? _window;

[ViewVariables]
private TimeSpan _printCooldown;

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

_window = this.CreateWindow<TapeRecorderWindow>();
_window.Owner = Owner;
_window.OnModeChanged += mode => SendMessage(new ChangeModeTapeRecorderMessage(mode));
_window.OnPrintTranscript += PrintTranscript;
}

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);
}
}
23 changes: 23 additions & 0 deletions Content.Client/DeltaV/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>
129 changes: 129 additions & 0 deletions Content.Client/DeltaV/TapeRecorder/UI/TapeRecorderWindow.xaml.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
using Content.Client.UserInterface.Controls;
using Content.Shared.DeltaV.TapeRecorder;
using Content.Shared.DeltaV.TapeRecorder.Components;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Timing;

namespace Content.Client.DeltaV.TapeRecorder.UI;

[GenerateTypedNameReferences]
public sealed partial class TapeRecorderWindow : FancyWindow
{
[Dependency] private readonly IEntityManager _entMan = default!;

public 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()
{
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);

_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/DeltaV/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.DeltaV.TapeRecorder;
using Content.Shared.DeltaV.TapeRecorder.Components;
using Content.Shared.DeltaV.TapeRecorder.Systems;
using Robust.Server.Audio;
using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
using System.Text;

namespace Content.Server.DeltaV.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.DeltaV.TapeRecorder.Components;

/// <summary>
/// Added to tape records that are updating, winding or rewinding the tape.
/// </summary>
[RegisterComponent, NetworkedComponent]
public sealed partial class ActiveTapeRecorderComponent : Component;
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using Robust.Shared.GameStates;

namespace Content.Shared.DeltaV.TapeRecorder.Components;

/// <summary>
/// Removed from the cassette when damaged to prevent it being played until repaired
/// </summary>
[RegisterComponent, NetworkedComponent]
public sealed partial class FitsInTapeRecorderComponent : Component;
Loading
Loading