Predict Forensics V2.0 (Ready for Review) (#40693)

* What sitting on code does to a branch + no mo owner v2

* Replacing ViewVariables with AutoNetworkedField + Missed One V2

* Removing Virtual Methods & Renaming ForensicsSystem to SharedForensicsSystem (Also made it abstract) V2

* IProduceWidgets Review Changes & some of my own V2

* Lil Cleanup V2

* Part 1 of Review Changes (Only thing left is the BUI stuff and the constructor) V2

* It now WORKS!!! (Still needs the BUI and constuctor changes, ill figure it out later)

* Test (Removal of ServerSide VomitSystem.cs)

* Ok...

* Moved System BloodstreamSystem.cs to SharedBloodstreamSystem.cs & deleted client BloodStreamSystem.cs to make SharedBloodstreamSystem.cs sealed. (No Unregistered errors)

* Prediction

* Fix?

* No mo log error?

* Revert "No mo log error?"

This reverts commit 50c15a3deab6af9dffd7baed45820bae5558e41f.

* Constructor going up

* No ForensicScannerBoundUserInterface.cs (It doesnt wanna scan anymore but it doesnt crash)

* For Conflicts

* Guh

* Yea

* Organization & no ErrorHell (maybe)

* Dirtying

* Dirtying take 2

* Revert "Dirtying take 2"

This reverts commit 5735012e21f43808db3ae010fce268eac24d0583.

* the dirties dont do much here

* For Conflict

* Readded

* Another Conflict Commit Fix

* Conflict Fix Thrice

* Empty Space

* Keep it simple

* Add it back in

* Move BloodstreamSystem stuff thats causing the error back to system. This'll prob fix the conflict

* Conflict fix?

* Trust the fix

* Looked at other prediction PR's, they had better code.

* To not mix up the system in client

* Removing Conflict

* For conflict

* For the conflic fix, will be removed later

* Scuffed Fix

* Removal & Adding the Decouple

* Took some HEAVY pointers from SirWarocks work. The Roadblock has been CLEARED, DOAFTERS WORK NOW!!

* Cleanup & Optimizing, mostly comps.

* More Optimization, most of this is just imported Warock Code, his QOL included. Did not want to remove SolutionDNA's tho, mostly due to a Log.Debug in OnPrint.

* Forgot that the Print Refreshes, more of Warocks work.

* Removing Loc.GetString

* All of BloodStream to Shared, hopefully no errors.

* Sketchy Dirty Removed

* Removed Timer.Spawn, added FrameUpdate cause anything else only updated when the window opened, & a bit of cleanup.

* No mo Mispredict

* GenerateDnaEvent Predicted

* Forgot the user

* I knew it was real, bloodstutter is REAL and now GONE.

* Cant dirty NewEntityBloodData due to potential stutter, so its safer to put it all back into server. again...

* Clearer

* Thank you leahcat, no more identity leak.

* Routine Maintenance

* sealing the class

* it checks for this now?!

* While im here, FileScope Namespaces & removing unused namespaces

* Update Attribute Type (SirWarock Import), it works and is much easier to read.

* Forever Fixing Conflicts

* Readonly Genocide & Partial Maxing

* LGTM, BloodstreamSystem moved to SharedBloodstreamSystem!

* Sohn

* If OG Github back push this commit

* One Billionth Commit Fix

* Removed client/server stuff

* Made implicit

* NewLine & comment on why AutoGenCompState is true

* Updatin the rest of the Popups to use Entity

* I think that's what they meant? It looks cleaner at least.

* Mass Attribute for LocalEvents that are safe to change.

* Putting it back as abstract

* maybe?

* Guhh, rolling back empty client/server of BloodStream

* Removed Client/Server BloodstreamSystems, changed access modifier from protected to private, & renamed SharedBloodstreamSystem to BloodstreamSystem

* From Protected to Private

* Removed comment & made it explicit on what changed to true

* Guh, hopefully thats all of it...

* Forgot

* Review

* Test Push

* Line 316 in SharedForensicsSystem was causing the errors, better to play it safe

* Review Changes

* Update Content.Shared/Forensics/Events.cs

Co-authored-by: Whatstone <166147148+whatston3@users.noreply.github.com>

* The only thing SolutionDNAs does is search and clears itself. I see no reason to keep it

* The print.disabled didnt properly disable when cleared. So ill just use the old if check

* Figured out the rest of the SubscribeLocalEvents and networking BloodData cause its fully in Shared

---------

Co-authored-by: Whatstone <166147148+whatston3@users.noreply.github.com>
This commit is contained in:
Prole 2026-08-08 15:48:36 -07:00 • committed by GitHub
parent 8ad82dc665
commit 502af6955d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
52 changed files with 1178 additions and 1300 deletions

View file

@ -1,5 +0,0 @@
using Content.Shared.Body.Systems;
namespace Content.Client.Body.Systems;
public sealed class BloodstreamSystem : SharedBloodstreamSystem;

View file

@ -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<ForensicScannerMenu>();
_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<ForensicScannerMenu>();
_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);
}
}

View file

@ -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();
}
}

View file

@ -1,5 +0,0 @@
using Content.Shared.Forensics.Systems;
namespace Content.Client.Forensics.Systems;
public sealed class ForensicsSystem : SharedForensicsSystem;

View file

@ -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;

View file

