diff --git a/Content.Client/Forensics/ForensicScannerBoundUserInterface.cs b/Content.Client/Forensics/ForensicScannerBoundUserInterface.cs
new file mode 100644
index 0000000000..c5d3f15659
--- /dev/null
+++ b/Content.Client/Forensics/ForensicScannerBoundUserInterface.cs
@@ -0,0 +1,40 @@
+using Content.Shared.Forensics;
+using Robust.Client.GameObjects;
+
+namespace Content.Client.Forensics
+{
+ public sealed class ForensicScannerBoundUserInterface : BoundUserInterface
+ {
+ private ForensicScannerMenu? _window;
+
+ public ForensicScannerBoundUserInterface(ClientUserInterfaceComponent owner, object uiKey) : base(owner, uiKey)
+ {
+ }
+
+ protected override void Open()
+ {
+ base.Open();
+ _window = new ForensicScannerMenu();
+ _window.OnClose += Close;
+ _window.Print.OnPressed += _ => Print();
+ _window.OpenCentered();
+ }
+
+ private void Print()
+ {
+ SendMessage(new ForensicScannerPrintMessage());
+ _window?.Close();
+ }
+
+ protected override void ReceiveMessage(BoundUserInterfaceMessage message)
+ {
+ if (_window == null)
+ return;
+
+ if (message is not ForensicScannerUserMessage cast)
+ return;
+
+ _window.Populate(cast);
+ }
+ }
+}
diff --git a/Content.Client/Forensics/ForensicScannerMenu.xaml b/Content.Client/Forensics/ForensicScannerMenu.xaml
new file mode 100644
index 0000000000..b3bfff7771
--- /dev/null
+++ b/Content.Client/Forensics/ForensicScannerMenu.xaml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
diff --git a/Content.Client/Forensics/ForensicScannerMenu.xaml.cs b/Content.Client/Forensics/ForensicScannerMenu.xaml.cs
new file mode 100644
index 0000000000..88ded00703
--- /dev/null
+++ b/Content.Client/Forensics/ForensicScannerMenu.xaml.cs
@@ -0,0 +1,37 @@
+using System.Text;
+using Content.Shared.Forensics;
+using Robust.Client.AutoGenerated;
+using Robust.Client.UserInterface.CustomControls;
+using Robust.Client.UserInterface.XAML;
+
+namespace Content.Client.Forensics
+{
+ [GenerateTypedNameReferences]
+ public sealed partial class ForensicScannerMenu : DefaultWindow
+ {
+ public ForensicScannerMenu()
+ {
+ RobustXamlLoader.Load(this);
+ }
+
+ public void Populate(ForensicScannerUserMessage msg)
+ {
+ Print.Disabled = false;
+ var text = new StringBuilder();
+
+ text.AppendLine(Loc.GetString("forensic-scanner-interface-fingerprints"));
+ foreach (var fingerprint in msg.Fingerprints)
+ {
+ text.AppendLine(fingerprint);
+ }
+ text.AppendLine();
+ text.AppendLine(Loc.GetString("forensic-scanner-interface-fibers"));
+ foreach (var fiber in msg.Fibers)
+ {
+ text.AppendLine(fiber);
+ }
+ Diagnostics.Text = text.ToString();
+ SetSize = (350, 600);
+ }
+ }
+}
diff --git a/Content.Server/Forensics/Components/FiberComponent.cs b/Content.Server/Forensics/Components/FiberComponent.cs
new file mode 100644
index 0000000000..2d45730460
--- /dev/null
+++ b/Content.Server/Forensics/Components/FiberComponent.cs
@@ -0,0 +1,16 @@
+namespace Content.Server.Forensics
+{
+ ///
+ /// This controls fibers left by gloves on items,
+ /// which the forensics system uses.
+ ///
+ [RegisterComponent]
+ public sealed class FiberComponent : Component
+ {
+ [DataField("fiberMaterial")]
+ public string FiberMaterial = "fibers-synthetic";
+
+ [DataField("fiberColor")]
+ public string? FiberColor;
+ }
+}
diff --git a/Content.Server/Forensics/Components/FingerprintComponent.cs b/Content.Server/Forensics/Components/FingerprintComponent.cs
new file mode 100644
index 0000000000..a75b90e3aa
--- /dev/null
+++ b/Content.Server/Forensics/Components/FingerprintComponent.cs
@@ -0,0 +1,12 @@
+namespace Content.Server.Forensics
+{
+ ///
+ /// This component is for mobs that leave fingerprints.
+ ///
+ [RegisterComponent]
+ public sealed class FingerprintComponent : Component
+ {
+ [DataField("fingerprint")]
+ public string? Fingerprint;
+ }
+}
diff --git a/Content.Server/Forensics/Components/FingerprintMaskComponent.cs b/Content.Server/Forensics/Components/FingerprintMaskComponent.cs
new file mode 100644
index 0000000000..a9d80d06ba
--- /dev/null
+++ b/Content.Server/Forensics/Components/FingerprintMaskComponent.cs
@@ -0,0 +1,10 @@
+namespace Content.Server.Forensics
+{
+ ///
+ /// This component stops the entity from leaving finger prints,
+ /// usually so fibres can be left instead.
+ ///
+ [RegisterComponent]
+ public sealed class FingerprintMaskComponent : Component
+ {}
+}
diff --git a/Content.Server/Forensics/Components/ForensicPadComponent.cs b/Content.Server/Forensics/Components/ForensicPadComponent.cs
new file mode 100644
index 0000000000..ffdf7b7683
--- /dev/null
+++ b/Content.Server/Forensics/Components/ForensicPadComponent.cs
@@ -0,0 +1,19 @@
+using System.Threading;
+
+namespace Content.Server.Forensics
+{
+ ///
+ /// Used to take a sample of someone's fingerprints.
+ ///
+ [RegisterComponent]
+ public sealed class ForensicPadComponent : Component
+ {
+ public CancellationTokenSource? CancelToken;
+
+ [DataField("scanDelay")]
+ public float ScanDelay = 3.0f;
+
+ public bool Used = false;
+ public String Sample = string.Empty;
+ }
+}
diff --git a/Content.Server/Forensics/Components/ForensicScannerComponent.cs b/Content.Server/Forensics/Components/ForensicScannerComponent.cs
new file mode 100644
index 0000000000..ff36b13979
--- /dev/null
+++ b/Content.Server/Forensics/Components/ForensicScannerComponent.cs
@@ -0,0 +1,27 @@
+using System.Threading;
+
+namespace Content.Server.Forensics
+{
+ [RegisterComponent]
+ public sealed class ForensicScannerComponent : Component
+ {
+ public CancellationTokenSource? CancelToken;
+
+ ///
+ /// A list of fingerprint GUIDs that the forensic scanner found from the on an entity.
+ ///
+ [ViewVariables(VVAccess.ReadOnly)]
+ public List Fingerprints = new();
+ ///
+ /// A list of glove fibers that the forensic scanner found from the on an entity.
+ ///
+ [ViewVariables(VVAccess.ReadOnly)]
+ public List Fibers = new();
+
+ ///
+ /// The time (in seconds) that it takes to scan an entity.
+ ///
+ [DataField("scanDelay")]
+ public float ScanDelay = 3.0f;
+ }
+}
diff --git a/Content.Server/Forensics/Components/ForensicsComponent.cs b/Content.Server/Forensics/Components/ForensicsComponent.cs
new file mode 100644
index 0000000000..bc054d96e1
--- /dev/null
+++ b/Content.Server/Forensics/Components/ForensicsComponent.cs
@@ -0,0 +1,12 @@
+namespace Content.Server.Forensics
+{
+ [RegisterComponent]
+ public sealed class ForensicsComponent : Component
+ {
+ [DataField("fingerprints")]
+ public HashSet Fingerprints = new();
+
+ [DataField("fibers")]
+ public HashSet Fibers = new();
+ }
+}
diff --git a/Content.Server/Forensics/Systems/ForensicPadSystem.cs b/Content.Server/Forensics/Systems/ForensicPadSystem.cs
new file mode 100644
index 0000000000..faa166e060
--- /dev/null
+++ b/Content.Server/Forensics/Systems/ForensicPadSystem.cs
@@ -0,0 +1,140 @@
+using System.Threading;
+using Content.Shared.Examine;
+using Content.Shared.Interaction;
+using Content.Shared.Inventory;
+using Content.Server.DoAfter;
+using Content.Server.Popups;
+using Robust.Shared.Player;
+
+namespace Content.Server.Forensics
+{
+ ///
+ /// Used to transfer fingerprints from entities to forensic pads.
+ ///
+ public sealed class ForensicPadSystem : EntitySystem
+ {
+ [Dependency] private readonly DoAfterSystem _doAfterSystem = default!;
+ [Dependency] private readonly InventorySystem _inventory = default!;
+
+ [Dependency] private readonly PopupSystem _popupSystem = default!;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+ SubscribeLocalEvent(OnExamined);
+ SubscribeLocalEvent(OnAfterInteract);
+ SubscribeLocalEvent(OnTargetPadSuccessful);
+ SubscribeLocalEvent(OnPadCancelled);
+ }
+
+ private void OnExamined(EntityUid uid, ForensicPadComponent component, ExaminedEvent args)
+ {
+ if (!args.IsInDetailsRange)
+ return;
+
+ if (!component.Used)
+ {
+ args.PushMarkup(Loc.GetString("forensic-pad-unused"));
+ return;
+ }
+
+ args.PushMarkup(Loc.GetString("forensic-pad-sample", ("sample", component.Sample)));
+ }
+
+ private void OnAfterInteract(EntityUid uid, ForensicPadComponent component, AfterInteractEvent args)
+ {
+ if (component.CancelToken != null || !args.CanReach || args.Target == null)
+ return;
+
+ if (HasComp(args.Target))
+ return;
+
+ args.Handled = true;
+
+ if (component.Used)
+ {
+ _popupSystem.PopupEntity(Loc.GetString("forensic-pad-already-used"), args.Target.Value, Filter.Entities(args.User));
+ return;
+ }
+
+ if (_inventory.TryGetSlotEntity(args.Target.Value, "gloves", out var gloves))
+ {
+ _popupSystem.PopupEntity(Loc.GetString("forensic-pad-gloves", ("target", args.Target.Value)), args.Target.Value, Filter.Entities(args.User));
+ return;
+ }
+
+ if (TryComp(args.Target, out var fingerprint) && fingerprint.Fingerprint != null)
+ {
+ if (args.User != args.Target)
+ {
+ _popupSystem.PopupEntity(Loc.GetString("forensic-pad-start-scan-user", ("target", args.Target.Value)), args.Target.Value, Filter.Entities(args.User));
+ _popupSystem.PopupEntity(Loc.GetString("forensic-pad-start-scan-target", ("user", args.User)), args.Target.Value, Filter.Entities(args.Target.Value));
+ }
+ StartScan(args.User, args.Target.Value, component, fingerprint.Fingerprint);
+ return;
+ }
+
+ if (TryComp(args.Target, out var fiber))
+ StartScan(args.User, args.Target.Value, component, string.IsNullOrEmpty(fiber.FiberColor) ? Loc.GetString("forensic-fibers", ("material", fiber.FiberMaterial)) : Loc.GetString("forensic-fibers-colored", ("color", fiber.FiberColor), ("material", fiber.FiberMaterial)));
+ }
+
+ private void StartScan(EntityUid user, EntityUid target, ForensicPadComponent pad, string sample)
+ {
+ pad.CancelToken = new CancellationTokenSource();
+ _doAfterSystem.DoAfter(new DoAfterEventArgs(user, pad.ScanDelay, pad.CancelToken.Token, target: target)
+ {
+ BroadcastFinishedEvent = new TargetPadSuccessfulEvent(user, target, pad.Owner, sample),
+ BroadcastCancelledEvent = new PadCancelledEvent(pad.Owner),
+ BreakOnTargetMove = true,
+ BreakOnUserMove = true,
+ BreakOnStun = true,
+ NeedHand = true
+ });
+ }
+
+ ///
+ /// When the forensic pad is successfully used, take their fingerprint sample and flag the pad as used.
+ ///
+ private void OnTargetPadSuccessful(TargetPadSuccessfulEvent ev)
+ {
+ if (!EntityManager.TryGetComponent(ev.Pad, out ForensicPadComponent? component))
+ return;
+
+ component.CancelToken = null;
+ component.Sample = ev.Sample;
+ component.Used = true;
+ }
+ private void OnPadCancelled(PadCancelledEvent ev)
+ {
+ if (!EntityManager.TryGetComponent(ev.Pad, out ForensicPadComponent? component))
+ return;
+ component.CancelToken = null;
+ }
+
+ private sealed class PadCancelledEvent : EntityEventArgs
+ {
+ public EntityUid Pad;
+
+ public PadCancelledEvent(EntityUid pad)
+ {
+ Pad = pad;
+ }
+ }
+
+ private sealed class TargetPadSuccessfulEvent : EntityEventArgs
+ {
+ public EntityUid User;
+ public EntityUid? Target;
+ public EntityUid Pad;
+ public string Sample = string.Empty;
+
+ public TargetPadSuccessfulEvent(EntityUid user, EntityUid? target, EntityUid pad, string sample)
+ {
+ User = user;
+ Target = target;
+ Pad = pad;
+ Sample = sample;
+ }
+ }
+ }
+}
diff --git a/Content.Server/Forensics/Systems/ForensicScannerSystem.cs b/Content.Server/Forensics/Systems/ForensicScannerSystem.cs
new file mode 100644
index 0000000000..3eca8f8fa3
--- /dev/null
+++ b/Content.Server/Forensics/Systems/ForensicScannerSystem.cs
@@ -0,0 +1,169 @@
+using System.Linq;
+using System.Text; // todo: remove this stinky LINQy
+using System.Threading;
+using Content.Server.DoAfter;
+using Content.Server.Paper;
+using Content.Server.Popups;
+using Content.Shared.Forensics;
+using Content.Shared.Hands.EntitySystems;
+using Content.Shared.Interaction;
+using Robust.Server.GameObjects;
+using Robust.Shared.Audio;
+using Robust.Shared.Player;
+
+namespace Content.Server.Forensics
+{
+ public sealed class ForensicScannerSystem : EntitySystem
+ {
+ [Dependency] private readonly DoAfterSystem _doAfterSystem = default!;
+ [Dependency] private readonly UserInterfaceSystem _uiSystem = default!;
+ [Dependency] private readonly PopupSystem _popupSystem = default!;
+ [Dependency] private readonly PaperSystem _paperSystem = default!;
+ [Dependency] private readonly SharedHandsSystem _handsSystem = default!;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent(OnAfterInteract);
+ SubscribeLocalEvent(OnAfterInteractUsing);
+ SubscribeLocalEvent(OnPrint);
+ SubscribeLocalEvent(OnTargetScanSuccessful);
+ SubscribeLocalEvent(OnScanCancelled);
+ }
+
+ private void OnScanCancelled(ScanCancelledEvent ev)
+ {
+ if (!EntityManager.TryGetComponent(ev.Scanner, out ForensicScannerComponent? scanner))
+ return;
+ scanner.CancelToken = null;
+ }
+
+ private void OnTargetScanSuccessful(TargetScanSuccessfulEvent ev)
+ {
+ if (!EntityManager.TryGetComponent(ev.Scanner, out ForensicScannerComponent? scanner))
+ return;
+
+ scanner.CancelToken = null;
+
+ if (!TryComp(ev.Target, out var forensics))
+ return;
+
+ scanner.Fingerprints = forensics.Fingerprints.ToList();
+ scanner.Fibers = forensics.Fibers.ToList();
+ OpenUserInterface(ev.User, scanner);
+ }
+
+ private void OnAfterInteract(EntityUid uid, ForensicScannerComponent component, AfterInteractEvent args)
+ {
+ if (component.CancelToken != null || args.Target == null || !args.CanReach)
+ return;
+
+ component.CancelToken = new CancellationTokenSource();
+ _doAfterSystem.DoAfter(new DoAfterEventArgs(args.User, component.ScanDelay, component.CancelToken.Token, target: args.Target)
+ {
+ BroadcastFinishedEvent = new TargetScanSuccessfulEvent(args.User, args.Target, component.Owner),
+ BroadcastCancelledEvent = new ScanCancelledEvent(component.Owner),
+ BreakOnTargetMove = true,
+ BreakOnUserMove = true,
+ BreakOnStun = true,
+ NeedHand = true
+ });
+ }
+
+ private void OnAfterInteractUsing(EntityUid uid, ForensicScannerComponent component, AfterInteractUsingEvent args)
+ {
+ if (args.Handled || !args.CanReach)
+ return;
+
+ if (!TryComp(args.Used, out var pad))
+ return;
+
+ foreach (var fiber in component.Fibers)
+ {
+ if (fiber == pad.Sample)
+ {
+ SoundSystem.Play("/Audio/Machines/Nuke/angry_beep.ogg", Filter.Pvs(uid), uid);
+ _popupSystem.PopupEntity(Loc.GetString("forensic-scanner-match-fiber"), uid, Filter.Entities(args.User));
+ return;
+ }
+ }
+
+ foreach (var fingerprint in component.Fingerprints)
+ {
+ if (fingerprint == pad.Sample)
+ {
+ SoundSystem.Play("/Audio/Machines/Nuke/angry_beep.ogg", Filter.Pvs(uid), uid);
+ _popupSystem.PopupEntity(Loc.GetString("forensic-scanner-match-fingerprint"), uid, Filter.Entities(args.User));
+ return;
+ }
+ }
+ SoundSystem.Play("/Audio/Machines/airlock_deny.ogg", Filter.Pvs(uid), uid);
+ _popupSystem.PopupEntity(Loc.GetString("forensic-scanner-match-none"), uid, Filter.Entities(args.User));
+ }
+
+ private void OpenUserInterface(EntityUid user, ForensicScannerComponent component)
+ {
+ if (!TryComp(user, out var actor))
+ return;
+
+ var ui = _uiSystem.GetUi(component.Owner, ForensicScannerUiKey.Key);
+
+ ui.Open(actor.PlayerSession);
+ ui.SendMessage(new ForensicScannerUserMessage(component.Fingerprints, component.Fibers));
+ }
+
+ private void OnPrint(EntityUid uid, ForensicScannerComponent component, ForensicScannerPrintMessage args)
+ {
+ if (!args.Session.AttachedEntity.HasValue || (component.Fibers.Count == 0 && component.Fingerprints.Count == 0)) return;
+
+ // spawn a piece of paper.
+ var printed = EntityManager.SpawnEntity("Paper", Transform(args.Session.AttachedEntity.Value).Coordinates);
+ _handsSystem.PickupOrDrop(args.Session.AttachedEntity, printed, checkActionBlocker: false);
+
+ if (!TryComp(printed, out var paper))
+ return;
+
+ MetaData(printed).EntityName = Loc.GetString("forensic-scanner-report-title");
+
+ var text = new StringBuilder();
+
+ text.AppendLine(Loc.GetString("forensic-scanner-interface-fingerprints"));
+ foreach (var fingerprint in component.Fingerprints)
+ {
+ text.AppendLine(fingerprint);
+ }
+ text.AppendLine();
+ text.AppendLine(Loc.GetString("forensic-scanner-interface-fibers"));
+ foreach (var fiber in component.Fibers)
+ {
+ text.AppendLine(fiber);
+ }
+
+ _paperSystem.SetContent(printed, text.ToString());
+ }
+
+ private sealed class ScanCancelledEvent : EntityEventArgs
+ {
+ public EntityUid Scanner;
+
+ public ScanCancelledEvent(EntityUid scanner)
+ {
+ Scanner = scanner;
+ }
+ }
+
+ private sealed class TargetScanSuccessfulEvent : EntityEventArgs
+ {
+ public EntityUid User;
+ public EntityUid? Target;
+ public EntityUid Scanner;
+ public TargetScanSuccessfulEvent(EntityUid user, EntityUid? target, EntityUid scanner)
+ {
+ User = user;
+ Target = target;
+ Scanner = scanner;
+ }
+ }
+ }
+}
diff --git a/Content.Server/Forensics/Systems/ForensicsSystem.cs b/Content.Server/Forensics/Systems/ForensicsSystem.cs
new file mode 100644
index 0000000000..b1b67fb476
--- /dev/null
+++ b/Content.Server/Forensics/Systems/ForensicsSystem.cs
@@ -0,0 +1,49 @@
+using Content.Shared.Inventory;
+using Content.Shared.Item;
+using Robust.Shared.Random;
+
+namespace Content.Server.Forensics
+{
+ public sealed class ForensicsSystem : EntitySystem
+ {
+ [Dependency] private readonly IRobustRandom _random = default!;
+ [Dependency] private readonly InventorySystem _inventory = default!;
+ public override void Initialize()
+ {
+ SubscribeLocalEvent(OnInteract);
+ SubscribeLocalEvent(OnInit);
+ }
+
+ private void OnInteract(EntityUid uid, FingerprintComponent component, UserInteractedWithItemEvent args)
+ {
+ ApplyEvidence(args.User, args.Item);
+ }
+
+ private void OnInit(EntityUid uid, FingerprintComponent component, ComponentInit args)
+ {
+ component.Fingerprint = GenerateFingerprint();
+ }
+
+ private string GenerateFingerprint()
+ {
+ byte[] fingerprint = new byte[16];
+ _random.NextBytes(fingerprint);
+ return Convert.ToHexString(fingerprint);
+ }
+
+ private void ApplyEvidence(EntityUid user, EntityUid target)
+ {
+ var component = EnsureComp(target);
+ if (_inventory.TryGetSlotEntity(user, "gloves", out var gloves))
+ {
+ if (TryComp(gloves, out var fiber) && !string.IsNullOrEmpty(fiber.FiberMaterial))
+ component.Fibers.Add(string.IsNullOrEmpty(fiber.FiberColor) ? Loc.GetString("forensic-fibers", ("material", fiber.FiberMaterial)) : Loc.GetString("forensic-fibers-colored", ("color", fiber.FiberColor), ("material", fiber.FiberMaterial)));
+
+ if (HasComp(gloves))
+ return;
+ }
+ if (TryComp(user, out var fingerprint))
+ component.Fingerprints.Add(fingerprint.Fingerprint ?? "");
+ }
+ }
+}
diff --git a/Content.Shared/ActionBlocker/ActionBlockerSystem.cs b/Content.Shared/ActionBlocker/ActionBlockerSystem.cs
index 4d3ff2619e..516b4365f5 100644
--- a/Content.Shared/ActionBlocker/ActionBlockerSystem.cs
+++ b/Content.Shared/ActionBlocker/ActionBlockerSystem.cs
@@ -75,6 +75,9 @@ namespace Content.Shared.ActionBlocker
var targetEv = new GettingInteractedWithAttemptEvent(user, target);
RaiseLocalEvent(target.Value, targetEv, true);
+ if (!targetEv.Cancelled)
+ InteractWithItem(user, target.Value);
+
return !targetEv.Cancelled;
}
@@ -128,6 +131,10 @@ namespace Content.Shared.ActionBlocker
var itemEv = new GettingPickedUpAttemptEvent(user, item);
RaiseLocalEvent(item, itemEv, false);
+
+ if (!itemEv.Cancelled)
+ InteractWithItem(user, item);
+
return !itemEv.Cancelled;
}
@@ -171,5 +178,11 @@ namespace Content.Shared.ActionBlocker
return !ev.Cancelled;
}
+
+ private void InteractWithItem(EntityUid user, EntityUid item)
+ {
+ var itemEvent = new UserInteractedWithItemEvent(user, item);
+ RaiseLocalEvent(user, itemEvent);
+ }
}
}
diff --git a/Content.Shared/Forensics/ForensicScannerEvent.cs b/Content.Shared/Forensics/ForensicScannerEvent.cs
new file mode 100644
index 0000000000..13f0a890c4
--- /dev/null
+++ b/Content.Shared/Forensics/ForensicScannerEvent.cs
@@ -0,0 +1,28 @@
+using Robust.Shared.Serialization;
+
+namespace Content.Shared.Forensics
+{
+ [Serializable, NetSerializable]
+ public sealed class ForensicScannerUserMessage : BoundUserInterfaceMessage
+ {
+ public readonly List Fingerprints = new();
+ public readonly List Fibers = new();
+
+ public ForensicScannerUserMessage(List fingerprints, List fibers)
+ {
+ Fingerprints = fingerprints;
+ Fibers = fibers;
+ }
+ }
+
+ [Serializable, NetSerializable]
+ public enum ForensicScannerUiKey : byte
+ {
+ Key
+ }
+
+ [Serializable, NetSerializable]
+ public sealed class ForensicScannerPrintMessage : BoundUserInterfaceMessage
+ {
+ }
+}
diff --git a/Content.Shared/Item/UserInteractedWithItemEvent.cs b/Content.Shared/Item/UserInteractedWithItemEvent.cs
new file mode 100644
index 0000000000..0d88348727
--- /dev/null
+++ b/Content.Shared/Item/UserInteractedWithItemEvent.cs
@@ -0,0 +1,17 @@
+namespace Content.Shared.Item;
+
+///
+/// Raised on the user after they do any sort of interaction with an item,
+/// useful for when you want a component on the user to do something to the item.
+/// E.g. forensics, disease, etc.
+///
+public sealed class UserInteractedWithItemEvent : EntityEventArgs
+{
+ public EntityUid User;
+ public EntityUid Item;
+ public UserInteractedWithItemEvent(EntityUid user, EntityUid item)
+ {
+ User = user;
+ Item = item;
+ }
+}
diff --git a/Resources/Locale/en-US/forensics/fibers.ftl b/Resources/Locale/en-US/forensics/fibers.ftl
new file mode 100644
index 0000000000..4d420be823
--- /dev/null
+++ b/Resources/Locale/en-US/forensics/fibers.ftl
@@ -0,0 +1,22 @@
+forensic-fibers = {LOC($material)} fibers
+forensic-fibers-colored = {LOC($color)} {LOC($material)} fibers
+
+fibers-insulative = insulative
+fibers-synthetic = synthetic
+fibers-leather = leather
+fibers-durathread = durathread
+fibers-latex = latex
+fibers-nitrile = nitrile
+fibers-nanomachines = insulative nanomachine
+
+fibers-purple = purple
+fibers-red = red
+fibers-black = black
+fibers-blue = blue
+fibers-brown = brown
+fibers-grey = grey
+fibers-green = green
+fibers-orange = orange
+fibers-white = white
+fibers-yellow = yellow
+fibers-regal-blue = regal blue
diff --git a/Resources/Locale/en-US/forensics/forensics.ftl b/Resources/Locale/en-US/forensics/forensics.ftl
new file mode 100644
index 0000000000..5d0ab78c3f
--- /dev/null
+++ b/Resources/Locale/en-US/forensics/forensics.ftl
@@ -0,0 +1,15 @@
+forensic-scanner-interface-title = Forensic scanner
+forensic-scanner-interface-fingerprints = Fingerprints
+forensic-scanner-interface-fibers = Fibers
+forensic-scanner-interface-no-data = No scan data available
+forensic-scanner-interface-print = Print
+forensic-scanner-report-title = Forensics Report
+forensic-pad-unused = It hasn't been used.
+forensic-pad-sample = It has a sample: {$sample}
+forensic-pad-gloves = {CAPITALIZE($target)} is wearing gloves.
+forensic-pad-start-scan-target = {CAPITALIZE($user)} is trying to take a sample of your fingerprints.
+forensic-pad-start-scan-user = You start taking a sample of {CAPITALIZE($target)}'s fingerprints.
+forensic-pad-already-used = This pad has already been used.
+forensic-scanner-match-fiber = Match in fiber found!
+forensic-scanner-match-fingerprint = Match in fingerprint found!
+forensic-scanner-match-none = No matches found!
diff --git a/Resources/Prototypes/Catalog/Fills/Backpacks/StarterGear/backpack.yml b/Resources/Prototypes/Catalog/Fills/Backpacks/StarterGear/backpack.yml
index 3360048845..d2edb5ea11 100644
--- a/Resources/Prototypes/Catalog/Fills/Backpacks/StarterGear/backpack.yml
+++ b/Resources/Prototypes/Catalog/Fills/Backpacks/StarterGear/backpack.yml
@@ -128,6 +128,19 @@
- id: AcousticGuitarInstrument
- id: SaxophoneInstrument
+- type: entity
+ noSpawn: true
+ parent: ClothingBackpack
+ id: ClothingBackpackDetectiveFilled
+ components:
+ - type: StorageFill
+ contents:
+ - id: BoxSurvival
+ - id: Lighter
+ - id: CigPackBlack
+ - id: HandLabeler
+ - id: BoxForensicPad
+
# ERT
- type: entity
diff --git a/Resources/Prototypes/Catalog/Fills/Backpacks/StarterGear/duffelbag.yml b/Resources/Prototypes/Catalog/Fills/Backpacks/StarterGear/duffelbag.yml
index 3a7699a047..43bd9a859a 100644
--- a/Resources/Prototypes/Catalog/Fills/Backpacks/StarterGear/duffelbag.yml
+++ b/Resources/Prototypes/Catalog/Fills/Backpacks/StarterGear/duffelbag.yml
@@ -105,3 +105,17 @@
- id: BoxSurvival
- id: AcousticGuitarInstrument
- id: SaxophoneInstrument
+
+- type: entity
+ noSpawn: true
+ parent: ClothingBackpackDuffel
+ id: ClothingBackpackDuffelDetectiveFilled
+ components:
+ - type: StorageFill
+ contents:
+ - id: BoxSurvival
+ - id: Lighter
+ - id: CigPackBlack
+ - id: BoxForensicPad
+ - id: HandLabeler
+
diff --git a/Resources/Prototypes/Catalog/Fills/Backpacks/StarterGear/satchel.yml b/Resources/Prototypes/Catalog/Fills/Backpacks/StarterGear/satchel.yml
index 80e178f06a..2899ba405a 100644
--- a/Resources/Prototypes/Catalog/Fills/Backpacks/StarterGear/satchel.yml
+++ b/Resources/Prototypes/Catalog/Fills/Backpacks/StarterGear/satchel.yml
@@ -104,6 +104,19 @@
- id: AcousticGuitarInstrument
- id: SaxophoneInstrument
+- type: entity
+ noSpawn: true
+ parent: ClothingBackpackSatchel
+ id: ClothingBackpackSatchelDetectiveFilled
+ components:
+ - type: StorageFill
+ contents:
+ - id: BoxSurvival
+ - id: Lighter
+ - id: CigPackBlack
+ - id: BoxForensicPad
+ - id: HandLabeler
+
- type: entity
noSpawn: true
parent: ClothingBackpackSatchel
diff --git a/Resources/Prototypes/Catalog/Fills/Boxes/security.yml b/Resources/Prototypes/Catalog/Fills/Boxes/security.yml
index 977c7bfe86..bb49741ccf 100644
--- a/Resources/Prototypes/Catalog/Fills/Boxes/security.yml
+++ b/Resources/Prototypes/Catalog/Fills/Boxes/security.yml
@@ -58,6 +58,20 @@
- state: box_security
- state: ziptie
+- type: entity
+ name: forensic pad box
+ parent: BoxCardboard
+ id: BoxForensicPad
+ description: A box of forensic pads.
+ components:
+ - type: StorageFill
+ contents:
+ - id: ForensicPad
+ amount: 10
+ - type: Sprite
+ layers:
+ - state: box_security
+
# TODO: THESE ARE BAD AND ARE DEPRECATED, DON'T USE THEM PLEASE
- type: entity
name: box of shotgun beanbag cartridges
diff --git a/Resources/Prototypes/Catalog/Fills/Lockers/security.yml b/Resources/Prototypes/Catalog/Fills/Lockers/security.yml
index 0a74b8986e..40ee7ee22c 100644
--- a/Resources/Prototypes/Catalog/Fills/Lockers/security.yml
+++ b/Resources/Prototypes/Catalog/Fills/Lockers/security.yml
@@ -53,6 +53,8 @@
- id: ClothingOuterVestDetective
- id: ClothingOuterCoatDetective
- id: FlashlightSeclite
+ - id: ForensicScanner
+ - id: BoxForensicPad
- id: WeaponRevolverInspector
- type: entity
diff --git a/Resources/Prototypes/Entities/Clothing/Hands/colored.yml b/Resources/Prototypes/Entities/Clothing/Hands/colored.yml
index 4b4f6c91e5..9db664b706 100644
--- a/Resources/Prototypes/Entities/Clothing/Hands/colored.yml
+++ b/Resources/Prototypes/Entities/Clothing/Hands/colored.yml
@@ -8,6 +8,10 @@
sprite: Clothing/Hands/Gloves/Color/purple.rsi
- type: Clothing
sprite: Clothing/Hands/Gloves/Color/purple.rsi
+ - type: Fiber
+ fiberMaterial: fibers-synthetic
+ fiberColor: fibers-purple
+ - type: FingerprintMask
- type: entity
parent: ClothingHandsBase
@@ -19,6 +23,10 @@
sprite: Clothing/Hands/Gloves/Color/red.rsi
- type: Clothing
sprite: Clothing/Hands/Gloves/Color/red.rsi
+ - type: Fiber
+ fiberMaterial: fibers-synthetic
+ fiberColor: fibers-red
+ - type: FingerprintMask
- type: entity
parent: ClothingHandsBase
@@ -31,6 +39,10 @@
- type: Clothing
sprite: Clothing/Hands/Gloves/Color/black.rsi
HeatResistance: 1400
+ - type: Fiber
+ fiberMaterial: fibers-synthetic
+ fiberColor: fibers-black
+ - type: FingerprintMask
- type: entity
parent: ClothingHandsBase
@@ -42,6 +54,10 @@
sprite: Clothing/Hands/Gloves/Color/blue.rsi
- type: Clothing
sprite: Clothing/Hands/Gloves/Color/blue.rsi
+ - type: Fiber
+ fiberMaterial: fibers-synthetic
+ fiberColor: fibers-blue
+ - type: FingerprintMask
- type: entity
parent: ClothingHandsBase
@@ -53,6 +69,10 @@
sprite: Clothing/Hands/Gloves/Color/brown.rsi
- type: Clothing
sprite: Clothing/Hands/Gloves/Color/brown.rsi
+ - type: Fiber
+ fiberMaterial: fibers-synthetic
+ fiberColor: fibers-brown
+ - type: FingerprintMask
- type: entity
parent: ClothingHandsBase
@@ -64,6 +84,10 @@
sprite: Clothing/Hands/Gloves/Color/gray.rsi
- type: Clothing
sprite: Clothing/Hands/Gloves/Color/gray.rsi
+ - type: Fiber
+ fiberMaterial: fibers-synthetic
+ fiberColor: fibers-grey
+ - type: FingerprintMask
- type: entity
parent: ClothingHandsBase
@@ -75,6 +99,10 @@
sprite: Clothing/Hands/Gloves/Color/green.rsi
- type: Clothing
sprite: Clothing/Hands/Gloves/Color/green.rsi
+ - type: Fiber
+ fiberMaterial: fibers-synthetic
+ fiberColor: fibers-green
+ - type: FingerprintMask
- type: entity
parent: ClothingHandsBase
@@ -86,6 +114,10 @@
sprite: Clothing/Hands/Gloves/Color/lightbrown.rsi
- type: Clothing
sprite: Clothing/Hands/Gloves/Color/lightbrown.rsi
+ - type: Fiber
+ fiberMaterial: fibers-synthetic
+ fiberColor: fibers-brown
+ - type: FingerprintMask
- type: entity
parent: ClothingHandsBase
@@ -97,6 +129,10 @@
sprite: Clothing/Hands/Gloves/Color/orange.rsi
- type: Clothing
sprite: Clothing/Hands/Gloves/Color/orange.rsi
+ - type: Fiber
+ fiberMaterial: fibers-synthetic
+ fiberColor: fibers-orange
+ - type: FingerprintMask
- type: entity
parent: ClothingHandsBase
@@ -108,6 +144,10 @@
sprite: Clothing/Hands/Gloves/Color/white.rsi
- type: Clothing
sprite: Clothing/Hands/Gloves/Color/white.rsi
+ - type: Fiber
+ fiberMaterial: fibers-synthetic
+ fiberColor: fibers-white
+ - type: FingerprintMask
- type: entity
parent: ClothingHandsBase
@@ -121,6 +161,10 @@
sprite: Clothing/Hands/Gloves/Color/yellow.rsi
HeatResistance: 1400
- type: Insulated
+ - type: Fiber
+ fiberMaterial: fibers-insulative
+ fiberColor: fibers-yellow
+ - type: FingerprintMask
- type: entity
parent: ClothingHandsGlovesColorYellow
@@ -131,6 +175,10 @@
- type: Clothing
HeatResistance: 0
- type: Insulated
+ - type: Fiber
+ fiberMaterial: fibers-insulative
+ fiberColor: fibers-yellow
+ - type: FingerprintMask
- type: RandomInsulation
# Why repeated numbers? So some numbers are more common, of course!
list:
diff --git a/Resources/Prototypes/Entities/Clothing/Hands/gloves.yml b/Resources/Prototypes/Entities/Clothing/Hands/gloves.yml
index efa38fb3d4..50cbee6b3a 100644
--- a/Resources/Prototypes/Entities/Clothing/Hands/gloves.yml
+++ b/Resources/Prototypes/Entities/Clothing/Hands/gloves.yml
@@ -8,6 +8,10 @@
sprite: Clothing/Hands/Gloves/boxing.rsi
- type: Clothing
sprite: Clothing/Hands/Gloves/boxing.rsi
+ - type: Fiber
+ fiberMaterial: fibers-leather
+ fiberColor: fibers-red
+ - type: FingerprintMask
- type: entity
parent: ClothingHandsBase
@@ -21,6 +25,10 @@
- type: Clothing
sprite: Clothing/Hands/Gloves/boxing.rsi
HeldPrefix: blue
+ - type: Fiber
+ fiberMaterial: fibers-leather
+ fiberColor: fibers-blue
+ - type: FingerprintMask
- type: entity
parent: ClothingHandsBase
@@ -34,6 +42,10 @@
- type: Clothing
sprite: Clothing/Hands/Gloves/boxing.rsi
HeldPrefix: green
+ - type: Fiber
+ fiberMaterial: fibers-leather
+ fiberColor: fibers-green
+ - type: FingerprintMask
- type: entity
parent: ClothingHandsBase
@@ -47,6 +59,10 @@
- type: Clothing
sprite: Clothing/Hands/Gloves/boxing.rsi
HeldPrefix: yellow
+ - type: Fiber
+ fiberMaterial: fibers-leather
+ fiberColor: fibers-yellow
+ - type: FingerprintMask
- type: entity
parent: ClothingHandsBase
@@ -60,6 +76,10 @@
sprite: Clothing/Hands/Gloves/captain.rsi
HeatResistance: 1400
- type: Insulated
+ - type: Fiber
+ fiberMaterial: fibers-durathread
+ fiberColor: fibers-regal-blue
+ - type: FingerprintMask
- type: entity
parent: ClothingHandsBase
@@ -71,6 +91,10 @@
sprite: Clothing/Hands/Gloves/ihscombat.rsi
- type: Clothing
sprite: Clothing/Hands/Gloves/ihscombat.rsi
+ - type: Fiber
+ fiberMaterial: fibers-durathread
+ - type: FingerprintMask
+
#### Medical
- type: entity
parent: ClothingHandsBase
@@ -84,6 +108,9 @@
sprite: Clothing/Hands/Gloves/latex.rsi
- type: DiseaseProtection
protection: 0.1
+ - type: Fiber
+ fiberMaterial: fibers-latex
+ - type: FingerprintMask
- type: entity
parent: ClothingHandsBase
@@ -97,29 +124,39 @@
sprite: Clothing/Hands/Gloves/Color/blue.rsi
- type: DiseaseProtection
protection: 0.15
+ - type: Fiber
+ fiberMaterial: fibers-nitrile
+ - type: FingerprintMask
####
- type: entity
parent: ClothingHandsBase
id: ClothingHandsGlovesLeather
- name: "botanist's leather gloves"
- description: "These leather gloves protect against thorns, barbs, prickles, spikes and other harmful objects of floral origin. They're also quite warm."
+ name: botanist's leather gloves
+ description: These leather gloves protect against thorns, barbs, prickles, spikes and other harmful objects of floral origin. They're also quite warm.
components:
- type: Sprite
sprite: Clothing/Hands/Gloves/leather.rsi
- type: Clothing
sprite: Clothing/Hands/Gloves/leather.rsi
HeatResistance: 1400
+ - type: Fiber
+ fiberMaterial: fibers-leather
+ fiberColor: fibers-brown
+ - type: FingerprintMask
- type: entity
parent: ClothingHandsBase
id: ClothingHandsGlovesPowerglove
name: power gloves
- description: Now I'm playin' with power! Wait, they are turned off. # Use "Now I'm playin' with power! BAM!" for when they're turned on
+ description: Now I'm playin' with power! Wait, they are turned off. # Use Now I'm playin' with power! BAM! for when they're turned on
components:
- type: Sprite
sprite: Clothing/Hands/Gloves/powerglove.rsi
- type: Clothing
sprite: Clothing/Hands/Gloves/powerglove.rsi
+ - type: Fiber
+ fiberMaterial: fibers-nanomachines
+ - type: FingerprintMask
- type: entity
parent: ClothingHandsBase
@@ -131,6 +168,10 @@
sprite: Clothing/Hands/Gloves/robohands.rsi
- type: Clothing
sprite: Clothing/Hands/Gloves/robohands.rsi
+ - type: Fiber
+ fiberMaterial: fibers-leather
+ fiberColor: fibers-black
+ - type: FingerprintMask
- type: entity
parent: ClothingHandsBase
@@ -144,6 +185,9 @@
sprite: Clothing/Hands/Gloves/spaceninja.rsi
HeatResistance: 1400
- type: Insulated
+ - type: Fiber
+ fiberMaterial: fibers-nanomachines
+ - type: FingerprintMask
- type: Thieving
stealTime: 1
stealthy: true
@@ -160,6 +204,10 @@
sprite: Clothing/Hands/Gloves/Color/black.rsi
HeatResistance: 1400
- type: Insulated
+ - type: Fiber
+ fiberMaterial: fibers-insulative
+ fiberColor: fibers-black
+ - type: FingerprintMask
- type: entity
parent: ClothingHandsBase
@@ -171,6 +219,9 @@
sprite: Clothing/Hands/Gloves/fingerless.rsi
- type: Clothing
sprite: Clothing/Hands/Gloves/fingerless.rsi
+ - type: Fiber
+ fiberMaterial: fibers-synthetic
+ fiberColor: fibers-black
- type: entity
parent: ClothingHandsBase
diff --git a/Resources/Prototypes/Entities/Mobs/Species/human.yml b/Resources/Prototypes/Entities/Mobs/Species/human.yml
index 74bb80fee4..b6b3a136ac 100644
--- a/Resources/Prototypes/Entities/Mobs/Species/human.yml
+++ b/Resources/Prototypes/Entities/Mobs/Species/human.yml
@@ -318,6 +318,7 @@
attributes:
proper: true
- type: StandingState
+ - type: Fingerprint
- type: MobPrice
price: 1500 # Kidnapping a living person and selling them for cred is a good move.
deathPenalty: 0.01 # However they really ought to be living and intact, otherwise they're worth 100x less.
diff --git a/Resources/Prototypes/Entities/Objects/Devices/forensic_scanner.yml b/Resources/Prototypes/Entities/Objects/Devices/forensic_scanner.yml
new file mode 100644
index 0000000000..0bf63af029
--- /dev/null
+++ b/Resources/Prototypes/Entities/Objects/Devices/forensic_scanner.yml
@@ -0,0 +1,23 @@
+- type: entity
+ name: forensic scanner
+ parent: BaseItem
+ id: ForensicScanner
+ description: A handheld device that can scan objects for fingerprints and fibers.
+ components:
+ - type: Sprite
+ netsync: false
+ sprite: Objects/Devices/forensic_scanner.rsi
+ state: forensicnew
+ - type: Clothing
+ size: 5
+ sprite: Objects/Devices/forensic_scanner.rsi
+ quickEquip: false
+ Slots:
+ - Belt
+ - type: ActivatableUI
+ key: enum.ForensicScannerUiKey.Key
+ - type: UserInterface
+ interfaces:
+ - key: enum.ForensicScannerUiKey.Key
+ type: ForensicScannerBoundUserInterface
+ - type: ForensicScanner
diff --git a/Resources/Prototypes/Entities/Objects/Specific/Forensics/forensics.yml b/Resources/Prototypes/Entities/Objects/Specific/Forensics/forensics.yml
new file mode 100644
index 0000000000..96222ed03d
--- /dev/null
+++ b/Resources/Prototypes/Entities/Objects/Specific/Forensics/forensics.yml
@@ -0,0 +1,15 @@
+- type: entity
+ id: ForensicPad
+ name: forensic pad
+ parent: BaseItem
+ description: A forensic pad for collecting fingerprints or fibers.
+ components:
+ - type: Item
+ size: 3
+ - type: ForensicPad
+ - type: Sprite
+ sprite: Objects/Misc/bureaucracy.rsi
+ netsync: false
+ layers:
+ - state: paper
+ color: yellow
diff --git a/Resources/Prototypes/Entities/Objects/base_item.yml b/Resources/Prototypes/Entities/Objects/base_item.yml
index 8498733c36..5e90746d13 100644
--- a/Resources/Prototypes/Entities/Objects/base_item.yml
+++ b/Resources/Prototypes/Entities/Objects/base_item.yml
@@ -48,3 +48,4 @@
interfaces:
- key: enum.StorageUiKey.Key
type: StorageBoundUserInterface
+
diff --git a/Resources/Textures/Objects/Devices/forensic_scanner.rsi/forensicnew.png b/Resources/Textures/Objects/Devices/forensic_scanner.rsi/forensicnew.png
new file mode 100644
index 0000000000..3aa0cb605f
Binary files /dev/null and b/Resources/Textures/Objects/Devices/forensic_scanner.rsi/forensicnew.png differ
diff --git a/Resources/Textures/Objects/Devices/forensic_scanner.rsi/meta.json b/Resources/Textures/Objects/Devices/forensic_scanner.rsi/meta.json
new file mode 100644
index 0000000000..d50f980320
--- /dev/null
+++ b/Resources/Textures/Objects/Devices/forensic_scanner.rsi/meta.json
@@ -0,0 +1,21 @@
+{
+ "copyright" : "Taken from https://github.com/tgstation/tgstation",
+ "license" : "CC-BY-SA-3.0",
+ "size" : {
+ "x" : 32,
+ "y" : 32
+ },
+ "states" : [
+ {
+ "delays" : [
+ [
+ 0.8,
+ 0.2
+ ]
+ ],
+ "directions" : 1,
+ "name" : "forensicnew"
+ }
+ ],
+ "version" : 1
+}