]> git.smokeofanarchy.ru Git - space-station-14.git/commitdiff
Hyposprays Draw from Jugs (#25544)
authorPlykiya <58439124+Plykiya@users.noreply.github.com>
Sat, 30 Mar 2024 03:59:16 +0000 (20:59 -0700)
committerGitHub <noreply@github.com>
Sat, 30 Mar 2024 03:59:16 +0000 (14:59 +1100)
* Hyposprays Draw from Jugs

* Fix last onlyMobs usage in yml

* Some Suggested Changes

* Remove unnecessary datafield name declarations

* Remove unnecessary dirtying of component

* Same line parentheses

* Added client-side HypospraySystem

* Cache UI values and only updates if values change

* empty line

* Update label

* Label change

* Reimplement ReactionMixerSystem

* Remove DataField from Hypospray Toggle Mode

* Change ToggleMode from enum to Bool OnlyAffectsMobs

* Add DataField required back since it's required for replays...?

* update EligibleEntity and uses of it

* Add user argument back

* Adds newline

Co-authored-by: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com>
* Guard for dirty entity

* Adds summary tag

---------

Co-authored-by: Plykiya <plykiya@protonmail.com>
Co-authored-by: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com>
16 files changed:
Content.Client/Chemistry/Components/HyposprayComponent.cs [deleted file]
Content.Client/Chemistry/EntitySystems/HypospraySystem.cs [new file with mode: 0644]
Content.Client/Chemistry/EntitySystems/InjectorSystem.cs
Content.Client/Chemistry/UI/HyposprayStatusControl.cs
Content.Server/Body/Components/BloodstreamComponent.cs
Content.Server/Chemistry/Components/HyposprayComponent.cs [deleted file]
Content.Server/Chemistry/EntitySystems/ChemistrySystem.cs [deleted file]
Content.Server/Chemistry/EntitySystems/ChemistrySystemHypospray.cs [deleted file]
Content.Server/Chemistry/EntitySystems/HypospraySystem.cs [new file with mode: 0644]
Content.Server/Chemistry/EntitySystems/ReactionMixerSystem.cs [moved from Content.Server/Chemistry/EntitySystems/ChemistrySystemMixer.cs with 76% similarity]
Content.Shared/Chemistry/Components/HyposprayComponent.cs [new file with mode: 0644]
Content.Shared/Chemistry/Components/SharedHyposprayComponent.cs [deleted file]
Content.Shared/Chemistry/EntitySystems/SharedHypospraySystem.cs [new file with mode: 0644]
Content.Shared/Chemistry/EntitySystems/SharedInjectorSystem.cs
Resources/Locale/en-US/chemistry/components/hypospray-component.ftl
Resources/Prototypes/Entities/Objects/Specific/Medical/hypospray.yml

diff --git a/Content.Client/Chemistry/Components/HyposprayComponent.cs b/Content.Client/Chemistry/Components/HyposprayComponent.cs
deleted file mode 100644 (file)
index 705b79a..0000000
+++ /dev/null
@@ -1,16 +0,0 @@
-using Content.Shared.Chemistry.Components;
-using Content.Shared.FixedPoint;
-
-namespace Content.Client.Chemistry.Components
-{
-    [RegisterComponent]
-    public sealed partial class HyposprayComponent : SharedHyposprayComponent
-    {
-        [ViewVariables]
-        public FixedPoint2 CurrentVolume;
-        [ViewVariables]
-        public FixedPoint2 TotalVolume;
-        [ViewVariables(VVAccess.ReadWrite)]
-        public bool UiUpdateNeeded;
-    }
-}
diff --git a/Content.Client/Chemistry/EntitySystems/HypospraySystem.cs b/Content.Client/Chemistry/EntitySystems/HypospraySystem.cs
new file mode 100644 (file)
index 0000000..ee7aa3a
--- /dev/null
@@ -0,0 +1,15 @@
+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 HypospraySystem : SharedHypospraySystem
+{
+    public override void Initialize()
+    {
+        base.Initialize();
+        Subs.ItemStatus<HyposprayComponent>(ent => new HyposprayStatusControl(ent, _solutionContainers));
+    }
+}
index 12eb7f3d14def795ef0bd7030395d3f7c1b082eb..0131a283c8c967dd467e766a7531a6e5fdd64ce1 100644 (file)
@@ -1,4 +1,3 @@
-using Content.Client.Chemistry.Components;
 using Content.Client.Chemistry.UI;
 using Content.Client.Items;
 using Content.Shared.Chemistry.Components;
@@ -13,17 +12,5 @@ public sealed class InjectorSystem : SharedInjectorSystem
     {
         base.Initialize();
         Subs.ItemStatus<InjectorComponent>(ent => new InjectorStatusControl(ent, SolutionContainers));
-        SubscribeLocalEvent<HyposprayComponent, ComponentHandleState>(OnHandleHyposprayState);
-        Subs.ItemStatus<HyposprayComponent>(ent => new HyposprayStatusControl(ent));
-    }
-
-    private void OnHandleHyposprayState(EntityUid uid, HyposprayComponent component, ref ComponentHandleState args)
-    {
-        if (args.Current is not HyposprayComponentState cState)
-            return;
-
-        component.CurrentVolume = cState.CurVolume;
-        component.TotalVolume = cState.MaxVolume;
-        component.UiUpdateNeeded = true;
     }
 }