@ -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<BloodstreamComponent, GenerateDnaEvent>(OnDnaGenerated);
}
// forensics is not predicted yet
private void OnDnaGenerated(Entity<BloodstreamComponent> 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> 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");
}
}

View file

@ -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;

View file

@ -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<ForensicsComponent> 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<StoreComponent> ent, ref CloningItemEvent args)

View file

@ -24,6 +24,9 @@
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
<ItemGroup>
<Folder Include="EntityEffects\" />
</ItemGroup>
<Import Project="..\RobustToolbox\MSBuild\Robust.Properties.targets" />
<Import Project="..\RobustToolbox\MSBuild\Robust.EntitySystemSubscriptionsGenerator.targets" />

View file

@ -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;

View file

@ -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;

View file

@ -1,9 +0,0 @@
namespace Content.Server.Forensics;
/// <summary>
/// This component stops the entity from leaving finger prints,
/// usually so fibres can be left instead.
/// </summary>
[RegisterComponent]
public sealed partial class DnaSubstanceTraceComponent : Component
{ }

View file

@ -1,16 +0,0 @@
namespace Content.Server.Forensics
{
/// <summary>
/// This controls fibers left by gloves on items,
/// which the forensics system uses.
/// </summary>
[RegisterComponent]
public sealed partial class FiberComponent : Component
{
[DataField]
public LocId FiberMaterial = "fibers-synthetic";
[DataField]
public string? FiberColor;
}
}

View file

@ -1,15 +0,0 @@
namespace Content.Server.Forensics
{
/// <summary>
/// Used to take a sample of someone's fingerprints.
/// </summary>
[RegisterComponent]
public sealed partial class ForensicPadComponent : Component
{
[DataField("scanDelay")]
public float ScanDelay = 3.0f;
public bool Used = false;
public String Sample = string.Empty;
}
}

View file

@ -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;
/// <summary>
/// A list of fingerprint GUIDs that the forensic scanner found from the <see cref="ForensicsComponent"/> on an entity.
/// </summary>
[ViewVariables(VVAccess.ReadOnly), DataField("fingerprints")]
public List<string> Fingerprints = new();
/// <summary>
/// A list of glove fibers that the forensic scanner found from the <see cref="ForensicsComponent"/> on an entity.
/// </summary>
[ViewVariables(VVAccess.ReadOnly), DataField("fibers")]
public List<string> Fibers = new();
/// <summary>
/// DNA that the forensic scanner found from the <see cref="DNAComponent"/> on an entity.
/// </summary>
[ViewVariables(VVAccess.ReadOnly), DataField("dnas")]
public List<string> TouchDNAs = new();
/// <summary>
/// DNA that the forensic scanner found from the solution containers in an entity.
/// </summary>
[ViewVariables(VVAccess.ReadOnly), DataField]
public List<string> SolutionDNAs = new();
/// <summary>
/// Residue that the forensic scanner found from the <see cref="ForensicsComponent"/> on an entity.
/// </summary>
[ViewVariables(VVAccess.ReadOnly), DataField("residues")]
public List<string> Residues = new();
/// <summary>
/// What is the name of the entity that was scanned last?
/// </summary>
/// <remarks>
/// This will be used for the title of the printout and displayed to players.
/// </remarks>
[ViewVariables(VVAccess.ReadOnly)]
public string LastScannedName = string.Empty;
/// <summary>
/// When will the scanner be ready to print again?
/// </summary>
[ViewVariables(VVAccess.ReadOnly)]
public TimeSpan PrintReadyAt = TimeSpan.Zero;
/// <summary>
/// The time (in seconds) that it takes to scan an entity.
/// </summary>
[DataField("scanDelay")]
public float ScanDelay = 3.0f;
/// <summary>
/// How often can the scanner print out reports?
/// </summary>
[DataField("printCooldown")]
public TimeSpan PrintCooldown = TimeSpan.FromSeconds(5);
/// <summary>
/// The sound that's played when there's a match between a scan and an
/// inserted forensic pad.
/// </summary>
[DataField("soundMatch")]
public SoundSpecifier SoundMatch = new SoundPathSpecifier("/Audio/Machines/Nuke/angry_beep.ogg");
/// <summary>
/// The sound that's played when there's no match between a scan and an
/// inserted forensic pad.
/// </summary>
[DataField("soundNoMatch")]
public SoundSpecifier SoundNoMatch = new SoundPathSpecifier("/Audio/Machines/airlock_deny.ogg");
/// <summary>
/// The sound that's played when the scanner prints off a report.
/// </summary>
[DataField("soundPrint")]
public SoundSpecifier SoundPrint = new SoundPathSpecifier("/Audio/Machines/short_print_and_rip.ogg");
/// <summary>
/// What the machine will print
/// </summary>
[DataField("machineOutput", customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>))]
public string MachineOutput = "ForensicReportPaper";
}
}

View file

@ -1,31 +0,0 @@
namespace Content.Server.Forensics
{
[RegisterComponent]
public sealed partial class ForensicsComponent : Component
{
[DataField("fingerprints")]
public HashSet<string> Fingerprints = new();
[DataField("fibers")]
public HashSet<string> Fibers = new();
[DataField("dnas")]
public HashSet<string> DNAs = new();
[DataField("residues")]
public HashSet<string> Residues = new();
/// <summary>
/// How close you must be to wipe the prints/blood/etc. off of this entity
/// </summary>
[DataField("cleanDistance")]
public float CleanDistance = 1.5f;
/// <summary>
/// Can the DNA be cleaned off of this entity?
/// e.g. you can wipe the DNA off of a knife, but not a cigarette
/// </summary>
[DataField("canDnaBeCleaned")]
public bool CanDnaBeCleaned = true;
}
}

