-
Notifications
You must be signed in to change notification settings - Fork 149
Commit
- Loading branch information
There are no files selected for viewing
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); | ||
} | ||
} |
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(); | ||
} | ||
} |
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> |
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; | ||
} | ||
} |
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); | ||
Check failure on line 53 in Content.Server/TapeRecorder/TapeRecorderSystem.cs GitHub Actions / Test Packaging
Check failure on line 53 in Content.Server/TapeRecorder/TapeRecorderSystem.cs GitHub Actions / Test Packaging
Check failure on line 53 in Content.Server/TapeRecorder/TapeRecorderSystem.cs GitHub Actions / build (ubuntu-latest)
Check failure on line 53 in Content.Server/TapeRecorder/TapeRecorderSystem.cs GitHub Actions / build (ubuntu-latest)
Check failure on line 53 in Content.Server/TapeRecorder/TapeRecorderSystem.cs GitHub Actions / YAML Linter
Check failure on line 53 in Content.Server/TapeRecorder/TapeRecorderSystem.cs GitHub Actions / YAML Linter
Check failure on line 53 in Content.Server/TapeRecorder/TapeRecorderSystem.cs GitHub Actions / build (ubuntu-latest)
|
||
} | ||
} | ||
|
||
/// <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; |