@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// This is used for...
|
||||
/// </summary>
|
||||
[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;
|
||||
|
||||
/// <summary>
|
||||
/// The sound that's played when the scanner prints off a report.
|
||||
/// </summary>
|
||||
[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");
|
||||
|
||||
/// <summary>
|
||||
/// What the machine will print
|
||||
/// </summary>
|
||||
[DataField("machineOutput", customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>))]
|
||||
public string MachineOutput = "PaperOffice";
|
||||
|
||||
[DataField("recordings")]
|
||||
public List<string> Recordings = new();
|
||||
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
[DataField("maximumEntries")]
|
||||
public int MaximumEntries = 100;
|
||||
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
[DataField("customTitle")]
|
||||
public string CustomTitle = "";
|
||||
|
||||
/// <summary>
|
||||
/// When will the recorder be ready to print again?
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadOnly)]
|
||||
public TimeSpan PrintReadyAt = TimeSpan.Zero;
|
||||
|
||||
/// <summary>
|
||||
/// How often can the recorder print out reports?
|
||||
/// </summary>
|
||||
[DataField("printCooldown")]
|
||||
public TimeSpan PrintCooldown = TimeSpan.FromSeconds(5);
|
||||
}
|
||||
178
Content.Server/_White/VoiceRecorder/VoiceRecorderSystem.cs
Normal file
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// This handles the voice recorder all itself.
|
||||
/// </summary>
|
||||
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<VoiceRecorderComponent, ActivateInWorldEvent>(OnActivate);
|
||||
SubscribeLocalEvent<VoiceRecorderComponent, ExaminedEvent>(OnExamine);
|
||||
SubscribeLocalEvent<VoiceRecorderComponent, ComponentInit>(OnInit);
|
||||
SubscribeLocalEvent<VoiceRecorderComponent, ListenEvent>(SaveEntityMessage);
|
||||
SubscribeLocalEvent<VoiceRecorderComponent, ListenAttemptEvent>(CanListen);
|
||||
SubscribeLocalEvent<VoiceRecorderComponent, GetVerbsEvent<AlternativeVerb>>(AddVerbs);
|
||||
}
|
||||
|
||||
public void OnInit(EntityUid uid, VoiceRecorderComponent component, ComponentInit args)
|
||||
{
|
||||
if (!TryComp<ActiveListenerComponent>(uid, out var listener))
|
||||
{
|
||||
RemComp<ActiveListenerComponent>(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<AlternativeVerb> 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<ActiveListenerComponent>(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<ActiveListenerComponent>(uid);
|
||||
_audioSystem.PlayPvs(component.SoundEndOfRecording, uid,
|
||||
AudioParams.Default
|
||||
.WithVariation(0.25f)
|
||||
.WithVolume(0.3f)
|
||||
.WithRolloffFactor(2.8f)
|
||||
.WithMaxDistance(1.5f));
|
||||
}
|
||||
if (TryComp<AppearanceComponent>(uid, out var appearance) &&
|
||||
TryComp<ItemComponent>(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;
|
||||
}
|
||||
}
|
||||
15
Resources/Locale/en-US/white/voice-recorder.ftl
Normal file
@@ -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.
|
||||
17
Resources/Locale/ru-RU/white/voice-recorder.ftl
Normal file
@@ -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 единицы. Не трубует дополнительных доступов.
|
||||
@@ -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
|
||||
|
||||
@@ -123,3 +123,12 @@
|
||||
contents:
|
||||
- id: BoxBodyCamera
|
||||
amount: 2
|
||||
|
||||
- type: entity
|
||||
id: CrateSecurityVoiceRecorder
|
||||
parent: CrateGenericSteel
|
||||
components:
|
||||
- type: StorageFill
|
||||
contents:
|
||||
- id: VoiceRecorder
|
||||
amount: 4
|
||||
|
||||
29
Resources/Prototypes/Entities/White/voice_recorder.yml
Normal file
@@ -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
|
||||
@@ -256,6 +256,9 @@
|
||||
},
|
||||
{
|
||||
"name": "paper_stamp-geraldiy"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "paper_stamp-transcript"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 3.9 KiB |
|
After Width: | Height: | Size: 3.7 KiB |
@@ -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
|
||||
]
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
After Width: | Height: | Size: 6.8 KiB |
|
After Width: | Height: | Size: 6.8 KiB |
|
After Width: | Height: | Size: 7.5 KiB |
|
After Width: | Height: | Size: 7.5 KiB |