index bd85cd546cc1fffc7a64b8bb72d956b8b1685bea..4a4d90dc4d5b0c822780d3500cdc1f8f767bb00b 100644 (file)
@@ -1,6 +1,8 @@
-using Content.Client.Chemistry.Components;
 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;
@@ -9,34 +11,48 @@ namespace Content.Client.Chemistry.UI;
 
 public sealed class HyposprayStatusControl : Control
 {
-    private readonly HyposprayComponent _parent;
+    private readonly Entity<HyposprayComponent> _parent;
     private readonly RichTextLabel _label;
+    private readonly SharedSolutionContainerSystem _solutionContainers;
 
-    public HyposprayStatusControl(HyposprayComponent parent)
+    private FixedPoint2 PrevVolume;
+    private FixedPoint2 PrevMaxVolume;
+    private bool PrevOnlyAffectsMobs;
+
+    public HyposprayStatusControl(Entity<HyposprayComponent> parent, SharedSolutionContainerSystem solutionContainers)
     {
         _parent = parent;
-        _label = new RichTextLabel {StyleClasses = {StyleNano.StyleClassItemStatus}};
+        _solutionContainers = solutionContainers;
+        _label = new RichTextLabel { StyleClasses = { StyleNano.StyleClassItemStatus } };
         AddChild(_label);
-
-        Update();
     }
 
     protected override void FrameUpdate(FrameEventArgs args)
     {
         base.FrameUpdate(args);
-        if (!_parent.UiUpdateNeeded)
+
+        if (!_solutionContainers.TryGetSolution(_parent.Owner, _parent.Comp.SolutionName, out _, out var solution))
             return;
-        Update();
-    }
 
-    public void Update()
-    {
+        // 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;
 
-        _parent.UiUpdateNeeded = false;
+        var modeStringLocalized = Loc.GetString(_parent.Comp.OnlyAffectsMobs switch
+        {
+            false => "hypospray-all-mode-text",
+            true => "hypospray-mobs-only-mode-text",
+        });
 
-        _label.SetMarkup(Loc.GetString(
-            "hypospray-volume-text",
-            ("currentVolume", _parent.CurrentVolume),
-            ("totalVolume", _parent.TotalVolume)));
+        _label.SetMarkup(Loc.GetString("hypospray-volume-label",
+            ("currentVolume", solution.Volume),
+            ("totalVolume", solution.MaxVolume),
+            ("modeString", modeStringLocalized)));
     }
 }
