From bd8acc5b6b1fe5ea4d6a95e869d00b04409258f7 Mon Sep 17 00:00:00 2001 From: Leon Friedrich <60421075+ElectroJr@users.noreply.github.com> Date: Tue, 7 Dec 2021 19:19:26 +1300 Subject: [PATCH] Fix food & do-after bugs (#5716) --- .../Chemistry/Components/InjectorComponent.cs | 19 +++++--- .../Chemistry/EntitySystems/InjectorSystem.cs | 14 +++++- Content.Server/DoAfter/DoAfterSystem.cs | 16 +++---- .../Nutrition/Components/DrinkComponent.cs | 7 +-- .../Nutrition/Components/FoodComponent.cs | 6 ++- .../Nutrition/EntitySystems/DrinkSystem.cs | 44 ++++++++++++++--- .../Nutrition/EntitySystems/FoodSystem.cs | 47 ++++++++++++++++--- 7 files changed, 117 insertions(+), 36 deletions(-) diff --git a/Content.Server/Chemistry/Components/InjectorComponent.cs b/Content.Server/Chemistry/Components/InjectorComponent.cs index aab8a633d5..446f6e935a 100644 --- a/Content.Server/Chemistry/Components/InjectorComponent.cs +++ b/Content.Server/Chemistry/Components/InjectorComponent.cs @@ -1,4 +1,5 @@ using System; +using System.Threading; using System.Threading.Tasks; using Content.Server.Administration.Logs; using Content.Server.Body.Components; @@ -64,9 +65,10 @@ namespace Content.Server.Chemistry.Components public float Delay = 5; /// - /// Is this component currently being used in a DoAfter? + /// Token for interrupting a do-after action (e.g., injection another player). If not null, implies + /// component is currently "in use". /// - public bool InUse = false; + public CancellationTokenSource? CancelToken; private InjectorToggleMode _toggleState; @@ -127,8 +129,11 @@ namespace Content.Server.Chemistry.Components /// async Task IAfterInteract.AfterInteract(AfterInteractEventArgs eventArgs) { - if (InUse) - return false; + if (CancelToken != null) + { + CancelToken.Cancel(); + return true; + } if (!eventArgs.InRangeUnobstructed(ignoreInsideBlocker: true, popup: true)) return false; @@ -197,7 +202,6 @@ namespace Content.Server.Chemistry.Components /// public async Task TryInjectDoAfter(EntityUid user, EntityUid target) { - InUse = true; var popupSys = EntitySystem.Get(); // Create a pop-up for the user @@ -249,8 +253,9 @@ namespace Content.Server.Chemistry.Components //TODO solution pretty string. } + CancelToken = new(); var status = await EntitySystem.Get().WaitDoAfter( - new DoAfterEventArgs(user, actualDelay, target: target) + new DoAfterEventArgs(user, actualDelay, CancelToken.Token, target) { BreakOnUserMove = true, BreakOnDamage = true, @@ -258,7 +263,7 @@ namespace Content.Server.Chemistry.Components BreakOnTargetMove = true, MovementThreshold = 1.0f }); - InUse = false; + CancelToken = null; return status == DoAfterStatus.Finished; } diff --git a/Content.Server/Chemistry/EntitySystems/InjectorSystem.cs b/Content.Server/Chemistry/EntitySystems/InjectorSystem.cs index e4affc4678..19bc66986e 100644 --- a/Content.Server/Chemistry/EntitySystems/InjectorSystem.cs +++ b/Content.Server/Chemistry/EntitySystems/InjectorSystem.cs @@ -1,6 +1,8 @@ -using Content.Server.Chemistry.Components; +using Content.Server.Chemistry.Components; +using Content.Shared.Hands; using JetBrains.Annotations; using Robust.Shared.GameObjects; +using System; namespace Content.Server.Chemistry.EntitySystems { @@ -12,6 +14,16 @@ namespace Content.Server.Chemistry.EntitySystems base.Initialize(); SubscribeLocalEvent(OnSolutionChange); + SubscribeLocalEvent(OnInjectorDeselected); + } + + private void OnInjectorDeselected(EntityUid uid, InjectorComponent component, HandDeselectedEvent args) + { + if (component.CancelToken != null) + { + component.CancelToken.Cancel(); + component.CancelToken = null; + } } private void OnSolutionChange(EntityUid uid, InjectorComponent component, SolutionChangedEvent args) diff --git a/Content.Server/DoAfter/DoAfterSystem.cs b/Content.Server/DoAfter/DoAfterSystem.cs index e64afc9456..ea301b9743 100644 --- a/Content.Server/DoAfter/DoAfterSystem.cs +++ b/Content.Server/DoAfter/DoAfterSystem.cs @@ -12,8 +12,8 @@ namespace Content.Server.DoAfter public sealed class DoAfterSystem : EntitySystem { // We cache these lists as to not allocate them every update tick... - private readonly List _cancelled = new(); - private readonly List _finished = new(); + private readonly Queue _cancelled = new(); + private readonly Queue _finished = new(); public override void Initialize() { @@ -52,17 +52,17 @@ namespace Content.Server.DoAfter case DoAfterStatus.Running: break; case DoAfterStatus.Cancelled: - _cancelled.Add(doAfter); + _cancelled.Enqueue(doAfter); break; case DoAfterStatus.Finished: - _finished.Add(doAfter); + _finished.Enqueue(doAfter); break; default: throw new ArgumentOutOfRangeException(); } } - foreach (var doAfter in _cancelled) + while (_cancelled.TryDequeue(out var doAfter)) { comp.Cancelled(doAfter); @@ -76,7 +76,7 @@ namespace Content.Server.DoAfter RaiseLocalEvent(doAfter.EventArgs.BroadcastCancelledEvent); } - foreach (var doAfter in _finished) + while (_finished.TryDequeue(out var doAfter)) { comp.Finished(doAfter); @@ -89,10 +89,6 @@ namespace Content.Server.DoAfter if(doAfter.EventArgs.BroadcastFinishedEvent != null) RaiseLocalEvent(doAfter.EventArgs.BroadcastFinishedEvent); } - - // Clean the shared lists at the end, ensuring they'll be clean for the next time we need them. - _cancelled.Clear(); - _finished.Clear(); } } diff --git a/Content.Server/Nutrition/Components/DrinkComponent.cs b/Content.Server/Nutrition/Components/DrinkComponent.cs index 2dfc6c495d..b6d893ae02 100644 --- a/Content.Server/Nutrition/Components/DrinkComponent.cs +++ b/Content.Server/Nutrition/Components/DrinkComponent.cs @@ -1,4 +1,3 @@ -using Content.Shared.Chemistry.Reagent; using Content.Shared.Sound; using JetBrains.Annotations; using Robust.Shared.GameObjects; @@ -7,6 +6,7 @@ using Robust.Shared.ViewVariables; using Content.Server.Nutrition.EntitySystems; using Content.Shared.FixedPoint; using Robust.Shared.Analyzers; +using System.Threading; namespace Content.Server.Nutrition.Components { @@ -50,8 +50,9 @@ namespace Content.Server.Nutrition.Components public float ForceFeedDelay = 3; /// - /// If true, this drink has some DoAfter active (someone is being force fed). + /// Token for interrupting a do-after action (e.g., force feeding). If not null, implies component is + /// currently "in use". /// - public bool InUse = false; + public CancellationTokenSource? CancelToken; } } diff --git a/Content.Server/Nutrition/Components/FoodComponent.cs b/Content.Server/Nutrition/Components/FoodComponent.cs index b567e5cdd0..8d3e1fac19 100644 --- a/Content.Server/Nutrition/Components/FoodComponent.cs +++ b/Content.Server/Nutrition/Components/FoodComponent.cs @@ -1,4 +1,5 @@ using System; +using System.Threading; using Content.Server.Chemistry.EntitySystems; using Content.Server.Nutrition.EntitySystems; using Content.Shared.FixedPoint; @@ -55,9 +56,10 @@ namespace Content.Server.Nutrition.Components public float ForceFeedDelay = 3; /// - /// If true, this food has some DoAfter active (someone is being force fed). + /// Token for interrupting a do-after action (e.g., force feeding). If not null, implies component is + /// currently "in use". /// - public bool InUse = false; + public CancellationTokenSource? CancelToken; [ViewVariables] public int UsesRemaining diff --git a/Content.Server/Nutrition/EntitySystems/DrinkSystem.cs b/Content.Server/Nutrition/EntitySystems/DrinkSystem.cs index b327c8c1a2..c2258c565b 100644 --- a/Content.Server/Nutrition/EntitySystems/DrinkSystem.cs +++ b/Content.Server/Nutrition/EntitySystems/DrinkSystem.cs @@ -16,6 +16,7 @@ using Content.Shared.Chemistry.Reagent; using Content.Shared.Database; using Content.Shared.Examine; using Content.Shared.FixedPoint; +using Content.Shared.Hands; using Content.Shared.Interaction; using Content.Shared.Interaction.Helpers; using Content.Shared.Nutrition.Components; @@ -54,12 +55,26 @@ namespace Content.Server.Nutrition.EntitySystems SubscribeLocalEvent(OnDrinkInit); SubscribeLocalEvent(HandleLand); SubscribeLocalEvent(OnUse); + SubscribeLocalEvent(OnDrinkDeselected); SubscribeLocalEvent(AfterInteract); SubscribeLocalEvent(OnExamined); SubscribeLocalEvent(OnForceDrink); SubscribeLocalEvent(OnForceDrinkCancelled); } + /// + /// If the user is currently forcing someone do drink, this cancels the attempt if they swap hands or + /// otherwise loose the item. Prevents force-feeding dual-wielding. + /// + private void OnDrinkDeselected(EntityUid uid, DrinkComponent component, HandDeselectedEvent args) + { + if (component.CancelToken != null) + { + component.CancelToken.Cancel(); + component.CancelToken = null; + } + } + public bool IsEmpty(EntityUid uid, DrinkComponent? component = null) { if(!Resolve(uid, ref component)) @@ -240,6 +255,14 @@ namespace Content.Server.Nutrition.EntitySystems if (!Resolve(uid, ref drink)) return false; + // if currently being used to force-feed, cancel that action. + if (drink.CancelToken != null) + { + drink.CancelToken.Cancel(); + drink.CancelToken = null; + return true; + } + if (!drink.Opened) { _popupSystem.PopupEntity(Loc.GetString("drink-component-try-use-drink-not-open", @@ -312,8 +335,12 @@ namespace Content.Server.Nutrition.EntitySystems return false; // cannot stack do-afters - if (drink.InUse) - return false; + if (drink.CancelToken != null) + { + drink.CancelToken.Cancel(); + drink.CancelToken = null; + return true; + } if (!EntityManager.HasComponent(targetUid)) return false; @@ -342,7 +369,8 @@ namespace Content.Server.Nutrition.EntitySystems _popupSystem.PopupEntity(Loc.GetString("drink-component-force-feed", ("user", userName)), userUid, Filter.Entities(targetUid)); - _doAfterSystem.DoAfter(new DoAfterEventArgs(userUid, drink.ForceFeedDelay, target: targetUid) + drink.CancelToken = new(); + _doAfterSystem.DoAfter(new DoAfterEventArgs(userUid, drink.ForceFeedDelay, drink.CancelToken.Token, targetUid) { BreakOnUserMove = true, BreakOnDamage = true, @@ -350,7 +378,7 @@ namespace Content.Server.Nutrition.EntitySystems BreakOnTargetMove = true, MovementThreshold = 1.0f, TargetFinishedEvent = new ForceDrinkEvent(userUid, drink, drinkSolution), - BroadcastCancelledEvent = new ForceDrinkCancelledEvent(drink) + BroadcastCancelledEvent = new ForceDrinkCancelledEvent(drink), }); // logging @@ -359,7 +387,6 @@ namespace Content.Server.Nutrition.EntitySystems var drinkable = EntityManager.GetEntity(uid); _logSystem.Add(LogType.ForceFeed, LogImpact.Medium, $"{user} is forcing {target} to drink {drinkable}"); - drink.InUse = true; return true; } @@ -368,7 +395,10 @@ namespace Content.Server.Nutrition.EntitySystems /// private void OnForceDrink(EntityUid uid, SharedBodyComponent body, ForceDrinkEvent args) { - args.Drink.InUse = false; + if (args.Drink.Deleted) + return; + + args.Drink.CancelToken = null; var transferAmount = FixedPoint2.Min(args.Drink.TransferAmount, args.DrinkSolution.DrainAvailable); var drained = _solutionContainerSystem.Drain(args.Drink.OwnerUid, args.DrinkSolution, transferAmount); @@ -414,7 +444,7 @@ namespace Content.Server.Nutrition.EntitySystems private void OnForceDrinkCancelled(ForceDrinkCancelledEvent args) { - args.Drink.InUse = false; + args.Drink.CancelToken = null; } } } diff --git a/Content.Server/Nutrition/EntitySystems/FoodSystem.cs b/Content.Server/Nutrition/EntitySystems/FoodSystem.cs index 9876afd2e1..1f872ca1dc 100644 --- a/Content.Server/Nutrition/EntitySystems/FoodSystem.cs +++ b/Content.Server/Nutrition/EntitySystems/FoodSystem.cs @@ -9,7 +9,6 @@ using Content.Server.Popups; using Content.Shared.ActionBlocker; using Content.Shared.Administration.Logs; using Content.Shared.Body.Components; -using Content.Shared.Chemistry.Components; using Content.Shared.Chemistry.Reagent; using Content.Shared.Database; using Content.Shared.FixedPoint; @@ -23,10 +22,10 @@ using Robust.Shared.IoC; using Robust.Shared.Localization; using Robust.Shared.Player; using System.Collections.Generic; -using System.Linq; using Robust.Shared.Utility; using Content.Server.Inventory.Components; using Content.Shared.Inventory; +using Content.Shared.Hands; namespace Content.Server.Nutrition.EntitySystems { @@ -50,12 +49,26 @@ namespace Content.Server.Nutrition.EntitySystems SubscribeLocalEvent(OnUseFoodInHand); SubscribeLocalEvent(OnFeedFood); + SubscribeLocalEvent(OnFoodDeselected); SubscribeLocalEvent(AddEatVerb); SubscribeLocalEvent(OnForceFeed); SubscribeLocalEvent(OnForceFeedCancelled); SubscribeLocalEvent(OnInventoryIngestAttempt); } + /// + /// If the user is currently force feeding someone, this cancels the attempt if they swap hands or otherwise + /// loose the item. Prevents force-feeding dual-wielding. + /// + private void OnFoodDeselected(EntityUid uid, FoodComponent component, HandDeselectedEvent args) + { + if (component.CancelToken != null) + { + component.CancelToken.Cancel(); + component.CancelToken = null; + } + } + /// /// Eat item /// @@ -120,6 +133,14 @@ namespace Content.Server.Nutrition.EntitySystems if (!Resolve(uid, ref component)) return false; + // if currently being used to force-feed, cancel that action. + if (component.CancelToken != null) + { + component.CancelToken.Cancel(); + component.CancelToken = null; + return true; + } + if (uid == userUid || //Suppresses self-eating EntityManager.TryGetComponent(uid, out var mobState) && mobState.IsAlive()) // Suppresses eating alive mobs return false; @@ -214,6 +235,9 @@ namespace Content.Server.Nutrition.EntitySystems private void AddEatVerb(EntityUid uid, FoodComponent component, GetInteractionVerbsEvent ev) { + if (component.CancelToken != null) + return; + if (uid == ev.UserUid || !ev.CanInteract || !ev.CanAccess || @@ -244,6 +268,14 @@ namespace Content.Server.Nutrition.EntitySystems if (!Resolve(uid, ref food)) return false; + // if currently being used to force-feed, cancel that action. + if (food.CancelToken != null) + { + food.CancelToken.Cancel(); + food.CancelToken = null; + return true; + } + if (!EntityManager.HasComponent(targetUid)) return false; @@ -270,7 +302,8 @@ namespace Content.Server.Nutrition.EntitySystems _popupSystem.PopupEntity(Loc.GetString("food-system-force-feed", ("user", userName)), userUid, Filter.Entities(targetUid)); - _doAfterSystem.DoAfter(new DoAfterEventArgs(userUid, food.ForceFeedDelay, target: targetUid) + food.CancelToken = new(); + _doAfterSystem.DoAfter(new DoAfterEventArgs(userUid, food.ForceFeedDelay, food.CancelToken.Token, targetUid) { BreakOnUserMove = true, BreakOnDamage = true, @@ -287,13 +320,15 @@ namespace Content.Server.Nutrition.EntitySystems var edible = EntityManager.GetEntity(uid); _logSystem.Add(LogType.ForceFeed, LogImpact.Medium, $"{user} is forcing {target} to eat {edible}"); - food.InUse = true; return true; } private void OnForceFeed(EntityUid uid, SharedBodyComponent body, ForceFeedEvent args) { - args.Food.InUse = false; + if (args.Food.Deleted) + return; + + args.Food.CancelToken = null; if (!_bodySystem.TryGetComponentsOnMechanisms(uid, out var stomachs, body)) return; @@ -433,7 +468,7 @@ namespace Content.Server.Nutrition.EntitySystems private void OnForceFeedCancelled(ForceFeedCancelledEvent args) { - args.Food.InUse = false; + args.Food.CancelToken = null; } ///