Fix eating and drinking verbs showing up after a short delay and making your verb UI bounce (#38164)

* Fix eating and drinking verbs showing up after a short delay and making your verb UI bounce

* Usings fix

* Usings fix

* Usings fix

* Usings fix

* CVar fix

* Predicted ppups

* Openable predicted popup

* Fix audio prediction
This commit is contained in:
DrSmugleaf
2025-06-09 07:36:04 -07:00
committed by GitHub
parent bd67e82093
commit 7f9b2a0434
40 changed files with 332 additions and 334 deletions

View File

@@ -1,7 +1,7 @@
using Content.Client.Eui;
using Content.Server.Ghost.Roles.Raffles;
using Content.Shared.Eui;
using Content.Shared.Ghost.Roles;
using Content.Shared.Ghost.Roles.Raffles;
using JetBrains.Annotations;
using Robust.Client.Console;
using Robust.Client.Player;

View File

@@ -1,6 +1,4 @@
using System.Linq;
using System.Numerics;
using Content.Server.Ghost.Roles.Raffles;
using System.Numerics;
using Content.Shared.Ghost.Roles.Raffles;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.Controls;

View File

@@ -1,11 +1,10 @@
using Content.Server.Actions;
using Content.Server.Bed.Components;
using Content.Server.Body.Systems;
using Content.Server.Power.EntitySystems;
using Content.Shared.Bed;
using Content.Shared.Bed.Components;
using Content.Shared.Bed.Sleep;
using Content.Shared.Body.Components;
using Content.Shared.Body.Events;
using Content.Shared.Buckle.Components;
using Content.Shared.Damage;
using Content.Shared.Emag.Systems;

View File

@@ -1,8 +1,8 @@
using Content.Server.Body.Components;
using Content.Shared.EntityEffects.Effects;
using Content.Server.Fluids.EntitySystems;
using Content.Server.Popups;
using Content.Shared.Alert;
using Content.Shared.Body.Events;
using Content.Shared.Chemistry.Components;
using Content.Shared.Chemistry.EntitySystems;
using Content.Shared.Chemistry.Reaction;
@@ -10,6 +10,7 @@ using Content.Shared.Chemistry.Reagent;
using Content.Shared.Damage;
using Content.Shared.Damage.Prototypes;
using Content.Shared.Drunk;
using Content.Shared.EntityEffects.Effects;
using Content.Shared.FixedPoint;
using Content.Shared.Forensics;
using Content.Shared.Forensics.Components;

View File

@@ -1,9 +1,12 @@
using System.Numerics;
using Content.Server.Body.Components;
using Content.Server.Ghost;
using Content.Server.Humanoid;
using Content.Shared.Body.Components;
using Content.Shared.Body.Events;
using Content.Shared.Body.Part;
using Content.Shared.Body.Systems;
using Content.Shared.Damage.Components;
using Content.Shared.Humanoid;
using Content.Shared.Mind;
using Content.Shared.Mobs.Systems;
@@ -11,8 +14,6 @@ using Content.Shared.Movement.Events;
using Content.Shared.Movement.Systems;
using Robust.Shared.Audio;
using Robust.Shared.Timing;
using System.Numerics;
using Content.Shared.Damage.Components;
namespace Content.Server.Body.Systems;

View File

@@ -1,9 +1,10 @@
using Content.Server.Body.Components;
using Content.Shared.Chemistry.EntitySystems;
using Content.Shared.Administration.Logs;
using Content.Shared.Body.Events;
using Content.Shared.Body.Organ;
using Content.Shared.Chemistry.Components;
using Content.Shared.Chemistry.Components.SolutionManager;
using Content.Shared.Chemistry.EntitySystems;
using Content.Shared.Chemistry.Reagent;
using Content.Shared.Database;
using Content.Shared.EntityEffects;
@@ -231,29 +232,4 @@ namespace Content.Server.Body.Systems
_solutionContainerSystem.UpdateChemicals(soln.Value);
}
}
// TODO REFACTOR THIS
// This will cause rates to slowly drift over time due to floating point errors.
// Instead, the system that raised this should trigger an update and subscribe to get-modifier events.
[ByRefEvent]
public readonly record struct ApplyMetabolicMultiplierEvent(
EntityUid Uid,
float Multiplier,
bool Apply)
{
/// <summary>
/// The entity whose metabolism is being modified.
/// </summary>
public readonly EntityUid Uid = Uid;
/// <summary>
/// What the metabolism's update rate will be multiplied by.
/// </summary>
public readonly float Multiplier = Multiplier;
/// <summary>
/// If true, apply the multiplier. If false, revert it.
/// </summary>
public readonly bool Apply = Apply;
}
}

View File

@@ -3,18 +3,19 @@ using Content.Server.Atmos.EntitySystems;
using Content.Server.Body.Components;
using Content.Server.Chat.Systems;
using Content.Server.EntityEffects;
using Content.Shared.EntityEffects.EffectConditions;
using Content.Shared.EntityEffects.Effects;
using Content.Shared.Chemistry.EntitySystems;
using Content.Shared.Alert;
using Content.Shared.Atmos;
using Content.Shared.Body.Components;
using Content.Shared.Body.Events;
using Content.Shared.Body.Prototypes;
using Content.Shared.Chemistry.Components;
using Content.Shared.Chemistry.EntitySystems;
using Content.Shared.Chemistry.Reagent;
using Content.Shared.Damage;
using Content.Shared.Database;
using Content.Shared.EntityEffects;
using Content.Shared.EntityEffects.EffectConditions;
using Content.Shared.EntityEffects.Effects;
using Content.Shared.Mobs.Systems;
using JetBrains.Annotations;
using Robust.Shared.Prototypes;

View File

@@ -2,18 +2,17 @@ using System.Linq;
using System.Numerics;
using Content.Server.Administration.Logs;
using Content.Server.Decals;
using Content.Server.Nutrition.EntitySystems;
using Content.Server.Popups;
using Content.Shared.Crayon;
using Content.Shared.Database;
using Content.Shared.Decals;
using Content.Shared.Interaction;
using Content.Shared.Interaction.Events;
using Content.Shared.Nutrition.EntitySystems;
using Robust.Server.GameObjects;
using Robust.Shared.Audio;
using Robust.Shared.Audio.Systems;
using Robust.Shared.GameStates;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
namespace Content.Server.Crayon;