index d448c4aab21b691dc8aa559f2188cd247d751f28..1d8aa9ffd3d49e3eceaeef3967345c3ef9e41237 100644 (file)
@@ -11,7 +11,7 @@ using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
 
 namespace Content.Server.Body.Components
 {
-    [RegisterComponent, Access(typeof(BloodstreamSystem), (typeof(ChemistrySystem)))]
+    [RegisterComponent, Access(typeof(BloodstreamSystem), typeof(ReactionMixerSystem))]
     public sealed partial class BloodstreamComponent : Component
     {
         public static string DefaultChemicalsSolutionName = "chemicals";
diff --git a/Content.Server/Chemistry/Components/HyposprayComponent.cs b/Content.Server/Chemistry/Components/HyposprayComponent.cs
deleted file mode 100644 (file)
index 2a80cec..0000000
+++ /dev/null
@@ -1,28 +0,0 @@
-using Content.Shared.Chemistry.Components;
-using Content.Shared.FixedPoint;
-using Robust.Shared.Audio;
-
-namespace Content.Server.Chemistry.Components
-{
-    [RegisterComponent]
-    public sealed partial class HyposprayComponent : SharedHyposprayComponent
-    {
-        // TODO: This should be on clumsycomponent.
-        [DataField("clumsyFailChance")]
-        [ViewVariables(VVAccess.ReadWrite)]
-        public float ClumsyFailChance = 0.5f;
-
-        [DataField("transferAmount")]
-        [ViewVariables(VVAccess.ReadWrite)]
-        public FixedPoint2 TransferAmount = FixedPoint2.New(5);
-
-        [DataField("injectSound")]
-        public SoundSpecifier InjectSound = new SoundPathSpecifier("/Audio/Items/hypospray.ogg");
-
-        /// <summary>
-        /// Whether or not the hypo is able to inject only into mobs. On false you can inject into beakers/jugs
-        /// </summary>
-        [DataField("onlyMobs")]
-        public bool OnlyMobs = true;
-    }
-}
diff --git a/Content.Server/Chemistry/EntitySystems/ChemistrySystem.cs b/Content.Server/Chemistry/EntitySystems/ChemistrySystem.cs
deleted file mode 100644 (file)
index c4f22dc..0000000
+++ /dev/null
@@ -1,26 +0,0 @@
-using Content.Server.Administration.Logs;
-using Content.Server.Chemistry.Containers.EntitySystems;
-using Content.Server.Interaction;
-using Content.Server.Popups;
-using Content.Shared.Chemistry;
-using Robust.Shared.Audio.Systems;
-
-namespace Content.Server.Chemistry.EntitySystems;
-
-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 PopupSystem _popup = default!;
-    [Dependency] private readonly ReactiveSystem _reactiveSystem = default!;
-    [Dependency] private readonly SharedAudioSystem _audio = default!;
-    [Dependency] private readonly SolutionContainerSystem _solutionContainers = default!;
-
-    public override void Initialize()
-    {
-        // Why ChemMaster duplicates reagentdispenser nobody knows.
-        InitializeHypospray();
-        InitializeMixing();
-    }
-}
diff --git a/Content.Server/Chemistry/EntitySystems/ChemistrySystemHypospray.cs b/Content.Server/Chemistry/EntitySystems/ChemistrySystemHypospray.cs
deleted file mode 100644 (file)
index be8faec..0000000
+++ /dev/null
@@ -1,164 +0,0 @@
-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.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.Timing;
-using Content.Shared.Weapons.Melee.Events;
-using Robust.Shared.GameStates;
-using System.Diagnostics.CodeAnalysis;
-using System.Linq;
-
-namespace Content.Server.Chemistry.EntitySystems
-{
-    public sealed partial class ChemistrySystem
-    {
-        [Dependency] private readonly UseDelaySystem _useDelay = default!;
-
-        private void InitializeHypospray()
-        {
-            SubscribeLocalEvent<HyposprayComponent, AfterInteractEvent>(OnAfterInteract);
-            SubscribeLocalEvent<HyposprayComponent, MeleeHitEvent>(OnAttack);
-            SubscribeLocalEvent<HyposprayComponent, SolutionContainerChangedEvent>(OnSolutionChange);
-            SubscribeLocalEvent<HyposprayComponent, UseInHandEvent>(OnUseInHand);
-            SubscribeLocalEvent<HyposprayComponent, ComponentGetState>(OnHypoGetState);
-        }
-
-        private void OnHypoGetState(Entity<HyposprayComponent> entity, ref ComponentGetState args)
-        {
-            args.State = _solutionContainers.TryGetSolution(entity.Owner, entity.Comp.SolutionName, out _, out var solution)
-                ? new HyposprayComponentState(solution.Volume, solution.MaxVolume)
-                : new HyposprayComponentState(FixedPoint2.Zero, FixedPoint2.Zero);
-        }
-
-        private void OnUseInHand(Entity<HyposprayComponent> entity, ref UseInHandEvent args)
-        {
-            if (args.Handled)
-                return;
-
-            TryDoInject(entity, args.User, args.User);
-            args.Handled = true;
-        }
-
-        private void OnSolutionChange(Entity<HyposprayComponent> entity, ref SolutionContainerChangedEvent args)
-        {
-            Dirty(entity);
-        }
-
-        public void OnAfterInteract(Entity<HyposprayComponent> entity, ref AfterInteractEvent args)
-        {
-            if (!args.CanReach)
-                return;
-
-            var target = args.Target;
-            var user = args.User;
-
-            TryDoInject(entity, target, user);
-        }
-
-        public void OnAttack(Entity<HyposprayComponent> entity, ref MeleeHitEvent args)
-        {
-            if (!args.HitEntities.Any())
-                return;
-
-            TryDoInject(entity, args.HitEntities.First(), args.User);
-        }
-
-        public bool TryDoInject(Entity<HyposprayComponent> hypo, EntityUid? target, EntityUid user)
-        {
-            var (uid, component) = hypo;
-
-            if (!EligibleEntity(target, _entMan, component))
-                return false;
-
-            if (TryComp(uid, out UseDelayComponent? delayComp))
-            {
-                if (_useDelay.IsDelayed((uid, delayComp)))
-                    return false;
-            }
-
-
-            string? msgFormat = null;
-
-            if (target == user)
-                msgFormat = "hypospray-component-inject-self-message";
-            else if (EligibleEntity(user, _entMan, component) && _interaction.TryRollClumsy(user, component.ClumsyFailChance))
-            {
-                msgFormat = "hypospray-component-inject-self-clumsy-message";
-                target = user;
-            }
-
-            if (!_solutionContainers.TryGetSolution(uid, component.SolutionName, out var hypoSpraySoln, out var hypoSpraySolution) || hypoSpraySolution.Volume == 0)
-            {
-                _popup.PopupCursor(Loc.GetString("hypospray-component-empty-message"), user);
-                return true;
-            }
-
-            if (!_solutionContainers.TryGetInjectableSolution(target.Value, out var targetSoln, out var targetSolution))
-            {
-                _popup.PopupCursor(Loc.GetString("hypospray-cant-inject", ("target", Identity.Entity(target.Value, _entMan))), user);
-                return false;
-            }
-
-            _popup.PopupCursor(Loc.GetString(msgFormat ?? "hypospray-component-inject-other-message", ("other", target)), user);
-
-            if (target != user)
-            {
-                _popup.PopupEntity(Loc.GetString("hypospray-component-feel-prick-message"), target.Value, target.Value);
-                // TODO: This should just be using melee attacks...
-                // meleeSys.SendLunge(angle, user);
-            }
-
-            _audio.PlayPvs(component.InjectSound, 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.PopupCursor(Loc.GetString("hypospray-component-transfer-already-full-message", ("owner", 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.Value, removedSolution, ReactionMethod.Injection);
-            _solutionContainers.TryAddSolution(targetSoln.Value, removedSolution);
-
-            var ev = new TransferDnaEvent { Donor = target.Value, Recipient = uid };
-            RaiseLocalEvent(target.Value, ref ev);
-
-            // same LogType as syringes...
-            _adminLogger.Add(LogType.ForceFeed, $"{_entMan.ToPrettyString(user):user} injected {_entMan.ToPrettyString(target.Value):target} with a solution {SolutionContainerSystem.ToPrettyString(removedSolution):removedSolution} using a {_entMan.ToPrettyString(uid):using}");
-
-            return true;
-        }
-
-        static bool EligibleEntity([NotNullWhen(true)] EntityUid? entity, IEntityManager entMan, 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.OnlyMobs
-                ? entMan.HasComponent<SolutionContainerManagerComponent>(entity) &&
-                  entMan.HasComponent<MobStateComponent>(entity)
-                : entMan.HasComponent<SolutionContainerManagerComponent>(entity);
-        }
-    }
-}
diff --git a/Content.Server/Chemistry/EntitySystems/HypospraySystem.cs b/Content.Server/Chemistry/EntitySystems/HypospraySystem.cs
new file mode 100644 (file)
index 0000000..dfbe45c
--- /dev/null
@@ -0,0 +1,197 @@
+using Content.Shared.Chemistry.EntitySystems;
+using Content.Shared.Chemistry.Components;
+using Content.Shared.Chemistry.Components.SolutionManager;
+using Content.Shared.Chemistry.Reagent;
+using Content.Shared.Database;
+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.Timing;
+using Content.Shared.Weapons.Melee.Events;
+using Content.Server.Interaction;
+using Content.Server.Body.Components;
+using Content.Server.Chemistry.Containers.EntitySystems;
+using Robust.Shared.GameStates;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+using Robust.Server.Audio;
+
+namespace Content.Server.Chemistry.EntitySystems;
+
+public sealed class HypospraySystem : SharedHypospraySystem
+{
+    [Dependency] private readonly AudioSystem _audio = default!;
+    [Dependency] private readonly InteractionSystem _interaction = default!;
+    [Dependency] private readonly SolutionContainerSystem _solutionContainerSystem = default!;
+
+    public override void Initialize()
+    {
+        base.Initialize();
+
+        SubscribeLocalEvent<HyposprayComponent, AfterInteractEvent>(OnAfterInteract);
+        SubscribeLocalEvent<HyposprayComponent, MeleeHitEvent>(OnAttack);
+        SubscribeLocalEvent<HyposprayComponent, UseInHandEvent>(OnUseInHand);
+    }
+
+    private void UseHypospray(Entity<HyposprayComponent> entity, EntityUid target, EntityUid user)
+    {
+        // if target is ineligible but is a container, try to draw from the container
+        if (!EligibleEntity(target, EntityManager, entity)
+            && _solutionContainers.TryGetDrawableSolution(target, out var drawableSolution, out _))
+        {
+            TryDraw(entity, target, drawableSolution.Value, user);
+        }
+
+        TryDoInject(entity, target, user);
+    }
+
+    private void OnUseInHand(Entity<HyposprayComponent> entity, ref UseInHandEvent args)
+    {
+        if (args.Handled)
+            return;
+
+        TryDoInject(entity, args.User, args.User);
+        args.Handled = true;
+    }
+
+    public void OnAfterInteract(Entity<HyposprayComponent> entity, ref AfterInteractEvent args)
+    {
+        if (args.Handled || !args.CanReach || args.Target == null)
+            return;
+
+        UseHypospray(entity, args.Target.Value, args.User);
+        args.Handled = true;
+    }
+
+    public void OnAttack(Entity<HyposprayComponent> entity, ref MeleeHitEvent args)
+    {
+        if (!args.HitEntities.Any())
+            return;
+
+        TryDoInject(entity, args.HitEntities.First(), args.User);
+    }
+
+    public bool TryDoInject(Entity<HyposprayComponent> entity, EntityUid target, EntityUid user)
+    {
+        var (uid, component) = entity;
+
+        if (!EligibleEntity(target, EntityManager, component))
+            return false;
+
+        if (TryComp(uid, out UseDelayComponent? delayComp))
+        {
+            if (_useDelay.IsDelayed((uid, delayComp)))
+                return false;
+        }
+
+        string? msgFormat = null;
+
+        if (target == user)
+            msgFormat = "hypospray-component-inject-self-message";
+        else if (EligibleEntity(user, EntityManager, component) && _interaction.TryRollClumsy(user, component.ClumsyFailChance))
+        {
+            msgFormat = "hypospray-component-inject-self-clumsy-message";
+            target = user;
+        }
+
+        if (!_solutionContainers.TryGetSolution(uid, component.SolutionName, out var hypoSpraySoln, out var hypoSpraySolution) || hypoSpraySolution.Volume == 0)
+        {
+            _popup.PopupEntity(Loc.GetString("hypospray-component-empty-message"), target, user);
+            return true;
+        }
+
+        if (!_solutionContainers.TryGetInjectableSolution(target, out var targetSoln, out var targetSolution))
+        {
+            _popup.PopupEntity(Loc.GetString("hypospray-cant-inject", ("target", Identity.Entity(target, EntityManager))), target, user);
+            return false;
+        }
+
+        _popup.PopupEntity(Loc.GetString(msgFormat ?? "hypospray-component-inject-other-message", ("other", target)), 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.PlayPvs(component.InjectSound, 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.PopupEntity(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, $"{EntityManager.ToPrettyString(user):user} injected {EntityManager.ToPrettyString(target):target} with a solution {SolutionContainerSystem.ToPrettyString(removedSolution):removedSolution} using a {EntityManager.ToPrettyString(uid):using}");
+
+        return true;
+    }
+
+    private void TryDraw(Entity<HyposprayComponent> entity, Entity<BloodstreamComponent?> target, Entity<SolutionComponent> targetSolution, EntityUid user)
+    {
+        if (!_solutionContainers.TryGetSolution(entity.Owner, entity.Comp.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(entity.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))),
+                entity.Owner, user);
+            return;
+        }
+
+        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))), entity.Owner, user);
+    }
+
+    private bool EligibleEntity(EntityUid entity, IEntityManager entMan, 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
+            ? entMan.HasComponent<SolutionContainerManagerComponent>(entity) &&
+              entMan.HasComponent<MobStateComponent>(entity)
+            : entMan.HasComponent<SolutionContainerManagerComponent>(entity);
+    }
+}
similarity index 76%
rename from Content.Server/Chemistry/EntitySystems/ChemistrySystemMixer.cs
rename to Content.Server/Chemistry/EntitySystems/ReactionMixerSystem.cs
index 0230671ec986eee1f6706f90302fd3f342b07d95..032374d4a5516d87de10556a2cf22ccf166d9af8 100644 (file)
@@ -1,13 +1,20 @@
 using Content.Shared.Chemistry.Reaction;
 using Content.Shared.IdentityManagement;
 using Content.Shared.Interaction;
