]> git.smokeofanarchy.ru Git - space-station-14.git/commitdiff
Merge Injector & Hypospray Systems & Components (#41833)
authorSir Warock <67167466+SirWarock@users.noreply.github.com>
Sat, 20 Dec 2025 23:58:26 +0000 (00:58 +0100)
committerGitHub <noreply@github.com>
Sat, 20 Dec 2025 23:58:26 +0000 (23:58 +0000)
* Merge Injector & Hyposprays

* Fixes

* Requested Changes

* Preview

* Inclusion of Prototypes

* Fix

* small oversight

* Further fixes

* A few more fixes & Bluespacesyringe buff

Co-Authored-By: āda <177162775+iaada@users.noreply.github.com>
* Final Commit, hopefully

* Merge conflict no more

* YML fix

* Add required changes

Co-Authored-By: Princess Cheeseballs <66055347+Princess-Cheeseballs@users.noreply.github.com>
* cleanup warnings removal

* Bug fix & Maintainer Requests

Co-Authored-By: āda <177162775+iaada@users.noreply.github.com>
* Adhere to requested changes

Co-Authored-By: āda <177162775+iaada@users.noreply.github.com>
---------

Co-authored-by: āda <177162775+iaada@users.noreply.github.com>
Co-authored-by: Princess Cheeseballs <66055347+Princess-Cheeseballs@users.noreply.github.com>
Co-authored-by: Princess Cheeseballs <66055347+Pronana@users.noreply.github.com>
24 files changed:
Content.Client/Chemistry/EntitySystems/HyposprayStatusControlSystem.cs [deleted file]
Content.Client/Chemistry/EntitySystems/InjectorStatusControlSystem.cs [new file with mode: 0644]
Content.Client/Chemistry/EntitySystems/InjectorSystem.cs [deleted file]
Content.Client/Chemistry/UI/HyposprayStatusControl.cs [deleted file]
Content.Client/Chemistry/UI/InjectorStatusControl.cs
Content.Server/Chemistry/EntitySystems/InjectorSystem.cs [deleted file]
Content.Shared/Chemistry/Components/HyposprayComponent.cs [deleted file]
Content.Shared/Chemistry/Components/InjectorComponent.cs
Content.Shared/Chemistry/EntitySystems/HypospraySystem.cs [deleted file]
Content.Shared/Chemistry/EntitySystems/InjectorSystem.cs [new file with mode: 0644]
Content.Shared/Chemistry/EntitySystems/SharedInjectorSystem.cs [deleted file]
Content.Shared/Chemistry/Events/HyposprayEvents.cs [deleted file]
Content.Shared/Chemistry/Events/InjectorEvents.cs [new file with mode: 0644]
Content.Shared/Chemistry/Prototypes/InjectorModePrototype.cs [new file with mode: 0644]
Content.Shared/Clumsy/ClumsySystem.cs
Content.Shared/Fluids/SharedPuddleSystem.Spillable.cs
Content.Shared/Inventory/InventorySystem.Relay.cs
Resources/Locale/en-US/chemistry/components/hypospray-component.ftl [deleted file]
Resources/Locale/en-US/chemistry/components/injector-component.ftl
Resources/Prototypes/Chemistry/injector_modes.yml [new file with mode: 0644]
Resources/Prototypes/Entities/Clothing/Belt/job.yml
Resources/Prototypes/Entities/Objects/Specific/Medical/hypospray.yml
Resources/Prototypes/Entities/Objects/Specific/chemistry.yml
Resources/Prototypes/tags.yml

diff --git a/Content.Client/Chemistry/EntitySystems/HyposprayStatusControlSystem.cs b/Content.Client/Chemistry/EntitySystems/HyposprayStatusControlSystem.cs
deleted file mode 100644 (file)
index 4dfc850..0000000
+++ /dev/null
@@ -1,16 +0,0 @@
-using Content.Client.Chemistry.UI;
-using Content.Client.Items;
-using Content.Shared.Chemistry.Components;
-using Content.Shared.Chemistry.EntitySystems;
-
-namespace Content.Client.Chemistry.EntitySystems;
-
-public sealed class HyposprayStatusControlSystem : EntitySystem
-{
-    [Dependency] private readonly SharedSolutionContainerSystem _solutionContainers = default!;
-    public override void Initialize()
-    {
-        base.Initialize();
-        Subs.ItemStatus<HyposprayComponent>(ent => new HyposprayStatusControl(ent, _solutionContainers));
-    }
-}
diff --git a/Content.Client/Chemistry/EntitySystems/InjectorStatusControlSystem.cs b/Content.Client/Chemistry/EntitySystems/InjectorStatusControlSystem.cs
new file mode 100644 (file)
index 0000000..ca8685e
--- /dev/null
@@ -0,0 +1,20 @@
+using Content.Client.Chemistry.UI;
+using Content.Client.Items;
+using Content.Shared.Chemistry.Components;
+using Content.Shared.Chemistry.EntitySystems;
+using Robust.Shared.Prototypes;
+
+namespace Content.Client.Chemistry.EntitySystems;
+
+public sealed class InjectorStatusControlSystem : EntitySystem
+{
+    [Dependency] private readonly SharedSolutionContainerSystem _solutionContainers = default!;
+    [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
+
+
+    public override void Initialize()
+    {
+        base.Initialize();
+        Subs.ItemStatus<InjectorComponent>(injector => new InjectorStatusControl(injector, _solutionContainers, _prototypeManager));
+    }
+}
diff --git a/Content.Client/Chemistry/EntitySystems/InjectorSystem.cs b/Content.Client/Chemistry/EntitySystems/InjectorSystem.cs
deleted file mode 100644 (file)
index 58cb533..0000000
+++ /dev/null
@@ -1,16 +0,0 @@
-using Content.Client.Chemistry.UI;
-using Content.Client.Items;
-using Content.Shared.Chemistry.Components;
-using Content.Shared.Chemistry.EntitySystems;
-
-namespace Content.Client.Chemistry.EntitySystems;
-
-public sealed class InjectorSystem : SharedInjectorSystem
-{
-    public override void Initialize()
-    {
-        base.Initialize();
-
-        Subs.ItemStatus<InjectorComponent>(ent => new InjectorStatusControl(ent, SolutionContainer));
-    }
-}
diff --git a/Content.Client/Chemistry/UI/HyposprayStatusControl.cs b/Content.Client/Chemistry/UI/HyposprayStatusControl.cs
deleted file mode 100644 (file)
index a564bce..0000000
+++ /dev/null
@@ -1,58 +0,0 @@
-using Content.Client.Message;
-using Content.Client.Stylesheets;
-using Content.Shared.Chemistry.Components;
-using Content.Shared.Chemistry.EntitySystems;
-using Content.Shared.FixedPoint;
-using Robust.Client.UserInterface;
-using Robust.Client.UserInterface.Controls;
-using Robust.Shared.Timing;
-
-namespace Content.Client.Chemistry.UI;
-
-public sealed class HyposprayStatusControl : Control
-{
-    private readonly Entity<HyposprayComponent> _parent;
-    private readonly RichTextLabel _label;
-    private readonly SharedSolutionContainerSystem _solutionContainers;
-
-    private FixedPoint2 PrevVolume;
-    private FixedPoint2 PrevMaxVolume;
-    private bool PrevOnlyAffectsMobs;
-
-    public HyposprayStatusControl(Entity<HyposprayComponent> parent, SharedSolutionContainerSystem solutionContainers)
-    {
-        _parent = parent;
-        _solutionContainers = solutionContainers;
-        _label = new RichTextLabel { StyleClasses = { StyleClass.ItemStatus } };
-        AddChild(_label);
-    }
-
-    protected override void FrameUpdate(FrameEventArgs args)
-    {
-        base.FrameUpdate(args);
-
-        if (!_solutionContainers.TryGetSolution(_parent.Owner, _parent.Comp.SolutionName, out _, out var solution))
-            return;
-
-        // only updates the UI if any of the details are different than they previously were
-        if (PrevVolume == solution.Volume
-            && PrevMaxVolume == solution.MaxVolume
-            && PrevOnlyAffectsMobs == _parent.Comp.OnlyAffectsMobs)
-            return;
-
-        PrevVolume = solution.Volume;
-        PrevMaxVolume = solution.MaxVolume;
-        PrevOnlyAffectsMobs = _parent.Comp.OnlyAffectsMobs;
-
-        var modeStringLocalized = Loc.GetString((_parent.Comp.OnlyAffectsMobs && _parent.Comp.CanContainerDraw) switch
-        {
-            false => "hypospray-all-mode-text",
-            true => "hypospray-mobs-only-mode-text",
-        });
-
-        _label.SetMarkup(Loc.GetString("hypospray-volume-label",
-            ("currentVolume", solution.Volume),
-            ("totalVolume", solution.MaxVolume),
-            ("modeString", modeStringLocalized)));
-    }
-}
index 24f988bd354cf4ab0cdc2b6e73a8c83bcce5ad5c..0c57da7813a4e868f3e58c888dc6b56ffdd9d718 100644 (file)
@@ -2,26 +2,32 @@ using Content.Client.Message;
 using Content.Client.Stylesheets;
 using Content.Shared.Chemistry.Components;
 using Content.Shared.Chemistry.EntitySystems;
+using Content.Shared.Chemistry.Prototypes;
 using Content.Shared.FixedPoint;
 using Robust.Client.UserInterface;
 using Robust.Client.UserInterface.Controls;
+using Robust.Shared.Prototypes;
 using Robust.Shared.Timing;
 
 namespace Content.Client.Chemistry.UI;
 
 public sealed class InjectorStatusControl : Control
 {
+    private readonly IPrototypeManager _prototypeManager;
+
     private readonly Entity<InjectorComponent> _parent;
     private readonly SharedSolutionContainerSystem _solutionContainers;
     private readonly RichTextLabel _label;
 
-    private FixedPoint2 PrevVolume;
-    private FixedPoint2 PrevMaxVolume;
-    private FixedPoint2 PrevTransferAmount;
-    private InjectorToggleMode PrevToggleState;
+    private FixedPoint2 _prevVolume;
+    private FixedPoint2 _prevMaxVolume;
+    private FixedPoint2? _prevTransferAmount;
+    private InjectorBehavior _prevBehavior;
 
-    public InjectorStatusControl(Entity<InjectorComponent> parent, SharedSolutionContainerSystem solutionContainers)
+    public InjectorStatusControl(Entity<InjectorComponent> parent, SharedSolutionContainerSystem solutionContainers, IPrototypeManager prototypeManager)
     {
+        _prototypeManager  = prototypeManager;
+
         _parent = parent;
         _solutionContainers = solutionContainers;
         _label = new RichTextLabel { StyleClasses = { StyleClass.ItemStatus } };
@@ -32,33 +38,38 @@ public sealed class InjectorStatusControl : Control
     {
         base.FrameUpdate(args);
 
-        if (!_solutionContainers.TryGetSolution(_parent.Owner, _parent.Comp.SolutionName, out _, out var solution))
+        if (!_solutionContainers.TryGetSolution(_parent.Owner, _parent.Comp.SolutionName, out _, out var solution)
+            || !_prototypeManager.Resolve(_parent.Comp.ActiveModeProtoId, out var activeMode))
             return;
 
         // 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.CurrentTransferAmount
-            && PrevToggleState == _parent.Comp.ToggleState)
+        if (_prevVolume == solution.Volume
+            && _prevMaxVolume == solution.MaxVolume
+            && _prevTransferAmount == _parent.Comp.CurrentTransferAmount
+            && _prevBehavior == activeMode.Behavior)
             return;
 
-        PrevVolume = solution.Volume;
-        PrevMaxVolume = solution.MaxVolume;
-        PrevTransferAmount = _parent.Comp.CurrentTransferAmount;
-        PrevToggleState = _parent.Comp.ToggleState;
+        _prevVolume = solution.Volume;
+        _prevMaxVolume = solution.MaxVolume;
+        _prevTransferAmount = _parent.Comp.CurrentTransferAmount;
+        _prevBehavior = activeMode.Behavior;
 
         // Update current volume and injector state
-        var modeStringLocalized = Loc.GetString(_parent.Comp.ToggleState switch
+        // Seeing transfer volume is only important for injectors that can change it.
+        if (activeMode.TransferAmounts.Count > 1 && _parent.Comp.CurrentTransferAmount.HasValue)
         {
-            InjectorToggleMode.Draw => "injector-draw-text",
-            InjectorToggleMode.Inject => "injector-inject-text",
-            _ => "injector-invalid-injector-toggle-mode"
-        });
-
-        _label.SetMarkup(Loc.GetString("injector-volume-label",
-            ("currentVolume", solution.Volume),
-            ("totalVolume", solution.MaxVolume),
-            ("modeString", modeStringLocalized),
-            ("transferVolume", _parent.Comp.CurrentTransferAmount)));
+            _label.SetMarkup(Loc.GetString("injector-volume-transfer-label",
+                ("currentVolume", solution.Volume),
+                ("totalVolume", solution.MaxVolume),
+                ("modeString", Loc.GetString(activeMode.Name)),
+                ("transferVolume", _parent.Comp.CurrentTransferAmount.Value)));
+        }
+        else
+        {
+            _label.SetMarkup(Loc.GetString("injector-volume-label",
+                ("currentVolume", solution.Volume),
+                ("totalVolume", solution.MaxVolume),
+                ("modeString", Loc.GetString(activeMode.Name))));
+        }
     }
 }
diff --git a/Content.Server/Chemistry/EntitySystems/InjectorSystem.cs b/Content.Server/Chemistry/EntitySystems/InjectorSystem.cs
deleted file mode 100644 (file)
index 6088d01..0000000
+++ /dev/null
@@ -1,6 +0,0 @@
-
-using Content.Shared.Chemistry.EntitySystems;
-
-namespace Content.Server.Chemistry.EntitySystems;
-
-public sealed class InjectorSystem : SharedInjectorSystem;
diff --git a/Content.Shared/Chemistry/Components/HyposprayComponent.cs b/Content.Shared/Chemistry/Components/HyposprayComponent.cs
deleted file mode 100644 (file)
index e1e4f21..0000000
+++ /dev/null
@@ -1,60 +0,0 @@
-using Content.Shared.FixedPoint;
-using Robust.Shared.GameStates;
-using Robust.Shared.Audio;
-
-namespace Content.Shared.Chemistry.Components;
-
-/// <summary>
-///     Component that allows an entity instantly transfer liquids by interacting with objects that have solutions.
-/// </summary>
-[RegisterComponent, NetworkedComponent]
-[AutoGenerateComponentState]
-public sealed partial class HyposprayComponent : Component
-{
-    /// <summary>
-    ///     Solution that will be used by hypospray for injections.
-    /// </summary>
-    [DataField]
-    public string SolutionName = "hypospray";
-
-    /// <summary>
-    ///     Amount of the units that will be transfered.
-    /// </summary>
-    [AutoNetworkedField]
-    [DataField]
-    public FixedPoint2 TransferAmount = FixedPoint2.New(5);
-
-    /// <summary>
-    /// The delay to draw reagents using the hypospray.
-    /// If set, <see cref="RefillableSolutionComponent"/> RefillTime should probably have the same value.
-    /// </summary>
-    [DataField]
-    public float DrawTime = 0f;
-
-    /// <summary>
-    ///     Sound that will be played when injecting.
-    /// </summary>
-    [DataField]
-    public SoundSpecifier InjectSound = new SoundPathSpecifier("/Audio/Items/hypospray.ogg");
-
-    /// <summary>
-    /// Decides whether you can inject everything or just mobs.
-    /// </summary>
-    [AutoNetworkedField]
-    [DataField(required: true)]
-    public bool OnlyAffectsMobs = false;
-
-    /// <summary>
-    /// If this can draw from containers in mob-only mode.
-    /// </summary>
-    [AutoNetworkedField]
-    [DataField]
-    public bool CanContainerDraw = true;
-
-    /// <summary>
-    /// Whether or not the hypospray is able to draw from containers or if it's a single use
-    /// device that can only inject.
-    /// </summary>
-    [DataField]
-    public bool InjectOnly = false;
-}
index d3a0503c3c7720574212eb9d90040bfb3b30ca7d..e31d8d350317987d373a66c46cf4a2069c0d6632 100644 (file)
@@ -1,10 +1,10 @@
 using Content.Shared.Chemistry.EntitySystems;
+using Content.Shared.Chemistry.Prototypes;
 using Content.Shared.Chemistry.Reagent;
 using Content.Shared.DoAfter;
 using Content.Shared.FixedPoint;
 using Robust.Shared.GameStates;
 using Robust.Shared.Prototypes;
-using Robust.Shared.Serialization;
 
 namespace Content.Shared.Chemistry.Components;
 
@@ -14,11 +14,10 @@ namespace Content.Shared.Chemistry.Components;
 /// <remarks>
 /// Can optionally support both
 /// injection and drawing or just injection. Can inject/draw reagents from solution
-/// containers, and can directly inject into a mobs bloodstream.
+/// containers, and can directly inject into a mob's bloodstream.
 /// </remarks>
-/// <seealso cref="SharedInjectorSystem"/>
-/// <seealso cref="InjectorToggleMode"/>
-[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
+/// <seealso cref="InjectorModePrototype"/>
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState, Access(typeof(InjectorSystem))]
 public sealed partial class InjectorComponent : Component
 {
     /// <summary>
@@ -34,66 +33,44 @@ public sealed partial class InjectorComponent : Component
     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.
-    /// </summary>
-    [DataField]
-    public bool InjectOnly;
-
-    /// <summary>
-    /// Whether or not the injector is able to draw from or inject from mobs.
+    /// Amount to inject or draw on each usage.
     /// </summary>
     /// <remarks>
-    /// For example: droppers would ignore mobs.
+    /// If its set null, this injector is marked to inject its entire contents upon usage.
     /// </remarks>
-    [DataField]
-    public bool IgnoreMobs;
+    [DataField, AutoNetworkedField]
+    public FixedPoint2? CurrentTransferAmount = FixedPoint2.New(5);
 
-    /// <summary>
-    /// 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.
-    /// </remarks>
-    [DataField]
-    public bool IgnoreClosed = true;
 
     /// <summary>
-    /// The transfer amounts for the set-transfer verb.
+    /// The mode that this injector starts with on MapInit.
     /// </summary>
-    [DataField]
-    public List<FixedPoint2> TransferAmounts = new() { 1, 5, 10, 15 };
+    [DataField(required: true), AutoNetworkedField]
+    public ProtoId<InjectorModePrototype> ActiveModeProtoId;
 
     /// <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.
+    /// The possible <see cref="InjectorModePrototype"/> that it can switch between.
     /// </summary>
-    [DataField, AutoNetworkedField]
-    public FixedPoint2 CurrentTransferAmount = FixedPoint2.New(5);
+    [DataField(required: true)]
+    public List<ProtoId<InjectorModePrototype>> AllowedModes;
 
     /// <summary>
-    /// Injection delay (seconds) when the target is a mob.
+    /// Whether the injector is able to draw from or inject from mobs.
     /// </summary>
-    /// <remarks>
-    /// The base delay has a minimum of 1 second, but this will still be modified if the target is incapacitated or
-    /// in combat mode.
-    /// </remarks>
+    /// <example>
+    /// Droppers ignore mobs.
+    /// </example>
     [DataField]
-    public TimeSpan Delay = TimeSpan.FromSeconds(5);
+    public bool IgnoreMobs;
 
     /// <summary>
-    /// Each additional 1u after first 5u increases the delay by X seconds.
+    /// Whether the injector is able to draw from or inject into containers that are closed/sealed.
     /// </summary>
+    /// <example>
+    /// Droppers can't inject into closed cans.
+    /// </example>
     [DataField]
-    public TimeSpan DelayPerVolume = TimeSpan.FromSeconds(0.1);
-
-    /// <summary>
-    /// The state of the injector. Determines it's attack behavior. Containers must have the
-    /// right SolutionCaps to support injection/drawing. For InjectOnly injectors this should
-    /// only ever be set to Inject
-    /// </summary>
-    [DataField, AutoNetworkedField]
-    public InjectorToggleMode ToggleState = InjectorToggleMode.Draw;
+    public bool IgnoreClosed = true;
 
     /// <summary>
     /// Reagents that are allowed to be within this injector.
@@ -101,44 +78,29 @@ public sealed partial class InjectorComponent : Component
     /// A null ReagentWhitelist indicates all reagents are allowed.
     /// </summary>
     [DataField]
-    public List<ProtoId<ReagentPrototype>>? ReagentWhitelist = null;
+    public List<ProtoId<ReagentPrototype>>? ReagentWhitelist;
 
     #region Arguments for injection doafter
 
-    /// <inheritdoc cref=DoAfterArgs.NeedHand>
+    /// <inheritdoc cref="DoAfterArgs.NeedHand"/>
     [DataField]
     public bool NeedHand = true;
 
-    /// <inheritdoc cref=DoAfterArgs.BreakOnHandChange>
+    /// <inheritdoc cref="DoAfterArgs.BreakOnHandChange"/>
     [DataField]
     public bool BreakOnHandChange = true;
 
-    /// <inheritdoc cref=DoAfterArgs.MovementThreshold>
+    /// <inheritdoc cref="DoAfterArgs.MovementThreshold"/>
     [DataField]
     public float MovementThreshold = 0.1f;
 
     #endregion
 }
 
-/// <summary>
-/// Possible modes for an <see cref="InjectorComponent"/>.
-/// </summary>
-[Serializable, NetSerializable]
-public enum InjectorToggleMode : byte
+internal static class InjectorToggleModeExtensions
 {
-    /// <summary>
-    /// The injector will try to inject reagent into things.
-    /// </summary>
-    Inject,
-
-    /// <summary>
-    /// The injector will try to draw reagent from things.
-    /// </summary>
-    Draw,
+    public static bool HasAnyFlag(this InjectorBehavior s1, InjectorBehavior s2)
+    {
+        return (s1 & s2) != 0;
+    }
 }
-
-/// <summary>
-/// Raised on the injector when the doafter has finished.
-/// </summary>
-[Serializable, NetSerializable]
-public sealed partial class InjectorDoAfterEvent : SimpleDoAfterEvent;
diff --git a/Content.Shared/Chemistry/EntitySystems/HypospraySystem.cs b/Content.Shared/Chemistry/EntitySystems/HypospraySystem.cs
deleted file mode 100644 (file)
index e179fb5..0000000
+++ /dev/null
@@ -1,329 +0,0 @@
-using System.Diagnostics.CodeAnalysis;
-using Content.Shared.Administration.Logs;
-using Content.Shared.Chemistry.Components;
-using Content.Shared.Chemistry.Components.SolutionManager;
-using Content.Shared.Chemistry.Hypospray.Events;
-using Content.Shared.Database;
-using Content.Shared.DoAfter;
-using Content.Shared.FixedPoint;
-using Content.Shared.Forensics;
-using Content.Shared.IdentityManagement;
-using Content.Shared.Interaction.Events;
-using Content.Shared.Interaction;
-using Content.Shared.Mobs.Components;
-using Content.Shared.Popups;
-using Content.Shared.Timing;
-using Content.Shared.Verbs;
-using Content.Shared.Weapons.Melee.Events;
-using Robust.Shared.Audio.Systems;
-using Robust.Shared.Serialization;
-
-namespace Content.Shared.Chemistry.EntitySystems;
-
-public sealed class HypospraySystem : EntitySystem
-{
-    [Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
-    [Dependency] private readonly ReactiveSystem _reactiveSystem = default!;
-    [Dependency] private readonly SharedAudioSystem _audio = default!;
-    [Dependency] private readonly SharedPopupSystem _popup = default!;
-    [Dependency] private readonly SharedSolutionContainerSystem _solutionContainers = default!;
-    [Dependency] private readonly UseDelaySystem _useDelay = default!;
-    [Dependency] private readonly SharedDoAfterSystem _doAfter = default!;
-
-    public override void Initialize()
-    {
-        base.Initialize();
-
-        SubscribeLocalEvent<HyposprayComponent, AfterInteractEvent>(OnAfterInteract);
-        SubscribeLocalEvent<HyposprayComponent, MeleeHitEvent>(OnAttack);
-        SubscribeLocalEvent<HyposprayComponent, UseInHandEvent>(OnUseInHand);
-        SubscribeLocalEvent<HyposprayComponent, GetVerbsEvent<AlternativeVerb>>(AddToggleModeVerb);
-        SubscribeLocalEvent<HyposprayComponent, HyposprayDrawDoAfterEvent>(OnDrawDoAfter);
-    }
-
-    #region Ref events
-    private void OnUseInHand(Entity<HyposprayComponent> entity, ref UseInHandEvent args)
-    {
-        if (args.Handled)
-            return;
-
-        args.Handled = TryDoInject(entity, args.User, args.User);
-    }
-
-    private void OnAfterInteract(Entity<HyposprayComponent> entity, ref AfterInteractEvent args)
-    {
-        if (args.Handled || !args.CanReach || args.Target == null)
-            return;
-
-        args.Handled = TryUseHypospray(entity, args.Target.Value, args.User);
-    }
-
-    private void OnAttack(Entity<HyposprayComponent> entity, ref MeleeHitEvent args)
-    {
-        if (args.HitEntities is [])
-            return;
-
-        TryDoInject(entity, args.HitEntities[0], args.User);
-    }
-
-    private void OnDrawDoAfter(Entity<HyposprayComponent> entity, ref HyposprayDrawDoAfterEvent args)
-    {
-        if (args.Cancelled)
-            return;
-
-        if (entity.Comp.CanContainerDraw
-            && args.Target.HasValue
-            && !EligibleEntity(args.Target.Value, entity)
-            && _solutionContainers.TryGetDrawableSolution(args.Target.Value, out var drawableSolution, out _))
-        {
-            TryDraw(entity, args.Target.Value, drawableSolution.Value, args.User);
-        }
-    }
-
-    #endregion
-
-    #region Draw/Inject
-    private bool TryUseHypospray(Entity<HyposprayComponent> entity, EntityUid target, EntityUid user)
-    {
-        // if target is ineligible but is a container, try to draw from the container if allowed
-        if (entity.Comp.CanContainerDraw
-            && !EligibleEntity(target, entity)
-            && _solutionContainers.TryGetDrawableSolution(target, out var drawableSolution, out _))
-        {
-            return TryStartDraw(entity, target, drawableSolution.Value, user);
-        }
-
-        return TryDoInject(entity, target, user);
-    }
-
-    public bool TryDoInject(Entity<HyposprayComponent> entity, EntityUid target, EntityUid user)
-    {
-        var (uid, component) = entity;
-
-        if (!EligibleEntity(target, component))
-            return false;
-
-        if (TryComp(uid, out UseDelayComponent? delayComp))
-        {
-            if (_useDelay.IsDelayed((uid, delayComp)))
-                return false;
-        }
-
-        string? msgFormat = null;
-
-        // Self event
-        var selfEvent = new SelfBeforeHyposprayInjectsEvent(user, entity.Owner, target);
-        RaiseLocalEvent(user, selfEvent);
-
-        if (selfEvent.Cancelled)
-        {
-            _popup.PopupClient(Loc.GetString(selfEvent.InjectMessageOverride ?? "hypospray-cant-inject", ("owner", Identity.Entity(target, EntityManager))), target, user);
-            return false;
-        }
-
-        target = selfEvent.TargetGettingInjected;
-
-        if (!EligibleEntity(target, component))
-            return false;
-
-        // Target event
-        var targetEvent = new TargetBeforeHyposprayInjectsEvent(user, entity.Owner, target);
-        RaiseLocalEvent(target, targetEvent);
-
-        if (targetEvent.Cancelled)
-        {
-            _popup.PopupClient(Loc.GetString(targetEvent.InjectMessageOverride ?? "hypospray-cant-inject", ("owner", Identity.Entity(target, EntityManager))), target, user);
-            return false;
-        }
-
-        target = targetEvent.TargetGettingInjected;
-
-        if (!EligibleEntity(target, component))
-            return false;
-
-        // The target event gets priority for the overriden message.
-        if (targetEvent.InjectMessageOverride != null)
-            msgFormat = targetEvent.InjectMessageOverride;
-        else if (selfEvent.InjectMessageOverride != null)
-            msgFormat = selfEvent.InjectMessageOverride;
-        else if (target == user)
-            msgFormat = "hypospray-component-inject-self-message";
-
-        if (!_solutionContainers.TryGetSolution(uid, component.SolutionName, out var hypoSpraySoln, out var hypoSpraySolution) || hypoSpraySolution.Volume == 0)
-        {
-            _popup.PopupClient(Loc.GetString("hypospray-component-empty-message"), target, user);
-            return true;
-        }
-
-        if (!_solutionContainers.TryGetInjectableSolution(target, out var targetSoln, out var targetSolution))
-        {
-            _popup.PopupClient(Loc.GetString("hypospray-cant-inject", ("target", Identity.Entity(target, EntityManager))), target, user);
-            return false;
-        }
-
-        _popup.PopupClient(Loc.GetString(msgFormat ?? "hypospray-component-inject-other-message", ("other", Identity.Entity(target, EntityManager))), target, user);
-
-        if (target != user)
-        {
-            _popup.PopupEntity(Loc.GetString("hypospray-component-feel-prick-message"), target, target);
-            // TODO: This should just be using melee attacks...
-            // meleeSys.SendLunge(angle, user);
-        }
-
-        _audio.PlayPredicted(component.InjectSound, target, user);
-
-        // Medipens and such use this system and don't have a delay, requiring extra checks
-        // BeginDelay function returns if item is already on delay
-        if (delayComp != null)
-            _useDelay.TryResetDelay((uid, delayComp));
-
-        // Get transfer amount. May be smaller than component.TransferAmount if not enough room
-        var realTransferAmount = FixedPoint2.Min(component.TransferAmount, targetSolution.AvailableVolume);
-
-        if (realTransferAmount <= 0)
-        {
-            _popup.PopupClient(Loc.GetString("hypospray-component-transfer-already-full-message", ("owner", target)), target, user);
-            return true;
-        }
-
-        // Move units from attackSolution to targetSolution
-        var removedSolution = _solutionContainers.SplitSolution(hypoSpraySoln.Value, realTransferAmount);
-
-        if (!targetSolution.CanAddSolution(removedSolution))
-            return true;
-        _reactiveSystem.DoEntityReaction(target, removedSolution, ReactionMethod.Injection);
-        _solutionContainers.TryAddSolution(targetSoln.Value, removedSolution);
-
-        var ev = new TransferDnaEvent { Donor = target, Recipient = uid };
-        RaiseLocalEvent(target, ref ev);
-
-        // same LogType as syringes...
-        _adminLogger.Add(LogType.ForceFeed, $"{ToPrettyString(user):user} injected {ToPrettyString(target):target} with a solution {SharedSolutionContainerSystem.ToPrettyString(removedSolution):removedSolution} using a {ToPrettyString(uid):using}");
-
-        return true;
-    }
-
-    public bool TryStartDraw(Entity<HyposprayComponent> entity, EntityUid target, Entity<SolutionComponent> targetSolution, EntityUid user)
-    {
-        if (!_solutionContainers.TryGetSolution(entity.Owner, entity.Comp.SolutionName, out var soln))
-            return false;
-
-        if (!TryGetDrawAmount(entity, target, targetSolution, user,  soln.Value, out _))
-            return false;
-
-        var doAfterArgs = new DoAfterArgs(EntityManager, user, entity.Comp.DrawTime, new HyposprayDrawDoAfterEvent(), entity, target)
-        {
-            BreakOnDamage = true,
-            BreakOnMove = true,
-            NeedHand = true,
-            Hidden = true,
-        };
-
-        return _doAfter.TryStartDoAfter(doAfterArgs, out _);
-    }
-
-    private bool TryGetDrawAmount(Entity<HyposprayComponent> entity, EntityUid target, Entity<SolutionComponent> targetSolution, EntityUid user, Entity<SolutionComponent> solutionEntity, [NotNullWhen(true)] out FixedPoint2? amount)
-    {
-        amount = null;
-
-        if (solutionEntity.Comp.Solution.AvailableVolume == 0)
-        {
-            return false;
-        }
-
-        // 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(entity.Comp.TransferAmount, targetSolution.Comp.Solution.Volume,
-            solutionEntity.Comp.Solution.AvailableVolume);
-
-        if (realTransferAmount <= 0)
-        {
-            _popup.PopupClient(
-                Loc.GetString("injector-component-target-is-empty-message",
-                    ("target", Identity.Entity(target, EntityManager))),
-                entity.Owner, user);
-            return false;
-        }
-
-        amount = realTransferAmount;
-        return true;
-    }
-
-    private bool TryDraw(Entity<HyposprayComponent> entity, EntityUid target, Entity<SolutionComponent> targetSolution, EntityUid user)
-    {
-        if (!_solutionContainers.TryGetSolution(entity.Owner, entity.Comp.SolutionName, out var soln))
-            return false;
-
-        if (!TryGetDrawAmount(entity, target, targetSolution, user, soln.Value, out var amount))
-            return false;
-
-        var removedSolution = _solutionContainers.Draw(target, targetSolution, amount.Value);
-
-        if (!_solutionContainers.TryAddSolution(soln.Value, removedSolution))
-        {
-            return false;
-        }
-
-        _popup.PopupClient(Loc.GetString("injector-component-draw-success-message",
-            ("amount", removedSolution.Volume),
-            ("target", Identity.Entity(target, EntityManager))), entity.Owner, user);
-        return true;
-    }
-
-    private bool EligibleEntity(EntityUid entity, HyposprayComponent component)
-    {
-        // TODO: Does checking for BodyComponent make sense as a "can be hypospray'd" tag?
-        // In SS13 the hypospray ONLY works on mobs, NOT beakers or anything else.
-        // But this is 14, we dont do what SS13 does just because SS13 does it.
-        return component.OnlyAffectsMobs
-            ? HasComp<SolutionContainerManagerComponent>(entity) &&
-              HasComp<MobStateComponent>(entity)
-            : HasComp<SolutionContainerManagerComponent>(entity);
-    }
-
-    #endregion
-
-    #region Verbs
-
-    // <summary>
-    // Uses the OnlyMobs field as a check to implement the ability
-    // to draw from jugs and containers with the hypospray
-    // Toggleable to allow people to inject containers if they prefer it over drawing
-    // </summary>
-    private void AddToggleModeVerb(Entity<HyposprayComponent> entity, ref GetVerbsEvent<AlternativeVerb> args)
-    {
-        if (!args.CanAccess || !args.CanInteract || args.Hands == null || entity.Comp.InjectOnly)
-            return;
-
-        var user = args.User;
-        var verb = new AlternativeVerb
-        {
-            Text = Loc.GetString("hypospray-verb-mode-label"),
-            Act = () =>
-            {
-                ToggleMode(entity, user);
-            }
-        };
-        args.Verbs.Add(verb);
-    }
-
-    private void ToggleMode(Entity<HyposprayComponent> entity, EntityUid user)
-    {
-        SetMode(entity, !entity.Comp.OnlyAffectsMobs);
-        var msg = (entity.Comp.OnlyAffectsMobs && entity.Comp.CanContainerDraw) ? "hypospray-verb-mode-inject-mobs-only" : "hypospray-verb-mode-inject-all";
-        _popup.PopupClient(Loc.GetString(msg), entity, user);
-    }
-
-    public void SetMode(Entity<HyposprayComponent> entity, bool onlyAffectsMobs)
-    {
-        if (entity.Comp.OnlyAffectsMobs == onlyAffectsMobs)
-            return;
-
-        entity.Comp.OnlyAffectsMobs = onlyAffectsMobs;
-        Dirty(entity);
-    }
-
-    #endregion
-}
-
-[Serializable, NetSerializable]
-public sealed partial class HyposprayDrawDoAfterEvent : SimpleDoAfterEvent {}
diff --git a/Content.Shared/Chemistry/EntitySystems/InjectorSystem.cs b/Content.Shared/Chemistry/EntitySystems/InjectorSystem.cs
new file mode 100644 (file)
index 0000000..438da65
--- /dev/null
@@ -0,0 +1,762 @@
+using System.Linq;
+using Content.Shared.Administration.Logs;
+using Content.Shared.Body.Components;
+using Content.Shared.Chemistry.Components;
+using Content.Shared.Chemistry.Events;
+using Content.Shared.Chemistry.Prototypes;
+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.Nutrition.EntitySystems;
+using Content.Shared.Popups;
+using Content.Shared.Stacks;
+using Content.Shared.Standing;
+using Content.Shared.Timing;
+using Content.Shared.Verbs;
+using Content.Shared.Weapons.Melee.Events;
+using JetBrains.Annotations;
+using Robust.Shared.Audio.Systems;
+using Robust.Shared.Prototypes;
+
+namespace Content.Shared.Chemistry.EntitySystems;
+
+/// <summary>
+/// This handles toggling injection modes, injections and drawings for all kinds of injectors.
+/// </summary>
+/// <seealso cref="InjectorComponent"/>
+/// <seealso cref="InjectorModePrototype"/>
+public sealed partial class InjectorSystem : EntitySystem
+{
+    [Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
+    [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
+    [Dependency] private readonly SharedAudioSystem _audio = default!;
+    [Dependency] private readonly SharedForensicsSystem _forensics = default!;
+    [Dependency] private readonly SharedDoAfterSystem _doAfter = default!;
+    [Dependency] private readonly OpenableSystem _openable = default!;
+    [Dependency] private readonly SharedPopupSystem _popup = default!;
+    [Dependency] private readonly ReactiveSystem _reactiveSystem = default!;
+    [Dependency] private readonly SharedSolutionContainerSystem _solutionContainer = default!;
+    [Dependency] private readonly StandingStateSystem _standingState = default!;
+    [Dependency] private readonly UseDelaySystem _useDelay = default!;
+
+    public override void Initialize()
+    {
+        SubscribeLocalEvent<InjectorComponent, UseInHandEvent>(OnInjectorUse);
+        SubscribeLocalEvent<InjectorComponent, AfterInteractEvent>(OnInjectorAfterInteract);
+        SubscribeLocalEvent<InjectorComponent, InjectorDoAfterEvent>(OnInjectDoAfter);
+        SubscribeLocalEvent<InjectorComponent, MeleeHitEvent>(OnAttack);
+        SubscribeLocalEvent<InjectorComponent, GetVerbsEvent<AlternativeVerb>>(AddVerbs);
+    }
+
+    #region Events Handling
+    private void OnInjectorUse(Entity<InjectorComponent> injector, ref UseInHandEvent args)
+    {
+        if (args.Handled
+            || !_prototypeManager.Resolve(injector.Comp.ActiveModeProtoId, out var activeProto))
+            return;
+
+        if (activeProto.InjectOnUse) // Injectors that can't toggle transferAmounts will be used.
+            TryMobsDoAfter(injector, args.User, args.User);
+        else // Syringes toggle Draw/Inject.
+            ToggleMode(injector, args.User);
+
+        args.Handled = true;
+    }
+
+    private void OnInjectorAfterInteract(Entity<InjectorComponent> injector, ref AfterInteractEvent args)
+    {
+        if (args.Handled || !args.CanReach || args.Target is not { Valid: true } target)
+            return;
+
+        // Is the target a mob? If yes, use a do-after to give them time to respond.
+        if (HasComp<BloodstreamComponent>(target))
+        {
+            // Are use using an injector capable of targeting a mob?
+            if (injector.Comp.IgnoreMobs)
+            {
+                _popup.PopupClient(Loc.GetString("injector-component-ignore-mobs"), args.Target.Value, args.User);
+                return;
+            }
+
+            args.Handled = TryMobsDoAfter(injector, args.User, target);
+            return;
+        }
+
+        // Draw from or inject into jugs, bottles, etc.
+        args.Handled = ContainerDoAfter(injector, args.User, target);
+    }
+
+    private void OnInjectDoAfter(Entity<InjectorComponent> injector, ref InjectorDoAfterEvent args)
+    {
+        if (args.Cancelled || args.Handled || args.Args.Target == null)
+            return;
+
+        args.Handled = TryUseInjector(injector, args.Args.User, args.Args.Target.Value);
+    }
+
+    private void OnAttack(Entity<InjectorComponent> injector, ref MeleeHitEvent args)
+    {
+        if (args.HitEntities is [])
+            return;
+
+        TryMobsDoAfter(injector, args.User, args.HitEntities[0]);
+    }
+
+    /// <summary>
+    /// Give the user interaction verbs for their injector.
+    /// </summary>
+    /// <param name="injector"></param>
+    /// <param name="args"></param>
+    /// <remarks>
+    /// If they have multiple transferAmounts, they'll be able to switch between them via the verbs.
+    /// If they have multiple injector modes and don't toggle when used in hand, they can toggle the mode with the verbs too.
+    /// </remarks>
+    private void AddVerbs(Entity<InjectorComponent> injector, ref GetVerbsEvent<AlternativeVerb> args)
+    {
+        if (!args.CanAccess || !args.CanInteract || args.Hands == null
+            || !_prototypeManager.Resolve(injector.Comp.ActiveModeProtoId, out var activeMode))
+            return;
+
+        var user = args.User;
+        var min = activeMode.TransferAmounts.Min();
+        var max = activeMode.TransferAmounts.Max();
+        var cur = injector.Comp.CurrentTransferAmount;
+        var toggleAmount = cur == max ? min : max;
+
+        var priority = 0;
+
+        if (activeMode.TransferAmounts.Count > 1)
+        {
+            AlternativeVerb toggleVerb = new()
+            {
+                Text = Loc.GetString("comp-solution-transfer-verb-toggle", ("amount", toggleAmount)),
+                Category = VerbCategory.SetTransferAmount,
+                Act = () =>
+                {
+                    injector.Comp.CurrentTransferAmount = toggleAmount;
+                    _popup.PopupClient(Loc.GetString("comp-solution-transfer-set-amount", ("amount", toggleAmount)), user, user);
+                    Dirty(injector);
+                },
+
+                Priority = priority
+            };
+            args.Verbs.Add(toggleVerb);
+
+            priority -= 1;
+
+            // Add specific transfer verbs for amounts defined in the component
+            foreach (var amount in activeMode.TransferAmounts)
+            {
+                AlternativeVerb verb = new()
+                {
+                    Text = Loc.GetString("comp-solution-transfer-verb-amount", ("amount", amount)),
+                    Category = VerbCategory.SetTransferAmount,
+                    Act = () =>
+                    {
+                        injector.Comp.CurrentTransferAmount = amount;
+                        _popup.PopupClient(Loc.GetString("comp-solution-transfer-set-amount", ("amount", amount)), user, user);
+                        Dirty(injector);
+                    },
+
+                    // we want to sort by size, not alphabetically by the verb text.
+                    Priority = priority
+                };
+                args.Verbs.Add(verb);
+            }
+        }
+
+        // If the injector cannot toggle via using in hand, allow toggling via verb.
+        if (!activeMode.InjectOnUse || injector.Comp.AllowedModes.Count <= 1)
+            return;
+
+        var toggleModeVerb = new AlternativeVerb
+        {
+            Text = Loc.GetString("injector-toggle-verb-text"),
+            Act = () =>
+            {
+                ToggleMode(injector, user);
+            },
+            Priority = priority,
+        };
+
+        args.Verbs.Add(toggleModeVerb);
+    }
+    #endregion Events Handling
+
+    #region Mob Interaction
+    /// <summary>
+    /// Send informative pop-up messages and wait for a do-after to complete.
+    /// </summary>
+    private bool TryMobsDoAfter(Entity<InjectorComponent> injector, EntityUid user, EntityUid target)
+    {
+        if (_useDelay.IsDelayed(injector.Owner) // Check for Delay.
+            || !GetMobsDoAfterTime(injector, user, target, out var doAfterTime, out var amount)) // Get the DoAfter time.
+            return false;
+
+        _doAfter.TryStartDoAfter(new DoAfterArgs(EntityManager, user, doAfterTime, 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,
+        });
+
+        // If the DoAfter was instant, don't send popups and logs indicating an attempt.
+        if (doAfterTime == TimeSpan.Zero)
+            return true;
+
+        if (!_solutionContainer.ResolveSolution(injector.Owner, injector.Comp.SolutionName, ref injector.Comp.Solution, out var injectorSolution)
+            || !_prototypeManager.Resolve(injector.Comp.ActiveModeProtoId, out var activeMode))
+            return false;
+
+        // Create a pop-up for the user.
+        _popup.PopupClient(Loc.GetString(activeMode.PopupUserAttempt), target, user);
+
+        if (user == target)
+        {
+            if (activeMode.Behavior.HasFlag(InjectorBehavior.Draw))
+            {
+                _adminLogger.Add(LogType.ForceFeed,
+                    $"{ToPrettyString(user):user} is attempting to draw {amount} units from themselves.");
+            }
+            else
+            {
+                _adminLogger.Add(LogType.Ingestion,
+                    $"{ToPrettyString(user):user} is attempting to inject themselves with a solution {SharedSolutionContainerSystem.ToPrettyString(injectorSolution):solution}.");
+            }
+        }
+        else
+        {
+            // Create a popup to the target.
+            var userName = Identity.Entity(user, EntityManager);
+            var popup = Loc.GetString(activeMode.PopupTargetAttempt, ("user", userName));
+            _popup.PopupEntity(popup, user, target);
+
+            if (activeMode.Behavior.HasFlag(InjectorBehavior.Draw))
+            {
+                _adminLogger.Add(LogType.ForceFeed,
+                    $"{ToPrettyString(user):user} is attempting to draw {amount} units from {ToPrettyString(target):target}");
+            }
+            else
+            {
+                _adminLogger.Add(LogType.ForceFeed,
+                    $"{ToPrettyString(user):user} is attempting to inject {ToPrettyString(target):target} with a solution {SharedSolutionContainerSystem.ToPrettyString(injectorSolution):solution}");
+            }
+        }
+
+        return true;
+    }
+
+    /// <summary>
+    /// Get the DoAfter Time for Mobs.
+    /// </summary>
+    /// <param name="injector">The injector that is interacting with the mob.</param>
+    /// <param name="user">The user using the injector.</param>
+    /// <param name="target">The target mob.</param>
+    /// <param name="doAfterTime">The duration of the resulting doAfter.</param>
+    /// <param name="amount">The amount of the reagents transferred.</param>
+    /// <returns>True if calculating the time was successful, false if not.</returns>
+    private bool GetMobsDoAfterTime(Entity<InjectorComponent> injector, EntityUid user, EntityUid target, out TimeSpan doAfterTime, out FixedPoint2 amount)
+    {
+        doAfterTime = TimeSpan.Zero;
+        amount = FixedPoint2.Zero;
+
+        if (!_solutionContainer.ResolveSolution(injector.Owner, injector.Comp.SolutionName, ref injector.Comp.Solution, out var injectorSolution)
+            || !_prototypeManager.Resolve(injector.Comp.ActiveModeProtoId, out var activeMode))
+            return false;
+
+        doAfterTime = activeMode.MobTime;
+
+        // Can only draw blood with a draw mode and a transferAmount.
+        if (activeMode.Behavior.HasFlag(InjectorBehavior.Draw) && injector.Comp.CurrentTransferAmount != null)
+        {
+            // additional delay is based on actual volume left to draw in syringe when smaller than transfer amount
+            amount = FixedPoint2.Min(injector.Comp.CurrentTransferAmount.Value, injectorSolution.AvailableVolume);
+        }
+        else
+        {
+            // additional delay is based on actual volume left to inject in syringe when smaller than transfer amount
+            // If CurrentTransferAmount is null, it'll want to inject its entire contents, e.g., epipens.
+            amount = injector.Comp.CurrentTransferAmount ?? injectorSolution.Volume;
+            amount = FixedPoint2.Min(amount, injectorSolution.Volume);
+        }
+
+        // Transfers over the IgnoreDelayForVolume amount take Xu times DelayPerVolume longer.
+        doAfterTime += activeMode.DelayPerVolume * FixedPoint2.Max(0, amount - activeMode.IgnoreDelayForVolume).Double();
+
+        // Check if the target is either the user or downed.
+        if (user == target) // Self-injections take priority.
+            doAfterTime *= activeMode.SelfModifier;
+        // Technically, both can be true, but that is probably a balance nightmare.
+        else if (_standingState.IsDown(target))
+            doAfterTime *= activeMode.DownedModifier;
+
+        return true;
+    }
+    #endregion Mob Interaction
+
+    #region Container Interaction
+    private bool ContainerDoAfter(Entity<InjectorComponent> injector, EntityUid user, EntityUid target)
+    {
+        if (!GetContainerDoAfterTime(injector, user, target, out var doAfterTime))
+            return false;
+
+        _doAfter.TryStartDoAfter(new DoAfterArgs(EntityManager, user, doAfterTime, 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,
+        });
+
+        return true;
+    }
+
+    /// <summary>
+    /// Get the DoAfter Time for Containers and check if it is possible.
+    /// </summary>
+    /// <param name="injector">The injector that is interacting with the container.</param>
+    /// <param name="user">The user using the injector.</param>
+    /// <param name="target">The target container,</param>
+    /// <param name="doAfterTime">The duration of the resulting DoAfter.</param>
+    /// <returns>True if calculating the time was successful, false if not.</returns>
+    private bool GetContainerDoAfterTime(Entity<InjectorComponent> injector, EntityUid user, EntityUid target, out TimeSpan doAfterTime)
+    {
+        doAfterTime = TimeSpan.Zero;
+
+        if (!_prototypeManager.Resolve(injector.Comp.ActiveModeProtoId, out var activeMode))
+            return false;
+
+        // Check if the Injector has a draw time, but only when drawing.
+        if (!activeMode.Behavior.HasAnyFlag(InjectorBehavior.Draw | InjectorBehavior.Dynamic))
+            return true;
+
+        if (!_solutionContainer.ResolveSolution(injector.Owner, injector.Comp.SolutionName, ref injector.Comp.Solution, out var solution)
+            || solution.AvailableVolume == 0)
+        {
+            _popup.PopupClient(Loc.GetString("injector-component-cannot-toggle-draw-message"), user, user);
+            return false; // If already full, fail drawing.
+        }
+
+        if (!_solutionContainer.TryGetDrawableSolution(target, out _, out var drawableSol))
+        {
+            _popup.PopupClient(Loc.GetString("injector-component-cannot-transfer-message", ("target", Identity.Entity(target, EntityManager))), injector, user);
+            return false;
+        }
+
+        if (drawableSol.Volume == 0)
+        {
+            _popup.PopupClient(Loc.GetString("injector-component-target-is-empty-message", ("target", Identity.Entity(target, EntityManager))), injector, user);
+            return false;
+        }
+
+        doAfterTime = activeMode.ContainerDrawTime;
+        return true;
+    }
+    #endregion Container Interaction
+
+    #region Injecting/Drawing
+    /// <summary>
+    /// Depending on the <see cref="InjectorBehavior"/>, this will deal with the result of the DoAfter and draw/inject accordingly.
+    /// </summary>
+    /// <param name="injector">The injector used.</param>
+    /// <param name="user">The entity using the injector.</param>
+    /// <param name="target">The entity targeted by the user.</param>
+    /// <returns>True if the injection/drawing was successful, false if not.</returns>
+    /// <exception cref="ArgumentOutOfRangeException">The injector has a different <see cref="InjectorBehavior"/>.</exception>
+    private bool TryUseInjector(Entity<InjectorComponent> injector, EntityUid user, EntityUid target)
+    {
+        if (!_prototypeManager.Resolve(injector.Comp.ActiveModeProtoId, out var activeMode))
+            return false;
+
+        var isOpenOrIgnored = injector.Comp.IgnoreClosed || !_openable.IsClosed(target);
+
+        LocId msg = target == user ? "injector-component-cannot-transfer-message-self" : "injector-component-cannot-transfer-message";
+
+        switch (activeMode.Behavior)
+        {
+            // Handle injecting/drawing for solutions
+            case InjectorBehavior.Inject:
+            {
+                if (isOpenOrIgnored && _solutionContainer.TryGetInjectableSolution(target, out var injectableSolution, out _))
+                    return TryInject(injector, user, target, injectableSolution.Value, false);
+
+                if (isOpenOrIgnored && _solutionContainer.TryGetRefillableSolution(target, out var refillableSolution, out _))
+                    return TryInject(injector, user, target, refillableSolution.Value, true);
+                break;
+            }
+            case InjectorBehavior.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, user, (target, stream), stream.BloodSolution.Value);
+                }
+
+                // Draw from an object (food, beaker, etc)
+                if (isOpenOrIgnored && _solutionContainer.TryGetDrawableSolution(target, out var drawableSolution, out _))
+                    return TryDraw(injector, user, target, drawableSolution.Value);
+
+                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, user);
+                break;
+            }
+            case InjectorBehavior.Dynamic:
+            {
+                // If it's a mob, inject. We're using injectableSolution so I don't have to code a sole method for injecting into bloodstreams.
+                if (HasComp<BloodstreamComponent>(target)
+                    && _solutionContainer.TryGetInjectableSolution(target, out var injectableSolution, out _))
+                {
+                    return TryInject(injector, user, target, injectableSolution.Value, false);
+                }
+
+                // Draw from an object (food, beaker, etc.)
+                if (isOpenOrIgnored && _solutionContainer.TryGetDrawableSolution(target, out var drawableSolution, out _))
+                    return TryDraw(injector, user, target, drawableSolution.Value);
+                break;
+            }
+            default:
+                throw new ArgumentOutOfRangeException();
+        }
+
+        _popup.PopupClient(Loc.GetString(msg, ("target", Identity.Entity(target, EntityManager))), injector, user);
+        return false;
+    }
+
+    /// <summary>
+    /// Attempt to inject the solution of the injector into the target.
+    /// </summary>
+    /// <param name="injector">The injector used.</param>
+    /// <param name="user">The entity using the injector.</param>
+    /// <param name="target">The entity targeted by the user.</param>
+    /// <param name="targetSolution">The solution of the target.</param>
+    /// <param name="asRefill">Whether or not the solution is refillable or injectable.</param>
+    /// <returns>True if the injection was successful, false if not.</returns>
+    private bool TryInject(Entity<InjectorComponent> injector, EntityUid user, EntityUid target, Entity<SolutionComponent> targetSolution, bool asRefill)
+    {
+        if (!_solutionContainer.ResolveSolution(injector.Owner,
+                injector.Comp.SolutionName,
+                ref injector.Comp.Solution,
+                out var injectorSolution) || injectorSolution.Volume == 0)
+        {
+            // If empty, show a popup.
+            _popup.PopupClient(Loc.GetString("injector-component-empty-message", ("injector", injector)), user, user);
+            return false;
+        }
+
+        if (!_prototypeManager.Resolve(injector.Comp.ActiveModeProtoId, out var activeMode))
+            return false;
+
+        var selfEv = new SelfBeforeInjectEvent(user, injector, target);
+        RaiseLocalEvent(user, selfEv);
+
+        if (selfEv.Cancelled)
+        {
+            // Clowns will now also fumble Syringes.
+            if (selfEv.OverrideMessage != null)
+                _popup.PopupPredicted(selfEv.OverrideMessage, user, user);
+            return true;
+        }
+
+        target = selfEv.TargetGettingInjected;
+
+        var ev = new TargetBeforeInjectEvent(user, injector, target);
+        RaiseLocalEvent(target, ref ev);
+
+        // Jugsuit blocking Hyposprays when
+        if (ev.Cancelled)
+        {
+            var userMessage = Loc.GetString("injector-component-blocked-user");
+            var otherMessage = Loc.GetString("injector-component-blocked-other", ("target", target), ("user", user));
+            _popup.PopupPredicted(userMessage, otherMessage, target, user, PopupType.SmallCaution);
+            return true;
+        }
+
+        // Get transfer amount. It may be smaller than _transferAmount if not enough room
+        var plannedTransferAmount = FixedPoint2.Min(injector.Comp.CurrentTransferAmount ?? injectorSolution.Volume, injectorSolution.Volume);
+        var realTransferAmount = FixedPoint2.Min(plannedTransferAmount, 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";
+
+        if (selfEv.OverrideMessage != null)
+            msgSuccess = selfEv.OverrideMessage;
+        else if (ev.OverrideMessage != null)
+            msgSuccess = ev.OverrideMessage;
+
+        _popup.PopupClient(Loc.GetString(msgSuccess, ("amount", removedSolution.Volume), ("target", Identity.Entity(target, EntityManager))), target, user);
+
+        // it is IMPERATIVE that when an injector is instant, that it has a pop-up.
+        if (activeMode.InjectPopupTarget != null && target != user)
+            _popup.PopupClient(Loc.GetString(activeMode.InjectPopupTarget), target, target);
+
+        // Some injectors like hyposprays have sound, some like syringes have not.
+        if (activeMode.InjectSound != null)
+            _audio.PlayPredicted(activeMode.InjectSound, injector, user);
+
+        // Log what happened.
+        _adminLogger.Add(LogType.ForceFeed, $"{ToPrettyString(user):user} injected {ToPrettyString(target):target} with a solution {SharedSolutionContainerSystem.ToPrettyString(removedSolution):removedSolution} using a {ToPrettyString(injector):using}");
+
+        AfterInject(injector, user, target);
+        return true;
+    }
+
+    /// <summary>
+    /// Attempt to draw reagents from a container.
+    /// </summary>
+    /// <param name="injector">The injector used.</param>
+    /// <param name="user">The entity using the injector.</param>
+    /// <param name="target">The entity targeted by the user.</param>
+    /// <param name="targetSolution">The solution of the target.</param>
+    /// <returns>True if the drawing was successful, false if not.</returns>
+    private bool TryDraw(Entity<InjectorComponent> injector, EntityUid user, Entity<BloodstreamComponent?> target, Entity<SolutionComponent> targetSolution)
+    {
+        if (!_solutionContainer.ResolveSolution(injector.Owner, injector.Comp.SolutionName, ref injector.Comp.Solution, out var solution) || solution.AvailableVolume == 0)
+        {
+            _popup.PopupClient("injector-component-cannot-toggle-draw-message", user, user);
+            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());
+        }
+
+        // If transferAmount is null, fallback to 5 units.
+        var plannedTransferAmount = injector.Comp.CurrentTransferAmount ?? FixedPoint2.New(5);
+        // Get transfer amount. It may be smaller than _transferAmount if not enough room, also make sure there's room in the injector
+        var realTransferAmount = FixedPoint2.Min(plannedTransferAmount,
+            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";
+            var targetIdentity = Identity.Entity(target, EntityManager);
+            _popup.PopupClient(Loc.GetString(msg, ("target", targetIdentity)), injector.Owner, user);
+            return false;
+        }
+
+        // We have some snowflaked behavior for streams.
+        if (target.Comp != null)
+        {
+            DrawFromBlood(injector, user, (target.Owner, target.Comp), injector.Comp.Solution.Value, realTransferAmount);
+            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";
+        var targetIdentitySuccess = Identity.Entity(target, EntityManager);
+        _popup.PopupClient(
+            Loc.GetString(msgSuccess, ("amount", removedSolution.Volume), ("target", targetIdentitySuccess)),
+            target,
+            user);
+
+        AfterDraw(injector, user, target);
+        return true;
+    }
+
+    /// <summary>
+    /// Attempt to draw blood from a mob.
+    /// </summary>
+    /// <param name="injector">The injector used.</param>
+    /// <param name="user">The entity using the injector.</param>
+    /// <param name="target">The entity targeted by the user.</param>
+    /// <param name="injectorSolution">The solution of the injector.</param>
+    /// <param name="transferAmount">The amount of blood to draw.</param>
+    private void DrawFromBlood(Entity<InjectorComponent> injector,
+        EntityUid user,
+        Entity<BloodstreamComponent> target,
+        Entity<SolutionComponent> injectorSolution,
+        FixedPoint2 transferAmount)
+    {
+        if (_solutionContainer.ResolveSolution(target.Owner, target.Comp.BloodSolutionName, ref target.Comp.BloodSolution))
+        {
+            var bloodTemp = _solutionContainer.SplitSolution(target.Comp.BloodSolution.Value, transferAmount);
+            _solutionContainer.TryAddSolution(injectorSolution, bloodTemp);
+        }
+
+        LocId msg = target.Owner == user ? "injector-component-draw-success-message-self" : "injector-component-draw-success-message";
+        var targetIdentity = Identity.Entity(target, EntityManager);
+        var finalMessage = Loc.GetString(msg, ("amount", transferAmount), ("target", targetIdentity));
+        _popup.PopupClient(finalMessage, target, user);
+
+        AfterDraw(injector, user, target);
+    }
+
+    /// <summary>
+    /// This handles logic like DNA and Delays after injection.
+    /// </summary>
+    /// <param name="injector">The injector used.</param>
+    /// <param name="user">The entity using the injector.</param>
+    /// <param name="target">The entity targeted by the user.</param>
+    private void AfterInject(Entity<InjectorComponent> injector, EntityUid user, EntityUid target)
+    {
+        // Leave some DNA from the injectee on it
+        _forensics.TransferDna(injector, target);
+        // Reset the delay, if present.
+
+        _useDelay.TryResetDelay(injector);
+
+        // 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)
+            return;
+
+        if (!_prototypeManager.Resolve(injector.Comp.ActiveModeProtoId, out var activeMode)
+            || activeMode.Behavior.HasFlag(InjectorBehavior.Dynamic))
+            return;
+
+        foreach (var mode in injector.Comp.AllowedModes)
+        {
+            if (!_prototypeManager.Resolve(mode, out var proto)
+                || !proto.Behavior.HasFlag(InjectorBehavior.Draw))
+                continue;
+
+            ToggleMode(injector, user, proto);
+            return;
+        }
+    }
+
+    /// <summary>
+    /// This handles logic like DNA after drawing.
+    /// </summary>
+    /// <param name="injector">The injector used.</param>
+    /// <param name="user">The entity using the injector.</param>
+    /// <param name="target">The entity targeted by the user.</param>
+    private void AfterDraw(Entity<InjectorComponent> injector, EntityUid user, EntityUid target)
+    {
+        // Leave some DNA from the drawee on it
+        _forensics.TransferDna(injector, target);
+
+        // Automatically set the 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)
+            return;
+
+        if (!_prototypeManager.Resolve(injector.Comp.ActiveModeProtoId, out var activeMode)
+            || activeMode.Behavior.HasFlag(InjectorBehavior.Dynamic))
+            return;
+
+        foreach (var mode in injector.Comp.AllowedModes)
+        {
+            if (!_prototypeManager.Resolve(mode, out var proto)
+                || !proto.Behavior.HasFlag(InjectorBehavior.Inject))
+                continue;
+
+            ToggleMode(injector, user, proto);
+            return;
+        }
+    }
+    #endregion Injecting/Drawing
+
+    #region Mode Toggling
+    /// <summary>
+    /// Toggle modes of the injector if possible.
+    /// </summary>
+    /// <param name="injector">The injector whose mode is to be toggled.</param>
+    /// <param name="user">The user toggling the mode.</param>
+    /// <param name="mode">The desired mode.</param>
+    /// <remarks>This will still check if the injector can use that mode.</remarks>
+    [PublicAPI]
+    public void ToggleMode(Entity<InjectorComponent> injector, EntityUid user, InjectorModePrototype mode)
+    {
+        var index = injector.Comp.AllowedModes.FindIndex(nextMode => mode == nextMode);
+
+        injector.Comp.ActiveModeProtoId = injector.Comp.AllowedModes[index];
+
+        if (!_prototypeManager.Resolve(injector.Comp.ActiveModeProtoId, out var newMode))
+            return;
+
+        var modeName = Loc.GetString(newMode.Name);
+        var message = Loc.GetString("injector-component-mode-changed-text", ("mode", modeName));
+        _popup.PopupClient(message, user, user);
+        Dirty(injector);
+    }
+
+    /// <summary>
+    /// Toggle the mode of the injector to the next allowed mode.
+    /// </summary>
+    /// <param name="injector">The injector whose mode is to be toggled.</param>
+    /// <param name="user">The user toggling the mode.</param>
+    [PublicAPI]
+    public void ToggleMode(Entity<InjectorComponent> injector, EntityUid user)
+    {
+        if (!_prototypeManager.Resolve(injector.Comp.ActiveModeProtoId, out var activeProto))
+            return;
+
+        string? errorMessage = null;
+
+        foreach (var allowedMode in injector.Comp.AllowedModes)
+        {
+            if (!_prototypeManager.Resolve(allowedMode, out var proto)
+                || proto.Behavior.HasFlag(activeProto.Behavior)
+                || !_solutionContainer.ResolveSolution(injector.Owner, injector.Comp.SolutionName, ref injector.Comp.Solution, out var solution))
+                continue;
+
+            if (proto.Behavior.HasFlag(InjectorBehavior.Inject) && solution.Volume == 0)
+            {
+                errorMessage = "injector-component-cannot-toggle-inject-message";
+                continue;
+            }
+
+            if (proto.Behavior.HasFlag(InjectorBehavior.Draw) && solution.AvailableVolume == 0)
+            {
+                errorMessage = "injector-component-cannot-toggle-draw-message";
+                continue;
+            }
+
+            ToggleMode(injector, user, proto);
+            return;
+        }
+        if (errorMessage != null)
+            _popup.PopupClient(Loc.GetString(errorMessage), user, user);
+    }
+    #endregion Mode Toggling
+}
diff --git a/Content.Shared/Chemistry/EntitySystems/SharedInjectorSystem.cs b/Content.Shared/Chemistry/EntitySystems/SharedInjectorSystem.cs
deleted file mode 100644 (file)
index e20045f..0000000
+++ /dev/null
@@ -1,536 +0,0 @@
-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;
-
-namespace Content.Shared.Chemistry.EntitySystems;
-
-public abstract class SharedInjectorSystem : EntitySystem
-{
-    [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, UseInHandEvent>(OnInjectorUse);
-        SubscribeLocalEvent<InjectorComponent, AfterInteractEvent>(OnInjectorAfterInteract);
-        SubscribeLocalEvent<InjectorComponent, InjectorDoAfterEvent>(OnInjectDoAfter);
-    }
-
-    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 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;
-        AlternativeVerb toggleVerb = new()
-        {
-            Text = Loc.GetString("comp-solution-transfer-verb-toggle", ("amount", toggleAmount)),
-            Category = VerbCategory.SetTransferAmount,
-            Act = () =>
-            {
-                ent.Comp.CurrentTransferAmount = toggleAmount;
-                _popup.PopupClient(Loc.GetString("comp-solution-transfer-set-amount", ("amount", toggleAmount)), user, user);
-                Dirty(ent);
-            },
-
-            Priority = priority
-        };
-        args.Verbs.Add(toggleVerb);
-
-        priority -= 1;
-
-        // Add specific transfer verbs for amounts defined in the component
-        foreach (var amount in ent.Comp.TransferAmounts)
-        {
-            AlternativeVerb verb = new()
-            {
-                Text = Loc.GetString("comp-solution-transfer-verb-amount", ("amount", amount)),
-                Category = VerbCategory.SetTransferAmount,
-                Act = () =>
-                {
-                    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.
-                Priority = priority
-            };
-
-            priority -= 1;
-
-            args.Verbs.Add(verb);
-        }
-    }
-
-    private void OnInjectorUse(Entity<InjectorComponent> ent, ref UseInHandEvent args)
-    {
-        if (args.Handled)
-            return;
-
-        Toggle(ent, args.User);
-        args.Handled = true;
-    }
-
-    private void OnInjectorAfterInteract(Entity<InjectorComponent> ent, 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>(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>
-    /// 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.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.BloodSolutionName,
-                ref target.Comp.BloodSolution, out var bloodSolution))
-        {
-            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, bloodSolution.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.BloodSolution.Value, realTransferAmount);
-
-        _blood.TryAddToBloodstream(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)
-    {
-        if (SolutionContainer.ResolveSolution(target.Owner, target.Comp.BloodSolutionName,
-                ref target.Comp.BloodSolution))
-        {
-            var bloodTemp = SolutionContainer.SplitSolution(target.Comp.BloodSolution.Value, transferAmount);
-            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 (!SolutionContainer.ResolveSolution(injector.Owner, injector.Comp.SolutionName, ref injector.Comp.Solution, out var solution))
-            return;
-
-        string msg;
-
-        switch (injector.Comp.ToggleState)
-        {
-            case InjectorToggleMode.Inject:
-                if (solution.AvailableVolume > 0) // If solution has empty space to fill up, allow toggling to draw
-                {
-                    SetMode(injector, InjectorToggleMode.Draw);
-                    msg = "injector-component-drawing-text";
-                }
-                else
-                {
-                    msg = "injector-component-cannot-toggle-draw-message";
-                }
-                break;
-            case InjectorToggleMode.Draw:
-                if (solution.Volume > 0) // If solution has anything in it, allow toggling to inject
-                {
-                    SetMode(injector, InjectorToggleMode.Inject);
-                    msg = "injector-component-injecting-text";
-                }
-                else
-                {
-                    msg = "injector-component-cannot-toggle-inject-message";
-                }
-                break;
-            default:
-                throw new ArgumentOutOfRangeException();
-        }
-
-        _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;
-        Dirty(injector);
-    }
-}
diff --git a/Content.Shared/Chemistry/Events/HyposprayEvents.cs b/Content.Shared/Chemistry/Events/HyposprayEvents.cs
deleted file mode 100644 (file)
index 33293a4..0000000
+++ /dev/null
@@ -1,38 +0,0 @@
-using Content.Shared.Inventory;
-
-namespace Content.Shared.Chemistry.Hypospray.Events;
-
-public abstract partial class BeforeHyposprayInjectsTargetEvent : CancellableEntityEventArgs, IInventoryRelayEvent
-{
-    public SlotFlags TargetSlots { get; } = SlotFlags.WITHOUT_POCKET;
-    public EntityUid EntityUsingHypospray;
-    public readonly EntityUid Hypospray;
-    public EntityUid TargetGettingInjected;
-    public string? InjectMessageOverride;
-
-    public BeforeHyposprayInjectsTargetEvent(EntityUid user, EntityUid hypospray, EntityUid target)
-    {
-        EntityUsingHypospray = user;
-        Hypospray = hypospray;
-        TargetGettingInjected = target;
-        InjectMessageOverride = null;
-    }
-}
-
-/// <summary>
-///     This event is raised on the user using the hypospray before the hypospray is injected.
-///     The event is triggered on the user and all their clothing.
-/// </summary>
-public sealed class SelfBeforeHyposprayInjectsEvent : BeforeHyposprayInjectsTargetEvent
-{
-    public SelfBeforeHyposprayInjectsEvent(EntityUid user, EntityUid hypospray, EntityUid target) : base(user, hypospray, target) { }
-}
-
-/// <summary>
-///     This event is raised on the target before the hypospray is injected.
-///     The event is triggered on the target itself and all its clothing.
-/// </summary>
-public sealed class TargetBeforeHyposprayInjectsEvent : BeforeHyposprayInjectsTargetEvent
-{
-    public TargetBeforeHyposprayInjectsEvent(EntityUid user, EntityUid hypospray, EntityUid target) : base(user, hypospray, target) { }
-}
diff --git a/Content.Shared/Chemistry/Events/InjectorEvents.cs b/Content.Shared/Chemistry/Events/InjectorEvents.cs
new file mode 100644 (file)
index 0000000..ce4ccb8
--- /dev/null
@@ -0,0 +1,43 @@
+using Content.Shared.DoAfter;
+using Content.Shared.Inventory;
+using Robust.Shared.Serialization;
+
+namespace Content.Shared.Chemistry.Events;
+
+/// <summary>
+/// Raised on the injector when the doafter has finished.
+/// </summary>
+[Serializable, NetSerializable]
+public sealed partial class InjectorDoAfterEvent : SimpleDoAfterEvent;
+
+/// <summary>
+/// The base injection attempt event. It'll be raised on the user and target when attempting to inject the target.
+/// </summary>
+/// <param name="user">The user who is trying to inject the target.</param>
+/// <param name="usedInjector">The injector being used by the user.</param>
+/// <param name="target">The target who the user is trying to inject.</param>
+/// <param name="overrideMessage">The resulting message that gets displayed per popup.</param>
+public abstract partial class BeforeInjectTargetEvent(EntityUid user, EntityUid usedInjector, EntityUid target, string? overrideMessage = null)
+    : CancellableEntityEventArgs, IInventoryRelayEvent
+{
+    public EntityUid EntityUsingInjector = user;
+    public readonly EntityUid UsedInjector = usedInjector;
+    public EntityUid TargetGettingInjected = target;
+    public string? OverrideMessage = overrideMessage;
+    public SlotFlags TargetSlots => SlotFlags.WITHOUT_POCKET;
+}
+
+/// <summary>
+///     This event is raised on the user using the injector before the injector is injected.
+///     The event is triggered on the user and all their clothing.
+/// </summary>
+public sealed class SelfBeforeInjectEvent(EntityUid user, EntityUid usedInjector, EntityUid target, string? overrideMessage = null)
+    : BeforeInjectTargetEvent(user, usedInjector, target, overrideMessage);
+
+/// <summary>
+///     This event is raised on the target before the injector is injected.
+///     The event is triggered on the target itself and all its clothing.
+/// </summary>
+[ByRefEvent]
+public sealed class TargetBeforeInjectEvent(EntityUid user, EntityUid usedInjector, EntityUid target, string? overrideMessage = null)
+    : BeforeInjectTargetEvent(user, usedInjector, target, overrideMessage);
diff --git a/Content.Shared/Chemistry/Prototypes/InjectorModePrototype.cs b/Content.Shared/Chemistry/Prototypes/InjectorModePrototype.cs
new file mode 100644 (file)
index 0000000..8434309
--- /dev/null
@@ -0,0 +1,145 @@
+using Content.Shared.Chemistry.Components;
+using Content.Shared.FixedPoint;
+using Robust.Shared.Audio;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Serialization;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Array;
+
+namespace Content.Shared.Chemistry.Prototypes;
+
+/// <summary>
+/// This defines the behavior of an injector.
+/// Every injector requires this and it defines how much an injector injects, what transferamounts they can switch between, etc.
+/// </summary>
+[Prototype]
+public sealed partial class InjectorModePrototype : IPrototype, IInheritingPrototype
+{
+    /// <inheritdoc/>
+    [IdDataField]
+    public string ID { get; private set; } = default!;
+
+    /// <inheritdoc/>
+    [ParentDataField(typeof(AbstractPrototypeIdArraySerializer<InjectorModePrototype>))]
+    public string[]? Parents { get; }
+
+    /// <inheritdoc/>
+    [AbstractDataField, NeverPushInheritance]
+    public bool Abstract { get; }
+
+    /// <summary>
+    /// The name of the mode that will be shown on the label UI.
+    /// </summary>
+    [DataField(required: true)]
+    public LocId Name;
+
+    /// <summary>
+    /// If true, it'll inject the user when used in hand (Default Key: Y/Z)
+    /// </summary>
+    [DataField]
+    public bool InjectOnUse;
+
+    /// <summary>
+    /// The transfer amounts for the set-transfer verb.
+    /// </summary>
+    [DataField]
+    public List<FixedPoint2> TransferAmounts = new() { 1, 5, 10, 15 };
+
+    /// <summary>
+    /// Injection/Drawing delay (seconds) when the target is a mob.
+    /// </summary>
+    [DataField]
+    public TimeSpan MobTime = TimeSpan.FromSeconds(5);
+
+    /// <summary>
+    /// The delay to draw Reagents from Containers.
+    /// If set, <see cref="RefillableSolutionComponent"/> RefillTime should probably have the same value.
+    /// </summary>
+    [DataField]
+    public TimeSpan ContainerDrawTime = TimeSpan.Zero;
+
+
+    /// <summary>
+    /// The number to multiply <see cref="MobTime"/> and <see cref="DelayPerVolume"/> if the target is the downed.
+    /// Downed counts as crouching, buckled on a bed or critical.
+    /// </summary>
+    [DataField]
+    public float DownedModifier = 0.5f;
+
+    /// <summary>
+    /// The number to multiply <see cref="MobTime"/> and <see cref="DelayPerVolume"/> if the target is the user.
+    /// </summary>
+    [DataField]
+    public float SelfModifier = 0.5f;
+
+    /// <summary>
+    /// This delay will increase the DoAfter time for each Xu above <see cref="IgnoreDelayForVolume"/>.
+    /// </summary>
+    [DataField]
+    public TimeSpan DelayPerVolume = TimeSpan.FromSeconds(0.1);
+
+    /// <summary>
+    /// This works in tandem with <see cref="DelayPerVolume"/>.
+    /// </summary>
+    [DataField]
+    public FixedPoint2 IgnoreDelayForVolume = FixedPoint2.New(5);
+
+    /// <summary>
+    /// What message will be displayed to the user when attempting to inject someone.
+    /// </summary>
+    /// <remarks>
+    /// This is used for when you aren't injecting with a needle or an instant hypospray.
+    /// It would be weird if someone injects with a spray, but the popup says "needle".
+    /// </remarks>
+    [DataField]
+    public LocId PopupUserAttempt = "injector-component-needle-injecting-user";
+
+    /// <summary>
+    /// What message will be displayed to the target when someone attempts to inject into them.
+    /// </summary>
+    [DataField]
+    public LocId PopupTargetAttempt = "injector-component-needle-injecting-target";
+
+    /// <summary>
+    /// The state of the injector. Determines its attack behavior. Containers must have the
+    /// right SolutionCaps to support injection/drawing. For InjectOnly injectors this should
+    /// only ever be set to Inject
+    /// </summary>
+    [DataField]
+    public InjectorBehavior Behavior = InjectorBehavior.Inject;
+
+    /// <summary>
+    ///     Sound that will be played when injecting.
+    /// </summary>
+    [DataField]
+    public SoundSpecifier? InjectSound;
+
+    /// <summary>
+    /// A popup for the target upon a successful injection.
+    /// It's imperative that this is not null when <see cref="MobTime"/> is instant.
+    /// </summary>
+    [DataField]
+    public LocId? InjectPopupTarget;
+
+}
+
+/// <summary>
+/// Possible modes for an <see cref="InjectorModePrototype"/>.
+/// </summary>
+[Serializable, NetSerializable, Flags]
+public enum InjectorBehavior
+{
+    /// <summary>
+    /// The injector will try to inject reagent into things.
+    /// </summary>
+    Inject = 1 << 0,
+
+    /// <summary>
+    /// The injector will try to draw reagent from things.
+    /// </summary>
+    Draw = 1 << 1,
+
+    /// <summary>
+    /// The injector will draw from containers and inject into mobs.
+    /// </summary>
+    Dynamic = 1 << 2,
+}
index 35866b155afd7a160125ef4d01d92c0c29b364a9..4650065ad628eba9d4da490e80bcf2ef27b04f12 100644 (file)
@@ -1,5 +1,5 @@
 using Content.Shared.CCVar;
-using Content.Shared.Chemistry.Hypospray.Events;
+using Content.Shared.Chemistry.Events;
 using Content.Shared.Climbing.Components;
 using Content.Shared.Climbing.Events;
 using Content.Shared.Damage.Systems;
@@ -31,7 +31,7 @@ public sealed class ClumsySystem : EntitySystem
 
     public override void Initialize()
     {
-        SubscribeLocalEvent<ClumsyComponent, SelfBeforeHyposprayInjectsEvent>(BeforeHyposprayEvent);
+        SubscribeLocalEvent<ClumsyComponent, SelfBeforeInjectEvent>(BeforeHyposprayEvent);
         SubscribeLocalEvent<ClumsyComponent, SelfBeforeDefibrillatorZapsEvent>(BeforeDefibrillatorZapsEvent);
         SubscribeLocalEvent<ClumsyComponent, SelfBeforeGunShotEvent>(BeforeGunShotEvent);
         SubscribeLocalEvent<ClumsyComponent, CatchAttemptEvent>(OnCatchAttempt);
@@ -40,7 +40,7 @@ public sealed class ClumsySystem : EntitySystem
 
     // If you add more clumsy interactions add them in this section!
     #region Clumsy interaction events
-    private void BeforeHyposprayEvent(Entity<ClumsyComponent> ent, ref SelfBeforeHyposprayInjectsEvent args)
+    private void BeforeHyposprayEvent(Entity<ClumsyComponent> ent, ref SelfBeforeInjectEvent args)
     {
         // Clumsy people sometimes inject themselves! Apparently syringes are clumsy proof...
 
@@ -54,9 +54,9 @@ public sealed class ClumsySystem : EntitySystem
         if (!rand.Prob(ent.Comp.ClumsyDefaultCheck))
             return;
 
-        args.TargetGettingInjected = args.EntityUsingHypospray;
-        args.InjectMessageOverride = Loc.GetString(ent.Comp.HypoFailedMessage);
-        _audio.PlayPredicted(ent.Comp.ClumsySound, ent, args.EntityUsingHypospray);
+        args.TargetGettingInjected = args.EntityUsingInjector;
+        args.OverrideMessage = Loc.GetString(ent.Comp.HypoFailedMessage);
+        _audio.PlayPredicted(ent.Comp.ClumsySound, ent, args.EntityUsingInjector);
     }
 
     private void BeforeDefibrillatorZapsEvent(Entity<ClumsyComponent> ent, ref SelfBeforeDefibrillatorZapsEvent args)
index 05062aed2e818ad7e5214b19eca59b34bb67fc9f..9dcd965492024fe6c1ad73acb059923ee7fc5239 100644 (file)
@@ -1,6 +1,7 @@
 using Content.Shared.Chemistry;
 using Content.Shared.Chemistry.Components;
 using Content.Shared.Chemistry.EntitySystems;
+using Content.Shared.Chemistry.Prototypes;
 using Content.Shared.Chemistry.Reaction;
 using Content.Shared.CombatMode.Pacification;
 using Content.Shared.Database;
@@ -22,6 +23,7 @@ namespace Content.Shared.Fluids;
 public abstract partial class SharedPuddleSystem
 {
     private static readonly FixedPoint2 MeleeHitTransferProportion = 0.25;
+    [Dependency] private readonly InjectorSystem _injectorSystem = default!;
 
     protected virtual void InitializeSpillable()
     {
@@ -68,6 +70,7 @@ public abstract partial class SharedPuddleSystem
         if (entity.Comp.SpillDelay == null)
         {
             var target = args.Target;
+            var user = args.User;
             verb.Act = () =>
             {
                 var puddleSolution = _solutionContainerSystem.SplitSolution(soln.Value, solution.Volume);
@@ -75,9 +78,21 @@ public abstract partial class SharedPuddleSystem
 
                 // TODO: Make this an event subscription once spilling puddles is predicted.
                 // Injectors should not be hardcoded here.
-                if (TryComp<InjectorComponent>(entity, out var injectorComp))
+                if (TryComp<InjectorComponent>(entity, out var injectorComp)
+                    && _prototypeManager.Resolve(injectorComp.ActiveModeProtoId, out var activeMode)
+                    && !activeMode.Behavior.HasFlag(InjectorBehavior.Draw))
                 {
-                    injectorComp.ToggleState = InjectorToggleMode.Draw;
+                    foreach (var mode in injectorComp.AllowedModes)
+                    {
+                        if (!_prototypeManager.Resolve(mode, out var protoMode))
+                            continue;
+
+                        if (protoMode.Behavior.HasAnyFlag(InjectorBehavior.Draw | InjectorBehavior.Dynamic))
+                        {
+                            _injectorSystem.ToggleMode((entity, injectorComp), user, protoMode);
+                            break;
+                        }
+                    }
                     Dirty(entity, injectorComp);
                 }
             };
index 11b7f6113070f815d94d8e3436ed21feaef98986..242e8d0de9a8b90019c74a4c801b659f55dea3ab 100644 (file)
@@ -2,10 +2,9 @@ using Content.Shared.Armor;
 using Content.Shared.Atmos;
 using Content.Shared.Chat;
 using Content.Shared.Chemistry;
-using Content.Shared.Chemistry.Hypospray.Events;
+using Content.Shared.Chemistry.Events;
 using Content.Shared.Climbing.Events;
 using Content.Shared.Contraband;
-using Content.Shared.Damage;
 using Content.Shared.Damage.Events;
 using Content.Shared.Damage.Systems;
 using Content.Shared.Electrocution;
@@ -48,8 +47,8 @@ public partial class InventorySystem
         SubscribeLocalEvent<InventoryComponent, GetDefaultRadioChannelEvent>(RelayInventoryEvent);
         SubscribeLocalEvent<InventoryComponent, RefreshNameModifiersEvent>(RelayInventoryEvent);
         SubscribeLocalEvent<InventoryComponent, TransformSpeakerNameEvent>(RelayInventoryEvent);
-        SubscribeLocalEvent<InventoryComponent, SelfBeforeHyposprayInjectsEvent>(RelayInventoryEvent);
-        SubscribeLocalEvent<InventoryComponent, TargetBeforeHyposprayInjectsEvent>(RelayInventoryEvent);
+        SubscribeLocalEvent<InventoryComponent, SelfBeforeInjectEvent>(RelayInventoryEvent);
+        SubscribeLocalEvent<InventoryComponent, BeforeInjectTargetEvent>(RelayInventoryEvent);
         SubscribeLocalEvent<InventoryComponent, SelfBeforeGunShotEvent>(RelayInventoryEvent);
         SubscribeLocalEvent<InventoryComponent, SelfBeforeClimbEvent>(RelayInventoryEvent);
         SubscribeLocalEvent<InventoryComponent, CoefficientQueryEvent>(RelayInventoryEvent);
diff --git a/Resources/Locale/en-US/chemistry/components/hypospray-component.ftl b/Resources/Locale/en-US/chemistry/components/hypospray-component.ftl
deleted file mode 100644 (file)
index 36d229e..0000000
+++ /dev/null
@@ -1,20 +0,0 @@
-## UI
-
-hypospray-all-mode-text = Only Injects
-hypospray-mobs-only-mode-text = Draws and Injects
-hypospray-invalid-text = Invalid
-hypospray-volume-label = Volume: [color=white]{$currentVolume}/{$totalVolume}u[/color]
-    Mode: [color=white]{$modeString}[/color]
-
-## Entity
-
-hypospray-component-inject-other-message = You inject {THE($other)}.
-hypospray-component-inject-self-message = You inject yourself.
-hypospray-component-empty-message = Nothing to inject.
-hypospray-component-feel-prick-message = You feel a tiny prick!
-hypospray-component-transfer-already-full-message = {$owner} is already full!
-hypospray-cant-inject = Can't inject into {$target}!
-
-hypospray-verb-mode-label = Toggle Container Draw
-hypospray-verb-mode-inject-all = You cannot draw from containers anymore.
-hypospray-verb-mode-inject-mobs-only = You can now draw from containers.
index 53387ea1a45b5fef5d36a4575ca24d9f617e3cd4..5dc12a1f7df2cbbf7a0e8c0f2b0c84b082e14319 100644 (file)
@@ -1,37 +1,50 @@
 ## UI
 
-injector-draw-text = Draw
-injector-inject-text = Inject
-injector-invalid-injector-toggle-mode = Invalid
-injector-volume-label = Volume: [color=white]{$currentVolume}/{$totalVolume}[/color]
+injector-volume-transfer-label = Volume: [color=white]{$currentVolume}/{$totalVolume}u[/color]
     Mode: [color=white]{$modeString}[/color] ([color=white]{$transferVolume}u[/color])
+injector-volume-label = Volume: [color=white]{$currentVolume}/{$totalVolume}u[/color]
+    Mode: [color=white]{$modeString}[/color]
+injector-toggle-verb-text = Toggle Injector Mode
 
 ## Entity
 
-injector-component-drawing-text = Now drawing
-injector-component-injecting-text = Now injecting
-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-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-mode-name = Inject
+injector-component-draw-mode-name = Draw
+injector-component-dynamic-mode-name = Dynamic
+injector-component-mode-changed-text = Now {$mode}
 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.
+
+## Fail Messages
+
 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!
+injector-component-cannot-toggle-dynamic-message = Can't toggle dynamic!
+injector-component-empty-message = {CAPITALIZE(THE($injector))} is empty!
+injector-component-blocked-user = Protective gear blocked your injection!
+injector-component-blocked-other = {CAPITALIZE(THE(POSS-ADJ($target)))} armor blocked {THE($user)}'s injection!
+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-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-ignore-mobs = This injector can only interact with containers!
 
 ## mob-inject doafter messages
 
-injector-component-drawing-user = You start drawing the needle.
-injector-component-injecting-user = You start injecting the needle.
-injector-component-drawing-target = {CAPITALIZE(THE($user))} is trying to use a needle to draw from you!
-injector-component-injecting-target = {CAPITALIZE(THE($user))} is trying to inject a needle into you!
+injector-component-needle-injecting-user = You start injecting the needle.
+injector-component-needle-injecting-target = {CAPITALIZE(THE($user))} is trying to inject a needle into you!
+injector-component-needle-drawing-user = You start drawing the needle.
+injector-component-needle-drawing-target = {CAPITALIZE(THE($user))} is trying to use a needle to draw from you!
+
+## Target Popup Success messages
+injector-component-feel-prick-message = You feel a tiny prick!
diff --git a/Resources/Prototypes/Chemistry/injector_modes.yml b/Resources/Prototypes/Chemistry/injector_modes.yml
new file mode 100644 (file)
index 0000000..2a791eb
--- /dev/null
@@ -0,0 +1,118 @@
+## Abstracts
+- type: injectorMode
+  abstract: true
+  id: BaseInjectMode
+  name: injector-component-inject-mode-name
+  behavior: Inject
+  popupUserAttempt: injector-component-needle-injecting-user
+  popupTargetAttempt: injector-component-needle-injecting-target
+
+- type: injectorMode
+  abstract: true
+  id: BaseDrawMode
+  name: injector-component-draw-mode-name
+  behavior: Draw
+  popupUserAttempt: injector-component-needle-drawing-user
+  popupTargetAttempt: injector-component-needle-drawing-target
+
+- type: injectorMode
+  abstract: true
+  id: BaseDynamicMode
+  name: injector-component-dynamic-mode-name
+  behavior: Dynamic
+  popupUserAttempt: injector-component-needle-injecting-user
+  popupTargetAttempt: injector-component-needle-injecting-target
+
+## Syringes
+- type: injectorMode
+  abstract: true
+  id: BaseSyringeMode
+  transferAmounts:
+  - 5
+  - 10
+  - 15
+
+- type: injectorMode
+  abstract: true
+  id: BaseCryostasisSyringeMode
+  transferAmounts:
+  - 5
+  - 10
+
+- type: injectorMode
+  abstract: true
+  id: BaseBluespaceSyringeMode
+  mobTime: 2.5
+  transferAmounts:
+  - 5
+  - 10
+  - 15
+  - 50
+
+- type: injectorMode
+  parent: [ BaseSyringeMode, BaseInjectMode ]
+  id: SyringeInjectMode
+
+- type: injectorMode
+  parent: [ BaseSyringeMode, BaseDrawMode ]
+  id: SyringeDrawMode
+
+- type: injectorMode
+  parent: [ BaseBluespaceSyringeMode, BaseInjectMode ]
+  id: BluespaceSyringeInjectMode
+
+- type: injectorMode
+  parent: [ BaseBluespaceSyringeMode, BaseDrawMode ]
+  id: BluespaceSyringeDrawMode
+
+- type: injectorMode
+  parent: [ BaseCryostasisSyringeMode, BaseInjectMode ]
+  id: CryostasisSyringeInjectMode
+
+- type: injectorMode
+  parent: [ BaseCryostasisSyringeMode, BaseDrawMode ]
+  id: CryostasisSyringeDrawMode
+
+## Dropper
+- type: injectorMode
+  abstract: true
+  id: BaseDropperMode
+  transferAmounts:
+  - 1
+  - 2
+  - 3
+  - 4
+  - 5
+
+- type: injectorMode
+  parent: [ BaseDropperMode, BaseInjectMode ]
+  id: DropperInjectMode
+
+- type: injectorMode
+  parent: [ BaseDropperMode, BaseDrawMode ]
+  id: DropperDrawMode
+
+## Hyposprays
+- type: injectorMode
+  abstract: true
+  id: HyposprayBaseMode
+  injectSound: /Audio/Items/hypospray.ogg
+  injectPopupTarget: injector-component-feel-prick-message
+  injectOnUse: true
+  mobTime: 0
+  delayPerVolume: 0
+  transferAmounts:
+  - 5
+
+- type: injectorMode
+  parent: [ HyposprayBaseMode, BaseInjectMode ]
+  id: HyposprayInjectMode
+
+- type: injectorMode
+  parent: [ HyposprayBaseMode, BaseDynamicMode ]
+  id: HyposprayDynamicMode
+
+- type: injectorMode
+  parent: HyposprayDynamicMode
+  id: HypopenDynamicMode
+  containerDrawTime: 0.75
index 820a59e019756bc4a5b75b396ef209765c4079b2..a40740f46a219a983b75929c6a52e1a83236fdf4 100644 (file)
       - SurgeryTool
       - Dropper
       components:
-      - Hypospray
       - Injector
       - Pill
       - HandLabeler
           - Bottle
       hypo:
         whitelist:
-          components:
+          tags:
           - Hypospray
       pill:
         whitelist:
index 343113e2f02ba9266348ee58302ac8999f72b1a5..9b76330d982f93eca004c22d0c4ca49eff518fec 100644 (file)
@@ -1,5 +1,18 @@
 - type: entity
-  parent: [BaseItem, BaseGrandTheftContraband]
+  abstract: true
+  parent: BaseItem
+  id: BaseHypospray
+  components:
+  - type: Injector
+    solutionName: hypospray
+    ignoreClosed: false
+    activeModeProtoId: HyposprayDynamicMode
+    allowedModes:
+    - HyposprayDynamicMode
+    - HyposprayInjectMode
+
+- type: entity
+  parent: [BaseHypospray, BaseGrandTheftContraband]
   id: Hypospray
   name: hypospray
   description: A sterile injector for rapid administration of drugs to patients.
   - type: Sprite
     sprite: Objects/Specific/Medical/hypospray.rsi
     layers:
-      - state: hypo
-        map: ["enum.SolutionContainerLayers.Base"]
-      - state: hypo_fill1
-        map: ["enum.SolutionContainerLayers.Fill"]
-        visible: false
+    - state: hypo
+      map: ["enum.SolutionContainerLayers.Base"]
+    - state: hypo_fill1
+      map: ["enum.SolutionContainerLayers.Fill"]
+      visible: false
   - type: Item
     sprite: Objects/Specific/Medical/hypospray.rsi
   - type: SolutionContainerManager
@@ -23,8 +36,6 @@
   - type: ExaminableSolution
     solution: hypospray
     exactVolume: true
-  - type: Hypospray
-    onlyAffectsMobs: false
   - type: UseDelay
     delay: 0.5
   - type: StaticPrice
@@ -41,7 +52,7 @@
     solutionName: hypospray
 
 - type: entity
-  parent: BaseItem
+  parent: [BaseHypospray, BaseSyndicateContraband]
   id: SyndiHypo
   name: gorlex hypospray
   description: A sterile injector for rapid administration of drugs. Reverse-engineered from Nanotrasen designs, Cybersun produces these in limited quantities for Gorlex Marauders' corpsmen.
     solutions:
       hypospray:
         maxVol: 20
-  - type: RefillableSolution
-    solution: hypospray
-  - type: ExaminableSolution
-    solution: hypospray
-    exactVolume: true
-  - type: Hypospray
-    onlyAffectsMobs: false
-  - type: UseDelay
-    delay: 0.5
-  - type: Appearance
   - type: SolutionContainerVisuals
     maxFillLevels: 4
     fillBaseName: hypo_fill
     solutionName: hypospray
 
 - type: entity
-  parent: BaseItem
+  parent: BaseHypospray
   id: BorgHypo
   name: borghypo
   description: A sterile injector for rapid administration of drugs to patients. This integrated model is specialized for use by medical borgs.
@@ -94,8 +95,6 @@
     solution: hypospray
   - type: ExaminableSolution
     solution: hypospray
-  - type: Hypospray
-    onlyAffectsMobs: false
   - type: UseDelay
     delay: 0.5
 
       delay: 0.0
 
 - type: entity
-  parent: BaseItem
+  parent: BaseHypospray
   id: ChemicalMedipen
   name: chemical medipen
   description: A single-dose, non-refillable medipen.
   - type: ExaminableSolution
     solution: pen
     exactVolume: true
-  - type: Hypospray
+  - type: Injector
     solutionName: pen
-    transferAmount: 15
-    onlyAffectsMobs: false
-    injectOnly: true
+    currentTransferAmount: null
+    activeModeProtoId: HyposprayInjectMode
+    allowedModes:
+    - HyposprayInjectMode
   - type: Appearance
   - type: SolutionContainerVisuals
     maxFillLevels: 1
     maxFillLevels: 1
     changeColor: false
     emptySpriteName: bicpen_empty
-  - type: Hypospray
-    solutionName: pen
-    transferAmount: 20
-    onlyAffectsMobs: false
-    injectOnly: true
   - type: SolutionContainerManager
     solutions:
       pen:
     maxFillLevels: 1
     changeColor: false
     emptySpriteName: dermpen_empty
-  - type: Hypospray
-    solutionName: pen
-    transferAmount: 20
-    onlyAffectsMobs: false
-    injectOnly: true
   - type: SolutionContainerManager
     solutions:
       pen:
     maxFillLevels: 1
     changeColor: false
     emptySpriteName: arithpen_empty
-  - type: Hypospray
-    solutionName: pen
-    transferAmount: 20
-    onlyAffectsMobs: false
-    injectOnly: true
   - type: SolutionContainerManager
     solutions:
       pen:
     maxFillLevels: 1
     changeColor: false
     emptySpriteName: punctpen_empty
-  - type: Hypospray
-    solutionName: pen
-    transferAmount: 15
-    onlyAffectsMobs: false
-    injectOnly: true
   - type: SolutionContainerManager
     solutions:
       pen:
     maxFillLevels: 1
     changeColor: false
     emptySpriteName: pyrapen_empty
-  - type: Hypospray
-    solutionName: pen
-    transferAmount: 20
-    onlyAffectsMobs: false
-    injectOnly: true
   - type: SolutionContainerManager
     solutions:
       pen:
     maxFillLevels: 1
     changeColor: false
     emptySpriteName: dexpen_empty
-  - type: Hypospray
-    solutionName: pen
-    transferAmount: 40
-    onlyAffectsMobs: false
-    injectOnly: true
   - type: SolutionContainerManager
     solutions:
       pen:
     maxFillLevels: 1
     changeColor: false
     emptySpriteName: hypovolemic_empty
-  - type: Hypospray
-    solutionName: pen
-    transferAmount: 30
-    onlyAffectsMobs: false
-    injectOnly: true
   - type: SolutionContainerManager
     solutions:
       pen:
     maxFillLevels: 1
     changeColor: false
     emptySpriteName: stimpen_empty
-  - type: Hypospray
-    solutionName: pen
-    transferAmount: 30
-    onlyAffectsMobs: false
-    injectOnly: true
   - type: StaticPrice
     price: 1500
 
           Quantity: 25
         - ReagentId: TranexamicAcid
           Quantity: 5
-  - type: Hypospray
-    solutionName: pen
-    transferAmount: 30
-    onlyAffectsMobs: false
-    injectOnly: true
   - type: StaticPrice
     price: 1500
 
     solution: hypospray
     heldOnly: true # Allow examination only when held in hand.
     exactVolume: true
-  - type: Hypospray
-    onlyAffectsMobs: false
-    drawTime: 0.75
+  - type: Injector
+    solutionName: hypospray
+    activeModeProtoId: HypopenDynamicMode
+    allowedModes:
+    - HyposprayInjectMode
+    - HypopenDynamicMode
   - type: UseDelay
     delay: 0.5
   - type: StaticPrice # A new shitcurity meta
         reagents:
         - ReagentId: JuiceThatMakesYouWeh
           Quantity: 60
-  - type: Hypospray
-    solutionName: pen
-    transferAmount: 1
-    onlyAffectsMobs: false
-    injectOnly: true
index 82cf3dd16bdf819356d9a6e5b573a04728993006..95482244408dcff263944fd9250879bd2396ed6b 100644 (file)
       injector:
         maxVol: 5
   - type: Injector
-    injectOnly: false
     ignoreMobs: true
     ignoreClosed: false
-    transferAmounts:
-    - 1
-    - 2
-    - 3
-    - 4
-    - 5
-    currentTransferAmount: 1
+    activeModeProtoId: DropperDrawMode
+    allowedModes:
+    - DropperDrawMode
+    - DropperInjectMode
   - type: ExaminableSolution
     solution: dropper
     exactVolume: true
       injector:
         maxVol: 15
   - type: Injector
-    injectOnly: false
-    transferAmounts:
-    - 5
-    - 10
-    - 15
+    activeModeProtoId: SyringeDrawMode
+    allowedModes:
+    - SyringeDrawMode
+    - SyringeInjectMode
   - type: ExaminableSolution
     solution: injector
     exactVolume: true
   parent: BaseSyringe
   id: Syringe
   components:
-  - type: Injector
-    currentTransferAmount: 15
   - type: Tag
     tags:
     - Syringe
     solutions:
       injector:
         maxVol: 5
-  - type: Injector
-    transferAmounts:
-    - 1
-    - 2
-    - 3
-    - 4
-    - 5
-    currentTransferAmount: 5
   - type: SolutionContainerVisuals
     maxFillLevels: 3
     fillBaseName: minisyringe
   id: PrefilledSyringe
   components:
   - type: Injector
-    toggleState: Inject
+    activeModeProtoId: SyringeInjectMode
 
 - type: entity
   id: SyringeBluespace
       injector:
         maxVol: 100
   - type: Injector
-    delay: 2.5
-    injectOnly: false
+    activeModeProtoId: BluespaceSyringeDrawMode
+    allowedModes:
+    - BluespaceSyringeInjectMode
+    - BluespaceSyringeDrawMode
   - type: SolutionContainerVisuals
     maxFillLevels: 2
     fillBaseName: syringe
         maxVol: 10
         canReact: false
   - type: Injector
-    injectOnly: false
-    transferAmounts:
-    - 5
-    - 10
-    currentTransferAmount: 10
+    activeModeProtoId: CryostasisSyringeDrawMode
+    allowedModes:
+    - CryostasisSyringeInjectMode
+    - CryostasisSyringeDrawMode
   - type: Tag
     tags:
     - Syringe
index a1e01a14a6c7273102414d2bc77593ac7d7fe8bf..ab7bc3c3abe4b97aa3a4055a40ff2daba19025ff 100644 (file)
 - type: Tag
   id: HudSecurity # ConstructionGraph: HudMedSec, GlassesSecHUD
 
+- type: Tag
+  id: Hypospray # ItemMapper: ClothingBeltMedical
+
 ## I ##
 
 - type: Tag