View File

@@ -1,21 +1,20 @@
using Content.Server.Body.Systems;
using Content.Server.Kitchen.Components;
using Content.Server.Nutrition.EntitySystems;
using Content.Shared.Body.Components;
using Content.Shared.Administration.Logs;
using Content.Shared.Body.Components;
using Content.Shared.Database;
using Content.Shared.Interaction;
using Content.Shared.Nutrition.Components;
using Content.Shared.Popups;
using Content.Shared.Storage;
using Content.Shared.Verbs;
using Content.Shared.Destructible;
using Content.Shared.DoAfter;
using Content.Shared.Hands.Components;
using Content.Shared.IdentityManagement;
using Content.Shared.Interaction;
using Content.Shared.Kitchen;
using Content.Shared.Mobs.Components;
using Content.Shared.Mobs.Systems;
using Content.Shared.Nutrition.Components;
using Content.Shared.Nutrition.EntitySystems;
using Content.Shared.Popups;
using Content.Shared.Storage;
using Content.Shared.Verbs;
using Robust.Server.Containers;
using Robust.Server.GameObjects;
using Robust.Shared.Random;

View File

@@ -4,8 +4,10 @@ using Content.Server.Fluids.EntitySystems;
using Content.Server.Forensics;
using Content.Server.Popups;
using Content.Server.Stunnable;
using Content.Shared.Chemistry.EntitySystems;
using Content.Shared.Body.Components;
using Content.Shared.Body.Systems;
using Content.Shared.Chemistry.Components;
using Content.Shared.Chemistry.EntitySystems;
using Content.Shared.Chemistry.Reagent;
using Content.Shared.IdentityManagement;
using Content.Shared.Nutrition.Components;

View File

@@ -1,4 +1,4 @@
using Content.Server.Nutrition.EntitySystems;
using Content.Shared.Nutrition.EntitySystems;
namespace Content.Server.Nutrition.Components;

View File

@@ -1,4 +1,4 @@
using Content.Server.Nutrition.EntitySystems;
using Content.Shared.Nutrition.EntitySystems;
namespace Content.Server.Nutrition.Components;

View File

@@ -1,12 +1,11 @@
using Content.Server.Body.Components;
using Content.Server.Body.Systems;
using Content.Shared.EntityEffects.Effects;
using Content.Server.Fluids.EntitySystems;
using Content.Server.Forensics;
using Content.Server.Inventory;
using Content.Server.Popups;
using Content.Shared.Administration.Logs;
using Content.Shared.Body.Components;
using Content.Shared.Body.Systems;
using Content.Shared.Chemistry;
using Content.Shared.Chemistry.Components;
using Content.Shared.Chemistry.Components.SolutionManager;
@@ -14,7 +13,7 @@ using Content.Shared.Chemistry.EntitySystems;
using Content.Shared.Chemistry.Reagent;
using Content.Shared.Database;
using Content.Shared.DoAfter;
using Content.Shared.EntityEffects;
using Content.Shared.EntityEffects.Effects;
using Content.Shared.FixedPoint;
using Content.Shared.Hands.EntitySystems;
using Content.Shared.IdentityManagement;
@@ -24,7 +23,6 @@ using Content.Shared.Mobs.Systems;
using Content.Shared.Nutrition;
using Content.Shared.Nutrition.Components;
using Content.Shared.Nutrition.EntitySystems;
using Content.Shared.Verbs;
using Robust.Shared.Audio;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Player;
@@ -65,7 +63,6 @@ public sealed class DrinkSystem : SharedDrinkSystem
// run after openable so its always open -> drink
SubscribeLocalEvent<DrinkComponent, UseInHandEvent>(OnUse, before: [typeof(ServerInventorySystem)], after: [typeof(OpenableSystem)]);
SubscribeLocalEvent<DrinkComponent, AfterInteractEvent>(AfterInteract);
SubscribeLocalEvent<DrinkComponent, GetVerbsEvent<AlternativeVerb>>(AddDrinkVerb);
SubscribeLocalEvent<DrinkComponent, ConsumeDoAfterEvent>(OnDoAfter);
}
@@ -157,76 +154,6 @@ public sealed class DrinkSystem : SharedDrinkSystem
_appearance.SetData(uid, FoodVisuals.Visual, drainAvailable.Float(), appearance);
}
/// <summary>
/// Tries to feed the drink item to the target entity
/// </summary>
private bool TryDrink(EntityUid user, EntityUid target, DrinkComponent drink, EntityUid item)
{
if (!HasComp<BodyComponent>(target))
return false;
if (!_body.TryGetBodyOrganEntityComps<StomachComponent>(target, out var stomachs))
return false;
if (_openable.IsClosed(item, user))
return true;
if (!_solutionContainer.TryGetSolution(item, drink.Solution, out _, out var drinkSolution) || drinkSolution.Volume <= 0)
{
if (drink.IgnoreEmpty)
return false;
_popup.PopupEntity(Loc.GetString("drink-component-try-use-drink-is-empty", ("entity", item)), item, user);
return true;
}
if (_food.IsMouthBlocked(target, user))
return true;
if (!_interaction.InRangeUnobstructed(user, item, popup: true))
return true;
var forceDrink = user != target;
if (forceDrink)
{
var userName = Identity.Entity(user, EntityManager);
_popup.PopupEntity(Loc.GetString("drink-component-force-feed", ("user", userName)), user, target);
// logging
_adminLogger.Add(LogType.ForceFeed, LogImpact.High, $"{ToPrettyString(user):user} is forcing {ToPrettyString(target):target} to drink {ToPrettyString(item):drink} {SharedSolutionContainerSystem.ToPrettyString(drinkSolution)}");
}
else
{
// log voluntary drinking
_adminLogger.Add(LogType.Ingestion, LogImpact.Low, $"{ToPrettyString(target):target} is drinking {ToPrettyString(item):drink} {SharedSolutionContainerSystem.ToPrettyString(drinkSolution)}");
}
var flavors = _flavorProfile.GetLocalizedFlavorsMessage(user, drinkSolution);
var doAfterEventArgs = new DoAfterArgs(EntityManager,
user,
forceDrink ? drink.ForceFeedDelay : drink.Delay,
new ConsumeDoAfterEvent(drink.Solution, flavors),
eventTarget: item,
target: target,
used: item)
{
BreakOnHandChange = false,
BreakOnMove = forceDrink,
BreakOnDamage = true,
MovementThreshold = 0.01f,
DistanceThreshold = 1.0f,
// do-after will stop if item is dropped when trying to feed someone else
// or if the item started out in the user's own hands
NeedHand = forceDrink || _hands.IsHolding(user, item),
};
_doAfter.TryStartDoAfter(doAfterEventArgs);
return true;
}
/// <summary>
/// Raised directed at a victim when someone has force fed them a drink.
/// </summary>
@@ -241,7 +168,7 @@ public sealed class DrinkSystem : SharedDrinkSystem
if (args.Used is null || !_solutionContainer.TryGetSolution(args.Used.Value, args.Solution, out var soln, out var solution))
return;
if (_openable.IsClosed(args.Used.Value, args.Target.Value))
if (_openable.IsClosed(args.Used.Value, args.Target.Value, predicted: true))
return;
// TODO this should really be checked every tick.
@@ -330,36 +257,4 @@ public sealed class DrinkSystem : SharedDrinkSystem
if (!forceDrink && solution.Volume > 0)
args.Repeat = true;
}
private void AddDrinkVerb(Entity<DrinkComponent> entity, ref GetVerbsEvent<AlternativeVerb> ev)
{
if (entity.Owner == ev.User ||
!ev.CanInteract ||
!ev.CanAccess ||
!TryComp<BodyComponent>(ev.User, out var body) ||
!_body.TryGetBodyOrganEntityComps<StomachComponent>((ev.User, body), out var stomachs))
return;
// Make sure the solution exists
if (!_solutionContainer.TryGetSolution(entity.Owner, entity.Comp.Solution, out var solution))
return;
// no drinking from living drinks, have to kill them first.
if (_mobState.IsAlive(entity))
return;
var user = ev.User;
AlternativeVerb verb = new()
{
Act = () =>
{
TryDrink(user, user, entity.Comp, entity);
},
Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/VerbIcons/drink.svg.192dpi.png")),
Text = Loc.GetString("drink-system-verb-drink"),
Priority = 2
};
ev.Verbs.Add(verb);
}
}