+using Content.Server.Chemistry.Containers.EntitySystems;
+using Content.Server.Popups;
 
 namespace Content.Server.Chemistry.EntitySystems;
 
-public sealed partial class ChemistrySystem
+public sealed partial class ReactionMixerSystem : EntitySystem
 {
-    public void InitializeMixing()
+    [Dependency] private readonly PopupSystem _popup = default!;
+    [Dependency] private readonly SolutionContainerSystem _solutionContainers = default!;
+
+    public override void Initialize()
     {
+        base.Initialize();
+
         SubscribeLocalEvent<ReactionMixerComponent, AfterInteractEvent>(OnAfterInteract);
     }
 
diff --git a/Content.Shared/Chemistry/Components/HyposprayComponent.cs b/Content.Shared/Chemistry/Components/HyposprayComponent.cs
new file mode 100644 (file)
index 0000000..05d202a
--- /dev/null
@@ -0,0 +1,33 @@
+using Content.Shared.FixedPoint;
+using Robust.Shared.GameStates;
+using Robust.Shared.Serialization;
+using Robust.Shared.Audio;
+
+namespace Content.Shared.Chemistry.Components;
+
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
+public sealed partial class HyposprayComponent : Component
+{
+    [DataField]
+    public string SolutionName = "hypospray";
+
+    // TODO: This should be on clumsycomponent.
+    [DataField]
+    [ViewVariables(VVAccess.ReadWrite)]
+    public float ClumsyFailChance = 0.5f;
+
+    [DataField]
+    [ViewVariables(VVAccess.ReadWrite)]
+    public FixedPoint2 TransferAmount = FixedPoint2.New(5);
+
+    [DataField]
+    public SoundSpecifier InjectSound = new SoundPathSpecifier("/Audio/Items/hypospray.ogg");
+
+    /// <summary>
+    /// Decides whether you can inject everything or just mobs.
+    /// When you can only affect mobs, you're capable of drawing from beakers.
+    /// </summary>
+    [AutoNetworkedField]
+    [DataField(required: true)]
+    public bool OnlyAffectsMobs = false;
+}
diff --git a/Content.Shared/Chemistry/Components/SharedHyposprayComponent.cs b/Content.Shared/Chemistry/Components/SharedHyposprayComponent.cs
deleted file mode 100644 (file)
index a8df6be..0000000
+++ /dev/null
@@ -1,25 +0,0 @@
-using Content.Shared.FixedPoint;
-using Robust.Shared.GameStates;
-using Robust.Shared.Serialization;
-
-namespace Content.Shared.Chemistry.Components;
-
-[NetworkedComponent()]
-public abstract partial class SharedHyposprayComponent : Component
-{
-    [DataField("solutionName")]
-    public string SolutionName = "hypospray";
-}
-
-[Serializable, NetSerializable]
-public sealed class HyposprayComponentState : ComponentState
-{
-    public FixedPoint2 CurVolume { get; }
-    public FixedPoint2 MaxVolume { get; }
-
-    public HyposprayComponentState(FixedPoint2 curVolume, FixedPoint2 maxVolume)
-    {
-        CurVolume = curVolume;
-        MaxVolume = maxVolume;
-    }
-}
diff --git a/Content.Shared/Chemistry/EntitySystems/SharedHypospraySystem.cs b/Content.Shared/Chemistry/EntitySystems/SharedHypospraySystem.cs
new file mode 100644 (file)
index 0000000..f91e562
--- /dev/null
@@ -0,0 +1,61 @@
+using Content.Shared.Chemistry.Components;
+using Content.Shared.Timing;
+using Content.Shared.Verbs;
+using Content.Shared.Popups;
+using Robust.Shared.Player;
+using Content.Shared.Administration.Logs;
+
+namespace Content.Shared.Chemistry.EntitySystems;
+
+public abstract class SharedHypospraySystem : EntitySystem
+{
+    [Dependency] protected readonly UseDelaySystem _useDelay = default!;
+    [Dependency] protected readonly SharedPopupSystem _popup = default!;
+    [Dependency] protected readonly SharedSolutionContainerSystem _solutionContainers = default!;
+    [Dependency] protected readonly ISharedAdminLogManager _adminLogger = default!;
+    [Dependency] protected readonly ReactiveSystem _reactiveSystem = default!;
+    
+    public override void Initialize()
+    {
+        SubscribeLocalEvent<HyposprayComponent, GetVerbsEvent<AlternativeVerb>>(AddToggleModeVerb);
+    }
+
+    // <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)
+            return;
+
+        var (_, component) = entity;
+        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);
+        string msg = entity.Comp.OnlyAffectsMobs ? "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);
+    }
+}
index 7e41cb39bd673dab9a5aead0efae232e83a0ae8d..6c43c1d5f06dc15672a65e651f6228f3310a0f51 100644 (file)
@@ -37,10 +37,7 @@ public abstract class SharedInjectorSystem : EntitySystem
         if (!args.CanAccess || !args.CanInteract || args.Hands == null)
             return;
 
