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