View File

@@ -1,19 +1,16 @@
using Content.Server.Atmos;
using Content.Server.Atmos.EntitySystems;
using Content.Server.Body.Components;
using Content.Server.DoAfter;
using Content.Server.Explosion.EntitySystems;
using Content.Server.Nutrition.Components;
using Content.Server.Popups;
using Content.Shared.Atmos;
using Content.Shared.Damage;
using Content.Shared.DoAfter;
using Content.Shared.Emag.Components;
using Content.Shared.Emag.Systems;
using Content.Shared.IdentityManagement;
using Content.Shared.Interaction;
using Content.Shared.Nutrition;
using System.Threading;
using Content.Shared.Atmos;
using Content.Shared.Nutrition.EntitySystems;
/// <summary>
/// System for vapes

View File

@@ -1,79 +0,0 @@
using Content.Shared.Containers.ItemSlots;
using Content.Server.Nutrition.Components;
using Content.Server.Popups;
using Content.Shared.Interaction;
using Content.Shared.Nutrition.Components;
using Content.Shared.Nutrition.EntitySystems;
using Content.Shared.Tools.EntitySystems;
using Robust.Shared.Audio;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Random;
namespace Content.Server.Nutrition.EntitySystems
{
/// <summary>
/// Handles usage of the utensils on the food items
/// </summary>
internal sealed class UtensilSystem : SharedUtensilSystem
{
[Dependency] private readonly IRobustRandom _robustRandom = default!;
[Dependency] private readonly FoodSystem _foodSystem = default!;
[Dependency] private readonly PopupSystem _popupSystem = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly SharedInteractionSystem _interactionSystem = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<UtensilComponent, AfterInteractEvent>(OnAfterInteract, after: new[] { typeof(ItemSlotsSystem), typeof(ToolOpenableSystem) });
}
/// <summary>
/// Clicked with utensil
/// </summary>
private void OnAfterInteract(Entity<UtensilComponent> entity, ref AfterInteractEvent ev)
{
if (ev.Handled || ev.Target == null || !ev.CanReach)
return;
var result = TryUseUtensil(ev.User, ev.Target.Value, entity);
ev.Handled = result.Handled;
}
public (bool Success, bool Handled) TryUseUtensil(EntityUid user, EntityUid target, Entity<UtensilComponent> utensil)
{
if (!EntityManager.TryGetComponent(target, out FoodComponent? food))
return (false, false);
//Prevents food usage with a wrong utensil
if ((food.Utensil & utensil.Comp.Types) == 0)
{
_popupSystem.PopupEntity(Loc.GetString("food-system-wrong-utensil", ("food", target), ("utensil", utensil.Owner)), user, user);
return (false, true);
}
if (!_interactionSystem.InRangeUnobstructed(user, target, popup: true))
return (false, true);
return _foodSystem.TryFeed(user, user, target, food);
}
/// <summary>
/// Attempt to break the utensil after interaction.
/// </summary>
/// <param name="uid">Utensil.</param>
/// <param name="userUid">User of the utensil.</param>
public void TryBreak(EntityUid uid, EntityUid userUid, UtensilComponent? component = null)
{
if (!Resolve(uid, ref component))
return;
if (_robustRandom.Prob(component.BreakChance))
{
_audio.PlayPvs(component.BreakSound, userUid, AudioParams.Default.WithVolume(-2f));
EntityManager.DeleteEntity(uid);
}
}
}
}

View File