View file

@ -1,15 +0,0 @@
namespace Content.Server.Forensics;
/// <summary>
/// This controls residues left on items
/// which the forensics system uses.
/// </summary>
[RegisterComponent]
public sealed partial class ResidueComponent : Component
{
[DataField]
public LocId ResidueAdjective = "residue-unknown";
[DataField]
public string? ResidueColor;
}

View file

@ -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<FingerprintMaskComponent, InventoryRelayedEvent<TryAccessFingerprintEvent>>(OnTryAccessFingerprint);
}
private void OnTryAccessFingerprint(EntityUid uid, FingerprintMaskComponent comp, ref InventoryRelayedEvent<TryAccessFingerprintEvent> args)
{
if (args.Args.Cancelled)
return;
args.Args.Blocker = uid;
args.Args.Cancel();
}
}

View file

@ -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
{
/// <summary>
/// Used to transfer fingerprints from entities to forensic pads.
/// </summary>
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<ForensicPadComponent, ExaminedEvent>(OnExamined);
SubscribeLocalEvent<ForensicPadComponent, AfterInteractEvent>(OnAfterInteract);
SubscribeLocalEvent<ForensicPadComponent, ForensicPadDoAfterEvent>(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<ForensicScannerComponent>(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<FingerprintComponent>(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<FiberComponent>(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;
}
}
}

View file

@ -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<TagPrototype> DNASolutionScannableTag = "DNASolutionScannable";
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<ForensicScannerComponent, AfterInteractEvent>(OnAfterInteract);
SubscribeLocalEvent<ForensicScannerComponent, AfterInteractUsingEvent>(OnAfterInteractUsing);
SubscribeLocalEvent<ForensicScannerComponent, BeforeActivatableUIOpenEvent>(OnBeforeActivatableUIOpen);
SubscribeLocalEvent<ForensicScannerComponent, GetVerbsEvent<UtilityVerb>>(OnUtilityVerb);
SubscribeLocalEvent<ForensicScannerComponent, ForensicScannerPrintMessage>(OnPrint);
SubscribeLocalEvent<ForensicScannerComponent, ForensicScannerClearMessage>(OnClear);
SubscribeLocalEvent<ForensicScannerComponent, ForensicScannerDoAfterEvent>(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<ForensicsComponent>(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));
}
/// <remarks>
/// Hosts logic common between OnUtilityVerb and OnAfterInteract.
/// </remarks>
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<UtilityVerb> 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<ForensicPadComponent>(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<ForensicScannerComponent> 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<PaperComponent>(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);
}
}
}

View file