-        if (!HasComp<ActorComponent>(args.User))
-            return;
         var user = args.User;
-
         var (_, component) = entity;
 
         var min = component.MinimumTransferAmount;
index 7acbe8664c4a4ab732369f1833423cf119033b3a..52dbf9010e3b3478259a0588fb3ec6107e29ee97 100644 (file)
@@ -1,13 +1,21 @@
 ## UI
 
-hypospray-volume-text = Volume: [color=white]{$currentVolume}/{$totalVolume}[/color]
+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 {$other}.
 hypospray-component-inject-self-message = You inject yourself.
 hypospray-component-inject-self-clumsy-message = Oops! You injected yourself.
-hypospray-component-empty-message = It's empty!
+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 3d28487d68e65b3e2c7feb0d74fe948b9c4ae967..abcabd74810c665861f73b9441c86619281e0679 100644 (file)
@@ -18,7 +18,7 @@
   - type: ExaminableSolution
     solution: hypospray
   - type: Hypospray
-    onlyMobs: false
+    onlyAffectsMobs: false
   - type: UseDelay
     delay: 0.5
   - type: StaticPrice
@@ -49,7 +49,7 @@
   - type: ExaminableSolution
     solution: hypospray
   - type: Hypospray
-    onlyMobs: false
+    onlyAffectsMobs: false
   - type: UseDelay
     delay: 0.5
 
