]> git.smokeofanarchy.ru Git - space-station-14.git/commitdiff
Predict InjectorSystem (#39976)
authorslarticodefast <161409025+slarticodefast@users.noreply.github.com>
Mon, 1 Sep 2025 15:24:37 +0000 (17:24 +0200)
committerGitHub <noreply@github.com>
Mon, 1 Sep 2025 15:24:37 +0000 (18:24 +0300)
* predict injectors

* hide verbs if no options

Content.Client/Chemistry/EntitySystems/InjectorSystem.cs
Content.Client/Chemistry/UI/InjectorStatusControl.cs
Content.Server/Chemistry/EntitySystems/InjectorSystem.cs
Content.Server/Forensics/Systems/ForensicsSystem.cs
Content.Shared/Chemistry/Components/InjectorComponent.cs
Content.Shared/Chemistry/EntitySystems/SharedInjectorSystem.cs
Content.Shared/Fluids/SharedPuddleSystem.Spillable.cs
Content.Shared/Forensics/Systems/SharedForensicsSystem.cs
Resources/Locale/en-US/chemistry/components/injector-component.ftl
Resources/Prototypes/Entities/Objects/Specific/chemistry.yml
Resources/migration.yml

index 0131a283c8c967dd467e766a7531a6e5fdd64ce1..58cb5330a2d90adbb61aa12314ca9a117e8ab55f 100644 (file)
@@ -2,7 +2,6 @@ using Content.Client.Chemistry.UI;
 using Content.Client.Items;
 using Content.Shared.Chemistry.Components;
 using Content.Shared.Chemistry.EntitySystems;
-using Robust.Shared.GameStates;
 
 namespace Content.Client.Chemistry.EntitySystems;
 
@@ -11,6 +10,7 @@ public sealed class InjectorSystem : SharedInjectorSystem
     public override void Initialize()
     {
         base.Initialize();
-        Subs.ItemStatus<InjectorComponent>(ent => new InjectorStatusControl(ent, SolutionContainers));
+
+        Subs.ItemStatus<InjectorComponent>(ent => new InjectorStatusControl(ent, SolutionContainer));
     }
 }
index f9b0d90e205597d75befad12c68fab061640cfb8..0358876b76e90e9d63298840b6a933de27dae0b3 100644 (file)
@@ -38,13 +38,13 @@ public sealed class InjectorStatusControl : Control
         // only updates the UI if any of the details are different than they previously were
         if (PrevVolume == solution.Volume
             && PrevMaxVolume == solution.MaxVolume
-            && PrevTransferAmount == _parent.Comp.TransferAmount
+            && PrevTransferAmount == _parent.Comp.CurrentTransferAmount
             && PrevToggleState == _parent.Comp.ToggleState)
             return;
 
         PrevVolume = solution.Volume;
         PrevMaxVolume = solution.MaxVolume;
-        PrevTransferAmount = _parent.Comp.TransferAmount;
+        PrevTransferAmount = _parent.Comp.CurrentTransferAmount;
         PrevToggleState = _parent.Comp.ToggleState;
 
         // Update current volume and injector state
@@ -59,6 +59,6 @@ public sealed class InjectorStatusControl : Control
             ("currentVolume", solution.Volume),
             ("totalVolume", solution.MaxVolume),
             ("modeString", modeStringLocalized),
-            ("transferVolume", _parent.Comp.TransferAmount)));
+            ("transferVolume", _parent.Comp.CurrentTransferAmount)));
     }
 }
index 7b43e7f092628b6840fca294838f3c4806ac0fa7..6088d01c592941848f59238ac9cf67377af7e074 100644 (file)
@@ -1,414 +1,6 @@
-using Content.Server.Body.Systems;
-using Content.Shared.Chemistry;
-using Content.Shared.Chemistry.Components;
-using Content.Shared.Chemistry.Components.SolutionManager;
+
 using Content.Shared.Chemistry.EntitySystems;
-using Content.Shared.Body.Components;
-using Content.Shared.Database;
-using Content.Shared.DoAfter;
-using Content.Shared.FixedPoint;
-using Content.Shared.Forensics;
-using Content.Shared.IdentityManagement;
-using Content.Shared.Interaction;
-using Content.Shared.Mobs.Components;
-using Content.Shared.Stacks;
-using Content.Shared.Nutrition.EntitySystems;
 
 namespace Content.Server.Chemistry.EntitySystems;
 
