]> git.smokeofanarchy.ru Git - space-station-14.git/commitdiff
Predict Injector (syringes), cleanup (#25235)
authorPieter-Jan Briers <pieterjan.briers+git@gmail.com>
Wed, 14 Feb 2024 23:05:01 +0000 (00:05 +0100)
committerGitHub <noreply@github.com>
Wed, 14 Feb 2024 23:05:01 +0000 (16:05 -0700)
At least the mode/transfer amount logic. Actual transfer logic needs Bloodstream which I didn't wanna move into shared.

Content.Client/Chemistry/Components/InjectorComponent.cs [deleted file]
Content.Client/Chemistry/EntitySystems/InjectorSystem.cs
Content.Client/Chemistry/UI/InjectorStatusControl.cs
Content.Server/Chemistry/Components/InjectorComponent.cs [deleted file]
Content.Server/Chemistry/EntitySystems/ChemistrySystem.Injector.cs [deleted file]
Content.Server/Chemistry/EntitySystems/ChemistrySystem.cs
Content.Server/Chemistry/EntitySystems/InjectorSystem.cs [new file with mode: 0644]
Content.Shared/Chemistry/Components/InjectorComponent.cs [new file with mode: 0644]
Content.Shared/Chemistry/Components/SharedInjectorComponent.cs [deleted file]
Content.Shared/Chemistry/EntitySystems/SharedInjectorSystem.cs [new file with mode: 0644]
Resources/Locale/en-US/chemistry/components/injector-component.ftl

diff --git a/Content.Client/Chemistry/Components/InjectorComponent.cs b/Content.Client/Chemistry/Components/InjectorComponent.cs
deleted file mode 100644 (file)
index 4d10517..0000000
+++ /dev/null
@@ -1,21 +0,0 @@
-using Content.Shared.Chemistry.Components;
-using Content.Shared.FixedPoint;
-
-namespace Content.Client.Chemistry.Components
-{
-    /// <summary>
-    /// Client behavior for injectors & syringes. Used for item status on injectors
-    /// </summary>
-    [RegisterComponent]
-    public sealed partial class InjectorComponent : SharedInjectorComponent
-    {
-        [ViewVariables]
-        public FixedPoint2 CurrentVolume;
-        [ViewVariables]
-        public FixedPoint2 TotalVolume;
-        [ViewVariables]
-        public InjectorToggleMode CurrentMode;
-        [ViewVariables(VVAccess.ReadWrite)]
-        public bool UiUpdateNeeded;
-    }
-}
index 896349a161420a6d539b8c3444089314ec3f03df..12eb7f3d14def795ef0bd7030395d3f7c1b082eb 100644 (file)
@@ -2,34 +2,21 @@ using Content.Client.Chemistry.Components;
 using Content.Client.Chemistry.UI;
 using Content.Client.Items;
 using Content.Shared.Chemistry.Components;
+using Content.Shared.Chemistry.EntitySystems;
 using Robust.Shared.GameStates;
 
 namespace Content.Client.Chemistry.EntitySystems;
 
-public sealed class InjectorSystem : EntitySystem
+public sealed class InjectorSystem : SharedInjectorSystem
 {
     public override void Initialize()
     {
         base.Initialize();
-        SubscribeLocalEvent<InjectorComponent, ComponentHandleState>(OnHandleInjectorState);
-        Subs.ItemStatus<InjectorComponent>(ent => new InjectorStatusControl(ent));
+        Subs.ItemStatus<InjectorComponent>(ent => new InjectorStatusControl(ent, SolutionContainers));
         SubscribeLocalEvent<HyposprayComponent, ComponentHandleState>(OnHandleHyposprayState);
         Subs.ItemStatus<HyposprayComponent>(ent => new HyposprayStatusControl(ent));
     }
 
-    private void OnHandleInjectorState(EntityUid uid, InjectorComponent component, ref ComponentHandleState args)
-    {
-        if (args.Current is not SharedInjectorComponent.InjectorComponentState state)
-        {
-            return;
-        }
-
-        component.CurrentVolume = state.CurrentVolume;
-        component.TotalVolume = state.TotalVolume;
-        component.CurrentMode = state.CurrentMode;
-        component.UiUpdateNeeded = true;
-    }
-
     private void OnHandleHyposprayState(EntityUid uid, HyposprayComponent component, ref ComponentHandleState args)
     {
         if (args.Current is not HyposprayComponentState cState)
index f77232016861cc64b0128eaf3592a2e562a37829..979e9ea64555435caeda8fdd53b1fe14ae544911 100644 (file)
@@ -1,7 +1,7 @@
-using Content.Client.Chemistry.Components;
 using Content.Client.Message;
 using Content.Client.Stylesheets;
 using Content.Shared.Chemistry.Components;
+using Content.Shared.Chemistry.EntitySystems;
 using Robust.Client.UserInterface;
 using Robust.Client.UserInterface.Controls;
 using Robust.Shared.Timing;
@@ -10,40 +10,37 @@ namespace Content.Client.Chemistry.UI;
 
 public sealed class InjectorStatusControl : Control
 {
-    private readonly InjectorComponent _parent;
+    private readonly Entity<InjectorComponent> _parent;
+    private readonly SharedSolutionContainerSystem _solutionContainers;
     private readonly RichTextLabel _label;
 
-    public InjectorStatusControl(InjectorComponent parent)
+    public InjectorStatusControl(Entity<InjectorComponent> parent, SharedSolutionContainerSystem solutionContainers)
     {
         _parent = parent;
+        _solutionContainers = solutionContainers;
         _label = new RichTextLabel { StyleClasses = { StyleNano.StyleClassItemStatus } };
         AddChild(_label);
-
-        Update();
     }
 
     protected override void FrameUpdate(FrameEventArgs args)
     {
         base.FrameUpdate(args);
-        if (!_parent.UiUpdateNeeded)
-            return;
-        Update();
-    }
 
-    public void Update()
-    {
-        _parent.UiUpdateNeeded = false;
+        if (!_solutionContainers.TryGetSolution(_parent.Owner, InjectorComponent.SolutionName, out _, out var solution))
+            return;
 
-        //Update current volume and injector state
-        var modeStringLocalized = _parent.CurrentMode switch
+        // Update current volume and injector state
+        var modeStringLocalized = Loc.GetString(_parent.Comp.ToggleState switch
         {
-            SharedInjectorComponent.InjectorToggleMode.Draw => Loc.GetString("injector-draw-text"),
-            SharedInjectorComponent.InjectorToggleMode.Inject => Loc.GetString("injector-inject-text"),
-            _ => Loc.GetString("injector-invalid-injector-toggle-mode")
-        };
+            InjectorToggleMode.Draw => "injector-draw-text",
+            InjectorToggleMode.Inject => "injector-inject-text",
+            _ => "injector-invalid-injector-toggle-mode"
+        });
+
         _label.SetMarkup(Loc.GetString("injector-volume-label",
-            ("currentVolume", _parent.CurrentVolume),
-            ("totalVolume", _parent.TotalVolume),
-            ("modeString", modeStringLocalized)));
+            ("currentVolume", solution.Volume),
+            ("totalVolume", solution.MaxVolume),
+            ("modeString", modeStringLocalized),
+            ("transferVolume", _parent.Comp.TransferAmount)));
     }
 }
