diff --git a/Content.Server/_White/VoiceRecorder/VoiceRecorderComponent.cs b/Content.Server/_White/VoiceRecorder/VoiceRecorderComponent.cs new file mode 100644 index 0000000000..de038e7810 --- /dev/null +++ b/Content.Server/_White/VoiceRecorder/VoiceRecorderComponent.cs @@ -0,0 +1,68 @@ +using Content.Shared.Whitelist; +using Robust.Shared.Audio; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; + +namespace Content.Shared._White.VoiceRecorder; + +/// +/// This is used for... +/// +[RegisterComponent] +public sealed partial class VoiceRecorderComponent : Component +{ + [ViewVariables(VVAccess.ReadWrite)] + [DataField("enabled")] + public bool Enabled { get; set; } = true; + + [DataField("blacklist")] + public EntityWhitelist Blacklist { get; private set; } = new(); + + [ViewVariables(VVAccess.ReadWrite)] + [DataField("range")] + public int Range { get; private set; } = 10; + + [DataField("listening")] + public bool Listening { get; set; } = false; + + /// + /// The sound that's played when the scanner prints off a report. + /// + [DataField("soundPrint")] + public SoundSpecifier SoundPrint = new SoundPathSpecifier("/Audio/Machines/short_print_and_rip.ogg"); + + [DataField("soundEndOfRecording")] + public SoundSpecifier SoundEndOfRecording = new SoundPathSpecifier("/Audio/Machines/id_insert.ogg"); + + [DataField("soundStartOfRecording")] + public SoundSpecifier SoundStartOfRecording = new SoundPathSpecifier("/Audio/Weapons/Guns/MagIn/pistol_magin.ogg"); + + /// + /// What the machine will print + /// + [DataField("machineOutput", customTypeSerializer: typeof(PrototypeIdSerializer))] + public string MachineOutput = "PaperOffice"; + + [DataField("recordings")] + public List Recordings = new(); + + [ViewVariables(VVAccess.ReadWrite)] + [DataField("maximumEntries")] + public int MaximumEntries = 100; + + [ViewVariables(VVAccess.ReadWrite)] + [DataField("customTitle")] + public string CustomTitle = ""; + + /// + /// When will the recorder be ready to print again? + /// + [ViewVariables(VVAccess.ReadOnly)] + public TimeSpan PrintReadyAt = TimeSpan.Zero; + + /// + /// How often can the recorder print out reports? + /// + [DataField("printCooldown")] + public TimeSpan PrintCooldown = TimeSpan.FromSeconds(5); +} diff --git a/Content.Server/_White/VoiceRecorder/VoiceRecorderSystem.cs b/Content.Server/_White/VoiceRecorder/VoiceRecorderSystem.cs new file mode 100644 index 0000000000..87c42b7ea8 --- /dev/null +++ b/Content.Server/_White/VoiceRecorder/VoiceRecorderSystem.cs @@ -0,0 +1,178 @@ +using System.Text; +using Content.Server.Paper; +using Content.Server.Popups; +using Content.Server.Speech; +using Content.Server.Speech.Components; +using Content.Shared._White.VoiceRecorder; +using Content.Shared.Examine; +using Content.Shared.GameTicking; +using Content.Shared.Hands.EntitySystems; +using Content.Shared.Interaction; +using Content.Shared.Item; +using Content.Shared.Paper; +using Content.Shared.Toggleable; +using Content.Shared.Verbs; +using Robust.Shared.Audio; +using Robust.Shared.Audio.Systems; +using Robust.Shared.Timing; +using Robust.Shared.Utility; + +namespace Content.Server._White.VoiceRecorder; + +/// +/// This handles the voice recorder all itself. +/// +public sealed class VoiceRecorderSystem : EntitySystem +{ + [Dependency] private readonly PopupSystem _popup = default!; + [Dependency] private readonly SharedHandsSystem _handsSystem = default!; + [Dependency] private readonly PaperSystem _paperSystem = default!; + [Dependency] private readonly SharedAudioSystem _audioSystem = default!; + [Dependency] private readonly MetaDataSystem _metaData = default!; + [Dependency] private readonly SharedItemSystem _item = default!; + [Dependency] private readonly SharedAppearanceSystem _appearance = default!; + [Dependency] private readonly IGameTiming _gameTiming = default!; + [Dependency] private readonly SharedGameTicker _gameTicker = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnActivate); + SubscribeLocalEvent(OnExamine); + SubscribeLocalEvent(OnInit); + SubscribeLocalEvent(SaveEntityMessage); + SubscribeLocalEvent(CanListen); + SubscribeLocalEvent>(AddVerbs); + } + + public void OnInit(EntityUid uid, VoiceRecorderComponent component, ComponentInit args) + { + if (!TryComp(uid, out var listener)) + { + RemComp(uid); + component.Listening = false; + return; + } + ToggleListening(uid, component, component.Listening); + + } + + public void OnExamine(EntityUid uid, VoiceRecorderComponent component, ExaminedEvent args) + { + if (args.IsInDetailsRange) + { + var message = $"{Loc.GetString("voice-recorder-state")} {Loc.GetString( component.Listening ? "voice-recorder-state-on" : "voice-recorder-state-off")}"; + args.PushMarkup(message); + } + } + + public void OnActivate(EntityUid uid, VoiceRecorderComponent component, ActivateInWorldEvent args) + { + component.Listening = !component.Listening; + ToggleListening(uid, component, component.Listening); + var message = Loc.GetString(component.Listening ? "voice-recorder-on" : "voice-recorder-off"); + _popup.PopupEntity(message, args.User, args.User); + args.Handled = true; + } + + private void AddVerbs(EntityUid uid, VoiceRecorderComponent component, GetVerbsEvent args) + { + if (!args.CanAccess || !component.Enabled || component.Listening || component.Recordings.Count == 0) + return; + AlternativeVerb verb = new(); + verb.Text = Loc.GetString("voice-recorder-print"); + verb.Icon = new SpriteSpecifier.Texture(new ("/Textures/Interface/VerbIcons/eject.svg.192dpi.png")); + verb.Act = () => OnPrint(uid, component, component.Recordings.ToArray(), args.User); + args.Verbs.Add(verb); + } + + public void ToggleListening(EntityUid uid, VoiceRecorderComponent component, bool listening) + { + component.Listening = listening; + if (listening) + { + component.Recordings.Clear(); + EnsureComp(uid).Range = component.Range; + _audioSystem.PlayPvs(component.SoundStartOfRecording, uid, + AudioParams.Default + .WithVariation(0.25f) + .WithVolume(0.3f) + .WithRolloffFactor(2.8f) + .WithMaxDistance(1.5f)); + } + else + { + RemComp(uid); + _audioSystem.PlayPvs(component.SoundEndOfRecording, uid, + AudioParams.Default + .WithVariation(0.25f) + .WithVolume(0.3f) + .WithRolloffFactor(2.8f) + .WithMaxDistance(1.5f)); + } + if (TryComp(uid, out var appearance) && + TryComp(uid, out var item)) + { + _item.SetHeldPrefix(uid, listening ? "on" : "off", false, item); + _appearance.SetData(uid, ToggleVisuals.Toggled, listening, appearance); + } + } + + public void CanListen(EntityUid uid, VoiceRecorderComponent component, ListenAttemptEvent args) + { + if (component.Blacklist.IsValid(args.Source)) + args.Cancel(); + } + + public void SaveEntityMessage(EntityUid uid, VoiceRecorderComponent component, ListenEvent args) + { + if (!component.Listening) + return; + + var message = $"{_gameTiming.CurTime.Subtract(_gameTicker.RoundStartTimeSpan).ToString("hh\\:mm\\:ss")} {Name(args.Source)}: {args.Message}"; + component.Recordings.Add(message); + + if (component.Recordings.Count > (component.MaximumEntries - 1)) + { + ToggleListening(uid, component, false); + } + } + + public void OnPrint(EntityUid uid, VoiceRecorderComponent component, string[] messages, EntityUid user) + { + if (_gameTiming.CurTime < component.PrintReadyAt) + { + _popup.PopupEntity(Loc.GetString("forensic-scanner-printer-not-ready"), uid, user); + return; + } + // TEXT TO PRINT + var text = new StringBuilder(); + text.AppendLine(component.CustomTitle == "" ? Loc.GetString("voice-recorder-title") : component.CustomTitle); + text.AppendLine(""); + text.AppendLine(Loc.GetString("voice-recorder-start")); + foreach (var message in messages) + { + text.AppendLine(message); + } + text.AppendLine(Loc.GetString("voice-recorder-end")); + + var printed = EntityManager.SpawnEntity(component.MachineOutput, Transform(uid).Coordinates); + _paperSystem.SetContent(printed, text.ToString()); + var stamp = new StampDisplayInfo(); + stamp.StampedName = Loc.GetString("voice-recorder-stamp"); + stamp.StampedColor = new Color(47, 47, 56); + _paperSystem.TryStamp(printed, stamp, "paper_stamp-transcript", null); + _handsSystem.PickupOrDrop(user, printed, checkActionBlocker: false); + _audioSystem.PlayPvs(component.SoundPrint, uid, + AudioParams.Default + .WithVariation(0.25f) + .WithVolume(3f) + .WithRolloffFactor(2.8f) + .WithMaxDistance(4.5f)); + _metaData.SetEntityName(printed, Loc.GetString("voice-recorder-paper-name")); + _metaData.SetEntityDescription(printed, Loc.GetString("voice-recorder-paper-desc")); + ToggleListening(uid, component, false); + component.PrintReadyAt = _gameTiming.CurTime + component.PrintCooldown; + } +} diff --git a/Resources/Locale/en-US/white/voice-recorder.ftl b/Resources/Locale/en-US/white/voice-recorder.ftl new file mode 100644 index 0000000000..61bf498023 --- /dev/null +++ b/Resources/Locale/en-US/white/voice-recorder.ftl @@ -0,0 +1,15 @@ +voice-recorder-on = recording now +voice-recorder-off = recording stopped +voice-recorder-print = Print recording +voice-recorder-stamp = TRANSCRIPT +voice-recorder-title = NANOTRASEN BLCK-M VOICE RECORDER TRANSCRIPT +voice-recorder-start = *start of recording* +voice-recorder-end = *end of recording* +voice-recorder-paper-name = recorder transcript +voice-recorder-paper-desc = BLCK-M voice recorder transcript +voice-recorder-state = Voice recorder +voice-recorder-state-on = is recording +voice-recorder-state-off = is off + +ent-CrateSecurityVoiceRecorder = voice recorder crate + .desc = Contains 3 voice recorders to ensure that no evidence will be lost. Does not require any access to open. diff --git a/Resources/Locale/ru-RU/white/voice-recorder.ftl b/Resources/Locale/ru-RU/white/voice-recorder.ftl new file mode 100644 index 0000000000..ebea1af1cb --- /dev/null +++ b/Resources/Locale/ru-RU/white/voice-recorder.ftl @@ -0,0 +1,17 @@ +voice-recorder-on = запись включена +voice-recorder-off = запись остановлена +voice-recorder-print = Распечатать +voice-recorder-stamp = СТЕНОГРАММА +voice-recorder-title = NANOTRASEN BLCK-M VOICE RECORDER TRANSCRIPT +voice-recorder-start = *начало записи* +voice-recorder-end = *конец записи* +voice-recorder-paper-name = стенограмма +voice-recorder-paper-desc = копия стенограммы записанной на диктофон типа BLCK-M +voice-recorder-state = Диктофон +voice-recorder-state-on = ведёт запись +voice-recorder-state-off = выключен + +ent-VoiceRecorder = диктофон + .desc = Диктофон типа BLCK-M. Имеет втроенный принтер для печати стенограмм. Каждая новая запись стирает предыдущую. +ent-CrateSecurityVoiceRecorder = ящик с диктофонами + .desc = Ящик с диктофонами типа BLCK-M. Содержит 3 единицы. Не трубует дополнительных доступов. diff --git a/Resources/Prototypes/Catalog/Cargo/cargo_security.yml b/Resources/Prototypes/Catalog/Cargo/cargo_security.yml index f3121395d9..302805c392 100644 --- a/Resources/Prototypes/Catalog/Cargo/cargo_security.yml +++ b/Resources/Prototypes/Catalog/Cargo/cargo_security.yml @@ -87,3 +87,13 @@ cost: 2000 category: Security group: market + +- type: cargoProduct + id: SecurityVoiceRecorder + icon: + sprite: White/VoiceRecorder/voicerecorder.rsi + state: icon + product: CrateSecurityVoiceRecorder + cost: 1000 + category: Security + group: market diff --git a/Resources/Prototypes/Catalog/Fills/Crates/security.yml b/Resources/Prototypes/Catalog/Fills/Crates/security.yml index 950b11f1f9..6784ded19d 100644 --- a/Resources/Prototypes/Catalog/Fills/Crates/security.yml +++ b/Resources/Prototypes/Catalog/Fills/Crates/security.yml @@ -123,3 +123,12 @@ contents: - id: BoxBodyCamera amount: 2 + +- type: entity + id: CrateSecurityVoiceRecorder + parent: CrateGenericSteel + components: + - type: StorageFill + contents: + - id: VoiceRecorder + amount: 4 diff --git a/Resources/Prototypes/Entities/White/voice_recorder.yml b/Resources/Prototypes/Entities/White/voice_recorder.yml new file mode 100644 index 0000000000..53b1e02c81 --- /dev/null +++ b/Resources/Prototypes/Entities/White/voice_recorder.yml @@ -0,0 +1,29 @@ +- type: entity + parent: BaseItem + id: VoiceRecorder + name: voice recorder + description: BLCK-M type voice recorder. Has built-in printer for printing transcripts. Each new record erases previous one. + components: + - type: Sprite + sprite: White/VoiceRecorder/voicerecorder.rsi + layers: + - state: icon-on + - type: GenericVisualizer + visuals: + enum.ToggleVisuals.Toggled: + enum.ToggleVisuals.Layer: + True: { state: icon-on } + False: { state: icon } + - type: Item + heldPrefix: off + sprite: White/VoiceRecorder/voicerecorder.rsi + - type: Appearance + - type: VoiceRecorder + blacklist: + components: + - SurveillanceCamera + - SurveillanceCameraMonitor + - RadioSpeaker + range: 5 + - type: ActiveListener + range: 5 diff --git a/Resources/Textures/Objects/Misc/bureaucracy.rsi/meta.json b/Resources/Textures/Objects/Misc/bureaucracy.rsi/meta.json index cfa5fb3095..6ceed3d96f 100644 --- a/Resources/Textures/Objects/Misc/bureaucracy.rsi/meta.json +++ b/Resources/Textures/Objects/Misc/bureaucracy.rsi/meta.json @@ -256,6 +256,9 @@ }, { "name": "paper_stamp-geraldiy" - } + }, + { + "name": "paper_stamp-transcript" + } ] } diff --git a/Resources/Textures/Objects/Misc/bureaucracy.rsi/paper_stamp-transcript.png b/Resources/Textures/Objects/Misc/bureaucracy.rsi/paper_stamp-transcript.png new file mode 100644 index 0000000000..f721327a2e Binary files /dev/null and b/Resources/Textures/Objects/Misc/bureaucracy.rsi/paper_stamp-transcript.png differ diff --git a/Resources/Textures/White/VoiceRecorder/voicerecorder.rsi/icon-on.png b/Resources/Textures/White/VoiceRecorder/voicerecorder.rsi/icon-on.png new file mode 100644 index 0000000000..7a0bcd80c4 Binary files /dev/null and b/Resources/Textures/White/VoiceRecorder/voicerecorder.rsi/icon-on.png differ diff --git a/Resources/Textures/White/VoiceRecorder/voicerecorder.rsi/icon.png b/Resources/Textures/White/VoiceRecorder/voicerecorder.rsi/icon.png new file mode 100644 index 0000000000..3c5b4ff4b7 Binary files /dev/null and b/Resources/Textures/White/VoiceRecorder/voicerecorder.rsi/icon.png differ diff --git a/Resources/Textures/White/VoiceRecorder/voicerecorder.rsi/meta.json b/Resources/Textures/White/VoiceRecorder/voicerecorder.rsi/meta.json new file mode 100644 index 0000000000..8212493b99 --- /dev/null +++ b/Resources/Textures/White/VoiceRecorder/voicerecorder.rsi/meta.json @@ -0,0 +1,97 @@ +{ + "version": 1, + "license": "CC-BY-SA-3.0", + "copyright": "by Gargrarien", + "size": {"x": 32, "y": 32}, + "states": + [ + { + "name": "icon-on", + "directions": 1, + "delays": [ + [ + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1 + ] + ] + }, + { + "name": "icon", + "directions": 1 + }, + { + "name": "off-inhand-left", + "directions": 4 + }, + { + "name": "off-inhand-right", + "directions": 4 + }, + { + "name": "on-inhand-left", + "directions": 4, + "delays": [ + [ + 0.2, + 0.2, + 0.2, + 0.2 + ], + [ + 0.2, + 0.2, + 0.2, + 0.2 + ], + [ + 0.2, + 0.2, + 0.2, + 0.2 + ], + [ + 0.2, + 0.2, + 0.2, + 0.2 + ] + ] + }, + { + "name": "on-inhand-right", + "directions": 4, + "delays": [ + [ + 0.2, + 0.2, + 0.2, + 0.2 + ], + [ + 0.2, + 0.2, + 0.2, + 0.2 + ], + [ + 0.2, + 0.2, + 0.2, + 0.2 + ], + [ + 0.2, + 0.2, + 0.2, + 0.2 + ] + ] + } + ] +} diff --git a/Resources/Textures/White/VoiceRecorder/voicerecorder.rsi/off-inhand-left.png b/Resources/Textures/White/VoiceRecorder/voicerecorder.rsi/off-inhand-left.png new file mode 100644 index 0000000000..32d397683f Binary files /dev/null and b/Resources/Textures/White/VoiceRecorder/voicerecorder.rsi/off-inhand-left.png differ diff --git a/Resources/Textures/White/VoiceRecorder/voicerecorder.rsi/off-inhand-right.png b/Resources/Textures/White/VoiceRecorder/voicerecorder.rsi/off-inhand-right.png new file mode 100644 index 0000000000..140c8115b3 Binary files /dev/null and b/Resources/Textures/White/VoiceRecorder/voicerecorder.rsi/off-inhand-right.png differ diff --git a/Resources/Textures/White/VoiceRecorder/voicerecorder.rsi/on-inhand-left.png b/Resources/Textures/White/VoiceRecorder/voicerecorder.rsi/on-inhand-left.png new file mode 100644 index 0000000000..e234eb1c87 Binary files /dev/null and b/Resources/Textures/White/VoiceRecorder/voicerecorder.rsi/on-inhand-left.png differ diff --git a/Resources/Textures/White/VoiceRecorder/voicerecorder.rsi/on-inhand-right.png b/Resources/Textures/White/VoiceRecorder/voicerecorder.rsi/on-inhand-right.png new file mode 100644 index 0000000000..65fe926d3a Binary files /dev/null and b/Resources/Textures/White/VoiceRecorder/voicerecorder.rsi/on-inhand-right.png differ