diff --git a/Content.Client/Chemistry/Components/InjectorComponent.cs b/Content.Client/Chemistry/Components/InjectorComponent.cs
deleted file mode 100644
index 4d10517a11..0000000000
--- a/Content.Client/Chemistry/Components/InjectorComponent.cs
+++ /dev/null
@@ -1,21 +0,0 @@
-using Content.Shared.Chemistry.Components;
-using Content.Shared.FixedPoint;
-
-namespace Content.Client.Chemistry.Components
-{
- ///
- /// Client behavior for injectors & syringes. Used for item status on injectors
- ///
- [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;
- }
-}
diff --git a/Content.Client/Chemistry/EntitySystems/InjectorSystem.cs b/Content.Client/Chemistry/EntitySystems/InjectorSystem.cs
index 896349a161..12eb7f3d14 100644
--- a/Content.Client/Chemistry/EntitySystems/InjectorSystem.cs
+++ b/Content.Client/Chemistry/EntitySystems/InjectorSystem.cs
@@ -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(OnHandleInjectorState);
- Subs.ItemStatus(ent => new InjectorStatusControl(ent));
+ Subs.ItemStatus(ent => new InjectorStatusControl(ent, SolutionContainers));
SubscribeLocalEvent(OnHandleHyposprayState);
Subs.ItemStatus(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)
diff --git a/Content.Client/Chemistry/UI/InjectorStatusControl.cs b/Content.Client/Chemistry/UI/InjectorStatusControl.cs
index f772320168..979e9ea645 100644
--- a/Content.Client/Chemistry/UI/InjectorStatusControl.cs
+++ b/Content.Client/Chemistry/UI/InjectorStatusControl.cs
@@ -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 _parent;
+ private readonly SharedSolutionContainerSystem _solutionContainers;
private readonly RichTextLabel _label;
- public InjectorStatusControl(InjectorComponent parent)
+ public InjectorStatusControl(Entity 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)
+
+ if (!_solutionContainers.TryGetSolution(_parent.Owner, InjectorComponent.SolutionName, out _, out var solution))
return;
- Update();
- }
- public void Update()
- {
- _parent.UiUpdateNeeded = false;
-
- //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
index d6d149ad0f..0000000000
--- a/Content.Server/Chemistry/Components/InjectorComponent.cs
+++ /dev/null
@@ -1,83 +0,0 @@
-using Content.Shared.Chemistry.Components;
-using Content.Shared.FixedPoint;
-
-namespace Content.Server.Chemistry.Components
-{
- ///
- /// 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.
- ///
- [RegisterComponent]
- public sealed partial class InjectorComponent : SharedInjectorComponent
- {
- public const string SolutionName = "injector";
-
- ///
- /// Whether or not the injector is able to draw from containers or if it's a single use
- /// device that can only inject.
- ///
- [DataField("injectOnly")]
- public bool InjectOnly;
-
- ///
- /// Whether or not the injector is able to draw from or inject from mobs
- ///
- ///
- /// for example: droppers would ignore mobs
- ///
- [DataField("ignoreMobs")]
- public bool IgnoreMobs = false;
-
- ///
- /// The minimum amount of solution that can be transferred at once from this solution.
- ///
- [DataField("minTransferAmount")]
- [ViewVariables(VVAccess.ReadWrite)]
- public FixedPoint2 MinimumTransferAmount { get; set; } = FixedPoint2.New(5);
-
- ///
- /// The maximum amount of solution that can be transferred at once from this solution.
- ///
- [DataField("maxTransferAmount")]
- [ViewVariables(VVAccess.ReadWrite)]
- public FixedPoint2 MaximumTransferAmount { get; set; } = FixedPoint2.New(50);
-
- ///
- /// 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.
- ///
- [ViewVariables(VVAccess.ReadWrite)]
- [DataField("transferAmount")]
- public FixedPoint2 TransferAmount = FixedPoint2.New(5);
-
- ///
- /// Injection delay (seconds) when the target is a mob.
- ///
- ///
- /// The base delay has a minimum of 1 second, but this will still be modified if the target is incapacitated or
- /// in combat mode.
- ///
- [ViewVariables(VVAccess.ReadWrite)]
- [DataField("delay")]
- public float Delay = 5;
-
- [DataField("toggleState")] private InjectorToggleMode _toggleState;
-
- ///
- /// 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
- ///
- [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
index 4f149db836..0000000000
--- a/Content.Server/Chemistry/EntitySystems/ChemistrySystem.Injector.cs
+++ /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
-{
-
- ///
- /// Default transfer amounts for the set-transfer verb.
- ///
- public static readonly List TransferAmounts = new() { 1, 5, 10, 15 };
- private void InitializeInjector()
- {
- SubscribeLocalEvent>(AddSetTransferVerbs);
- SubscribeLocalEvent(OnSolutionChange);
- SubscribeLocalEvent(OnInjectDoAfter);
- SubscribeLocalEvent(OnInjectorStartup);
- SubscribeLocalEvent(OnInjectorUse);
- SubscribeLocalEvent(OnInjectorAfterInteract);
- SubscribeLocalEvent(OnInjectorGetState);
- }
-
- private void AddSetTransferVerbs(Entity entity, ref GetVerbsEvent 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 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(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(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 entity, ref SolutionContainerChangedEvent args)
- {
- Dirty(entity);
- }
-
- private void OnInjectorGetState(Entity 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 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 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(entity))
- return;
-
- // Is the target a mob? If yes, use a do-after to give them time to respond.
- if (HasComp(target) || HasComp(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 entity, ref ComponentStartup args)
- {
- // ???? why ?????
- Dirty(entity);
- }
-
- private void OnInjectorUse(Entity entity, ref UseInHandEvent args)
- {
- if (args.Handled)
- return;
-
- Toggle(entity, args.User);
- args.Handled = true;
- }
-
- ///
- /// Toggle between draw/inject state if applicable
- ///
- private void Toggle(Entity 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);
- }
-
- ///
- /// Send informative pop-up messages and wait for a do-after to complete.
- ///
- private void InjectDoAfter(Entity 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 injector, Entity 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 injector, EntityUid targetEntity, Entity 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(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 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 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 injector, Entity target, Entity 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 injector, Entity target, Entity 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.Server/Chemistry/EntitySystems/ChemistrySystem.cs b/Content.Server/Chemistry/EntitySystems/ChemistrySystem.cs
index a2b4f399e2..c4f22dc63a 100644
--- a/Content.Server/Chemistry/EntitySystems/ChemistrySystem.cs
+++ b/Content.Server/Chemistry/EntitySystems/ChemistrySystem.cs
@@ -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
index 0000000000..a4497c0bd6
--- /dev/null
+++ b/Content.Server/Chemistry/EntitySystems/InjectorSystem.cs
@@ -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(OnInjectDoAfter);
+ SubscribeLocalEvent(OnInjectorAfterInteract);
+ }
+
+ private void UseInjector(Entity 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(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(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 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 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(entity))
+ return;
+
+ // Is the target a mob? If yes, use a do-after to give them time to respond.
+ if (HasComp(target) || HasComp(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;
+ }
+
+ ///
+ /// Send informative pop-up messages and wait for a do-after to complete.
+ ///
+ private void InjectDoAfter(Entity 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 injector, Entity 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 injector, EntityUid targetEntity,
+ Entity 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(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 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 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 injector, Entity target,
+ Entity 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 injector, Entity target,
+ Entity 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
index 0000000000..e29047b6de
--- /dev/null
+++ b/Content.Shared/Chemistry/Components/InjectorComponent.cs
@@ -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
+{
+}
+
+///
+/// Implements draw/inject behavior for droppers 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.
+///
+///
+///
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
+public sealed partial class InjectorComponent : Component
+{
+ public const string SolutionName = "injector";
+
+ ///
+ /// Whether or not the injector is able to draw from containers or if it's a single use
+ /// device that can only inject.
+ ///
+ [DataField("injectOnly")]
+ public bool InjectOnly;
+
+ ///
+ /// Whether or not the injector is able to draw from or inject from mobs
+ ///
+ ///
+ /// for example: droppers would ignore mobs
+ ///
+ [DataField("ignoreMobs")]
+ public bool IgnoreMobs;
+
+ ///
+ /// The minimum amount of solution that can be transferred at once from this solution.
+ ///
+ [DataField("minTransferAmount")]
+ [ViewVariables(VVAccess.ReadWrite)]
+ public FixedPoint2 MinimumTransferAmount = FixedPoint2.New(5);
+
+ ///
+ /// The maximum amount of solution that can be transferred at once from this solution.
+ ///
+ [DataField("maxTransferAmount")]
+ [ViewVariables(VVAccess.ReadWrite)]
+ public FixedPoint2 MaximumTransferAmount = FixedPoint2.New(50);
+
+ ///
+ /// 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.
+ ///
+ [ViewVariables(VVAccess.ReadWrite)]
+ [DataField("transferAmount")]
+ [AutoNetworkedField]
+ public FixedPoint2 TransferAmount = FixedPoint2.New(5);
+
+ ///
+ /// Injection delay (seconds) when the target is a mob.
+ ///
+ ///
+ /// The base delay has a minimum of 1 second, but this will still be modified if the target is incapacitated or
+ /// in combat mode.
+ ///
+ [ViewVariables(VVAccess.ReadWrite)]
+ [DataField("delay")]
+ public TimeSpan Delay = TimeSpan.FromSeconds(5);
+
+ ///
+ /// 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
+ ///
+ [ViewVariables(VVAccess.ReadWrite)]
+ [AutoNetworkedField]
+ [DataField]
+ public InjectorToggleMode ToggleState = InjectorToggleMode.Draw;
+}
+
+///
+/// Possible modes for an .
+///
+public enum InjectorToggleMode : byte
+{
+ ///
+ /// The injector will try to inject reagent into things.
+ ///
+ Inject,
+
+ ///
+ /// The injector will try to draw reagent from things.
+ ///
+ Draw
+}
diff --git a/Content.Shared/Chemistry/Components/SharedInjectorComponent.cs b/Content.Shared/Chemistry/Components/SharedInjectorComponent.cs
deleted file mode 100644
index a4cea4e7c4..0000000000
--- a/Content.Shared/Chemistry/Components/SharedInjectorComponent.cs
+++ /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
- {
- }
-
- ///
- /// Shared class for injectors & syringes
- ///
- [NetworkedComponent, ComponentProtoName("Injector")]
- public abstract partial class SharedInjectorComponent : Component
- {
- ///
- /// Component data used for net updates. Used by client for item status ui
- ///
- [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
index 0000000000..7ad2170281
--- /dev/null
+++ b/Content.Shared/Chemistry/EntitySystems/SharedInjectorSystem.cs
@@ -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
+{
+ ///
+ /// Default transfer amounts for the set-transfer verb.
+ ///
+ 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>(AddSetTransferVerbs);
+ SubscribeLocalEvent(OnInjectorStartup);
+ SubscribeLocalEvent(OnInjectorUse);
+ }
+
+ private void AddSetTransferVerbs(Entity entity, ref GetVerbsEvent args)
+ {
+ if (!args.CanAccess || !args.CanInteract || args.Hands == null)
+ return;
+
+ if (!HasComp(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 entity, ref ComponentStartup args)
+ {
+ // ???? why ?????
+ Dirty(entity);
+ }
+
+ private void OnInjectorUse(Entity entity, ref UseInHandEvent args)
+ {
+ if (args.Handled)
+ return;
+
+ Toggle(entity, args.User);
+ args.Handled = true;
+ }
+
+ ///
+ /// Toggle between draw/inject state if applicable
+ ///
+ private void Toggle(Entity 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 injector, InjectorToggleMode mode)
+ {
+ injector.Comp.ToggleState = mode;
+ Dirty(injector);
+ }
+}
diff --git a/Resources/Locale/en-US/chemistry/components/injector-component.ftl b/Resources/Locale/en-US/chemistry/components/injector-component.ftl
index e137130290..4fafc9cd3b 100644
--- a/Resources/Locale/en-US/chemistry/components/injector-component.ftl
+++ b/Resources/Locale/en-US/chemistry/components/injector-component.ftl
@@ -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