[PORT] Магнитофон (#591)
* ported(small sprite issue) * added language support * added pickup and drop sounds * тесты ИДИТЕ НАХУЙ * unhardcode locale * fix translating * tts support * small fixes * fix * fogor * fix2 * bug fix * MORE SOUNDS FOR SOUND GOD
@@ -0,0 +1,24 @@
|
||||
using Content.Shared._Goobstation.TapeRecorder.Systems;
|
||||
|
||||
namespace Content.Client._Goobstation.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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
using Content.Shared._Goobstation.TapeRecorder;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Shared.Timing;
|
||||
|
||||
namespace Content.Client._Goobstation.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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
using Content.Client.UserInterface.Controls;
|
||||
using Content.Shared._Goobstation.TapeRecorder;
|
||||
using Content.Shared._Goobstation.TapeRecorder.Components;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Shared.Timing;
|
||||
|
||||
namespace Content.Client._Goobstation.TapeRecorder.UI;
|
||||
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class TapeRecorderWindow : FancyWindow
|
||||
{
|
||||
[Dependency] private 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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<controls:FancyWindow
|
||||
xmlns="https://spacestation14.io"
|
||||
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls;assembly=Content.Client"
|
||||
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>
|
||||
@@ -0,0 +1,167 @@
|
||||
using System.Text;
|
||||
using Content.Server._WL.Languages;
|
||||
using Content.Server.Chat.Systems;
|
||||
using Content.Server.Hands.Systems;
|
||||
using Content.Server.Speech.Components;
|
||||
using Content.Shared._Goobstation.TapeRecorder;
|
||||
using Content.Shared._Goobstation.TapeRecorder.Components;
|
||||
using Content.Shared._Goobstation.TapeRecorder.Systems;
|
||||
using Content.Shared._WL.Languages.Components;
|
||||
using Content.Shared.Chat;
|
||||
using Content.Shared.Corvax.TTS;
|
||||
using Content.Shared.Paper;
|
||||
using Content.Shared.Speech;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Server._Goobstation.TapeRecorder;
|
||||
|
||||
public sealed partial class TapeRecorderSystem : SharedTapeRecorderSystem
|
||||
{
|
||||
[Dependency] private ChatSystem _chat = default!;
|
||||
[Dependency] private HandsSystem _hands = default!;
|
||||
[Dependency] private IPrototypeManager _proto = default!;
|
||||
[Dependency] private PaperSystem _paper = default!;
|
||||
[Dependency] private LanguagesSystem _language = default!; // WL-Languages
|
||||
|
||||
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);
|
||||
|
||||
// WL-Changes-Start
|
||||
if (TryComp<TTSComponent>(ent, out var tts))
|
||||
tts.VoicePrototypeId = message.TTS;
|
||||
|
||||
if (TryComp<LanguagesComponent>(ent, out var languageComp))
|
||||
{
|
||||
// I already know that's a bad way to do it
|
||||
languageComp.CurrentLanguage = message.Language != "Translate"
|
||||
? message.Language
|
||||
: "Translate";
|
||||
}
|
||||
// WL-Changes-end
|
||||
|
||||
//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;
|
||||
|
||||
// WL-Changes-Start
|
||||
var language = "Translate";
|
||||
var tts = string.Empty;
|
||||
|
||||
if (TryComp<LanguagesComponent>(args.Source, out var languagesSpeaker) && languagesSpeaker.CurrentLanguage.HasValue)
|
||||
language = languagesSpeaker.CurrentLanguage;
|
||||
|
||||
if (TryComp<TTSComponent>(args.Source, out var ttsComp))
|
||||
tts = ttsComp.VoicePrototypeId ?? "";
|
||||
// WL-Changes-end
|
||||
|
||||
cassette.Comp.Buffer.Add(new TapeCassetteRecordedMessage(cassette.Comp.CurrentPosition, name, verb, args.Message, language, tts)); // WL-Changes: added Language and TTS support
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
// WL-Languages-Start
|
||||
if (message.Language != "Translate")
|
||||
{
|
||||
var language = _language.GetLanguagePrototype(message.Language);
|
||||
message.Message = language.Obfuscation.Obfuscate(message.Message, 634);
|
||||
}
|
||||
// WL-Languages-End
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
using Robust.Shared.GameStates;
|
||||
|
||||
namespace Content.Shared._Goobstation.TapeRecorder.Components;
|
||||
|
||||
/// <summary>
|
||||
/// Added to tape records that are updating, winding or rewinding the tape.
|
||||
/// </summary>
|
||||
[RegisterComponent, NetworkedComponent]
|
||||
public sealed partial class ActiveTapeRecorderComponent : Component;
|
||||
@@ -0,0 +1,9 @@
|
||||
using Robust.Shared.GameStates;
|
||||
|
||||
namespace Content.Shared._Goobstation.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;
|
||||
@@ -0,0 +1,53 @@
|
||||
using Content.Shared._Goobstation.TapeRecorder.Systems;
|
||||
using Content.Shared.Whitelist;
|
||||
using Robust.Shared.GameStates;
|
||||
|
||||
namespace Content.Shared._Goobstation.TapeRecorder.Components;
|
||||
|
||||
[RegisterComponent, NetworkedComponent, Access(typeof(SharedTapeRecorderSystem))]
|
||||
[AutoGenerateComponentState]
|
||||
public sealed partial class TapeCassetteComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// A list of all recorded voice, containing timestamp, name and spoken words
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public List<TapeCassetteRecordedMessage> RecordedData = new();
|
||||
|
||||
/// <summary>
|
||||
/// The current position within the tape we are at, in seconds
|
||||
/// Only dirtied when the tape recorder is stopped
|
||||
/// </summary>
|
||||
[DataField, AutoNetworkedField]
|
||||
public float CurrentPosition = 0f;
|
||||
|
||||
/// <summary>
|
||||
/// Maximum capacity of this tape
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public TimeSpan MaxCapacity = TimeSpan.FromSeconds(120);
|
||||
|
||||
/// <summary>
|
||||
/// How long to spool the tape after it was damaged
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public TimeSpan RepairDelay = TimeSpan.FromSeconds(3);
|
||||
|
||||
/// <summary>
|
||||
/// When an entry is damaged, the chance of each character being corrupted.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public float CorruptionChance = 0.25f;
|
||||
|
||||
/// <summary>
|
||||
/// Temporary storage for all heard messages that need processing
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public List<TapeCassetteRecordedMessage> Buffer = new();
|
||||
|
||||
/// <summary>
|
||||
/// Whitelist for tools that can be used to respool a damaged tape.
|
||||
/// </summary>
|
||||
[DataField(required: true)]
|
||||
public EntityWhitelist RepairWhitelist = new();
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
using Content.Shared._Goobstation.TapeRecorder.Systems;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.GameStates;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
|
||||
|
||||
namespace Content.Shared._Goobstation.TapeRecorder.Components;
|
||||
|
||||
[RegisterComponent, NetworkedComponent, Access(typeof(SharedTapeRecorderSystem))]
|
||||
[AutoGenerateComponentState, AutoGenerateComponentPause]
|
||||
public sealed partial class TapeRecorderComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// The current tape recorder mode, controls what using the item will do
|
||||
/// </summary>
|
||||
[DataField, AutoNetworkedField]
|
||||
public TapeRecorderMode Mode = TapeRecorderMode.Stopped;
|
||||
|
||||
/// <summary>
|
||||
/// Paper that will spawn when printing transcript
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public EntProtoId PaperPrototype = "TapeRecorderTranscript";
|
||||
|
||||
/// <summary>
|
||||
/// How fast can this tape recorder rewind
|
||||
/// Acts as a multiplier for the frameTime
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public float RewindSpeed = 3f;
|
||||
|
||||
[DataField(customTypeSerializer: typeof(TimeOffsetSerializer)), AutoPausedField]
|
||||
public TimeSpan CooldownEndTime = TimeSpan.Zero;
|
||||
|
||||
/// <summary>
|
||||
/// Cooldown of print button
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public TimeSpan PrintCooldown = TimeSpan.FromSeconds(4);
|
||||
|
||||
/// <summary>
|
||||
/// Default name as fallback if a message doesn't have one.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public LocId DefaultName = "tape-recorder-voice-unknown";
|
||||
|
||||
/// <summary>
|
||||
/// Sound on print transcript
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public SoundSpecifier PrintSound = new SoundPathSpecifier("/Audio/_WL/Items/tape/taperecorder_print.ogg") // WL-Changes: Old sound /Audio/Machines/diagnoser_printing.ogg
|
||||
{
|
||||
Params = AudioParams.Default.WithVolume(-2f).WithMaxDistance(3f)
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// What sound is used when play mode is activated
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public SoundSpecifier PlaySound = new SoundPathSpecifier("/Audio/_DV/Items/TapeRecorder/play.ogg")
|
||||
{
|
||||
Params = AudioParams.Default.WithVolume(-2f).WithMaxDistance(3f)
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// What sound is used when stop mode is activated
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public SoundSpecifier StopSound = new SoundPathSpecifier("/Audio/_DV/Items/TapeRecorder/stop.ogg")
|
||||
{
|
||||
Params = AudioParams.Default.WithVolume(-2f).WithMaxDistance(3f)
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// What sound is used when rewind mode is activated
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public SoundSpecifier RewindSound = new SoundPathSpecifier("/Audio/_DV/Items/TapeRecorder/rewind.ogg")
|
||||
{
|
||||
Params = AudioParams.Default.WithVolume(-2f).WithMaxDistance(3f)
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,427 @@
|
||||
// SPDX-FileCopyrightText: 2024 deltanedas <39013340+deltanedas@users.noreply.github.com>
|
||||
// SPDX-FileCopyrightText: 2025 BombasterDS <deniskaporoshok@gmail.com>
|
||||
// SPDX-FileCopyrightText: 2025 GoobBot <uristmchands@proton.me>
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
using Content.Shared.Containers.ItemSlots;
|
||||
using Content.Shared.Damage;
|
||||
using Content.Shared.DoAfter;
|
||||
using Content.Shared.Examine;
|
||||
using Content.Shared.Interaction;
|
||||
using Content.Shared.Labels.Components;
|
||||
using Content.Shared.Popups;
|
||||
using Content.Shared.Toggleable;
|
||||
using Content.Shared.UserInterface;
|
||||
using Content.Shared.Whitelist;
|
||||
using Robust.Shared.Audio.Systems;
|
||||
using Robust.Shared.Containers;
|
||||
using Robust.Shared.Random;
|
||||
using Robust.Shared.Serialization;
|
||||
using Robust.Shared.Timing;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Text;
|
||||
using Content.Shared._Goobstation.TapeRecorder.Components;
|
||||
using Content.Shared.Damage.Systems;
|
||||
|
||||
namespace Content.Shared._Goobstation.TapeRecorder.Systems;
|
||||
|
||||
public abstract partial class SharedTapeRecorderSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private EntityWhitelistSystem _whitelist = default!;
|
||||
[Dependency] protected IGameTiming Timing = default!;
|
||||
[Dependency] private IRobustRandom _random = default!;
|
||||
[Dependency] private SharedAppearanceSystem _appearance = default!;
|
||||
[Dependency] protected SharedAudioSystem Audio = default!;
|
||||
[Dependency] private SharedDoAfterSystem _doAfter = default!;
|
||||
[Dependency] private ItemSlotsSystem _slots = default!;
|
||||
// [Dependency] private SharedPopupSystem _popup = default!; // WL-Disabled
|
||||
[Dependency] private SharedUserInterfaceSystem _ui = default!;
|
||||
|
||||
protected const string SlotName = "cassette_tape";
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<TapeRecorderComponent, ItemSlotEjectAttemptEvent>(OnCassetteRemoveAttempt);
|
||||
SubscribeLocalEvent<TapeRecorderComponent, EntRemovedFromContainerMessage>(OnCassetteRemoved);
|
||||
SubscribeLocalEvent<TapeRecorderComponent, EntInsertedIntoContainerMessage>(OnCassetteInserted);
|
||||
SubscribeLocalEvent<TapeRecorderComponent, ExaminedEvent>(OnRecorderExamined);
|
||||
SubscribeLocalEvent<TapeRecorderComponent, ChangeModeTapeRecorderMessage>(OnChangeModeMessage);
|
||||
SubscribeLocalEvent<TapeRecorderComponent, AfterActivatableUIOpenEvent>(OnUIOpened);
|
||||
|
||||
SubscribeLocalEvent<TapeCassetteComponent, ExaminedEvent>(OnTapeExamined);
|
||||
SubscribeLocalEvent<TapeCassetteComponent, DamageChangedEvent>(OnDamagedChanged);
|
||||
SubscribeLocalEvent<TapeCassetteComponent, InteractUsingEvent>(OnInteractingWithCassette);
|
||||
SubscribeLocalEvent<TapeCassetteComponent, TapeCassetteRepairDoAfterEvent>(OnTapeCassetteRepair);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Process active tape recorder modes
|
||||
/// </summary>
|
||||
public override void Update(float frameTime)
|
||||
{
|
||||
base.Update(frameTime);
|
||||
|
||||
var query = EntityQueryEnumerator<ActiveTapeRecorderComponent, TapeRecorderComponent>();
|
||||
while (query.MoveNext(out var uid, out _, out var comp))
|
||||
{
|
||||
var ent = (uid, comp);
|
||||
if (!TryGetTapeCassette(uid, out var tape))
|
||||
{
|
||||
SetMode(ent, TapeRecorderMode.Stopped);
|
||||
continue;
|
||||
}
|
||||
|
||||
var continuing = comp.Mode switch
|
||||
{
|
||||
TapeRecorderMode.Recording => ProcessRecordingTapeRecorder(ent, frameTime),
|
||||
TapeRecorderMode.Playing => ProcessPlayingTapeRecorder(ent, frameTime),
|
||||
TapeRecorderMode.Rewinding => ProcessRewindingTapeRecorder(ent, frameTime),
|
||||
_ => false
|
||||
};
|
||||
|
||||
if (continuing)
|
||||
continue;
|
||||
|
||||
SetMode(ent, TapeRecorderMode.Stopped);
|
||||
Dirty(tape); // make sure clients have the right value once it's stopped
|
||||
}
|
||||
}
|
||||
|
||||
private void OnUIOpened(Entity<TapeRecorderComponent> ent, ref AfterActivatableUIOpenEvent args)
|
||||
{
|
||||
UpdateUI(ent);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// UI message when choosing between recorder modes
|
||||
/// </summary>
|
||||
private void OnChangeModeMessage(Entity<TapeRecorderComponent> ent, ref ChangeModeTapeRecorderMessage args)
|
||||
{
|
||||
SetMode(ent, args.Mode);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update the tape position and overwrite any messages between the previous and new position
|
||||
/// </summary>
|
||||
/// <param name="ent">The tape recorder to process</param>
|
||||
/// <param name="frameTime">Number of seconds that have passed since the last call</param>
|
||||
/// <returns>True if the tape recorder should continue in the current mode, False if it should switch to the Stopped mode</returns>
|
||||
private bool ProcessRecordingTapeRecorder(Entity<TapeRecorderComponent> ent, float frameTime)
|
||||
{
|
||||
if (!TryGetTapeCassette(ent, out var tape))
|
||||
return false;
|
||||
|
||||
var currentTime = tape.Comp.CurrentPosition + frameTime;
|
||||
|
||||
//'Flushed' in this context is a mark indicating the message was not added between the last update and this update
|
||||
//Remove any flushed messages in the segment we just recorded over (ie old messages)
|
||||
tape.Comp.RecordedData.RemoveAll(x => x.Timestamp > tape.Comp.CurrentPosition && x.Timestamp <= currentTime);
|
||||
|
||||
tape.Comp.RecordedData.AddRange(tape.Comp.Buffer);
|
||||
|
||||
tape.Comp.Buffer.Clear();
|
||||
|
||||
//Update the tape's current time
|
||||
tape.Comp.CurrentPosition = (float) Math.Min(currentTime, tape.Comp.MaxCapacity.TotalSeconds);
|
||||
|
||||
//If we have reached the end of the tape - stop
|
||||
return tape.Comp.CurrentPosition < tape.Comp.MaxCapacity.TotalSeconds;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update the tape position and play any messages with timestamps between the previous and new position
|
||||
/// </summary>
|
||||
/// <param name="ent">The tape recorder to process</param>
|
||||
/// <param name="frameTime">Number of seconds that have passed since the last call</param>
|
||||
/// <returns>True if the tape recorder should continue in the current mode, False if it should switch to the Stopped mode</returns>
|
||||
private bool ProcessPlayingTapeRecorder(Entity<TapeRecorderComponent> ent, float frameTime)
|
||||
{
|
||||
if (!TryGetTapeCassette(ent, out var tape))
|
||||
return false;
|
||||
|
||||
//Get the segment of the tape to be played
|
||||
//And any messages within that time period
|
||||
var currentTime = tape.Comp.CurrentPosition + frameTime;
|
||||
|
||||
ReplayMessagesInSegment(ent, tape.Comp, tape.Comp.CurrentPosition, currentTime);
|
||||
|
||||
//Update the tape's position
|
||||
tape.Comp.CurrentPosition = (float) Math.Min(currentTime, tape.Comp.MaxCapacity.TotalSeconds);
|
||||
|
||||
//Stop when we reach the end of the tape
|
||||
return tape.Comp.CurrentPosition < tape.Comp.MaxCapacity.TotalSeconds;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update the tape position in reverse
|
||||
/// </summary>
|
||||
/// <param name="ent">The tape recorder to process</param>
|
||||
/// <param name="frameTime">Number of seconds that have passed since the last call</param>
|
||||
/// <returns>True if the tape recorder should continue in the current mode, False if it should switch to the Stopped mode</returns>
|
||||
private bool ProcessRewindingTapeRecorder(Entity<TapeRecorderComponent> ent, float frameTime)
|
||||
{
|
||||
if (!TryGetTapeCassette(ent, out var tape))
|
||||
return false;
|
||||
|
||||
//Calculate how far we have rewound
|
||||
var rewindTime = frameTime * ent.Comp.RewindSpeed;
|
||||
//Update the current time, clamp to 0
|
||||
tape.Comp.CurrentPosition = Math.Max(0, tape.Comp.CurrentPosition - rewindTime);
|
||||
|
||||
//If we have reached the beginning of the tape, stop
|
||||
return tape.Comp.CurrentPosition >= float.Epsilon;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Plays messages back on the server.
|
||||
/// Does nothing on the client.
|
||||
/// </summary>
|
||||
protected virtual void ReplayMessagesInSegment(Entity<TapeRecorderComponent> ent, TapeCassetteComponent tape, float segmentStart, float segmentEnd)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Start repairing a damaged tape when using a screwdriver or pen on it
|
||||
/// </summary>
|
||||
protected void OnInteractingWithCassette(Entity<TapeCassetteComponent> ent, ref InteractUsingEvent args)
|
||||
{
|
||||
//Is the tape damaged?
|
||||
if (HasComp<FitsInTapeRecorderComponent>(ent))
|
||||
return;
|
||||
|
||||
//Are we using a valid repair tool?
|
||||
if (_whitelist.IsWhitelistFail(ent.Comp.RepairWhitelist, args.Used))
|
||||
return;
|
||||
|
||||
_doAfter.TryStartDoAfter(new DoAfterArgs(EntityManager, args.User, ent.Comp.RepairDelay, new TapeCassetteRepairDoAfterEvent(), ent, target: ent, used: args.Used)
|
||||
{
|
||||
BreakOnMove = true,
|
||||
NeedHand = true
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Repair a damaged tape
|
||||
/// </summary>
|
||||
protected void OnTapeCassetteRepair(Entity<TapeCassetteComponent> ent, ref TapeCassetteRepairDoAfterEvent args)
|
||||
{
|
||||
if (args.Handled || args.Cancelled || args.Args.Target == null)
|
||||
return;
|
||||
|
||||
//Cant repair if not damaged
|
||||
if (HasComp<FitsInTapeRecorderComponent>(ent))
|
||||
return;
|
||||
|
||||
_appearance.SetData(ent, ToggleableVisuals.Enabled, false);
|
||||
AddComp<FitsInTapeRecorderComponent>(ent);
|
||||
args.Handled = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// When the cassette has been damaged, corrupt and entry and unspool it
|
||||
/// </summary>
|
||||
protected void OnDamagedChanged(Entity<TapeCassetteComponent> ent, ref DamageChangedEvent args)
|
||||
{
|
||||
if (args.DamageDelta == null || args.DamageDelta.GetTotal() < 5)
|
||||
return;
|
||||
|
||||
_appearance.SetData(ent, ToggleableVisuals.Enabled, true);
|
||||
|
||||
RemComp<FitsInTapeRecorderComponent>(ent);
|
||||
CorruptRandomEntry(ent);
|
||||
}
|
||||
|
||||
protected void OnTapeExamined(Entity<TapeCassetteComponent> ent, ref ExaminedEvent args)
|
||||
{
|
||||
if (!args.IsInDetailsRange)
|
||||
return;
|
||||
|
||||
if (!HasComp<FitsInTapeRecorderComponent>(ent))
|
||||
{
|
||||
args.PushMarkup(Loc.GetString("tape-cassette-damaged"));
|
||||
return;
|
||||
}
|
||||
|
||||
var positionPercentage = Math.Floor(ent.Comp.CurrentPosition / ent.Comp.MaxCapacity.TotalSeconds * 100);
|
||||
var tapePosMsg = Loc.GetString("tape-cassette-position", ("position", positionPercentage));
|
||||
args.PushMarkup(tapePosMsg);
|
||||
}
|
||||
|
||||
protected void OnRecorderExamined(Entity<TapeRecorderComponent> ent, ref ExaminedEvent args)
|
||||
{
|
||||
if (!args.IsInDetailsRange)
|
||||
return;
|
||||
|
||||
//Check if we have a tape cassette inserted
|
||||
if (!TryGetTapeCassette(ent, out var tape))
|
||||
{
|
||||
args.PushMarkup(Loc.GetString("tape-recorder-empty"));
|
||||
return;
|
||||
}
|
||||
|
||||
var state = ent.Comp.Mode.ToString().ToLower();
|
||||
args.PushMarkup(Loc.GetString("tape-recorder-" + state));
|
||||
|
||||
OnTapeExamined(tape, ref args);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Prevent removing the tape cassette while the recorder is active
|
||||
/// </summary>
|
||||
protected void OnCassetteRemoveAttempt(Entity<TapeRecorderComponent> ent, ref ItemSlotEjectAttemptEvent args)
|
||||
{
|
||||
if (!HasComp<ActiveTapeRecorderComponent>(ent))
|
||||
return;
|
||||
|
||||
args.Cancelled = true;
|
||||
}
|
||||
|
||||
protected void OnCassetteRemoved(Entity<TapeRecorderComponent> ent, ref EntRemovedFromContainerMessage args)
|
||||
{
|
||||
SetMode(ent, TapeRecorderMode.Stopped);
|
||||
UpdateAppearance(ent);
|
||||
UpdateUI(ent);
|
||||
}
|
||||
|
||||
protected void OnCassetteInserted(Entity<TapeRecorderComponent> ent, ref EntInsertedIntoContainerMessage args)
|
||||
{
|
||||
UpdateAppearance(ent);
|
||||
UpdateUI(ent);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update the appearance of the tape recorder.
|
||||
/// </summary>
|
||||
/// <param name="ent">The tape recorder to update</param>
|
||||
protected void UpdateAppearance(Entity<TapeRecorderComponent> ent)
|
||||
{
|
||||
var hasCassette = TryGetTapeCassette(ent, out _);
|
||||
_appearance.SetData(ent, TapeRecorderVisuals.Mode, ent.Comp.Mode);
|
||||
_appearance.SetData(ent, TapeRecorderVisuals.TapeInserted, hasCassette);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Choose a random recorded entry on the cassette and replace some of the text with hashes
|
||||
/// </summary>
|
||||
/// <param name="component"></param>
|
||||
protected void CorruptRandomEntry(TapeCassetteComponent tape)
|
||||
{
|
||||
if (tape.RecordedData.Count == 0)
|
||||
return;
|
||||
|
||||
var entry = _random.Pick(tape.RecordedData);
|
||||
|
||||
var corruption = Loc.GetString("tape-recorder-message-corruption");
|
||||
|
||||
var corruptedMessage = new StringBuilder();
|
||||
foreach (var character in entry.Message)
|
||||
{
|
||||
if (_random.Prob(tape.CorruptionChance))
|
||||
corruptedMessage.Append(corruption);
|
||||
else
|
||||
corruptedMessage.Append(character);
|
||||
}
|
||||
|
||||
entry.Name = Loc.GetString("tape-recorder-voice-unintelligible");
|
||||
entry.Message = corruptedMessage.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set the tape recorder mode and dirty if it is different from the previous mode
|
||||
/// </summary>
|
||||
/// <param name="ent">The tape recorder to update</param>
|
||||
/// <param name="mode">The new mode</param>
|
||||
private void SetMode(Entity<TapeRecorderComponent> ent, TapeRecorderMode mode)
|
||||
{
|
||||
if (mode == ent.Comp.Mode)
|
||||
return;
|
||||
|
||||
if (mode == TapeRecorderMode.Stopped)
|
||||
{
|
||||
RemComp<ActiveTapeRecorderComponent>(ent);
|
||||
}
|
||||
else
|
||||
{
|
||||
// can't play without a tape in it...
|
||||
if (!TryGetTapeCassette(ent, out _))
|
||||
return;
|
||||
|
||||
EnsureComp<ActiveTapeRecorderComponent>(ent);
|
||||
}
|
||||
|
||||
var sound = ent.Comp.Mode switch
|
||||
{
|
||||
TapeRecorderMode.Stopped => ent.Comp.StopSound,
|
||||
TapeRecorderMode.Rewinding => ent.Comp.RewindSound,
|
||||
_ => ent.Comp.PlaySound
|
||||
};
|
||||
Audio.PlayPvs(sound, ent);
|
||||
|
||||
ent.Comp.Mode = mode;
|
||||
Dirty(ent);
|
||||
|
||||
UpdateUI(ent);
|
||||
}
|
||||
|
||||
protected bool TryGetTapeCassette(EntityUid ent, [NotNullWhen(true)] out Entity<TapeCassetteComponent> tape)
|
||||
{
|
||||
if (_slots.GetItemOrNull(ent, SlotName) is not {} cassette)
|
||||
{
|
||||
tape = default!;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!TryComp<TapeCassetteComponent>(cassette, out var comp))
|
||||
{
|
||||
tape = default!;
|
||||
return false;
|
||||
}
|
||||
|
||||
tape = new(cassette, comp);
|
||||
return true;
|
||||
}
|
||||
|
||||
private void UpdateUI(Entity<TapeRecorderComponent> ent)
|
||||
{
|
||||
var (uid, comp) = ent;
|
||||
if (!_ui.IsUiOpen(uid, TapeRecorderUIKey.Key))
|
||||
return;
|
||||
|
||||
var hasCassette = TryGetTapeCassette(ent, out var tape);
|
||||
var hasData = false;
|
||||
var currentTime = 0f;
|
||||
var maxTime = 0f;
|
||||
// WL-Changes-Start
|
||||
//var cassetteName = "Unnamed";
|
||||
var cassetteName = Loc.GetString("tape-recorder-menu-cassette-unnamed");
|
||||
// WL-Changes-End
|
||||
var cooldown = comp.PrintCooldown;
|
||||
|
||||
if (hasCassette)
|
||||
{
|
||||
hasData = tape.Comp.RecordedData.Count > 0;
|
||||
currentTime = tape.Comp.CurrentPosition;
|
||||
maxTime = (float) tape.Comp.MaxCapacity.TotalSeconds;
|
||||
|
||||
if (TryComp<LabelComponent>(tape, out var labelComp))
|
||||
if (labelComp.CurrentLabel != null)
|
||||
cassetteName = labelComp.CurrentLabel;
|
||||
}
|
||||
|
||||
var state = new TapeRecorderState(
|
||||
hasCassette,
|
||||
hasData,
|
||||
currentTime,
|
||||
maxTime,
|
||||
cassetteName,
|
||||
cooldown);
|
||||
|
||||
_ui.SetUiState(uid, TapeRecorderUIKey.Key, state);
|
||||
}
|
||||
}
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
public sealed partial class TapeCassetteRepairDoAfterEvent : SimpleDoAfterEvent;
|
||||
@@ -0,0 +1,63 @@
|
||||
using Content.Shared.Speech;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Shared._Goobstation.TapeRecorder;
|
||||
|
||||
/// <summary>
|
||||
/// Every chat event recorded on a tape is saved in this format
|
||||
/// </summary>
|
||||
[DataDefinition]
|
||||
public sealed partial class TapeCassetteRecordedMessage : IComparable<TapeCassetteRecordedMessage>
|
||||
{
|
||||
/// <summary>
|
||||
/// Number of seconds since the start of the tape that this event was recorded at
|
||||
/// </summary>
|
||||
[DataField(required: true)]
|
||||
public float Timestamp = 0;
|
||||
|
||||
/// <summary>
|
||||
/// The name of the entity that spoke
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public string? Name;
|
||||
|
||||
/// <summary>
|
||||
/// The verb used for this message.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public ProtoId<SpeechVerbPrototype>? Verb;
|
||||
|
||||
/// <summary>
|
||||
/// What was spoken
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public string Message = string.Empty;
|
||||
|
||||
// WL-Languages-start
|
||||
[DataField]
|
||||
public string Language = "Translate";
|
||||
// WL-Languages-end
|
||||
|
||||
// WL-TTS-start
|
||||
[DataField]
|
||||
public string TTS = string.Empty;
|
||||
// WL-TTS-end
|
||||
|
||||
public TapeCassetteRecordedMessage(float timestamp, string name, ProtoId<SpeechVerbPrototype> verb, string message, string language, string tts) // WL-Languages: added Language and TTS support
|
||||
{
|
||||
Timestamp = timestamp;
|
||||
Name = name;
|
||||
Verb = verb;
|
||||
Message = message;
|
||||
Language = language; // WL-Languages: added Language support
|
||||
TTS = tts;
|
||||
}
|
||||
|
||||
public int CompareTo(TapeCassetteRecordedMessage? other)
|
||||
{
|
||||
if (other == null)
|
||||
return 0;
|
||||
|
||||
return (int) (Timestamp - other.Timestamp);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
using Robust.Shared.Serialization;
|
||||
|
||||
namespace Content.Shared._Goobstation.TapeRecorder;
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
public enum TapeRecorderVisuals : byte
|
||||
{
|
||||
Mode,
|
||||
TapeInserted
|
||||
}
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
public enum TapeRecorderMode : byte
|
||||
{
|
||||
Stopped,
|
||||
Recording,
|
||||
Playing,
|
||||
Rewinding
|
||||
}
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
public enum TapeRecorderUIKey : byte
|
||||
{
|
||||
Key
|
||||
}
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class ChangeModeTapeRecorderMessage(TapeRecorderMode mode) : BoundUserInterfaceMessage
|
||||
{
|
||||
public TapeRecorderMode Mode = mode;
|
||||
}
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class PrintTapeRecorderMessage : BoundUserInterfaceMessage;
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class TapeRecorderState : BoundUserInterfaceState
|
||||
{
|
||||
// TODO: check the itemslot on client instead of putting easy casette stuff in the state
|
||||
public bool HasCasette;
|
||||
public bool HasData;
|
||||
public float CurrentTime;
|
||||
public float MaxTime;
|
||||
public string CassetteName;
|
||||
public TimeSpan PrintCooldown;
|
||||
|
||||
public TapeRecorderState(
|
||||
bool hasCasette,
|
||||
bool hasData,
|
||||
float currentTime,
|
||||
float maxTime,
|
||||
string cassetteName,
|
||||
TimeSpan printCooldown)
|
||||
{
|
||||
HasCasette = hasCasette;
|
||||
HasData = hasData;
|
||||
CurrentTime = currentTime;
|
||||
MaxTime = maxTime;
|
||||
CassetteName = cassetteName;
|
||||
PrintCooldown = printCooldown;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
# SPDX-FileCopyrightText: 2024 deltanedas <39013340+deltanedas@users.noreply.github.com>
|
||||
# SPDX-FileCopyrightText: 2025 GoobBot <uristmchands@proton.me>
|
||||
#
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
- files: [ "play.ogg" ]
|
||||
license: "CC0-1.0"
|
||||
copyright: "Taken from cassette tape deck open, close +tape handling.aif by kyles. Converted from Aiff to Ogg."
|
||||
source: "https://freesound.org/people/kyles/sounds/450525/"
|
||||
|
||||
- files: [ "stop.ogg" ]
|
||||
license: "CC-BY-4.0"
|
||||
copyright: "Taken from Pressing Stop on An Old Tape Machine by djlprojects. Converted from Mp3 to Ogg."
|
||||
source: "https://freesound.org/people/djlprojects/sounds/392889/"
|
||||
|
||||
- files: [ "rewind.ogg" ]
|
||||
license: "CC-BY-NC-4.0"
|
||||
copyright: "Taken from CassetteRewind.flac by acclivity. Converted from Flac to Ogg."
|
||||
source: "https://freesound.org/people/acclivity/sounds/23393/"
|
||||
@@ -0,0 +1 @@
|
||||
taken from tgstation
|
||||
@@ -0,0 +1,28 @@
|
||||
cassette-repair-start = You start winding the tape back into {THE($item)}.
|
||||
cassette-repair-finish = You manage to wind the tape back into {THE($item)}.
|
||||
tape-cassette-position = The cassette is about [color=green]{$position}%[/color] the way through.
|
||||
tape-cassette-damaged = The cassette is unspooled, use a pen or screwdriver to repair it.
|
||||
tape-recorder-playing = The tape recorder is in [color=green]playback[/color] mode.
|
||||
tape-recorder-stopped = The tape recorder is stopped.
|
||||
tape-recorder-empty = The tape recorder is empty.
|
||||
tape-recorder-recording = The tape recorder is in [color=red]recording[/color] mode.
|
||||
tape-recorder-rewinding = The tape recorder is in [color=yellow]rewinding[/color] mode.
|
||||
tape-recorder-locked = Cant eject while the tape recorder is running.
|
||||
tape-recorder-voice-unknown = Unknown
|
||||
tape-recorder-voice-unintelligible = Unintelligible
|
||||
tape-recorder-message-corruption = #
|
||||
|
||||
tape-recorder-menu-title = Tape Recorder
|
||||
tape-recorder-menu-controls-label = Controls:
|
||||
tape-recorder-menu-stopped-button = Pause
|
||||
tape-recorder-menu-recording-button = Record
|
||||
tape-recorder-menu-playing-button = Playback
|
||||
tape-recorder-menu-rewinding-button = Rewind
|
||||
tape-recorder-menu-print-button = Print record transcript
|
||||
tape-recorder-menu-cassette-unnamed = Unnamed
|
||||
tape-recorder-menu-cassette-label = Cassette tape: {$cassetteName}
|
||||
tape-recorder-menu-no-cassette-label = Cassette tape is not inserted
|
||||
|
||||
tape-recorder-print-start-text = [bold]Start of recorded transcript[/bold]
|
||||
tape-recorder-print-message-text = [bold][{$time}] {$source}: [/bold] {$message}
|
||||
tape-recorder-print-end-text = [bold]End of recorded transcript[/bold]
|
||||
@@ -0,0 +1,2 @@
|
||||
ent-BoxTapeRecorder = коробка с магнитофоном
|
||||
.desc = Коробка с разноцветными кассетами и магнитофоном.
|
||||
@@ -0,0 +1,10 @@
|
||||
ent-TapeRecorder = магнитофон
|
||||
.desc = Всё, что вы скажете в это устройство, может и будет использовано против вас в космическом суде.
|
||||
ent-TapeRecorderFilled = { ent-TapeRecorder }
|
||||
.suffix = Записанный
|
||||
.desc = { ent-TapeRecorder.desc }
|
||||
ent-CassetteTape = кассетная лента
|
||||
.desc = Магнитная лента, способная хранить до двух минут записи с каждой стороны.
|
||||
ent-CassetteTapeInterview = { ent-CassetteTape }
|
||||
.suffix = Интервью с Гарри Смошем.
|
||||
.desc = { ent-CassetteTape.desc }
|
||||
@@ -0,0 +1,2 @@
|
||||
ent-TapeRecorderTranscript = расшифровка записи
|
||||
.desc = { ent-Paper.desc }
|
||||
@@ -0,0 +1,26 @@
|
||||
cassette-repair-start = Вы начинаете перематывать плёнку обратно в { $item }.
|
||||
cassette-repair-finish = Вам удаётся перемотать плёнку обратно в { $item }.
|
||||
tape-cassette-position = Плёнка перемотана примерно на [color=green]{ $position }%[/color].
|
||||
tape-cassette-damaged = Плёнка размотана, используйте ручку или отвёртку для починки.
|
||||
tape-recorder-playing = Диктофон находится в режиме [color=green]воспроизведения[/color].
|
||||
tape-recorder-stopped = Диктофон остановлен.
|
||||
tape-recorder-empty = В Диктофоне нет кассеты.
|
||||
tape-recorder-recording = Диктофон находится в режиме [color=red]записи[/color].
|
||||
tape-recorder-rewinding = Диктофон находится в режиме [color=yellow]перемотки[/color].
|
||||
tape-recorder-locked = Невозможно извлечь кассету во время работы Диктофона.
|
||||
tape-recorder-voice-unknown = Неизвестный голос
|
||||
tape-recorder-voice-unintelligible = Невнятная речь
|
||||
tape-recorder-message-corruption = #
|
||||
tape-recorder-menu-title = Диктофон
|
||||
tape-recorder-menu-controls-label = Управление:
|
||||
tape-recorder-menu-stopped-button = Пауза
|
||||
tape-recorder-menu-recording-button = Запись
|
||||
tape-recorder-menu-playing-button = Воспроизведение
|
||||
tape-recorder-menu-rewinding-button = Перемотка
|
||||
tape-recorder-menu-cassette-unnamed = Без названия
|
||||
tape-recorder-menu-print-button = Распечатать расшифровку
|
||||
tape-recorder-menu-cassette-label = Кассета: { $cassetteName }
|
||||
tape-recorder-menu-no-cassette-label = Кассета не вставлена
|
||||
tape-recorder-print-start-text = [bold]Начало записи[/bold]
|
||||
tape-recorder-print-message-text = [bold][{ $time }] { $source }:[/bold] { $message }
|
||||
tape-recorder-print-end-text = [bold]Конец записи[/bold]
|
||||
@@ -199,6 +199,7 @@
|
||||
- id: HoloprojectorSecurity
|
||||
- id: BoxEvidenceMarkers
|
||||
- id: HandLabeler
|
||||
- id: BoxTapeRecorder # DeltaV
|
||||
- id: PlushieLizardJobDetective
|
||||
prob: 0.02
|
||||
|
||||
|
||||
@@ -143,6 +143,7 @@
|
||||
- PowerCellsStatic
|
||||
- ElectronicsStatic
|
||||
- ManualStorageStatic # WL-changes-storage
|
||||
- TapeRecorderStatic # DeltaV
|
||||
- type: EmagLatheRecipes
|
||||
emagStaticPacks:
|
||||
- SecurityAmmoStatic
|
||||
|
||||
@@ -32,9 +32,11 @@
|
||||
shoes: ClothingShoesColorWhite
|
||||
id: ReporterPDA
|
||||
ears: ClothingHeadsetService
|
||||
#storage:
|
||||
#back:
|
||||
#- Stuff
|
||||
storage: # DeltaV: Give reporters tape recording equipment
|
||||
back:
|
||||
- TapeRecorder
|
||||
- CassetteTape
|
||||
- CassetteTape
|
||||
|
||||
- type: chameleonOutfit
|
||||
id: ReporterChameleonOutfit
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
- type: entity
|
||||
parent: BoxCardboard
|
||||
id: BoxTapeRecorder
|
||||
name: tape recorder box
|
||||
description: A box with colorful cassette tapes and a tape recorder.
|
||||
components:
|
||||
- type: Sprite
|
||||
layers:
|
||||
- state: box_security
|
||||
- sprite: _DV/Objects/Storage/boxes.rsi
|
||||
state: recorder
|
||||
- type: StorageFill
|
||||
contents:
|
||||
- id: CassetteTape
|
||||
amount: 4
|
||||
- id: TapeRecorder
|
||||
@@ -0,0 +1,190 @@
|
||||
# SPDX-FileCopyrightText: 2024 deltanedas <39013340+deltanedas@users.noreply.github.com>
|
||||
# SPDX-FileCopyrightText: 2025 GoobBot <uristmchands@proton.me>
|
||||
#
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
- type: entity
|
||||
parent: BaseItem
|
||||
id: TapeRecorder
|
||||
name: tape recorder
|
||||
description: Anything said into this device can and will be used against you in a court of space law.
|
||||
components:
|
||||
- type: Sprite
|
||||
sprite: _DV/Objects/Devices/tape_recorder.rsi
|
||||
layers:
|
||||
- state: empty
|
||||
- state: idle
|
||||
map: ["tape"]
|
||||
visible: false
|
||||
- type: Item
|
||||
size: Small
|
||||
- type: TapeRecorder
|
||||
- type: Languages # WL-Languages
|
||||
- type: TTS # WL-TTS
|
||||
# WL-Sounds-Start
|
||||
- type: EmitSoundOnPickup
|
||||
sound:
|
||||
path: /Audio/_WL/Items/tape/taperecorder_pickup.ogg
|
||||
params:
|
||||
volume: -6
|
||||
- type: EmitSoundOnDrop
|
||||
sound:
|
||||
path: /Audio/_WL/Items/tape/taperecorder_drop.ogg
|
||||
params:
|
||||
volume: -4
|
||||
- type: EmitSoundOnLand
|
||||
sound:
|
||||
path: /Audio/_WL/Items/tape/taperecorder_drop.ogg
|
||||
params:
|
||||
volume: -4
|
||||
# WL-Sounds-End
|
||||
- type: ActiveListener
|
||||
range: 4
|
||||
- type: UseDelay
|
||||
delay: 1
|
||||
- type: Speech
|
||||
- type: ItemSlots
|
||||
slots:
|
||||
cassette_tape:
|
||||
priority: 4
|
||||
insertSound: /Audio/_WL/Items/tape/taperecorder_close.ogg # WL-Changes
|
||||
ejectSound: /Audio/_WL/Items/tape/taperecorder_open.ogg # WL-Changes
|
||||
whitelist:
|
||||
components:
|
||||
- FitsInTapeRecorder
|
||||
- type: ContainerContainer
|
||||
containers:
|
||||
cassette_tape: !type:ContainerSlot
|
||||
- type: Appearance
|
||||
- type: GenericVisualizer
|
||||
visuals:
|
||||
enum.TapeRecorderVisuals.Mode:
|
||||
tape:
|
||||
Stopped: { state: "idle" }
|
||||
Playing: { state: "playing" }
|
||||
Recording: { state: "recording" }
|
||||
Rewinding: { state: "rewinding" }
|
||||
enum.TapeRecorderVisuals.TapeInserted:
|
||||
tape:
|
||||
True: { visible: true }
|
||||
False: { visible: false }
|
||||
- type: ActivatableUI
|
||||
key: enum.TapeRecorderUIKey.Key
|
||||
inHandsOnly: true
|
||||
requireActiveHand: false
|
||||
- type: UserInterface
|
||||
interfaces:
|
||||
enum.TapeRecorderUIKey.Key:
|
||||
type: TapeRecorderBoundUserInterface
|
||||
|
||||
- type: entity
|
||||
parent: TapeRecorder
|
||||
id: TapeRecorderFilled
|
||||
suffix: Filled
|
||||
components:
|
||||
- type: ContainerFill
|
||||
containers:
|
||||
cassette_tape:
|
||||
- CassetteTape
|
||||
|
||||
- type: entity
|
||||
parent: BaseItem
|
||||
id: CassetteTape
|
||||
name: cassette tape
|
||||
description: A magnetic tape that can hold up to two minutes of content on either side.
|
||||
components:
|
||||
- type: Sprite
|
||||
sprite: _DV/Objects/Devices/cassette_tapes.rsi
|
||||
layers:
|
||||
- state: tape_greyscale
|
||||
map: [ "enum.DamageStateVisualLayers.Base" ]
|
||||
- state: tape_ribbonoverlay
|
||||
map: [ "enum.ToggleableVisuals.Layer" ]
|
||||
visible: false
|
||||
- type: Item
|
||||
size: Tiny
|
||||
- type: Injurable
|
||||
- type: TapeCassette
|
||||
maxCapacity: 180
|
||||
repairWhitelist:
|
||||
tags:
|
||||
- Screwdriver
|
||||
- Write
|
||||
- type: FitsInTapeRecorder
|
||||
- type: Appearance
|
||||
# WL-Sounds-Start
|
||||
- type: EmitSoundOnPickup
|
||||
sound:
|
||||
path: /Audio/_WL/Items/tape/tape_pickup.ogg
|
||||
params:
|
||||
volume: -6
|
||||
- type: EmitSoundOnDrop
|
||||
sound:
|
||||
path: /Audio/_WL/Items/tape/tape_drop.ogg
|
||||
params:
|
||||
volume: -4
|
||||
- type: EmitSoundOnLand
|
||||
sound:
|
||||
path: /Audio/_WL/Items/tape/tape_drop.ogg
|
||||
params:
|
||||
volume: -4
|
||||
# WL-Sounds-End
|
||||
- type: GenericVisualizer
|
||||
visuals:
|
||||
enum.ToggleableVisuals.Enabled:
|
||||
enum.ToggleableVisuals.Layer:
|
||||
True: { visible: true }
|
||||
False: { visible: false }
|
||||
- type: RandomSprite
|
||||
available:
|
||||
- enum.DamageStateVisualLayers.Base:
|
||||
tape_greyscale: Rainbow
|
||||
|
||||
- type: entity
|
||||
suffix: Interview with Garry Smosh
|
||||
parent: CassetteTape
|
||||
id: CassetteTapeInterview
|
||||
components:
|
||||
- type: Label
|
||||
currentLabel: Interview with Garry Smosh
|
||||
- type: TapeCassette
|
||||
recordedData:
|
||||
- timestamp: 2
|
||||
name: Phil Dervin
|
||||
message: "Its 11:43am, present in the room are Phil Dervin, Detective first class, Officer Belview and Grarry Smosh, Suspect of one count of secure tresspass, four counts of assault, two counts of theft and 85 counts of disturbing the peace."
|
||||
- timestamp: 6
|
||||
name: Phil Dervin
|
||||
message: "Mr Smosh, do you understand the charges you have been accused of?"
|
||||
- timestamp: 14
|
||||
name: Grarry Smosh
|
||||
message: "I don't care what you say, i ain't done anything."
|
||||
- timestamp: 18
|
||||
name: Phil Dervin
|
||||
message: "Sir, you were caught redhanded in the Captains bedroom. In the middle of an attempt at stealing his whiskey reserve no less."
|
||||
- timestamp: 23
|
||||
name: Phil Dervin
|
||||
message: "You are lucky he didn't shoot you for that."
|
||||
- timestamp: 28
|
||||
name: Grarry Smosh
|
||||
message: "I didn't see no signs saying i couldn't be there."
|
||||
- timestamp: 34
|
||||
name: Phil Dervin
|
||||
message: "The Captains bedroom? I don't think we need a sign telling people to stay out - it's common sense."
|
||||
- timestamp: 38
|
||||
name: Phil Dervin
|
||||
message: "Anyway that's besides the point, even if it were not off limits there is still the matter of the restricted items we found on your person and the subsequent attempt at evading arrest."
|
||||
- timestamp: 42
|
||||
name: Grarry Smosh
|
||||
message: "I ain't done nothing."
|
||||
- timestamp: 46
|
||||
name: Officer Belview
|
||||
message: "You slipped 3 officers, stole a stun baton and beat Ian with it. The HOP was very upset at that last part."
|
||||
- timestamp: 50
|
||||
name: Grarry Smosh
|
||||
message: "Which one of you gave the HOP a disabler?"
|
||||
- timestamp: 54
|
||||
name: Phil Dervin
|
||||
message: "The Warden did, turned out to be a good idea eh?"
|
||||
- timestamp: 58
|
||||
name: Officer Belview
|
||||
message: "I would say so."
|
||||
@@ -0,0 +1,5 @@
|
||||
- type: entity
|
||||
parent: Paper
|
||||
id: TapeRecorderTranscript
|
||||
name: record transcript
|
||||
# TODO: could have a unique sprite in the future
|
||||
@@ -0,0 +1,25 @@
|
||||
- type: latheRecipe
|
||||
id: CassetteTape
|
||||
result: CassetteTape
|
||||
categories:
|
||||
- Tools
|
||||
completetime: 2
|
||||
materials:
|
||||
Steel: 50
|
||||
Plastic: 150
|
||||
|
||||
- type: latheRecipe
|
||||
id: TapeRecorder
|
||||
result: TapeRecorder
|
||||
categories:
|
||||
- Tools
|
||||
completetime: 3
|
||||
materials:
|
||||
Steel: 250
|
||||
Plastic: 250
|
||||
|
||||
- type: latheRecipePack
|
||||
id: TapeRecorderStatic
|
||||
recipes:
|
||||
- CassetteTape
|
||||
- TapeRecorder
|
||||
|
After Width: | Height: | Size: 419 B |
|
After Width: | Height: | Size: 2.9 KiB |
|
After Width: | Height: | Size: 242 B |
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"version": 1,
|
||||
"license": "CC-BY-SA-3.0",
|
||||
"copyright": "Timfa, plus edits by portfiend, cart-chat made by kushbreth (discord)",
|
||||
"size": {
|
||||
"x": 32,
|
||||
"y": 32
|
||||
},
|
||||
"states": [
|
||||
{
|
||||
"name": "cart-cri"
|
||||
},
|
||||
{
|
||||
"name": "cart-mail"
|
||||
},
|
||||
{
|
||||
"name": "cart-chat"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"version": 1,
|
||||
"license": "CC-BY-SA-3.0",
|
||||
"copyright": "Taken from tgstation at https://github.com/tgstation/tgstation/commit/92dc954ab5317b370e98dd070ad60ba8c3e8a6e9",
|
||||
"size": {
|
||||
"x": 32,
|
||||
"y": 32
|
||||
},
|
||||
"states": [
|
||||
{
|
||||
"name": "tape_greyscale"
|
||||
},
|
||||
{
|
||||
"name": "tape_ribbonoverlay"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
After Width: | Height: | Size: 423 B |
|
After Width: | Height: | Size: 449 B |
|
After Width: | Height: | Size: 581 B |
|
After Width: | Height: | Size: 597 B |
|
After Width: | Height: | Size: 491 B |
|
After Width: | Height: | Size: 487 B |
@@ -0,0 +1,58 @@
|
||||
{
|
||||
"version": 1,
|
||||
"license": "CC-BY-SA-3.0",
|
||||
"copyright": "Taken from tgstation at https://github.com/tgstation/tgstation/commit/92dc954ab5317b370e98dd070ad60ba8c3e8a6e9",
|
||||
"size": {
|
||||
"x": 32,
|
||||
"y": 32
|
||||
},
|
||||
"states": [
|
||||
{
|
||||
"name": "idle"
|
||||
},
|
||||
{
|
||||
"name": "inhand-right",
|
||||
"directions": 4
|
||||
},
|
||||
{
|
||||
"name": "inhand-left",
|
||||
"directions": 4
|
||||
},
|
||||
{
|
||||
"name": "recording",
|
||||
"delays": [
|
||||
[
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "playing",
|
||||
"delays": [
|
||||
[
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "rewinding",
|
||||
"delays": [
|
||||
[
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "empty"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
After Width: | Height: | Size: 955 B |
|
After Width: | Height: | Size: 934 B |
|
After Width: | Height: | Size: 887 B |
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"version": 1,
|
||||
"license": "CC-BY-SA-3.0",
|
||||
"copyright": "Maybe taken from /tg/station. No attribution was present originally, so this is only an assumption.",
|
||||
"size": {
|
||||
"x": 32,
|
||||
"y": 32
|
||||
},
|
||||
"states": [
|
||||
{
|
||||
"name": "recorder"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
After Width: | Height: | Size: 189 B |