-public sealed class InjectorSystem : SharedInjectorSystem
-{
-    [Dependency] private readonly BloodstreamSystem _blood = default!;
-    [Dependency] private readonly ReactiveSystem _reactiveSystem = default!;
-    [Dependency] private readonly OpenableSystem _openable = default!;
-
-    public override void Initialize()
-    {
-        base.Initialize();
-
-        SubscribeLocalEvent<InjectorComponent, InjectorDoAfterEvent>(OnInjectDoAfter);
-        SubscribeLocalEvent<InjectorComponent, AfterInteractEvent>(OnInjectorAfterInteract);
-    }
-
-    private bool TryUseInjector(Entity<InjectorComponent> injector, EntityUid target, EntityUid user)
-    {
-        var isOpenOrIgnored = injector.Comp.IgnoreClosed || !_openable.IsClosed(target);
-        // Handle injecting/drawing for solutions
-        if (injector.Comp.ToggleState == InjectorToggleMode.Inject)
-        {
-            if (isOpenOrIgnored && SolutionContainers.TryGetInjectableSolution(target, out var injectableSolution, out _))
-                return TryInject(injector, target, injectableSolution.Value, user, false);
-
-            if (isOpenOrIgnored && SolutionContainers.TryGetRefillableSolution(target, out var refillableSolution, out _))
-                return TryInject(injector, target, refillableSolution.Value, user, true);
-
-            if (TryComp<BloodstreamComponent>(target, out var bloodstream))
-                return TryInjectIntoBloodstream(injector, (target, bloodstream), user);
-
-            Popup.PopupEntity(Loc.GetString("injector-component-cannot-transfer-message",
-                ("target", Identity.Entity(target, EntityManager))), injector, user);
-            return false;
-        }
-
-        if (injector.Comp.ToggleState == InjectorToggleMode.Draw)
-        {
-            // Draw from a bloodstream, if the target has that
-            if (TryComp<BloodstreamComponent>(target, out var stream) &&
-                SolutionContainers.ResolveSolution(target, stream.BloodSolutionName, ref stream.BloodSolution))
-            {
-                return TryDraw(injector, (target, stream), stream.BloodSolution.Value, user);
-            }
-
-            // Draw from an object (food, beaker, etc)
-            if (isOpenOrIgnored && SolutionContainers.TryGetDrawableSolution(target, out var drawableSolution, out _))
-                return TryDraw(injector, target, drawableSolution.Value, user);
-
-            Popup.PopupEntity(Loc.GetString("injector-component-cannot-draw-message",
-                ("target", Identity.Entity(target, EntityManager))), injector.Owner, user);
-            return false;
-        }
-        return false;
-    }
-
-    private void OnInjectDoAfter(Entity<InjectorComponent> entity, ref InjectorDoAfterEvent args)
-    {
-        if (args.Cancelled || args.Handled || args.Args.Target == null)
-            return;
-
-        args.Handled = TryUseInjector(entity, args.Args.Target.Value, args.Args.User);
-    }
-
-    private void OnInjectorAfterInteract(Entity<InjectorComponent> entity, ref AfterInteractEvent args)
-    {
-        if (args.Handled || !args.CanReach)
-            return;
-
-        //Make sure we have the attacking entity
-        if (args.Target is not { Valid: true } target || !HasComp<SolutionContainerManagerComponent>(entity))
-            return;
-
-        // Is the target a mob? If yes, use a do-after to give them time to respond.
-        if (HasComp<MobStateComponent>(target) || HasComp<BloodstreamComponent>(target))
-        {
-            // Are use using an injector capable of targeting a mob?
-            if (entity.Comp.IgnoreMobs)
-                return;
-
-            InjectDoAfter(entity, target, args.User);
-            args.Handled = true;
-            return;
-        }
-
-        args.Handled = TryUseInjector(entity, target, args.User);
-    }
-
-    /// <summary>
-    /// Send informative pop-up messages and wait for a do-after to complete.
-    /// </summary>
-    private void InjectDoAfter(Entity<InjectorComponent> injector, EntityUid target, EntityUid user)
-    {
-        // Create a pop-up for the user
-        if (injector.Comp.ToggleState == InjectorToggleMode.Draw)
-        {
-            Popup.PopupEntity(Loc.GetString("injector-component-drawing-user"), target, user);
-        }
-        else
-        {
-            Popup.PopupEntity(Loc.GetString("injector-component-injecting-user"), target, user);
-        }
-
-        if (!SolutionContainers.TryGetSolution(injector.Owner, injector.Comp.SolutionName, out _, out var solution))
-            return;
-
-        var actualDelay = injector.Comp.Delay;
-        FixedPoint2 amountToInject;
-        if (injector.Comp.ToggleState == InjectorToggleMode.Draw)
-        {
-            // additional delay is based on actual volume left to draw in syringe when smaller than transfer amount
-            amountToInject = FixedPoint2.Min(injector.Comp.TransferAmount, (solution.MaxVolume - solution.Volume));
-        }
-        else
-        {
-            // additional delay is based on actual volume left to inject in syringe when smaller than transfer amount
-            amountToInject = FixedPoint2.Min(injector.Comp.TransferAmount, solution.Volume);
-        }
-
-        // Injections take 0.5 seconds longer per 5u of possible space/content
-        // First 5u(MinimumTransferAmount) doesn't incur delay
-        actualDelay += injector.Comp.DelayPerVolume * FixedPoint2.Max(0, amountToInject - injector.Comp.MinimumTransferAmount).Double();
-
-        // Ensure that minimum delay before incapacitation checks is 1 seconds
-        actualDelay = MathHelper.Max(actualDelay, TimeSpan.FromSeconds(1));
-
-
-        var isTarget = user != target;
-
-        if (isTarget)
-        {
-            // Create a pop-up for the target
-            var userName = Identity.Entity(user, EntityManager);
-            if (injector.Comp.ToggleState == InjectorToggleMode.Draw)
-            {
-                Popup.PopupEntity(Loc.GetString("injector-component-drawing-target",
-    ("user", userName)), user, target);
-            }
-            else
-            {
-                Popup.PopupEntity(Loc.GetString("injector-component-injecting-target",
-    ("user", userName)), user, target);
-            }
-
-
-            // Check if the target is incapacitated or in combat mode and modify time accordingly.
-            if (MobState.IsIncapacitated(target))
-            {
-                actualDelay /= 2.5f;
-            }
-            else if (Combat.IsInCombatMode(target))
-            {
-                // Slightly increase the delay when the target is in combat mode. Helps prevents cheese injections in
-                // combat with fast syringes & lag.
-                actualDelay += TimeSpan.FromSeconds(1);
-            }
-
-            // Add an admin log, using the "force feed" log type. It's not quite feeding, but the effect is the same.
-            if (injector.Comp.ToggleState == InjectorToggleMode.Inject)
-            {
-                AdminLogger.Add(LogType.ForceFeed,
-                    $"{ToPrettyString(user):user} is attempting to inject {ToPrettyString(target):target} with a solution {SharedSolutionContainerSystem.ToPrettyString(solution):solution}");
-            }
-            else
-            {
-                AdminLogger.Add(LogType.ForceFeed,
-                    $"{ToPrettyString(user):user} is attempting to draw {injector.Comp.TransferAmount.ToString()} units from {ToPrettyString(target):target}");
-            }
-        }
-        else
-        {
-            // Self-injections take half as long.
-            actualDelay /= 2;
-
-            if (injector.Comp.ToggleState == InjectorToggleMode.Inject)
-            {
-                AdminLogger.Add(LogType.Ingestion,
-                    $"{ToPrettyString(user):user} is attempting to inject themselves with a solution {SharedSolutionContainerSystem.ToPrettyString(solution):solution}.");
-            }
-            else
-            {
-                AdminLogger.Add(LogType.ForceFeed,
-                    $"{ToPrettyString(user):user} is attempting to draw {injector.Comp.TransferAmount.ToString()} units from themselves.");
-            }
-        }
-
-        DoAfter.TryStartDoAfter(new DoAfterArgs(EntityManager, user, actualDelay, new InjectorDoAfterEvent(), injector.Owner, target: target, used: injector.Owner)
-        {
-            BreakOnMove = true,
-            BreakOnWeightlessMove = false,
-            BreakOnDamage = true,
-            NeedHand = injector.Comp.NeedHand,
-            BreakOnHandChange = injector.Comp.BreakOnHandChange,
-            MovementThreshold = injector.Comp.MovementThreshold,
-        });
-    }
-
-    private bool TryInjectIntoBloodstream(Entity<InjectorComponent> injector, Entity<BloodstreamComponent> target,
-        EntityUid user)
-    {
-        // Get transfer amount. May be smaller than _transferAmount if not enough room
-        if (!SolutionContainers.ResolveSolution(target.Owner, target.Comp.ChemicalSolutionName,
-                ref target.Comp.ChemicalSolution, out var chemSolution))
-        {
-            Popup.PopupEntity(
-                Loc.GetString("injector-component-cannot-inject-message",
-                    ("target", Identity.Entity(target, EntityManager))), injector.Owner, user);
-            return false;
-        }
-
-        var realTransferAmount = FixedPoint2.Min(injector.Comp.TransferAmount, chemSolution.AvailableVolume);
-        if (realTransferAmount <= 0)
-        {
-            Popup.PopupEntity(
-                Loc.GetString("injector-component-cannot-inject-message",
-                    ("target", Identity.Entity(target, EntityManager))), injector.Owner, user);
-            return false;
-        }
-
-        // Move units from attackSolution to targetSolution
-        var removedSolution = SolutionContainers.SplitSolution(target.Comp.ChemicalSolution.Value, realTransferAmount);
-
-        _blood.TryAddToChemicals(target.AsNullable(), removedSolution);
-
-        _reactiveSystem.DoEntityReaction(target, removedSolution, ReactionMethod.Injection);
-
-        Popup.PopupEntity(Loc.GetString("injector-component-inject-success-message",
-            ("amount", removedSolution.Volume),
-            ("target", Identity.Entity(target, EntityManager))), injector.Owner, user);
-
-        Dirty(injector);
-        AfterInject(injector, target);
-        return true;
-    }
-
-    private bool TryInject(Entity<InjectorComponent> injector, EntityUid targetEntity,
-        Entity<SolutionComponent> targetSolution, EntityUid user, bool asRefill)
-    {
-        if (!SolutionContainers.TryGetSolution(injector.Owner, injector.Comp.SolutionName, out var soln,
-                out var solution) || solution.Volume == 0)
-            return false;
-
-        // Get transfer amount. May be smaller than _transferAmount if not enough room
-        var realTransferAmount =
-            FixedPoint2.Min(injector.Comp.TransferAmount, targetSolution.Comp.Solution.AvailableVolume);
-
-        if (realTransferAmount <= 0)
-        {
-            Popup.PopupEntity(
-                Loc.GetString("injector-component-target-already-full-message",
-                    ("target", Identity.Entity(targetEntity, EntityManager))),
-                injector.Owner, user);
-            return false;
-        }
-
-        // Move units from attackSolution to targetSolution
-        Solution removedSolution;
-        if (TryComp<StackComponent>(targetEntity, out var stack))
-            removedSolution = SolutionContainers.SplitStackSolution(soln.Value, realTransferAmount, stack.Count);
-        else
-            removedSolution = SolutionContainers.SplitSolution(soln.Value, realTransferAmount);
-
-        _reactiveSystem.DoEntityReaction(targetEntity, removedSolution, ReactionMethod.Injection);
-
-        if (!asRefill)
-            SolutionContainers.Inject(targetEntity, targetSolution, removedSolution);
-        else
-            SolutionContainers.Refill(targetEntity, targetSolution, removedSolution);
-
-        Popup.PopupEntity(Loc.GetString("injector-component-transfer-success-message",
-            ("amount", removedSolution.Volume),
-            ("target", Identity.Entity(targetEntity, EntityManager))), injector.Owner, user);
-
-        Dirty(injector);
-        AfterInject(injector, targetEntity);
-        return true;
-    }
-
-    private void AfterInject(Entity<InjectorComponent> injector, EntityUid target)
-    {
-        // Automatically set syringe to draw after completely draining it.
-        if (SolutionContainers.TryGetSolution(injector.Owner, injector.Comp.SolutionName, out _,
-                out var solution) && solution.Volume == 0)
-        {
-            SetMode(injector, InjectorToggleMode.Draw);
-        }
-
-        // Leave some DNA from the injectee on it
-        var ev = new TransferDnaEvent { Donor = target, Recipient = injector };
-        RaiseLocalEvent(target, ref ev);
-    }
-
-    private void AfterDraw(Entity<InjectorComponent> injector, EntityUid target)
-    {
-        // Automatically set syringe to inject after completely filling it.
-        if (SolutionContainers.TryGetSolution(injector.Owner, injector.Comp.SolutionName, out _,
-                out var solution) && solution.AvailableVolume == 0)
-        {
-            SetMode(injector, InjectorToggleMode.Inject);
-        }
-
-        // Leave some DNA from the drawee on it
-        var ev = new TransferDnaEvent { Donor = target, Recipient = injector };
-        RaiseLocalEvent(target, ref ev);
-    }
-
-    private bool TryDraw(Entity<InjectorComponent> injector, Entity<BloodstreamComponent?> target,
-        Entity<SolutionComponent> targetSolution, EntityUid user)
-    {
-        if (!SolutionContainers.TryGetSolution(injector.Owner, injector.Comp.SolutionName, out var soln,
-                out var solution) || solution.AvailableVolume == 0)
-        {
-            return false;
-        }
-
-        var applicableTargetSolution = targetSolution.Comp.Solution;
-        // If a whitelist exists, remove all non-whitelisted reagents from the target solution temporarily
-        var temporarilyRemovedSolution = new Solution();
-        if (injector.Comp.ReagentWhitelist is { } reagentWhitelist)
-        {
-            string[] reagentPrototypeWhitelistArray = new string[reagentWhitelist.Count];
-            var i = 0;
-            foreach (var reagent in reagentWhitelist)
-            {
-                reagentPrototypeWhitelistArray[i] = reagent;
-                ++i;
-            }
-            temporarilyRemovedSolution = applicableTargetSolution.SplitSolutionWithout(applicableTargetSolution.Volume, reagentPrototypeWhitelistArray);
-        }
-
-        // Get transfer amount. May be smaller than _transferAmount if not enough room, also make sure there's room in the injector
-        var realTransferAmount = FixedPoint2.Min(injector.Comp.TransferAmount, applicableTargetSolution.Volume,
-            solution.AvailableVolume);
-
-        if (realTransferAmount <= 0)
-        {
-            Popup.PopupEntity(
-                Loc.GetString("injector-component-target-is-empty-message",
-                    ("target", Identity.Entity(target, EntityManager))),
-                injector.Owner, user);
-            return false;
-        }
-
-        // We have some snowflaked behavior for streams.
-        if (target.Comp != null)
-        {
-            DrawFromBlood(injector, (target.Owner, target.Comp), soln.Value, realTransferAmount, user);
-            return true;
-        }
-
-        // Move units from attackSolution to targetSolution
-        var removedSolution = SolutionContainers.Draw(target.Owner, targetSolution, realTransferAmount);
-
-        // Add back non-whitelisted reagents to the target solution
-        SolutionContainers.TryAddSolution(targetSolution, temporarilyRemovedSolution);
-
-        if (!SolutionContainers.TryAddSolution(soln.Value, removedSolution))
-        {
-            return false;
-        }
-
-        Popup.PopupEntity(Loc.GetString("injector-component-draw-success-message",
-            ("amount", removedSolution.Volume),
-            ("target", Identity.Entity(target, EntityManager))), injector.Owner, user);
-
-        Dirty(injector);
-        AfterDraw(injector, target);
-        return true;
-    }
-
-    private void DrawFromBlood(Entity<InjectorComponent> injector, Entity<BloodstreamComponent> target,
-        Entity<SolutionComponent> injectorSolution, FixedPoint2 transferAmount, EntityUid user)
-    {
-        var drawAmount = (float) transferAmount;
-
-        if (SolutionContainers.ResolveSolution(target.Owner, target.Comp.ChemicalSolutionName,
-                ref target.Comp.ChemicalSolution))
-        {
-            var chemTemp = SolutionContainers.SplitSolution(target.Comp.ChemicalSolution.Value, drawAmount * 0.15f);
-            SolutionContainers.TryAddSolution(injectorSolution, chemTemp);
-            drawAmount -= (float) chemTemp.Volume;
-        }
-
-        if (SolutionContainers.ResolveSolution(target.Owner, target.Comp.BloodSolutionName,
-                ref target.Comp.BloodSolution))
-        {
-            var bloodTemp = SolutionContainers.SplitSolution(target.Comp.BloodSolution.Value, drawAmount);
-            SolutionContainers.TryAddSolution(injectorSolution, bloodTemp);
-        }
-
-        Popup.PopupEntity(Loc.GetString("injector-component-draw-success-message",
-            ("amount", transferAmount),
-            ("target", Identity.Entity(target, EntityManager))), injector.Owner, user);
-
-        Dirty(injector);
-        AfterDraw(injector, target);
-    }
-}
+public sealed class InjectorSystem : SharedInjectorSystem;
index cc74c1d14143eb48e0442c1f0096659c0c8c978f..8370014ad668f4333b5aaf27a7bbd61705f85e7a 100644 (file)
@@ -307,6 +307,8 @@ namespace Content.Server.Forensics
                 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)