diff --git a/Content.Server/Chemistry/Components/InjectorComponent.cs b/Content.Server/Chemistry/Components/InjectorComponent.cs
deleted file mode 100644 (file)
index d6d149a..0000000
+++ /dev/null
@@ -1,83 +0,0 @@
-using Content.Shared.Chemistry.Components;
-using Content.Shared.FixedPoint;
-
-namespace Content.Server.Chemistry.Components
-{
-    /// <summary>
-    /// Server behavior for reagent injectors and syringes. 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.
-    /// </summary>
-    [RegisterComponent]
-    public sealed partial class InjectorComponent : SharedInjectorComponent
-    {
-        public const string SolutionName = "injector";
-
-        /// <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("injectOnly")]
-        public bool InjectOnly;
-
-        /// <summary>
-        /// Whether or not the injector is able to draw from or inject from mobs
-        /// </summary>
-        /// <remarks>
-        ///     for example: droppers would ignore mobs
-        /// </remarks>
-        [DataField("ignoreMobs")]
-        public bool IgnoreMobs = false;
-
-        /// <summary>
-        ///     The minimum amount of solution that can be transferred at once from this solution.
-        /// </summary>
-        [DataField("minTransferAmount")]
-        [ViewVariables(VVAccess.ReadWrite)]
-        public FixedPoint2 MinimumTransferAmount { get; set; } = FixedPoint2.New(5);
-
-        /// <summary>
-        ///     The maximum amount of solution that can be transferred at once from this solution.
-        /// </summary>
-        [DataField("maxTransferAmount")]
-        [ViewVariables(VVAccess.ReadWrite)]
-        public FixedPoint2 MaximumTransferAmount { get; set; } = FixedPoint2.New(50);
-
-        /// <summary>
-        /// Amount to inject or draw on each usage. If the injector is inject only, it will
-        /// attempt to inject it's entire contents upon use.
-        /// </summary>
-        [ViewVariables(VVAccess.ReadWrite)]
-        [DataField("transferAmount")]
-        public FixedPoint2 TransferAmount = FixedPoint2.New(5);
-
-        /// <summary>
-        /// Injection delay (seconds) when the target is a mob.
-        /// </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>
-        [ViewVariables(VVAccess.ReadWrite)]
-        [DataField("delay")]
-        public float Delay = 5;
-
-        [DataField("toggleState")] private InjectorToggleMode _toggleState;
-
-        /// <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>
-        [ViewVariables(VVAccess.ReadWrite)]
-        public InjectorToggleMode ToggleState
-        {
-            get => _toggleState;
-            set
-            {
-                _toggleState = value;
-                Dirty();
-            }
-        }
-    }
-}
diff --git a/Content.Server/Chemistry/EntitySystems/ChemistrySystem.Injector.cs b/Content.Server/Chemistry/EntitySystems/ChemistrySystem.Injector.cs
deleted file mode 100644 (file)
index 4f149db..0000000
+++ /dev/null
@@ -1,447 +0,0 @@
-using Content.Server.Body.Components;
-using Content.Server.Chemistry.Components;
-using Content.Server.Chemistry.Containers.EntitySystems;
-using Content.Shared.Chemistry.Components;
-using Content.Shared.Chemistry.Components.SolutionManager;
-using Content.Shared.Chemistry.EntitySystems;
-using Content.Shared.Chemistry.Reagent;
-using Content.Shared.Database;
-using Content.Shared.DoAfter;
-using Content.Shared.FixedPoint;
-using Content.Shared.Forensics;
-using Content.Shared.IdentityManagement;
-using Content.Shared.Interaction;
-using Content.Shared.Interaction.Events;
-using Content.Shared.Mobs.Components;
-using Content.Shared.Stacks;
-using Content.Shared.Verbs;
-using Robust.Shared.GameStates;
-using Robust.Shared.Player;
-
-namespace Content.Server.Chemistry.EntitySystems;
-
-public sealed partial class ChemistrySystem
-{
-
-    /// <summary>
-    ///     Default transfer amounts for the set-transfer verb.
-    /// </summary>
-    public static readonly List<int> TransferAmounts = new() { 1, 5, 10, 15 };
-    private void InitializeInjector()
-    {
-        SubscribeLocalEvent<InjectorComponent, GetVerbsEvent<AlternativeVerb>>(AddSetTransferVerbs);
-        SubscribeLocalEvent<InjectorComponent, SolutionContainerChangedEvent>(OnSolutionChange);
-        SubscribeLocalEvent<InjectorComponent, InjectorDoAfterEvent>(OnInjectDoAfter);
-        SubscribeLocalEvent<InjectorComponent, ComponentStartup>(OnInjectorStartup);
-        SubscribeLocalEvent<InjectorComponent, UseInHandEvent>(OnInjectorUse);
-        SubscribeLocalEvent<InjectorComponent, AfterInteractEvent>(OnInjectorAfterInteract);
-        SubscribeLocalEvent<InjectorComponent, ComponentGetState>(OnInjectorGetState);
-    }
-
-    private void AddSetTransferVerbs(Entity<InjectorComponent> entity, ref GetVerbsEvent<AlternativeVerb> args)
-    {
-        if (!args.CanAccess || !args.CanInteract || args.Hands == null)
-            return;
-
-        if (!EntityManager.TryGetComponent(args.User, out ActorComponent? actor))
-            return;
-
-        var (uid, component) = entity;
-
-        // Add specific transfer verbs according to the container's size
-        var priority = 0;
-        var user = args.User;
-        foreach (var amount in TransferAmounts)
-        {
-            if (amount < component.MinimumTransferAmount.Int() || amount > component.MaximumTransferAmount.Int())
-                continue;
-
-            AlternativeVerb verb = new();
-            verb.Text = Loc.GetString("comp-solution-transfer-verb-amount", ("amount", amount));
-            verb.Category = VerbCategory.SetTransferAmount;
-            verb.Act = () =>
-            {
-                component.TransferAmount = FixedPoint2.New(amount);
-                _popup.PopupEntity(Loc.GetString("comp-solution-transfer-set-amount", ("amount", amount)), user, user);
-            };
-
-            // we want to sort by size, not alphabetically by the verb text.
-            verb.Priority = priority;
-            priority--;
-
-            args.Verbs.Add(verb);
-        }
-    }
-
-    private void UseInjector(Entity<InjectorComponent> injector, EntityUid target, EntityUid user)
-    {
-        // Handle injecting/drawing for solutions
-        if (injector.Comp.ToggleState == SharedInjectorComponent.InjectorToggleMode.Inject)
-        {
-            if (_solutionContainers.TryGetInjectableSolution(target, out var injectableSolution, out _))
-            {
-                TryInject(injector, target, injectableSolution.Value, user, false);
-            }
-            else if (_solutionContainers.TryGetRefillableSolution(target, out var refillableSolution, out _))
-            {
-                TryInject(injector, target, refillableSolution.Value, user, true);
-            }
-            else if (TryComp<BloodstreamComponent>(target, out var bloodstream))
-            {
-                TryInjectIntoBloodstream(injector, (target, bloodstream), user);
-            }
-            else
-            {
-                _popup.PopupEntity(Loc.GetString("injector-component-cannot-transfer-message",
-                    ("target", Identity.Entity(target, EntityManager))), injector, user);
-            }
-        }
-        else if (injector.Comp.ToggleState == SharedInjectorComponent.InjectorToggleMode.Draw)
-        {
-            // Draw from a bloodstream, if the target has that
-            if (TryComp<BloodstreamComponent>(target, out var stream) &&
-                _solutionContainers.ResolveSolution(target, stream.BloodSolutionName, ref stream.BloodSolution))
-            {
-                TryDraw(injector, (target, stream), stream.BloodSolution.Value, user);
-                return;
-            }
-
-            // Draw from an object (food, beaker, etc)
-            if (_solutionContainers.TryGetDrawableSolution(target, out var drawableSolution, out _))
-            {
-                TryDraw(injector, target, drawableSolution.Value, user);
-            }
-            else
-            {
-                _popup.PopupEntity(Loc.GetString("injector-component-cannot-draw-message",
-                    ("target", Identity.Entity(target, EntityManager))), injector.Owner, user);
-            }
-        }
-    }
-
-    private void OnSolutionChange(Entity<InjectorComponent> entity, ref SolutionContainerChangedEvent args)
-    {
-        Dirty(entity);
-    }
-
-    private void OnInjectorGetState(Entity<InjectorComponent> entity, ref ComponentGetState args)
-    {
-        _solutionContainers.TryGetSolution(entity.Owner, InjectorComponent.SolutionName, out _, out var solution);
-
-        var currentVolume = solution?.Volume ?? FixedPoint2.Zero;
-        var maxVolume = solution?.MaxVolume ?? FixedPoint2.Zero;
-
-        args.State = new SharedInjectorComponent.InjectorComponentState(currentVolume, maxVolume, entity.Comp.ToggleState);
-    }
-
-    private void OnInjectDoAfter(Entity<InjectorComponent> entity, ref InjectorDoAfterEvent args)
-    {
-        if (args.Cancelled || args.Handled || args.Args.Target == null)
-            return;
-
-        UseInjector(entity, args.Args.Target.Value, args.Args.User);
-        args.Handled = true;
-    }
-
-    private void OnInjectorAfterInteract(Entity<InjectorComponent> entity, ref AfterInteractEvent args)
-    {
-        if (args.Handled || !args.CanReach)
-            return;
-
-        //Make sure we have the attacking entity
-        if (args.Target is not { Valid: true } target || !HasComp<SolutionContainerManagerComponent>(entity))
-            return;
-
-        // Is the target a mob? If yes, use a do-after to give them time to respond.
-        if (HasComp<MobStateComponent>(target) || HasComp<BloodstreamComponent>(target))
-        {
-            // Are use using an injector capible of targeting a mob?
-            if (entity.Comp.IgnoreMobs)
-                return;
-
-            InjectDoAfter(entity, target, args.User);
-            args.Handled = true;
-            return;
-        }
-
-        UseInjector(entity, target, args.User);
-        args.Handled = true;
-    }
-
-    private void OnInjectorStartup(Entity<InjectorComponent> entity, ref ComponentStartup args)
-    {
-        // ???? why ?????
-        Dirty(entity);
-    }
-
-    private void OnInjectorUse(Entity<InjectorComponent> entity, ref UseInHandEvent args)
-    {
-        if (args.Handled)
-            return;
-
-        Toggle(entity, args.User);
-        args.Handled = true;
-    }
-
-    /// <summary>
-    /// Toggle between draw/inject state if applicable
-    /// </summary>
-    private void Toggle(Entity<InjectorComponent> injector, EntityUid user)
-    {
-        if (injector.Comp.InjectOnly)
-        {
-            return;
-        }
-
-        string msg;
-        switch (injector.Comp.ToggleState)
-        {
-            case SharedInjectorComponent.InjectorToggleMode.Inject:
-                injector.Comp.ToggleState = SharedInjectorComponent.InjectorToggleMode.Draw;
-                msg = "injector-component-drawing-text";
-                break;
-            case SharedInjectorComponent.InjectorToggleMode.Draw:
-                injector.Comp.ToggleState = SharedInjectorComponent.InjectorToggleMode.Inject;
-                msg = "injector-component-injecting-text";
-                break;
-            default:
-                throw new ArgumentOutOfRangeException();
-        }
-
-        _popup.PopupEntity(Loc.GetString(msg), injector, 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
-        _popup.PopupEntity(Loc.GetString("injector-component-injecting-user"), target, user);
-
-        if (!_solutionContainers.TryGetSolution(injector.Owner, InjectorComponent.SolutionName, out _, out var solution))
-            return;
-
-        var actualDelay = MathF.Max(injector.Comp.Delay, 1f);
-
-        // Injections take 0.5 seconds longer per additional 5u
-        actualDelay += (float) injector.Comp.TransferAmount / injector.Comp.Delay - 0.5f;
-
-        var isTarget = user != target;
-
-        if (isTarget)
-        {
-            // Create a pop-up for the target
-            var userName = Identity.Entity(user, EntityManager);
-            _popup.PopupEntity(Loc.GetString("injector-component-injecting-target",
-                ("user", userName)), user, target);
-
-            // Check if the target is incapacitated or in combat mode and modify time accordingly.
-            if (_mobState.IsIncapacitated(target))
-            {
-                actualDelay /= 2.5f;
-            }
-            else if (_combat.IsInCombatMode(target))
-            {
-                // Slightly increase the delay when the target is in combat mode. Helps prevents cheese injections in
-                // combat with fast syringes & lag.
-                actualDelay += 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 == SharedInjectorComponent.InjectorToggleMode.Inject)
-            {
-                _adminLogger.Add(LogType.ForceFeed,
-                    $"{EntityManager.ToPrettyString(user):user} is attempting to inject {EntityManager.ToPrettyString(target):target} with a solution {SolutionContainerSystem.ToPrettyString(solution):solution}");
-            }
-            else
-            {
-                _adminLogger.Add(LogType.ForceFeed,
-                    $"{EntityManager.ToPrettyString(user):user} is attempting to draw {injector.Comp.TransferAmount.ToString()} units from {EntityManager.ToPrettyString(target):target}");
-            }
-        }
-        else
-        {
-            // Self-injections take half as long.
-            actualDelay /= 2;
-
-            if (injector.Comp.ToggleState == SharedInjectorComponent.InjectorToggleMode.Inject)
-            {
-                _adminLogger.Add(LogType.Ingestion,
-                    $"{EntityManager.ToPrettyString(user):user} is attempting to inject themselves with a solution {SolutionContainerSystem.ToPrettyString(solution):solution}.");
-            }
-            else
-            {
-                _adminLogger.Add(LogType.ForceFeed,
-                    $"{EntityManager.ToPrettyString(user):user} is attempting to draw {injector.Comp.TransferAmount.ToString()} units from themselves.");
-            }
-        }
-
-        _doAfter.TryStartDoAfter(new DoAfterArgs(EntityManager, user, actualDelay, new InjectorDoAfterEvent(), injector.Owner, target: target, used: injector.Owner)
-        {
-            BreakOnUserMove = true,
-            BreakOnDamage = true,
-            BreakOnTargetMove = true,
-            MovementThreshold = 0.1f,
-        });
-    }
-
-    private void TryInjectIntoBloodstream(Entity<InjectorComponent> injector, Entity<BloodstreamComponent> target, EntityUid user)
-    {
-        // Get transfer amount. May be smaller than _transferAmount if not enough room
-        if (!_solutionContainers.ResolveSolution(target.Owner, target.Comp.ChemicalSolutionName, ref target.Comp.ChemicalSolution, out var chemSolution))
-        {
-            _popup.PopupEntity(Loc.GetString("injector-component-cannot-inject-message", ("target", Identity.Entity(target, EntityManager))), injector.Owner, user);
-            return;
-        }
-
-        var realTransferAmount = FixedPoint2.Min(injector.Comp.TransferAmount, chemSolution.AvailableVolume);
-        if (realTransferAmount <= 0)
-        {
-            _popup.PopupEntity(Loc.GetString("injector-component-cannot-inject-message", ("target", Identity.Entity(target, EntityManager))), injector.Owner, user);
-            return;
-        }
-
-        // Move units from attackSolution to targetSolution
-        var removedSolution = _solutionContainers.SplitSolution(target.Comp.ChemicalSolution.Value, realTransferAmount);
-
-        _blood.TryAddToChemicals(target, removedSolution, target.Comp);
-
-        _reactiveSystem.DoEntityReaction(target, removedSolution, ReactionMethod.Injection);
-
-        _popup.PopupEntity(Loc.GetString("injector-component-inject-success-message",
-                ("amount", removedSolution.Volume),
-                ("target", Identity.Entity(target, EntityManager))), injector.Owner, user);
-
-        Dirty(injector);
-        AfterInject(injector, target);
-    }
-
-    private void TryInject(Entity<InjectorComponent> injector, EntityUid targetEntity, Entity<SolutionComponent> targetSolution, EntityUid user, bool asRefill)
-    {
-        if (!_solutionContainers.TryGetSolution(injector.Owner, InjectorComponent.SolutionName, out var soln, out var solution) || solution.Volume == 0)
-            return;
-
-        // Get transfer amount. May be smaller than _transferAmount if not enough room
-        var realTransferAmount = FixedPoint2.Min(injector.Comp.TransferAmount, targetSolution.Comp.Solution.AvailableVolume);
-
-        if (realTransferAmount <= 0)
-        {
-            _popup.PopupEntity(Loc.GetString("injector-component-target-already-full-message", ("target", Identity.Entity(targetEntity, EntityManager))),
-                injector.Owner, user);
-            return;
-        }
-
-        // Move units from attackSolution to targetSolution
-        Solution removedSolution;
-        if (TryComp<StackComponent>(targetEntity, out var stack))
-            removedSolution = _solutionContainers.SplitStackSolution(soln.Value, realTransferAmount, stack.Count);
-        else
-            removedSolution = _solutionContainers.SplitSolution(soln.Value, realTransferAmount);
-
-        _reactiveSystem.DoEntityReaction(targetEntity, removedSolution, ReactionMethod.Injection);
-
-        if (!asRefill)
-            _solutionContainers.Inject(targetEntity, targetSolution, removedSolution);
-        else
-            _solutionContainers.Refill(targetEntity, targetSolution, removedSolution);
-
-        _popup.PopupEntity(Loc.GetString("injector-component-transfer-success-message",
-                ("amount", removedSolution.Volume),
-                ("target", Identity.Entity(targetEntity, EntityManager))), injector.Owner, user);
-
-        Dirty(injector);
-        AfterInject(injector, targetEntity);
-    }
-
-    private void AfterInject(Entity<InjectorComponent> injector, EntityUid target)
-    {
-        // Automatically set syringe to draw after completely draining it.
-        if (_solutionContainers.TryGetSolution(injector.Owner, InjectorComponent.SolutionName, out _, out var solution) && solution.Volume == 0)
-        {
-            injector.Comp.ToggleState = SharedInjectorComponent.InjectorToggleMode.Draw;
-        }
-
-        // Leave some DNA from the injectee on it
-        var ev = new TransferDnaEvent { Donor = target, Recipient = injector };
-        RaiseLocalEvent(target, ref ev);
-    }
-
-    private void AfterDraw(Entity<InjectorComponent> injector, EntityUid target)
-    {
-        // Automatically set syringe to inject after completely filling it.
-        if (_solutionContainers.TryGetSolution(injector.Owner, InjectorComponent.SolutionName, out _, out var solution) && solution.AvailableVolume == 0)
-        {
-            injector.Comp.ToggleState = SharedInjectorComponent.InjectorToggleMode.Inject;
-        }
-
-        // Leave some DNA from the drawee on it
-        var ev = new TransferDnaEvent { Donor = target, Recipient = injector };
-        RaiseLocalEvent(target, ref ev);
-    }
-
-    private void TryDraw(Entity<InjectorComponent> injector, Entity<BloodstreamComponent?> target, Entity<SolutionComponent> targetSolution, EntityUid user)
-    {
-        if (!_solutionContainers.TryGetSolution(injector.Owner, InjectorComponent.SolutionName, out var soln, out var solution) || solution.AvailableVolume == 0)
-        {
-            return;
-        }
-
-        // Get transfer amount. May be smaller than _transferAmount if not enough room, also make sure there's room in the injector
-        var realTransferAmount = FixedPoint2.Min(injector.Comp.TransferAmount, targetSolution.Comp.Solution.Volume, solution.AvailableVolume);
-
-        if (realTransferAmount <= 0)
-        {
-            _popup.PopupEntity(Loc.GetString("injector-component-target-is-empty-message", ("target", Identity.Entity(target, EntityManager))),
-                injector.Owner, user);
-            return;
-        }
-
-        // We have some snowflaked behavior for streams.
-        if (target.Comp != null)
-        {
-            DrawFromBlood(injector, (target.Owner, target.Comp), soln.Value, realTransferAmount, user);
-            return;
-        }
-
-        // Move units from attackSolution to targetSolution
-        var removedSolution = _solutionContainers.Draw(target.Owner, targetSolution, realTransferAmount);
-
-        if (!_solutionContainers.TryAddSolution(soln.Value, removedSolution))
-        {
-            return;
-        }
-
-        _popup.PopupEntity(Loc.GetString("injector-component-draw-success-message",
-                ("amount", removedSolution.Volume),
-                ("target", Identity.Entity(target, EntityManager))), injector.Owner, user);
-
-        Dirty(injector);
-        AfterDraw(injector, target);
-    }
-
-    private void DrawFromBlood(Entity<InjectorComponent> injector, Entity<BloodstreamComponent> target, Entity<SolutionComponent> injectorSolution, FixedPoint2 transferAmount, EntityUid user)
-    {
-        var drawAmount = (float) transferAmount;
-
-        if (_solutionContainers.ResolveSolution(target.Owner, target.Comp.ChemicalSolutionName, ref target.Comp.ChemicalSolution))
-        {
-            var chemTemp = _solutionContainers.SplitSolution(target.Comp.ChemicalSolution.Value, drawAmount * 0.15f);
-            _solutionContainers.TryAddSolution(injectorSolution, chemTemp);
-            drawAmount -= (float) chemTemp.Volume;
-        }
-
-        if (_solutionContainers.ResolveSolution(target.Owner, target.Comp.BloodSolutionName, ref target.Comp.BloodSolution))
-        {
-            var bloodTemp = _solutionContainers.SplitSolution(target.Comp.BloodSolution.Value, drawAmount);
-            _solutionContainers.TryAddSolution(injectorSolution, bloodTemp);
-        }
-
-        _popup.PopupEntity(Loc.GetString("injector-component-draw-success-message",
-                ("amount", transferAmount),
-                ("target", Identity.Entity(target, EntityManager))), injector.Owner, user);
-
-        Dirty(injector);
-        AfterDraw(injector, target);
-    }
-}
index a2b4f399e2d869d83373f0ee14a84ea311008d00..c4f22dc63aaa8fb2d6922e773b3e652ef8fbbcf0 100644 (file)
@@ -1,12 +1,8 @@
 using Content.Server.Administration.Logs;
-using Content.Server.Body.Systems;
 using Content.Server.Chemistry.Containers.EntitySystems;
 using Content.Server.Interaction;
 using Content.Server.Popups;
 using Content.Shared.Chemistry;
-using Content.Shared.CombatMode;
-using Content.Shared.DoAfter;
-using Content.Shared.Mobs.Systems;
 using Robust.Shared.Audio.Systems;
 
 namespace Content.Server.Chemistry.EntitySystems;
@@ -16,20 +12,15 @@ public sealed partial class ChemistrySystem : EntitySystem
     [Dependency] private readonly IAdminLogManager _adminLogger = default!;
     [Dependency] private readonly IEntityManager _entMan = default!;
     [Dependency] private readonly InteractionSystem _interaction = default!;
-    [Dependency] private readonly BloodstreamSystem _blood = default!;
-    [Dependency] private readonly SharedDoAfterSystem _doAfter = default!;
     [Dependency] private readonly PopupSystem _popup = default!;
     [Dependency] private readonly ReactiveSystem _reactiveSystem = default!;
     [Dependency] private readonly SharedAudioSystem _audio = default!;
-    [Dependency] private readonly MobStateSystem _mobState = default!;
-    [Dependency] private readonly SharedCombatModeSystem _combat = default!;
     [Dependency] private readonly SolutionContainerSystem _solutionContainers = default!;
 
     public override void Initialize()
     {
         // Why ChemMaster duplicates reagentdispenser nobody knows.
         InitializeHypospray();
-        InitializeInjector();
         InitializeMixing();
     }
 }
