diff --git a/Content.Client/Body/Systems/BloodStreamSystem.cs b/Content.Client/Body/Systems/BloodStreamSystem.cs deleted file mode 100644 index 85f4f6198e..0000000000 --- a/Content.Client/Body/Systems/BloodStreamSystem.cs +++ /dev/null @@ -1,5 +0,0 @@ -using Content.Shared.Body.Systems; - -namespace Content.Client.Body.Systems; - -public sealed class BloodstreamSystem : SharedBloodstreamSystem; diff --git a/Content.Client/Forensics/ForensicScannerBoundUserInterface.cs b/Content.Client/Forensics/ForensicScannerBoundUserInterface.cs index 2e94497131..df5cb54cd1 100644 --- a/Content.Client/Forensics/ForensicScannerBoundUserInterface.cs +++ b/Content.Client/Forensics/ForensicScannerBoundUserInterface.cs @@ -1,75 +1,45 @@ -using Robust.Client.GameObjects; -using Robust.Shared.Timing; using Content.Shared.Forensics; +using Content.Shared.Forensics.Components; +using JetBrains.Annotations; using Robust.Client.UserInterface; -namespace Content.Client.Forensics +namespace Content.Client.Forensics; + +public sealed partial class ForensicScannerBoundUserInterface(EntityUid owner, Enum uiKey) : BoundUserInterface(owner, uiKey) { - public sealed partial class ForensicScannerBoundUserInterface : BoundUserInterface + [ViewVariables] + private ForensicScannerMenu? _window; + + protected override void Open() { - [Dependency] private IGameTiming _gameTiming = default!; + base.Open(); + _window = this.CreateWindow(); + _window.Print.OnPressed += _ => Print(); + _window.Clear.OnPressed += _ => Clear(); - [ViewVariables] - private ForensicScannerMenu? _window; + Update(); + } - [ViewVariables] - private TimeSpan _printCooldown; + private void Print() + { + SendPredictedMessage(new ForensicScannerPrintMessage()); + } - public ForensicScannerBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey) - { - } + private void Clear() + { + SendPredictedMessage(new ForensicScannerClearMessage()); + } - protected override void Open() - { - base.Open(); - _window = this.CreateWindow(); - _window.Print.OnPressed += _ => Print(); - _window.Clear.OnPressed += _ => Clear(); - } + public override void Update() + { + base.Update(); - private void Print() - { - SendMessage(new ForensicScannerPrintMessage()); + if (_window == null) + return; - if (_window != null) - _window.UpdatePrinterState(true); + if (!EntMan.TryGetComponent(Owner, out ForensicScannerComponent? scanner)) + return; - // This UI does not require pinpoint accuracy as to when the Print - // button is available again, so spawning client-side timers is - // fine. The server will make sure the cooldown is honored. - Timer.Spawn(_printCooldown, () => - { - if (_window != null) - _window.UpdatePrinterState(false); - }); - } - - private void Clear() - { - SendMessage(new ForensicScannerClearMessage()); - } - - protected override void UpdateState(BoundUserInterfaceState state) - { - base.UpdateState(state); - - if (_window == null) - return; - - if (state is not ForensicScannerBoundUserInterfaceState cast) - return; - - _printCooldown = cast.PrintCooldown; - - // TODO: Fix this - if (cast.PrintReadyAt > _gameTiming.CurTime) - Timer.Spawn(cast.PrintReadyAt - _gameTiming.CurTime, () => - { - if (_window != null) - _window.UpdatePrinterState(false); - }); - - _window.UpdateState(cast); - } + _window.Update(scanner); } } diff --git a/Content.Client/Forensics/ForensicScannerMenu.xaml.cs b/Content.Client/Forensics/ForensicScannerMenu.xaml.cs index 8e1462bf21..f8cab6fa8a 100644 --- a/Content.Client/Forensics/ForensicScannerMenu.xaml.cs +++ b/Content.Client/Forensics/ForensicScannerMenu.xaml.cs @@ -3,74 +3,75 @@ using Robust.Client.AutoGenerated; using Robust.Client.UserInterface.CustomControls; using Robust.Client.UserInterface.XAML; using Robust.Shared.Timing; -using Content.Shared.Forensics; +using Content.Shared.Forensics.Components; -namespace Content.Client.Forensics +namespace Content.Client.Forensics; + +[GenerateTypedNameReferences] +public sealed partial class ForensicScannerMenu : DefaultWindow { - [GenerateTypedNameReferences] - public sealed partial class ForensicScannerMenu : DefaultWindow + [Dependency] private IGameTiming _gameTiming = default!; + + private TimeSpan? _printReady; + + public ForensicScannerMenu() { - [Dependency] private IGameTiming _gameTiming = default!; + RobustXamlLoader.Load(this); + IoCManager.InjectDependencies(this); + } - public ForensicScannerMenu() + protected override void FrameUpdate(FrameEventArgs args) + { + base.FrameUpdate(args); + + if (string.IsNullOrEmpty(NameLabel.Text)) + return; + + Print.Disabled = _printReady > _gameTiming.CurTime; + } + + public void Update(ForensicScannerComponent scanner) + { + if (string.IsNullOrEmpty(scanner.LastScannedName)) { - RobustXamlLoader.Load(this); - IoCManager.InjectDependencies(this); + Print.Disabled = true; + Clear.Disabled = true; + NameLabel.Text = string.Empty; + Diagnostics.Text = string.Empty; + return; } - public void UpdatePrinterState(bool disabled) + _printReady = scanner.PrintReadyAt; + Clear.Disabled = false; + + NameLabel.Text = scanner.LastScannedName; + + var text = new StringBuilder(); + + text.AppendLine(Loc.GetString("forensic-scanner-interface-fingerprints")); + foreach (var fingerprint in scanner.Fingerprints) { - Print.Disabled = disabled; + text.AppendLine(fingerprint); + } + text.AppendLine(); + text.AppendLine(Loc.GetString("forensic-scanner-interface-fibers")); + foreach (var fiber in scanner.Fibers) + { + text.AppendLine(fiber); + } + text.AppendLine(); + text.AppendLine(Loc.GetString("forensic-scanner-interface-dnas")); + foreach (var dna in scanner.DNAs) + { + text.AppendLine(dna); + } + text.AppendLine(); + text.AppendLine(Loc.GetString("forensic-scanner-interface-residues")); + foreach (var residue in scanner.Residues) + { + text.AppendLine(residue); } - public void UpdateState(ForensicScannerBoundUserInterfaceState msg) - { - if (string.IsNullOrEmpty(msg.LastScannedName)) - { - Print.Disabled = true; - Clear.Disabled = true; - NameLabel.Text = string.Empty; - Diagnostics.Text = string.Empty; - return; - } - - Print.Disabled = (msg.PrintReadyAt > _gameTiming.CurTime); - Clear.Disabled = false; - - NameLabel.Text = msg.LastScannedName; - - 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); - } - text.AppendLine(); - text.AppendLine(Loc.GetString("forensic-scanner-interface-dnas")); - foreach (var dna in msg.TouchDNAs) - { - text.AppendLine(dna); - } - foreach (var dna in msg.SolutionDNAs) - { - if (msg.TouchDNAs.Contains(dna)) - continue; - text.AppendLine(dna); - } - text.AppendLine(); - text.AppendLine(Loc.GetString("forensic-scanner-interface-residues")); - foreach (var residue in msg.Residues) - { - text.AppendLine(residue); - } - Diagnostics.Text = text.ToString(); - } + Diagnostics.Text = text.ToString(); } } diff --git a/Content.Client/Forensics/Systems/ForensicsSystem.cs b/Content.Client/Forensics/Systems/ForensicsSystem.cs deleted file mode 100644 index 048fff600e..0000000000 --- a/Content.Client/Forensics/Systems/ForensicsSystem.cs +++ /dev/null @@ -1,5 +0,0 @@ -using Content.Shared.Forensics.Systems; - -namespace Content.Client.Forensics.Systems; - -public sealed class ForensicsSystem : SharedForensicsSystem; diff --git a/Content.Server/Administration/Systems/AdminVerbSystem.Smites.cs b/Content.Server/Administration/Systems/AdminVerbSystem.Smites.cs index a114c5f112..c196fc061d 100644 --- a/Content.Server/Administration/Systems/AdminVerbSystem.Smites.cs +++ b/Content.Server/Administration/Systems/AdminVerbSystem.Smites.cs @@ -23,6 +23,7 @@ using Content.Shared.Atmos; using Content.Shared.Atmos.Components; using Content.Shared.Body; using Content.Shared.Body.Components; +using Content.Shared.Body.Systems; using Content.Shared.Clothing.Components; using Content.Shared.Clumsy; using Content.Shared.Cluwne; diff --git a/Content.Server/Body/Systems/BloodstreamSystem.cs b/Content.Server/Body/Systems/BloodstreamSystem.cs deleted file mode 100644 index 8318e41045..0000000000 --- a/Content.Server/Body/Systems/BloodstreamSystem.cs +++ /dev/null @@ -1,35 +0,0 @@ -using Content.Shared.Body.Components; -using Content.Shared.Body.Systems; -using Content.Shared.Chemistry.Reagent; -using Content.Shared.Forensics; - -namespace Content.Server.Body.Systems; - -public sealed class BloodstreamSystem : SharedBloodstreamSystem -{ - public override void Initialize() - { - base.Initialize(); - - SubscribeLocalEvent(OnDnaGenerated); - } - - // forensics is not predicted yet - private void OnDnaGenerated(Entity entity, ref GenerateDnaEvent args) - { - if (SolutionContainer.ResolveSolution(entity.Owner, entity.Comp.BloodSolutionName, ref entity.Comp.BloodSolution, out var bloodSolution)) - { - var data = NewEntityBloodData(entity); - entity.Comp.BloodReferenceSolution.SetReagentData(data); - - foreach (var reagent in bloodSolution.Contents) - { - List reagentData = reagent.Reagent.EnsureReagentData(); - reagentData.RemoveAll(x => x is DnaData); - reagentData.AddRange(data); - } - } - else - Log.Error("Unable to set bloodstream DNA, solution entity could not be resolved"); - } -} diff --git a/Content.Server/Chemistry/EntitySystems/SolutionInjectOnEventSystem.cs b/Content.Server/Chemistry/EntitySystems/SolutionInjectOnEventSystem.cs index 2722d2c634..a98c272eed 100644 --- a/Content.Server/Chemistry/EntitySystems/SolutionInjectOnEventSystem.cs +++ b/Content.Server/Chemistry/EntitySystems/SolutionInjectOnEventSystem.cs @@ -2,6 +2,7 @@ using Content.Server.Body.Systems; using Content.Server.Chemistry.Components; using Content.Shared.Chemistry.EntitySystems; using Content.Shared.Body.Components; +using Content.Shared.Body.Systems; using Content.Shared.Chemistry.Events; using Content.Shared.Inventory; using Content.Shared.Popups; diff --git a/Content.Server/Cloning/CloningSystem.Subscriptions.cs b/Content.Server/Cloning/CloningSystem.Subscriptions.cs index 559618306e..b385814114 100644 --- a/Content.Server/Cloning/CloningSystem.Subscriptions.cs +++ b/Content.Server/Cloning/CloningSystem.Subscriptions.cs @@ -1,10 +1,12 @@ -using Content.Server.Forensics; using Content.Shared.Body.Components; using Content.Shared.Body.Systems; +using Content.Server.Speech.EntitySystems; using Content.Shared.Cloning.Events; using Content.Shared.Clothing.Components; using Content.Shared.Clothing.EntitySystems; using Content.Shared.FixedPoint; +using Content.Shared.Forensics.Components; +using Content.Shared.Forensics.Systems; using Content.Shared.Inventory; using Content.Shared.Labels.Components; using Content.Shared.Labels.EntitySystems; @@ -34,13 +36,13 @@ public sealed partial class CloningSystem { [Dependency] private SharedStackSystem _stack = default!; [Dependency] private LabelSystem _label = default!; - [Dependency] private ForensicsSystem _forensics = default!; [Dependency] private PaperSystem _paper = default!; [Dependency] private VocalSystem _vocal = default!; [Dependency] private MovementSpeedModifierSystem _movementSpeedModifier = default!; [Dependency] private SharedChameleonClothingSystem _chameleonClothing = default!; [Dependency] private PullingSystem _pulling = default!; - [Dependency] private SharedBloodstreamSystem _bloodstream = default!; + [Dependency] private BloodstreamSystem _bloodstream = default!; + [Dependency] private SharedForensicsSystem _forensics = default!; public override void Initialize() { @@ -94,7 +96,7 @@ public sealed partial class CloningSystem private void OnCloneItemForensics(Entity ent, ref CloningItemEvent args) { // copy any forensics to the cloned item - _forensics.CopyForensicsFrom(ent.Comp, args.CloneUid); + _forensics.CopyForensicsFrom(ent.AsNullable(), args.CloneUid); } private void OnCloneItemStore(Entity ent, ref CloningItemEvent args) diff --git a/Content.Server/Content.Server.csproj b/Content.Server/Content.Server.csproj index 551301f9f7..320a65795c 100644 --- a/Content.Server/Content.Server.csproj +++ b/Content.Server/Content.Server.csproj @@ -24,6 +24,9 @@ PreserveNewest + + + diff --git a/Content.Server/Destructible/Thresholds/Behaviors/SpawnEntitiesBehavior.cs b/Content.Server/Destructible/Thresholds/Behaviors/SpawnEntitiesBehavior.cs index c7a15d6699..31c63b606c 100644 --- a/Content.Server/Destructible/Thresholds/Behaviors/SpawnEntitiesBehavior.cs +++ b/Content.Server/Destructible/Thresholds/Behaviors/SpawnEntitiesBehavior.cs @@ -1,7 +1,6 @@ using System.Numerics; -using Content.Server.Forensics; -using Content.Server.Stack; using Content.Shared.Destructible.Thresholds; +using Content.Shared.Forensics.Components; using Content.Shared.Prototypes; using Content.Shared.Stacks; using Robust.Server.GameObjects; diff --git a/Content.Server/Fluids/EntitySystems/SmokeSystem.cs b/Content.Server/Fluids/EntitySystems/SmokeSystem.cs index c23658ae38..4ed5abe55c 100644 --- a/Content.Server/Fluids/EntitySystems/SmokeSystem.cs +++ b/Content.Server/Fluids/EntitySystems/SmokeSystem.cs @@ -19,6 +19,7 @@ using Robust.Shared.Physics.Systems; using Robust.Shared.Random; using Robust.Shared.Timing; using System.Linq; +using Content.Shared.Body.Systems; using Content.Shared.EntityEffects.Effects.Solution; using TimedDespawnComponent = Robust.Shared.Spawners.TimedDespawnComponent; diff --git a/Content.Server/Forensics/Components/DnaSubstanceTraceComponent.cs b/Content.Server/Forensics/Components/DnaSubstanceTraceComponent.cs deleted file mode 100644 index 7b3cb978f2..0000000000 --- a/Content.Server/Forensics/Components/DnaSubstanceTraceComponent.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace Content.Server.Forensics; - -/// -/// This component stops the entity from leaving finger prints, -/// usually so fibres can be left instead. -/// -[RegisterComponent] -public sealed partial class DnaSubstanceTraceComponent : Component -{ } diff --git a/Content.Server/Forensics/Components/FiberComponent.cs b/Content.Server/Forensics/Components/FiberComponent.cs deleted file mode 100644 index 2086c95870..0000000000 --- a/Content.Server/Forensics/Components/FiberComponent.cs +++ /dev/null @@ -1,16 +0,0 @@ -namespace Content.Server.Forensics -{ - /// - /// This controls fibers left by gloves on items, - /// which the forensics system uses. - /// - [RegisterComponent] - public sealed partial class FiberComponent : Component - { - [DataField] - public LocId FiberMaterial = "fibers-synthetic"; - - [DataField] - public string? FiberColor; - } -} diff --git a/Content.Server/Forensics/Components/ForensicPadComponent.cs b/Content.Server/Forensics/Components/ForensicPadComponent.cs deleted file mode 100644 index d707886481..0000000000 --- a/Content.Server/Forensics/Components/ForensicPadComponent.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace Content.Server.Forensics -{ - /// - /// Used to take a sample of someone's fingerprints. - /// - [RegisterComponent] - public sealed partial class ForensicPadComponent : Component - { - [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 deleted file mode 100644 index 033ea913c3..0000000000 --- a/Content.Server/Forensics/Components/ForensicScannerComponent.cs +++ /dev/null @@ -1,97 +0,0 @@ -using System.Threading; -using Robust.Shared.Audio; -using Robust.Shared.Prototypes; -using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; - -namespace Content.Server.Forensics -{ - [RegisterComponent] - public sealed partial class ForensicScannerComponent : Component - { - public CancellationTokenSource? CancelToken; - - /// - /// A list of fingerprint GUIDs that the forensic scanner found from the on an entity. - /// - [ViewVariables(VVAccess.ReadOnly), DataField("fingerprints")] - public List Fingerprints = new(); - - /// - /// A list of glove fibers that the forensic scanner found from the on an entity. - /// - [ViewVariables(VVAccess.ReadOnly), DataField("fibers")] - public List Fibers = new(); - - /// - /// DNA that the forensic scanner found from the on an entity. - /// - [ViewVariables(VVAccess.ReadOnly), DataField("dnas")] - public List TouchDNAs = new(); - - /// - /// DNA that the forensic scanner found from the solution containers in an entity. - /// - [ViewVariables(VVAccess.ReadOnly), DataField] - public List SolutionDNAs = new(); - - /// - /// Residue that the forensic scanner found from the on an entity. - /// - [ViewVariables(VVAccess.ReadOnly), DataField("residues")] - public List Residues = new(); - - /// - /// What is the name of the entity that was scanned last? - /// - /// - /// This will be used for the title of the printout and displayed to players. - /// - [ViewVariables(VVAccess.ReadOnly)] - public string LastScannedName = string.Empty; - - /// - /// When will the scanner be ready to print again? - /// - [ViewVariables(VVAccess.ReadOnly)] - public TimeSpan PrintReadyAt = TimeSpan.Zero; - - /// - /// The time (in seconds) that it takes to scan an entity. - /// - [DataField("scanDelay")] - public float ScanDelay = 3.0f; - - /// - /// How often can the scanner print out reports? - /// - [DataField("printCooldown")] - public TimeSpan PrintCooldown = TimeSpan.FromSeconds(5); - - /// - /// The sound that's played when there's a match between a scan and an - /// inserted forensic pad. - /// - [DataField("soundMatch")] - public SoundSpecifier SoundMatch = new SoundPathSpecifier("/Audio/Machines/Nuke/angry_beep.ogg"); - - /// - /// The sound that's played when there's no match between a scan and an - /// inserted forensic pad. - /// - [DataField("soundNoMatch")] - public SoundSpecifier SoundNoMatch = new SoundPathSpecifier("/Audio/Machines/airlock_deny.ogg"); - - /// - /// 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"); - - /// - /// What the machine will print - /// - [DataField("machineOutput", customTypeSerializer: typeof(PrototypeIdSerializer))] - public string MachineOutput = "ForensicReportPaper"; - - } -} diff --git a/Content.Server/Forensics/Components/ForensicsComponent.cs b/Content.Server/Forensics/Components/ForensicsComponent.cs deleted file mode 100644 index e4579c5f3a..0000000000 --- a/Content.Server/Forensics/Components/ForensicsComponent.cs +++ /dev/null @@ -1,31 +0,0 @@ -namespace Content.Server.Forensics -{ - [RegisterComponent] - public sealed partial class ForensicsComponent : Component - { - [DataField("fingerprints")] - public HashSet Fingerprints = new(); - - [DataField("fibers")] - public HashSet Fibers = new(); - - [DataField("dnas")] - public HashSet DNAs = new(); - - [DataField("residues")] - public HashSet Residues = new(); - - /// - /// How close you must be to wipe the prints/blood/etc. off of this entity - /// - [DataField("cleanDistance")] - public float CleanDistance = 1.5f; - - /// - /// Can the DNA be cleaned off of this entity? - /// e.g. you can wipe the DNA off of a knife, but not a cigarette - /// - [DataField("canDnaBeCleaned")] - public bool CanDnaBeCleaned = true; - } -} diff --git a/Content.Server/Forensics/Components/ResidueComponent.cs b/Content.Server/Forensics/Components/ResidueComponent.cs deleted file mode 100644 index 10895e7c2b..0000000000 --- a/Content.Server/Forensics/Components/ResidueComponent.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace Content.Server.Forensics; - -/// -/// This controls residues left on items -/// which the forensics system uses. -/// -[RegisterComponent] -public sealed partial class ResidueComponent : Component -{ - [DataField] - public LocId ResidueAdjective = "residue-unknown"; - - [DataField] - public string? ResidueColor; -} diff --git a/Content.Server/Forensics/Systems/FingerprintMaskSystem.cs b/Content.Server/Forensics/Systems/FingerprintMaskSystem.cs deleted file mode 100644 index 05008d1662..0000000000 --- a/Content.Server/Forensics/Systems/FingerprintMaskSystem.cs +++ /dev/null @@ -1,24 +0,0 @@ -using Content.Shared.Forensics; -using Content.Shared.Forensics.Components; -using Content.Shared.Inventory; - -namespace Content.Server.Forensics; - -public sealed class FingerprintMaskSystem : EntitySystem -{ - public override void Initialize() - { - base.Initialize(); - - SubscribeLocalEvent>(OnTryAccessFingerprint); - } - - private void OnTryAccessFingerprint(EntityUid uid, FingerprintMaskComponent comp, ref InventoryRelayedEvent args) - { - if (args.Args.Cancelled) - return; - - args.Args.Blocker = uid; - args.Args.Cancel(); - } -} diff --git a/Content.Server/Forensics/Systems/ForensicPadSystem.cs b/Content.Server/Forensics/Systems/ForensicPadSystem.cs deleted file mode 100644 index 72fd1a09e1..0000000000 --- a/Content.Server/Forensics/Systems/ForensicPadSystem.cs +++ /dev/null @@ -1,119 +0,0 @@ -using Content.Server.Popups; -using Content.Shared.DoAfter; -using Content.Shared.Examine; -using Content.Shared.Forensics; -using Content.Shared.Forensics.Components; -using Content.Shared.IdentityManagement; -using Content.Shared.Interaction; -using Content.Shared.Inventory; -using Content.Shared.Labels.EntitySystems; - -namespace Content.Server.Forensics -{ - /// - /// Used to transfer fingerprints from entities to forensic pads. - /// - public sealed partial class ForensicPadSystem : EntitySystem - { - [Dependency] private SharedDoAfterSystem _doAfterSystem = default!; - [Dependency] private PopupSystem _popupSystem = default!; - [Dependency] private ForensicsSystem _forensics = default!; - [Dependency] private LabelSystem _label = default!; - - public override void Initialize() - { - base.Initialize(); - SubscribeLocalEvent(OnExamined); - SubscribeLocalEvent(OnAfterInteract); - SubscribeLocalEvent(OnDoAfter); - } - - 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 (!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, args.User); - return; - } - - if (!_forensics.CanAccessFingerprint(args.Target.Value, out var blocker)) - { - - if (blocker is { } item) - _popupSystem.PopupEntity(Loc.GetString("forensic-pad-no-access-due", ("entity", Identity.Entity(item, EntityManager))), args.Target.Value, args.User); - else - _popupSystem.PopupEntity(Loc.GetString("forensic-pad-no-access"), args.Target.Value, 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", Identity.Entity(args.Target.Value, EntityManager))), args.Target.Value, args.User); - _popupSystem.PopupEntity(Loc.GetString("forensic-pad-start-scan-target", ("user", Identity.Entity(args.User, EntityManager))), args.Target.Value, args.Target.Value); - } - StartScan(uid, args.User, args.Target.Value, component, fingerprint.Fingerprint); - return; - } - - if (TryComp(args.Target, out var fiber)) - StartScan(uid, 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 used, EntityUid user, EntityUid target, ForensicPadComponent pad, string sample) - { - var ev = new ForensicPadDoAfterEvent(sample); - - var doAfterEventArgs = new DoAfterArgs(EntityManager, user, pad.ScanDelay, ev, used, target: target, used: used) - { - NeedHand = true, - BreakOnMove = true, - }; - - _doAfterSystem.TryStartDoAfter(doAfterEventArgs); - } - - private void OnDoAfter(EntityUid uid, ForensicPadComponent padComponent, ForensicPadDoAfterEvent args) - { - if (args.Handled || args.Cancelled) - { - return; - } - - if (args.Args.Target != null) - { - string label = Identity.Name(args.Args.Target.Value, EntityManager); - _label.Label(uid, label); - } - - padComponent.Sample = args.Sample; - padComponent.Used = true; - - args.Handled = true; - } - } -} diff --git a/Content.Server/Forensics/Systems/ForensicScannerSystem.cs b/Content.Server/Forensics/Systems/ForensicScannerSystem.cs deleted file mode 100644 index 4bdd19de5a..0000000000 --- a/Content.Server/Forensics/Systems/ForensicScannerSystem.cs +++ /dev/null @@ -1,262 +0,0 @@ -using System.Linq; -using System.Text; -using Content.Server.Popups; -using Content.Shared.UserInterface; -using Content.Shared.DoAfter; -using Content.Shared.Fluids.Components; -using Content.Shared.Forensics; -using Content.Shared.Hands.EntitySystems; -using Content.Shared.Interaction; -using Content.Shared.Paper; -using Content.Shared.Verbs; -using Content.Shared.Tag; -using Robust.Shared.Audio.Systems; -using Robust.Server.GameObjects; -using Robust.Shared.Audio; -using Robust.Shared.Timing; -using Robust.Shared.Prototypes; -// todo: remove this stinky LINQy - -namespace Content.Server.Forensics -{ - public sealed partial class ForensicScannerSystem : EntitySystem - { - [Dependency] private IGameTiming _gameTiming = default!; - [Dependency] private SharedDoAfterSystem _doAfterSystem = default!; - [Dependency] private UserInterfaceSystem _uiSystem = default!; - [Dependency] private PopupSystem _popupSystem = default!; - [Dependency] private PaperSystem _paperSystem = default!; - [Dependency] private SharedHandsSystem _handsSystem = default!; - [Dependency] private SharedAudioSystem _audioSystem = default!; - [Dependency] private MetaDataSystem _metaData = default!; - [Dependency] private ForensicsSystem _forensicsSystem = default!; - [Dependency] private TagSystem _tag = default!; - - private static readonly ProtoId DNASolutionScannableTag = "DNASolutionScannable"; - - public override void Initialize() - { - base.Initialize(); - - SubscribeLocalEvent(OnAfterInteract); - SubscribeLocalEvent(OnAfterInteractUsing); - SubscribeLocalEvent(OnBeforeActivatableUIOpen); - SubscribeLocalEvent>(OnUtilityVerb); - SubscribeLocalEvent(OnPrint); - SubscribeLocalEvent(OnClear); - SubscribeLocalEvent(OnDoAfter); - } - - private void UpdateUserInterface(EntityUid uid, ForensicScannerComponent component) - { - var state = new ForensicScannerBoundUserInterfaceState( - component.Fingerprints, - component.Fibers, - component.TouchDNAs, - component.SolutionDNAs, - component.Residues, - component.LastScannedName, - component.PrintCooldown, - component.PrintReadyAt); - - _uiSystem.SetUiState(uid, ForensicScannerUiKey.Key, state); - } - - private void OnDoAfter(EntityUid uid, ForensicScannerComponent component, DoAfterEvent args) - { - if (args.Handled || args.Cancelled) - return; - - if (!TryComp(uid, out ForensicScannerComponent? scanner)) - return; - - if (args.Args.Target != null) - { - if (!TryComp(args.Args.Target, out var forensics)) - { - scanner.Fingerprints = new(); - scanner.Fibers = new(); - scanner.TouchDNAs = new(); - scanner.Residues = new(); - } - else - { - scanner.Fingerprints = forensics.Fingerprints.ToList(); - scanner.Fibers = forensics.Fibers.ToList(); - scanner.TouchDNAs = forensics.DNAs.ToList(); - scanner.Residues = forensics.Residues.ToList(); - } - - if (_tag.HasTag(args.Args.Target.Value, DNASolutionScannableTag)) - { - scanner.SolutionDNAs = _forensicsSystem.GetSolutionsDNA(args.Args.Target.Value); - } else - { - scanner.SolutionDNAs = new(); - } - - scanner.LastScannedName = MetaData(args.Args.Target.Value).EntityName; - } - - OpenUserInterface(args.Args.User, (uid, scanner)); - } - - /// - /// Hosts logic common between OnUtilityVerb and OnAfterInteract. - /// - private void StartScan(EntityUid uid, ForensicScannerComponent component, EntityUid user, EntityUid target) - { - _doAfterSystem.TryStartDoAfter(new DoAfterArgs(EntityManager, user, component.ScanDelay, new ForensicScannerDoAfterEvent(), uid, target: target, used: uid) - { - BreakOnMove = true, - NeedHand = true - }); - } - - private void OnUtilityVerb(EntityUid uid, ForensicScannerComponent component, GetVerbsEvent args) - { - if (!args.CanInteract || !args.CanAccess || component.CancelToken != null) - return; - - var verb = new UtilityVerb() - { - Act = () => StartScan(uid, component, args.User, args.Target), - IconEntity = GetNetEntity(uid), - Text = Loc.GetString("forensic-scanner-verb-text"), - Message = Loc.GetString("forensic-scanner-verb-message"), - // This is important because if its true using the scanner will count as touching the object. - DoContactInteraction = false - }; - - args.Verbs.Add(verb); - } - - private void OnAfterInteract(EntityUid uid, ForensicScannerComponent component, AfterInteractEvent args) - { - if (component.CancelToken != null || args.Target == null || !args.CanReach) - return; - - StartScan(uid, component, args.User, args.Target.Value); - } - - 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) - { - _audioSystem.PlayPvs(component.SoundMatch, uid); - _popupSystem.PopupEntity(Loc.GetString("forensic-scanner-match-fiber"), uid, args.User); - return; - } - } - - foreach (var fingerprint in component.Fingerprints) - { - if (fingerprint == pad.Sample) - { - _audioSystem.PlayPvs(component.SoundMatch, uid); - _popupSystem.PopupEntity(Loc.GetString("forensic-scanner-match-fingerprint"), uid, args.User); - return; - } - } - - _audioSystem.PlayPvs(component.SoundNoMatch, uid); - _popupSystem.PopupEntity(Loc.GetString("forensic-scanner-match-none"), uid, args.User); - } - - private void OnBeforeActivatableUIOpen(EntityUid uid, ForensicScannerComponent component, BeforeActivatableUIOpenEvent args) - { - UpdateUserInterface(uid, component); - } - - private void OpenUserInterface(EntityUid user, Entity scanner) - { - UpdateUserInterface(scanner, scanner.Comp); - - _uiSystem.OpenUi(scanner.Owner, ForensicScannerUiKey.Key, user); - } - - private void OnPrint(EntityUid uid, ForensicScannerComponent component, ForensicScannerPrintMessage args) - { - var user = args.Actor; - - if (_gameTiming.CurTime < component.PrintReadyAt) - { - // This shouldn't occur due to the UI guarding against it, but - // if it does, tell the user why nothing happened. - _popupSystem.PopupEntity(Loc.GetString("forensic-scanner-printer-not-ready"), uid, user); - return; - } - - // Spawn a piece of paper. - var printed = Spawn(component.MachineOutput, Transform(uid).Coordinates); - _handsSystem.PickupOrDrop(args.Actor, printed, checkActionBlocker: false); - - if (!TryComp(printed, out var paperComp)) - { - Log.Error("Printed paper did not have PaperComponent."); - return; - } - - _metaData.SetEntityName(printed, Loc.GetString("forensic-scanner-report-title", ("entity", component.LastScannedName))); - - 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); - } - text.AppendLine(); - text.AppendLine(Loc.GetString("forensic-scanner-interface-dnas")); - foreach (var dna in component.TouchDNAs) - { - text.AppendLine(dna); - } - foreach (var dna in component.SolutionDNAs) - { - Log.Debug(dna); - if (component.TouchDNAs.Contains(dna)) - continue; - text.AppendLine(dna); - } - text.AppendLine(); - text.AppendLine(Loc.GetString("forensic-scanner-interface-residues")); - foreach (var residue in component.Residues) - { - text.AppendLine(residue); - } - - _paperSystem.SetContent((printed, paperComp), text.ToString()); - var audioParams = component.SoundPrint?.Params ?? AudioParams.Default; - audioParams = audioParams.WithVariation(0.25f).AddVolume(3f).WithRolloffFactor(2.8f).WithMaxDistance(4.5f); - _audioSystem.PlayPvs(component.SoundPrint, uid, audioParams); - - component.PrintReadyAt = _gameTiming.CurTime + component.PrintCooldown; - } - - private void OnClear(EntityUid uid, ForensicScannerComponent component, ForensicScannerClearMessage args) - { - component.Fingerprints = new(); - component.Fibers = new(); - component.TouchDNAs = new(); - component.SolutionDNAs = new(); - component.LastScannedName = string.Empty; - - UpdateUserInterface(uid, component); - } - } -} diff --git a/Content.Server/Forensics/Systems/ForensicsSystem.cs b/Content.Server/Forensics/Systems/ForensicsSystem.cs deleted file mode 100644 index bd8410ab90..0000000000 --- a/Content.Server/Forensics/Systems/ForensicsSystem.cs +++ /dev/null @@ -1,372 +0,0 @@ -using Content.Server.Body.Systems; -using Content.Server.DoAfter; -using Content.Server.Fluids.EntitySystems; -using Content.Server.Forensics.Components; -using Content.Server.Popups; -using Content.Shared.Body.Events; -using Content.Shared.Chemistry.EntitySystems; -using Content.Shared.Popups; -using Content.Shared.Chemistry.Components; -using Content.Shared.Chemistry.Reagent; -using Content.Shared.Chemistry.Components.SolutionManager; -using Content.Shared.DoAfter; -using Content.Shared.Forensics; -using Content.Shared.Forensics.Components; -using Content.Shared.Forensics.Systems; -using Content.Shared.Gibbing; -using Content.Shared.IdentityManagement; -using Content.Shared.Interaction; -using Content.Shared.Interaction.Events; -using Content.Shared.Inventory; -using Content.Shared.Weapons.Melee.Events; -using Robust.Shared.Random; -using Content.Shared.Verbs; -using Robust.Shared.Utility; -using Content.Shared.Hands.Components; - -namespace Content.Server.Forensics -{ - public sealed partial class ForensicsSystem : SharedForensicsSystem - { - [Dependency] private IRobustRandom _random = default!; - [Dependency] private InventorySystem _inventory = default!; - [Dependency] private DoAfterSystem _doAfterSystem = default!; - [Dependency] private PopupSystem _popupSystem = default!; - [Dependency] private SharedSolutionContainerSystem _solutionContainerSystem = default!; - - public override void Initialize() - { - SubscribeLocalEvent(OnInteract); - SubscribeLocalEvent(OnFingerprintInit, after: new[] { typeof(BloodstreamSystem) }); - // The solution entities are spawned on MapInit as well, so we have to wait for that to be able to set the DNA in the bloodstream correctly without ResolveSolution failing - SubscribeLocalEvent(OnDNAInit, after: new[] { typeof(BloodstreamSystem) }); - - SubscribeLocalEvent(OnBeingGibbed); - SubscribeLocalEvent(OnMeleeHit); - SubscribeLocalEvent(OnRehydrated); - SubscribeLocalEvent(OnAfterInteract, after: new[] { typeof(AbsorbentSystem) }); - SubscribeLocalEvent(OnCleanForensicsDoAfter); - SubscribeLocalEvent(OnTransferDnaEvent); - SubscribeLocalEvent(OnSolutionChanged); - SubscribeLocalEvent>(OnUtilityVerb); - } - - private void OnSolutionChanged(Entity ent, ref SolutionChangedEvent ev) - { - var soln = GetSolutionsDNA(ev.Solution); - if (soln.Count > 0) - { - var comp = EnsureComp(ent.Owner); - foreach (string dna in soln) - { - comp.DNAs.Add(dna); - } - } - } - - private void OnInteract(EntityUid uid, HandsComponent component, ContactInteractionEvent args) - { - ApplyEvidence(uid, args.Other); - } - - private void OnFingerprintInit(Entity ent, ref MapInitEvent args) - { - if (ent.Comp.Fingerprint == null) - RandomizeFingerprint((ent.Owner, ent.Comp)); - } - - private void OnDNAInit(Entity ent, ref MapInitEvent args) - { - if (ent.Comp.DNA == null) - RandomizeDNA((ent.Owner, ent.Comp)); - else - { - // If set manually (for example by cloning) we also need to inform the bloodstream of the correct DNA string so it can be updated - var ev = new GenerateDnaEvent { Owner = ent.Owner, DNA = ent.Comp.DNA }; - RaiseLocalEvent(ent.Owner, ref ev); - } - } - - private void OnBeingGibbed(Entity ent, ref GibbedBeforeDeletionEvent args) - { - string dna = Loc.GetString("forensics-dna-unknown"); - - if (TryComp(ent, out DnaComponent? dnaComp) && dnaComp.DNA != null) - dna = dnaComp.DNA; - - foreach (var part in args.Giblets) - { - var partComp = EnsureComp(part); - partComp.DNAs.Add(dna); - partComp.CanDnaBeCleaned = false; - } - } - - private void OnMeleeHit(EntityUid uid, ForensicsComponent component, MeleeHitEvent args) - { - if ((args.BaseDamage.DamageDict.TryGetValue("Blunt", out var bluntDamage) && bluntDamage.Value > 0) || - (args.BaseDamage.DamageDict.TryGetValue("Slash", out var slashDamage) && slashDamage.Value > 0) || - (args.BaseDamage.DamageDict.TryGetValue("Piercing", out var pierceDamage) && pierceDamage.Value > 0)) - { - foreach (EntityUid hitEntity in args.HitEntities) - { - if (TryComp(hitEntity, out var hitEntityComp) && hitEntityComp.DNA != null) - component.DNAs.Add(hitEntityComp.DNA); - } - } - } - - private void OnRehydrated(Entity ent, ref GotRehydratedEvent args) - { - CopyForensicsFrom(ent.Comp, args.Target); - } - - /// - /// Copy forensic information from a source entity to a destination. - /// Existing forensic information on the target is still kept. - /// - public void CopyForensicsFrom(ForensicsComponent src, EntityUid target) - { - var dest = EnsureComp(target); - foreach (var dna in src.DNAs) - { - dest.DNAs.Add(dna); - } - - foreach (var fiber in src.Fibers) - { - dest.Fibers.Add(fiber); - } - - foreach (var print in src.Fingerprints) - { - dest.Fingerprints.Add(print); - } - - foreach (var residue in src.Residues) - { - dest.Residues.Add(residue); - } - } - - public List GetSolutionsDNA(EntityUid uid) - { - List list = new(); - foreach (var (_, soln) in _solutionContainerSystem.EnumerateSolutions(uid)) - { - list.AddRange(GetSolutionsDNA(soln.Comp.Solution)); - } - return list; - } - - public List GetSolutionsDNA(Solution soln) - { - List list = new(); - foreach (var reagent in soln.Contents) - { - foreach (var data in reagent.Reagent.EnsureReagentData()) - { - if (data is DnaData) - { - list.Add(((DnaData) data).DNA); - } - } - } - return list; - } - private void OnAfterInteract(Entity cleanForensicsEntity, ref AfterInteractEvent args) - { - if (args.Handled || !args.CanReach || args.Target == null) - return; - - args.Handled = TryStartCleaning(cleanForensicsEntity, args.User, args.Target.Value); - } - - private void OnUtilityVerb(Entity entity, ref GetVerbsEvent args) - { - if (!args.CanInteract || !args.CanAccess) - return; - - // These need to be set outside for the anonymous method! - var user = args.User; - var target = args.Target; - - var verb = new UtilityVerb() - { - Act = () => TryStartCleaning(entity, user, target), - Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/VerbIcons/bubbles.svg.192dpi.png")), - Text = Loc.GetString("forensics-verb-text"), - Message = Loc.GetString("forensics-verb-message"), - // This is important because if its true using the cleaning device will count as touching the object. - DoContactInteraction = false - }; - - args.Verbs.Add(verb); - } - - /// - /// Attempts to clean the given item with the given CleansForensics entity. - /// - /// The entity that is being used to clean the target. - /// The user that is using the cleanForensicsEntity. - /// The target of the forensics clean. - /// True if the target can be cleaned and has some sort of DNA or fingerprints / fibers and false otherwise. - public bool TryStartCleaning(Entity cleanForensicsEntity, EntityUid user, EntityUid target) - { - if (!TryComp(target, out var forensicsComp)) - { - _popupSystem.PopupEntity(Loc.GetString("forensics-cleaning-cannot-clean", ("target", Identity.Entity(target, EntityManager))), user, user, PopupType.MediumCaution); - return false; - } - - var totalPrintsAndFibers = forensicsComp.Fingerprints.Count + forensicsComp.Fibers.Count; - var hasRemovableDNA = forensicsComp.DNAs.Count > 0 && forensicsComp.CanDnaBeCleaned; - - if (hasRemovableDNA || totalPrintsAndFibers > 0) - { - var cleanDelay = cleanForensicsEntity.Comp.CleanDelay; - var doAfterArgs = new DoAfterArgs(EntityManager, user, cleanDelay, new CleanForensicsDoAfterEvent(), cleanForensicsEntity, target: target, used: cleanForensicsEntity) - { - NeedHand = true, - BreakOnDamage = true, - BreakOnMove = true, - MovementThreshold = 0.01f, - DistanceThreshold = forensicsComp.CleanDistance, - }; - - _doAfterSystem.TryStartDoAfter(doAfterArgs); - - _popupSystem.PopupEntity(Loc.GetString("forensics-cleaning", ("target", Identity.Entity(target, EntityManager))), user, user); - - return true; - } - else - { - _popupSystem.PopupEntity(Loc.GetString("forensics-cleaning-cannot-clean", ("target", Identity.Entity(target, EntityManager))), user, user, PopupType.MediumCaution); - return false; - } - - } - - private void OnCleanForensicsDoAfter(EntityUid uid, ForensicsComponent component, CleanForensicsDoAfterEvent args) - { - if (args.Handled || args.Cancelled || args.Args.Target == null) - return; - - if (!TryComp(args.Target, out var targetComp)) - return; - - targetComp.Fibers = new(); - targetComp.Fingerprints = new(); - - if (targetComp.CanDnaBeCleaned) - targetComp.DNAs = new(); - - // leave behind evidence it was cleaned - if (TryComp(args.Used, out var fiber)) - targetComp.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 (TryComp(args.Used, out var residue)) - targetComp.Residues.Add(string.IsNullOrEmpty(residue.ResidueColor) ? Loc.GetString("forensic-residue", ("adjective", residue.ResidueAdjective)) : Loc.GetString("forensic-residue-colored", ("color", residue.ResidueColor), ("adjective", residue.ResidueAdjective))); - } - - public string GenerateFingerprint() - { - var fingerprint = new byte[16]; - _random.NextBytes(fingerprint); - return Convert.ToHexString(fingerprint); - } - - public string GenerateDNA() - { - var letters = new[] { "A", "C", "G", "T" }; - var DNA = string.Empty; - - for (var i = 0; i < 16; i++) - { - DNA += letters[_random.Next(letters.Length)]; - } - - return DNA; - } - - private void ApplyEvidence(EntityUid user, EntityUid target) - { - if (HasComp(target)) - return; - - 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 (TryComp(user, out var fingerprint) && CanAccessFingerprint(user, out _)) - component.Fingerprints.Add(fingerprint.Fingerprint ?? ""); - } - - // TODO: Delete this. A lot of systems are manually raising this method event instead of calling the identical method. - // According to our code conventions we should not use method events. - private void OnTransferDnaEvent(EntityUid uid, DnaComponent component, ref TransferDnaEvent args) - { - if (component.DNA == null) - return; - - var recipientComp = EnsureComp(args.Recipient); - recipientComp.DNAs.Add(component.DNA); - recipientComp.CanDnaBeCleaned = args.CanDnaBeCleaned; - } - - #region Public API - public override void RandomizeDNA(Entity ent) - { - if (!Resolve(ent, ref ent.Comp, false)) - return; - - ent.Comp.DNA = GenerateDNA(); - Dirty(ent); - - var ev = new GenerateDnaEvent { Owner = ent.Owner, DNA = ent.Comp.DNA }; - RaiseLocalEvent(ent.Owner, ref ev); - } - - public override void RandomizeFingerprint(Entity ent) - { - if (!Resolve(ent, ref ent.Comp, false)) - return; - - ent.Comp.Fingerprint = GenerateFingerprint(); - Dirty(ent); - } - - public override void TransferDna(EntityUid recipient, EntityUid donor, bool canDnaBeCleaned = true) - { - if (TryComp(donor, out var donorComp) && donorComp.DNA != null) - { - EnsureComp(recipient, out var recipientComp); - recipientComp.DNAs.Add(donorComp.DNA); - recipientComp.CanDnaBeCleaned = canDnaBeCleaned; - } - } - - /// - /// Checks if there's a way to access the fingerprint of the target entity. - /// - /// The entity with the fingerprint - /// The entity that blocked accessing the fingerprint - public bool CanAccessFingerprint(EntityUid target, out EntityUid? blocker) - { - var ev = new TryAccessFingerprintEvent(); - - RaiseLocalEvent(target, ev); - if (!ev.Cancelled && TryComp(target, out var inv)) - _inventory.RelayEvent((target, inv), ev); - - blocker = ev.Blocker; - return !ev.Cancelled; - } - - #endregion - } -} diff --git a/Content.Server/Medical/HealthAnalyzerSystem.cs b/Content.Server/Medical/HealthAnalyzerSystem.cs index 869cfc69d5..69147425b9 100644 --- a/Content.Server/Medical/HealthAnalyzerSystem.cs +++ b/Content.Server/Medical/HealthAnalyzerSystem.cs @@ -19,6 +19,7 @@ using Robust.Shared.Audio.Systems; using Robust.Shared.Containers; using Robust.Shared.Timing; using Content.Server.Body.Systems; +using Content.Shared.Body.Systems; namespace Content.Server.Medical; diff --git a/Content.Server/Nutrition/EntitySystems/SmokingSystem.cs b/Content.Server/Nutrition/EntitySystems/SmokingSystem.cs index 0a63dbe714..ca81e9e222 100644 --- a/Content.Server/Nutrition/EntitySystems/SmokingSystem.cs +++ b/Content.Server/Nutrition/EntitySystems/SmokingSystem.cs @@ -1,12 +1,15 @@ +using Content.Shared.Atmos; using Content.Server.Atmos.EntitySystems; +using Robust.Shared.Audio.Systems; using Content.Server.Body.Systems; -using Content.Shared.Chemistry.EntitySystems; -using Content.Server.Forensics; using Content.Shared.Body.Components; +using Content.Shared.Body.Systems; using Content.Shared.Chemistry; +using Content.Shared.Chemistry.EntitySystems; using Content.Shared.Clothing.Components; using Content.Shared.Clothing.EntitySystems; using Content.Shared.FixedPoint; +using Content.Shared.Forensics.Systems; using Content.Shared.Inventory; using Content.Shared.Inventory.Events; using Content.Shared.Item; @@ -14,9 +17,7 @@ using Content.Shared.Nutrition.Components; using Content.Shared.Smoking; using Content.Shared.Temperature; using Robust.Server.GameObjects; -using Robust.Shared.Audio.Systems; using Robust.Shared.Containers; -using Content.Shared.Atmos; namespace Content.Server.Nutrition.EntitySystems { @@ -33,7 +34,7 @@ namespace Content.Server.Nutrition.EntitySystems [Dependency] private SharedItemSystem _items = default!; [Dependency] private SharedContainerSystem _container = default!; [Dependency] private SharedAppearanceSystem _appearance = default!; - [Dependency] private ForensicsSystem _forensics = default!; + [Dependency] private SharedForensicsSystem _forensics = default!; private const float UpdateTimer = 3f; diff --git a/Content.Server/Zombies/ZombieSystem.cs b/Content.Server/Zombies/ZombieSystem.cs index 1d1be3c0ba..acfd43bcd4 100644 --- a/Content.Server/Zombies/ZombieSystem.cs +++ b/Content.Server/Zombies/ZombieSystem.cs @@ -8,6 +8,7 @@ using Content.Shared.Speech.EntitySystems; using Content.Shared.Anomaly.Components; using Content.Shared.Armor; using Content.Shared.Bed.Sleep; +using Content.Shared.Body.Systems; using Content.Shared.Cloning.Events; using Content.Shared.Chat; using Content.Shared.Damage.Systems; diff --git a/Content.Shared/Body/Components/BloodstreamComponent.cs b/Content.Shared/Body/Components/BloodstreamComponent.cs index 73f042f234..dc9991af01 100644 --- a/Content.Shared/Body/Components/BloodstreamComponent.cs +++ b/Content.Shared/Body/Components/BloodstreamComponent.cs @@ -17,7 +17,7 @@ namespace Content.Shared.Body.Components; /// [RegisterComponent, NetworkedComponent,] [AutoGenerateComponentState(fieldDeltas: true), AutoGenerateComponentPause] -[Access(typeof(SharedBloodstreamSystem))] +[Access(typeof(BloodstreamSystem))] public sealed partial class BloodstreamComponent : Component { public const string DefaultBloodSolutionName = "bloodstream"; @@ -157,7 +157,7 @@ public sealed partial class BloodstreamComponent : Component /// Caches the blood data of an entity. /// This is modified by DNA on init so it's not savable. /// - [ViewVariables(VVAccess.ReadOnly)] + [ViewVariables(VVAccess.ReadOnly), AutoNetworkedField] public List? BloodData; /// diff --git a/Content.Shared/Body/Systems/SharedBloodstreamSystem.cs b/Content.Shared/Body/Systems/BloodstreamSystem.cs similarity index 87% rename from Content.Shared/Body/Systems/SharedBloodstreamSystem.cs rename to Content.Shared/Body/Systems/BloodstreamSystem.cs index 2ea349a622..532b3928cb 100644 --- a/Content.Shared/Body/Systems/SharedBloodstreamSystem.cs +++ b/Content.Shared/Body/Systems/BloodstreamSystem.cs @@ -10,6 +10,7 @@ using Content.Shared.Damage.Systems; using Content.Shared.EntityEffects.Effects.Solution; using Content.Shared.FixedPoint; using Content.Shared.Fluids; +using Content.Shared.Forensics; using Content.Shared.Forensics.Components; using Content.Shared.Gibbing; using Content.Shared.HealthExaminable; @@ -22,16 +23,15 @@ using Content.Shared.StatusEffectNew; using Robust.Shared.Audio.Systems; using Robust.Shared.Containers; using Robust.Shared.Prototypes; -using Robust.Shared.Random; using Robust.Shared.Timing; namespace Content.Shared.Body.Systems; -public abstract partial class SharedBloodstreamSystem : EntitySystem +public sealed partial class BloodstreamSystem : EntitySystem { public static readonly EntProtoId Bloodloss = "StatusEffectBloodloss"; - [Dependency] protected SharedSolutionContainerSystem SolutionContainer = default!; + [Dependency] private SharedSolutionContainerSystem _solutionContainer = default!; [Dependency] private IGameTiming _timing = default!; [Dependency] private SharedAudioSystem _audio = default!; [Dependency] private SharedPopupSystem _popup = default!; @@ -42,22 +42,6 @@ public abstract partial class SharedBloodstreamSystem : EntitySystem [Dependency] private DamageableSystem _damageableSystem = default!; [Dependency] private MetabolizerSystem _metabolizer = default!; - public override void Initialize() - { - base.Initialize(); - - SubscribeLocalEvent(OnMapInit); - SubscribeLocalEvent(OnEntRemoved); - SubscribeLocalEvent(OnReactionAttempt); - SubscribeLocalEvent>(OnReactionAttempt); - SubscribeLocalEvent(OnDamageChanged); - SubscribeLocalEvent(OnHealthBeingExamined); - SubscribeLocalEvent(OnBeingGibbed); - SubscribeLocalEvent(OnApplyMetabolicMultiplier); - SubscribeLocalEvent(OnRejuvenate); - SubscribeLocalEvent(OnMetabolismExclusion); - } - public override void Update(float frameTime) { base.Update(frameTime); @@ -72,7 +56,7 @@ public abstract partial class SharedBloodstreamSystem : EntitySystem bloodstream.NextUpdate += bloodstream.AdjustedUpdateInterval; DirtyField(uid, bloodstream, nameof(BloodstreamComponent.NextUpdate)); // needs to be dirtied on the client so it can be rerolled during prediction - if (!SolutionContainer.ResolveSolution(uid, bloodstream.BloodSolutionName, ref bloodstream.BloodSolution)) + if (!_solutionContainer.ResolveSolution(uid, bloodstream.BloodSolutionName, ref bloodstream.BloodSolution)) continue; // Blood level regulation. Must be alive. @@ -111,14 +95,15 @@ public abstract partial class SharedBloodstreamSystem : EntitySystem } } + [SubscribeLocalEvent] private void OnMapInit(Entity entity, ref MapInitEvent args) { entity.Comp.NextUpdate = _timing.CurTime + entity.Comp.AdjustedUpdateInterval; DirtyField(entity, entity.Comp, nameof(BloodstreamComponent.NextUpdate)); - SolutionContainer.EnsureSolution(entity.Owner, entity.Comp.BloodSolutionName, out var bloodSolution); - SolutionContainer.EnsureSolution(entity.Owner, entity.Comp.BloodTemporarySolutionName, out var tempSolution); - SolutionContainer.EnsureSolution(entity.Owner, entity.Comp.MetabolitesSolutionName, out var metabolitesSolution); + _solutionContainer.EnsureSolution(entity.Owner, entity.Comp.BloodSolutionName, out var bloodSolution); + _solutionContainer.EnsureSolution(entity.Owner, entity.Comp.BloodTemporarySolutionName, out var tempSolution); + _solutionContainer.EnsureSolution(entity.Owner, entity.Comp.MetabolitesSolutionName, out var metabolitesSolution); bloodSolution.Comp.Solution.MaxVolume = entity.Comp.BloodReferenceSolution.Volume * entity.Comp.MaxVolumeModifier; metabolitesSolution.Comp.Solution.MaxVolume = bloodSolution.Comp.Solution.MaxVolume; @@ -134,6 +119,7 @@ public abstract partial class SharedBloodstreamSystem : EntitySystem // prevent the infamous UdderSystem debug assert, see https://github.com/space-wizards/space-station-14/pull/35314 // TODO: find a better solution than copy pasting this into every shared system that caches solution entities + [SubscribeLocalEvent] private void OnEntRemoved(Entity entity, ref EntRemovedFromContainerMessage args) { // Make sure the removed entity was our contained solution and set it to null @@ -144,6 +130,26 @@ public abstract partial class SharedBloodstreamSystem : EntitySystem entity.Comp.TemporarySolution = null; } + [SubscribeLocalEvent] + private void OnDnaGenerated(Entity entity, ref GenerateDnaEvent args) + { + if (_solutionContainer.ResolveSolution(entity.Owner, entity.Comp.BloodSolutionName, ref entity.Comp.BloodSolution, out var bloodSolution)) + { + var data = NewEntityBloodData(entity); + entity.Comp.BloodReferenceSolution.SetReagentData(data); + + foreach (var reagent in bloodSolution.Contents) + { + List reagentData = reagent.Reagent.EnsureReagentData(); + reagentData.RemoveAll(x => x is DnaData); + reagentData.AddRange(data); + } + } + else + Log.Error("Unable to set bloodstream DNA, solution entity could not be resolved"); + } + + [SubscribeLocalEvent] private void OnReactionAttempt(Entity ent, ref ReactionAttemptEvent args) { if (args.Cancelled) @@ -171,6 +177,7 @@ public abstract partial class SharedBloodstreamSystem : EntitySystem // Having cheese-clots form in your veins can't be good for you. } + [SubscribeLocalEvent] private void OnReactionAttempt(Entity ent, ref SolutionRelayEvent args) { if (args.Solution.Comp.Id != ent.Comp.BloodSolutionName @@ -182,6 +189,7 @@ public abstract partial class SharedBloodstreamSystem : EntitySystem OnReactionAttempt(ent, ref args.Event); } + [SubscribeLocalEvent] private void OnDamageChanged(Entity ent, ref DamageChangedEvent args) { // The incoming state from the server raises a DamageChangedEvent as well. @@ -241,6 +249,7 @@ public abstract partial class SharedBloodstreamSystem : EntitySystem /// /// Shows text on health examine, based on bleed rate and blood level. /// + [SubscribeLocalEvent] private void OnHealthBeingExamined(Entity ent, ref HealthBeingExaminedEvent args) { // Shows massively bleeding at 0.75x the max bleed rate. @@ -276,29 +285,33 @@ public abstract partial class SharedBloodstreamSystem : EntitySystem } } + [SubscribeLocalEvent] private void OnBeingGibbed(Entity ent, ref GibbedBeforeDeletionEvent args) { SpillAllSolutions(ent.AsNullable()); } + [SubscribeLocalEvent] private void OnApplyMetabolicMultiplier(Entity ent, ref ApplyMetabolicMultiplierEvent args) { ent.Comp.UpdateIntervalMultiplier = args.Multiplier; DirtyField(ent, ent.Comp, nameof(BloodstreamComponent.UpdateIntervalMultiplier)); } + [SubscribeLocalEvent] private void OnRejuvenate(Entity ent, ref RejuvenateEvent args) { TryModifyBleedAmount(ent.AsNullable(), -ent.Comp.BleedAmount); - if (SolutionContainer.ResolveSolution(ent.Owner, ent.Comp.BloodSolutionName, ref ent.Comp.BloodSolution)) + if (_solutionContainer.ResolveSolution(ent.Owner, ent.Comp.BloodSolutionName, ref ent.Comp.BloodSolution)) { - SolutionContainer.RemoveAllSolution(ent.Comp.BloodSolution.Value); + _solutionContainer.RemoveAllSolution(ent.Comp.BloodSolution.Value); // TODO: Use Solutions API for this when it exists TryRegulateBloodLevel(ent.AsNullable(), ent.Comp.BloodReferenceSolution.Volume); } } + [SubscribeLocalEvent] private void OnMetabolismExclusion(Entity ent, ref MetabolismExclusionEvent args) { // Adding all blood reagents for filtering blood in metabolizer @@ -317,7 +330,7 @@ public abstract partial class SharedBloodstreamSystem : EntitySystem public float GetBloodLevel(Entity entity) { if (!Resolve(entity, ref entity.Comp) - || !SolutionContainer.ResolveSolution(entity.Owner, entity.Comp.BloodSolutionName, ref entity.Comp.BloodSolution, out var bloodSolution) + || !_solutionContainer.ResolveSolution(entity.Owner, entity.Comp.BloodSolutionName, ref entity.Comp.BloodSolution, out var bloodSolution) || entity.Comp.BloodReferenceSolution.Volume == 0) { return 0.0f; @@ -352,10 +365,10 @@ public abstract partial class SharedBloodstreamSystem : EntitySystem public bool TryAddToBloodstream(Entity ent, Solution solution) { if (!Resolve(ent, ref ent.Comp, logMissing: false) - || !SolutionContainer.ResolveSolution(ent.Owner, ent.Comp.BloodSolutionName, ref ent.Comp.BloodSolution)) + || !_solutionContainer.ResolveSolution(ent.Owner, ent.Comp.BloodSolutionName, ref ent.Comp.BloodSolution)) return false; - if (SolutionContainer.TryAddSolution(ent.Comp.BloodSolution.Value, solution)) + if (_solutionContainer.TryAddSolution(ent.Comp.BloodSolution.Value, solution)) return true; return false; @@ -370,7 +383,7 @@ public abstract partial class SharedBloodstreamSystem : EntitySystem public Solution? FlushChemicals(Entity ent, FixedPoint2 quantity, ProtoId? excludedReagent = null ) { if (!Resolve(ent, ref ent.Comp, logMissing: false) - || !SolutionContainer.ResolveSolution(ent.Owner, ent.Comp.BloodSolutionName, ref ent.Comp.BloodSolution, out var bloodSolution)) + || !_solutionContainer.ResolveSolution(ent.Owner, ent.Comp.BloodSolutionName, ref ent.Comp.BloodSolution, out var bloodSolution)) return null; var flushedSolution = new Solution(); @@ -381,7 +394,7 @@ public abstract partial class SharedBloodstreamSystem : EntitySystem if (ent.Comp.BloodReferenceSolution.ContainsPrototype(reagentId.Prototype) || reagentId.Prototype == excludedReagent) continue; - var reagentFlushAmount = SolutionContainer.RemoveReagent(ent.Comp.BloodSolution.Value, reagentId, quantity); + var reagentFlushAmount = _solutionContainer.RemoveReagent(ent.Comp.BloodSolution.Value, reagentId, quantity); flushedSolution.AddReagent(reagentId, reagentFlushAmount); } @@ -416,7 +429,7 @@ public abstract partial class SharedBloodstreamSystem : EntitySystem public bool TryRegulateBloodLevel(Entity ent, FixedPoint2 amount, float referenceFactor = 1f) { if (!Resolve(ent, ref ent.Comp, logMissing: false) - || !SolutionContainer.ResolveSolution(ent.Owner, ent.Comp.BloodSolutionName, ref ent.Comp.BloodSolution, out var bloodSolution) + || !_solutionContainer.ResolveSolution(ent.Owner, ent.Comp.BloodSolutionName, ref ent.Comp.BloodSolution, out var bloodSolution) || amount == 0) return false; @@ -469,15 +482,15 @@ public abstract partial class SharedBloodstreamSystem : EntitySystem public bool TryBleedOut(Entity ent, FixedPoint2 amount) { if (!Resolve(ent, ref ent.Comp, logMissing: false) - || !SolutionContainer.ResolveSolution(ent.Owner, ent.Comp.BloodSolutionName, ref ent.Comp.BloodSolution) + || !_solutionContainer.ResolveSolution(ent.Owner, ent.Comp.BloodSolutionName, ref ent.Comp.BloodSolution) || amount <= 0) { return false; } - var leakedBlood = SolutionContainer.SplitSolution(ent.Comp.BloodSolution.Value, amount); + var leakedBlood = _solutionContainer.SplitSolution(ent.Comp.BloodSolution.Value, amount); - if (!SolutionContainer.ResolveSolution(ent.Owner, ent.Comp.BloodTemporarySolutionName, ref ent.Comp.TemporarySolution, out var tempSolution)) + if (!_solutionContainer.ResolveSolution(ent.Owner, ent.Comp.BloodTemporarySolutionName, ref ent.Comp.TemporarySolution, out var tempSolution)) return true; tempSolution.AddSolution(leakedBlood, ProtoMan); @@ -489,7 +502,7 @@ public abstract partial class SharedBloodstreamSystem : EntitySystem tempSolution.RemoveAllSolution(); } - SolutionContainer.UpdateChemicals(ent.Comp.TemporarySolution.Value); + _solutionContainer.UpdateChemicals(ent.Comp.TemporarySolution.Value); return true; } @@ -529,18 +542,18 @@ public abstract partial class SharedBloodstreamSystem : EntitySystem var tempSol = new Solution(); - if (SolutionContainer.ResolveSolution(ent.Owner, ent.Comp.BloodSolutionName, ref ent.Comp.BloodSolution, out var bloodSolution)) + if (_solutionContainer.ResolveSolution(ent.Owner, ent.Comp.BloodSolutionName, ref ent.Comp.BloodSolution, out var bloodSolution)) { tempSol.MaxVolume += bloodSolution.MaxVolume; tempSol.AddSolution(bloodSolution, ProtoMan); - SolutionContainer.RemoveAllSolution(ent.Comp.BloodSolution.Value); + _solutionContainer.RemoveAllSolution(ent.Comp.BloodSolution.Value); } - if (SolutionContainer.ResolveSolution(ent.Owner, ent.Comp.BloodTemporarySolutionName, ref ent.Comp.TemporarySolution, out var tempSolution)) + if (_solutionContainer.ResolveSolution(ent.Owner, ent.Comp.BloodTemporarySolutionName, ref ent.Comp.TemporarySolution, out var tempSolution)) { tempSol.MaxVolume += tempSolution.MaxVolume; tempSol.AddSolution(tempSolution, ProtoMan); - SolutionContainer.RemoveAllSolution(ent.Comp.TemporarySolution.Value); + _solutionContainer.RemoveAllSolution(ent.Comp.TemporarySolution.Value); } _puddle.TrySpillAt(ent, tempSol, out _); @@ -565,7 +578,7 @@ public abstract partial class SharedBloodstreamSystem : EntitySystem return; } - if (!SolutionContainer.ResolveSolution(ent.Owner, ent.Comp.BloodSolutionName, ref ent.Comp.BloodSolution, out var bloodSolution)) + if (!_solutionContainer.ResolveSolution(ent.Owner, ent.Comp.BloodSolutionName, ref ent.Comp.BloodSolution, out var bloodSolution)) { ent.Comp.BloodReferenceSolution = reagents.Clone(); DirtyField(ent, ent.Comp, nameof(BloodstreamComponent.BloodReferenceSolution)); @@ -586,7 +599,7 @@ public abstract partial class SharedBloodstreamSystem : EntitySystem var solution = ent.Comp.BloodReferenceSolution.Clone(); solution.ScaleSolution(currentVolume / solution.Volume); - SolutionContainer.AddSolution(ent.Comp.BloodSolution.Value, solution); + _solutionContainer.AddSolution(ent.Comp.BloodSolution.Value, solution); } /// @@ -603,7 +616,7 @@ public abstract partial class SharedBloodstreamSystem : EntitySystem /// /// Gets new blood data for this entity and caches it in /// - protected List NewEntityBloodData(EntityUid uid) + private List NewEntityBloodData(EntityUid uid) { var bloodData = new List(); var dnaData = new DnaData(); diff --git a/Content.Shared/Devour/DevourSystem.cs b/Content.Shared/Devour/DevourSystem.cs index 98eb776435..fd181ee871 100644 --- a/Content.Shared/Devour/DevourSystem.cs +++ b/Content.Shared/Devour/DevourSystem.cs @@ -19,7 +19,7 @@ public sealed partial class DevourSystem : EntitySystem [Dependency] private EntityWhitelistSystem _whitelistSystem = default!; [Dependency] private SharedActionsSystem _actionsSystem = default!; [Dependency] private SharedAudioSystem _audioSystem = default!; - [Dependency] private SharedBloodstreamSystem _bloodstreamSystem = default!; + [Dependency] private BloodstreamSystem _bloodstreamSystem = default!; [Dependency] private SharedContainerSystem _containerSystem = default!; [Dependency] private SharedDoAfterSystem _doAfterSystem = default!; [Dependency] private SharedPopupSystem _popupSystem = default!; diff --git a/Content.Shared/EntityEffects/Effects/Body/AddReagentToBloodstreamEntityEffectSystem.cs b/Content.Shared/EntityEffects/Effects/Body/AddReagentToBloodstreamEntityEffectSystem.cs index 67e8690d0c..fbbc20f2c6 100644 --- a/Content.Shared/EntityEffects/Effects/Body/AddReagentToBloodstreamEntityEffectSystem.cs +++ b/Content.Shared/EntityEffects/Effects/Body/AddReagentToBloodstreamEntityEffectSystem.cs @@ -14,7 +14,7 @@ namespace Content.Shared.EntityEffects.Effects.Body; /// public sealed partial class AddReagentToBloodstreamEntityEffectSystem : EntityEffectSystem { - [Dependency] private SharedBloodstreamSystem _bloodstream = default!; + [Dependency] private BloodstreamSystem _bloodstream = default!; [Dependency] private ReactiveSystem _reactive = default!; protected override void Effect(Entity entity, ref EntityEffectEvent args) diff --git a/Content.Shared/EntityEffects/Effects/Body/CleanBloodstreamEntityEffectSystem.cs b/Content.Shared/EntityEffects/Effects/Body/CleanBloodstreamEntityEffectSystem.cs index 726b54d20d..db8cdf2a37 100644 --- a/Content.Shared/EntityEffects/Effects/Body/CleanBloodstreamEntityEffectSystem.cs +++ b/Content.Shared/EntityEffects/Effects/Body/CleanBloodstreamEntityEffectSystem.cs @@ -13,7 +13,7 @@ namespace Content.Shared.EntityEffects.Effects.Body; /// public sealed partial class CleanBloodstreamEntityEffectSystem : EntityEffectSystem { - [Dependency] private SharedBloodstreamSystem _bloodstream = default!; + [Dependency] private BloodstreamSystem _bloodstream = default!; protected override void Effect(Entity entity, ref EntityEffectEvent args) { diff --git a/Content.Shared/EntityEffects/Effects/Body/ModifyBleedEntityEffectSystem.cs b/Content.Shared/EntityEffects/Effects/Body/ModifyBleedEntityEffectSystem.cs index 9b0c9a9e52..79b2e3ce49 100644 --- a/Content.Shared/EntityEffects/Effects/Body/ModifyBleedEntityEffectSystem.cs +++ b/Content.Shared/EntityEffects/Effects/Body/ModifyBleedEntityEffectSystem.cs @@ -10,7 +10,7 @@ namespace Content.Shared.EntityEffects.Effects.Body; /// public sealed partial class ModifyBleedEntityEffectSystem : EntityEffectSystem { - [Dependency] private SharedBloodstreamSystem _bloodstream = default!; + [Dependency] private BloodstreamSystem _bloodstream = default!; protected override void Effect(Entity entity, ref EntityEffectEvent args) { diff --git a/Content.Shared/EntityEffects/Effects/Body/ModifyBloodLevelEntityEffectSystem.cs b/Content.Shared/EntityEffects/Effects/Body/ModifyBloodLevelEntityEffectSystem.cs index 8a8f299ea6..cd864fd964 100644 --- a/Content.Shared/EntityEffects/Effects/Body/ModifyBloodLevelEntityEffectSystem.cs +++ b/Content.Shared/EntityEffects/Effects/Body/ModifyBloodLevelEntityEffectSystem.cs @@ -12,7 +12,7 @@ namespace Content.Shared.EntityEffects.Effects.Body; /// public sealed partial class ModifyBloodLevelEntityEffectSystem : EntityEffectSystem { - [Dependency] private SharedBloodstreamSystem _bloodstream = default!; + [Dependency] private BloodstreamSystem _bloodstream = default!; protected override void Effect(Entity entity, ref EntityEffectEvent args) { diff --git a/Content.Server/Forensics/Components/CleansForensicsComponent.cs b/Content.Shared/Forensics/Components/CleansForensicsComponent.cs similarity index 62% rename from Content.Server/Forensics/Components/CleansForensicsComponent.cs rename to Content.Shared/Forensics/Components/CleansForensicsComponent.cs index a1f40c9527..dffa0af917 100644 --- a/Content.Server/Forensics/Components/CleansForensicsComponent.cs +++ b/Content.Shared/Forensics/Components/CleansForensicsComponent.cs @@ -1,14 +1,16 @@ -namespace Content.Server.Forensics; +using Robust.Shared.GameStates; + +namespace Content.Shared.Forensics.Components; /// /// This component is for items that can clean up forensic evidence /// -[RegisterComponent] +[RegisterComponent, NetworkedComponent] public sealed partial class CleansForensicsComponent : Component { /// /// How long it takes to wipe prints/blood/etc. off of things using this entity /// [DataField] - public float CleanDelay = 12.0f; + public TimeSpan CleanDelay = TimeSpan.FromSeconds(12.0f); } diff --git a/Content.Shared/Forensics/Components/DnaSubstanceTraceComponent.cs b/Content.Shared/Forensics/Components/DnaSubstanceTraceComponent.cs new file mode 100644 index 0000000000..bc276d72a1 --- /dev/null +++ b/Content.Shared/Forensics/Components/DnaSubstanceTraceComponent.cs @@ -0,0 +1,10 @@ +using Robust.Shared.GameStates; + +namespace Content.Shared.Forensics.Components; + +/// +/// This component stops the entity from leaving finger prints, +/// usually so fibers can be left instead. +/// +[RegisterComponent, NetworkedComponent] +public sealed partial class DnaSubstanceTraceComponent : Component; diff --git a/Content.Shared/Forensics/Components/FiberComponent.cs b/Content.Shared/Forensics/Components/FiberComponent.cs new file mode 100644 index 0000000000..36944251f4 --- /dev/null +++ b/Content.Shared/Forensics/Components/FiberComponent.cs @@ -0,0 +1,17 @@ +using Robust.Shared.GameStates; + +namespace Content.Shared.Forensics.Components; + +/// +/// This controls fibers left by gloves on items, +/// which the forensics system uses. +/// +[RegisterComponent, NetworkedComponent] +public sealed partial class FiberComponent : Component +{ + [DataField] + public LocId FiberMaterial = "fibers-synthetic"; + + [DataField] + public LocId? FiberColor; +} diff --git a/Content.Shared/Forensics/Components/ForensicPadComponent.cs b/Content.Shared/Forensics/Components/ForensicPadComponent.cs new file mode 100644 index 0000000000..0af5086d4a --- /dev/null +++ b/Content.Shared/Forensics/Components/ForensicPadComponent.cs @@ -0,0 +1,25 @@ +using Robust.Shared.GameStates; + +namespace Content.Shared.Forensics.Components; + +/// +/// Used to take a sample of someone's fingerprints. +/// +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] +public sealed partial class ForensicPadComponent : Component +{ + /// + /// The amount of time for the pad to be used to take a sample. + /// + [DataField] + public TimeSpan ScanDelay = TimeSpan.FromSeconds(3.0f); + + [AutoNetworkedField] + public bool Used; + + /// + /// The information acquired from using the forensic pad. + /// + [AutoNetworkedField] + public String Sample = string.Empty; +} diff --git a/Content.Shared/Forensics/Components/ForensicScannerComponent.cs b/Content.Shared/Forensics/Components/ForensicScannerComponent.cs new file mode 100644 index 0000000000..f5d6f809b5 --- /dev/null +++ b/Content.Shared/Forensics/Components/ForensicScannerComponent.cs @@ -0,0 +1,88 @@ +using Robust.Shared.Audio; +using Robust.Shared.GameStates; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom; + +namespace Content.Shared.Forensics.Components; + +[RegisterComponent, NetworkedComponent] +[AutoGenerateComponentState(raiseAfterAutoHandleState: true, fieldDeltas: true), AutoGenerateComponentPause] +public sealed partial class ForensicScannerComponent : Component +{ + /// + /// A list of fingerprint GUIDs that the forensic scanner found from the on an entity. + /// + [DataField, AutoNetworkedField] + public List Fingerprints = []; + + /// + /// A list of glove fibers that the forensic scanner found from the on an entity. + /// + [DataField, AutoNetworkedField] + public List Fibers = []; + + /// + /// DNA that the forensic scanner found from the on an entity. + /// + [DataField, AutoNetworkedField] + public List DNAs = []; + + /// + /// Residue that the forensic scanner found from the on an entity. + /// + [DataField, AutoNetworkedField] + public List Residues = []; + + /// + /// What is the name of the entity that was scanned last? + /// + /// + /// This will be used for the title of the printout and displayed to players. + /// + [DataField, AutoNetworkedField] + public string LastScannedName = string.Empty; + + /// + /// When will the scanner be ready to print again? + /// + [DataField(customTypeSerializer: typeof(TimeOffsetSerializer)), AutoPausedField, AutoNetworkedField] + public TimeSpan PrintReadyAt = TimeSpan.Zero; + + /// + /// The time (in seconds) that it takes to scan an entity. + /// + [DataField] + public float ScanDelay = 3.0f; + + /// + /// How often can the scanner print out reports? + /// + [DataField] + public TimeSpan PrintCooldown = TimeSpan.FromSeconds(5); + + /// + /// The sound that's played when there's a match between a scan and an + /// inserted forensic pad. + /// + [DataField] + public SoundSpecifier SoundMatch = new SoundPathSpecifier("/Audio/Machines/Nuke/angry_beep.ogg"); + + /// + /// The sound that's played when there's no match between a scan and an + /// inserted forensic pad. + /// + [DataField] + public SoundSpecifier SoundNoMatch = new SoundPathSpecifier("/Audio/Machines/airlock_deny.ogg"); + + /// + /// The sound that's played when the scanner prints off a report. + /// + [DataField] + public SoundSpecifier SoundPrint = new SoundPathSpecifier("/Audio/Machines/short_print_and_rip.ogg"); + + /// + /// What type of paper the device will print. + /// + [DataField] + public EntProtoId PaperPrototypeID = "ForensicReportPaper"; +} diff --git a/Content.Shared/Forensics/Components/ForensicsComponent.cs b/Content.Shared/Forensics/Components/ForensicsComponent.cs new file mode 100644 index 0000000000..77e824291e --- /dev/null +++ b/Content.Shared/Forensics/Components/ForensicsComponent.cs @@ -0,0 +1,47 @@ +using Robust.Shared.GameStates; + +namespace Content.Shared.Forensics.Components; + +/// +/// Leaves evidence on entities for to find. +/// +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] +public sealed partial class ForensicsComponent : Component +{ + /// + /// Leaves evidence of FingerPrints left by uncovered hands. + /// + [DataField, AutoNetworkedField] + public HashSet Fingerprints = []; + + /// + /// Leaves evidence of Fibers left by gloves. + /// + [DataField, AutoNetworkedField] + public HashSet Fibers = []; + + /// + /// Leaves evidence of DNA left by bodily fluids. + /// + [DataField, AutoNetworkedField] + public HashSet DNAs = []; + + /// + /// Leaves evidence of Residues left by cleaning products. + /// + [DataField, AutoNetworkedField] + public HashSet Residues = []; + + /// + /// How close you must be to wipe the prints/blood/etc. off of this entity + /// + [DataField] + public float CleanDistance = 1.5f; + + /// + /// Can the DNA be cleaned off of this entity? + /// e.g. you can wipe the DNA off of a knife, but not a cigarette + /// + [DataField] + public bool CanDnaBeCleaned = true; +} diff --git a/Content.Server/Forensics/Components/IgnoresFingerprintsComponent.cs b/Content.Shared/Forensics/Components/IgnoresFingerprintsComponent.cs similarity index 59% rename from Content.Server/Forensics/Components/IgnoresFingerprintsComponent.cs rename to Content.Shared/Forensics/Components/IgnoresFingerprintsComponent.cs index 4ecaf849af..ddc6423646 100644 --- a/Content.Server/Forensics/Components/IgnoresFingerprintsComponent.cs +++ b/Content.Shared/Forensics/Components/IgnoresFingerprintsComponent.cs @@ -1,7 +1,9 @@ -namespace Content.Server.Forensics.Components; +using Robust.Shared.GameStates; + +namespace Content.Shared.Forensics.Components; /// /// This component is for entities we do not wish to track fingerprints/fibers, like puddles /// -[RegisterComponent] -public sealed partial class IgnoresFingerprintsComponent : Component { } +[RegisterComponent, NetworkedComponent] +public sealed partial class IgnoresFingerprintsComponent : Component; diff --git a/Content.Shared/Forensics/Components/ResidueComponent.cs b/Content.Shared/Forensics/Components/ResidueComponent.cs new file mode 100644 index 0000000000..debcd128fa --- /dev/null +++ b/Content.Shared/Forensics/Components/ResidueComponent.cs @@ -0,0 +1,23 @@ +using Robust.Shared.GameStates; + +namespace Content.Shared.Forensics.Components; + +/// +/// This controls residues left on items +/// which the forensics system uses. +/// +[RegisterComponent, NetworkedComponent] +public sealed partial class ResidueComponent : Component +{ + /// + /// Specifies residue, for now only color. + /// + [DataField] + public LocId ResidueAdjective = "residue-unknown"; + + /// + /// Leaves color of cleaning product for ResidueAdjective to use. + /// + [DataField] + public string? ResidueColor; +} diff --git a/Content.Shared/Forensics/Events.cs b/Content.Shared/Forensics/Events.cs index 85494b37a0..bbab8e4522 100644 --- a/Content.Shared/Forensics/Events.cs +++ b/Content.Shared/Forensics/Events.cs @@ -1,4 +1,5 @@ using Content.Shared.DoAfter; +using Content.Shared.Forensics.Components; using Content.Shared.Inventory; using Robust.Shared.Serialization; @@ -12,7 +13,8 @@ public sealed partial class ForensicScannerDoAfterEvent : SimpleDoAfterEvent [Serializable, NetSerializable] public sealed partial class ForensicPadDoAfterEvent : DoAfterEvent { - [DataField("sample", required: true)] public string Sample = default!; + [DataField(required: true)] + public string Sample = default!; private ForensicPadDoAfterEvent() { @@ -27,47 +29,23 @@ public sealed partial class ForensicPadDoAfterEvent : DoAfterEvent } [Serializable, NetSerializable] -public sealed partial class CleanForensicsDoAfterEvent : SimpleDoAfterEvent -{ -} - -/// -/// An event to apply DNA evidence from a donor onto some recipient. -/// -[ByRefEvent] -public record struct TransferDnaEvent() -{ - /// - /// The entity donating the DNA. - /// - public EntityUid Donor; - - /// - /// The entity receiving the DNA. - /// - public EntityUid Recipient; - - /// - /// Can the DNA be cleaned off? - /// - public bool CanDnaBeCleaned = true; -} +public sealed partial class CleanForensicsDoAfterEvent : SimpleDoAfterEvent; /// /// Raised on an entity when its DNA has been changed. /// [ByRefEvent] -public record struct GenerateDnaEvent() +public record struct GenerateDnaEvent(EntityUid Owner, string DNA) { /// /// The entity getting new DNA. /// - public EntityUid Owner; + public EntityUid Owner = Owner; /// /// The generated DNA. /// - public required string DNA; + public string DNA = DNA; } /// diff --git a/Content.Shared/Forensics/ForensicScannerEvent.cs b/Content.Shared/Forensics/ForensicScannerEvent.cs index f275680b05..9eb6787830 100644 --- a/Content.Shared/Forensics/ForensicScannerEvent.cs +++ b/Content.Shared/Forensics/ForensicScannerEvent.cs @@ -1,53 +1,15 @@ using Robust.Shared.Serialization; -namespace Content.Shared.Forensics +namespace Content.Shared.Forensics; + +[Serializable, NetSerializable] +public enum ForensicScannerUiKey : byte { - [Serializable, NetSerializable] - public sealed class ForensicScannerBoundUserInterfaceState : BoundUserInterfaceState - { - public readonly List Fingerprints = new(); - public readonly List Fibers = new(); - public readonly List TouchDNAs = new(); - public readonly List SolutionDNAs = new(); - public readonly List Residues = new(); - public readonly string LastScannedName = string.Empty; - public readonly TimeSpan PrintCooldown = TimeSpan.Zero; - public readonly TimeSpan PrintReadyAt = TimeSpan.Zero; - - public ForensicScannerBoundUserInterfaceState( - List fingerprints, - List fibers, - List touchDnas, - List solutionDnas, - List residues, - string lastScannedName, - TimeSpan printCooldown, - TimeSpan printReadyAt) - { - Fingerprints = fingerprints; - Fibers = fibers; - TouchDNAs = touchDnas; - SolutionDNAs = solutionDnas; - Residues = residues; - LastScannedName = lastScannedName; - PrintCooldown = printCooldown; - PrintReadyAt = printReadyAt; - } - } - - [Serializable, NetSerializable] - public enum ForensicScannerUiKey : byte - { - Key - } - - [Serializable, NetSerializable] - public sealed class ForensicScannerPrintMessage : BoundUserInterfaceMessage - { - } - - [Serializable, NetSerializable] - public sealed class ForensicScannerClearMessage : BoundUserInterfaceMessage - { - } + Key } + +[Serializable, NetSerializable] +public sealed class ForensicScannerPrintMessage : BoundUserInterfaceMessage; + +[Serializable, NetSerializable] +public sealed class ForensicScannerClearMessage : BoundUserInterfaceMessage; diff --git a/Content.Shared/Forensics/Systems/FingerprintMaskSystem.cs b/Content.Shared/Forensics/Systems/FingerprintMaskSystem.cs new file mode 100644 index 0000000000..a200ac5414 --- /dev/null +++ b/Content.Shared/Forensics/Systems/FingerprintMaskSystem.cs @@ -0,0 +1,17 @@ +using Content.Shared.Forensics.Components; +using Content.Shared.Inventory; + +namespace Content.Shared.Forensics.Systems; + +public sealed partial class FingerprintMaskSystem : EntitySystem +{ + [SubscribeLocalEvent] + private void OnTryAccessFingerprint(Entity gloves, ref InventoryRelayedEvent args) + { + if (args.Args.Cancelled) + return; + + args.Args.Blocker = gloves.Owner; + args.Args.Cancel(); + } +} diff --git a/Content.Shared/Forensics/Systems/ForensicPadSystem.cs b/Content.Shared/Forensics/Systems/ForensicPadSystem.cs new file mode 100644 index 0000000000..00daebb17a --- /dev/null +++ b/Content.Shared/Forensics/Systems/ForensicPadSystem.cs @@ -0,0 +1,118 @@ +using Content.Shared.DoAfter; +using Content.Shared.Examine; +using Content.Shared.Forensics.Components; +using Content.Shared.IdentityManagement; +using Content.Shared.Interaction; +using Content.Shared.Labels.EntitySystems; +using Content.Shared.Popups; + +namespace Content.Shared.Forensics.Systems; + +/// +/// Used to transfer fingerprints from entities to forensic pads. +/// +public sealed partial class ForensicPadSystem : EntitySystem +{ + [Dependency] private SharedDoAfterSystem _doAfterSystem = default!; + [Dependency] private SharedPopupSystem _popupSystem = default!; + [Dependency] private SharedForensicsSystem _forensics = default!; + [Dependency] private LabelSystem _label = default!; + + [SubscribeLocalEvent] + private void OnExamined(Entity pad, ref ExaminedEvent args) + { + if (!args.IsInDetailsRange) + return; + + if (!pad.Comp.Used) + { + args.PushMarkup(Loc.GetString("forensic-pad-unused")); + return; + } + + args.PushMarkup(Loc.GetString("forensic-pad-sample", ("sample", pad.Comp.Sample))); + } + + [SubscribeLocalEvent] + private void OnAfterInteract(Entity pad, ref AfterInteractEvent args) + { + if (!args.CanReach || args.Target == null || HasComp(args.Target)) + return; + + args.Handled = true; + + if (pad.Comp.Used) + { + _popupSystem.PopupEntity(Loc.GetString("forensic-pad-already-used"), args.Target.Value, args.User); + return; + } + + if (!_forensics.CanAccessFingerprint(args.Target.Value, out var blocker)) + { + var message = blocker is { } item + ? Loc.GetString("forensic-pad-no-access-due", ("entity", Identity.Entity(item, EntityManager))) + : Loc.GetString("forensic-pad-no-access"); + + _popupSystem.PopupEntity(message, args.Target.Value, args.User); + return; + } + + if (TryComp(args.Target, out var fingerprint) && fingerprint.Fingerprint != null) + { + var skipDelay = true; + if (args.User != args.Target) + { + var userMessage = Loc.GetString("forensic-pad-start-scan-user", ("target", Identity.Entity(args.Target.Value, EntityManager))); + var targetMessage = Loc.GetString("forensic-pad-start-scan-target", ("user", Identity.Entity(args.User, EntityManager))); + + skipDelay = false; + _popupSystem.PopupEntity(userMessage, args.Target.Value, args.User); + _popupSystem.PopupEntity(targetMessage, args.Target.Value, args.Target.Value); + } + StartScan(pad, args.User, args.Target.Value, pad.Comp, fingerprint.Fingerprint, skipDelay); + return; + } + + if (TryComp(args.Target, out var fiber)) + { + var fiberString = string.IsNullOrEmpty(fiber.FiberColor) + ? Loc.GetString("forensic-fibers", ("material", fiber.FiberMaterial)) + : Loc.GetString("forensic-fibers-colored", ("color", fiber.FiberColor), ("material", fiber.FiberMaterial)); + + StartScan(pad, args.User, args.Target.Value, pad.Comp, fiberString, true); + } + } + + private void StartScan(EntityUid used, EntityUid user, EntityUid target, ForensicPadComponent pad, string sample, bool skipDelay = false) + { + var ev = new ForensicPadDoAfterEvent(sample); + var delay = skipDelay ? TimeSpan.Zero : pad.ScanDelay; + + var doAfterEventArgs = new DoAfterArgs(EntityManager, user, delay, ev, used, target: target, used: used) + { + NeedHand = true, + BreakOnMove = true, + }; + + _doAfterSystem.TryStartDoAfter(doAfterEventArgs); + } + + [SubscribeLocalEvent] + private void OnDoAfter(Entity pad, ref ForensicPadDoAfterEvent args) + { + if (args.Handled || args.Cancelled) + return; + + if (args.Args.Target != null) + { + var label = Identity.Name(args.Args.Target.Value, EntityManager); + _label.Label(pad, label); + } + + pad.Comp.Sample = args.Sample; + pad.Comp.Used = true; + + args.Handled = true; + Dirty(pad); + } +} diff --git a/Content.Shared/Forensics/Systems/SharedForensicScannerSystem.cs b/Content.Shared/Forensics/Systems/SharedForensicScannerSystem.cs new file mode 100644 index 0000000000..5be60c0c3d --- /dev/null +++ b/Content.Shared/Forensics/Systems/SharedForensicScannerSystem.cs @@ -0,0 +1,260 @@ +using System.Linq; +using System.Text; +using Content.Shared.UserInterface; +using Content.Shared.DoAfter; +using Content.Shared.Hands.EntitySystems; +using Content.Shared.Interaction; +using Content.Shared.Paper; +using Content.Shared.Verbs; +using Content.Shared.Tag; +using Robust.Shared.Audio.Systems; +using Robust.Shared.Audio; +using Robust.Shared.Timing; +using Content.Shared.Forensics.Components; +using Content.Shared.IdentityManagement; +using Content.Shared.Popups; +using Robust.Shared.Prototypes; +// todo: remove this stinky LINQy + +namespace Content.Shared.Forensics.Systems; + +public sealed partial class SharedForensicScannerSystem : EntitySystem +{ + [Dependency] private IGameTiming _gameTiming = default!; + [Dependency] private SharedDoAfterSystem _doAfterSystem = default!; + [Dependency] private SharedUserInterfaceSystem _uiSystem = default!; + [Dependency] private SharedPopupSystem _popupSystem = default!; + [Dependency] private PaperSystem _paperSystem = default!; + [Dependency] private SharedHandsSystem _handsSystem = default!; + [Dependency] private SharedAudioSystem _audioSystem = default!; + [Dependency] private MetaDataSystem _metaData = default!; + [Dependency] private SharedForensicsSystem _forensicsSystem = default!; + [Dependency] private TagSystem _tag = default!; + + private static readonly ProtoId DNASolutionScannableTag = "DNASolutionScannable"; + + [SubscribeLocalEvent] + private void OnScannerUpdate(Entity scanner, ref AfterAutoHandleStateEvent args) + { + UpdateUi(scanner); + } + + private void UpdateUi(Entity scanner) + { + if (_uiSystem.TryGetOpenUi(scanner.Owner, ForensicScannerUiKey.Key, out var bui)) + { + bui.Update(); + } + } + + [SubscribeLocalEvent] + private void OnDoAfter(Entity scanner, ref ForensicScannerDoAfterEvent args) + { + if (args.Handled || args.Cancelled) + return; + + if (args.Args.Target != null) + { + if (TryComp(args.Args.Target, out var forensics)) + { + scanner.Comp.Fingerprints = forensics.Fingerprints.ToList(); + scanner.Comp.Fibers = forensics.Fibers.ToList(); + scanner.Comp.DNAs = forensics.DNAs.ToList(); + scanner.Comp.Residues = forensics.Residues.ToList(); + } + else + { + scanner.Comp.Fingerprints = []; + scanner.Comp.Fibers = []; + scanner.Comp.DNAs = []; + scanner.Comp.Residues = []; + } + + if (_tag.HasTag(args.Args.Target.Value, DNASolutionScannableTag)) + { + scanner.Comp.DNAs.AddRange(_forensicsSystem.GetSolutionsDNA(args.Args.Target.Value)); + } + + scanner.Comp.LastScannedName = Identity.Name(args.Args.Target.Value, EntityManager, args.Args.User); + + DirtyFields(scanner.AsNullable(), + null, + nameof(ForensicScannerComponent.Fingerprints), + nameof(ForensicScannerComponent.Fibers), + nameof(ForensicScannerComponent.DNAs), + nameof(ForensicScannerComponent.Residues), + nameof(ForensicScannerComponent.LastScannedName)); + } + + OpenUserInterface(args.Args.User, scanner); + } + + /// + /// Hosts logic common between OnUtilityVerb and OnAfterInteract. + /// + private void StartScan(Entity scanner, EntityUid user, EntityUid target) + { + _doAfterSystem.TryStartDoAfter(new DoAfterArgs(EntityManager, user, scanner.Comp.ScanDelay, new ForensicScannerDoAfterEvent(), scanner, target: target, used: scanner) + { + BreakOnMove = true, + NeedHand = true, + }); + } + + [SubscribeLocalEvent] + private void OnUtilityVerb(Entity scanner, ref GetVerbsEvent args) + { + if (!args.CanInteract || !args.CanAccess) + return; + + var evArgs = args; + + var verb = new UtilityVerb() + { + Act = () => StartScan(scanner, evArgs.User, evArgs.Target), + IconEntity = GetNetEntity(scanner), + Text = Loc.GetString("forensic-scanner-verb-text"), + Message = Loc.GetString("forensic-scanner-verb-message"), + // This is important because if its true using the scanner will count as touching the object. + DoContactInteraction = false, + }; + + args.Verbs.Add(verb); + } + + [SubscribeLocalEvent] + private void OnAfterInteract(Entity scanner, ref AfterInteractEvent args) + { + if (args.Target == null || !args.CanReach) + return; + + StartScan(scanner, args.User, args.Target.Value); + } + + [SubscribeLocalEvent] + private void OnAfterInteractUsing(Entity scanner, ref AfterInteractUsingEvent args) + { + if (args.Handled || !args.CanReach) + return; + + if (!TryComp(args.Used, out var pad)) + return; + + foreach (var fiber in scanner.Comp.Fibers) + { + if (fiber != pad.Sample) + continue; + + _audioSystem.PlayPredicted(scanner.Comp.SoundMatch, scanner.Owner, args.User); + _popupSystem.PopupEntity(Loc.GetString("forensic-scanner-match-fiber"), scanner, args.User); + return; + } + + foreach (var fingerprint in scanner.Comp.Fingerprints) + { + if (fingerprint != pad.Sample) + continue; + + _audioSystem.PlayPredicted(scanner.Comp.SoundMatch, scanner.Owner, args.User); + _popupSystem.PopupEntity(Loc.GetString("forensic-scanner-match-fingerprint"), scanner, args.User); + return; + } + + _audioSystem.PlayPredicted(scanner.Comp.SoundNoMatch, scanner.Owner, args.User); + _popupSystem.PopupEntity(Loc.GetString("forensic-scanner-match-none"), scanner, args.User); + } + + [SubscribeLocalEvent] + private void OnBeforeActivatableUIOpen(Entity scanner, ref BeforeActivatableUIOpenEvent args) + { + UpdateUi(scanner); + } + + private void OpenUserInterface(EntityUid user, Entity scanner) + { + _uiSystem.OpenUi(scanner.Owner, ForensicScannerUiKey.Key, user, true); + + UpdateUi(scanner); + } + + [SubscribeLocalEvent] + private void OnPrint(Entity scanner, ref ForensicScannerPrintMessage args) + { + var user = args.Actor; + + if (_gameTiming.CurTime < scanner.Comp.PrintReadyAt) + { + // This shouldn't occur due to the UI guarding against it, but + // if it does, tell the user why nothing happened. + _popupSystem.PopupEntity(Loc.GetString("forensic-scanner-printer-not-ready"), scanner, user); + return; + } + + // Spawn a piece of paper. + var printed = PredictedSpawnAtPosition(scanner.Comp.PaperPrototypeID, Transform(scanner).Coordinates); + _handsSystem.PickupOrDrop(user, printed, checkActionBlocker: false); + + if (!TryComp(printed, out var paperComp)) + { + Log.Error("Printed paper did not have PaperComponent."); + return; + } + + _metaData.SetEntityName(printed, Loc.GetString("forensic-scanner-report-title", ("entity", scanner.Comp.LastScannedName))); + + var text = new StringBuilder(); + + text.AppendLine(Loc.GetString("forensic-scanner-interface-fingerprints")); + foreach (var fingerprint in scanner.Comp.Fingerprints) + { + text.AppendLine(fingerprint); + } + text.AppendLine(); + text.AppendLine(Loc.GetString("forensic-scanner-interface-fibers")); + foreach (var fiber in scanner.Comp.Fibers) + { + text.AppendLine(fiber); + } + text.AppendLine(); + text.AppendLine(Loc.GetString("forensic-scanner-interface-dnas")); + foreach (var dna in scanner.Comp.DNAs) + { + text.AppendLine(dna); + } + text.AppendLine(); + text.AppendLine(Loc.GetString("forensic-scanner-interface-residues")); + foreach (var residue in scanner.Comp.Residues) + { + text.AppendLine(residue); + } + + _paperSystem.SetContent((printed, paperComp), text.ToString()); + var audioParams = scanner.Comp.SoundPrint?.Params ?? AudioParams.Default; + audioParams = audioParams.WithVariation(0.25f).AddVolume(3f).WithRolloffFactor(2.8f).WithMaxDistance(4.5f); + _audioSystem.PlayPredicted(scanner.Comp.SoundPrint, scanner, user, audioParams); + + scanner.Comp.PrintReadyAt = _gameTiming.CurTime + scanner.Comp.PrintCooldown; + + DirtyField(scanner.AsNullable(), nameof(ForensicScannerComponent.PrintReadyAt)); + + UpdateUi(scanner); + } + + [SubscribeLocalEvent] + private void OnClear(Entity scanner, ref ForensicScannerClearMessage args) + { + scanner.Comp.Fingerprints = []; + scanner.Comp.Fibers = []; + scanner.Comp.DNAs = []; + scanner.Comp.LastScannedName = string.Empty; + + DirtyFields(scanner.AsNullable(), + null, + nameof(ForensicScannerComponent.Fingerprints), + nameof(ForensicScannerComponent.Fibers), + nameof(ForensicScannerComponent.DNAs), + nameof(ForensicScannerComponent.LastScannedName)); + + UpdateUi(scanner); + } +} diff --git a/Content.Shared/Forensics/Systems/SharedForensicsSystem.cs b/Content.Shared/Forensics/Systems/SharedForensicsSystem.cs index be26fd6fd0..c63fd264a7 100644 --- a/Content.Shared/Forensics/Systems/SharedForensicsSystem.cs +++ b/Content.Shared/Forensics/Systems/SharedForensicsSystem.cs @@ -1,27 +1,371 @@ +using Content.Shared.Body.Systems; +using Content.Shared.Chemistry.Components; +using Content.Shared.Chemistry.EntitySystems; +using Content.Shared.Chemistry.Reagent; +using Content.Shared.DoAfter; +using Content.Shared.Fluids; using Content.Shared.Forensics.Components; +using Content.Shared.Hands.Components; +using Content.Shared.Interaction; +using Content.Shared.Interaction.Events; +using Content.Shared.Inventory; +using Content.Shared.Popups; +using Content.Shared.Verbs; +using Content.Shared.Gibbing; +using Content.Shared.IdentityManagement; +using Content.Shared.Nutrition.EntitySystems; +using Content.Shared.Weapons.Melee.Events; +using Robust.Shared.Random; +using Robust.Shared.Utility; namespace Content.Shared.Forensics.Systems; -public abstract class SharedForensicsSystem : EntitySystem +public sealed partial class SharedForensicsSystem : EntitySystem { - /// - /// Give the entity a new, random DNA string and call an event to notify other systems like the bloodstream that it has been changed. - /// Does nothing if it does not have the DnaComponent. - /// - public virtual void RandomizeDNA(Entity ent) { } + [Dependency] private IRobustRandom _random = default!; + [Dependency] private InventorySystem _inventory = default!; + [Dependency] private SharedDoAfterSystem _doAfterSystem = default!; + [Dependency] private SharedPopupSystem _popupSystem = default!; + [Dependency] private SharedSolutionContainerSystem _solutionContainerSystem = default!; + + [SubscribeLocalEvent] + private void OnSolutionChanged(Entity ent, ref SolutionChangedEvent ev) + { + var soln = GetSolutionsDNA(ev.Solution); + + if (soln.Count <= 0) + return; + + var comp = EnsureComp(ent.Owner); + foreach (var dna in soln) + { + comp.DNAs.Add(dna); + } + Dirty(ent); + } + + [SubscribeLocalEvent] + private void OnInteract(Entity ent, ref ContactInteractionEvent args) + { + ApplyEvidence(ent.Owner, args.Other); + } + + [SubscribeLocalEvent(after: [typeof(BloodstreamSystem)])] + private void OnFingerprintInit(Entity ent, ref MapInitEvent args) + { + if (ent.Comp.Fingerprint == null) + RandomizeFingerprint((ent.Owner, ent.Comp)); + } + + [SubscribeLocalEvent(after: [typeof(BloodstreamSystem)])] + private void OnDNAInit(Entity ent, ref MapInitEvent args) + { + if (ent.Comp.DNA == null) + RandomizeDNA(ent.AsNullable()); + else + { + // If set manually (for example by cloning) we also need to inform the bloodstream of the correct DNA string so it can be updated + var ev = new GenerateDnaEvent { Owner = ent.Owner, DNA = ent.Comp.DNA }; + RaiseLocalEvent(ent.Owner, ref ev); + Dirty(ent); + } + } + + [SubscribeLocalEvent] + private void OnBeingGibbed(Entity ent, ref GibbedBeforeDeletionEvent args) + { + var dna = Loc.GetString("forensics-dna-unknown"); + + if (TryComp(ent, out DnaComponent? dnaComp) && dnaComp.DNA != null) + dna = dnaComp.DNA; + + foreach (var part in args.Giblets) + { + var partComp = EnsureComp(part); + partComp.DNAs.Add(dna); + partComp.CanDnaBeCleaned = false; + Dirty(part, partComp); + } + } + + [SubscribeLocalEvent] + private void OnMeleeHit(Entity weapon, ref MeleeHitEvent args) + { + if ((!args.BaseDamage.DamageDict.TryGetValue("Blunt", out var bluntDamage) || bluntDamage.Value <= 0) && + (!args.BaseDamage.DamageDict.TryGetValue("Slash", out var slashDamage) || slashDamage.Value <= 0) && + (!args.BaseDamage.DamageDict.TryGetValue("Piercing", out var pierceDamage) || pierceDamage.Value <= 0)) + return; + + foreach (var hitEntity in args.HitEntities) + { + if (TryComp(hitEntity, out var hitEntityComp) && hitEntityComp.DNA != null) + weapon.Comp.DNAs.Add(hitEntityComp.DNA); + } + Dirty(weapon); + } + + [SubscribeLocalEvent] + private void OnRehydrated(Entity ent, ref GotRehydratedEvent args) + { + CopyForensicsFrom(ent.Owner, args.Target); + } /// - /// Give the entity a new, random fingerprint string. - /// Does nothing if it does not have the FingerprintComponent. + /// Copy forensic information from a source entity to a destination. + /// Existing forensic information on the target is still kept. /// - public virtual void RandomizeFingerprint(Entity ent) { } + public void CopyForensicsFrom(Entity src, EntityUid target) + { + if (!Resolve(target, ref src.Comp, false)) + { + return; + } + + var targetComp = EnsureComp(target); + foreach (var dna in src.Comp.DNAs) + { + targetComp.DNAs.Add(dna); + } + + foreach (var fiber in src.Comp.Fibers) + { + targetComp.Fibers.Add(fiber); + } + + foreach (var print in src.Comp.Fingerprints) + { + targetComp.Fingerprints.Add(print); + } + + foreach (var residue in src.Comp.Residues) + { + targetComp.Residues.Add(residue); + } + + Dirty(target, targetComp); + } + + public List GetSolutionsDNA(EntityUid uid) + { + List list = new(); + foreach (var (_, soln) in _solutionContainerSystem.EnumerateSolutions(uid)) + { + list.AddRange(GetSolutionsDNA(soln.Comp.Solution)); + } + return list; + } + + public List GetSolutionsDNA(Solution soln) + { + List list = []; + foreach (var reagent in soln.Contents) + { + foreach (var data in reagent.Reagent.EnsureReagentData()) + { + if (data is DnaData) + { + list.Add(((DnaData) data).DNA); + } + } + } + return list; + } + + // IngestionSystem is for stopping the player from feeding soap when trying to clean someone. + [SubscribeLocalEvent(before: [typeof(IngestionSystem)], after: [typeof(SharedAbsorbentSystem)])] + private void OnAfterInteract(Entity cleanForensicsEntity, ref AfterInteractEvent args) + { + if (args.Handled || !args.CanReach || args.Target == null) + return; + + args.Handled = TryStartCleaning(cleanForensicsEntity, args.User, args.Target.Value); + } + + [SubscribeLocalEvent] + private void OnUtilityVerb(Entity entity, ref GetVerbsEvent args) + { + if (!args.CanInteract || !args.CanAccess) + return; + + // These need to be set outside for the anonymous method! + var user = args.User; + var target = args.Target; + + var verb = new UtilityVerb + { + Act = () => TryStartCleaning(entity, user, target), + Icon = new SpriteSpecifier.Texture(new ResPath("/Textures/Interface/VerbIcons/bubbles.svg.192dpi.png")), + Text = Loc.GetString("forensics-verb-text"), + Message = Loc.GetString("forensics-verb-message"), + // This is important because if its true using the cleaning device will count as touching the object. + DoContactInteraction = false, + }; + + args.Verbs.Add(verb); + } /// - /// Transfer DNA from one entity onto the forensics of another. + /// Attempts to clean the given item with the given CleansForensics entity. /// - /// The entity receiving the DNA. - /// The entity applying its DNA. - /// If this DNA be cleaned off of the recipient. e.g. cleaning a knife vs cleaning a puddle of blood. - public virtual void TransferDna(EntityUid recipient, EntityUid donor, bool canDnaBeCleaned = true) { } + /// The entity that is being used to clean the target. + /// The user that is using the cleanForensicsEntity. + /// The target of the forensics clean. + /// True if the target can be cleaned and has some sort of DNA or fingerprints / fibers and false otherwise. + public bool TryStartCleaning(Entity cleanForensicsEntity, EntityUid user, EntityUid target) + { + if (!TryComp(target, out var forensicsComp)) + { + _popupSystem.PopupEntity(Loc.GetString("forensics-cleaning-cannot-clean", ("target", Identity.Entity(target, EntityManager))), user, user, PopupType.MediumCaution); + return false; + } + var totalPrintsAndFibers = forensicsComp.Fingerprints.Count + forensicsComp.Fibers.Count; + var hasRemovableDNA = forensicsComp.DNAs.Count > 0 && forensicsComp.CanDnaBeCleaned; + + if (hasRemovableDNA || totalPrintsAndFibers > 0) + { + var cleanDelay = cleanForensicsEntity.Comp.CleanDelay; + var doAfterArgs = new DoAfterArgs(EntityManager, user, cleanDelay, new CleanForensicsDoAfterEvent(), cleanForensicsEntity, target: target, used: cleanForensicsEntity) + { + NeedHand = true, + BreakOnDamage = true, + BreakOnMove = true, + MovementThreshold = 0.01f, + DistanceThreshold = forensicsComp.CleanDistance, + }; + + _doAfterSystem.TryStartDoAfter(doAfterArgs); + + var userPopupText = Loc.GetString("forensics-cleaning-user", ("target", Identity.Entity(target, EntityManager))); + var othersPopupText = Loc.GetString("forensics-cleaning-others", ("user", Identity.Entity(user, EntityManager)), ("target", Identity.Entity(target, EntityManager))); + _popupSystem.PopupEntity(userPopupText, othersPopupText, user, user); + + return true; + } + + _popupSystem.PopupEntity(Loc.GetString("forensics-cleaning-cannot-clean", ("target", Identity.Entity(target, EntityManager))), user, user, PopupType.MediumCaution); + return false; + } + + [SubscribeLocalEvent] + private void OnCleanForensicsDoAfter(Entity component, ref CleanForensicsDoAfterEvent args) + { + if (args.Handled || args.Cancelled || args.Args.Target == null) + return; + + if (!TryComp(args.Target, out var targetComp)) + return; + + targetComp.Fibers = []; + targetComp.Fingerprints = []; + + if (targetComp.CanDnaBeCleaned) + targetComp.DNAs = []; + + // leave behind evidence it was cleaned + if (TryComp(args.Used, out var fiber)) + targetComp.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 (TryComp(args.Used, out var residue)) + targetComp.Residues.Add(string.IsNullOrEmpty(residue.ResidueColor) ? Loc.GetString("forensic-residue", ("adjective", residue.ResidueAdjective)) : Loc.GetString("forensic-residue-colored", ("color", residue.ResidueColor), ("adjective", residue.ResidueAdjective))); + + Dirty(args.Target.Value, targetComp); + } + + public string GenerateFingerprint() + { + var fingerprint = new byte[16]; + _random.NextBytes(fingerprint); + return Convert.ToHexString(fingerprint); + } + + public string GenerateDNA() + { + var letters = new[] { "A", "C", "G", "T" }; + var DNA = string.Empty; + + for (var i = 0; i < 16; i++) + { + DNA += letters[_random.Next(letters.Length)]; + } + + return DNA; + } + + private void ApplyEvidence(EntityUid user, EntityUid target) + { + if (HasComp(target)) + return; + + 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 (TryComp(user, out var fingerprint) && CanAccessFingerprint(user, out _)) + component.Fingerprints.Add(fingerprint.Fingerprint ?? ""); + + Dirty(target, component); + } + + #region PublicAPI + + public void RandomizeDNA(Entity ent) + { + if (!Resolve(ent, ref ent.Comp, false)) + return; + + ent.Comp.DNA = GenerateDNA(); + + var ev = new GenerateDnaEvent { Owner = ent.Owner, DNA = ent.Comp.DNA }; + RaiseLocalEvent(ent.Owner, ref ev); + Dirty(ent); + } + + public void RandomizeFingerprint(Entity ent) + { + if (!Resolve(ent, ref ent.Comp, false)) + return; + + ent.Comp.Fingerprint = GenerateFingerprint(); + Dirty(ent); + } + + /// + /// Transfer DNA from one entity onto the forensics of another + /// + /// The entity receiving the DNA + /// The entity applying its DNA + /// If this DNA be cleaned off of the recipient. e.g. cleaning a knife vs cleaning a puddle of blood + public void TransferDna(EntityUid recipient, EntityUid donor, bool canDnaBeCleaned = true) + { + if (!TryComp(donor, out var donorComp) || donorComp.DNA == null) + return; + + EnsureComp(recipient, out var recipientComp); + recipientComp.DNAs.Add(donorComp.DNA); + recipientComp.CanDnaBeCleaned = canDnaBeCleaned; + + Dirty(recipient, recipientComp); + } + + /// + /// Checks if there's a way to access the fingerprint of the target entity. + /// + /// The entity with the fingerprint + /// The entity that blocked accessing the fingerprint + public bool CanAccessFingerprint(EntityUid target, out EntityUid? blocker) + { + var ev = new TryAccessFingerprintEvent(); + + RaiseLocalEvent(target, ev); + if (!ev.Cancelled && TryComp(target, out var inv)) + _inventory.RelayEvent((target, inv), ev); + + blocker = ev.Blocker; + return !ev.Cancelled; + } + #endregion } diff --git a/Content.Shared/Implants/SharedImplanterSystem.cs b/Content.Shared/Implants/SharedImplanterSystem.cs index ea22c2941e..3a1a2d616e 100644 --- a/Content.Shared/Implants/SharedImplanterSystem.cs +++ b/Content.Shared/Implants/SharedImplanterSystem.cs @@ -5,6 +5,7 @@ using Content.Shared.Damage.Systems; using Content.Shared.DoAfter; using Content.Shared.Examine; using Content.Shared.Forensics; +using Content.Shared.Forensics.Systems; using Content.Shared.IdentityManagement; using Content.Shared.Implants.Components; using Content.Shared.Interaction; @@ -33,6 +34,7 @@ public abstract partial class SharedImplanterSystem : EntitySystem [Dependency] private SharedContainerSystem _container = default!; [Dependency] private SharedDoAfterSystem _doAfter = default!; [Dependency] private SharedPopupSystem _popup = default!; + [Dependency] private SharedForensicsSystem _forensics = default!; [Dependency] private SharedUserInterfaceSystem _ui = default!; [Dependency] private EntityQuery _implantCompQuery; @@ -286,8 +288,7 @@ public abstract partial class SharedImplanterSystem : EntitySystem else ImplantMode(ent); - var ev = new TransferDnaEvent { Donor = target, Recipient = ent.Owner }; - RaiseLocalEvent(target, ref ev); + _forensics.TransferDna(ent, target); Dirty(ent); } @@ -435,8 +436,7 @@ public abstract partial class SharedImplanterSystem : EntitySystem _container.Remove(implant, implantContainer); _container.Insert(implant, implanterContainer); - var ev = new TransferDnaEvent { Donor = target, Recipient = implanter }; - RaiseLocalEvent(target, ref ev); + _forensics.TransferDna(implanter, target); } /// diff --git a/Content.Shared/Medical/Cryogenics/SharedCryoPodSystem.cs b/Content.Shared/Medical/Cryogenics/SharedCryoPodSystem.cs index b4630ffb9b..60a088e0b8 100644 --- a/Content.Shared/Medical/Cryogenics/SharedCryoPodSystem.cs +++ b/Content.Shared/Medical/Cryogenics/SharedCryoPodSystem.cs @@ -42,7 +42,7 @@ public abstract partial class SharedCryoPodSystem : EntitySystem [Dependency] private MobStateSystem _mobState = default!; [Dependency] private ReactiveSystem _reactive = default!; [Dependency] protected SharedAppearanceSystem Appearance = default!; - [Dependency] private SharedBloodstreamSystem _bloodstream = default!; + [Dependency] private BloodstreamSystem _bloodstream = default!; [Dependency] private SharedContainerSystem _container = default!; [Dependency] private SharedDoAfterSystem _doAfter = default!; [Dependency] private SharedHandsSystem _hands = default!; diff --git a/Content.Shared/Medical/Healing/HealingSystem.cs b/Content.Shared/Medical/Healing/HealingSystem.cs index f7eaffedc1..7f8b61cbea 100644 --- a/Content.Shared/Medical/Healing/HealingSystem.cs +++ b/Content.Shared/Medical/Healing/HealingSystem.cs @@ -24,7 +24,7 @@ public sealed partial class HealingSystem : EntitySystem [Dependency] private SharedAudioSystem _audio = default!; [Dependency] private ISharedAdminLogManager _adminLogger = default!; [Dependency] private DamageableSystem _damageable = default!; - [Dependency] private SharedBloodstreamSystem _bloodstreamSystem = default!; + [Dependency] private BloodstreamSystem _bloodstreamSystem = default!; [Dependency] private SharedDoAfterSystem _doAfter = default!; [Dependency] private SharedStackSystem _stacks = default!; [Dependency] private SharedInteractionSystem _interactionSystem = default!; diff --git a/Content.Shared/Medical/VomitSystem.cs b/Content.Shared/Medical/VomitSystem.cs index dc221906ff..70bf6d629f 100644 --- a/Content.Shared/Medical/VomitSystem.cs +++ b/Content.Shared/Medical/VomitSystem.cs @@ -26,7 +26,7 @@ public sealed partial class VomitSystem : EntitySystem [Dependency] private MobStateSystem _mobState = default!; [Dependency] private MovementModStatusSystem _movementMod = default!; [Dependency] private SharedAudioSystem _audio = default!; - [Dependency] private SharedBloodstreamSystem _bloodstream = default!; + [Dependency] private BloodstreamSystem _bloodstream = default!; [Dependency] private SharedForensicsSystem _forensics = default!; [Dependency] private SharedPopupSystem _popup = default!; [Dependency] private SharedPuddleSystem _puddle = default!; diff --git a/Content.Shared/Nutrition/EntitySystems/IngestionSystem.cs b/Content.Shared/Nutrition/EntitySystems/IngestionSystem.cs index add2ce186a..b4a10f8011 100644 --- a/Content.Shared/Nutrition/EntitySystems/IngestionSystem.cs +++ b/Content.Shared/Nutrition/EntitySystems/IngestionSystem.cs @@ -10,7 +10,7 @@ using Content.Shared.Database; using Content.Shared.Destructible; using Content.Shared.DoAfter; using Content.Shared.FixedPoint; -using Content.Shared.Forensics; +using Content.Shared.Forensics.Systems; using Content.Shared.Hands.EntitySystems; using Content.Shared.IdentityManagement; using Content.Shared.Interaction; @@ -56,6 +56,7 @@ public sealed partial class IngestionSystem : EntitySystem [Dependency] private SharedPopupSystem _popup = default!; [Dependency] private SharedSolutionContainerSystem _solutionContainer = default!; [Dependency] private SharedTransformSystem _transform = default!; + [Dependency] private SharedForensicsSystem _forensics = default!; // Body Component Dependencies [Dependency] private BodySystem _body = default!; @@ -497,13 +498,7 @@ public sealed partial class IngestionSystem : EntitySystem if (!IsEmpty(entity)) { // Leave some of the consumer's DNA on the consumed item... - var ev = new TransferDnaEvent - { - Donor = args.Target, - Recipient = entity, - CanDnaBeCleaned = false, - }; - RaiseLocalEvent(args.Target, ref ev); + _forensics.TransferDna(entity, args.Target, false); args.Repeat = !args.ForceFed; return; diff --git a/Content.Shared/Rootable/RootableSystem.cs b/Content.Shared/Rootable/RootableSystem.cs index 14c445a291..62c52ef13f 100644 --- a/Content.Shared/Rootable/RootableSystem.cs +++ b/Content.Shared/Rootable/RootableSystem.cs @@ -39,7 +39,7 @@ public sealed partial class RootableSystem : EntitySystem [Dependency] private ReactiveSystem _reactive = default!; [Dependency] private SharedActionsSystem _actions = default!; [Dependency] private SharedAudioSystem _audio = default!; - [Dependency] private SharedBloodstreamSystem _blood = default!; + [Dependency] private BloodstreamSystem _blood = default!; [Dependency] private SharedGravitySystem _gravity = default!; [Dependency] private SharedPhysicsSystem _physics = default!; [Dependency] private SharedSolutionContainerSystem _solutionContainer = default!; diff --git a/Resources/Locale/en-US/forensics/forensics.ftl b/Resources/Locale/en-US/forensics/forensics.ftl index 6eae96f27d..46da5f2a84 100644 --- a/Resources/Locale/en-US/forensics/forensics.ftl +++ b/Resources/Locale/en-US/forensics/forensics.ftl @@ -25,6 +25,7 @@ forensics-dna-unknown = unknown DNA forensics-verb-text = Remove evidence forensics-verb-message = Remove fingerprints and DNA residues from the object! -forensics-cleaning = You begin cleaning the evidence off of {THE($target)}... +forensics-cleaning-user = You begin cleaning the evidence off of {THE($target)}... +forensics-cleaning-others = {CAPITALIZE($user)} begins cleaning the evidence off of {THE($target)}. forensics-cleaning-cannot-clean = There is nothing cleanable on {THE($target)}!