@@ -73,6 +73,7 @@
   - type: ExaminableSolution
     solution: hypospray
   - type: Hypospray
+    onlyAffectsMobs: false
   - type: UseDelay
     delay: 0.5
 
   - type: Hypospray
     solutionName: pen
     transferAmount: 15
+    onlyAffectsMobs: false
   - type: Appearance
   - type: SolutionContainerVisuals
     maxFillLevels: 1
   - type: Hypospray
     solutionName: pen
     transferAmount: 20
+    onlyAffectsMobs: false
   - type: SolutionContainerManager
     solutions:
       pen:
   - type: Hypospray
     solutionName: pen
     transferAmount: 20
+    onlyAffectsMobs: false
   - type: SolutionContainerManager
     solutions:
       pen:
   - type: Hypospray
     solutionName: pen
     transferAmount: 20
+    onlyAffectsMobs: false
   - type: SolutionContainerManager
     solutions:
       pen:
   - type: Hypospray
     solutionName: pen
     transferAmount: 30
+    onlyAffectsMobs: false
   - type: SolutionContainerManager
     solutions:
       pen:
   - type: Hypospray
     solutionName: pen
     transferAmount: 30
+    onlyAffectsMobs: false
   - type: StaticPrice
     price: 500
   - type: Tag
   - type: Hypospray
     solutionName: pen
     transferAmount: 30
+    onlyAffectsMobs: false
   - type: StaticPrice
     price: 500
   - type: Tag
   - type: ExaminableSolution
     solution: hypospray
   - type: Hypospray
-    onlyMobs: false
+    onlyAffectsMobs: false
   - type: UseDelay
     delay: 0.5
   - type: StaticPrice # A new shitcurity meta