@ -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<HandsComponent, ContactInteractionEvent>(OnInteract);
SubscribeLocalEvent<FingerprintComponent, MapInitEvent>(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<DnaComponent, MapInitEvent>(OnDNAInit, after: new[] { typeof(BloodstreamSystem) });
SubscribeLocalEvent<ForensicsComponent, GibbedBeforeDeletionEvent>(OnBeingGibbed);
SubscribeLocalEvent<ForensicsComponent, MeleeHitEvent>(OnMeleeHit);
SubscribeLocalEvent<ForensicsComponent, GotRehydratedEvent>(OnRehydrated);
SubscribeLocalEvent<CleansForensicsComponent, AfterInteractEvent>(OnAfterInteract, after: new[] { typeof(AbsorbentSystem) });
SubscribeLocalEvent<ForensicsComponent, CleanForensicsDoAfterEvent>(OnCleanForensicsDoAfter);
SubscribeLocalEvent<DnaComponent, TransferDnaEvent>(OnTransferDnaEvent);
SubscribeLocalEvent<DnaSubstanceTraceComponent, SolutionChangedEvent>(OnSolutionChanged);
SubscribeLocalEvent<CleansForensicsComponent, GetVerbsEvent<UtilityVerb>>(OnUtilityVerb);
}
private void OnSolutionChanged(Entity<DnaSubstanceTraceComponent> ent, ref SolutionChangedEvent ev)
{
var soln = GetSolutionsDNA(ev.Solution);
if (soln.Count > 0)
{
var comp = EnsureComp<ForensicsComponent>(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<FingerprintComponent> ent, ref MapInitEvent args)
{
if (ent.Comp.Fingerprint == null)
RandomizeFingerprint((ent.Owner, ent.Comp));
}
private void OnDNAInit(Entity<DnaComponent> 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<ForensicsComponent> 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<ForensicsComponent>(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<DnaComponent>(hitEntity, out var hitEntityComp) && hitEntityComp.DNA != null)
component.DNAs.Add(hitEntityComp.DNA);
}
}
}
private void OnRehydrated(Entity<ForensicsComponent> ent, ref GotRehydratedEvent args)
{
CopyForensicsFrom(ent.Comp, args.Target);
}
/// <summary>
/// Copy forensic information from a source entity to a destination.
/// Existing forensic information on the target is still kept.
/// </summary>
public void CopyForensicsFrom(ForensicsComponent src, EntityUid target)
{
var dest = EnsureComp<ForensicsComponent>(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<string> GetSolutionsDNA(EntityUid uid)
{
List<string> list = new();
foreach (var (_, soln) in _solutionContainerSystem.EnumerateSolutions(uid))
{
list.AddRange(GetSolutionsDNA(soln.Comp.Solution));
}
return list;
}
public List<string> GetSolutionsDNA(Solution soln)
{
List<string> 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<CleansForensicsComponent> 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<CleansForensicsComponent> entity, ref GetVerbsEvent<UtilityVerb> 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);
}
/// <summary>
/// Attempts to clean the given item with the given CleansForensics entity.
/// </summary>
/// <param name="cleanForensicsEntity">The entity that is being used to clean the target.</param>
/// <param name="user">The user that is using the cleanForensicsEntity.</param>
/// <param name="target">The target of the forensics clean.</param>
/// <returns>True if the target can be cleaned and has some sort of DNA or fingerprints / fibers and false otherwise.</returns>
public bool TryStartCleaning(Entity<CleansForensicsComponent> cleanForensicsEntity, EntityUid user, EntityUid target)
{
if (!TryComp<ForensicsComponent>(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<ForensicsComponent>(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<FiberComponent>(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<ResidueComponent>(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<IgnoresFingerprintsComponent>(target))
return;
var component = EnsureComp<ForensicsComponent>(target);
if (_inventory.TryGetSlotEntity(user, "gloves", out var gloves))
{
if (TryComp<FiberComponent>(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<FingerprintComponent>(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 <see cref="TransferDna"/> 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<ForensicsComponent>(args.Recipient);
recipientComp.DNAs.Add(component.DNA);
recipientComp.CanDnaBeCleaned = args.CanDnaBeCleaned;
}
#region Public API
public override void RandomizeDNA(Entity<DnaComponent?> 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<FingerprintComponent?> 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<DnaComponent>(donor, out var donorComp) && donorComp.DNA != null)
{
EnsureComp<ForensicsComponent>(recipient, out var recipientComp);
recipientComp.DNAs.Add(donorComp.DNA);
recipientComp.CanDnaBeCleaned = canDnaBeCleaned;
}
}
/// <summary>
/// Checks if there's a way to access the fingerprint of the target entity.
/// </summary>
/// <param name="target">The entity with the fingerprint</param>
/// <param name="blocker">The entity that blocked accessing the fingerprint</param>
public bool CanAccessFingerprint(EntityUid target, out EntityUid? blocker)
{
var ev = new TryAccessFingerprintEvent();
RaiseLocalEvent(target, ev);
if (!ev.Cancelled && TryComp<InventoryComponent>(target, out var inv))
_inventory.RelayEvent((target, inv), ev);
blocker = ev.Blocker;
return !ev.Cancelled;
}
#endregion
}
}

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -17,7 +17,7 @@ namespace Content.Shared.Body.Components;
/// </summary>
[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.
/// </summary>
[ViewVariables(VVAccess.ReadOnly)]
[ViewVariables(VVAccess.ReadOnly), AutoNetworkedField]
public List<ReagentData>? BloodData;
/// <summary>

View file

@ -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<BloodstreamComponent, MapInitEvent>(OnMapInit);
SubscribeLocalEvent<BloodstreamComponent, EntRemovedFromContainerMessage>(OnEntRemoved);
SubscribeLocalEvent<BloodstreamComponent, ReactionAttemptEvent>(OnReactionAttempt);
SubscribeLocalEvent<BloodstreamComponent, SolutionRelayEvent<ReactionAttemptEvent>>(OnReactionAttempt);
SubscribeLocalEvent<BloodstreamComponent, DamageChangedEvent>(OnDamageChanged);
SubscribeLocalEvent<BloodstreamComponent, HealthBeingExaminedEvent>(OnHealthBeingExamined);
SubscribeLocalEvent<BloodstreamComponent, GibbedBeforeDeletionEvent>(OnBeingGibbed);
SubscribeLocalEvent<BloodstreamComponent, ApplyMetabolicMultiplierEvent>(OnApplyMetabolicMultiplier);
SubscribeLocalEvent<BloodstreamComponent, RejuvenateEvent>(OnRejuvenate);
SubscribeLocalEvent<BloodstreamComponent, MetabolismExclusionEvent>(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<BloodstreamComponent> 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<BloodstreamComponent> 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<BloodstreamComponent> 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> 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<BloodstreamComponent> 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<BloodstreamComponent> ent, ref SolutionRelayEvent<ReactionAttemptEvent> 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<BloodstreamComponent> 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
/// <summary>
/// Shows text on health examine, based on bleed rate and blood level.
/// </summary>
[SubscribeLocalEvent]
private void OnHealthBeingExamined(Entity<BloodstreamComponent> 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<BloodstreamComponent> ent, ref GibbedBeforeDeletionEvent args)
{
SpillAllSolutions(ent.AsNullable());
}
[SubscribeLocalEvent]
private void OnApplyMetabolicMultiplier(Entity<BloodstreamComponent> ent, ref ApplyMetabolicMultiplierEvent args)
{
ent.Comp.UpdateIntervalMultiplier = args.Multiplier;
DirtyField(ent, ent.Comp, nameof(BloodstreamComponent.UpdateIntervalMultiplier));
}
[SubscribeLocalEvent]
private void OnRejuvenate(Entity<BloodstreamComponent> 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<BloodstreamComponent> 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<BloodstreamComponent?> 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<BloodstreamComponent?> 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<BloodstreamComponent?> ent, FixedPoint2 quantity, ProtoId<ReagentPrototype>? 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<BloodstreamComponent?> 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<BloodstreamComponent?> 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);
}
/// <summary>
@ -603,7 +616,7 @@ public abstract partial class SharedBloodstreamSystem : EntitySystem
/// <summary>
/// Gets new blood data for this entity and caches it in <see cref="BloodstreamComponent.BloodData"/>
/// </summary>
protected List<ReagentData> NewEntityBloodData(EntityUid uid)
private List<ReagentData> NewEntityBloodData(EntityUid uid)
{
var bloodData = new List<ReagentData>();
var dnaData = new DnaData();

View file

@ -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!;

View file

@ -14,7 +14,7 @@ namespace Content.Shared.EntityEffects.Effects.Body;
/// <inheritdoc cref="EntityEffectSystem{T,TEffect}"/>
public sealed partial class AddReagentToBloodstreamEntityEffectSystem : EntityEffectSystem<BloodstreamComponent, AddReagentToBloodstream>
{
[Dependency] private SharedBloodstreamSystem _bloodstream = default!;
[Dependency] private BloodstreamSystem _bloodstream = default!;
[Dependency] private ReactiveSystem _reactive = default!;
protected override void Effect(Entity<BloodstreamComponent> entity, ref EntityEffectEvent<AddReagentToBloodstream> args)

View file

@ -13,7 +13,7 @@ namespace Content.Shared.EntityEffects.Effects.Body;
/// <inheritdoc cref="EntityEffectSystem{T,TEffect}"/>
public sealed partial class CleanBloodstreamEntityEffectSystem : EntityEffectSystem<BloodstreamComponent, CleanBloodstream>
{
[Dependency] private SharedBloodstreamSystem _bloodstream = default!;
[Dependency] private BloodstreamSystem _bloodstream = default!;
protected override void Effect(Entity<BloodstreamComponent> entity, ref EntityEffectEvent<CleanBloodstream> args)
{

View file

@ -10,7 +10,7 @@ namespace Content.Shared.EntityEffects.Effects.Body;
/// <inheritdoc cref="EntityEffectSystem{T,TEffect}"/>
public sealed partial class ModifyBleedEntityEffectSystem : EntityEffectSystem<BloodstreamComponent, ModifyBleed>
{
[Dependency] private SharedBloodstreamSystem _bloodstream = default!;
[Dependency] private BloodstreamSystem _bloodstream = default!;
protected override void Effect(Entity<BloodstreamComponent> entity, ref EntityEffectEvent<ModifyBleed> args)
{

View file

@ -12,7 +12,7 @@ namespace Content.Shared.EntityEffects.Effects.Body;
/// <inheritdoc cref="EntityEffectSystem{T,TEffect}"/>
public sealed partial class ModifyBloodLevelEntityEffectSystem : EntityEffectSystem<BloodstreamComponent, ModifyBloodLevel>
{
[Dependency] private SharedBloodstreamSystem _bloodstream = default!;
[Dependency] private BloodstreamSystem _bloodstream = default!;
protected override void Effect(Entity<BloodstreamComponent> entity, ref EntityEffectEvent<ModifyBloodLevel> args)
{

View file

@ -1,14 +1,16 @@
namespace Content.Server.Forensics;
using Robust.Shared.GameStates;
namespace Content.Shared.Forensics.Components;
/// <summary>
/// This component is for items that can clean up forensic evidence
/// </summary>
[RegisterComponent]
[RegisterComponent, NetworkedComponent]
public sealed partial class CleansForensicsComponent : Component
{
/// <summary>
/// How long it takes to wipe prints/blood/etc. off of things using this entity
/// </summary>
[DataField]
public float CleanDelay = 12.0f;
public TimeSpan CleanDelay = TimeSpan.FromSeconds(12.0f);
}

View file

@ -0,0 +1,10 @@
using Robust.Shared.GameStates;
namespace Content.Shared.Forensics.Components;
/// <summary>
/// This component stops the entity from leaving finger prints,
/// usually so fibers can be left instead.
/// </summary>
[RegisterComponent, NetworkedComponent]
public sealed partial class DnaSubstanceTraceComponent : Component;

View file

@ -0,0 +1,17 @@
using Robust.Shared.GameStates;
namespace Content.Shared.Forensics.Components;
/// <summary>
/// This controls fibers left by gloves on items,
/// which the forensics system uses.
/// </summary>
[RegisterComponent, NetworkedComponent]
public sealed partial class FiberComponent : Component
{
[DataField]
public LocId FiberMaterial = "fibers-synthetic";
[DataField]
public LocId? FiberColor;
}

View file

@ -0,0 +1,25 @@
using Robust.Shared.GameStates;
namespace Content.Shared.Forensics.Components;
/// <summary>
/// Used to take a sample of someone's fingerprints.
/// </summary>
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
public sealed partial class ForensicPadComponent : Component
{
/// <summary>
/// The amount of time for the pad to be used to take a sample.
/// </summary>
[DataField]
public TimeSpan ScanDelay = TimeSpan.FromSeconds(3.0f);
[AutoNetworkedField]
public bool Used;
/// <summary>
/// The information acquired from using the forensic pad.
/// </summary>
[AutoNetworkedField]
public String Sample = string.Empty;
}

View file

@ -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
{
/// <summary>
/// A list of fingerprint GUIDs that the forensic scanner found from the <see cref="ForensicsComponent"/> on an entity.
/// </summary>
[DataField, AutoNetworkedField]
public List<string> Fingerprints = [];
/// <summary>
/// A list of glove fibers that the forensic scanner found from the <see cref="ForensicsComponent"/> on an entity.
/// </summary>
[DataField, AutoNetworkedField]
public List<string> Fibers = [];
/// <summary>
/// DNA that the forensic scanner found from the <see cref="DnaComponent"/> on an entity.
/// </summary>
[DataField, AutoNetworkedField]
public List<string> DNAs = [];
/// <summary>
/// Residue that the forensic scanner found from the <see cref="ForensicsComponent"/> on an entity.
/// </summary>
[DataField, AutoNetworkedField]
public List<string> Residues = [];
/// <summary>
/// What is the name of the entity that was scanned last?
/// </summary>
/// <remarks>
/// This will be used for the title of the printout and displayed to players.
/// </remarks>
[DataField, AutoNetworkedField]
public string LastScannedName = string.Empty;
/// <summary>
/// When will the scanner be ready to print again?
/// </summary>
[DataField(customTypeSerializer: typeof(TimeOffsetSerializer)), AutoPausedField, AutoNetworkedField]
public TimeSpan PrintReadyAt = TimeSpan.Zero;
/// <summary>
/// The time (in seconds) that it takes to scan an entity.
/// </summary>
[DataField]
public float ScanDelay = 3.0f;
/// <summary>
/// How often can the scanner print out reports?
/// </summary>
[DataField]
public TimeSpan PrintCooldown = TimeSpan.FromSeconds(5);
/// <summary>
/// The sound that's played when there's a match between a scan and an
/// inserted forensic pad.
/// </summary>
[DataField]
public SoundSpecifier SoundMatch = new SoundPathSpecifier("/Audio/Machines/Nuke/angry_beep.ogg");
/// <summary>
/// The sound that's played when there's no match between a scan and an
/// inserted forensic pad.
/// </summary>
[DataField]
public SoundSpecifier SoundNoMatch = new SoundPathSpecifier("/Audio/Machines/airlock_deny.ogg");
/// <summary>
/// The sound that's played when the scanner prints off a report.
/// </summary>
[DataField]
public SoundSpecifier SoundPrint = new SoundPathSpecifier("/Audio/Machines/short_print_and_rip.ogg");
/// <summary>
/// What type of paper the device will print.
/// </summary>
[DataField]
public EntProtoId PaperPrototypeID = "ForensicReportPaper";
}

View file

@ -0,0 +1,47 @@
using Robust.Shared.GameStates;
namespace Content.Shared.Forensics.Components;
/// <summary>
/// Leaves evidence on entities for <see cref="ForensicScannerComponent"/> to find.
/// </summary>
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
public sealed partial class ForensicsComponent : Component
{
/// <summary>
/// Leaves evidence of FingerPrints left by uncovered hands.
/// </summary>
[DataField, AutoNetworkedField]
public HashSet<string> Fingerprints = [];
/// <summary>
/// Leaves evidence of Fibers left by gloves.
/// </summary>
[DataField, AutoNetworkedField]
public HashSet<string> Fibers = [];
/// <summary>
/// Leaves evidence of DNA left by bodily fluids.
/// </summary>
[DataField, AutoNetworkedField]
public HashSet<string> DNAs = [];
/// <summary>
/// Leaves evidence of Residues left by cleaning products.
/// </summary>
[DataField, AutoNetworkedField]
public HashSet<string> Residues = [];
/// <summary>
/// How close you must be to wipe the prints/blood/etc. off of this entity
/// </summary>
[DataField]
public float CleanDistance = 1.5f;
/// <summary>
/// Can the DNA be cleaned off of this entity?
/// e.g. you can wipe the DNA off of a knife, but not a cigarette
/// </summary>
[DataField]
public bool CanDnaBeCleaned = true;
}

View file

@ -1,7 +1,9 @@
namespace Content.Server.Forensics.Components;
using Robust.Shared.GameStates;
namespace Content.Shared.Forensics.Components;
/// <summary>
/// This component is for entities we do not wish to track fingerprints/fibers, like puddles
/// </summary>
[RegisterComponent]
public sealed partial class IgnoresFingerprintsComponent : Component { }
[RegisterComponent, NetworkedComponent]
public sealed partial class IgnoresFingerprintsComponent : Component;

View file

@ -0,0 +1,23 @@
using Robust.Shared.GameStates;
namespace Content.Shared.Forensics.Components;
/// <summary>
/// This controls residues left on items
/// which the forensics system uses.
/// </summary>
[RegisterComponent, NetworkedComponent]
public sealed partial class ResidueComponent : Component
{
/// <summary>
/// Specifies residue, for now only color.
/// </summary>
[DataField]
public LocId ResidueAdjective = "residue-unknown";
/// <summary>
/// Leaves color of cleaning product for ResidueAdjective to use.
/// </summary>
[DataField]
public string? ResidueColor;
}

View file

@ -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
{
}
/// <summary>
/// An event to apply DNA evidence from a donor onto some recipient.
/// </summary>
[ByRefEvent]
public record struct TransferDnaEvent()
{
/// <summary>
/// The entity donating the DNA.
/// </summary>
public EntityUid Donor;
/// <summary>
/// The entity receiving the DNA.
/// </summary>
public EntityUid Recipient;
/// <summary>
/// Can the DNA be cleaned off?
/// </summary>
public bool CanDnaBeCleaned = true;
}
public sealed partial class CleanForensicsDoAfterEvent : SimpleDoAfterEvent;
/// <summary>
/// Raised on an entity when its DNA has been changed.
/// </summary>
[ByRefEvent]
public record struct GenerateDnaEvent()
public record struct GenerateDnaEvent(EntityUid Owner, string DNA)
{
/// <summary>
/// The entity getting new DNA.
/// </summary>
public EntityUid Owner;
public EntityUid Owner = Owner;
/// <summary>
/// The generated DNA.
/// </summary>
public required string DNA;
public string DNA = DNA;
}
/// <summary>

View file

@ -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<string> Fingerprints = new();
public readonly List<string> Fibers = new();
public readonly List<string> TouchDNAs = new();
public readonly List<string> SolutionDNAs = new();
public readonly List<string> Residues = new();
public readonly string LastScannedName = string.Empty;
public readonly TimeSpan PrintCooldown = TimeSpan.Zero;
public readonly TimeSpan PrintReadyAt = TimeSpan.Zero;
public ForensicScannerBoundUserInterfaceState(
List<string> fingerprints,
List<string> fibers,
List<string> touchDnas,
List<string> solutionDnas,
List<string> 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;

View file

@ -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<FingerprintMaskComponent> gloves, ref InventoryRelayedEvent<TryAccessFingerprintEvent> args)
{
if (args.Args.Cancelled)
return;
args.Args.Blocker = gloves.Owner;
args.Args.Cancel();
}
}

View file

@ -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;
/// <summary>
/// Used to transfer fingerprints from entities to forensic pads.
/// </summary>
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<ForensicPadComponent> 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<ForensicPadComponent> pad, ref AfterInteractEvent args)
{
if (!args.CanReach || args.Target == null || HasComp<ForensicScannerComponent>(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<FingerprintComponent>(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<FiberComponent>(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<ForensicPadComponent> 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);
}
}

View file

@ -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<TagPrototype> DNASolutionScannableTag = "DNASolutionScannable";
[SubscribeLocalEvent]
private void OnScannerUpdate(Entity<ForensicScannerComponent> scanner, ref AfterAutoHandleStateEvent args)
{
UpdateUi(scanner);
}
private void UpdateUi(Entity<ForensicScannerComponent> scanner)
{
if (_uiSystem.TryGetOpenUi(scanner.Owner, ForensicScannerUiKey.Key, out var bui))
{
bui.Update();
}
}
[SubscribeLocalEvent]
private void OnDoAfter(Entity<ForensicScannerComponent> scanner, ref ForensicScannerDoAfterEvent args)
{
if (args.Handled || args.Cancelled)
return;
if (args.Args.Target != null)
{
if (TryComp<ForensicsComponent>(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);
}
/// <remarks>
/// Hosts logic common between OnUtilityVerb and OnAfterInteract.
/// </remarks>
private void StartScan(Entity<ForensicScannerComponent> 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<ForensicScannerComponent> scanner, ref GetVerbsEvent<UtilityVerb> 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<ForensicScannerComponent> scanner, ref AfterInteractEvent args)
{
if (args.Target == null || !args.CanReach)
return;
StartScan(scanner, args.User, args.Target.Value);
}
[SubscribeLocalEvent]
private void OnAfterInteractUsing(Entity<ForensicScannerComponent> scanner, ref AfterInteractUsingEvent args)
{
if (args.Handled || !args.CanReach)
return;
if (!TryComp<ForensicPadComponent>(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<ForensicScannerComponent> scanner, ref BeforeActivatableUIOpenEvent args)
{
UpdateUi(scanner);
}
private void OpenUserInterface(EntityUid user, Entity<ForensicScannerComponent> scanner)
{
_uiSystem.OpenUi(scanner.Owner, ForensicScannerUiKey.Key, user, true);
UpdateUi(scanner);
}
[SubscribeLocalEvent]
private void OnPrint(Entity<ForensicScannerComponent> 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<PaperComponent>(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<ForensicScannerComponent> 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);
}
}

View file

@ -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
{
/// <summary>
/// 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.
/// </summary>
public virtual void RandomizeDNA(Entity<DnaComponent?> 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<DnaSubstanceTraceComponent> ent, ref SolutionChangedEvent ev)
{
var soln = GetSolutionsDNA(ev.Solution);
if (soln.Count <= 0)
return;
var comp = EnsureComp<ForensicsComponent>(ent.Owner);
foreach (var dna in soln)
{
comp.DNAs.Add(dna);
}
Dirty(ent);
}
[SubscribeLocalEvent]
private void OnInteract(Entity<HandsComponent> ent, ref ContactInteractionEvent args)
{
ApplyEvidence(ent.Owner, args.Other);
}
[SubscribeLocalEvent(after: [typeof(BloodstreamSystem)])]
private void OnFingerprintInit(Entity<FingerprintComponent> ent, ref MapInitEvent args)
{
if (ent.Comp.Fingerprint == null)
RandomizeFingerprint((ent.Owner, ent.Comp));
}
[SubscribeLocalEvent(after: [typeof(BloodstreamSystem)])]
private void OnDNAInit(Entity<DnaComponent> 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<ForensicsComponent> 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<ForensicsComponent>(part);
partComp.DNAs.Add(dna);
partComp.CanDnaBeCleaned = false;
Dirty(part, partComp);
}
}
[SubscribeLocalEvent]
private void OnMeleeHit(Entity<ForensicsComponent> 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<DnaComponent>(hitEntity, out var hitEntityComp) && hitEntityComp.DNA != null)
weapon.Comp.DNAs.Add(hitEntityComp.DNA);
}
Dirty(weapon);
}
[SubscribeLocalEvent]
private void OnRehydrated(Entity<ForensicsComponent> ent, ref GotRehydratedEvent args)
{
CopyForensicsFrom(ent.Owner, args.Target);
}
/// <summary>
/// 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.
/// </summary>
public virtual void RandomizeFingerprint(Entity<FingerprintComponent?> ent) { }
public void CopyForensicsFrom(Entity<ForensicsComponent?> src, EntityUid target)
{
if (!Resolve(target, ref src.Comp, false))
{
return;
}
var targetComp = EnsureComp<ForensicsComponent>(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<string> GetSolutionsDNA(EntityUid uid)
{
List<string> list = new();
foreach (var (_, soln) in _solutionContainerSystem.EnumerateSolutions(uid))
{
list.AddRange(GetSolutionsDNA(soln.Comp.Solution));
}
return list;
}
public List<string> GetSolutionsDNA(Solution soln)
{
List<string> 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<CleansForensicsComponent> 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<CleansForensicsComponent> entity, ref GetVerbsEvent<UtilityVerb> 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);
}
/// <summary>
/// Transfer DNA from one entity onto the forensics of another.
/// Attempts to clean the given item with the given CleansForensics entity.
/// </summary>
/// <param name="recipient">The entity receiving the DNA.</param>
/// <param name="donor">The entity applying its DNA.</param>
/// <param name="canDnaBeCleaned">If this DNA be cleaned off of the recipient. e.g. cleaning a knife vs cleaning a puddle of blood.</param>
public virtual void TransferDna(EntityUid recipient, EntityUid donor, bool canDnaBeCleaned = true) { }
/// <param name="cleanForensicsEntity">The entity that is being used to clean the target.</param>
/// <param name="user">The user that is using the cleanForensicsEntity.</param>
/// <param name="target">The target of the forensics clean.</param>
/// <returns>True if the target can be cleaned and has some sort of DNA or fingerprints / fibers and false otherwise.</returns>
public bool TryStartCleaning(Entity<CleansForensicsComponent> cleanForensicsEntity, EntityUid user, EntityUid target)
{
if (!TryComp<ForensicsComponent>(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<ForensicsComponent> component, ref CleanForensicsDoAfterEvent args)
{
if (args.Handled || args.Cancelled || args.Args.Target == null)
return;
if (!TryComp<ForensicsComponent>(args.Target, out var targetComp))
return;
targetComp.Fibers = [];
targetComp.Fingerprints = [];
if (targetComp.CanDnaBeCleaned)
targetComp.DNAs = [];
// leave behind evidence it was cleaned
if (TryComp<FiberComponent>(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<ResidueComponent>(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<IgnoresFingerprintsComponent>(target))
return;
var component = EnsureComp<ForensicsComponent>(target);
if (_inventory.TryGetSlotEntity(user, "gloves", out var gloves))
{
if (TryComp<FiberComponent>(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<FingerprintComponent>(user, out var fingerprint) && CanAccessFingerprint(user, out _))
component.Fingerprints.Add(fingerprint.Fingerprint ?? "");
Dirty(target, component);
}
#region PublicAPI
public void RandomizeDNA(Entity<DnaComponent?> 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<FingerprintComponent?> ent)
{
if (!Resolve(ent, ref ent.Comp, false))
return;
ent.Comp.Fingerprint = GenerateFingerprint();
Dirty(ent);
}
/// <summary>
/// Transfer DNA from one entity onto the forensics of another
/// </summary>
/// <param name="recipient">The entity receiving the DNA</param>
/// <param name="donor">The entity applying its DNA</param>
/// <param name="canDnaBeCleaned">If this DNA be cleaned off of the recipient. e.g. cleaning a knife vs cleaning a puddle of blood</param>
public void TransferDna(EntityUid recipient, EntityUid donor, bool canDnaBeCleaned = true)
{
if (!TryComp<DnaComponent>(donor, out var donorComp) || donorComp.DNA == null)
return;
EnsureComp<ForensicsComponent>(recipient, out var recipientComp);
recipientComp.DNAs.Add(donorComp.DNA);
recipientComp.CanDnaBeCleaned = canDnaBeCleaned;
Dirty(recipient, recipientComp);
}
/// <summary>
/// Checks if there's a way to access the fingerprint of the target entity.
/// </summary>
/// <param name="target">The entity with the fingerprint</param>
/// <param name="blocker">The entity that blocked accessing the fingerprint</param>
public bool CanAccessFingerprint(EntityUid target, out EntityUid? blocker)
{
var ev = new TryAccessFingerprintEvent();
RaiseLocalEvent(target, ev);
if (!ev.Cancelled && TryComp<InventoryComponent>(target, out var inv))
_inventory.RelayEvent((target, inv), ev);
blocker = ev.Blocker;
return !ev.Cancelled;
}
#endregion
}

View file

@ -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<SubdermalImplantComponent> _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);
}
/// <summary>

View file

@ -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!;

View file

@ -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!;

View file

@ -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!;

View file

@ -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;

View file

@ -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!;

View file

@ -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)}!