From c40ac26cedd5c625459562206ece3d61b8fa3278 Mon Sep 17 00:00:00 2001 From: Pieter-Jan Briers Date: Wed, 3 Feb 2021 14:05:31 +0100 Subject: [PATCH] A big hecking chemistry-related refactor. (#3055) * A big hecking chemistry-related refactor. Changed SolutionContainerCaps. It now describes "stock" behavior for interacting with solutions that is pre-implemented by SolutionContainerComponent. As such things like syringes do not check it anymore (on themselves) to see "can we remove reagent from ourselves". That's assumed by it... being a syringe. SolutionContainerCaps now has different flags more accurately describing possible reagent interaction behaviors. ISolutionInteractionsComponent is the interface that describes the common behaviors like "what happens when injected with a syringe". This is implemented by SolutionContainerComponent but could be implemented by other classes. One notable example that drove me to making this interface was the /vg/station circuit imprinter which splits reagent poured in into its two reservoir beakers. Having this interface allows us to do this "proxying" behavior hack-free. (the hacks in /vg/ code were somewhat dirty...). PourableComponent has been replaced SolutionTransferComponent. It now describes both give-and-take behavior for the common reagent containers. This is in line with /vg/'s /obj/item/weapon/reagent_containers architecture. "Taking" in this context is ONLY from reagent tanks like fuel tanks. Oh, should I mention that fuel tanks and such have a proper component now? They do. Because of this behavioral change, reagent tanks DO NOT have Pourable anymore. Removing from reagent tanks is now in the hands of the item used on them. Welders and fire extinguishers now have code for removing from them. This sounds bad at first but remember that all have quite unique behavior related to this: Welders cause explosions if lit and can ONLY be fueled at fuel tanks. Extinguishers can be filled at any tank, etc... The code for this is also simpler due to ISolutionInteractionsComponent now so... IAfterInteract now works like IInteractUsing with the Priority levels and "return true to block further handlers" behavior. This was necessary to make extinguishers prioritize taking from tanks over spraying. Explicitly coded interactions like welders refueling also means they refuse instantly to full now, which they didn't before. And it plays the sound. Etc... Probably more stuff I'm forgetting. * Review improvements. --- .../ActionBlocking/HandcuffComponent.cs | 15 +- .../Components/Atmos/GasAnalyzerComponent.cs | 6 +- .../Components/Body/MechanismComponent.cs | 6 +- .../Components/Body/Part/BodyPartComponent.cs | 6 +- .../Body/Surgery/SurgeryToolComponent.cs | 12 +- .../Components/Botany/PlantHolderComponent.cs | 20 +- .../Chemistry/HyposprayComponent.cs | 8 +- .../Components/Chemistry/InjectorComponent.cs | 87 +++--- .../Components/Chemistry/PillComponent.cs | 5 +- .../Components/Chemistry/PourableComponent.cs | 113 ------- .../Chemistry/ReagentTankComponent.cs | 35 +++ .../Chemistry/SolutionContainerComponent.cs | 210 +------------ .../Chemistry/SolutionTransferComponent.cs | 144 +++++++++ .../Chemistry/SolutionTransferVerbs.cs | 282 ++++++++++++++++++ .../GameObjects/Components/CrayonComponent.cs | 12 +- .../Components/Culinary/UtensilComponent.cs | 3 +- .../Components/Fluids/MopComponent.cs | 22 +- .../Components/Fluids/SpillableComponent.cs | 24 +- .../Components/Fluids/SprayComponent.cs | 16 +- .../Interactable/TilePryingComponent.cs | 3 +- .../Interactable/WelderComponent.cs | 39 ++- .../Items/FireExtinguisherComponent.cs | 46 ++- .../Items/FloorTileItemComponent.cs | 13 +- .../Components/Items/RCD/RCDAmmoComponent.cs | 7 +- .../Components/Items/RCD/RCDComponent.cs | 8 +- .../Components/Kitchen/MicrowaveComponent.cs | 9 +- .../Components/Medical/HealingComponent.cs | 14 +- .../Components/Nutrition/DrinkComponent.cs | 9 +- .../Components/Nutrition/FoodComponent.cs | 5 +- .../Components/Portal/TeleporterComponent.cs | 6 +- .../Components/Power/WirePlacerComponent.cs | 17 +- .../Ranged/Ammunition/SpeedLoaderComponent.cs | 6 +- .../EntitySystems/Click/InteractionSystem.cs | 27 +- Content.Shared/Chemistry/Solution.cs | 2 +- Content.Shared/Chemistry/SolutionCaps.cs | 46 --- .../Chemistry/SolutionContainerCaps.cs | 62 ++++ .../ISolutionInteractionsComponent.cs | 84 ++++++ .../SharedSolutionContainerComponent.cs | 47 ++- .../Components/Interaction/IAfterInteract.cs | 8 +- Resources/Audio/Effects/refill.ogg | Bin 0 -> 14607 bytes Resources/Maps/saltern.yml | 10 - .../Specific/Cooking/microwave.yml | 2 +- .../Constructible/Specific/hydroponics.yml | 3 +- .../Storage/StorageTanks/base_tank.yml | 6 +- .../Storage/StorageTanks/fuel_tank.yml | 6 +- .../Entities/Mobs/Species/human.yml | 2 +- .../Entities/Objects/Consumable/drinks.yml | 6 +- .../Objects/Consumable/drinks_bottles.yml | 2 +- .../Objects/Consumable/drinks_cans.yml | 4 +- .../Objects/Consumable/drinks_cups.yml | 2 +- .../Consumable/kitchen_reagent_containers.yml | 2 +- .../Objects/Consumable/trash_drinks.yml | 2 +- .../Objects/Misc/fire_extinguisher.yml | 2 +- .../Entities/Objects/Power/powercells.yml | 2 +- .../Entities/Objects/Specific/chemistry.yml | 12 +- .../Entities/Objects/Specific/janitor.yml | 4 +- .../Objects/Specific/rehydrateable.yml | 4 +- .../Entities/Objects/Tools/botany_tools.yml | 10 +- .../Entities/Objects/Tools/cowtools.yml | 4 +- .../Entities/Objects/Tools/welders.yml | 6 +- .../Guns/Ammunition/Shotgun/cartridges.yml | 4 +- .../Guns/Ammunition/Shotgun/projectiles.yml | 2 +- .../Entities/Objects/Weapons/Melee/spear.yml | 4 +- .../Prototypes/Entities/Objects/hypospray.yml | 2 +- SpaceStation14.sln.DotSettings | 1 + 65 files changed, 987 insertions(+), 601 deletions(-) delete mode 100644 Content.Server/GameObjects/Components/Chemistry/PourableComponent.cs create mode 100644 Content.Server/GameObjects/Components/Chemistry/ReagentTankComponent.cs create mode 100644 Content.Server/GameObjects/Components/Chemistry/SolutionTransferComponent.cs create mode 100644 Content.Server/GameObjects/Components/Chemistry/SolutionTransferVerbs.cs delete mode 100644 Content.Shared/Chemistry/SolutionCaps.cs create mode 100644 Content.Shared/Chemistry/SolutionContainerCaps.cs create mode 100644 Content.Shared/GameObjects/Components/Chemistry/ISolutionInteractionsComponent.cs create mode 100644 Resources/Audio/Effects/refill.ogg diff --git a/Content.Server/GameObjects/Components/ActionBlocking/HandcuffComponent.cs b/Content.Server/GameObjects/Components/ActionBlocking/HandcuffComponent.cs index 18f4892d11..9e05ab7cd1 100644 --- a/Content.Server/GameObjects/Components/ActionBlocking/HandcuffComponent.cs +++ b/Content.Server/GameObjects/Components/ActionBlocking/HandcuffComponent.cs @@ -148,41 +148,41 @@ namespace Content.Server.GameObjects.Components.ActionBlocking return new HandcuffedComponentState(Broken ? BrokenState : string.Empty); } - async Task IAfterInteract.AfterInteract(AfterInteractEventArgs eventArgs) + async Task IAfterInteract.AfterInteract(AfterInteractEventArgs eventArgs) { if (eventArgs.Target == null || !ActionBlockerSystem.CanUse(eventArgs.User) || !eventArgs.Target.TryGetComponent(out var cuffed)) { - return; + return false; } if (eventArgs.Target == eventArgs.User) { eventArgs.User.PopupMessage(Loc.GetString("You can't cuff yourself!")); - return; + return true; } if (Broken) { eventArgs.User.PopupMessage(Loc.GetString("The cuffs are broken!")); - return; + return true; } if (!eventArgs.Target.TryGetComponent(out var hands)) { eventArgs.User.PopupMessage(Loc.GetString("{0:theName} has no hands!", eventArgs.Target)); - return; + return true; } if (cuffed.CuffedHandCount == hands.Count) { eventArgs.User.PopupMessage(Loc.GetString("{0:theName} has no free hands to handcuff!", eventArgs.Target)); - return; + return true; } if (!eventArgs.InRangeUnobstructed(_interactRange, ignoreInsideBlocker: true)) { eventArgs.User.PopupMessage(Loc.GetString("You are too far away to use the cuffs!")); - return; + return true; } eventArgs.User.PopupMessage(Loc.GetString("You start cuffing {0:theName}.", eventArgs.Target)); @@ -190,6 +190,7 @@ namespace Content.Server.GameObjects.Components.ActionBlocking _audioSystem.PlayFromEntity(StartCuffSound, Owner); TryUpdateCuff(eventArgs.User, eventArgs.Target, cuffed); + return true; } /// diff --git a/Content.Server/GameObjects/Components/Atmos/GasAnalyzerComponent.cs b/Content.Server/GameObjects/Components/Atmos/GasAnalyzerComponent.cs index a63a27d274..586a332bdb 100644 --- a/Content.Server/GameObjects/Components/Atmos/GasAnalyzerComponent.cs +++ b/Content.Server/GameObjects/Components/Atmos/GasAnalyzerComponent.cs @@ -253,18 +253,20 @@ namespace Content.Server.GameObjects.Components.Atmos } } - async Task IAfterInteract.AfterInteract(AfterInteractEventArgs eventArgs) + async Task IAfterInteract.AfterInteract(AfterInteractEventArgs eventArgs) { if (!eventArgs.CanReach) { eventArgs.User.PopupMessage(Loc.GetString("You can't reach there!")); - return; + return true; } if (eventArgs.User.TryGetComponent(out IActorComponent? actor)) { OpenInterface(actor.playerSession, eventArgs.ClickLocation); } + + return true; } diff --git a/Content.Server/GameObjects/Components/Body/MechanismComponent.cs b/Content.Server/GameObjects/Components/Body/MechanismComponent.cs index 1ff3f77f52..8dd4658573 100644 --- a/Content.Server/GameObjects/Components/Body/MechanismComponent.cs +++ b/Content.Server/GameObjects/Components/Body/MechanismComponent.cs @@ -36,11 +36,11 @@ namespace Content.Server.GameObjects.Components.Body } } - async Task IAfterInteract.AfterInteract(AfterInteractEventArgs eventArgs) + async Task IAfterInteract.AfterInteract(AfterInteractEventArgs eventArgs) { if (eventArgs.Target == null) { - return; + return false; } CloseAllSurgeryUIs(); @@ -61,6 +61,8 @@ namespace Content.Server.GameObjects.Components.Body eventArgs.Target.PopupMessage(eventArgs.User, Loc.GetString("You can't fit it in!")); } } + + return true; } private void SendBodyPartListToUser(AfterInteractEventArgs eventArgs, IBody body) diff --git a/Content.Server/GameObjects/Components/Body/Part/BodyPartComponent.cs b/Content.Server/GameObjects/Components/Body/Part/BodyPartComponent.cs index 44c7cc5a7e..6a1c2e5220 100644 --- a/Content.Server/GameObjects/Components/Body/Part/BodyPartComponent.cs +++ b/Content.Server/GameObjects/Components/Body/Part/BodyPartComponent.cs @@ -99,12 +99,12 @@ namespace Content.Server.GameObjects.Components.Body.Part } } - public async Task AfterInteract(AfterInteractEventArgs eventArgs) + public async Task AfterInteract(AfterInteractEventArgs eventArgs) { // TODO BODY if (eventArgs.Target == null) { - return; + return false; } CloseAllSurgeryUIs(); @@ -116,6 +116,8 @@ namespace Content.Server.GameObjects.Components.Body.Part { SendSlots(eventArgs, body); } + + return true; } private void SendSlots(AfterInteractEventArgs eventArgs, IBody body) diff --git a/Content.Server/GameObjects/Components/Body/Surgery/SurgeryToolComponent.cs b/Content.Server/GameObjects/Components/Body/Surgery/SurgeryToolComponent.cs index ac1aaa3ad7..717bbccd99 100644 --- a/Content.Server/GameObjects/Components/Body/Surgery/SurgeryToolComponent.cs +++ b/Content.Server/GameObjects/Components/Body/Surgery/SurgeryToolComponent.cs @@ -50,16 +50,16 @@ namespace Content.Server.GameObjects.Components.Body.Surgery public IEntity? PerformerCache { get; private set; } - async Task IAfterInteract.AfterInteract(AfterInteractEventArgs eventArgs) + async Task IAfterInteract.AfterInteract(AfterInteractEventArgs eventArgs) { if (eventArgs.Target == null) { - return; + return false; } if (!eventArgs.User.TryGetComponent(out IActorComponent? actor)) { - return; + return false; } CloseAllSurgeryUIs(); @@ -101,20 +101,22 @@ namespace Content.Server.GameObjects.Components.Body.Surgery if (!part.SurgeryCheck(_surgeryType)) { NotUsefulPopup(); - return; + return true; } // ...do the surgery. if (part.AttemptSurgery(_surgeryType, part, this, eventArgs.User)) { - return; + return true; } // Log error if the surgery fails somehow. Logger.Debug($"Error when trying to perform surgery on ${nameof(IBodyPart)} {eventArgs.User.Name}"); throw new InvalidOperationException(); } + + return true; } public float BaseOperationTime { get => _baseOperateTime; set => _baseOperateTime = value; } diff --git a/Content.Server/GameObjects/Components/Botany/PlantHolderComponent.cs b/Content.Server/GameObjects/Components/Botany/PlantHolderComponent.cs index ec9fa8b951..64712083ab 100644 --- a/Content.Server/GameObjects/Components/Botany/PlantHolderComponent.cs +++ b/Content.Server/GameObjects/Components/Botany/PlantHolderComponent.cs @@ -12,6 +12,7 @@ using Content.Server.Utility; using Content.Shared.Audio; using Content.Shared.Chemistry; using Content.Shared.GameObjects.Components.Botany; +using Content.Shared.GameObjects.Components.Chemistry; using Content.Shared.GameObjects.EntitySystems; using Content.Shared.GameObjects.EntitySystems.ActionBlocker; using Content.Shared.Interfaces; @@ -718,23 +719,28 @@ namespace Content.Server.GameObjects.Components.Botany return true; } - if (usingItem.TryGetComponent(out SolutionContainerComponent? solution) && solution.CanRemoveSolutions) + if (usingItem.TryGetComponent(out ISolutionInteractionsComponent? solution) && solution.CanDrain) { - var amount = 5f; + var amount = ReagentUnit.New(5); var sprayed = false; if (usingItem.TryGetComponent(out SprayComponent? spray)) { sprayed = true; - amount = 1f; + amount = ReagentUnit.New(1); EntitySystem.Get().PlayFromEntity(spray.SpraySound, usingItem, AudioHelpers.WithVariation(0.125f)); } - var chemAmount = ReagentUnit.New(amount); + var split = solution.Drain(amount); + if (split.TotalVolume == 0) + { + user.PopupMessageCursor(Loc.GetString("{0:TheName} is empty!", usingItem)); + return true; + } - var split = solution.Solution.SplitSolution(chemAmount <= solution.Solution.TotalVolume ? chemAmount : solution.Solution.TotalVolume); - - user.PopupMessageCursor(Loc.GetString(sprayed ? $"You spray {Owner.Name} with {usingItem.Name}." : $"You transfer {split.TotalVolume.ToString()}u to {Owner.Name}")); + user.PopupMessageCursor(Loc.GetString( + sprayed ? "You spray {0:TheName}" : "You transfer {1}u to {0:TheName}", + Owner, split.TotalVolume)); _solutionContainer?.TryAddSolution(split); diff --git a/Content.Server/GameObjects/Components/Chemistry/HyposprayComponent.cs b/Content.Server/GameObjects/Components/Chemistry/HyposprayComponent.cs index 8b88d21d4b..7783b38f72 100644 --- a/Content.Server/GameObjects/Components/Chemistry/HyposprayComponent.cs +++ b/Content.Server/GameObjects/Components/Chemistry/HyposprayComponent.cs @@ -52,10 +52,12 @@ namespace Content.Server.GameObjects.Components.Chemistry return TryDoInject(target, user); } - Task IAfterInteract.AfterInteract(AfterInteractEventArgs eventArgs) + async Task IAfterInteract.AfterInteract(AfterInteractEventArgs eventArgs) { - TryDoInject(eventArgs.Target, eventArgs.User); - return Task.CompletedTask; + if (!eventArgs.CanReach) + return false; + + return TryDoInject(eventArgs.Target, eventArgs.User); } private bool TryDoInject(IEntity? target, IEntity user) diff --git a/Content.Server/GameObjects/Components/Chemistry/InjectorComponent.cs b/Content.Server/GameObjects/Components/Chemistry/InjectorComponent.cs index 275fdc9342..fa23f8c0de 100644 --- a/Content.Server/GameObjects/Components/Chemistry/InjectorComponent.cs +++ b/Content.Server/GameObjects/Components/Chemistry/InjectorComponent.cs @@ -28,21 +28,18 @@ namespace Content.Server.GameObjects.Components.Chemistry /// Whether or not the injector is able to draw from containers or if it's a single use /// device that can only inject. /// - [ViewVariables] - private bool _injectOnly; + [ViewVariables] private bool _injectOnly; /// /// 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] - private ReagentUnit _transferAmount; + [ViewVariables] private ReagentUnit _transferAmount; /// /// Initial storage volume of the injector /// - [ViewVariables] - private ReagentUnit _initialMaxVolume; + [ViewVariables] private ReagentUnit _initialMaxVolume; private InjectorToggleMode _toggleState; @@ -68,19 +65,14 @@ namespace Content.Server.GameObjects.Components.Chemistry serializer.DataField(ref _injectOnly, "injectOnly", false); serializer.DataField(ref _initialMaxVolume, "initialMaxVolume", ReagentUnit.New(15)); serializer.DataField(ref _transferAmount, "transferAmount", ReagentUnit.New(5)); - serializer.DataField(ref _toggleState, "toggleState", _injectOnly ? InjectorToggleMode.Inject : InjectorToggleMode.Draw); + serializer.DataField(ref _toggleState, "toggleState", + _injectOnly ? InjectorToggleMode.Inject : InjectorToggleMode.Draw); } protected override void Startup() { base.Startup(); - var solution = Owner.EnsureComponent(); - solution.Capabilities = SolutionContainerCaps.AddTo | SolutionContainerCaps.RemoveFrom; - - // Set _toggleState based on prototype - _toggleState = _injectOnly ? InjectorToggleMode.Inject : InjectorToggleMode.Draw; - Dirty(); } @@ -116,58 +108,55 @@ namespace Content.Server.GameObjects.Components.Chemistry /// Called when clicking on entities while holding in active hand /// /// - async Task IAfterInteract.AfterInteract(AfterInteractEventArgs eventArgs) + async Task IAfterInteract.AfterInteract(AfterInteractEventArgs eventArgs) { - if (!eventArgs.InRangeUnobstructed(ignoreInsideBlocker: true, popup: true)) return; + if (!eventArgs.InRangeUnobstructed(ignoreInsideBlocker: true, popup: true)) + return false; //Make sure we have the attacking entity - if (eventArgs.Target == null || !Owner.TryGetComponent(out SolutionContainerComponent? solution)) + if (eventArgs.Target == null || !Owner.HasComponent()) { - return; + return false; } var targetEntity = eventArgs.Target; // Handle injecting/drawing for solutions - if (targetEntity.TryGetComponent(out var targetSolution)) + if (targetEntity.TryGetComponent(out var targetSolution)) { if (ToggleState == InjectorToggleMode.Inject) { - if (solution.CanRemoveSolutions && targetSolution.CanAddSolutions) + if (targetSolution.CanInject) { TryInject(targetSolution, eventArgs.User); } else { - eventArgs.User.PopupMessage(eventArgs.User, Loc.GetString("You aren't able to transfer to {0:theName}!", targetSolution.Owner)); + eventArgs.User.PopupMessage(eventArgs.User, + Loc.GetString("You aren't able to transfer to {0:theName}!", targetSolution.Owner)); } } else if (ToggleState == InjectorToggleMode.Draw) { - if (targetSolution.CanRemoveSolutions && solution.CanAddSolutions) + if (targetSolution.CanDraw) { TryDraw(targetSolution, eventArgs.User); } else { - eventArgs.User.PopupMessage(eventArgs.User, Loc.GetString("You aren't able to draw from {0:theName}!", targetSolution.Owner)); + eventArgs.User.PopupMessage(eventArgs.User, + Loc.GetString("You aren't able to draw from {0:theName}!", targetSolution.Owner)); } } } - else // Handle injecting into bloodstream + // Handle injecting into bloodstream + else if (targetEntity.TryGetComponent(out BloodstreamComponent? bloodstream) && + ToggleState == InjectorToggleMode.Inject) { - if (targetEntity.TryGetComponent(out BloodstreamComponent? bloodstream) && ToggleState == InjectorToggleMode.Inject) - { - if (solution.CanRemoveSolutions) - { - TryInjectIntoBloodstream(bloodstream, eventArgs.User); - } - else - { - eventArgs.User.PopupMessage(eventArgs.User, Loc.GetString("You aren't able to inject {0:theName}!", targetEntity)); - } - } + TryInjectIntoBloodstream(bloodstream, eventArgs.User); } + + return true; } /// @@ -193,7 +182,8 @@ namespace Content.Server.GameObjects.Components.Chemistry if (realTransferAmount <= 0) { - Owner.PopupMessage(user, Loc.GetString("You aren't able to inject {0:theName}!", targetBloodstream.Owner)); + Owner.PopupMessage(user, + Loc.GetString("You aren't able to inject {0:theName}!", targetBloodstream.Owner)); return; } @@ -213,12 +203,14 @@ namespace Content.Server.GameObjects.Components.Chemistry removedSolution.DoEntityReaction(targetBloodstream.Owner, ReactionMethod.Injection); - Owner.PopupMessage(user, Loc.GetString("You inject {0}u into {1:theName}!", removedSolution.TotalVolume, targetBloodstream.Owner)); + Owner.PopupMessage(user, + Loc.GetString("You inject {0}u into {1:theName}!", removedSolution.TotalVolume, + targetBloodstream.Owner)); Dirty(); AfterInject(); } - private void TryInject(SolutionContainerComponent targetSolution, IEntity user) + private void TryInject(ISolutionInteractionsComponent targetSolution, IEntity user) { if (!Owner.TryGetComponent(out SolutionContainerComponent? solution) || solution.CurrentVolume == 0) { @@ -226,7 +218,7 @@ namespace Content.Server.GameObjects.Components.Chemistry } // Get transfer amount. May be smaller than _transferAmount if not enough room - var realTransferAmount = ReagentUnit.Min(_transferAmount, targetSolution.EmptyVolume); + var realTransferAmount = ReagentUnit.Min(_transferAmount, targetSolution.InjectSpaceAvailable); if (realTransferAmount <= 0) { @@ -237,16 +229,12 @@ namespace Content.Server.GameObjects.Components.Chemistry // Move units from attackSolution to targetSolution var removedSolution = solution.SplitSolution(realTransferAmount); - if (!targetSolution.CanAddSolution(removedSolution)) - { - return; - } - removedSolution.DoEntityReaction(targetSolution.Owner, ReactionMethod.Injection); - targetSolution.TryAddSolution(removedSolution); + targetSolution.Inject(removedSolution); - Owner.PopupMessage(user, Loc.GetString("You transfer {0}u to {1:theName}", removedSolution.TotalVolume, targetSolution.Owner)); + Owner.PopupMessage(user, + Loc.GetString("You transfer {0}u to {1:theName}", removedSolution.TotalVolume, targetSolution.Owner)); Dirty(); AfterInject(); } @@ -260,7 +248,7 @@ namespace Content.Server.GameObjects.Components.Chemistry } } - private void TryDraw(SolutionContainerComponent targetSolution, IEntity user) + private void TryDraw(ISolutionInteractionsComponent targetSolution, IEntity user) { if (!Owner.TryGetComponent(out SolutionContainerComponent? solution) || solution.EmptyVolume == 0) { @@ -268,7 +256,7 @@ namespace Content.Server.GameObjects.Components.Chemistry } // Get transfer amount. May be smaller than _transferAmount if not enough room - var realTransferAmount = ReagentUnit.Min(_transferAmount, targetSolution.CurrentVolume); + var realTransferAmount = ReagentUnit.Min(_transferAmount, targetSolution.DrawAvailable); if (realTransferAmount <= 0) { @@ -277,14 +265,15 @@ namespace Content.Server.GameObjects.Components.Chemistry } // Move units from attackSolution to targetSolution - var removedSolution = targetSolution.SplitSolution(realTransferAmount); + var removedSolution = targetSolution.Draw(realTransferAmount); if (!solution.TryAddSolution(removedSolution)) { return; } - Owner.PopupMessage(user, Loc.GetString("Drew {0}u from {1:theName}", removedSolution.TotalVolume, targetSolution.Owner)); + Owner.PopupMessage(user, + Loc.GetString("Drew {0}u from {1:theName}", removedSolution.TotalVolume, targetSolution.Owner)); Dirty(); AfterDraw(); } diff --git a/Content.Server/GameObjects/Components/Chemistry/PillComponent.cs b/Content.Server/GameObjects/Components/Chemistry/PillComponent.cs index d96e27cc60..f333605dd1 100644 --- a/Content.Server/GameObjects/Components/Chemistry/PillComponent.cs +++ b/Content.Server/GameObjects/Components/Chemistry/PillComponent.cs @@ -59,14 +59,15 @@ namespace Content.Server.GameObjects.Components.Chemistry } // Feeding someone else - public async Task AfterInteract(AfterInteractEventArgs eventArgs) + public async Task AfterInteract(AfterInteractEventArgs eventArgs) { if (eventArgs.Target == null) { - return; + return false; } TryUseFood(eventArgs.User, eventArgs.Target); + return true; } public override bool TryUseFood(IEntity user, IEntity target, UtensilComponent utensilUsed = null) diff --git a/Content.Server/GameObjects/Components/Chemistry/PourableComponent.cs b/Content.Server/GameObjects/Components/Chemistry/PourableComponent.cs deleted file mode 100644 index ac747c962a..0000000000 --- a/Content.Server/GameObjects/Components/Chemistry/PourableComponent.cs +++ /dev/null @@ -1,113 +0,0 @@ -using System.Threading.Tasks; -using Content.Shared.Chemistry; -using Content.Shared.Interfaces; -using Content.Shared.Interfaces.GameObjects.Components; -using Robust.Shared.GameObjects; -using Robust.Shared.Localization; -using Robust.Shared.Serialization; -using Robust.Shared.ViewVariables; - -namespace Content.Server.GameObjects.Components.Chemistry -{ - /// - /// Gives an entity click behavior for pouring reagents into - /// other entities and being poured into. The entity must have - /// a SolutionComponent or DrinkComponent for this to work. - /// (DrinkComponent adds a SolutionComponent if one isn't present). - /// - [RegisterComponent] - class PourableComponent : Component, IInteractUsing - { - public override string Name => "Pourable"; - - private ReagentUnit _transferAmount; - - /// - /// The amount of solution to be transferred from this solution when clicking on other solutions with it. - /// - [ViewVariables(VVAccess.ReadWrite)] - public ReagentUnit TransferAmount - { - get => _transferAmount; - set => _transferAmount = value; - } - - public override void ExposeData(ObjectSerializer serializer) - { - base.ExposeData(serializer); - serializer.DataField(ref _transferAmount, "transferAmount", ReagentUnit.New(5.0)); - } - - /// - /// Called when the owner of this component is clicked on with another entity. - /// The owner of this component is the target. - /// The entity used to click on this one is the attacker. - /// - /// Attack event args - /// - async Task IInteractUsing.InteractUsing(InteractUsingEventArgs eventArgs) - { - //Get target solution component - if (!Owner.TryGetComponent(out var targetSolution)) - return false; - - //Get attack solution component - var attackEntity = eventArgs.Using; - if (!attackEntity.TryGetComponent(out var attackSolution)) - return false; - - // Calculate possibe solution transfer - if (targetSolution.CanAddSolutions && attackSolution.CanRemoveSolutions) - { - // default logic (beakers and glasses) - // transfer solution from object in hand to attacked - return TryTransfer(eventArgs, attackSolution, targetSolution); - } - else if (targetSolution.CanRemoveSolutions && attackSolution.CanAddSolutions) - { - // storage tanks and sinks logic - // drain solution from attacked object to object in hand - return TryTransfer(eventArgs, targetSolution, attackSolution); - } - - // No transfer possible - return false; - } - - bool TryTransfer(InteractUsingEventArgs eventArgs, SolutionContainerComponent fromSolution, SolutionContainerComponent toSolution) - { - var fromEntity = fromSolution.Owner; - - if (!fromEntity.TryGetComponent(out var fromPourable)) - { - return false; - } - - //Get transfer amount. May be smaller than _transferAmount if not enough room - var realTransferAmount = ReagentUnit.Min(fromPourable.TransferAmount, toSolution.EmptyVolume); - - if (realTransferAmount <= 0) // Special message if container is full - { - Owner.PopupMessage(eventArgs.User, Loc.GetString("{0:theName} is full!", toSolution.Owner)); - return false; - } - - //Move units from attackSolution to targetSolution - var removedSolution = fromSolution.SplitSolution(realTransferAmount); - - if (removedSolution.TotalVolume <= ReagentUnit.Zero) - { - return false; - } - - if (!toSolution.TryAddSolution(removedSolution)) - { - return false; - } - - Owner.PopupMessage(eventArgs.User, Loc.GetString("You transfer {0}u to {1:theName}.", removedSolution.TotalVolume, toSolution.Owner)); - - return true; - } - } -} diff --git a/Content.Server/GameObjects/Components/Chemistry/ReagentTankComponent.cs b/Content.Server/GameObjects/Components/Chemistry/ReagentTankComponent.cs new file mode 100644 index 0000000000..0f654380fb --- /dev/null +++ b/Content.Server/GameObjects/Components/Chemistry/ReagentTankComponent.cs @@ -0,0 +1,35 @@ +using Content.Shared.Chemistry; +using Robust.Shared.GameObjects; +using Robust.Shared.Serialization; +using Robust.Shared.ViewVariables; + +#nullable enable + +namespace Content.Server.GameObjects.Components.Chemistry +{ + [RegisterComponent] + public class ReagentTankComponent : Component + { + public override string Name => "ReagentTank"; + + [ViewVariables(VVAccess.ReadWrite)] + public ReagentUnit TransferAmount { get; set; } + + [ViewVariables(VVAccess.ReadWrite)] + public ReagentTankType TankType { get; set; } + + public override void ExposeData(ObjectSerializer serializer) + { + base.ExposeData(serializer); + + serializer.DataField(this, c => c.TransferAmount, "transferAmount", ReagentUnit.New(10)); + serializer.DataField(this, c => c.TankType, "tankType", ReagentTankType.Unspecified); + } + } + + public enum ReagentTankType : byte + { + Unspecified, + Fuel + } +} diff --git a/Content.Server/GameObjects/Components/Chemistry/SolutionContainerComponent.cs b/Content.Server/GameObjects/Components/Chemistry/SolutionContainerComponent.cs index d60b1dc0fb..36ff877874 100644 --- a/Content.Server/GameObjects/Components/Chemistry/SolutionContainerComponent.cs +++ b/Content.Server/GameObjects/Components/Chemistry/SolutionContainerComponent.cs @@ -1,4 +1,4 @@ -#nullable enable +#nullable enable using Content.Server.Administration; using Content.Server.Eui; using Content.Server.GameObjects.Components.GUI; @@ -6,6 +6,7 @@ using Content.Shared.Administration; using Content.Shared.Chemistry; using Content.Shared.Eui; using Content.Shared.GameObjects.Components.Chemistry; +using Content.Shared.GameObjects.EntitySystems; using Content.Shared.GameObjects.EntitySystems.ActionBlocker; using Content.Shared.GameObjects.Verbs; using Robust.Server.Interfaces.GameObjects; @@ -19,213 +20,8 @@ namespace Content.Server.GameObjects.Components.Chemistry { [RegisterComponent] [ComponentReference(typeof(SharedSolutionContainerComponent))] + [ComponentReference(typeof(ISolutionInteractionsComponent))] public class SolutionContainerComponent : SharedSolutionContainerComponent { - /// - /// Transfers solution from the held container to the target container. - /// - [Verb] - private sealed class FillTargetVerb : Verb - { - protected override void GetData(IEntity user, SolutionContainerComponent component, VerbData data) - { - if (!ActionBlockerSystem.CanInteract(user) || - !user.TryGetComponent(out var hands) || - hands.GetActiveHand == null || - hands.GetActiveHand.Owner == component.Owner || - !hands.GetActiveHand.Owner.TryGetComponent(out var solution) || - !solution.CanRemoveSolutions || - !component.CanAddSolutions) - { - data.Visibility = VerbVisibility.Invisible; - return; - } - - var heldEntityName = hands.GetActiveHand.Owner?.Prototype?.Name ?? ""; - var myName = component.Owner.Prototype?.Name ?? ""; - - var locHeldEntityName = Loc.GetString(heldEntityName); - var locMyName = Loc.GetString(myName); - - data.Visibility = VerbVisibility.Visible; - data.Text = Loc.GetString("Transfer liquid from [{0}] to [{1}].", locHeldEntityName, locMyName); - } - - protected override void Activate(IEntity user, SolutionContainerComponent component) - { - if (!user.TryGetComponent(out var hands) || hands.GetActiveHand == null) - { - return; - } - - if (!hands.GetActiveHand.Owner.TryGetComponent(out var handSolutionComp) || - !handSolutionComp.CanRemoveSolutions || - !component.CanAddSolutions) - { - return; - } - - var transferQuantity = ReagentUnit.Min(component.MaxVolume - component.CurrentVolume, - handSolutionComp.CurrentVolume, ReagentUnit.New(10)); - - if (transferQuantity <= 0) - { - return; - } - - var transferSolution = handSolutionComp.SplitSolution(transferQuantity); - component.TryAddSolution(transferSolution); - } - } - - /// - /// Transfers solution from a target container to the held container. - /// - [Verb] - private sealed class EmptyTargetVerb : Verb - { - protected override void GetData(IEntity user, SolutionContainerComponent component, VerbData data) - { - if (!ActionBlockerSystem.CanInteract(user) || - !user.TryGetComponent(out var hands) || - hands.GetActiveHand == null || - hands.GetActiveHand.Owner == component.Owner || - !hands.GetActiveHand.Owner.TryGetComponent(out var solution) || - !solution.CanAddSolutions || - !component.CanRemoveSolutions) - { - data.Visibility = VerbVisibility.Invisible; - return; - } - - var heldEntityName = hands.GetActiveHand.Owner?.Prototype?.Name ?? ""; - var myName = component.Owner.Prototype?.Name ?? ""; - - var locHeldEntityName = Loc.GetString(heldEntityName); - var locMyName = Loc.GetString(myName); - - data.Visibility = VerbVisibility.Visible; - data.Text = Loc.GetString("Transfer liquid from [{0}] to [{1}].", locMyName, locHeldEntityName); - return; - } - - protected override void Activate(IEntity user, SolutionContainerComponent component) - { - if (!user.TryGetComponent(out var hands) || hands.GetActiveHand == null) - { - return; - } - - if (!hands.GetActiveHand.Owner.TryGetComponent(out var handSolutionComp) || - !handSolutionComp.CanAddSolutions || - !component.CanRemoveSolutions) - { - return; - } - - var transferQuantity = ReagentUnit.Min(handSolutionComp.MaxVolume - handSolutionComp.CurrentVolume, - component.CurrentVolume, ReagentUnit.New(10)); - - if (transferQuantity <= 0) - { - return; - } - - var transferSolution = component.SplitSolution(transferQuantity); - handSolutionComp.TryAddSolution(transferSolution); - } - } - - [Verb] - private sealed class AdminAddReagentVerb : Verb - { - private const AdminFlags ReqFlags = AdminFlags.Fun; - - protected override void GetData(IEntity user, SolutionContainerComponent component, VerbData data) - { - data.Text = Loc.GetString("Add Reagent..."); - data.CategoryData = VerbCategories.Debug; - data.Visibility = VerbVisibility.Invisible; - - var adminManager = IoCManager.Resolve(); - - if (user.TryGetComponent(out var player)) - { - if (adminManager.HasAdminFlag(player.playerSession, ReqFlags)) - { - data.Visibility = VerbVisibility.Visible; - } - } - } - - protected override void Activate(IEntity user, SolutionContainerComponent component) - { - var groupController = IoCManager.Resolve(); - if (user.TryGetComponent(out var player)) - { - if (groupController.HasAdminFlag(player.playerSession, ReqFlags)) - OpenAddReagentMenu(player.playerSession, component); - } - } - - private static void OpenAddReagentMenu(IPlayerSession player, SolutionContainerComponent comp) - { - var euiMgr = IoCManager.Resolve(); - euiMgr.OpenEui(new AdminAddReagentEui(comp), player); - } - - private sealed class AdminAddReagentEui : BaseEui - { - private readonly SolutionContainerComponent _target; - [Dependency] private readonly IAdminManager _adminManager = default!; - - public AdminAddReagentEui(SolutionContainerComponent target) - { - _target = target; - - IoCManager.InjectDependencies(this); - } - - public override void Opened() - { - StateDirty(); - } - - public override EuiStateBase GetNewState() - { - return new AdminAddReagentEuiState - { - CurVolume = _target.CurrentVolume, - MaxVolume = _target.MaxVolume - }; - } - - public override void HandleMessage(EuiMessageBase msg) - { - switch (msg) - { - case AdminAddReagentEuiMsg.Close: - Close(); - break; - case AdminAddReagentEuiMsg.DoAdd doAdd: - // Double check that user wasn't de-adminned in the mean time... - // Or the target was deleted. - if (!_adminManager.HasAdminFlag(Player, ReqFlags) || _target.Deleted) - { - Close(); - return; - } - - _target.TryAddReagent(doAdd.ReagentId, doAdd.Amount, out _); - StateDirty(); - - if (doAdd.CloseAfter) - Close(); - - break; - } - } - } - } } } diff --git a/Content.Server/GameObjects/Components/Chemistry/SolutionTransferComponent.cs b/Content.Server/GameObjects/Components/Chemistry/SolutionTransferComponent.cs new file mode 100644 index 0000000000..b0f43572eb --- /dev/null +++ b/Content.Server/GameObjects/Components/Chemistry/SolutionTransferComponent.cs @@ -0,0 +1,144 @@ +#nullable enable +using System.Threading.Tasks; +using Content.Shared.Chemistry; +using Content.Shared.GameObjects.Components.Chemistry; +using Content.Shared.Interfaces; +using Content.Shared.Interfaces.GameObjects.Components; +using Robust.Shared.GameObjects; +using Robust.Shared.Interfaces.GameObjects; +using Robust.Shared.Localization; +using Robust.Shared.Serialization; +using Robust.Shared.ViewVariables; + +namespace Content.Server.GameObjects.Components.Chemistry +{ + /// + /// Gives click behavior for transferring to/from other reagent containers. + /// + [RegisterComponent] + public sealed class SolutionTransferComponent : Component, IAfterInteract + { + // Behavior is as such: + // If it's a reagent tank, TAKE reagent. + // If it's anything else, GIVE reagent. + // Of course, only if possible. + + public override string Name => "SolutionTransfer"; + + private ReagentUnit _transferAmount; + private bool _canReceive; + private bool _canSend; + + /// + /// The amount of solution to be transferred from this solution when clicking on other solutions with it. + /// + [ViewVariables(VVAccess.ReadWrite)] + public ReagentUnit TransferAmount + { + get => _transferAmount; + set => _transferAmount = value; + } + + /// + /// Can this entity take reagent from reagent tanks? + /// + [ViewVariables(VVAccess.ReadWrite)] + public bool CanReceive + { + get => _canReceive; + set => _canReceive = value; + } + + /// + /// Can this entity give reagent to other reagent containers? + /// + [ViewVariables(VVAccess.ReadWrite)] + public bool CanSend + { + get => _canSend; + set => _canSend = value; + } + + public override void ExposeData(ObjectSerializer serializer) + { + base.ExposeData(serializer); + serializer.DataField(ref _transferAmount, "transferAmount", ReagentUnit.New(5)); + serializer.DataField(ref _canReceive, "canReceive", true); + serializer.DataField(ref _canSend, "canSend", true); + } + + async Task IAfterInteract.AfterInteract(AfterInteractEventArgs eventArgs) + { + if (!eventArgs.CanReach || eventArgs.Target == null) + return false; + + if (!Owner.TryGetComponent(out ISolutionInteractionsComponent? ownerSolution)) + return false; + + var target = eventArgs.Target; + if (!target.TryGetComponent(out ISolutionInteractionsComponent? targetSolution)) + { + return false; + } + + if (CanReceive && target.TryGetComponent(out ReagentTankComponent? tank) + && ownerSolution.CanRefill && targetSolution.CanDrain) + { + var transferred = DoTransfer(targetSolution, ownerSolution, tank.TransferAmount, eventArgs.User); + if (transferred > 0) + { + var toTheBrim = ownerSolution.RefillSpaceAvailable == 0; + var msg = toTheBrim + ? "You fill {0:TheName} to the brim with {1}u from {2:theName}" + : "You fill {0:TheName} with {1}u from {2:theName}"; + + target.PopupMessage(eventArgs.User, Loc.GetString(msg, Owner, transferred, target)); + return true; + } + } + + if (CanSend && targetSolution.CanRefill && ownerSolution.CanDrain) + { + var transferred = DoTransfer(ownerSolution, targetSolution, TransferAmount, eventArgs.User); + + if (transferred > 0) + { + Owner.PopupMessage(eventArgs.User, Loc.GetString("You transfer {0}u to {1:theName}.", + transferred, target)); + + return true; + } + } + + return true; + } + + /// The actual amount transferred. + private static ReagentUnit DoTransfer( + ISolutionInteractionsComponent source, + ISolutionInteractionsComponent target, + ReagentUnit amount, + IEntity user) + { + if (source.DrainAvailable == 0) + { + source.Owner.PopupMessage(user, Loc.GetString("{0:TheName} is empty!", source.Owner)); + return ReagentUnit.Zero; + } + + if (target.RefillSpaceAvailable == 0) + { + target.Owner.PopupMessage(user, Loc.GetString("{0:TheName} is full!", target.Owner)); + return ReagentUnit.Zero; + } + + var actualAmount = + ReagentUnit.Min(amount, ReagentUnit.Min(source.DrainAvailable, target.RefillSpaceAvailable)); + + var solution = source.Drain(actualAmount); + target.Refill(solution); + + return actualAmount; + } + } +} diff --git a/Content.Server/GameObjects/Components/Chemistry/SolutionTransferVerbs.cs b/Content.Server/GameObjects/Components/Chemistry/SolutionTransferVerbs.cs new file mode 100644 index 0000000000..8715ed027a --- /dev/null +++ b/Content.Server/GameObjects/Components/Chemistry/SolutionTransferVerbs.cs @@ -0,0 +1,282 @@ +using System.Diagnostics.CodeAnalysis; +using Content.Server.Administration; +using Content.Server.Eui; +using Content.Server.GameObjects.Components.GUI; +using Content.Shared.Administration; +using Content.Shared.Chemistry; +using Content.Shared.Eui; +using Content.Shared.GameObjects.Components.Chemistry; +using Content.Shared.GameObjects.EntitySystems.ActionBlocker; +using Content.Shared.GameObjects.Verbs; +using Robust.Server.Interfaces.GameObjects; +using Robust.Server.Interfaces.Player; +using Robust.Shared.Interfaces.GameObjects; +using Robust.Shared.IoC; +using Robust.Shared.Localization; + +#nullable enable + +namespace Content.Server.GameObjects.Components.Chemistry +{ + internal abstract class SolutionTransferVerbBase : GlobalVerb + { + protected static bool GetHeldSolution( + IEntity holder, + [NotNullWhen(true)] + out IEntity? held, + [NotNullWhen(true)] + out ISolutionInteractionsComponent? heldSolution) + { + if (!holder.TryGetComponent(out HandsComponent? hands) + || hands.GetActiveHand == null + || !hands.GetActiveHand.Owner.TryGetComponent(out heldSolution)) + { + held = null; + heldSolution = null; + return false; + } + + held = heldSolution.Owner; + return true; + } + } + + /// + /// Transfers solution from the held container to the target container. + /// + [GlobalVerb] + internal sealed class SolutionFillTargetVerb : SolutionTransferVerbBase + { + public override void GetData(IEntity user, IEntity target, VerbData data) + { + if (!target.TryGetComponent(out ISolutionInteractionsComponent? targetSolution) || + !ActionBlockerSystem.CanInteract(user) || + !GetHeldSolution(user, out var source, out var sourceSolution) || + source != target || + !sourceSolution.CanDrain || + !targetSolution.CanRefill) + { + data.Visibility = VerbVisibility.Invisible; + return; + } + + data.Visibility = VerbVisibility.Visible; + data.Text = Loc.GetString("Transfer liquid from [{0}] to [{1}].", source.Name, target.Name); + } + + public override void Activate(IEntity user, IEntity target) + { + if (!GetHeldSolution(user, out _, out var handSolutionComp)) + { + return; + } + + if (!handSolutionComp.CanDrain || + !target.TryGetComponent(out ISolutionInteractionsComponent? targetComp) || + !targetComp.CanRefill) + { + return; + } + + var transferQuantity = ReagentUnit.Min( + targetComp.RefillSpaceAvailable, + handSolutionComp.DrainAvailable, + ReagentUnit.New(10)); + + if (transferQuantity <= 0) + { + return; + } + + var transferSolution = handSolutionComp.Drain(transferQuantity); + targetComp.Refill(transferSolution); + } + } + + /// + /// Transfers solution from a target container to the held container. + /// + [GlobalVerb] + internal sealed class SolutionDrainTargetVerb : SolutionTransferVerbBase + { + public override void GetData(IEntity user, IEntity target, VerbData data) + { + if (!target.TryGetComponent(out ISolutionInteractionsComponent? sourceSolution) || + !ActionBlockerSystem.CanInteract(user) || + !GetHeldSolution(user, out var held, out var targetSolution) || + !sourceSolution.CanDrain || + !targetSolution.CanRefill) + { + data.Visibility = VerbVisibility.Invisible; + return; + } + + data.Visibility = VerbVisibility.Visible; + data.Text = Loc.GetString("Transfer liquid from [{0}] to [{1}].", held.Name, target.Name); + } + + public override void Activate(IEntity user, IEntity target) + { + if (!GetHeldSolution(user, out _, out var targetComp)) + { + return; + } + + if (!targetComp.CanRefill || + !target.TryGetComponent(out ISolutionInteractionsComponent? sourceComp) || + !sourceComp.CanDrain) + { + return; + } + + var transferQuantity = ReagentUnit.Min( + targetComp.RefillSpaceAvailable, + sourceComp.DrainAvailable, + ReagentUnit.New(10)); + + if (transferQuantity <= 0) + { + return; + } + + var transferSolution = sourceComp.Drain(transferQuantity); + targetComp.Refill(transferSolution); + } + } + + [GlobalVerb] + internal sealed class AdminAddReagentVerb : GlobalVerb + { + public override bool RequireInteractionRange => false; + public override bool BlockedByContainers => false; + + private const AdminFlags ReqFlags = AdminFlags.Fun; + + private static void OpenAddReagentMenu(IPlayerSession player, IEntity target) + { + var euiMgr = IoCManager.Resolve(); + euiMgr.OpenEui(new AdminAddReagentEui(target), player); + } + + public override void GetData(IEntity user, IEntity target, VerbData data) + { + // ISolutionInteractionsComponent doesn't exactly have an interface for "admin tries to refill this", so... + // Still have a path for SolutionContainerComponent in case it doesn't allow direct refilling. + if (!target.HasComponent() + && !(target.TryGetComponent(out ISolutionInteractionsComponent? interactions) + && interactions.CanInject)) + { + data.Visibility = VerbVisibility.Invisible; + return; + } + + data.Text = Loc.GetString("Add Reagent..."); + data.CategoryData = VerbCategories.Debug; + data.Visibility = VerbVisibility.Invisible; + + var adminManager = IoCManager.Resolve(); + + if (user.TryGetComponent(out var player)) + { + if (adminManager.HasAdminFlag(player.playerSession, ReqFlags)) + { + data.Visibility = VerbVisibility.Visible; + } + } + } + + public override void Activate(IEntity user, IEntity target) + { + var groupController = IoCManager.Resolve(); + if (user.TryGetComponent(out var player)) + { + if (groupController.HasAdminFlag(player.playerSession, ReqFlags)) + OpenAddReagentMenu(player.playerSession, target); + } + } + + private sealed class AdminAddReagentEui : BaseEui + { + private readonly IEntity _target; + [Dependency] private readonly IAdminManager _adminManager = default!; + + public AdminAddReagentEui(IEntity target) + { + _target = target; + + IoCManager.InjectDependencies(this); + } + + public override void Opened() + { + StateDirty(); + } + + public override EuiStateBase GetNewState() + { + if (_target.TryGetComponent(out SolutionContainerComponent? container)) + { + return new AdminAddReagentEuiState + { + CurVolume = container.CurrentVolume, + MaxVolume = container.MaxVolume + }; + } + + if (_target.TryGetComponent(out ISolutionInteractionsComponent? interactions)) + { + return new AdminAddReagentEuiState + { + // We don't exactly have an absolute total volume so good enough. + CurVolume = ReagentUnit.Zero, + MaxVolume = interactions.InjectSpaceAvailable + }; + } + + return new AdminAddReagentEuiState + { + CurVolume = ReagentUnit.Zero, + MaxVolume = ReagentUnit.Zero + }; + } + + public override void HandleMessage(EuiMessageBase msg) + { + switch (msg) + { + case AdminAddReagentEuiMsg.Close: + Close(); + break; + case AdminAddReagentEuiMsg.DoAdd doAdd: + // Double check that user wasn't de-adminned in the mean time... + // Or the target was deleted. + if (!_adminManager.HasAdminFlag(Player, ReqFlags) || _target.Deleted) + { + Close(); + return; + } + + var id = doAdd.ReagentId; + var amount = doAdd.Amount; + + if (_target.TryGetComponent(out SolutionContainerComponent? container)) + { + container.TryAddReagent(id, amount, out _); + } + else if (_target.TryGetComponent(out ISolutionInteractionsComponent? interactions)) + { + var solution = new Solution(id, amount); + interactions.Inject(solution); + } + + StateDirty(); + + if (doAdd.CloseAfter) + Close(); + + break; + } + } + } + } +} diff --git a/Content.Server/GameObjects/Components/CrayonComponent.cs b/Content.Server/GameObjects/Components/CrayonComponent.cs index 5c0067a4f7..d44644999a 100644 --- a/Content.Server/GameObjects/Components/CrayonComponent.cs +++ b/Content.Server/GameObjects/Components/CrayonComponent.cs @@ -109,19 +109,22 @@ namespace Content.Server.GameObjects.Components return false; } - async Task IAfterInteract.AfterInteract(AfterInteractEventArgs eventArgs) + async Task IAfterInteract.AfterInteract(AfterInteractEventArgs eventArgs) { if (!eventArgs.InRangeUnobstructed(ignoreInsideBlocker: false, popup: true, - collisionMask: Shared.Physics.CollisionGroup.MobImpassable)) return; + collisionMask: Shared.Physics.CollisionGroup.MobImpassable)) + { + return true; + } if (Charges <= 0) { eventArgs.User.PopupMessage(Loc.GetString("Not enough left.")); - return; + return true; } var entityManager = IoCManager.Resolve(); - + var entity = entityManager.SpawnEntity("CrayonDecal", eventArgs.ClickLocation); if (entity.TryGetComponent(out AppearanceComponent? appearance)) { @@ -138,6 +141,7 @@ namespace Content.Server.GameObjects.Components // Decrease "Ammo" Charges--; Dirty(); + return true; } void IDropped.Dropped(DroppedEventArgs eventArgs) diff --git a/Content.Server/GameObjects/Components/Culinary/UtensilComponent.cs b/Content.Server/GameObjects/Components/Culinary/UtensilComponent.cs index d6c8629c6b..01eaeb5068 100644 --- a/Content.Server/GameObjects/Components/Culinary/UtensilComponent.cs +++ b/Content.Server/GameObjects/Components/Culinary/UtensilComponent.cs @@ -107,9 +107,10 @@ namespace Content.Server.GameObjects.Components.Culinary serializer.DataField(ref _breakSound, "breakSound", "/Audio/Items/snap.ogg"); } - async Task IAfterInteract.AfterInteract(AfterInteractEventArgs eventArgs) + async Task IAfterInteract.AfterInteract(AfterInteractEventArgs eventArgs) { TryUseUtensil(eventArgs.User, eventArgs.Target); + return true; } private void TryUseUtensil(IEntity user, IEntity? target) diff --git a/Content.Server/GameObjects/Components/Fluids/MopComponent.cs b/Content.Server/GameObjects/Components/Fluids/MopComponent.cs index ad6e85b951..11fda8a54a 100644 --- a/Content.Server/GameObjects/Components/Fluids/MopComponent.cs +++ b/Content.Server/GameObjects/Components/Fluids/MopComponent.cs @@ -62,14 +62,16 @@ namespace Content.Server.GameObjects.Components.Fluids Owner.EnsureComponentWarn(out SolutionContainerComponent _); } - async Task IAfterInteract.AfterInteract(AfterInteractEventArgs eventArgs) + async Task IAfterInteract.AfterInteract(AfterInteractEventArgs eventArgs) { - if (!Owner.TryGetComponent(out SolutionContainerComponent? contents)) return; - if (!eventArgs.InRangeUnobstructed(ignoreInsideBlocker: true, popup: true)) return; + if (!Owner.TryGetComponent(out SolutionContainerComponent? contents)) + return false; + if (!eventArgs.InRangeUnobstructed(ignoreInsideBlocker: true, popup: true)) + return false; if (CurrentVolume <= 0) { - return; + return true; } if (eventArgs.Target == null) @@ -77,12 +79,12 @@ namespace Content.Server.GameObjects.Components.Fluids // Drop the liquid on the mop on to the ground contents.SplitSolution(CurrentVolume).SpillAt(eventArgs.ClickLocation, "PuddleSmear"); - return; + return true; } if (!eventArgs.Target.TryGetComponent(out PuddleComponent? puddleComponent)) { - return; + return true; } // Essentially pickup either: // - _pickupAmount, @@ -101,7 +103,7 @@ namespace Content.Server.GameObjects.Components.Fluids } else { - return; + return true; } } else @@ -121,12 +123,12 @@ namespace Content.Server.GameObjects.Components.Fluids // Give some visual feedback shit's happening (for anyone who can't hear sound) Owner.PopupMessage(eventArgs.User, Loc.GetString("Swish")); - if (string.IsNullOrWhiteSpace(_pickupSound)) + if (!string.IsNullOrWhiteSpace(_pickupSound)) { - return; + EntitySystem.Get().PlayFromEntity(_pickupSound, Owner); } - EntitySystem.Get().PlayFromEntity(_pickupSound, Owner); + return true; } } } diff --git a/Content.Server/GameObjects/Components/Fluids/SpillableComponent.cs b/Content.Server/GameObjects/Components/Fluids/SpillableComponent.cs index 62b2a5ec6c..5026ede0e4 100644 --- a/Content.Server/GameObjects/Components/Fluids/SpillableComponent.cs +++ b/Content.Server/GameObjects/Components/Fluids/SpillableComponent.cs @@ -1,6 +1,5 @@ -using Content.Server.GameObjects.Components.Chemistry; -using Content.Shared.Chemistry; -using Content.Shared.GameObjects.EntitySystems; +using Content.Shared.Chemistry; +using Content.Shared.GameObjects.Components.Chemistry; using Content.Shared.GameObjects.EntitySystems.ActionBlocker; using Content.Shared.GameObjects.Verbs; using Content.Shared.Interfaces; @@ -24,34 +23,37 @@ namespace Content.Server.GameObjects.Components.Fluids protected override void GetData(IEntity user, SpillableComponent component, VerbData data) { if (!ActionBlockerSystem.CanInteract(user) || - !component.Owner.TryGetComponent(out SolutionContainerComponent solutionComponent) || - !solutionComponent.CanRemoveSolutions) + !component.Owner.TryGetComponent(out ISolutionInteractionsComponent solutionComponent) || + !solutionComponent.CanDrain) { data.Visibility = VerbVisibility.Invisible; return; } data.Text = Loc.GetString("Spill liquid"); - data.Visibility = solutionComponent.CurrentVolume > ReagentUnit.Zero ? VerbVisibility.Visible : VerbVisibility.Disabled; + data.Visibility = solutionComponent.DrainAvailable > ReagentUnit.Zero + ? VerbVisibility.Visible + : VerbVisibility.Disabled; } protected override void Activate(IEntity user, SpillableComponent component) { - if (component.Owner.TryGetComponent(out var solutionComponent)) + if (component.Owner.TryGetComponent(out var solutionComponent)) { - if (!solutionComponent.CanRemoveSolutions) + if (!solutionComponent.CanDrain) { - user.PopupMessage(user, Loc.GetString("You can't pour anything from {0:theName}!", component.Owner)); + user.PopupMessage(user, + Loc.GetString("You can't pour anything from {0:theName}!", component.Owner)); } - if (solutionComponent.CurrentVolume.Float() <= 0) + if (solutionComponent.DrainAvailable <= 0) { user.PopupMessage(user, Loc.GetString("{0:theName} is empty!", component.Owner)); } // Need this as when we split the component's owner may be deleted var entityLocation = component.Owner.Transform.Coordinates; - var solution = solutionComponent.SplitSolution(solutionComponent.CurrentVolume); + var solution = solutionComponent.Drain(solutionComponent.DrainAvailable); solution.SpillAt(entityLocation, "PuddleSmear"); } } diff --git a/Content.Server/GameObjects/Components/Fluids/SprayComponent.cs b/Content.Server/GameObjects/Components/Fluids/SprayComponent.cs index 8a45830637..1d020093b4 100644 --- a/Content.Server/GameObjects/Components/Fluids/SprayComponent.cs +++ b/Content.Server/GameObjects/Components/Fluids/SprayComponent.cs @@ -100,34 +100,34 @@ namespace Content.Server.GameObjects.Components.Fluids serializer.DataField(ref _safety, "safety", true); } - async Task IAfterInteract.AfterInteract(AfterInteractEventArgs eventArgs) + async Task IAfterInteract.AfterInteract(AfterInteractEventArgs eventArgs) { if (!ActionBlockerSystem.CanInteract(eventArgs.User)) - return; + return false; if (_hasSafety && _safety) { Owner.PopupMessage(eventArgs.User, Loc.GetString("Its safety is on!")); - return; + return true; } if (CurrentVolume <= 0) { Owner.PopupMessage(eventArgs.User, Loc.GetString("It's empty!")); - return; + return true; } var curTime = _gameTiming.CurTime; if(curTime < _cooldownEnd) - return; + return true; var playerPos = eventArgs.User.Transform.Coordinates; if (eventArgs.ClickLocation.GetGridId(_serverEntityManager) != playerPos.GetGridId(_serverEntityManager)) - return; + return true; if (!Owner.TryGetComponent(out SolutionContainerComponent contents)) - return; + return true; var direction = (eventArgs.ClickLocation.Position - playerPos.Position).Normalized; var threeQuarters = direction * 0.75f; @@ -183,6 +183,8 @@ namespace Content.Server.GameObjects.Components.Fluids cooldown.CooldownStart = _lastUseTime; cooldown.CooldownEnd = _cooldownEnd; } + + return true; } public bool UseEntity(UseEntityEventArgs eventArgs) diff --git a/Content.Server/GameObjects/Components/Interactable/TilePryingComponent.cs b/Content.Server/GameObjects/Components/Interactable/TilePryingComponent.cs index 3099b9779c..26edc3c903 100644 --- a/Content.Server/GameObjects/Components/Interactable/TilePryingComponent.cs +++ b/Content.Server/GameObjects/Components/Interactable/TilePryingComponent.cs @@ -21,9 +21,10 @@ namespace Content.Server.GameObjects.Components.Interactable public override string Name => "TilePrying"; private bool _toolComponentNeeded = true; - public async Task AfterInteract(AfterInteractEventArgs eventArgs) + public async Task AfterInteract(AfterInteractEventArgs eventArgs) { TryPryTile(eventArgs.User, eventArgs.ClickLocation); + return true; } public override void ExposeData(ObjectSerializer serializer) diff --git a/Content.Server/GameObjects/Components/Interactable/WelderComponent.cs b/Content.Server/GameObjects/Components/Interactable/WelderComponent.cs index 21776ab4e8..76348dbeff 100644 --- a/Content.Server/GameObjects/Components/Interactable/WelderComponent.cs +++ b/Content.Server/GameObjects/Components/Interactable/WelderComponent.cs @@ -2,6 +2,7 @@ using System; using System.Threading.Tasks; using Content.Server.Atmos; +using Content.Server.Explosions; using Content.Server.GameObjects.Components.Chemistry; using Content.Server.GameObjects.Components.Items.Storage; using Content.Server.GameObjects.EntitySystems; @@ -10,12 +11,15 @@ using Content.Server.Interfaces.GameObjects; using Content.Server.Utility; using Content.Shared.Chemistry; using Content.Shared.GameObjects; +using Content.Shared.GameObjects.Components.Chemistry; using Content.Shared.GameObjects.Components.Interactable; using Content.Shared.GameObjects.EntitySystems; using Content.Shared.Interfaces; using Content.Shared.Interfaces.GameObjects.Components; using Robust.Server.GameObjects; +using Robust.Server.GameObjects.EntitySystems; using Robust.Shared.GameObjects; +using Robust.Shared.GameObjects.Systems; using Robust.Shared.Interfaces.GameObjects; using Robust.Shared.IoC; using Robust.Shared.Localization; @@ -29,7 +33,7 @@ namespace Content.Server.GameObjects.Components.Interactable [ComponentReference(typeof(ToolComponent))] [ComponentReference(typeof(IToolComponent))] [ComponentReference(typeof(IHotItem))] - public class WelderComponent : ToolComponent, IExamine, IUse, ISuicideAct, ISolutionChange, IHotItem + public class WelderComponent : ToolComponent, IExamine, IUse, ISuicideAct, ISolutionChange, IHotItem, IAfterInteract { [Dependency] private readonly IEntitySystemManager _entitySystemManager = default!; @@ -293,5 +297,38 @@ namespace Content.Server.GameObjects.Components.Interactable } + async Task IAfterInteract.AfterInteract(AfterInteractEventArgs eventArgs) + { + if (eventArgs.Target == null || !eventArgs.CanReach) + { + return false; + } + + if (eventArgs.Target.TryGetComponent(out ReagentTankComponent? tank) + && tank.TankType == ReagentTankType.Fuel + && eventArgs.Target.TryGetComponent(out ISolutionInteractionsComponent? targetSolution) + && targetSolution.CanDrain + && _solutionComponent != null) + { + if (WelderLit) + { + // Oh no no + eventArgs.Target.SpawnExplosion(); + return true; + } + + var trans = ReagentUnit.Min(_solutionComponent.EmptyVolume, targetSolution.DrainAvailable); + if (trans > 0) + { + var drained = targetSolution.Drain(trans); + _solutionComponent.TryAddSolution(drained); + + EntitySystem.Get().PlayFromEntity("/Audio/Effects/refill.ogg", Owner); + eventArgs.Target.PopupMessage(eventArgs.User, Loc.GetString("Welder refueled")); + } + } + + return true; + } } } diff --git a/Content.Server/GameObjects/Components/Items/FireExtinguisherComponent.cs b/Content.Server/GameObjects/Components/Items/FireExtinguisherComponent.cs index 6159aa7f52..3edcd5b392 100644 --- a/Content.Server/GameObjects/Components/Items/FireExtinguisherComponent.cs +++ b/Content.Server/GameObjects/Components/Items/FireExtinguisherComponent.cs @@ -1,10 +1,52 @@ -using Robust.Shared.GameObjects; +using System.Threading.Tasks; +using Content.Server.GameObjects.Components.Chemistry; +using Content.Shared.Chemistry; +using Content.Shared.GameObjects.Components.Chemistry; +using Content.Shared.Interfaces; +using Content.Shared.Interfaces.GameObjects.Components; +using Robust.Server.GameObjects.EntitySystems; +using Robust.Shared.GameObjects; +using Robust.Shared.GameObjects.Systems; +using Robust.Shared.Localization; + +#nullable enable namespace Content.Server.GameObjects.Components.Items { [RegisterComponent] - public class FireExtinguisherComponent : Component + public class FireExtinguisherComponent : Component, IAfterInteract { public override string Name => "FireExtinguisher"; + + // Higher priority than sprays. + int IAfterInteract.Priority => 1; + + async Task IAfterInteract.AfterInteract(AfterInteractEventArgs eventArgs) + { + if (eventArgs.Target == null || !eventArgs.CanReach) + { + return false; + } + + if (eventArgs.Target.TryGetComponent(out ReagentTankComponent? tank) + && eventArgs.Target.TryGetComponent(out ISolutionInteractionsComponent? targetSolution) + && targetSolution.CanDrain + && Owner.TryGetComponent(out SolutionContainerComponent? container)) + { + var trans = ReagentUnit.Min(container.EmptyVolume, targetSolution.DrainAvailable); + if (trans > 0) + { + var drained = targetSolution.Drain(trans); + container.TryAddSolution(drained); + + EntitySystem.Get().PlayFromEntity("/Audio/Effects/refill.ogg", Owner); + eventArgs.Target.PopupMessage(eventArgs.User, Loc.GetString("{0:TheName} is now refilled", Owner)); + } + + return true; + } + + return false; + } } } diff --git a/Content.Server/GameObjects/Components/Items/FloorTileItemComponent.cs b/Content.Server/GameObjects/Components/Items/FloorTileItemComponent.cs index 57969eda49..341cdca33b 100644 --- a/Content.Server/GameObjects/Components/Items/FloorTileItemComponent.cs +++ b/Content.Server/GameObjects/Components/Items/FloorTileItemComponent.cs @@ -56,10 +56,14 @@ namespace Content.Server.GameObjects.Components.Items EntitySystem.Get().PlayAtCoords("/Audio/Items/genhit.ogg", location, AudioHelpers.WithVariation(0.125f)); } - public async Task AfterInteract(AfterInteractEventArgs eventArgs) + public async Task AfterInteract(AfterInteractEventArgs eventArgs) { - if (!eventArgs.InRangeUnobstructed(ignoreInsideBlocker: true, popup: true)) return; - if (!Owner.TryGetComponent(out StackComponent stack)) return; + if (!eventArgs.InRangeUnobstructed(ignoreInsideBlocker: true, popup: true)) + return true; + + if (!Owner.TryGetComponent(out StackComponent stack)) + return true; + var mapManager = IoCManager.Resolve(); var location = eventArgs.ClickLocation.AlignWithClosestGridTile(); @@ -88,10 +92,9 @@ namespace Content.Server.GameObjects.Components.Items PlaceAt(mapGrid, location, _tileDefinitionManager[_outputTiles[0]].TileId, mapGrid.TileSize / 2f); break; } - - } + return true; } } } diff --git a/Content.Server/GameObjects/Components/Items/RCD/RCDAmmoComponent.cs b/Content.Server/GameObjects/Components/Items/RCD/RCDAmmoComponent.cs index b79f422205..a06aa830b6 100644 --- a/Content.Server/GameObjects/Components/Items/RCD/RCDAmmoComponent.cs +++ b/Content.Server/GameObjects/Components/Items/RCD/RCDAmmoComponent.cs @@ -32,17 +32,17 @@ namespace Content.Server.GameObjects.Components.Items.RCD message.AddMarkup(Loc.GetString("It holds {0} charges.", refillAmmo)); } - public async Task AfterInteract(AfterInteractEventArgs eventArgs) + public async Task AfterInteract(AfterInteractEventArgs eventArgs) { if (eventArgs.Target == null || !eventArgs.Target.TryGetComponent(out RCDComponent rcdComponent) || !eventArgs.User.TryGetComponent(out IHandsComponent hands)) { - return; + return false; } if (rcdComponent.maxAmmo - rcdComponent._ammo < refillAmmo) { rcdComponent.Owner.PopupMessage(eventArgs.User, Loc.GetString("The RCD is full!")); - return; + return true; } rcdComponent._ammo = Math.Min(rcdComponent.maxAmmo, rcdComponent._ammo + refillAmmo); @@ -51,6 +51,7 @@ namespace Content.Server.GameObjects.Components.Items.RCD //Deleting a held item causes a lot of errors hands.Drop(Owner, false); Owner.Delete(); + return true; } } } diff --git a/Content.Server/GameObjects/Components/Items/RCD/RCDComponent.cs b/Content.Server/GameObjects/Components/Items/RCD/RCDComponent.cs index bef07051db..393a3780d8 100644 --- a/Content.Server/GameObjects/Components/Items/RCD/RCDComponent.cs +++ b/Content.Server/GameObjects/Components/Items/RCD/RCDComponent.cs @@ -94,7 +94,7 @@ namespace Content.Server.GameObjects.Components.Items.RCD message.AddMarkup(Loc.GetString("It's currently on {0} mode, and holds {1} charges.",_mode.ToString(), _ammo)); } - public async Task AfterInteract(AfterInteractEventArgs eventArgs) + public async Task AfterInteract(AfterInteractEventArgs eventArgs) { //No changing mode mid-RCD var startingMode = _mode; @@ -116,7 +116,7 @@ namespace Content.Server.GameObjects.Components.Items.RCD var result = await doAfterSystem.DoAfter(doAfterEventArgs); if (result == DoAfterStatus.Cancelled) { - return; + return true; } switch (_mode) @@ -146,12 +146,12 @@ namespace Content.Server.GameObjects.Components.Items.RCD airlock.Transform.LocalRotation = Owner.Transform.LocalRotation; //Now apply icon smoothing. break; default: - return; //I don't know why this would happen, but sure I guess. Get out of here invalid state! + return true; //I don't know why this would happen, but sure I guess. Get out of here invalid state! } _entitySystemManager.GetEntitySystem().PlayFromEntity("/Audio/Items/deconstruct.ogg", Owner); _ammo--; - + return true; } private bool IsRCDStillValid(AfterInteractEventArgs eventArgs, IMapGrid mapGrid, TileRef tile, Vector2i snapPos, RcdMode startingMode) diff --git a/Content.Server/GameObjects/Components/Kitchen/MicrowaveComponent.cs b/Content.Server/GameObjects/Components/Kitchen/MicrowaveComponent.cs index 60aca373e1..4009844a5b 100644 --- a/Content.Server/GameObjects/Components/Kitchen/MicrowaveComponent.cs +++ b/Content.Server/GameObjects/Components/Kitchen/MicrowaveComponent.cs @@ -13,6 +13,7 @@ using Content.Server.Utility; using Content.Shared.Chemistry; using Content.Shared.GameObjects.Components.Body; using Content.Shared.GameObjects.Components.Body.Part; +using Content.Shared.GameObjects.Components.Chemistry; using Content.Shared.GameObjects.Components.Power; using Content.Shared.GameObjects.EntitySystems; using Content.Shared.Interfaces; @@ -213,10 +214,10 @@ namespace Content.Server.GameObjects.Components.Kitchen return false; } - if (itemEntity.TryGetComponent(out var attackPourable)) + if (itemEntity.TryGetComponent(out var attackPourable)) { - if (!itemEntity.TryGetComponent(out var attackSolution) - || !attackSolution.CanRemoveSolutions) + if (!itemEntity.TryGetComponent(out var attackSolution) + || !attackSolution.CanDrain) { return false; } @@ -235,7 +236,7 @@ namespace Content.Server.GameObjects.Components.Kitchen } //Move units from attackSolution to targetSolution - var removedSolution = attackSolution.SplitSolution(realTransferAmount); + var removedSolution = attackSolution.Drain(realTransferAmount); if (!solution.TryAddSolution(removedSolution)) { return false; diff --git a/Content.Server/GameObjects/Components/Medical/HealingComponent.cs b/Content.Server/GameObjects/Components/Medical/HealingComponent.cs index cb4684238a..abe0c0a83a 100644 --- a/Content.Server/GameObjects/Components/Medical/HealingComponent.cs +++ b/Content.Server/GameObjects/Components/Medical/HealingComponent.cs @@ -26,39 +26,41 @@ namespace Content.Server.GameObjects.Components.Medical serializer.DataField(this, h => h.Heal, "heal", new Dictionary()); } - public async Task AfterInteract(AfterInteractEventArgs eventArgs) + public async Task AfterInteract(AfterInteractEventArgs eventArgs) { if (eventArgs.Target == null) { - return; + return false; } if (!eventArgs.Target.TryGetComponent(out IDamageableComponent damageable)) { - return; + return true; } if (!ActionBlockerSystem.CanInteract(eventArgs.User)) { - return; + return true; } if (eventArgs.User != eventArgs.Target && !eventArgs.InRangeUnobstructed(ignoreInsideBlocker: true, popup: true)) { - return; + return true; } if (Owner.TryGetComponent(out StackComponent stack) && !stack.Use(1)) { - return; + return true; } foreach (var (type, amount) in Heal) { damageable.ChangeDamage(type, -amount, true); } + + return true; } } } diff --git a/Content.Server/GameObjects/Components/Nutrition/DrinkComponent.cs b/Content.Server/GameObjects/Components/Nutrition/DrinkComponent.cs index 4990ac6075..cd41d3f2c5 100644 --- a/Content.Server/GameObjects/Components/Nutrition/DrinkComponent.cs +++ b/Content.Server/GameObjects/Components/Nutrition/DrinkComponent.cs @@ -3,11 +3,9 @@ using System.Threading.Tasks; using Content.Server.GameObjects.Components.Body.Behavior; using Content.Server.GameObjects.Components.Chemistry; using Content.Server.GameObjects.Components.Fluids; -using Content.Server.GameObjects.EntitySystems; using Content.Shared.Audio; using Content.Shared.Chemistry; using Content.Shared.GameObjects.Components.Body; -using Content.Shared.GameObjects.Components.Body.Mechanism; using Content.Shared.GameObjects.Components.Nutrition; using Content.Shared.GameObjects.EntitySystems; using Content.Shared.Interfaces; @@ -75,7 +73,7 @@ namespace Content.Server.GameObjects.Components.Nutrition _contents = Owner.AddComponent(); } - _contents.Capabilities = SolutionContainerCaps.AddTo | SolutionContainerCaps.RemoveFrom; + _contents.Capabilities = SolutionContainerCaps.Refillable | SolutionContainerCaps.Drainable; Opened = _defaultToOpened; UpdateAppearance(); } @@ -113,9 +111,10 @@ namespace Content.Server.GameObjects.Components.Nutrition } //Force feeding a drink to someone. - async Task IAfterInteract.AfterInteract(AfterInteractEventArgs eventArgs) + async Task IAfterInteract.AfterInteract(AfterInteractEventArgs eventArgs) { TryUseDrink(eventArgs.Target, forced: true); + return true; } public void Examine(FormattedMessage message, bool inDetailsRange) @@ -131,7 +130,7 @@ namespace Content.Server.GameObjects.Components.Nutrition private bool TryUseDrink(IEntity target, bool forced = false) { - if (target == null || !_contents.CanRemoveSolutions) + if (target == null || !_contents.CanDrain) { return false; } diff --git a/Content.Server/GameObjects/Components/Nutrition/FoodComponent.cs b/Content.Server/GameObjects/Components/Nutrition/FoodComponent.cs index b7318fdce1..0cb8436e11 100644 --- a/Content.Server/GameObjects/Components/Nutrition/FoodComponent.cs +++ b/Content.Server/GameObjects/Components/Nutrition/FoodComponent.cs @@ -100,14 +100,15 @@ namespace Content.Server.GameObjects.Components.Nutrition } // Feeding someone else - async Task IAfterInteract.AfterInteract(AfterInteractEventArgs eventArgs) + async Task IAfterInteract.AfterInteract(AfterInteractEventArgs eventArgs) { if (eventArgs.Target == null) { - return; + return false; } TryUseFood(eventArgs.User, eventArgs.Target); + return true; } public virtual bool TryUseFood(IEntity? user, IEntity? target, UtensilComponent? utensilUsed = null) diff --git a/Content.Server/GameObjects/Components/Portal/TeleporterComponent.cs b/Content.Server/GameObjects/Components/Portal/TeleporterComponent.cs index f53d4b8f1d..b92632aa3b 100644 --- a/Content.Server/GameObjects/Components/Portal/TeleporterComponent.cs +++ b/Content.Server/GameObjects/Components/Portal/TeleporterComponent.cs @@ -23,7 +23,7 @@ using Robust.Shared.ViewVariables; namespace Content.Server.GameObjects.Components.Portal { [RegisterComponent] - public class TeleporterComponent : Component, IAfterInteract + public class TeleporterComponent : Component, IAfterInteract { [Dependency] private readonly IServerEntityManager _serverEntityManager = default!; [Dependency] private readonly IRobustRandom _spreadRandom = default!; @@ -78,7 +78,7 @@ namespace Content.Server.GameObjects.Components.Portal _state = newState; } - async Task IAfterInteract.AfterInteract(AfterInteractEventArgs eventArgs) + async Task IAfterInteract.AfterInteract(AfterInteractEventArgs eventArgs) { if (_teleporterType == TeleporterType.Directed) { @@ -89,6 +89,8 @@ namespace Content.Server.GameObjects.Components.Portal { TryRandomTeleport(eventArgs.User); } + + return true; } public void TryDirectedTeleport(IEntity user, MapCoordinates mapCoords) diff --git a/Content.Server/GameObjects/Components/Power/WirePlacerComponent.cs b/Content.Server/GameObjects/Components/Power/WirePlacerComponent.cs index 81d8654360..e3a33418ee 100644 --- a/Content.Server/GameObjects/Components/Power/WirePlacerComponent.cs +++ b/Content.Server/GameObjects/Components/Power/WirePlacerComponent.cs @@ -34,28 +34,29 @@ namespace Content.Server.GameObjects.Components.Power } /// - public async Task AfterInteract(AfterInteractEventArgs eventArgs) + public async Task AfterInteract(AfterInteractEventArgs eventArgs) { if (_wirePrototypeID == null) - return; - - if (!eventArgs.InRangeUnobstructed(ignoreInsideBlocker: true, popup: true)) return; + return true; + if (!eventArgs.InRangeUnobstructed(ignoreInsideBlocker: true, popup: true)) + return true; if(!_mapManager.TryGetGrid(eventArgs.ClickLocation.GetGridId(Owner.EntityManager), out var grid)) - return; + return true; var snapPos = grid.SnapGridCellFor(eventArgs.ClickLocation, SnapGridOffset.Center); var snapCell = grid.GetSnapGridCell(snapPos, SnapGridOffset.Center); if(grid.GetTileRef(snapPos).Tile.IsEmpty) - return; + return true; foreach (var snapComp in snapCell) { if (snapComp.Owner.TryGetComponent(out var wire) && wire.WireType == _blockingWireType) { - return; + return true; } } if (Owner.TryGetComponent(out var stack) && !stack.Use(1)) - return; + return true; Owner.EntityManager.SpawnEntity(_wirePrototypeID, grid.GridTileToLocal(snapPos)); + return true; } } } diff --git a/Content.Server/GameObjects/Components/Weapon/Ranged/Ammunition/SpeedLoaderComponent.cs b/Content.Server/GameObjects/Components/Weapon/Ranged/Ammunition/SpeedLoaderComponent.cs index f674459c2a..5ddf430552 100644 --- a/Content.Server/GameObjects/Components/Weapon/Ranged/Ammunition/SpeedLoaderComponent.cs +++ b/Content.Server/GameObjects/Components/Weapon/Ranged/Ammunition/SpeedLoaderComponent.cs @@ -146,11 +146,11 @@ namespace Content.Server.GameObjects.Components.Weapon.Ranged.Ammunition return entity; } - async Task IAfterInteract.AfterInteract(AfterInteractEventArgs eventArgs) + async Task IAfterInteract.AfterInteract(AfterInteractEventArgs eventArgs) { if (eventArgs.Target == null) { - return; + return false; } // This area is dirty but not sure of an easier way to do it besides add an interface or somethin @@ -203,6 +203,8 @@ namespace Content.Server.GameObjects.Components.Weapon.Ranged.Ammunition { UpdateAppearance(); } + + return true; } async Task IInteractUsing.InteractUsing(InteractUsingEventArgs eventArgs) diff --git a/Content.Server/GameObjects/EntitySystems/Click/InteractionSystem.cs b/Content.Server/GameObjects/EntitySystems/Click/InteractionSystem.cs index 552d3c43c5..40ee427b78 100644 --- a/Content.Server/GameObjects/EntitySystems/Click/InteractionSystem.cs +++ b/Content.Server/GameObjects/EntitySystems/Click/InteractionSystem.cs @@ -416,13 +416,8 @@ namespace Content.Server.GameObjects.EntitySystems.Click return; } - var afterInteracts = weapon.GetAllComponents().ToList(); var afterInteractEventArgs = new AfterInteractEventArgs(user, clickLocation, null, canReach); - - foreach (var afterInteract in afterInteracts) - { - await afterInteract.AfterInteract(afterInteractEventArgs); - } + await DoAfterInteract(weapon, afterInteractEventArgs); } /// @@ -465,13 +460,9 @@ namespace Content.Server.GameObjects.EntitySystems.Click } // If we aren't directly attacking the nearby object, lets see if our item has an after attack we can do - var afterAttacks = weapon.GetAllComponents().ToList(); var afterAttackEventArgs = new AfterInteractEventArgs(user, clickLocation, attacked, canReach: true); - foreach (var afterAttack in afterAttacks) - { - await afterAttack.AfterInteract(afterAttackEventArgs); - } + await DoAfterInteract(weapon, afterAttackEventArgs); } /// @@ -835,13 +826,21 @@ namespace Content.Server.GameObjects.EntitySystems.Click if (afterAtkMsg.Handled) return; - var afterAttacks = weapon.GetAllComponents().ToList(); + // See if we have a ranged attack interaction var afterAttackEventArgs = new AfterInteractEventArgs(user, clickLocation, attacked, canReach: false); + await DoAfterInteract(weapon, afterAttackEventArgs); + } + + private static async Task DoAfterInteract(IEntity weapon, AfterInteractEventArgs afterAttackEventArgs) + { + var afterAttacks = weapon.GetAllComponents().OrderByDescending(x => x.Priority).ToList(); - //See if we have a ranged attack interaction foreach (var afterAttack in afterAttacks) { - await afterAttack.AfterInteract(afterAttackEventArgs); + if (await afterAttack.AfterInteract(afterAttackEventArgs)) + { + return; + } } } diff --git a/Content.Shared/Chemistry/Solution.cs b/Content.Shared/Chemistry/Solution.cs index 7fb2315c1e..cd6d801210 100644 --- a/Content.Shared/Chemistry/Solution.cs +++ b/Content.Shared/Chemistry/Solution.cs @@ -92,7 +92,7 @@ namespace Content.Shared.Chemistry return ""; } - var majorReagent = Contents.OrderByDescending(reagent => reagent.Quantity).First(); ; + var majorReagent = Contents.OrderByDescending(reagent => reagent.Quantity).First(); return majorReagent.ReagentId; } diff --git a/Content.Shared/Chemistry/SolutionCaps.cs b/Content.Shared/Chemistry/SolutionCaps.cs deleted file mode 100644 index 3dada4d98e..0000000000 --- a/Content.Shared/Chemistry/SolutionCaps.cs +++ /dev/null @@ -1,46 +0,0 @@ -using System; -using Robust.Shared.Serialization; - -namespace Content.Shared.Chemistry -{ - /// - /// These are the defined capabilities of a container of a solution. - /// - [Flags] - [Serializable, NetSerializable] - public enum SolutionContainerCaps - { - None = 0, - - /// - /// Can solutions be added into the container? - /// - AddTo = 1, - - /// - /// Can solutions be removed from the container? - /// - RemoveFrom = 2, - - /// - /// Allows the container to be placed in a ReagentDispenserComponent. - /// Otherwise it's considered to be too large or the improper shape to fit. - /// Allows us to have obscenely large containers that are harder to abuse in chem dispensers - /// since they can't be placed directly in them. - /// - FitsInDispenser = 4, - - /// - /// Can people examine the solution in the container or is it impossible to see? - /// - CanExamine = 8, - } - - public static class SolutionContainerCapsHelpers - { - public static bool HasCap(this SolutionContainerCaps cap, SolutionContainerCaps other) - { - return (cap & other) == other; - } - } -} diff --git a/Content.Shared/Chemistry/SolutionContainerCaps.cs b/Content.Shared/Chemistry/SolutionContainerCaps.cs new file mode 100644 index 0000000000..47d8a76371 --- /dev/null +++ b/Content.Shared/Chemistry/SolutionContainerCaps.cs @@ -0,0 +1,62 @@ +using System; +using Content.Shared.GameObjects.Components.Chemistry; +using Robust.Shared.Serialization; + +namespace Content.Shared.Chemistry +{ + /// + /// Define common interaction behaviors for + /// + /// + [Flags] + [Serializable, NetSerializable] + public enum SolutionContainerCaps : ushort + { + None = 0, + + /// + /// Reagents can be added with syringes. + /// + Injectable = 1 << 0, + + /// + /// Reagents can be removed with syringes. + /// + Drawable = 1 << 1, + + /// + /// Reagents can be easily added via all reagent containers. + /// Think pouring something into another beaker or into the gas tank of a car. + /// + Refillable = 1 << 2, + + /// + /// Reagents can be easily removed through any reagent container. + /// Think pouring this or draining from a water tank. + /// + Drainable = 1 << 3, + + /// + /// The contents of the solution can be examined directly. + /// + CanExamine = 1 << 4, + + /// + /// Allows the container to be placed in a ReagentDispenserComponent. + /// Otherwise it's considered to be too large or the improper shape to fit. + /// Allows us to have obscenely large containers that are harder to abuse in chem dispensers + /// since they can't be placed directly in them. + /// + FitsInDispenser = 1 << 5, + + OpenContainer = Refillable | Drainable | CanExamine + } + + public static class SolutionContainerCapsHelpers + { + public static bool HasCap(this SolutionContainerCaps cap, SolutionContainerCaps other) + { + return (cap & other) == other; + } + } +} diff --git a/Content.Shared/GameObjects/Components/Chemistry/ISolutionInteractionsComponent.cs b/Content.Shared/GameObjects/Components/Chemistry/ISolutionInteractionsComponent.cs new file mode 100644 index 0000000000..c88c46234d --- /dev/null +++ b/Content.Shared/GameObjects/Components/Chemistry/ISolutionInteractionsComponent.cs @@ -0,0 +1,84 @@ +using Content.Shared.Chemistry; +using Robust.Shared.Interfaces.GameObjects; + +namespace Content.Shared.GameObjects.Components.Chemistry +{ + /// + /// High-level solution transferring operations like "what happens when a syringe tries to inject this entity." + /// + /// + /// This interface is most often implemented by using + /// and setting the appropriate + /// + public interface ISolutionInteractionsComponent : IComponent + { + // + // INJECTING + // + + /// + /// Whether we CAN POTENTIALLY be injected with solutions by items like syringes. + /// + /// + /// + /// This should NOT change to communicate behavior like "the container is full". + /// Change to 0 for that. + /// + /// + /// If refilling is allowed () you should also always allow injecting. + /// + /// + bool CanInject => false; + + /// + /// The amount of solution space available for injecting. + /// + ReagentUnit InjectSpaceAvailable => ReagentUnit.Zero; + + /// + /// Actually inject reagents. + /// + void Inject(Solution solution) + { + + } + + // + // DRAWING + // + + bool CanDraw => false; + ReagentUnit DrawAvailable => ReagentUnit.Zero; + + Solution Draw(ReagentUnit amount) + { + return new(); + } + + + + // + // REFILLING + // + + bool CanRefill => false; + ReagentUnit RefillSpaceAvailable => ReagentUnit.Zero; + + void Refill(Solution solution) + { + + } + + // + // DRAINING + // + + bool CanDrain => false; + ReagentUnit DrainAvailable => ReagentUnit.Zero; + + Solution Drain(ReagentUnit amount) + { + return new(); + } + } +} diff --git a/Content.Shared/GameObjects/Components/Chemistry/SharedSolutionContainerComponent.cs b/Content.Shared/GameObjects/Components/Chemistry/SharedSolutionContainerComponent.cs index e453f4aeda..0a7d1fe7d2 100644 --- a/Content.Shared/GameObjects/Components/Chemistry/SharedSolutionContainerComponent.cs +++ b/Content.Shared/GameObjects/Components/Chemistry/SharedSolutionContainerComponent.cs @@ -20,7 +20,7 @@ namespace Content.Shared.GameObjects.Components.Chemistry /// /// Holds a with a limited volume. /// - public abstract class SharedSolutionContainerComponent : Component, IExamine + public abstract class SharedSolutionContainerComponent : Component, IExamine, ISolutionInteractionsComponent { public override string Name => "SolutionContainer"; @@ -60,9 +60,11 @@ namespace Content.Shared.GameObjects.Components.Chemistry public bool CanUseWithChemDispenser => Capabilities.HasCap(SolutionContainerCaps.FitsInDispenser); - public bool CanAddSolutions => Capabilities.HasCap(SolutionContainerCaps.AddTo); + public bool CanInject => Capabilities.HasCap(SolutionContainerCaps.Injectable) || CanRefill; + public bool CanDraw => Capabilities.HasCap(SolutionContainerCaps.Drawable) || CanDrain; - public bool CanRemoveSolutions => Capabilities.HasCap(SolutionContainerCaps.RemoveFrom); + public bool CanRefill => Capabilities.HasCap(SolutionContainerCaps.Refillable); + public bool CanDrain => Capabilities.HasCap(SolutionContainerCaps.Drainable); public override void ExposeData(ObjectSerializer serializer) { @@ -71,7 +73,7 @@ namespace Content.Shared.GameObjects.Components.Chemistry serializer.DataField(this, x => x.CanReact, "canReact", true); serializer.DataField(this, x => x.MaxVolume, "maxVol", ReagentUnit.New(0)); serializer.DataField(this, x => x.Solution, "contents", new Solution()); - serializer.DataField(this, x => x.Capabilities, "caps", SolutionContainerCaps.AddTo | SolutionContainerCaps.RemoveFrom | SolutionContainerCaps.CanExamine); + serializer.DataField(this, x => x.Capabilities, "caps", SolutionContainerCaps.None); } public void RemoveAllSolution() @@ -209,6 +211,43 @@ namespace Content.Shared.GameObjects.Components.Chemistry message.AddMarkup(Loc.GetString(messageString, colorHex, Loc.GetString(proto.PhysicalDescription))); } + ReagentUnit ISolutionInteractionsComponent.RefillSpaceAvailable => EmptyVolume; + ReagentUnit ISolutionInteractionsComponent.InjectSpaceAvailable => EmptyVolume; + ReagentUnit ISolutionInteractionsComponent.DrawAvailable => CurrentVolume; + ReagentUnit ISolutionInteractionsComponent.DrainAvailable => CurrentVolume; + + void ISolutionInteractionsComponent.Refill(Solution solution) + { + if (!CanRefill) + return; + + TryAddSolution(solution); + } + + void ISolutionInteractionsComponent.Inject(Solution solution) + { + if (!CanInject) + return; + + TryAddSolution(solution); + } + + Solution ISolutionInteractionsComponent.Draw(ReagentUnit amount) + { + if (!CanDraw) + return new Solution(); + + return SplitSolution(amount); + } + + Solution ISolutionInteractionsComponent.Drain(ReagentUnit amount) + { + if (!CanDrain) + return new Solution(); + + return SplitSolution(amount); + } + private void UpdateAppearance() { if (Owner.Deleted || !Owner.TryGetComponent(out var appearance)) diff --git a/Content.Shared/Interfaces/GameObjects/Components/Interaction/IAfterInteract.cs b/Content.Shared/Interfaces/GameObjects/Components/Interaction/IAfterInteract.cs index 16299a23c9..716d0b3fd7 100644 --- a/Content.Shared/Interfaces/GameObjects/Components/Interaction/IAfterInteract.cs +++ b/Content.Shared/Interfaces/GameObjects/Components/Interaction/IAfterInteract.cs @@ -16,10 +16,16 @@ namespace Content.Shared.Interfaces.GameObjects.Components /// public interface IAfterInteract { + /// + /// The interaction priority. Higher numbers get called first. + /// + /// Priority defaults to 0 + int Priority => 0; + /// /// Called when we interact with nothing, or when we interact with an entity out of range that has no behavior /// - Task AfterInteract(AfterInteractEventArgs eventArgs); + Task AfterInteract(AfterInteractEventArgs eventArgs); } public class AfterInteractEventArgs : EventArgs diff --git a/Resources/Audio/Effects/refill.ogg b/Resources/Audio/Effects/refill.ogg new file mode 100644 index 0000000000000000000000000000000000000000..ec5c4dc34442acf29766ea43e4bae49ed2a4ff7f GIT binary patch literal 14607 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 True True + True True True True