diff --git a/Content.Server/Chemistry/EntitySystems/InjectorSystem.cs b/Content.Server/Chemistry/EntitySystems/InjectorSystem.cs
new file mode 100644 (file)
index 0000000..a4497c0
--- /dev/null
@@ -0,0 +1,366 @@
+using Content.Server.Body.Components;
+using Content.Server.Body.Systems;
+using Content.Shared.Chemistry;
+using Content.Shared.Chemistry.Components;
+using Content.Shared.Chemistry.Components.SolutionManager;
+using Content.Shared.Chemistry.EntitySystems;
+using Content.Shared.Chemistry.Reagent;
+using Content.Shared.Database;
+using Content.Shared.DoAfter;
+using Content.Shared.FixedPoint;
+using Content.Shared.Forensics;
+using Content.Shared.IdentityManagement;
+using Content.Shared.Interaction;
+using Content.Shared.Mobs.Components;
+using Content.Shared.Stacks;
+
+namespace Content.Server.Chemistry.EntitySystems;
+
+public sealed class InjectorSystem : SharedInjectorSystem
+{
+    [Dependency] private readonly BloodstreamSystem _blood = default!;
+    [Dependency] private readonly ReactiveSystem _reactiveSystem = default!;
+
+    public override void Initialize()
+    {
+        base.Initialize();
+
+        SubscribeLocalEvent<InjectorComponent, InjectorDoAfterEvent>(OnInjectDoAfter);
+        SubscribeLocalEvent<InjectorComponent, AfterInteractEvent>(OnInjectorAfterInteract);
+    }
+
+    private void UseInjector(Entity<InjectorComponent> injector, EntityUid target, EntityUid user)
+    {
+        // Handle injecting/drawing for solutions
+        if (injector.Comp.ToggleState == InjectorToggleMode.Inject)
+        {
+            if (SolutionContainers.TryGetInjectableSolution(target, out var injectableSolution, out _))
+            {
+                TryInject(injector, target, injectableSolution.Value, user, false);
+            }
+            else if (SolutionContainers.TryGetRefillableSolution(target, out var refillableSolution, out _))
+            {
+                TryInject(injector, target, refillableSolution.Value, user, true);
+            }
+            else if (TryComp<BloodstreamComponent>(target, out var bloodstream))
+            {
+                TryInjectIntoBloodstream(injector, (target, bloodstream), user);
+            }
+            else
+            {
+                Popup.PopupEntity(Loc.GetString("injector-component-cannot-transfer-message",
+                    ("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) &&
+                SolutionContainers.ResolveSolution(target, stream.BloodSolutionName, ref stream.BloodSolution))
+            {
+                TryDraw(injector, (target, stream), stream.BloodSolution.Value, user);
+                return;
+            }
+
+            // Draw from an object (food, beaker, etc)
+            if (SolutionContainers.TryGetDrawableSolution(target, out var drawableSolution, out _))
+            {
+                TryDraw(injector, target, drawableSolution.Value, user);
+            }
+            else
+            {
+                Popup.PopupEntity(Loc.GetString("injector-component-cannot-draw-message",
+                    ("target", Identity.Entity(target, EntityManager))), injector.Owner, user);
+            }
+        }
+    }
+
+    private void OnInjectDoAfter(Entity<InjectorComponent> entity, ref InjectorDoAfterEvent args)
+    {
+        if (args.Cancelled || args.Handled || args.Args.Target == null)
+            return;
+
+        UseInjector(entity, args.Args.Target.Value, args.Args.User);
+        args.Handled = true;
+    }
+
+    private void OnInjectorAfterInteract(Entity<InjectorComponent> entity, ref AfterInteractEvent args)
+    {
+        if (args.Handled || !args.CanReach)
+            return;
+
+        //Make sure we have the attacking entity
+        if (args.Target is not { Valid: true } target || !HasComp<SolutionContainerManagerComponent>(entity))
+            return;
+
+        // Is the target a mob? If yes, use a do-after to give them time to respond.
+        if (HasComp<MobStateComponent>(target) || HasComp<BloodstreamComponent>(target))
+        {
+            // Are use using an injector capible of targeting a mob?
+            if (entity.Comp.IgnoreMobs)
+                return;
+
+            InjectDoAfter(entity, target, args.User);
+            args.Handled = true;
+            return;
+        }
+
+        UseInjector(entity, target, args.User);
+        args.Handled = true;
+    }
+
+    /// <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
+        Popup.PopupEntity(Loc.GetString("injector-component-injecting-user"), target, user);
+
+        if (!SolutionContainers.TryGetSolution(injector.Owner, InjectorComponent.SolutionName, out _, out var solution))
+            return;
+
+        var actualDelay = MathHelper.Max(injector.Comp.Delay, TimeSpan.FromSeconds(1));
+
+        // Injections take 0.5 seconds longer per additional 5u
+        actualDelay += TimeSpan.FromSeconds(injector.Comp.TransferAmount.Float() / injector.Comp.Delay.TotalSeconds - 0.5f);
+
+        var isTarget = user != target;
+
+        if (isTarget)
+        {
+            // Create a pop-up for the target
+            var userName = Identity.Entity(user, EntityManager);
+            Popup.PopupEntity(Loc.GetString("injector-component-injecting-target",
+                ("user", userName)), user, target);
+
+            // Check if the target is incapacitated or in combat mode and modify time accordingly.
+            if (MobState.IsIncapacitated(target))
+            {
+                actualDelay /= 2.5f;
+            }
+            else if (Combat.IsInCombatMode(target))
+            {
+                // Slightly increase the delay when the target is in combat mode. Helps prevents cheese injections in
+                // combat with fast syringes & lag.
+                actualDelay += TimeSpan.FromSeconds(1);
+            }
+
+            // Add an admin log, using the "force feed" log type. It's not quite feeding, but the effect is the same.
+            if (injector.Comp.ToggleState == InjectorToggleMode.Inject)
+            {
+                AdminLogger.Add(LogType.ForceFeed,
+                    $"{EntityManager.ToPrettyString(user):user} is attempting to inject {EntityManager.ToPrettyString(target):target} with a solution {SharedSolutionContainerSystem.ToPrettyString(solution):solution}");
+            }
+            else
+            {
+                AdminLogger.Add(LogType.ForceFeed,
+                    $"{EntityManager.ToPrettyString(user):user} is attempting to draw {injector.Comp.TransferAmount.ToString()} units from {EntityManager.ToPrettyString(target):target}");
+            }
+        }
+        else
+        {
+            // Self-injections take half as long.
+            actualDelay /= 2;
+
+            if (injector.Comp.ToggleState == InjectorToggleMode.Inject)
+            {
+                AdminLogger.Add(LogType.Ingestion,
+                    $"{EntityManager.ToPrettyString(user):user} is attempting to inject themselves with a solution {SharedSolutionContainerSystem.ToPrettyString(solution):solution}.");
+            }
+            else
+            {
+                AdminLogger.Add(LogType.ForceFeed,
+                    $"{EntityManager.ToPrettyString(user):user} is attempting to draw {injector.Comp.TransferAmount.ToString()} units from themselves.");
+            }
+        }
+
+        DoAfter.TryStartDoAfter(new DoAfterArgs(EntityManager, user, actualDelay, new InjectorDoAfterEvent(), injector.Owner, target: target, used: injector.Owner)
+        {
+            BreakOnUserMove = true,
+            BreakOnDamage = true,
+            BreakOnTargetMove = true,
+            MovementThreshold = 0.1f,
+        });
+    }
+
+    private void TryInjectIntoBloodstream(Entity<InjectorComponent> injector, Entity<BloodstreamComponent> target,
+        EntityUid user)
+    {
+        // Get transfer amount. May be smaller than _transferAmount if not enough room
+        if (!SolutionContainers.ResolveSolution(target.Owner, target.Comp.ChemicalSolutionName,
+                ref target.Comp.ChemicalSolution, out var chemSolution))
+        {
+            Popup.PopupEntity(
+                Loc.GetString("injector-component-cannot-inject-message",
+                    ("target", Identity.Entity(target, EntityManager))), injector.Owner, user);
+            return;
+        }
+
+        var realTransferAmount = FixedPoint2.Min(injector.Comp.TransferAmount, chemSolution.AvailableVolume);
+        if (realTransferAmount <= 0)
+        {
+            Popup.PopupEntity(
+                Loc.GetString("injector-component-cannot-inject-message",
+                    ("target", Identity.Entity(target, EntityManager))), injector.Owner, user);
+            return;
+        }
+
+        // Move units from attackSolution to targetSolution
+        var removedSolution = SolutionContainers.SplitSolution(target.Comp.ChemicalSolution.Value, realTransferAmount);
+
+        _blood.TryAddToChemicals(target, removedSolution, target.Comp);
+
+        _reactiveSystem.DoEntityReaction(target, removedSolution, ReactionMethod.Injection);
+
+        Popup.PopupEntity(Loc.GetString("injector-component-inject-success-message",
+            ("amount", removedSolution.Volume),
+            ("target", Identity.Entity(target, EntityManager))), injector.Owner, user);
+
+        Dirty(injector);
+        AfterInject(injector, target);
+    }
+
+    private void TryInject(Entity<InjectorComponent> injector, EntityUid targetEntity,
+        Entity<SolutionComponent> targetSolution, EntityUid user, bool asRefill)
+    {
+        if (!SolutionContainers.TryGetSolution(injector.Owner, InjectorComponent.SolutionName, out var soln,
+                out var solution) || solution.Volume == 0)
+            return;
+
+        // Get transfer amount. May be smaller than _transferAmount if not enough room
+        var realTransferAmount =
+            FixedPoint2.Min(injector.Comp.TransferAmount, targetSolution.Comp.Solution.AvailableVolume);
+
+        if (realTransferAmount <= 0)
+        {
+            Popup.PopupEntity(
+                Loc.GetString("injector-component-target-already-full-message",
+                    ("target", Identity.Entity(targetEntity, EntityManager))),
+                injector.Owner, user);
+            return;
+        }
+
+        // Move units from attackSolution to targetSolution
+        Solution removedSolution;
+        if (TryComp<StackComponent>(targetEntity, out var stack))
+            removedSolution = SolutionContainers.SplitStackSolution(soln.Value, realTransferAmount, stack.Count);
+        else
+            removedSolution = SolutionContainers.SplitSolution(soln.Value, realTransferAmount);
+
+        _reactiveSystem.DoEntityReaction(targetEntity, removedSolution, ReactionMethod.Injection);
+
+        if (!asRefill)
+            SolutionContainers.Inject(targetEntity, targetSolution, removedSolution);
+        else
+            SolutionContainers.Refill(targetEntity, targetSolution, removedSolution);
+
+        Popup.PopupEntity(Loc.GetString("injector-component-transfer-success-message",
+            ("amount", removedSolution.Volume),
+            ("target", Identity.Entity(targetEntity, EntityManager))), injector.Owner, user);
+
+        Dirty(injector);
+        AfterInject(injector, targetEntity);
+    }
+
+    private void AfterInject(Entity<InjectorComponent> injector, EntityUid target)
+    {
+        // Automatically set syringe to draw after completely draining it.
+        if (SolutionContainers.TryGetSolution(injector.Owner, InjectorComponent.SolutionName, out _,
+                out var solution) && solution.Volume == 0)
+        {
+            SetMode(injector, InjectorToggleMode.Draw);
+        }
+
+        // Leave some DNA from the injectee on it
+        var ev = new TransferDnaEvent { Donor = target, Recipient = injector };
+        RaiseLocalEvent(target, ref ev);
+    }
+
+    private void AfterDraw(Entity<InjectorComponent> injector, EntityUid target)
+    {
+        // Automatically set syringe to inject after completely filling it.
+        if (SolutionContainers.TryGetSolution(injector.Owner, InjectorComponent.SolutionName, out _,
+                out var solution) && solution.AvailableVolume == 0)
+        {
+            SetMode(injector, InjectorToggleMode.Inject);
+        }
+
+        // Leave some DNA from the drawee on it
+        var ev = new TransferDnaEvent { Donor = target, Recipient = injector };
+        RaiseLocalEvent(target, ref ev);
+    }
+
+    private void TryDraw(Entity<InjectorComponent> injector, Entity<BloodstreamComponent?> target,
+        Entity<SolutionComponent> targetSolution, EntityUid user)
+    {
+        if (!SolutionContainers.TryGetSolution(injector.Owner, InjectorComponent.SolutionName, out var soln,
+                out var solution) || solution.AvailableVolume == 0)
+        {
+            return;
+        }
+
+        // Get transfer amount. May be smaller than _transferAmount if not enough room, also make sure there's room in the injector
+        var realTransferAmount = FixedPoint2.Min(injector.Comp.TransferAmount, targetSolution.Comp.Solution.Volume,
+            solution.AvailableVolume);
+
+        if (realTransferAmount <= 0)
+        {
+            Popup.PopupEntity(
+                Loc.GetString("injector-component-target-is-empty-message",
+                    ("target", Identity.Entity(target, EntityManager))),
+                injector.Owner, user);
+            return;
+        }
+
+        // We have some snowflaked behavior for streams.
+        if (target.Comp != null)
+        {
+            DrawFromBlood(injector, (target.Owner, target.Comp), soln.Value, realTransferAmount, user);
+            return;
+        }
+
+        // Move units from attackSolution to targetSolution
+        var removedSolution = SolutionContainers.Draw(target.Owner, targetSolution, realTransferAmount);
+
+        if (!SolutionContainers.TryAddSolution(soln.Value, removedSolution))
+        {
+            return;
+        }
+
+        Popup.PopupEntity(Loc.GetString("injector-component-draw-success-message",
+            ("amount", removedSolution.Volume),
+            ("target", Identity.Entity(target, EntityManager))), injector.Owner, user);
+
+        Dirty(injector);
+        AfterDraw(injector, target);
+    }
+
+    private void DrawFromBlood(Entity<InjectorComponent> injector, Entity<BloodstreamComponent> target,
+        Entity<SolutionComponent> injectorSolution, FixedPoint2 transferAmount, EntityUid user)
+    {
+        var drawAmount = (float) transferAmount;
+
+        if (SolutionContainers.ResolveSolution(target.Owner, target.Comp.ChemicalSolutionName,
+                ref target.Comp.ChemicalSolution))
+        {
+            var chemTemp = SolutionContainers.SplitSolution(target.Comp.ChemicalSolution.Value, drawAmount * 0.15f);
+            SolutionContainers.TryAddSolution(injectorSolution, chemTemp);
+            drawAmount -= (float) chemTemp.Volume;
+        }
+
+        if (SolutionContainers.ResolveSolution(target.Owner, target.Comp.BloodSolutionName,
+                ref target.Comp.BloodSolution))
+        {
+            var bloodTemp = SolutionContainers.SplitSolution(target.Comp.BloodSolution.Value, drawAmount);
+            SolutionContainers.TryAddSolution(injectorSolution, bloodTemp);
+        }
+
+        Popup.PopupEntity(Loc.GetString("injector-component-draw-success-message",
+            ("amount", transferAmount),
+            ("target", Identity.Entity(target, EntityManager))), injector.Owner, user);
+
+        Dirty(injector);
+        AfterDraw(injector, target);
+    }
+}
diff --git a/Content.Shared/Chemistry/Components/InjectorComponent.cs b/Content.Shared/Chemistry/Components/InjectorComponent.cs
new file mode 100644 (file)
index 0000000..e29047b
--- /dev/null
@@ -0,0 +1,104 @@
+using Content.Shared.Chemistry.EntitySystems;
+using Content.Shared.DoAfter;
+using Content.Shared.FixedPoint;
+using Robust.Shared.GameStates;
+using Robust.Shared.Serialization;
+
+namespace Content.Shared.Chemistry.Components;
+
+[Serializable, NetSerializable]
+public sealed partial class InjectorDoAfterEvent : SimpleDoAfterEvent
+{
+}
+
+/// <summary>
+/// Implements draw/inject behavior for droppers and syringes.
+/// </summary>
+/// <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.
+/// </remarks>
+/// <seealso cref="SharedInjectorSystem"/>
+/// <seealso cref="InjectorToggleMode"/>
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
+public sealed partial class InjectorComponent : Component
+{
+    public const string SolutionName = "injector";
+
+    /// <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("injectOnly")]
+    public bool InjectOnly;
+
+    /// <summary>
+    /// Whether or not the injector is able to draw from or inject from mobs
+    /// </summary>
+    /// <remarks>
+    ///     for example: droppers would ignore mobs
+    /// </remarks>
+    [DataField("ignoreMobs")]
+    public bool IgnoreMobs;
+
+    /// <summary>
+    ///     The minimum amount of solution that can be transferred at once from this solution.
+    /// </summary>
+    [DataField("minTransferAmount")]
+    [ViewVariables(VVAccess.ReadWrite)]
+    public FixedPoint2 MinimumTransferAmount = FixedPoint2.New(5);
+
+    /// <summary>
+    ///     The maximum amount of solution that can be transferred at once from this solution.
+    /// </summary>
+    [DataField("maxTransferAmount")]
+    [ViewVariables(VVAccess.ReadWrite)]
+    public FixedPoint2 MaximumTransferAmount = FixedPoint2.New(50);
+
+    /// <summary>
+    /// Amount to inject or draw on each usage. If the injector is inject only, it will
+    /// attempt to inject it's entire contents upon use.
+    /// </summary>
+    [ViewVariables(VVAccess.ReadWrite)]
+    [DataField("transferAmount")]
+    [AutoNetworkedField]
+    public FixedPoint2 TransferAmount = FixedPoint2.New(5);
+
+    /// <summary>
+    /// Injection delay (seconds) when the target is a mob.
+    /// </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>
+    [ViewVariables(VVAccess.ReadWrite)]
+    [DataField("delay")]
+    public TimeSpan Delay = TimeSpan.FromSeconds(5);
+
+    /// <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>
+    [ViewVariables(VVAccess.ReadWrite)]
+    [AutoNetworkedField]
+    [DataField]
+    public InjectorToggleMode ToggleState = InjectorToggleMode.Draw;
+}
+
+/// <summary>
+/// Possible modes for an <see cref="InjectorComponent"/>.
+/// </summary>
+public enum InjectorToggleMode : byte
+{
+    /// <summary>
+    /// The injector will try to inject reagent into things.
+    /// </summary>
+    Inject,
+
+    /// <summary>
+    /// The injector will try to draw reagent from things.
+    /// </summary>
+    Draw
+}
diff --git a/Content.Shared/Chemistry/Components/SharedInjectorComponent.cs b/Content.Shared/Chemistry/Components/SharedInjectorComponent.cs
deleted file mode 100644 (file)
index a4cea4e..0000000
+++ /dev/null
@@ -1,44 +0,0 @@
-using Content.Shared.DoAfter;
-using Content.Shared.FixedPoint;
-using Robust.Shared.GameStates;
-using Robust.Shared.Serialization;
-
-namespace Content.Shared.Chemistry.Components
-{
-    [Serializable, NetSerializable]
-    public sealed partial class InjectorDoAfterEvent : SimpleDoAfterEvent
-    {
-    }
-
-    /// <summary>
-    /// Shared class for injectors & syringes
-    /// </summary>
-    [NetworkedComponent, ComponentProtoName("Injector")]
-    public abstract partial class SharedInjectorComponent : Component
-    {
-        /// <summary>
-        /// Component data used for net updates. Used by client for item status ui
-        /// </summary>
-        [Serializable, NetSerializable]
-        public sealed class InjectorComponentState : ComponentState
-        {
-            public FixedPoint2 CurrentVolume { get; }
-            public FixedPoint2 TotalVolume { get; }
-            public InjectorToggleMode CurrentMode { get; }
-
-            public InjectorComponentState(FixedPoint2 currentVolume, FixedPoint2 totalVolume,
-                InjectorToggleMode currentMode)
-            {
-                CurrentVolume = currentVolume;
-                TotalVolume = totalVolume;
-                CurrentMode = currentMode;
-            }
-        }
-
-        public enum InjectorToggleMode : byte
-        {
-            Inject,
-            Draw
-        }
-    }
-}
diff --git a/Content.Shared/Chemistry/EntitySystems/SharedInjectorSystem.cs b/Content.Shared/Chemistry/EntitySystems/SharedInjectorSystem.cs
new file mode 100644 (file)
index 0000000..7ad2170
--- /dev/null
@@ -0,0 +1,120 @@
+using Content.Shared.Administration.Logs;
+using Content.Shared.Chemistry.Components;
+using Content.Shared.CombatMode;
+using Content.Shared.DoAfter;
+using Content.Shared.FixedPoint;
+using Content.Shared.Interaction.Events;
+using Content.Shared.Mobs.Systems;
+using Content.Shared.Popups;
+using Content.Shared.Verbs;
+using Robust.Shared.Player;
+
+namespace Content.Shared.Chemistry.EntitySystems;
+
+public abstract class SharedInjectorSystem : EntitySystem
+{
+    /// <summary>
+    ///     Default transfer amounts for the set-transfer verb.
+    /// </summary>
+    public static readonly FixedPoint2[] TransferAmounts = { 1, 5, 10, 15 };
+
+    [Dependency] protected readonly SharedPopupSystem Popup = default!;
+    [Dependency] protected readonly SharedSolutionContainerSystem SolutionContainers = default!;
+    [Dependency] protected readonly MobStateSystem MobState = default!;
+    [Dependency] protected readonly SharedCombatModeSystem Combat = default!;
+    [Dependency] protected readonly SharedDoAfterSystem DoAfter = default!;
+    [Dependency] protected readonly ISharedAdminLogManager AdminLogger = default!;
+
+    public override void Initialize()
+    {
+        SubscribeLocalEvent<InjectorComponent, GetVerbsEvent<AlternativeVerb>>(AddSetTransferVerbs);
+        SubscribeLocalEvent<InjectorComponent, ComponentStartup>(OnInjectorStartup);
+        SubscribeLocalEvent<InjectorComponent, UseInHandEvent>(OnInjectorUse);
+    }
+
+    private void AddSetTransferVerbs(Entity<InjectorComponent> entity, ref GetVerbsEvent<AlternativeVerb> args)
+    {
+        if (!args.CanAccess || !args.CanInteract || args.Hands == null)
+            return;
+
+        if (!HasComp<ActorComponent>(args.User))
+            return;
+
+        var (_, component) = entity;
+
+        // Add specific transfer verbs according to the container's size
+        var priority = 0;
+        var user = args.User;
+        foreach (var amount in TransferAmounts)
+        {
+            if (amount < component.MinimumTransferAmount || amount > component.MaximumTransferAmount)
+                continue;
+
+            AlternativeVerb verb = new()
+            {
+                Text = Loc.GetString("comp-solution-transfer-verb-amount", ("amount", amount)),
+                Category = VerbCategory.SetTransferAmount,
+                Act = () =>
+                {
+                    component.TransferAmount = amount;
+                    Popup.PopupClient(Loc.GetString("comp-solution-transfer-set-amount", ("amount", amount)), user, user);
+                    Dirty(entity);
+                },
+
+                // we want to sort by size, not alphabetically by the verb text.
+                Priority = priority
+            };
+
+            priority -= 1;
+
+            args.Verbs.Add(verb);
+        }
+    }
+
+    private void OnInjectorStartup(Entity<InjectorComponent> entity, ref ComponentStartup args)
+    {
+        // ???? why ?????
+        Dirty(entity);
+    }
+
+    private void OnInjectorUse(Entity<InjectorComponent> entity, ref UseInHandEvent args)
+    {
+        if (args.Handled)
+            return;
+
+        Toggle(entity, args.User);
+        args.Handled = true;
+    }
+
+    /// <summary>
+    /// Toggle between draw/inject state if applicable
+    /// </summary>
+    private void Toggle(Entity<InjectorComponent> injector, EntityUid user)
+    {
+        if (injector.Comp.InjectOnly)
+            return;
+
+        string msg;
+        switch (injector.Comp.ToggleState)
+        {
+            case InjectorToggleMode.Inject:
+                SetMode(injector, InjectorToggleMode.Draw);
+                msg = "injector-component-drawing-text";
+                break;
+            case InjectorToggleMode.Draw:
+                SetMode(injector, InjectorToggleMode.Inject);
+                msg = "injector-component-injecting-text";
+                break;
+            default:
+                throw new ArgumentOutOfRangeException();
+        }
+
+        Popup.PopupClient(Loc.GetString(msg), injector, user);
+    }
+
+    public void SetMode(Entity<InjectorComponent> injector, InjectorToggleMode mode)
+    {
+        injector.Comp.ToggleState = mode;
+        Dirty(injector);
+    }
+}
index e137130290c7b79d820ec8986e0809fd245a1fae..4fafc9cd3bbc2235c7c42e9dc6ac7a45ccd821b5 100644 (file)
@@ -3,7 +3,8 @@
 injector-draw-text = Draw
 injector-inject-text = Inject
 injector-invalid-injector-toggle-mode = Invalid
-injector-volume-label = Volume: [color=white]{$currentVolume}/{$totalVolume}[/color] | [color=white]{$modeString}[/color]
+injector-volume-label = Volume: [color=white]{$currentVolume}/{$totalVolume}[/color]
+    Mode: [color=white]{$modeString}[/color] ([color=white]{$transferVolume}u[/color])
 
 ## Entity