@@ -1,13 +1,14 @@
using Content.Server.Body.Systems;
using Content.Server.Nutrition.EntitySystems;
using Content.Shared.Body.Systems;
using Content.Shared.Chemistry.Components;
using Content.Shared.Chemistry.Reagent;
using Content.Shared.Nutrition.EntitySystems;
using Content.Shared.Whitelist;
using Robust.Shared.GameStates;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
namespace Content.Server.Body.Components
namespace Content.Shared.Body.Components
{
[RegisterComponent, Access(typeof(StomachSystem), typeof(FoodSystem))]
[RegisterComponent, NetworkedComponent, Access(typeof(StomachSystem), typeof(FoodSystem))]
public sealed partial class StomachComponent : Component
{
/// <summary>
@@ -32,7 +33,7 @@ namespace Content.Server.Body.Components
/// What solution should this stomach push reagents into, on the body?
/// </summary>
[DataField]
public string BodySolutionName = BloodstreamComponent.DefaultChemicalsSolutionName;
public string BodySolutionName = "chemicals";
/// <summary>
/// Time between reagents being ingested and them being

View File

@@ -0,0 +1,26 @@
namespace Content.Shared.Body.Events;
// TODO REFACTOR THIS
// This will cause rates to slowly drift over time due to floating point errors.
// Instead, the system that raised this should trigger an update and subscribe to get-modifier events.
[ByRefEvent]
public readonly record struct ApplyMetabolicMultiplierEvent(
EntityUid Uid,
float Multiplier,
bool Apply)
{
/// <summary>
/// The entity whose metabolism is being modified.
/// </summary>
public readonly EntityUid Uid = Uid;
/// <summary>
/// What the metabolism's update rate will be multiplied by.
/// </summary>
public readonly float Multiplier = Multiplier;
/// <summary>
/// If true, apply the multiplier. If false, revert it.
/// </summary>
public readonly bool Apply = Apply;
}

View File

@@ -1,12 +1,13 @@
using Content.Server.Body.Components;
using Content.Shared.Chemistry.EntitySystems;
using Content.Shared.Body.Components;
using Content.Shared.Body.Events;
using Content.Shared.Body.Organ;
using Content.Shared.Chemistry.Components;
using Content.Shared.Chemistry.Components.SolutionManager;
using Content.Shared.Chemistry.EntitySystems;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
namespace Content.Server.Body.Systems
namespace Content.Shared.Body.Systems
{
public sealed class StomachSystem : EntitySystem
{

View File

@@ -1,5 +1,4 @@
using Content.Shared.ActionBlocker;
using Content.Shared.Burial;
using Content.Shared.Burial.Components;
using Content.Shared.DoAfter;
using Content.Shared.Interaction;
@@ -10,7 +9,7 @@ using Content.Shared.Storage.Components;
using Content.Shared.Storage.EntitySystems;
using Robust.Shared.Audio.Systems;
namespace Content.Server.Burial.Systems;
namespace Content.Shared.Burial;
public sealed class BurialSystem : EntitySystem
{

View File

@@ -38,7 +38,7 @@ public sealed partial class CCVars
/// some food object won't spam a user with flavors.
/// </summary>
public static readonly CVarDef<int>
FlavorLimit = CVarDef.Create("flavor.limit", 10, CVar.SERVERONLY);
FlavorLimit = CVarDef.Create("flavor.limit", 10, CVar.SERVER | CVar.REPLICATED);
public static readonly CVarDef<string> DestinationFile =
CVarDef.Create("autogen.destination_file", "", CVar.SERVER | CVar.SERVERONLY);

View File

@@ -1,8 +1,7 @@
using Content.Shared.Body.Components;
using Content.Shared.EntityEffects;
using Robust.Shared.Prototypes;
namespace Content.Server.EntityEffects.EffectConditions;
namespace Content.Shared.EntityEffects.EffectConditions;
/// <summary>
/// Condition for if the entity is or isn't wearing internals.

View File

@@ -1,4 +1,4 @@
namespace Content.Server.Ghost.Roles.Raffles;
namespace Content.Shared.Ghost.Roles.Raffles;
/// <summary>
/// Defines settings for a ghost role raffle.

View File

@@ -1,5 +1,4 @@
using Content.Server.Ghost.Roles.Raffles;
using Robust.Shared.Prototypes;
using Robust.Shared.Prototypes;
namespace Content.Shared.Ghost.Roles.Raffles;

View File

@@ -4,7 +4,7 @@ using Robust.Shared.Audio;
using Robust.Shared.Containers;
using Robust.Shared.GameStates;
namespace Content.Server.Light.Components;
namespace Content.Shared.Light.Components;
/// <summary>
/// Device that allows user to quikly change bulbs in <see cref="PoweredLightComponent"/>

View File

@@ -1,4 +1,4 @@
namespace Content.Server.Medical.Components;
namespace Content.Shared.Medical.Cryogenics;
/// <summary>
/// Tracking component for an enabled cryo pod (which periodically tries to inject chemicals in the occupant, if one exists)

View File

@@ -1,4 +1,3 @@
using Content.Server.Medical.Components;
using Content.Shared.Administration.Logs;
using Content.Shared.Body.Components;
using Content.Shared.Database;

View File

@@ -1,22 +1,24 @@
namespace Content.Server.Nutrition.Components;
using Robust.Shared.GameStates;
[RegisterComponent]
namespace Content.Shared.Nutrition.Components;
[RegisterComponent, NetworkedComponent]
public sealed partial class FlavorProfileComponent : Component
{
/// <summary>
/// Localized string containing the base flavor of this entity.
/// </summary>
[DataField("flavors")]
[DataField]
public HashSet<string> Flavors { get; private set; } = new();
/// <summary>
/// Reagent IDs to ignore when processing this flavor profile. Defaults to nutriment.
/// </summary>
[DataField("ignoreReagents")]
[DataField]
public HashSet<string> IgnoreReagents { get; private set; } = new()
{
"Nutriment",
"Vitamin",
"Protein"
"Protein",
};
}

View File

@@ -1,11 +1,10 @@
using Content.Server.Body.Components;
using Content.Shared.Nutrition.Components;
using Content.Server.Nutrition.EntitySystems;
using Content.Shared.Body.Components;
using Content.Shared.FixedPoint;
using Content.Shared.Nutrition.EntitySystems;
using Robust.Shared.Audio;
using Robust.Shared.Prototypes;
namespace Content.Server.Nutrition.Components;
namespace Content.Shared.Nutrition.Components;
[RegisterComponent, Access(typeof(FoodSystem), typeof(FoodSequenceSystem))]
public sealed partial class FoodComponent : Component

View File

@@ -1,6 +1,6 @@
using Content.Server.Nutrition.EntitySystems;
using Content.Shared.Nutrition.EntitySystems;
namespace Content.Server.Nutrition.Components;
namespace Content.Shared.Nutrition.Components;
/// <summary>
/// Component that denotes a piece of clothing that blocks the mouth or otherwise prevents eating & drinking.
@@ -9,7 +9,7 @@ namespace Content.Server.Nutrition.Components;
/// In the event that more head-wear & mask functionality is added (like identity systems, or raising/lowering of
/// masks), then this component might become redundant.
/// </remarks>
[RegisterComponent, Access(typeof(FoodSystem), typeof(DrinkSystem), typeof(IngestionBlockerSystem))]
[RegisterComponent, Access(typeof(FoodSystem), typeof(SharedDrinkSystem), typeof(IngestionBlockerSystem))]
public sealed partial class IngestionBlockerComponent : Component
{
/// <summary>

View File

@@ -4,7 +4,7 @@ using Robust.Shared.GameStates;
namespace Content.Shared.Nutrition.Components
{
[RegisterComponent, NetworkedComponent, Access(typeof(SharedUtensilSystem))]
[RegisterComponent, NetworkedComponent, Access(typeof(UtensilSystem))]
public sealed partial class UtensilComponent : Component
{
[DataField("types")]

View File

@@ -1,12 +1,11 @@
using Content.Server.Nutrition.Components;
using System.Linq;
using Content.Shared.CCVar;
using Content.Shared.Chemistry.Components;
using Content.Shared.Nutrition;
using Content.Shared.Nutrition.Components;
using Robust.Shared.Configuration;
using Robust.Shared.Prototypes;
using System.Linq;
namespace Content.Server.Nutrition.EntitySystems;
namespace Content.Shared.Nutrition.EntitySystems;
/// <summary>
/// Deals with flavor profiles when you eat something.

View File

@@ -1,21 +1,17 @@
using System.Numerics;
using System.Text;
using Content.Server.Nutrition.Components;
using Content.Shared.Chemistry.EntitySystems;
using Content.Shared.Interaction;
using Content.Shared.Mobs.Systems;
using Content.Shared.Nutrition;
using Content.Shared.Nutrition.Components;
using Content.Shared.Nutrition.EntitySystems;
using Content.Shared.Nutrition.Prototypes;
using Content.Shared.Popups;
using Content.Shared.Storage.Components;
using Content.Shared.Tag;
using Robust.Server.GameObjects;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
namespace Content.Server.Nutrition.EntitySystems;
namespace Content.Shared.Nutrition.EntitySystems;
public sealed class FoodSequenceSystem : SharedFoodSequenceSystem
{
@@ -26,7 +22,7 @@ public sealed class FoodSequenceSystem : SharedFoodSequenceSystem
[Dependency] private readonly TagSystem _tag = default!;
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly IPrototypeManager _proto = default!;
[Dependency] private readonly TransformSystem _transform = default!;
[Dependency] private readonly SharedTransformSystem _transform = default!;
public override void Initialize()
{
@@ -126,7 +122,7 @@ public sealed class FoodSequenceSystem : SharedFoodSequenceSystem
if (start.Comp.FoodLayers.Count >= start.Comp.MaxLayers && !elementIndexed.Final || start.Comp.Finished)
{
if (user is not null)
_popup.PopupEntity(Loc.GetString("food-sequence-no-space"), start, user.Value);
_popup.PopupClient(Loc.GetString("food-sequence-no-space"), start, user.Value);
return false;
}

View File

@@ -1,16 +1,13 @@
using Content.Server.Body.Components;
using Content.Server.Body.Systems;
using Content.Shared.Chemistry.EntitySystems;
using Content.Server.Inventory;
using Content.Server.Nutrition.Components;
using Content.Shared.Nutrition.Components;
using Content.Server.Popups;
using Content.Server.Stack;
using System.Linq;
using Content.Shared.Administration.Logs;
using Content.Shared.Body.Components;
using Content.Shared.Body.Organ;
using Content.Shared.Body.Systems;
using Content.Shared.Chemistry;
using Content.Shared.Chemistry.EntitySystems;
using Content.Shared.Containers.ItemSlots;
using Content.Shared.Database;
using Content.Shared.Destructible;
using Content.Shared.DoAfter;
using Content.Shared.FixedPoint;
using Content.Shared.Hands.Components;
@@ -21,42 +18,38 @@ using Content.Shared.Interaction.Components;
using Content.Shared.Interaction.Events;
using Content.Shared.Inventory;
using Content.Shared.Mobs.Systems;
using Content.Shared.Nutrition;
using Content.Shared.Nutrition.EntitySystems;
using Content.Shared.Nutrition.Components;
using Content.Shared.Popups;
using Content.Shared.Stacks;
using Content.Shared.Storage;
using Content.Shared.Verbs;
using Content.Shared.Whitelist;
using Robust.Shared.Audio;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Utility;
using System.Linq;
using Content.Shared.Containers.ItemSlots;
using Robust.Server.GameObjects;
using Content.Shared.Whitelist;
using Content.Shared.Destructible;
namespace Content.Server.Nutrition.EntitySystems;
namespace Content.Shared.Nutrition.EntitySystems;
/// <summary>
/// Handles feeding attempts both on yourself and on the target.
/// </summary>
public sealed class FoodSystem : EntitySystem
{
[Dependency] private readonly BodySystem _body = default!;
[Dependency] private readonly SharedBodySystem _body = default!;
[Dependency] private readonly FlavorProfileSystem _flavorProfile = default!;
[Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
[Dependency] private readonly InventorySystem _inventory = default!;
[Dependency] private readonly MobStateSystem _mobState = default!;
[Dependency] private readonly OpenableSystem _openable = default!;
[Dependency] private readonly PopupSystem _popup = default!;
[Dependency] private readonly SharedPopupSystem _popup = default!;
[Dependency] private readonly ReactiveSystem _reaction = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly SharedDoAfterSystem _doAfter = default!;
[Dependency] private readonly SharedHandsSystem _hands = default!;
[Dependency] private readonly SharedInteractionSystem _interaction = default!;
[Dependency] private readonly SharedSolutionContainerSystem _solutionContainer = default!;
[Dependency] private readonly TransformSystem _transform = default!;
[Dependency] private readonly StackSystem _stack = default!;
[Dependency] private readonly SharedTransformSystem _transform = default!;
[Dependency] private readonly SharedStackSystem _stack = default!;
[Dependency] private readonly StomachSystem _stomach = default!;
[Dependency] private readonly UtensilSystem _utensil = default!;
[Dependency] private readonly EntityWhitelistSystem _whitelistSystem = default!;
@@ -69,7 +62,7 @@ public sealed class FoodSystem : EntitySystem
// TODO add InteractNoHandEvent for entities like mice.
// run after openable for wrapped/peelable foods
SubscribeLocalEvent<FoodComponent, UseInHandEvent>(OnUseFoodInHand, after: new[] { typeof(OpenableSystem), typeof(ServerInventorySystem) });
SubscribeLocalEvent<FoodComponent, UseInHandEvent>(OnUseFoodInHand, after: new[] { typeof(OpenableSystem), typeof(InventorySystem) });
SubscribeLocalEvent<FoodComponent, AfterInteractEvent>(OnFeedFood);
SubscribeLocalEvent<FoodComponent, GetVerbsEvent<AlternativeVerb>>(AddEatVerb);
SubscribeLocalEvent<FoodComponent, ConsumeDoAfterEvent>(OnDoAfter);
@@ -116,7 +109,7 @@ public sealed class FoodSystem : EntitySystem
if (HasComp<UnremoveableComponent>(food))
return (false, false);
if (_openable.IsClosed(food, user))
if (_openable.IsClosed(food, user, predicted: true))
return (false, true);
if (!_solutionContainer.TryGetSolution(food, foodComp.Solution, out _, out var foodSolution))
@@ -135,7 +128,7 @@ public sealed class FoodSystem : EntitySystem
// Check for used storage on the food item
if (TryComp<StorageComponent>(food, out var storageState) && storageState.Container.ContainedEntities.Any())
{
_popup.PopupEntity(Loc.GetString("food-has-used-storage", ("food", food)), user, user);
_popup.PopupClient(Loc.GetString("food-has-used-storage", ("food", food)), user, user);
return (false, true);
}
@@ -144,7 +137,7 @@ public sealed class FoodSystem : EntitySystem
{
if (itemSlots.Slots.Any(slot => slot.Value.HasItem))
{
_popup.PopupEntity(Loc.GetString("food-has-used-storage", ("food", food)), user, user);
_popup.PopupClient(Loc.GetString("food-has-used-storage", ("food", food)), user, user);
return (false, true);
}
}
@@ -153,7 +146,7 @@ public sealed class FoodSystem : EntitySystem
if (GetUsesRemaining(food, foodComp) <= 0)
{
_popup.PopupEntity(Loc.GetString("food-system-try-use-food-is-empty", ("entity", food)), user, user);
_popup.PopupClient(Loc.GetString("food-system-try-use-food-is-empty", ("entity", food)), user, user);
DeleteAndSpawnTrash(foodComp, food, user);
return (false, true);
}
@@ -171,7 +164,7 @@ public sealed class FoodSystem : EntitySystem
if (!_transform.GetMapCoordinates(user).InRange(_transform.GetMapCoordinates(target), MaxFeedDistance))
{
var message = Loc.GetString("interaction-system-user-interaction-cannot-reach");
_popup.PopupEntity(message, user, user);
_popup.PopupClient(message, user, user);
return (false, true);
}
@@ -268,7 +261,7 @@ public sealed class FoodSystem : EntitySystem
if (stomachToUse == null)
{
_solutionContainer.TryAddSolution(soln.Value, split);
_popup.PopupEntity(forceFeed ? Loc.GetString("food-system-you-cannot-eat-any-more-other", ("target", args.Target.Value)) : Loc.GetString("food-system-you-cannot-eat-any-more"), args.Target.Value, args.User);
_popup.PopupClient(forceFeed ? Loc.GetString("food-system-you-cannot-eat-any-more-other", ("target", args.Target.Value)) : Loc.GetString("food-system-you-cannot-eat-any-more"), args.Target.Value, args.User);
return;
}
@@ -283,20 +276,20 @@ public sealed class FoodSystem : EntitySystem
var userName = Identity.Entity(args.User, EntityManager);
_popup.PopupEntity(Loc.GetString("food-system-force-feed-success", ("user", userName), ("flavors", flavors)), entity.Owner, entity.Owner);
_popup.PopupEntity(Loc.GetString("food-system-force-feed-success-user", ("target", targetName)), args.User, args.User);
_popup.PopupClient(Loc.GetString("food-system-force-feed-success-user", ("target", targetName)), args.User, args.User);
// log successful force feed
_adminLogger.Add(LogType.ForceFeed, LogImpact.Medium, $"{ToPrettyString(entity.Owner):user} forced {ToPrettyString(args.User):target} to eat {ToPrettyString(entity.Owner):food}");
}
else
{
_popup.PopupEntity(Loc.GetString(entity.Comp.EatMessage, ("food", entity.Owner), ("flavors", flavors)), args.User, args.User);
_popup.PopupClient(Loc.GetString(entity.Comp.EatMessage, ("food", entity.Owner), ("flavors", flavors)), args.User, args.User);
// log successful voluntary eating
_adminLogger.Add(LogType.Ingestion, LogImpact.Low, $"{ToPrettyString(args.User):target} ate {ToPrettyString(entity.Owner):food}");
}
_audio.PlayPvs(entity.Comp.UseSound, args.Target.Value, AudioParams.Default.WithVolume(-1f).WithVariation(0.20f));
_audio.PlayPredicted(entity.Comp.UseSound, args.Target.Value, args.User, AudioParams.Default.WithVolume(-1f).WithVariation(0.20f));
// Try to break all used utensils
foreach (var utensil in utensils)
@@ -484,7 +477,7 @@ public sealed class FoodSystem : EntitySystem
// If "required" field is set, try to block eating without proper utensils used
if (component.UtensilRequired && (usedTypes & component.Utensil) != component.Utensil)
{
_popup.PopupEntity(Loc.GetString("food-you-need-to-hold-utensil", ("utensil", component.Utensil ^ usedTypes)), user, user);
_popup.PopupClient(Loc.GetString("food-you-need-to-hold-utensil", ("utensil", component.Utensil ^ usedTypes)), user, user);
return false;
}
@@ -533,7 +526,7 @@ public sealed class FoodSystem : EntitySystem
RaiseLocalEvent(uid, attempt, false);
if (attempt.Cancelled && attempt.Blocker != null && popupUid != null)
{
_popup.PopupEntity(Loc.GetString("food-system-remove-mask", ("entity", attempt.Blocker.Value)),
_popup.PopupClient(Loc.GetString("food-system-remove-mask", ("entity", attempt.Blocker.Value)),
uid, popupUid.Value);
}

View File

@@ -1,7 +1,7 @@
using Content.Server.Nutrition.Components;
using Content.Shared.Clothing;
using Content.Shared.Clothing;
using Content.Shared.Nutrition.Components;
namespace Content.Server.Nutrition.EntitySystems;
namespace Content.Shared.Nutrition.EntitySystems;
public sealed class IngestionBlockerSystem : EntitySystem
{

View File

@@ -1,8 +1,8 @@
using Content.Shared.Chemistry.EntitySystems;
using Content.Shared.Examine;
using Content.Shared.Lock;
using Content.Shared.Interaction;
using Content.Shared.Interaction.Events;
using Content.Shared.Lock;
using Content.Shared.Nutrition.Components;
using Content.Shared.Popups;
using Content.Shared.Verbs;
@@ -166,7 +166,7 @@ public sealed partial class OpenableSystem : EntitySystem
/// Drinks that don't have OpenableComponent are automatically open, so it returns false.
/// If user is not null a popup will be shown to them.
/// </summary>
public bool IsClosed(EntityUid uid, EntityUid? user = null, OpenableComponent? comp = null)
public bool IsClosed(EntityUid uid, EntityUid? user = null, OpenableComponent? comp = null, bool predicted = false)
{
if (!Resolve(uid, ref comp, false))
return false;
@@ -175,7 +175,12 @@ public sealed partial class OpenableSystem : EntitySystem
return false;
if (user != null)
_popup.PopupEntity(Loc.GetString(comp.ClosedPopup, ("owner", uid)), user.Value, user.Value);
{
if (predicted)
_popup.PopupClient(Loc.GetString(comp.ClosedPopup, ("owner", uid)), user.Value, user.Value);
else
_popup.PopupEntity(Loc.GetString(comp.ClosedPopup, ("owner", uid)), user.Value, user.Value);
}
return true;
}

View File

@@ -1,15 +1,36 @@
using Content.Shared.Administration.Logs;
using Content.Shared.Body.Components;
using Content.Shared.Body.Systems;
using Content.Shared.Chemistry.Components.SolutionManager;
using Content.Shared.Chemistry.EntitySystems;
using Content.Shared.Database;
using Content.Shared.DoAfter;
using Content.Shared.Examine;
using Content.Shared.FixedPoint;
using Content.Shared.Hands.EntitySystems;
using Content.Shared.IdentityManagement;
using Content.Shared.Interaction;
using Content.Shared.Mobs.Systems;
using Content.Shared.Nutrition.Components;
using Content.Shared.Popups;
using Content.Shared.Verbs;
using Robust.Shared.Utility;
namespace Content.Shared.Nutrition.EntitySystems;
public abstract partial class SharedDrinkSystem : EntitySystem
{
[Dependency] private readonly SharedSolutionContainerSystem _solutionContainer = default!;
[Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
[Dependency] private readonly SharedBodySystem _body = default!;
[Dependency] private readonly SharedDoAfterSystem _doAfter = default!;
[Dependency] private readonly FlavorProfileSystem _flavorProfile = default!;
[Dependency] private readonly FoodSystem _food = default!;
[Dependency] private readonly SharedHandsSystem _hands = default!;
[Dependency] private readonly SharedInteractionSystem _interaction = default!;
[Dependency] private readonly MobStateSystem _mobState = default!;
[Dependency] private readonly OpenableSystem _openable = default!;
[Dependency] private readonly SharedPopupSystem _popup = default!;
[Dependency] private readonly SharedSolutionContainerSystem _solutionContainer = default!;
public override void Initialize()
{
@@ -17,6 +38,7 @@ public abstract partial class SharedDrinkSystem : EntitySystem
SubscribeLocalEvent<DrinkComponent, AttemptShakeEvent>(OnAttemptShake);
SubscribeLocalEvent<DrinkComponent, ExaminedEvent>(OnExamined);
SubscribeLocalEvent<DrinkComponent, GetVerbsEvent<AlternativeVerb>>(AddDrinkVerb);
}
protected void OnAttemptShake(Entity<DrinkComponent> entity, ref AttemptShakeEvent args)
@@ -28,7 +50,7 @@ public abstract partial class SharedDrinkSystem : EntitySystem
protected void OnExamined(Entity<DrinkComponent> entity, ref ExaminedEvent args)
{
TryComp<OpenableComponent>(entity, out var openable);
if (_openable.IsClosed(entity.Owner, null, openable) || !args.IsInDetailsRange || !entity.Comp.Examinable)
if (_openable.IsClosed(entity.Owner, null, openable, true) || !args.IsInDetailsRange || !entity.Comp.Examinable)
return;
var empty = IsEmpty(entity, entity.Comp);
@@ -57,6 +79,38 @@ public abstract partial class SharedDrinkSystem : EntitySystem
}
}
private void AddDrinkVerb(Entity<DrinkComponent> entity, ref GetVerbsEvent<AlternativeVerb> ev)
{
if (entity.Owner == ev.User ||
!ev.CanInteract ||
!ev.CanAccess ||
!TryComp<BodyComponent>(ev.User, out var body) ||
!_body.TryGetBodyOrganEntityComps<StomachComponent>((ev.User, body), out var stomachs))
return;
// Make sure the solution exists
if (!_solutionContainer.TryGetSolution(entity.Owner, entity.Comp.Solution, out var solution))
return;
// no drinking from living drinks, have to kill them first.
if (_mobState.IsAlive(entity))
return;
var user = ev.User;
AlternativeVerb verb = new()
{
Act = () =>
{
TryDrink(user, user, entity.Comp, entity);
},
Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/VerbIcons/drink.svg.192dpi.png")),
Text = Loc.GetString("drink-system-verb-drink"),
Priority = 2
};
ev.Verbs.Add(verb);
}
protected FixedPoint2 DrinkVolume(EntityUid uid, DrinkComponent? component = null)
{
if (!Resolve(uid, ref component))
@@ -87,4 +141,74 @@ public abstract partial class SharedDrinkSystem : EntitySystem
return remainingString;
}
/// <summary>
/// Tries to feed the drink item to the target entity
/// </summary>
protected bool TryDrink(EntityUid user, EntityUid target, DrinkComponent drink, EntityUid item)
{
if (!HasComp<BodyComponent>(target))
return false;
if (!_body.TryGetBodyOrganEntityComps<StomachComponent>(target, out var stomachs))
return false;
if (_openable.IsClosed(item, user, predicted: true))
return true;
if (!_solutionContainer.TryGetSolution(item, drink.Solution, out _, out var drinkSolution) || drinkSolution.Volume <= 0)
{
if (drink.IgnoreEmpty)
return false;
_popup.PopupClient(Loc.GetString("drink-component-try-use-drink-is-empty", ("entity", item)), item, user);
return true;
}
if (_food.IsMouthBlocked(target, user))
return true;
if (!_interaction.InRangeUnobstructed(user, item, popup: true))
return true;
var forceDrink = user != target;
if (forceDrink)
{
var userName = Identity.Entity(user, EntityManager);
_popup.PopupEntity(Loc.GetString("drink-component-force-feed", ("user", userName)), user, target);
// logging
_adminLogger.Add(LogType.ForceFeed, LogImpact.High, $"{ToPrettyString(user):user} is forcing {ToPrettyString(target):target} to drink {ToPrettyString(item):drink} {SharedSolutionContainerSystem.ToPrettyString(drinkSolution)}");
}
else
{
// log voluntary drinking
_adminLogger.Add(LogType.Ingestion, LogImpact.Low, $"{ToPrettyString(target):target} is drinking {ToPrettyString(item):drink} {SharedSolutionContainerSystem.ToPrettyString(drinkSolution)}");
}
var flavors = _flavorProfile.GetLocalizedFlavorsMessage(user, drinkSolution);
var doAfterEventArgs = new DoAfterArgs(EntityManager,
user,
forceDrink ? drink.ForceFeedDelay : drink.Delay,
new ConsumeDoAfterEvent(drink.Solution, flavors),
eventTarget: item,
target: target,
used: item)
{
BreakOnHandChange = false,
BreakOnMove = forceDrink,
BreakOnDamage = true,
MovementThreshold = 0.01f,
DistanceThreshold = 1.0f,
// do-after will stop if item is dropped when trying to feed someone else
// or if the item started out in the user's own hands
NeedHand = forceDrink || _hands.IsHolding(user, item),
};
_doAfter.TryStartDoAfter(doAfterEventArgs);
return true;
}
}

View File

@@ -1,5 +0,0 @@
namespace Content.Shared.Nutrition.EntitySystems;
public abstract class SharedUtensilSystem : EntitySystem
{
}

View File

@@ -0,0 +1,73 @@
using Content.Shared.Containers.ItemSlots;
using Content.Shared.Interaction;
using Content.Shared.Nutrition.Components;
using Content.Shared.Popups;
using Content.Shared.Tools.EntitySystems;
using Robust.Shared.Audio;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Random;
namespace Content.Shared.Nutrition.EntitySystems;
public sealed class UtensilSystem : EntitySystem
{
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly FoodSystem _foodSystem = default!;
[Dependency] private readonly SharedInteractionSystem _interactionSystem = default!;
[Dependency] private readonly SharedPopupSystem _popupSystem = default!;
[Dependency] private readonly IRobustRandom _robustRandom = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<UtensilComponent, AfterInteractEvent>(OnAfterInteract, after: new[] { typeof(ItemSlotsSystem), typeof(ToolOpenableSystem) });
}
/// <summary>
/// Clicked with utensil
/// </summary>
private void OnAfterInteract(Entity<UtensilComponent> entity, ref AfterInteractEvent ev)
{
if (ev.Handled || ev.Target == null || !ev.CanReach)
return;
var result = TryUseUtensil(ev.User, ev.Target.Value, entity);
ev.Handled = result.Handled;
}
public (bool Success, bool Handled) TryUseUtensil(EntityUid user, EntityUid target, Entity<UtensilComponent> utensil)
{
if (!EntityManager.TryGetComponent(target, out FoodComponent? food))
return (false, false);
//Prevents food usage with a wrong utensil
if ((food.Utensil & utensil.Comp.Types) == 0)
{
_popupSystem.PopupEntity(Loc.GetString("food-system-wrong-utensil", ("food", target), ("utensil", utensil.Owner)), user, user);
return (false, true);
}
if (!_interactionSystem.InRangeUnobstructed(user, target, popup: true))
return (false, true);
return _foodSystem.TryFeed(user, user, target, food);
}
/// <summary>
/// Attempt to break the utensil after interaction.
/// </summary>
/// <param name="uid">Utensil.</param>
/// <param name="userUid">User of the utensil.</param>
public void TryBreak(EntityUid uid, EntityUid userUid, UtensilComponent? component = null)
{
if (!Resolve(uid, ref component))
return;
if (_robustRandom.Prob(component.BreakChance))
{
_audio.PlayPredicted(component.BreakSound, userUid, userUid, AudioParams.Default.WithVolume(-2f));
EntityManager.DeleteEntity(uid);
}
}
}

View File

@@ -1,6 +1,6 @@
using Content.Shared.Inventory;
namespace Content.Server.Storage.Components;
namespace Content.Shared.Storage.Components;
/// <summary>
/// Applies an ongoing pickup area around the attached entity.

View File

@@ -1,7 +1,6 @@
using Content.Server.Storage.Components;
using Content.Shared.Inventory;
using Content.Shared.Storage.Components;
using Content.Shared.Whitelist;
using Robust.Shared.Map;
using Robust.Shared.Physics.Components;
using Robust.Shared.Timing;