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