diff --git a/Content.Client/Nutrition/EntitySystems/OpenableSystem.cs b/Content.Client/Nutrition/EntitySystems/OpenableSystem.cs deleted file mode 100644 index f8c3f7c447..0000000000 --- a/Content.Client/Nutrition/EntitySystems/OpenableSystem.cs +++ /dev/null @@ -1,7 +0,0 @@ -using Content.Shared.Nutrition.EntitySystems; - -namespace Content.Client.Nutrition.EntitySystems; - -public sealed class OpenableSystem : SharedOpenableSystem -{ -} diff --git a/Content.Server/Chemistry/EntitySystems/ReagentDispenserSystem.cs b/Content.Server/Chemistry/EntitySystems/ReagentDispenserSystem.cs index a8583e6bcb..d6433da56a 100644 --- a/Content.Server/Chemistry/EntitySystems/ReagentDispenserSystem.cs +++ b/Content.Server/Chemistry/EntitySystems/ReagentDispenserSystem.cs @@ -1,11 +1,12 @@ using Content.Server.Chemistry.Components; using Content.Server.Chemistry.Containers.EntitySystems; -using Content.Server.Nutrition.EntitySystems; +using Content.Server.Nutrition.Components; using Content.Shared.Chemistry; using Content.Shared.Chemistry.Dispenser; using Content.Shared.Chemistry.EntitySystems; using Content.Shared.Containers.ItemSlots; using Content.Shared.FixedPoint; +using Content.Shared.Nutrition.EntitySystems; using JetBrains.Annotations; using Robust.Server.Audio; using Robust.Server.GameObjects; diff --git a/Content.Server/Chemistry/EntitySystems/SolutionTransferSystem.cs b/Content.Server/Chemistry/EntitySystems/SolutionTransferSystem.cs deleted file mode 100644 index 1ed5cec8dd..0000000000 --- a/Content.Server/Chemistry/EntitySystems/SolutionTransferSystem.cs +++ /dev/null @@ -1,234 +0,0 @@ -using Content.Server.Administration.Logs; -using Content.Server.Chemistry.Containers.EntitySystems; -using Content.Shared.Chemistry; -using Content.Shared.Chemistry.Components; -using Content.Shared.Database; -using Content.Shared.FixedPoint; -using Content.Shared.Interaction; -using Content.Shared.Popups; -using Content.Shared.Verbs; -using JetBrains.Annotations; -using Robust.Server.GameObjects; -using Robust.Shared.Player; - -namespace Content.Server.Chemistry.EntitySystems -{ - [UsedImplicitly] - public sealed class SolutionTransferSystem : EntitySystem - { - [Dependency] private readonly SharedPopupSystem _popupSystem = default!; - [Dependency] private readonly SolutionContainerSystem _solutionContainerSystem = default!; - [Dependency] private readonly UserInterfaceSystem _userInterfaceSystem = default!; - [Dependency] private readonly IAdminLogManager _adminLogger = default!; - - /// - /// Default transfer amounts for the set-transfer verb. - /// - public static readonly List DefaultTransferAmounts = new() { 1, 5, 10, 25, 50, 100, 250, 500, 1000 }; - - public override void Initialize() - { - base.Initialize(); - - SubscribeLocalEvent>(AddSetTransferVerbs); - SubscribeLocalEvent(OnAfterInteract); - SubscribeLocalEvent(OnTransferAmountSetValueMessage); - } - - private void OnTransferAmountSetValueMessage(Entity entity, ref TransferAmountSetValueMessage message) - { - var newTransferAmount = FixedPoint2.Clamp(message.Value, entity.Comp.MinimumTransferAmount, entity.Comp.MaximumTransferAmount); - entity.Comp.TransferAmount = newTransferAmount; - - if (message.Session.AttachedEntity is { Valid: true } user) - _popupSystem.PopupEntity(Loc.GetString("comp-solution-transfer-set-amount", ("amount", newTransferAmount)), entity.Owner, user); - } - - private void AddSetTransferVerbs(Entity entity, ref GetVerbsEvent args) - { - var (uid, component) = entity; - - if (!args.CanAccess || !args.CanInteract || !component.CanChangeTransferAmount || args.Hands == null) - return; - - if (!EntityManager.TryGetComponent(args.User, out ActorComponent? actor)) - return; - - // Custom transfer verb - AlternativeVerb custom = new(); - custom.Text = Loc.GetString("comp-solution-transfer-verb-custom-amount"); - custom.Category = VerbCategory.SetTransferAmount; - custom.Act = () => _userInterfaceSystem.TryOpen(uid, TransferAmountUiKey.Key, actor.PlayerSession); - custom.Priority = 1; - args.Verbs.Add(custom); - - // Add specific transfer verbs according to the container's size - var priority = 0; - var user = args.User; - foreach (var amount in DefaultTransferAmounts) - { - if (amount < component.MinimumTransferAmount.Int() || amount > component.MaximumTransferAmount.Int()) - continue; - - AlternativeVerb verb = new(); - verb.Text = Loc.GetString("comp-solution-transfer-verb-amount", ("amount", amount)); - verb.Category = VerbCategory.SetTransferAmount; - verb.Act = () => - { - component.TransferAmount = FixedPoint2.New(amount); - _popupSystem.PopupEntity(Loc.GetString("comp-solution-transfer-set-amount", ("amount", amount)), uid, user); - }; - - // we want to sort by size, not alphabetically by the verb text. - verb.Priority = priority; - priority--; - - args.Verbs.Add(verb); - } - } - - private void OnAfterInteract(Entity entity, ref AfterInteractEvent args) - { - if (!args.CanReach || args.Target == null) - return; - - var target = args.Target!.Value; - var (uid, component) = entity; - - //Special case for reagent tanks, because normally clicking another container will give solution, not take it. - if (component.CanReceive && !EntityManager.HasComponent(target) // target must not be refillable (e.g. Reagent Tanks) - && _solutionContainerSystem.TryGetDrainableSolution(target, out var targetSoln, out _) // target must be drainable - && EntityManager.TryGetComponent(uid, out RefillableSolutionComponent? refillComp) - && _solutionContainerSystem.TryGetRefillableSolution((uid, refillComp, null), out var ownerSoln, out var ownerRefill)) - - { - - var transferAmount = component.TransferAmount; // This is the player-configurable transfer amount of "uid," not the target reagent tank. - - if (EntityManager.TryGetComponent(uid, out RefillableSolutionComponent? refill) && refill.MaxRefill != null) // uid is the entity receiving solution from target. - { - transferAmount = FixedPoint2.Min(transferAmount, (FixedPoint2) refill.MaxRefill); // if the receiver has a smaller transfer limit, use that instead - } - - var transferred = Transfer(args.User, target, targetSoln.Value, uid, ownerSoln.Value, transferAmount); - if (transferred > 0) - { - var toTheBrim = ownerRefill.AvailableVolume == 0; - var msg = toTheBrim - ? "comp-solution-transfer-fill-fully" - : "comp-solution-transfer-fill-normal"; - - _popupSystem.PopupEntity(Loc.GetString(msg, ("owner", args.Target), ("amount", transferred), ("target", uid)), uid, args.User); - - args.Handled = true; - return; - } - } - - // if target is refillable, and owner is drainable - if (component.CanSend && _solutionContainerSystem.TryGetRefillableSolution(target, out targetSoln, out var targetRefill) - && _solutionContainerSystem.TryGetDrainableSolution(uid, out ownerSoln, out var ownerDrain)) - { - var transferAmount = component.TransferAmount; - - if (EntityManager.TryGetComponent(target, out RefillableSolutionComponent? refill) && refill.MaxRefill != null) - { - transferAmount = FixedPoint2.Min(transferAmount, (FixedPoint2) refill.MaxRefill); - } - - var transferred = Transfer(args.User, uid, ownerSoln.Value, target, targetSoln.Value, transferAmount); - - if (transferred > 0) - { - var message = Loc.GetString("comp-solution-transfer-transfer-solution", ("amount", transferred), ("target", target)); - _popupSystem.PopupEntity(message, uid, args.User); - - args.Handled = true; - } - } - } - - /// - /// Transfer from a solution to another. - /// - /// The actual amount transferred. - public FixedPoint2 Transfer(EntityUid user, - EntityUid sourceEntity, - Entity source, - EntityUid targetEntity, - Entity target, - FixedPoint2 amount) - { - var transferAttempt = new SolutionTransferAttemptEvent(sourceEntity, targetEntity); - - // Check if the source is cancelling the transfer - RaiseLocalEvent(sourceEntity, transferAttempt, broadcast: true); - if (transferAttempt.Cancelled) - { - _popupSystem.PopupEntity(transferAttempt.CancelReason!, sourceEntity, user); - return FixedPoint2.Zero; - } - - var sourceSolution = source.Comp.Solution; - if (sourceSolution.Volume == 0) - { - _popupSystem.PopupEntity(Loc.GetString("comp-solution-transfer-is-empty", ("target", sourceEntity)), sourceEntity, user); - return FixedPoint2.Zero; - } - - // Check if the target is cancelling the transfer - RaiseLocalEvent(targetEntity, transferAttempt, broadcast: true); - if (transferAttempt.Cancelled) - { - _popupSystem.PopupEntity(transferAttempt.CancelReason!, sourceEntity, user); - return FixedPoint2.Zero; - } - - var targetSolution = target.Comp.Solution; - if (targetSolution.AvailableVolume == 0) - { - _popupSystem.PopupEntity(Loc.GetString("comp-solution-transfer-is-full", ("target", targetEntity)), targetEntity, user); - return FixedPoint2.Zero; - } - - var actualAmount = FixedPoint2.Min(amount, FixedPoint2.Min(sourceSolution.Volume, targetSolution.AvailableVolume)); - - var solution = _solutionContainerSystem.Drain(sourceEntity, source, actualAmount); - _solutionContainerSystem.Refill(targetEntity, target, solution); - - _adminLogger.Add(LogType.Action, LogImpact.Medium, - $"{EntityManager.ToPrettyString(user):player} transferred {string.Join(", ", solution.Contents)} to {EntityManager.ToPrettyString(targetEntity):entity}, which now contains {SolutionContainerSystem.ToPrettyString(targetSolution)}"); - - return actualAmount; - } - } - - /// - /// Raised when attempting to transfer from one solution to another. - /// - public sealed class SolutionTransferAttemptEvent : CancellableEntityEventArgs - { - public SolutionTransferAttemptEvent(EntityUid from, EntityUid to) - { - From = from; - To = to; - } - - public EntityUid From { get; } - public EntityUid To { get; } - - /// - /// Why the transfer has been cancelled. - /// - public string? CancelReason { get; private set; } - - /// - /// Cancels the transfer. - /// - public void Cancel(string reason) - { - base.Cancel(); - CancelReason = reason; - } - } -} diff --git a/Content.Server/Destructible/Thresholds/Behaviors/OpenBehavior.cs b/Content.Server/Destructible/Thresholds/Behaviors/OpenBehavior.cs index f01e4f7048..7ab1fe11b0 100644 --- a/Content.Server/Destructible/Thresholds/Behaviors/OpenBehavior.cs +++ b/Content.Server/Destructible/Thresholds/Behaviors/OpenBehavior.cs @@ -1,4 +1,4 @@ -using Content.Server.Nutrition.EntitySystems; +using Content.Shared.Nutrition.EntitySystems; namespace Content.Server.Destructible.Thresholds.Behaviors; diff --git a/Content.Server/Extinguisher/FireExtinguisherSystem.cs b/Content.Server/Extinguisher/FireExtinguisherSystem.cs index dfecd72398..b33a1af157 100644 --- a/Content.Server/Extinguisher/FireExtinguisherSystem.cs +++ b/Content.Server/Extinguisher/FireExtinguisherSystem.cs @@ -73,6 +73,7 @@ public sealed class FireExtinguisherSystem : EntitySystem args.Handled = true; + // TODO: why is this copy paste shit here just have fire extinguisher cancel transfer when safety is on var transfer = containerSolution.AvailableVolume; if (TryComp(entity.Owner, out var solTrans)) { diff --git a/Content.Server/Fluids/EntitySystems/PuddleSystem.Spillable.cs b/Content.Server/Fluids/EntitySystems/PuddleSystem.Spillable.cs index a365b8d0a4..bd7c55e85e 100644 --- a/Content.Server/Fluids/EntitySystems/PuddleSystem.Spillable.cs +++ b/Content.Server/Fluids/EntitySystems/PuddleSystem.Spillable.cs @@ -1,5 +1,5 @@ using Content.Server.Chemistry.Containers.EntitySystems; -using Content.Server.Nutrition.EntitySystems; +using Content.Server.Fluids.Components; using Content.Shared.Chemistry.Components; using Content.Shared.Chemistry.EntitySystems; using Content.Shared.Chemistry.Reaction; @@ -11,6 +11,7 @@ using Content.Shared.FixedPoint; using Content.Shared.Fluids.Components; using Content.Shared.IdentityManagement; using Content.Shared.Inventory.Events; +using Content.Shared.Nutrition.EntitySystems; using Content.Shared.Popups; using Content.Shared.Spillable; using Content.Shared.Throwing; diff --git a/Content.Server/Glue/GlueSystem.cs b/Content.Server/Glue/GlueSystem.cs index 44ff4e5459..ff53ef91ca 100644 --- a/Content.Server/Glue/GlueSystem.cs +++ b/Content.Server/Glue/GlueSystem.cs @@ -1,12 +1,12 @@ using Content.Server.Administration.Logs; using Content.Server.Chemistry.Containers.EntitySystems; -using Content.Server.Nutrition.EntitySystems; using Content.Shared.Database; using Content.Shared.Glue; using Content.Shared.Hands; using Content.Shared.Interaction; using Content.Shared.Interaction.Components; using Content.Shared.Item; +using Content.Shared.Nutrition.EntitySystems; using Content.Shared.Popups; using Content.Shared.Verbs; using Robust.Shared.Audio.Systems; diff --git a/Content.Server/Lube/LubeSystem.cs b/Content.Server/Lube/LubeSystem.cs index 5285cb389c..06d6456a57 100644 --- a/Content.Server/Lube/LubeSystem.cs +++ b/Content.Server/Lube/LubeSystem.cs @@ -1,12 +1,12 @@ using Content.Server.Administration.Logs; using Content.Server.Chemistry.Containers.EntitySystems; -using Content.Server.Nutrition.EntitySystems; using Content.Shared.Database; using Content.Shared.Glue; using Content.Shared.IdentityManagement; using Content.Shared.Interaction; using Content.Shared.Item; using Content.Shared.Lube; +using Content.Shared.Nutrition.EntitySystems; using Content.Shared.Popups; using Content.Shared.Verbs; using Robust.Shared.Audio; diff --git a/Content.Server/Materials/MaterialReclaimerSystem.cs b/Content.Server/Materials/MaterialReclaimerSystem.cs index ae4444e059..0d6d27777a 100644 --- a/Content.Server/Materials/MaterialReclaimerSystem.cs +++ b/Content.Server/Materials/MaterialReclaimerSystem.cs @@ -1,8 +1,6 @@ using Content.Server.Chemistry.Containers.EntitySystems; -using Content.Server.Chemistry.EntitySystems; using Content.Server.Fluids.EntitySystems; using Content.Server.GameTicking; -using Content.Server.Nutrition.EntitySystems; using Content.Server.Popups; using Content.Server.Power.Components; using Content.Server.Stack; @@ -10,11 +8,13 @@ using Content.Server.Wires; using Content.Shared.Body.Systems; using Content.Shared.Chemistry.Components; using Content.Shared.Chemistry.Components.SolutionManager; +using Content.Shared.Chemistry.EntitySystems; using Content.Shared.IdentityManagement; using Content.Shared.Interaction; using Content.Shared.Interaction.Events; using Content.Shared.Materials; using Content.Shared.Mind; +using Content.Shared.Nutrition.EntitySystems; using Robust.Server.GameObjects; using Robust.Shared.Player; using Robust.Shared.Utility; diff --git a/Content.Server/NPC/Systems/NPCUtilitySystem.cs b/Content.Server/NPC/Systems/NPCUtilitySystem.cs index 6793161105..e8fb54022e 100644 --- a/Content.Server/NPC/Systems/NPCUtilitySystem.cs +++ b/Content.Server/NPC/Systems/NPCUtilitySystem.cs @@ -14,6 +14,7 @@ using Content.Shared.Inventory; using Content.Shared.Mobs.Systems; using Content.Shared.NPC.Systems; using Content.Shared.Nutrition.Components; +using Content.Shared.Nutrition.EntitySystems; using Content.Shared.Tools.Systems; using Content.Shared.Weapons.Melee; using Content.Shared.Weapons.Ranged.Components; diff --git a/Content.Server/Nutrition/EntitySystems/DrinkSystem.cs b/Content.Server/Nutrition/EntitySystems/DrinkSystem.cs index 036c855dbb..74637d4813 100644 --- a/Content.Server/Nutrition/EntitySystems/DrinkSystem.cs +++ b/Content.Server/Nutrition/EntitySystems/DrinkSystem.cs @@ -24,6 +24,7 @@ using Content.Shared.Interaction.Events; using Content.Shared.Mobs.Systems; using Content.Shared.Nutrition; using Content.Shared.Nutrition.Components; +using Content.Shared.Nutrition.EntitySystems; using Content.Shared.Throwing; using Content.Shared.Verbs; using Robust.Shared.Audio; diff --git a/Content.Server/Nutrition/EntitySystems/FoodSystem.cs b/Content.Server/Nutrition/EntitySystems/FoodSystem.cs index d87b0bd0b0..49d7374041 100644 --- a/Content.Server/Nutrition/EntitySystems/FoodSystem.cs +++ b/Content.Server/Nutrition/EntitySystems/FoodSystem.cs @@ -23,6 +23,7 @@ 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.Stacks; using Content.Shared.Storage; using Content.Shared.Verbs; diff --git a/Content.Server/Nutrition/EntitySystems/OpenableSystem.cs b/Content.Server/Nutrition/EntitySystems/OpenableSystem.cs deleted file mode 100644 index 8037b61572..0000000000 --- a/Content.Server/Nutrition/EntitySystems/OpenableSystem.cs +++ /dev/null @@ -1,27 +0,0 @@ -using Content.Server.Chemistry.EntitySystems; -using Content.Shared.Nutrition.EntitySystems; -using Content.Shared.Nutrition.Components; - -namespace Content.Server.Nutrition.EntitySystems; - -/// -/// Provides API for openable food and drinks, handles opening on use and preventing transfer when closed. -/// -public sealed class OpenableSystem : SharedOpenableSystem -{ - public override void Initialize() - { - base.Initialize(); - - SubscribeLocalEvent(OnTransferAttempt); - } - - private void OnTransferAttempt(EntityUid uid, OpenableComponent comp, SolutionTransferAttemptEvent args) - { - if (!comp.Opened) - { - // message says its just for drinks, shouldn't matter since you typically dont have a food that is openable and can be poured out - args.Cancel(Loc.GetString("drink-component-try-use-drink-not-open", ("owner", uid))); - } - } -} diff --git a/Content.Shared/Chemistry/Components/ScoopableSolutionComponent.cs b/Content.Shared/Chemistry/Components/ScoopableSolutionComponent.cs new file mode 100644 index 0000000000..6c3f934b7a --- /dev/null +++ b/Content.Shared/Chemistry/Components/ScoopableSolutionComponent.cs @@ -0,0 +1,31 @@ +using Content.Shared.Chemistry.EntitySystems; +using Robust.Shared.GameStates; + +namespace Content.Shared.Chemistry.Components; + +/// +/// Basically reverse spiking, instead of using the solution-entity on a beaker, you use the beaker on the solution-entity. +/// If there is not enough volume it will stay in the solution-entity rather than spill onto the floor. +/// +[RegisterComponent, NetworkedComponent, Access(typeof(ScoopableSolutionSystem))] +public sealed partial class ScoopableSolutionComponent : Component +{ + /// + /// Solution name that can be scooped from. + /// + [DataField] + public string Solution = "default"; + + /// + /// If true, when the whole solution is scooped up the entity will be deleted. + /// + [DataField] + public bool Delete = true; + + /// + /// Popup to show the user when scooping. + /// Passed entities "scooped" and "beaker". + /// + [DataField] + public LocId Popup = "scoopable-component-popup"; +} diff --git a/Content.Shared/Chemistry/EntitySystems/ScoopableSolutionSystem.cs b/Content.Shared/Chemistry/EntitySystems/ScoopableSolutionSystem.cs new file mode 100644 index 0000000000..84f1e45616 --- /dev/null +++ b/Content.Shared/Chemistry/EntitySystems/ScoopableSolutionSystem.cs @@ -0,0 +1,53 @@ +using Content.Shared.Chemistry.Components; +using Content.Shared.Interaction; +using Content.Shared.Popups; +using Robust.Shared.Network; + +namespace Content.Shared.Chemistry.EntitySystems; + +/// +/// Handles solution transfer when a beaker is used on a scoopable entity. +/// +public sealed class ScoopableSolutionSystem : EntitySystem +{ + [Dependency] private readonly INetManager _netManager = default!; + [Dependency] private readonly SharedPopupSystem _popup = default!; + [Dependency] private readonly SharedSolutionContainerSystem _solution = default!; + [Dependency] private readonly SolutionTransferSystem _solutionTransfer = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnInteractUsing); + } + + private void OnInteractUsing(Entity ent, ref InteractUsingEvent args) + { + TryScoop(ent, args.Used, args.User); + } + + public bool TryScoop(Entity ent, EntityUid beaker, EntityUid user) + { + if (!_solution.TryGetSolution(ent.Owner, ent.Comp.Solution, out var src, out var srcSolution) || + !_solution.TryGetRefillableSolution(beaker, out var target, out _)) + return false; + + var scooped = _solutionTransfer.Transfer(user, ent, src.Value, beaker, target.Value, srcSolution.Volume); + if (scooped == 0) + return false; + + _popup.PopupClient(Loc.GetString(ent.Comp.Popup, ("scooped", ent.Owner), ("beaker", beaker)), user, user); + + if (srcSolution.Volume == 0 && ent.Comp.Delete) + { + // deletion isnt predicted so do this to prevent spam clicking to see "the ash is empty!" + RemCompDeferred(ent); + + if (!_netManager.IsClient) + QueueDel(ent); + } + + return true; + } +} diff --git a/Content.Shared/Chemistry/EntitySystems/SolutionTransferSystem.cs b/Content.Shared/Chemistry/EntitySystems/SolutionTransferSystem.cs new file mode 100644 index 0000000000..34a64d0edb --- /dev/null +++ b/Content.Shared/Chemistry/EntitySystems/SolutionTransferSystem.cs @@ -0,0 +1,223 @@ +using Content.Shared.Administration.Logs; +using Content.Shared.Chemistry; +using Content.Shared.Chemistry.Components; +using Content.Shared.Database; +using Content.Shared.FixedPoint; +using Content.Shared.Interaction; +using Content.Shared.Popups; +using Content.Shared.Verbs; +using Robust.Shared.Network; +using Robust.Shared.Player; + +namespace Content.Shared.Chemistry.EntitySystems; + +/// +/// Allows an entity to transfer solutions with a customizable amount per click. +/// Also provides API for other systems. +/// +public sealed class SolutionTransferSystem : EntitySystem +{ + [Dependency] private readonly INetManager _net = default!; + [Dependency] private readonly ISharedAdminLogManager _adminLogger = default!; + [Dependency] private readonly SharedPopupSystem _popup = default!; + [Dependency] private readonly SharedSolutionContainerSystem _solution = default!; + [Dependency] private readonly SharedUserInterfaceSystem _ui = default!; + + /// + /// Default transfer amounts for the set-transfer verb. + /// + public static readonly FixedPoint2[] DefaultTransferAmounts = new FixedPoint2[] { 1, 5, 10, 25, 50, 100, 250, 500, 1000 }; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent>(AddSetTransferVerbs); + SubscribeLocalEvent(OnAfterInteract); + SubscribeLocalEvent(OnTransferAmountSetValueMessage); + } + + private void OnTransferAmountSetValueMessage(Entity ent, ref TransferAmountSetValueMessage message) + { + var newTransferAmount = FixedPoint2.Clamp(message.Value, ent.Comp.MinimumTransferAmount, ent.Comp.MaximumTransferAmount); + ent.Comp.TransferAmount = newTransferAmount; + + if (message.Session.AttachedEntity is { Valid: true } user) + _popup.PopupClient(Loc.GetString("comp-solution-transfer-set-amount", ("amount", newTransferAmount)), ent, user); + } + + private void AddSetTransferVerbs(Entity ent, ref GetVerbsEvent args) + { + var (uid, comp) = ent; + + if (!args.CanAccess || !args.CanInteract || !comp.CanChangeTransferAmount || args.Hands == null) + return; + + if (!TryComp(args.User, out var actor)) + return; + + // Custom transfer verb + args.Verbs.Add(new AlternativeVerb() + { + Text = Loc.GetString("comp-solution-transfer-verb-custom-amount"), + Category = VerbCategory.SetTransferAmount, + // TODO: remove server check when bui prediction is a thing + Act = () => + { + if (_net.IsServer) + _ui.TryOpen(uid, TransferAmountUiKey.Key, actor.PlayerSession); + }, + Priority = 1 + }); + + // Add specific transfer verbs according to the container's size + var priority = 0; + var user = args.User; + foreach (var amount in DefaultTransferAmounts) + { + AlternativeVerb verb = new(); + verb.Text = Loc.GetString("comp-solution-transfer-verb-amount", ("amount", amount)); + verb.Category = VerbCategory.SetTransferAmount; + verb.Act = () => + { + comp.TransferAmount = amount; + _popup.PopupClient(Loc.GetString("comp-solution-transfer-set-amount", ("amount", amount)), uid, user); + }; + + // we want to sort by size, not alphabetically by the verb text. + verb.Priority = priority; + priority--; + + args.Verbs.Add(verb); + } + } + + private void OnAfterInteract(Entity ent, ref AfterInteractEvent args) + { + if (!args.CanReach || args.Target is not {} target) + return; + + var (uid, comp) = ent; + + //Special case for reagent tanks, because normally clicking another container will give solution, not take it. + if (comp.CanReceive + && !HasComp(target) // target must not be refillable (e.g. Reagent Tanks) + && _solution.TryGetDrainableSolution(target, out var targetSoln, out _) // target must be drainable + && TryComp(uid, out var refill) + && _solution.TryGetRefillableSolution((uid, refill, null), out var ownerSoln, out var ownerRefill)) + { + var transferAmount = comp.TransferAmount; // This is the player-configurable transfer amount of "uid," not the target reagent tank. + + // if the receiver has a smaller transfer limit, use that instead + if (refill?.MaxRefill is {} maxRefill) + transferAmount = FixedPoint2.Min(transferAmount, maxRefill); + + var transferred = Transfer(args.User, target, targetSoln.Value, uid, ownerSoln.Value, transferAmount); + if (transferred > 0) + { + var toTheBrim = ownerRefill.AvailableVolume == 0; + var msg = toTheBrim + ? "comp-solution-transfer-fill-fully" + : "comp-solution-transfer-fill-normal"; + + _popup.PopupClient(Loc.GetString(msg, ("owner", args.Target), ("amount", transferred), ("target", uid)), uid, args.User); + + args.Handled = true; + return; + } + } + + // if target is refillable, and owner is drainable + if (comp.CanSend + && TryComp(target, out var targetRefill) + && _solution.TryGetRefillableSolution((target, targetRefill, null), out targetSoln, out _) + && _solution.TryGetDrainableSolution(uid, out ownerSoln, out _)) + { + var transferAmount = comp.TransferAmount; + + if (targetRefill?.MaxRefill is {} maxRefill) + transferAmount = FixedPoint2.Min(transferAmount, maxRefill); + + var transferred = Transfer(args.User, uid, ownerSoln.Value, target, targetSoln.Value, transferAmount); + + if (transferred > 0) + { + var message = Loc.GetString("comp-solution-transfer-transfer-solution", ("amount", transferred), ("target", target)); + _popup.PopupClient(message, uid, args.User); + + args.Handled = true; + } + } + } + + /// + /// Transfer from a solution to another, allowing either entity to cancel it and show a popup. + /// + /// The actual amount transferred. + public FixedPoint2 Transfer(EntityUid user, + EntityUid sourceEntity, + Entity source, + EntityUid targetEntity, + Entity target, + FixedPoint2 amount) + { + var transferAttempt = new SolutionTransferAttemptEvent(sourceEntity, targetEntity); + + // Check if the source is cancelling the transfer + RaiseLocalEvent(sourceEntity, ref transferAttempt); + if (transferAttempt.CancelReason is {} reason) + { + _popup.PopupClient(reason, sourceEntity, user); + return FixedPoint2.Zero; + } + + var sourceSolution = source.Comp.Solution; + if (sourceSolution.Volume == 0) + { + _popup.PopupClient(Loc.GetString("comp-solution-transfer-is-empty", ("target", sourceEntity)), sourceEntity, user); + return FixedPoint2.Zero; + } + + // Check if the target is cancelling the transfer + RaiseLocalEvent(targetEntity, ref transferAttempt); + if (transferAttempt.CancelReason is {} targetReason) + { + _popup.PopupClient(targetReason, targetEntity, user); + return FixedPoint2.Zero; + } + + var targetSolution = target.Comp.Solution; + if (targetSolution.AvailableVolume == 0) + { + _popup.PopupClient(Loc.GetString("comp-solution-transfer-is-full", ("target", targetEntity)), targetEntity, user); + return FixedPoint2.Zero; + } + + var actualAmount = FixedPoint2.Min(amount, FixedPoint2.Min(sourceSolution.Volume, targetSolution.AvailableVolume)); + + var solution = _solution.SplitSolution(source, actualAmount); + _solution.Refill(targetEntity, target, solution); + + _adminLogger.Add(LogType.Action, LogImpact.Medium, + $"{ToPrettyString(user):player} transferred {SharedSolutionContainerSystem.ToPrettyString(solution)} to {ToPrettyString(targetEntity):target}, which now contains {SharedSolutionContainerSystem.ToPrettyString(targetSolution)}"); + + return actualAmount; + } +} + +/// +/// Raised when attempting to transfer from one solution to another. +/// Raised on both the source and target entities so either can cancel the transfer. +/// To not mispredict this should always be cancelled in shared code and not server or client. +/// +[ByRefEvent] +public record struct SolutionTransferAttemptEvent(EntityUid From, EntityUid To, string? CancelReason = null) +{ + /// + /// Cancels the transfer. + /// + public void Cancel(string reason) + { + CancelReason = reason; + } +} diff --git a/Content.Shared/Fluids/SharedPuddleSystem.Spillable.cs b/Content.Shared/Fluids/SharedPuddleSystem.Spillable.cs index 92ea962140..f88f13e8b0 100644 --- a/Content.Shared/Fluids/SharedPuddleSystem.Spillable.cs +++ b/Content.Shared/Fluids/SharedPuddleSystem.Spillable.cs @@ -13,7 +13,7 @@ namespace Content.Shared.Fluids; public abstract partial class SharedPuddleSystem { - [Dependency] protected readonly SharedOpenableSystem Openable = default!; + [Dependency] protected readonly OpenableSystem Openable = default!; protected virtual void InitializeSpillable() { diff --git a/Content.Shared/Nutrition/Components/OpenableComponent.cs b/Content.Shared/Nutrition/Components/OpenableComponent.cs index 3a230fc765..0381888e28 100644 --- a/Content.Shared/Nutrition/Components/OpenableComponent.cs +++ b/Content.Shared/Nutrition/Components/OpenableComponent.cs @@ -9,7 +9,7 @@ namespace Content.Shared.Nutrition.Components; /// Starts closed, open it with Z or E. /// [NetworkedComponent, AutoGenerateComponentState] -[RegisterComponent, Access(typeof(SharedOpenableSystem))] +[RegisterComponent, Access(typeof(OpenableSystem))] public sealed partial class OpenableComponent : Component { /// diff --git a/Content.Shared/Nutrition/EntitySystems/SharedOpenableSystem.cs b/Content.Shared/Nutrition/EntitySystems/OpenableSystem.cs similarity index 91% rename from Content.Shared/Nutrition/EntitySystems/SharedOpenableSystem.cs rename to Content.Shared/Nutrition/EntitySystems/OpenableSystem.cs index f3b1127578..0ad0877d22 100644 --- a/Content.Shared/Nutrition/EntitySystems/SharedOpenableSystem.cs +++ b/Content.Shared/Nutrition/EntitySystems/OpenableSystem.cs @@ -1,3 +1,4 @@ +using Content.Shared.Chemistry.EntitySystems; using Content.Shared.Examine; using Content.Shared.Interaction; using Content.Shared.Interaction.Events; @@ -13,7 +14,7 @@ namespace Content.Shared.Nutrition.EntitySystems; /// /// Provides API for openable food and drinks, handles opening on use and preventing transfer when closed. /// -public abstract partial class SharedOpenableSystem : EntitySystem +public sealed partial class OpenableSystem : EntitySystem { [Dependency] protected readonly SharedAppearanceSystem Appearance = default!; [Dependency] protected readonly SharedAudioSystem Audio = default!; @@ -29,6 +30,7 @@ public abstract partial class SharedOpenableSystem : EntitySystem SubscribeLocalEvent(HandleIfClosed); SubscribeLocalEvent(HandleIfClosed); SubscribeLocalEvent>(AddOpenCloseVerbs); + SubscribeLocalEvent(OnTransferAttempt); } private void OnInit(EntityUid uid, OpenableComponent comp, ComponentInit args) @@ -89,6 +91,15 @@ public abstract partial class SharedOpenableSystem : EntitySystem args.Verbs.Add(verb); } + private void OnTransferAttempt(Entity ent, ref SolutionTransferAttemptEvent args) + { + if (!ent.Comp.Opened) + { + // message says its just for drinks, shouldn't matter since you typically dont have a food that is openable and can be poured out + args.Cancel(Loc.GetString("drink-component-try-use-drink-not-open", ("owner", ent.Owner))); + } + } + /// /// Returns true if the entity either does not have OpenableComponent or it is opened. /// Drinks that don't have OpenableComponent are automatically open, so it returns true. diff --git a/Content.Shared/Nutrition/EntitySystems/SealableSystem.cs b/Content.Shared/Nutrition/EntitySystems/SealableSystem.cs index b0873f23a1..414b8d182b 100644 --- a/Content.Shared/Nutrition/EntitySystems/SealableSystem.cs +++ b/Content.Shared/Nutrition/EntitySystems/SealableSystem.cs @@ -11,7 +11,7 @@ public sealed partial class SealableSystem : EntitySystem { base.Initialize(); - SubscribeLocalEvent(OnExamined, after: new[] { typeof(SharedOpenableSystem) }); + SubscribeLocalEvent(OnExamined, after: new[] { typeof(OpenableSystem) }); SubscribeLocalEvent(OnOpened); } diff --git a/Resources/Locale/en-US/chemistry/components/scoopable-component.ftl b/Resources/Locale/en-US/chemistry/components/scoopable-component.ftl new file mode 100644 index 0000000000..c2593cc61e --- /dev/null +++ b/Resources/Locale/en-US/chemistry/components/scoopable-component.ftl @@ -0,0 +1 @@ +scoopable-component-popup = You scoop up {$scooped} into {THE($beaker)}. diff --git a/Resources/Prototypes/Entities/Effects/chemistry_effects.yml b/Resources/Prototypes/Entities/Effects/chemistry_effects.yml index 739464e961..5b7ee46946 100644 --- a/Resources/Prototypes/Entities/Effects/chemistry_effects.yml +++ b/Resources/Prototypes/Entities/Effects/chemistry_effects.yml @@ -9,6 +9,7 @@ - type: SmokeVisuals - type: Transform anchored: true + - type: Clickable - type: Physics - type: Fixtures fixtures: @@ -76,6 +77,8 @@ animationState: foam-dissolve - type: Slippery - type: StepTrigger + - type: ScoopableSolution + solution: solutionArea - type: entity id: MetalFoam diff --git a/Resources/Prototypes/Entities/Objects/Specific/Medical/morgue.yml b/Resources/Prototypes/Entities/Objects/Specific/Medical/morgue.yml index fba12bebec..32aa114429 100644 --- a/Resources/Prototypes/Entities/Objects/Specific/Medical/morgue.yml +++ b/Resources/Prototypes/Entities/Objects/Specific/Medical/morgue.yml @@ -120,6 +120,8 @@ - type: SolutionSpiker sourceSolution: food ignoreEmpty: true + - type: ScoopableSolution + solution: food - type: Extractable grindableSolutionName: food