@@ -339,13 +341,7 @@ namespace Content.Server.Forensics
             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)
+        public override void TransferDna(EntityUid recipient, EntityUid donor, bool canDnaBeCleaned = true)
         {
             if (TryComp<DnaComponent>(donor, out var donorComp) && donorComp.DNA != null)
             {
index ebd6654d9f5ca3af54bdd984b4bc6eb552210760..d3a0503c3c7720574212eb9d90040bfb3b30ca7d 100644 (file)
@@ -8,11 +8,6 @@ using Robust.Shared.Serialization;
 
 namespace Content.Shared.Chemistry.Components;
 
-[Serializable, NetSerializable]
-public sealed partial class InjectorDoAfterEvent : SimpleDoAfterEvent
-{
-}
-
 /// <summary>
 /// Implements draw/inject behavior for droppers and syringes.
 /// </summary>
@@ -26,9 +21,18 @@ public sealed partial class InjectorDoAfterEvent : SimpleDoAfterEvent
 [RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
 public sealed partial class InjectorComponent : Component
 {
+    /// <summary>
+    /// The solution to draw into or inject from.
+    /// </summary>
     [DataField]
     public string SolutionName = "injector";
 
+    /// <summary>
+    /// A cached reference to the solution.
+    /// </summary>
+    [ViewVariables]
+    public Entity<SolutionComponent>? Solution = null;
+
     /// <summary>
     /// Whether or not the injector is able to draw from containers or if it's a single use
     /// device that can only inject.
@@ -37,42 +41,35 @@ public sealed partial class InjectorComponent : Component
     public bool InjectOnly;
 
     /// <summary>
-    /// Whether or not the injector is able to draw from or inject from mobs
+    /// Whether or not the injector is able to draw from or inject from mobs.
     /// </summary>
     /// <remarks>
-    ///     for example: droppers would ignore mobs
+    /// For example: droppers would ignore mobs.
     /// </remarks>
     [DataField]
     public bool IgnoreMobs;
 
     /// <summary>
-    /// Whether or not the injector is able to draw from or inject into containers that are closed/sealed
+    /// Whether or not the injector is able to draw from or inject into containers that are closed/sealed.
     /// </summary>
     /// <remarks>
-    ///     for example: droppers can not inject into cans, but syringes can
+    /// For example: droppers can not inject into cans, but syringes can.
     /// </remarks>
     [DataField]
     public bool IgnoreClosed = true;
 
     /// <summary>
-    ///     The minimum amount of solution that can be transferred at once from this solution.
-    /// </summary>
-    [DataField("minTransferAmount")]
-    public FixedPoint2 MinimumTransferAmount = FixedPoint2.New(5);
-
-    /// <summary>
-    ///     The maximum amount of solution that can be transferred at once from this solution.
+    /// The transfer amounts for the set-transfer verb.
     /// </summary>
-    [DataField("maxTransferAmount")]
-    public FixedPoint2 MaximumTransferAmount = FixedPoint2.New(15);
+    [DataField]
+    public List<FixedPoint2> TransferAmounts = new() { 1, 5, 10, 15 };
 
     /// <summary>
     /// Amount to inject or draw on each usage. If the injector is inject only, it will
     /// attempt to inject it's entire contents upon use.
     /// </summary>
-    [DataField]
-    [AutoNetworkedField]
-    public FixedPoint2 TransferAmount = FixedPoint2.New(5);
+    [DataField, AutoNetworkedField]
+    public FixedPoint2 CurrentTransferAmount = FixedPoint2.New(5);
 
     /// <summary>
     /// Injection delay (seconds) when the target is a mob.
@@ -95,8 +92,7 @@ public sealed partial class InjectorComponent : Component
     /// right SolutionCaps to support injection/drawing. For InjectOnly injectors this should
     /// only ever be set to Inject
     /// </summary>
-    [AutoNetworkedField]
-    [DataField]
+    [DataField, AutoNetworkedField]
     public InjectorToggleMode ToggleState = InjectorToggleMode.Draw;
 
     /// <summary>
@@ -127,6 +123,7 @@ public sealed partial class InjectorComponent : Component
 /// <summary>
 /// Possible modes for an <see cref="InjectorComponent"/>.
 /// </summary>
+[Serializable, NetSerializable]
 public enum InjectorToggleMode : byte
 {
     /// <summary>
@@ -137,5 +134,11 @@ public enum InjectorToggleMode : byte
     /// <summary>
     /// The injector will try to draw reagent from things.
     /// </summary>
-    Draw
+    Draw,
 }
+
+/// <summary>
+/// Raised on the injector when the doafter has finished.
+/// </summary>
+[Serializable, NetSerializable]
+public sealed partial class InjectorDoAfterEvent : SimpleDoAfterEvent;
index 1620344652c2bf622bd7c85431fb61b977d34ac2..a39f8514577895a063ea5ca32d8a901eb2094f49 100644 (file)
@@ -1,48 +1,60 @@
+using System.Linq;
 using Content.Shared.Administration.Logs;
+using Content.Shared.Body.Components;
+using Content.Shared.Body.Systems;
 using Content.Shared.Chemistry.Components;
+using Content.Shared.Chemistry.Components.SolutionManager;
 using Content.Shared.CombatMode;
+using Content.Shared.Database;
 using Content.Shared.DoAfter;
 using Content.Shared.FixedPoint;
+using Content.Shared.Forensics.Systems;
+using Content.Shared.IdentityManagement;
+using Content.Shared.Interaction;
 using Content.Shared.Interaction.Events;
+using Content.Shared.Mobs.Components;
 using Content.Shared.Mobs.Systems;
+using Content.Shared.Nutrition.EntitySystems;
 using Content.Shared.Popups;
+using Content.Shared.Stacks;
 using Content.Shared.Verbs;
-using Robust.Shared.Player;
 
 namespace Content.Shared.Chemistry.EntitySystems;
 
 public abstract class SharedInjectorSystem : EntitySystem
 {
-    /// <summary>
-    ///     Default transfer amounts for the set-transfer verb.
-    /// </summary>
-    public static readonly FixedPoint2[] TransferAmounts = { 1, 5, 10, 15 };
-
-    [Dependency] protected readonly SharedPopupSystem Popup = default!;
-    [Dependency] protected readonly SharedSolutionContainerSystem SolutionContainers = default!;
-    [Dependency] protected readonly MobStateSystem MobState = default!;
-    [Dependency] protected readonly SharedCombatModeSystem Combat = default!;
-    [Dependency] protected readonly SharedDoAfterSystem DoAfter = default!;
-    [Dependency] protected readonly ISharedAdminLogManager AdminLogger = default!;
+    [Dependency] private readonly SharedBloodstreamSystem _blood = default!;
+    [Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
+    [Dependency] private readonly MobStateSystem _mobState = default!;
+    [Dependency] private readonly OpenableSystem _openable = default!;
+    [Dependency] private readonly ReactiveSystem _reactiveSystem = default!;
+    [Dependency] private readonly SharedCombatModeSystem _combatMode = default!;
+    [Dependency] private readonly SharedDoAfterSystem _doAfter = default!;
+    [Dependency] private readonly SharedPopupSystem _popup = default!;
+    [Dependency] private readonly SharedForensicsSystem _forensics = default!;
+    [Dependency] protected readonly SharedSolutionContainerSystem SolutionContainer = default!;
 
     public override void Initialize()
     {
         SubscribeLocalEvent<InjectorComponent, GetVerbsEvent<AlternativeVerb>>(AddSetTransferVerbs);
-        SubscribeLocalEvent<InjectorComponent, ComponentStartup>(OnInjectorStartup);
         SubscribeLocalEvent<InjectorComponent, UseInHandEvent>(OnInjectorUse);
+        SubscribeLocalEvent<InjectorComponent, AfterInteractEvent>(OnInjectorAfterInteract);
+        SubscribeLocalEvent<InjectorComponent, InjectorDoAfterEvent>(OnInjectDoAfter);
     }
 
-    private void AddSetTransferVerbs(Entity<InjectorComponent> entity, ref GetVerbsEvent<AlternativeVerb> args)
+    private void AddSetTransferVerbs(Entity<InjectorComponent> ent, ref GetVerbsEvent<AlternativeVerb> args)
     {
         if (!args.CanAccess || !args.CanInteract || args.Hands == null)
             return;
 
+        if (ent.Comp.TransferAmounts.Count <= 1)
+            return; // No options to cycle between
+
         var user = args.User;
-        var (_, component) = entity;
 
-        var min = component.MinimumTransferAmount;
-        var max = component.MaximumTransferAmount;
-        var cur = component.TransferAmount;
+        var min = ent.Comp.TransferAmounts.Min();
+        var max = ent.Comp.TransferAmounts.Max();
+        var cur = ent.Comp.CurrentTransferAmount;
         var toggleAmount = cur == max ? min : max;
 
         var priority = 0;
@@ -52,9 +64,9 @@ public abstract class SharedInjectorSystem : EntitySystem
             Category = VerbCategory.SetTransferAmount,
             Act = () =>
             {
-                component.TransferAmount = toggleAmount;
-                Popup.PopupClient(Loc.GetString("comp-solution-transfer-set-amount", ("amount", toggleAmount)), user, user);
-                Dirty(entity);
+                ent.Comp.CurrentTransferAmount = toggleAmount;
+                _popup.PopupClient(Loc.GetString("comp-solution-transfer-set-amount", ("amount", toggleAmount)), user, user);
+                Dirty(ent);
             },
 
             Priority = priority
@@ -63,21 +75,18 @@ public abstract class SharedInjectorSystem : EntitySystem
 
         priority -= 1;
 
-        // Add specific transfer verbs according to the container's size
-        foreach (var amount in TransferAmounts)
+        // Add specific transfer verbs for amounts defined in the component
+        foreach (var amount in ent.Comp.TransferAmounts)
         {
-            if (amount < component.MinimumTransferAmount || amount > component.MaximumTransferAmount)
-                continue;
-
             AlternativeVerb verb = new()
             {
                 Text = Loc.GetString("comp-solution-transfer-verb-amount", ("amount", amount)),
                 Category = VerbCategory.SetTransferAmount,
                 Act = () =>
                 {
-                    component.TransferAmount = amount;
-                    Popup.PopupClient(Loc.GetString("comp-solution-transfer-set-amount", ("amount", amount)), user, user);
-                    Dirty(entity);
+                    ent.Comp.CurrentTransferAmount = amount;
+                    _popup.PopupClient(Loc.GetString("comp-solution-transfer-set-amount", ("amount", amount)), user, user);
+                    Dirty(ent);
                 },
 
                 // we want to sort by size, not alphabetically by the verb text.
@@ -90,30 +99,407 @@ public abstract class SharedInjectorSystem : EntitySystem
         }
     }
 
-    private void OnInjectorStartup(Entity<InjectorComponent> entity, ref ComponentStartup args)
+    private void OnInjectorUse(Entity<InjectorComponent> ent, ref UseInHandEvent args)
     {
-        // ???? why ?????
-        Dirty(entity);
+        if (args.Handled)
+            return;
+
+        Toggle(ent, args.User);
+        args.Handled = true;
     }
 
-    private void OnInjectorUse(Entity<InjectorComponent> entity, ref UseInHandEvent args)
+    private void OnInjectorAfterInteract(Entity<InjectorComponent> ent, ref AfterInteractEvent args)
     {
-        if (args.Handled)
+        if (args.Handled || !args.CanReach)
             return;
 
-        Toggle(entity, args.User);
-        args.Handled = true;
+        //Make sure we have the attacking entity
+        if (args.Target is not { Valid: true } target || !HasComp<SolutionContainerManagerComponent>(ent))
+            return;
+
+        // Is the target a mob? If yes, use a do-after to give them time to respond.
+        if (HasComp<MobStateComponent>(target) || HasComp<BloodstreamComponent>(target))
+        {
+            // Are use using an injector capable of targeting a mob?
+            if (ent.Comp.IgnoreMobs)
+                return;
+
+            InjectDoAfter(ent, target, args.User);
+            args.Handled = true;
+            return;
+        }
+
+        // Instantly draw from or inject into jugs, bottles etc.
+        args.Handled = TryUseInjector(ent, target, args.User);
+    }
+
+    private void OnInjectDoAfter(Entity<InjectorComponent> ent, ref InjectorDoAfterEvent args)
+    {
+        if (args.Cancelled || args.Handled || args.Args.Target == null)
+            return;
+
+        args.Handled = TryUseInjector(ent, args.Args.Target.Value, args.Args.User);
     }
 
     /// <summary>
-    /// Toggle between draw/inject state if applicable
+    /// Send informative pop-up messages and wait for a do-after to complete.
     /// </summary>
-    private void Toggle(Entity<InjectorComponent> injector, EntityUid user)
+    private void InjectDoAfter(Entity<InjectorComponent> injector, EntityUid target, EntityUid user)
+    {
+        // Create a pop-up for the user
+        if (injector.Comp.ToggleState == InjectorToggleMode.Draw)
+        {
+            _popup.PopupClient(Loc.GetString("injector-component-drawing-user"), target, user);
+        }
+        else
+        {
+            _popup.PopupClient(Loc.GetString("injector-component-injecting-user"), target, user);
+        }
+
+        if (!SolutionContainer.ResolveSolution(injector.Owner, injector.Comp.SolutionName, ref injector.Comp.Solution, out var solution))
+            return;
+
+        var actualDelay = injector.Comp.Delay;
+        FixedPoint2 amountToInject;
+        if (injector.Comp.ToggleState == InjectorToggleMode.Draw)
+        {
+            // additional delay is based on actual volume left to draw in syringe when smaller than transfer amount
+            amountToInject = FixedPoint2.Min(injector.Comp.CurrentTransferAmount, solution.MaxVolume - solution.Volume);
+        }
+        else
+        {
+            // additional delay is based on actual volume left to inject in syringe when smaller than transfer amount
+            amountToInject = FixedPoint2.Min(injector.Comp.CurrentTransferAmount, solution.Volume);
+        }
+
+        // Injections take 0.5 seconds longer per 5u of possible space/content
+        // First 5u(MinimumTransferAmount) doesn't incur delay
+        actualDelay += injector.Comp.DelayPerVolume * FixedPoint2.Max(0, amountToInject - injector.Comp.TransferAmounts.Min()).Double();
+
+        // Ensure that minimum delay before incapacitation checks is 1 seconds
+        actualDelay = MathHelper.Max(actualDelay, TimeSpan.FromSeconds(1));
+
+        if (user != target) // injecting someone else
+        {
+            // Create a pop-up for the target
+            var userName = Identity.Entity(user, EntityManager);
+            if (injector.Comp.ToggleState == InjectorToggleMode.Draw)
+            {
+                _popup.PopupEntity(Loc.GetString("injector-component-drawing-target",
+    ("user", userName)), user, target);
+            }
+            else
+            {
+                _popup.PopupEntity(Loc.GetString("injector-component-injecting-target",
+    ("user", userName)), user, target);
+            }
+
+
+            // Check if the target is incapacitated or in combat mode and modify time accordingly.
+            if (_mobState.IsIncapacitated(target))
+            {
+                actualDelay /= 2.5f;
+            }
+            else if (_combatMode.IsInCombatMode(target))
+            {
+                // Slightly increase the delay when the target is in combat mode. Helps prevents cheese injections in
+                // combat with fast syringes & lag.
+                actualDelay += TimeSpan.FromSeconds(1);
+            }
+
+            // Add an admin log, using the "force feed" log type. It's not quite feeding, but the effect is the same.
+            if (injector.Comp.ToggleState == InjectorToggleMode.Inject)
+            {
+                _adminLogger.Add(LogType.ForceFeed,
+                    $"{ToPrettyString(user):user} is attempting to inject {ToPrettyString(target):target} with a solution {SharedSolutionContainerSystem.ToPrettyString(solution):solution}");
+            }
+            else
+            {
+                _adminLogger.Add(LogType.ForceFeed,
+                    $"{ToPrettyString(user):user} is attempting to draw {injector.Comp.CurrentTransferAmount.ToString()} units from {ToPrettyString(target):target}");
+            }
+        }
+        else // injecting yourself
+        {
+            // Self-injections take half as long.
+            actualDelay /= 2;
+
+            if (injector.Comp.ToggleState == InjectorToggleMode.Inject)
+            {
+                _adminLogger.Add(LogType.Ingestion,
+                    $"{ToPrettyString(user):user} is attempting to inject themselves with a solution {SharedSolutionContainerSystem.ToPrettyString(solution):solution}.");
+            }
+            else
+            {
+                _adminLogger.Add(LogType.ForceFeed,
+                    $"{ToPrettyString(user):user} is attempting to draw {injector.Comp.CurrentTransferAmount.ToString()} units from themselves.");
+            }
+        }
+
+        _doAfter.TryStartDoAfter(new DoAfterArgs(EntityManager, user, actualDelay, new InjectorDoAfterEvent(), injector.Owner, target: target, used: injector.Owner)
+        {
+            BreakOnMove = true,
+            BreakOnWeightlessMove = false,
+            BreakOnDamage = true,
+            NeedHand = injector.Comp.NeedHand,
+            BreakOnHandChange = injector.Comp.BreakOnHandChange,
+            MovementThreshold = injector.Comp.MovementThreshold,
+        });
+    }
+
+    private bool TryUseInjector(Entity<InjectorComponent> injector, EntityUid target, EntityUid user)
+    {
+        var isOpenOrIgnored = injector.Comp.IgnoreClosed || !_openable.IsClosed(target);
+        // Handle injecting/drawing for solutions
+        if (injector.Comp.ToggleState == InjectorToggleMode.Inject)
+        {
+            if (isOpenOrIgnored && SolutionContainer.TryGetInjectableSolution(target, out var injectableSolution, out _))
+                return TryInject(injector, target, injectableSolution.Value, user, false);
+
+            if (isOpenOrIgnored && SolutionContainer.TryGetRefillableSolution(target, out var refillableSolution, out _))
+                return TryInject(injector, target, refillableSolution.Value, user, true);
+
+            if (TryComp<BloodstreamComponent>(target, out var bloodstream))
+                return TryInjectIntoBloodstream(injector, (target, bloodstream), user);
+
+            LocId msg = target == user ? "injector-component-cannot-transfer-message-self" : "injector-component-cannot-transfer-message";
+            _popup.PopupClient(Loc.GetString(msg, ("target", Identity.Entity(target, EntityManager))), injector, user);
+        }
+        else if (injector.Comp.ToggleState == InjectorToggleMode.Draw)
+        {
+            // Draw from a bloodstream, if the target has that
+            if (TryComp<BloodstreamComponent>(target, out var stream) &&
+                SolutionContainer.ResolveSolution(target, stream.BloodSolutionName, ref stream.BloodSolution))
+            {
+                return TryDraw(injector, (target, stream), stream.BloodSolution.Value, user);
+            }
+
+            // Draw from an object (food, beaker, etc)
+            if (isOpenOrIgnored && SolutionContainer.TryGetDrawableSolution(target, out var drawableSolution, out _))
+                return TryDraw(injector, target, drawableSolution.Value, user);
+
+            LocId msg = target == user ? "injector-component-cannot-draw-message-self" : "injector-component-cannot-draw-message";
+            _popup.PopupClient(Loc.GetString(msg, ("target", Identity.Entity(target, EntityManager))), injector.Owner, user);
+        }
+        return false;
+    }
+
+    private bool TryInject(Entity<InjectorComponent> injector, EntityUid target,
+        Entity<SolutionComponent> targetSolution, EntityUid user, bool asRefill)
+    {
+        if (!SolutionContainer.ResolveSolution(injector.Owner, injector.Comp.SolutionName, ref injector.Comp.Solution,
+                out var solution) || solution.Volume == 0)
+            return false;
+
+        // Get transfer amount. May be smaller than _transferAmount if not enough room
+        var realTransferAmount =
+            FixedPoint2.Min(injector.Comp.CurrentTransferAmount, targetSolution.Comp.Solution.AvailableVolume);
+
+        if (realTransferAmount <= 0)
+        {
+            LocId msg = target == user ? "injector-component-target-already-full-message-self" : "injector-component-target-already-full-message";
+            _popup.PopupClient(
+                Loc.GetString(msg,
+                    ("target", Identity.Entity(target, EntityManager))),
+                injector.Owner,
+                user);
+            return false;
+        }
+
+        // Move units from attackSolution to targetSolution
+        Solution removedSolution;
+        if (TryComp<StackComponent>(target, out var stack))
+            removedSolution = SolutionContainer.SplitStackSolution(injector.Comp.Solution.Value, realTransferAmount, stack.Count);
+        else
+            removedSolution = SolutionContainer.SplitSolution(injector.Comp.Solution.Value, realTransferAmount);
+
+        _reactiveSystem.DoEntityReaction(target, removedSolution, ReactionMethod.Injection);
+
+        if (!asRefill)
+            SolutionContainer.Inject(target, targetSolution, removedSolution);
+        else
+            SolutionContainer.Refill(target, targetSolution, removedSolution);
+
+        LocId msgSuccess = target == user ? "injector-component-transfer-success-message-self" : "injector-component-transfer-success-message";
+        _popup.PopupClient(
+            Loc.GetString(msgSuccess,
+                ("amount", removedSolution.Volume),
+                ("target", Identity.Entity(target, EntityManager))),
+            injector.Owner, user);
+
+        AfterInject(injector, target);
+        return true;
+    }
+
+    private bool TryInjectIntoBloodstream(Entity<InjectorComponent> injector, Entity<BloodstreamComponent> target,
+        EntityUid user)
+    {
+        // Get transfer amount. May be smaller than _transferAmount if not enough room
+        if (!SolutionContainer.ResolveSolution(target.Owner, target.Comp.ChemicalSolutionName,
+                ref target.Comp.ChemicalSolution, out var chemSolution))
+        {
+            LocId msg = target.Owner == user ? "injector-component-cannot-inject-message-self" : "injector-component-cannot-inject-message";
+            _popup.PopupClient(
+                Loc.GetString(msg,
+                    ("target", Identity.Entity(target, EntityManager))),
+                injector.Owner, user);
+            return false;
+        }
+
+        var realTransferAmount = FixedPoint2.Min(injector.Comp.CurrentTransferAmount, chemSolution.AvailableVolume);
+        if (realTransferAmount <= 0)
+        {
+            LocId msg = target.Owner == user ? "injector-component-cannot-inject-message-self" : "injector-component-cannot-inject-message";
+            _popup.PopupClient(
+                Loc.GetString(msg,
+                    ("target", Identity.Entity(target, EntityManager))),
+                injector.Owner, user);
+            return false;
+        }
+
+        // Move units from attackSolution to targetSolution
+        var removedSolution = SolutionContainer.SplitSolution(target.Comp.ChemicalSolution.Value, realTransferAmount);
+
+        _blood.TryAddToChemicals(target.AsNullable(), removedSolution);
+
+        _reactiveSystem.DoEntityReaction(target, removedSolution, ReactionMethod.Injection);
+
+        LocId msgSuccess = target.Owner == user ? "injector-component-inject-success-message-self" : "injector-component-inject-success-message";
+        _popup.PopupClient(
+            Loc.GetString(msgSuccess,
+                ("amount", removedSolution.Volume),
+                ("target", Identity.Entity(target, EntityManager))),
+            injector.Owner, user);
+
+        AfterInject(injector, target);
+        return true;
+    }
+
+    private bool TryDraw(Entity<InjectorComponent> injector, Entity<BloodstreamComponent?> target,
+        Entity<SolutionComponent> targetSolution, EntityUid user)
+    {
+        if (!SolutionContainer.ResolveSolution(injector.Owner, injector.Comp.SolutionName, ref injector.Comp.Solution,
+                out var solution) || solution.AvailableVolume == 0)
+        {
+            return false;
+        }
+
+        var applicableTargetSolution = targetSolution.Comp.Solution;
+        // If a whitelist exists, remove all non-whitelisted reagents from the target solution temporarily
+        var temporarilyRemovedSolution = new Solution();
+        if (injector.Comp.ReagentWhitelist is { } reagentWhitelist)
+        {
+            temporarilyRemovedSolution = applicableTargetSolution.SplitSolutionWithout(applicableTargetSolution.Volume, reagentWhitelist.ToArray());
+        }
+
+        // Get transfer amount. May be smaller than _transferAmount if not enough room, also make sure there's room in the injector
+        var realTransferAmount = FixedPoint2.Min(injector.Comp.CurrentTransferAmount, applicableTargetSolution.Volume,
+            solution.AvailableVolume);
+
+        if (realTransferAmount <= 0)
+        {
+            LocId msg = target.Owner == user ? "injector-component-target-is-empty-message-self" : "injector-component-target-is-empty-message";
+            _popup.PopupClient(
+                Loc.GetString(msg,
+                    ("target", Identity.Entity(target, EntityManager))),
+                injector.Owner, user);
+            return false;
+        }
+
+        // We have some snowflaked behavior for streams.
+        if (target.Comp != null)
+        {
+            DrawFromBlood(injector, (target.Owner, target.Comp), injector.Comp.Solution.Value, realTransferAmount, user);
+            return true;
+        }
+
+        // Move units from attackSolution to targetSolution
+        var removedSolution = SolutionContainer.Draw(target.Owner, targetSolution, realTransferAmount);
+
+        // Add back non-whitelisted reagents to the target solution
+        SolutionContainer.TryAddSolution(targetSolution, temporarilyRemovedSolution);
+
+        if (!SolutionContainer.TryAddSolution(injector.Comp.Solution.Value, removedSolution))
+        {
+            return false;
+        }
+
+        LocId msgSuccess = target.Owner == user ? "injector-component-draw-success-message-self" : "injector-component-draw-success-message";
+        _popup.PopupClient(
+            Loc.GetString(msgSuccess,
+                ("amount", removedSolution.Volume),
+                ("target", Identity.Entity(target, EntityManager))),
+            injector.Owner, user);
+
+        AfterDraw(injector, target);
+        return true;
+    }
+
+    private void DrawFromBlood(Entity<InjectorComponent> injector, Entity<BloodstreamComponent> target,
+        Entity<SolutionComponent> injectorSolution, FixedPoint2 transferAmount, EntityUid user)
+    {
+        var drawAmount = (float)transferAmount;
+
+        if (SolutionContainer.ResolveSolution(target.Owner, target.Comp.ChemicalSolutionName,
+                ref target.Comp.ChemicalSolution))
+        {
+            var chemTemp = SolutionContainer.SplitSolution(target.Comp.ChemicalSolution.Value, drawAmount * 0.15f);
+            SolutionContainer.TryAddSolution(injectorSolution, chemTemp);
+            drawAmount -= (float)chemTemp.Volume;
+        }
+
+        if (SolutionContainer.ResolveSolution(target.Owner, target.Comp.BloodSolutionName,
+                ref target.Comp.BloodSolution))
+        {
+            var bloodTemp = SolutionContainer.SplitSolution(target.Comp.BloodSolution.Value, drawAmount);
+            SolutionContainer.TryAddSolution(injectorSolution, bloodTemp);
+        }
+
+        LocId msg = target.Owner == user ? "injector-component-draw-success-message-self" : "injector-component-draw-success-message";
+        _popup.PopupClient(
+            Loc.GetString(msg,
+                ("amount", transferAmount),
+                ("target", Identity.Entity(target, EntityManager))),
+            injector.Owner, user);
+
+        AfterDraw(injector, target);
+    }
+
+    private void AfterInject(Entity<InjectorComponent> injector, EntityUid target)
+    {
+        // Automatically set syringe to draw after completely draining it.
+        if (SolutionContainer.ResolveSolution(injector.Owner, injector.Comp.SolutionName, ref injector.Comp.Solution,
+                out var solution) && solution.Volume == 0)
+        {
+            SetMode(injector, InjectorToggleMode.Draw);
+        }
+
+        // Leave some DNA from the injectee on it
+        _forensics.TransferDna(injector, target);
+    }
+
+    private void AfterDraw(Entity<InjectorComponent> injector, EntityUid target)
+    {
+        // Automatically set syringe to inject after completely filling it.
+        if (SolutionContainer.ResolveSolution(injector.Owner, injector.Comp.SolutionName, ref injector.Comp.Solution,
+                out var solution) && solution.AvailableVolume == 0)
+        {
+            SetMode(injector, InjectorToggleMode.Inject);
+        }
+
+        // Leave some DNA from the drawee on it
+        _forensics.TransferDna(injector, target);
+    }
+
+    /// <summary>
+    /// Toggle the injector between draw/inject state if applicable.
+    /// </summary>
+    public void Toggle(Entity<InjectorComponent> injector, EntityUid user)
     {
         if (injector.Comp.InjectOnly)
             return;
 
-        if (!SolutionContainers.TryGetSolution(injector.Owner, injector.Comp.SolutionName, out var solEnt, out var solution))
+        if (!SolutionContainer.ResolveSolution(injector.Owner, injector.Comp.SolutionName, ref injector.Comp.Solution, out var solution))
             return;
 
         string msg;
@@ -146,9 +532,12 @@ public abstract class SharedInjectorSystem : EntitySystem
                 throw new ArgumentOutOfRangeException();
         }
 
-        Popup.PopupClient(Loc.GetString(msg), injector, user);
+        _popup.PopupClient(Loc.GetString(msg), injector, user);
     }
 
+    /// <summary>
+    /// Set the mode of the injector to draw or inject.
+    /// </summary>
     public void SetMode(Entity<InjectorComponent> injector, InjectorToggleMode mode)
     {
         injector.Comp.ToggleState = mode;
index 2ce008da26240fb41020f23c4ce462f48f2bbe92..7d65dd242451796ec2470e5c449fa34a7422f544 100644 (file)
@@ -60,6 +60,8 @@ public abstract partial class SharedPuddleSystem
                 var puddleSolution = _solutionContainerSystem.SplitSolution(soln.Value, solution.Volume);
                 TrySpillAt(Transform(target).Coordinates, puddleSolution, out _);
 
+                // TODO: Make this an event subscription once spilling puddles is predicted.
+                // Injectors should not be hardcoded here.
                 if (TryComp<InjectorComponent>(entity, out var injectorComp))
                 {
                     injectorComp.ToggleState = InjectorToggleMode.Draw;
index 1220b75fffe5f721f8c200c7e897ed0f190fa369..be26fd6fd0158a4589a3b4d29faf30c850e6dbfb 100644 (file)
@@ -15,4 +15,13 @@ public abstract class SharedForensicsSystem : EntitySystem
     /// Does nothing if it does not have the FingerprintComponent.
     /// </summary>
     public virtual void RandomizeFingerprint(Entity<FingerprintComponent?> 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 virtual void TransferDna(EntityUid recipient, EntityUid donor, bool canDnaBeCleaned = true) { }
+
 }
index 0c3152774f00eaaf6a3c44139a24a07b085f3b82..53387ea1a45b5fef5d36a4575ca24d9f617e3cd4 100644 (file)
@@ -10,14 +10,22 @@ injector-volume-label = Volume: [color=white]{$currentVolume}/{$totalVolume}[/co
 
 injector-component-drawing-text = Now drawing
 injector-component-injecting-text = Now injecting
-injector-component-cannot-transfer-message = You aren't able to transfer to {THE($target)}!
+injector-component-cannot-transfer-message = You aren't able to transfer into {THE($target)}!
+injector-component-cannot-transfer-message-self = You aren't able to transfer into yourself!
 injector-component-cannot-draw-message = You aren't able to draw from {THE($target)}!
-injector-component-cannot-inject-message = You aren't able to inject to {THE($target)}!
+injector-component-cannot-draw-message-self = You aren't able to draw from yourself!
+injector-component-cannot-inject-message = You aren't able to inject into {THE($target)}!
+injector-component-cannot-inject-message-self = You aren't able to inject into yourself!
 injector-component-inject-success-message = You inject {$amount}u into {THE($target)}!
+injector-component-inject-success-message-self = You inject {$amount}u into yourself!
 injector-component-transfer-success-message = You transfer {$amount}u into {THE($target)}.
+injector-component-transfer-success-message-self = You transfer {$amount}u into yourself.
 injector-component-draw-success-message = You draw {$amount}u from {THE($target)}.
+injector-component-draw-success-message-self = You draw {$amount}u from youself.
 injector-component-target-already-full-message = {CAPITALIZE(THE($target))} is already full!
+injector-component-target-already-full-message-self = You are already full!
 injector-component-target-is-empty-message = {CAPITALIZE(THE($target))} is empty!
+injector-component-target-is-empty-message-self = You are empty!
 injector-component-cannot-toggle-draw-message = Too full to draw!
 injector-component-cannot-toggle-inject-message = Nothing to inject!
 
index 62c533bff1ff2caab033913a0a4c68f225a6459a..843515cffaf043a1ad26b2ce38366b3ba487011f 100644 (file)
     injectOnly: false
     ignoreMobs: true
     ignoreClosed: false
-    minTransferAmount: 1
-    maxTransferAmount: 5
-    transferAmount: 1
-    toggleState: 1 # draw
+    transferAmounts:
+    - 1
+    - 2
+    - 3
+    - 4
+    - 5
+    currentTransferAmount: 1
   - type: ExaminableSolution
     solution: dropper
     exactVolume: true
   id: Syringe
   components:
   - type: Injector
-    transferAmount: 15
-    toggleState: Draw
+    currentTransferAmount: 15
   - type: Tag
     tags:
     - Syringe
       injector:
         maxVol: 5
   - type: Injector
-    minTransferAmount: 1
-    maxTransferAmount: 5
-    transferAmount: 5
+    transferAmounts:
+    - 1
+    - 2
+    - 3
+    - 4
+    - 5
+    currentTransferAmount: 5
   - type: SolutionContainerVisuals
     maxFillLevels: 3
     fillBaseName: minisyringe
     - SyringeGunAmmo
 
 - type: entity
+  abstract: true
   parent: BaseSyringe
   id: PrefilledSyringe
   components:
         canReact: false
   - type: Injector
     injectOnly: false
-    minTransferAmount: 5
-    maxTransferAmount: 10
-    transferAmount: 10
+    transferAmounts:
+    - 5
+    - 10
+    currentTransferAmount: 10
   - type: Tag
     tags:
     - Syringe
     - Trash
 
-
 - type: entity
   name: pill
   parent: BaseItem
index 92715e9f2f8eea5e818cb9edcc28ae8fdaf2a5ca..0292d6872836cdc456f3f793ce5c4445e5ce53a6 100644 (file)
@@ -713,3 +713,6 @@ FoodDonutJellySlugcat: FoodDonutJellyScurret
 # 2025-08-11
 ClothingUniformJumpsuitChiefEngineerNT: ClothingUniformJumpsuitChiefEngineer
 ClothingUniformJumpsuitParamedicNT: ClothingUniformJumpsuitParamedic
+
+# 2025-08-29
+PrefilledSyringe: Syringe