A big hecking chemistry-related refactor. (#3055)

* A big hecking chemistry-related refactor.

Changed SolutionContainerCaps. It now describes "stock" behavior for interacting with solutions that is pre-implemented by SolutionContainerComponent. As such things like syringes do not check it anymore (on themselves) to see "can we remove reagent from ourselves". That's assumed by it... being a syringe.

SolutionContainerCaps now has different flags more accurately describing possible reagent interaction behaviors.

ISolutionInteractionsComponent is the interface that describes the common behaviors like "what happens when injected with a syringe". This is implemented by SolutionContainerComponent but could be implemented by other classes. One notable example that drove me to making this interface was the /vg/station circuit imprinter which splits reagent poured in into its two reservoir beakers. Having this interface allows us to do this "proxying" behavior hack-free. (the hacks in /vg/ code were somewhat dirty...).

PourableComponent has been replaced SolutionTransferComponent. It now describes both give-and-take behavior for the common reagent containers. This is in line with /vg/'s /obj/item/weapon/reagent_containers architecture. "Taking" in this context is ONLY from reagent tanks like fuel tanks.

Oh, should I mention that fuel tanks and such have a proper component now? They do.

Because of this behavioral change, reagent tanks DO NOT have Pourable anymore. Removing from reagent tanks is now in the hands of the item used on them. Welders and fire extinguishers now have code for removing from them. This sounds bad at first but remember that all have quite unique behavior related to this: Welders cause explosions if lit and can ONLY be fueled at fuel tanks. Extinguishers can be filled at any tank, etc... The code for this is also simpler due to ISolutionInteractionsComponent now so...

IAfterInteract now works like IInteractUsing with the Priority levels and "return true to block further handlers" behavior. This was necessary to make extinguishers prioritize taking from tanks over spraying.

Explicitly coded interactions like welders refueling also means they refuse instantly to full now, which they didn't before. And it plays the sound. Etc...

Probably more stuff I'm forgetting.

* Review improvements.
This commit is contained in:
Pieter-Jan Briers
2021-02-03 14:05:31 +01:00
committed by GitHub
parent b284c82668
commit c40ac26ced
65 changed files with 987 additions and 601 deletions

View File

@@ -148,41 +148,41 @@ namespace Content.Server.GameObjects.Components.ActionBlocking
return new HandcuffedComponentState(Broken ? BrokenState : string.Empty);
}
async Task IAfterInteract.AfterInteract(AfterInteractEventArgs eventArgs)
async Task<bool> IAfterInteract.AfterInteract(AfterInteractEventArgs eventArgs)
{
if (eventArgs.Target == null || !ActionBlockerSystem.CanUse(eventArgs.User) || !eventArgs.Target.TryGetComponent<CuffableComponent>(out var cuffed))
{
return;
return false;
}
if (eventArgs.Target == eventArgs.User)
{
eventArgs.User.PopupMessage(Loc.GetString("You can't cuff yourself!"));
return;
return true;
}
if (Broken)
{
eventArgs.User.PopupMessage(Loc.GetString("The cuffs are broken!"));
return;
return true;
}
if (!eventArgs.Target.TryGetComponent<HandsComponent>(out var hands))
{
eventArgs.User.PopupMessage(Loc.GetString("{0:theName} has no hands!", eventArgs.Target));
return;
return true;
}
if (cuffed.CuffedHandCount == hands.Count)
{
eventArgs.User.PopupMessage(Loc.GetString("{0:theName} has no free hands to handcuff!", eventArgs.Target));
return;
return true;
}
if (!eventArgs.InRangeUnobstructed(_interactRange, ignoreInsideBlocker: true))
{
eventArgs.User.PopupMessage(Loc.GetString("You are too far away to use the cuffs!"));
return;
return true;
}
eventArgs.User.PopupMessage(Loc.GetString("You start cuffing {0:theName}.", eventArgs.Target));
@@ -190,6 +190,7 @@ namespace Content.Server.GameObjects.Components.ActionBlocking
_audioSystem.PlayFromEntity(StartCuffSound, Owner);
TryUpdateCuff(eventArgs.User, eventArgs.Target, cuffed);
return true;
}
/// <summary>

View File

@@ -253,18 +253,20 @@ namespace Content.Server.GameObjects.Components.Atmos
}
}
async Task IAfterInteract.AfterInteract(AfterInteractEventArgs eventArgs)
async Task<bool> IAfterInteract.AfterInteract(AfterInteractEventArgs eventArgs)
{
if (!eventArgs.CanReach)
{
eventArgs.User.PopupMessage(Loc.GetString("You can't reach there!"));
return;
return true;
}
if (eventArgs.User.TryGetComponent(out IActorComponent? actor))
{
OpenInterface(actor.playerSession, eventArgs.ClickLocation);
}
return true;
}

View File

@@ -36,11 +36,11 @@ namespace Content.Server.GameObjects.Components.Body
}
}
async Task IAfterInteract.AfterInteract(AfterInteractEventArgs eventArgs)
async Task<bool> IAfterInteract.AfterInteract(AfterInteractEventArgs eventArgs)
{
if (eventArgs.Target == null)
{
return;
return false;
}
CloseAllSurgeryUIs();
@@ -61,6 +61,8 @@ namespace Content.Server.GameObjects.Components.Body
eventArgs.Target.PopupMessage(eventArgs.User, Loc.GetString("You can't fit it in!"));
}
}
return true;
}
private void SendBodyPartListToUser(AfterInteractEventArgs eventArgs, IBody body)

View File

@@ -99,12 +99,12 @@ namespace Content.Server.GameObjects.Components.Body.Part
}
}
public async Task AfterInteract(AfterInteractEventArgs eventArgs)
public async Task<bool> AfterInteract(AfterInteractEventArgs eventArgs)
{
// TODO BODY
if (eventArgs.Target == null)
{
return;
return false;
}
CloseAllSurgeryUIs();
@@ -116,6 +116,8 @@ namespace Content.Server.GameObjects.Components.Body.Part
{
SendSlots(eventArgs, body);
}
return true;
}
private void SendSlots(AfterInteractEventArgs eventArgs, IBody body)

View File

@@ -50,16 +50,16 @@ namespace Content.Server.GameObjects.Components.Body.Surgery
public IEntity? PerformerCache { get; private set; }
async Task IAfterInteract.AfterInteract(AfterInteractEventArgs eventArgs)
async Task<bool> IAfterInteract.AfterInteract(AfterInteractEventArgs eventArgs)
{
if (eventArgs.Target == null)
{
return;
return false;
}
if (!eventArgs.User.TryGetComponent(out IActorComponent? actor))
{
return;
return false;
}
CloseAllSurgeryUIs();
@@ -101,20 +101,22 @@ namespace Content.Server.GameObjects.Components.Body.Surgery
if (!part.SurgeryCheck(_surgeryType))
{
NotUsefulPopup();
return;
return true;
}
// ...do the surgery.
if (part.AttemptSurgery(_surgeryType, part, this,
eventArgs.User))
{
return;
return true;
}
// Log error if the surgery fails somehow.
Logger.Debug($"Error when trying to perform surgery on ${nameof(IBodyPart)} {eventArgs.User.Name}");
throw new InvalidOperationException();
}
return true;
}
public float BaseOperationTime { get => _baseOperateTime; set => _baseOperateTime = value; }

View File

@@ -12,6 +12,7 @@ using Content.Server.Utility;
using Content.Shared.Audio;
using Content.Shared.Chemistry;
using Content.Shared.GameObjects.Components.Botany;
using Content.Shared.GameObjects.Components.Chemistry;
using Content.Shared.GameObjects.EntitySystems;
using Content.Shared.GameObjects.EntitySystems.ActionBlocker;
using Content.Shared.Interfaces;
@@ -718,23 +719,28 @@ namespace Content.Server.GameObjects.Components.Botany
return true;
}
if (usingItem.TryGetComponent(out SolutionContainerComponent? solution) && solution.CanRemoveSolutions)
if (usingItem.TryGetComponent(out ISolutionInteractionsComponent? solution) && solution.CanDrain)
{
var amount = 5f;
var amount = ReagentUnit.New(5);
var sprayed = false;
if (usingItem.TryGetComponent(out SprayComponent? spray))
{
sprayed = true;
amount = 1f;
amount = ReagentUnit.New(1);
EntitySystem.Get<AudioSystem>().PlayFromEntity(spray.SpraySound, usingItem, AudioHelpers.WithVariation(0.125f));
}
var chemAmount = ReagentUnit.New(amount);
var split = solution.Drain(amount);
if (split.TotalVolume == 0)
{
user.PopupMessageCursor(Loc.GetString("{0:TheName} is empty!", usingItem));
return true;
}
var split = solution.Solution.SplitSolution(chemAmount <= solution.Solution.TotalVolume ? chemAmount : solution.Solution.TotalVolume);
user.PopupMessageCursor(Loc.GetString(sprayed ? $"You spray {Owner.Name} with {usingItem.Name}." : $"You transfer {split.TotalVolume.ToString()}u to {Owner.Name}"));
user.PopupMessageCursor(Loc.GetString(
sprayed ? "You spray {0:TheName}" : "You transfer {1}u to {0:TheName}",
Owner, split.TotalVolume));
_solutionContainer?.TryAddSolution(split);

View File

@@ -52,10 +52,12 @@ namespace Content.Server.GameObjects.Components.Chemistry
return TryDoInject(target, user);
}
Task IAfterInteract.AfterInteract(AfterInteractEventArgs eventArgs)
async Task<bool> IAfterInteract.AfterInteract(AfterInteractEventArgs eventArgs)
{
TryDoInject(eventArgs.Target, eventArgs.User);
return Task.CompletedTask;
if (!eventArgs.CanReach)
return false;
return TryDoInject(eventArgs.Target, eventArgs.User);
}
private bool TryDoInject(IEntity? target, IEntity user)

View File

@@ -28,21 +28,18 @@ namespace Content.Server.GameObjects.Components.Chemistry
/// Whether or not the injector is able to draw from containers or if it's a single use
/// device that can only inject.
/// </summary>
[ViewVariables]
private bool _injectOnly;
[ViewVariables] private bool _injectOnly;
/// <summary>
/// Amount to inject or draw on each usage. If the injector is inject only, it will
/// attempt to inject it's entire contents upon use.
/// </summary>
[ViewVariables]
private ReagentUnit _transferAmount;
[ViewVariables] private ReagentUnit _transferAmount;
/// <summary>
/// Initial storage volume of the injector
/// </summary>
[ViewVariables]
private ReagentUnit _initialMaxVolume;
[ViewVariables] private ReagentUnit _initialMaxVolume;
private InjectorToggleMode _toggleState;
@@ -68,19 +65,14 @@ namespace Content.Server.GameObjects.Components.Chemistry
serializer.DataField(ref _injectOnly, "injectOnly", false);
serializer.DataField(ref _initialMaxVolume, "initialMaxVolume", ReagentUnit.New(15));
serializer.DataField(ref _transferAmount, "transferAmount", ReagentUnit.New(5));
serializer.DataField(ref _toggleState, "toggleState", _injectOnly ? InjectorToggleMode.Inject : InjectorToggleMode.Draw);
serializer.DataField(ref _toggleState, "toggleState",
_injectOnly ? InjectorToggleMode.Inject : InjectorToggleMode.Draw);
}
protected override void Startup()
{
base.Startup();
var solution = Owner.EnsureComponent<SolutionContainerComponent>();
solution.Capabilities = SolutionContainerCaps.AddTo | SolutionContainerCaps.RemoveFrom;
// Set _toggleState based on prototype
_toggleState = _injectOnly ? InjectorToggleMode.Inject : InjectorToggleMode.Draw;
Dirty();
}
@@ -116,58 +108,55 @@ namespace Content.Server.GameObjects.Components.Chemistry
/// Called when clicking on entities while holding in active hand
/// </summary>
/// <param name="eventArgs"></param>
async Task IAfterInteract.AfterInteract(AfterInteractEventArgs eventArgs)
async Task<bool> IAfterInteract.AfterInteract(AfterInteractEventArgs eventArgs)
{
if (!eventArgs.InRangeUnobstructed(ignoreInsideBlocker: true, popup: true)) return;
if (!eventArgs.InRangeUnobstructed(ignoreInsideBlocker: true, popup: true))
return false;
//Make sure we have the attacking entity
if (eventArgs.Target == null || !Owner.TryGetComponent(out SolutionContainerComponent? solution))
if (eventArgs.Target == null || !Owner.HasComponent<SolutionContainerComponent>())
{
return;
return false;
}
var targetEntity = eventArgs.Target;
// Handle injecting/drawing for solutions
if (targetEntity.TryGetComponent<SolutionContainerComponent>(out var targetSolution))
if (targetEntity.TryGetComponent<ISolutionInteractionsComponent>(out var targetSolution))
{
if (ToggleState == InjectorToggleMode.Inject)
{
if (solution.CanRemoveSolutions && targetSolution.CanAddSolutions)
if (targetSolution.CanInject)
{
TryInject(targetSolution, eventArgs.User);
}
else
{
eventArgs.User.PopupMessage(eventArgs.User, Loc.GetString("You aren't able to transfer to {0:theName}!", targetSolution.Owner));
eventArgs.User.PopupMessage(eventArgs.User,
Loc.GetString("You aren't able to transfer to {0:theName}!", targetSolution.Owner));
}
}
else if (ToggleState == InjectorToggleMode.Draw)
{
if (targetSolution.CanRemoveSolutions && solution.CanAddSolutions)
if (targetSolution.CanDraw)
{
TryDraw(targetSolution, eventArgs.User);
}
else
{
eventArgs.User.PopupMessage(eventArgs.User, Loc.GetString("You aren't able to draw from {0:theName}!", targetSolution.Owner));
eventArgs.User.PopupMessage(eventArgs.User,
Loc.GetString("You aren't able to draw from {0:theName}!", targetSolution.Owner));
}
}
}
else // Handle injecting into bloodstream
// Handle injecting into bloodstream
else if (targetEntity.TryGetComponent(out BloodstreamComponent? bloodstream) &&
ToggleState == InjectorToggleMode.Inject)
{
if (targetEntity.TryGetComponent(out BloodstreamComponent? bloodstream) && ToggleState == InjectorToggleMode.Inject)
{
if (solution.CanRemoveSolutions)
{
TryInjectIntoBloodstream(bloodstream, eventArgs.User);
}
else
{
eventArgs.User.PopupMessage(eventArgs.User, Loc.GetString("You aren't able to inject {0:theName}!", targetEntity));
}
}
TryInjectIntoBloodstream(bloodstream, eventArgs.User);
}
return true;
}
/// <summary>
@@ -193,7 +182,8 @@ namespace Content.Server.GameObjects.Components.Chemistry
if (realTransferAmount <= 0)
{
Owner.PopupMessage(user, Loc.GetString("You aren't able to inject {0:theName}!", targetBloodstream.Owner));
Owner.PopupMessage(user,
Loc.GetString("You aren't able to inject {0:theName}!", targetBloodstream.Owner));
return;
}
@@ -213,12 +203,14 @@ namespace Content.Server.GameObjects.Components.Chemistry
removedSolution.DoEntityReaction(targetBloodstream.Owner, ReactionMethod.Injection);
Owner.PopupMessage(user, Loc.GetString("You inject {0}u into {1:theName}!", removedSolution.TotalVolume, targetBloodstream.Owner));
Owner.PopupMessage(user,
Loc.GetString("You inject {0}u into {1:theName}!", removedSolution.TotalVolume,
targetBloodstream.Owner));
Dirty();
AfterInject();
}
private void TryInject(SolutionContainerComponent targetSolution, IEntity user)
private void TryInject(ISolutionInteractionsComponent targetSolution, IEntity user)
{
if (!Owner.TryGetComponent(out SolutionContainerComponent? solution) || solution.CurrentVolume == 0)
{
@@ -226,7 +218,7 @@ namespace Content.Server.GameObjects.Components.Chemistry
}
// Get transfer amount. May be smaller than _transferAmount if not enough room
var realTransferAmount = ReagentUnit.Min(_transferAmount, targetSolution.EmptyVolume);
var realTransferAmount = ReagentUnit.Min(_transferAmount, targetSolution.InjectSpaceAvailable);
if (realTransferAmount <= 0)
{
@@ -237,16 +229,12 @@ namespace Content.Server.GameObjects.Components.Chemistry
// Move units from attackSolution to targetSolution
var removedSolution = solution.SplitSolution(realTransferAmount);
if (!targetSolution.CanAddSolution(removedSolution))
{
return;
}
removedSolution.DoEntityReaction(targetSolution.Owner, ReactionMethod.Injection);
targetSolution.TryAddSolution(removedSolution);
targetSolution.Inject(removedSolution);
Owner.PopupMessage(user, Loc.GetString("You transfer {0}u to {1:theName}", removedSolution.TotalVolume, targetSolution.Owner));
Owner.PopupMessage(user,
Loc.GetString("You transfer {0}u to {1:theName}", removedSolution.TotalVolume, targetSolution.Owner));
Dirty();
AfterInject();
}
@@ -260,7 +248,7 @@ namespace Content.Server.GameObjects.Components.Chemistry
}
}
private void TryDraw(SolutionContainerComponent targetSolution, IEntity user)
private void TryDraw(ISolutionInteractionsComponent targetSolution, IEntity user)
{
if (!Owner.TryGetComponent(out SolutionContainerComponent? solution) || solution.EmptyVolume == 0)
{
@@ -268,7 +256,7 @@ namespace Content.Server.GameObjects.Components.Chemistry
}
// Get transfer amount. May be smaller than _transferAmount if not enough room
var realTransferAmount = ReagentUnit.Min(_transferAmount, targetSolution.CurrentVolume);
var realTransferAmount = ReagentUnit.Min(_transferAmount, targetSolution.DrawAvailable);
if (realTransferAmount <= 0)
{
@@ -277,14 +265,15 @@ namespace Content.Server.GameObjects.Components.Chemistry
}
// Move units from attackSolution to targetSolution
var removedSolution = targetSolution.SplitSolution(realTransferAmount);
var removedSolution = targetSolution.Draw(realTransferAmount);
if (!solution.TryAddSolution(removedSolution))
{
return;
}
Owner.PopupMessage(user, Loc.GetString("Drew {0}u from {1:theName}", removedSolution.TotalVolume, targetSolution.Owner));
Owner.PopupMessage(user,
Loc.GetString("Drew {0}u from {1:theName}", removedSolution.TotalVolume, targetSolution.Owner));
Dirty();
AfterDraw();
}

View File

@@ -59,14 +59,15 @@ namespace Content.Server.GameObjects.Components.Chemistry
}
// Feeding someone else
public async Task AfterInteract(AfterInteractEventArgs eventArgs)
public async Task<bool> AfterInteract(AfterInteractEventArgs eventArgs)
{
if (eventArgs.Target == null)
{
return;
return false;
}
TryUseFood(eventArgs.User, eventArgs.Target);
return true;
}
public override bool TryUseFood(IEntity user, IEntity target, UtensilComponent utensilUsed = null)

View File

@@ -1,113 +0,0 @@
using System.Threading.Tasks;
using Content.Shared.Chemistry;
using Content.Shared.Interfaces;
using Content.Shared.Interfaces.GameObjects.Components;
using Robust.Shared.GameObjects;
using Robust.Shared.Localization;
using Robust.Shared.Serialization;
using Robust.Shared.ViewVariables;
namespace Content.Server.GameObjects.Components.Chemistry
{
/// <summary>
/// Gives an entity click behavior for pouring reagents into
/// other entities and being poured into. The entity must have
/// a SolutionComponent or DrinkComponent for this to work.
/// (DrinkComponent adds a SolutionComponent if one isn't present).
/// </summary>
[RegisterComponent]
class PourableComponent : Component, IInteractUsing
{
public override string Name => "Pourable";
private ReagentUnit _transferAmount;
/// <summary>
/// The amount of solution to be transferred from this solution when clicking on other solutions with it.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
public ReagentUnit TransferAmount
{
get => _transferAmount;
set => _transferAmount = value;
}
public override void ExposeData(ObjectSerializer serializer)
{
base.ExposeData(serializer);
serializer.DataField(ref _transferAmount, "transferAmount", ReagentUnit.New(5.0));
}
/// <summary>
/// Called when the owner of this component is clicked on with another entity.
/// The owner of this component is the target.
/// The entity used to click on this one is the attacker.
/// </summary>
/// <param name="eventArgs">Attack event args</param>
/// <returns></returns>
async Task<bool> IInteractUsing.InteractUsing(InteractUsingEventArgs eventArgs)
{
//Get target solution component
if (!Owner.TryGetComponent<SolutionContainerComponent>(out var targetSolution))
return false;
//Get attack solution component
var attackEntity = eventArgs.Using;
if (!attackEntity.TryGetComponent<SolutionContainerComponent>(out var attackSolution))
return false;
// Calculate possibe solution transfer
if (targetSolution.CanAddSolutions && attackSolution.CanRemoveSolutions)
{
// default logic (beakers and glasses)
// transfer solution from object in hand to attacked
return TryTransfer(eventArgs, attackSolution, targetSolution);
}
else if (targetSolution.CanRemoveSolutions && attackSolution.CanAddSolutions)
{
// storage tanks and sinks logic
// drain solution from attacked object to object in hand
return TryTransfer(eventArgs, targetSolution, attackSolution);
}
// No transfer possible
return false;
}
bool TryTransfer(InteractUsingEventArgs eventArgs, SolutionContainerComponent fromSolution, SolutionContainerComponent toSolution)
{
var fromEntity = fromSolution.Owner;
if (!fromEntity.TryGetComponent<PourableComponent>(out var fromPourable))
{
return false;
}
//Get transfer amount. May be smaller than _transferAmount if not enough room
var realTransferAmount = ReagentUnit.Min(fromPourable.TransferAmount, toSolution.EmptyVolume);
if (realTransferAmount <= 0) // Special message if container is full
{
Owner.PopupMessage(eventArgs.User, Loc.GetString("{0:theName} is full!", toSolution.Owner));
return false;
}
//Move units from attackSolution to targetSolution
var removedSolution = fromSolution.SplitSolution(realTransferAmount);
if (removedSolution.TotalVolume <= ReagentUnit.Zero)
{
return false;
}
if (!toSolution.TryAddSolution(removedSolution))
{
return false;
}
Owner.PopupMessage(eventArgs.User, Loc.GetString("You transfer {0}u to {1:theName}.", removedSolution.TotalVolume, toSolution.Owner));
return true;
}
}
}

View File

@@ -0,0 +1,35 @@
using Content.Shared.Chemistry;
using Robust.Shared.GameObjects;
using Robust.Shared.Serialization;
using Robust.Shared.ViewVariables;
#nullable enable
namespace Content.Server.GameObjects.Components.Chemistry
{
[RegisterComponent]
public class ReagentTankComponent : Component
{
public override string Name => "ReagentTank";
[ViewVariables(VVAccess.ReadWrite)]
public ReagentUnit TransferAmount { get; set; }
[ViewVariables(VVAccess.ReadWrite)]
public ReagentTankType TankType { get; set; }
public override void ExposeData(ObjectSerializer serializer)
{
base.ExposeData(serializer);
serializer.DataField(this, c => c.TransferAmount, "transferAmount", ReagentUnit.New(10));
serializer.DataField(this, c => c.TankType, "tankType", ReagentTankType.Unspecified);
}
}
public enum ReagentTankType : byte
{
Unspecified,
Fuel
}
}

View File

@@ -1,4 +1,4 @@
#nullable enable
#nullable enable
using Content.Server.Administration;
using Content.Server.Eui;
using Content.Server.GameObjects.Components.GUI;
@@ -6,6 +6,7 @@ using Content.Shared.Administration;
using Content.Shared.Chemistry;
using Content.Shared.Eui;
using Content.Shared.GameObjects.Components.Chemistry;
using Content.Shared.GameObjects.EntitySystems;
using Content.Shared.GameObjects.EntitySystems.ActionBlocker;
using Content.Shared.GameObjects.Verbs;
using Robust.Server.Interfaces.GameObjects;
@@ -19,213 +20,8 @@ namespace Content.Server.GameObjects.Components.Chemistry
{
[RegisterComponent]
[ComponentReference(typeof(SharedSolutionContainerComponent))]
[ComponentReference(typeof(ISolutionInteractionsComponent))]
public class SolutionContainerComponent : SharedSolutionContainerComponent
{
/// <summary>
/// Transfers solution from the held container to the target container.
/// </summary>
[Verb]
private sealed class FillTargetVerb : Verb<SolutionContainerComponent>
{
protected override void GetData(IEntity user, SolutionContainerComponent component, VerbData data)
{
if (!ActionBlockerSystem.CanInteract(user) ||
!user.TryGetComponent<HandsComponent>(out var hands) ||
hands.GetActiveHand == null ||
hands.GetActiveHand.Owner == component.Owner ||
!hands.GetActiveHand.Owner.TryGetComponent<SolutionContainerComponent>(out var solution) ||
!solution.CanRemoveSolutions ||
!component.CanAddSolutions)
{
data.Visibility = VerbVisibility.Invisible;
return;
}
var heldEntityName = hands.GetActiveHand.Owner?.Prototype?.Name ?? "<Item>";
var myName = component.Owner.Prototype?.Name ?? "<Item>";
var locHeldEntityName = Loc.GetString(heldEntityName);
var locMyName = Loc.GetString(myName);
data.Visibility = VerbVisibility.Visible;
data.Text = Loc.GetString("Transfer liquid from [{0}] to [{1}].", locHeldEntityName, locMyName);
}
protected override void Activate(IEntity user, SolutionContainerComponent component)
{
if (!user.TryGetComponent<HandsComponent>(out var hands) || hands.GetActiveHand == null)
{
return;
}
if (!hands.GetActiveHand.Owner.TryGetComponent<SolutionContainerComponent>(out var handSolutionComp) ||
!handSolutionComp.CanRemoveSolutions ||
!component.CanAddSolutions)
{
return;
}
var transferQuantity = ReagentUnit.Min(component.MaxVolume - component.CurrentVolume,
handSolutionComp.CurrentVolume, ReagentUnit.New(10));
if (transferQuantity <= 0)
{
return;
}
var transferSolution = handSolutionComp.SplitSolution(transferQuantity);
component.TryAddSolution(transferSolution);
}
}
/// <summary>
/// Transfers solution from a target container to the held container.
/// </summary>
[Verb]
private sealed class EmptyTargetVerb : Verb<SolutionContainerComponent>
{
protected override void GetData(IEntity user, SolutionContainerComponent component, VerbData data)
{
if (!ActionBlockerSystem.CanInteract(user) ||
!user.TryGetComponent<HandsComponent>(out var hands) ||
hands.GetActiveHand == null ||
hands.GetActiveHand.Owner == component.Owner ||
!hands.GetActiveHand.Owner.TryGetComponent<SolutionContainerComponent>(out var solution) ||
!solution.CanAddSolutions ||
!component.CanRemoveSolutions)
{
data.Visibility = VerbVisibility.Invisible;
return;
}
var heldEntityName = hands.GetActiveHand.Owner?.Prototype?.Name ?? "<Item>";
var myName = component.Owner.Prototype?.Name ?? "<Item>";
var locHeldEntityName = Loc.GetString(heldEntityName);
var locMyName = Loc.GetString(myName);
data.Visibility = VerbVisibility.Visible;
data.Text = Loc.GetString("Transfer liquid from [{0}] to [{1}].", locMyName, locHeldEntityName);
return;
}
protected override void Activate(IEntity user, SolutionContainerComponent component)
{
if (!user.TryGetComponent<HandsComponent>(out var hands) || hands.GetActiveHand == null)
{
return;
}
if (!hands.GetActiveHand.Owner.TryGetComponent<SolutionContainerComponent>(out var handSolutionComp) ||
!handSolutionComp.CanAddSolutions ||
!component.CanRemoveSolutions)
{
return;
}
var transferQuantity = ReagentUnit.Min(handSolutionComp.MaxVolume - handSolutionComp.CurrentVolume,
component.CurrentVolume, ReagentUnit.New(10));
if (transferQuantity <= 0)
{
return;
}
var transferSolution = component.SplitSolution(transferQuantity);
handSolutionComp.TryAddSolution(transferSolution);
}
}
[Verb]
private sealed class AdminAddReagentVerb : Verb<SolutionContainerComponent>
{
private const AdminFlags ReqFlags = AdminFlags.Fun;
protected override void GetData(IEntity user, SolutionContainerComponent component, VerbData data)
{
data.Text = Loc.GetString("Add Reagent...");
data.CategoryData = VerbCategories.Debug;
data.Visibility = VerbVisibility.Invisible;
var adminManager = IoCManager.Resolve<IAdminManager>();
if (user.TryGetComponent<IActorComponent>(out var player))
{
if (adminManager.HasAdminFlag(player.playerSession, ReqFlags))
{
data.Visibility = VerbVisibility.Visible;
}
}
}
protected override void Activate(IEntity user, SolutionContainerComponent component)
{
var groupController = IoCManager.Resolve<IAdminManager>();
if (user.TryGetComponent<IActorComponent>(out var player))
{
if (groupController.HasAdminFlag(player.playerSession, ReqFlags))
OpenAddReagentMenu(player.playerSession, component);
}
}
private static void OpenAddReagentMenu(IPlayerSession player, SolutionContainerComponent comp)
{
var euiMgr = IoCManager.Resolve<EuiManager>();
euiMgr.OpenEui(new AdminAddReagentEui(comp), player);
}
private sealed class AdminAddReagentEui : BaseEui
{
private readonly SolutionContainerComponent _target;
[Dependency] private readonly IAdminManager _adminManager = default!;
public AdminAddReagentEui(SolutionContainerComponent target)
{
_target = target;
IoCManager.InjectDependencies(this);
}
public override void Opened()
{
StateDirty();
}
public override EuiStateBase GetNewState()
{
return new AdminAddReagentEuiState
{
CurVolume = _target.CurrentVolume,
MaxVolume = _target.MaxVolume
};
}
public override void HandleMessage(EuiMessageBase msg)
{
switch (msg)
{
case AdminAddReagentEuiMsg.Close:
Close();
break;
case AdminAddReagentEuiMsg.DoAdd doAdd:
// Double check that user wasn't de-adminned in the mean time...
// Or the target was deleted.
if (!_adminManager.HasAdminFlag(Player, ReqFlags) || _target.Deleted)
{
Close();
return;
}
_target.TryAddReagent(doAdd.ReagentId, doAdd.Amount, out _);
StateDirty();
if (doAdd.CloseAfter)
Close();
break;
}
}
}
}
}
}

View File

@@ -0,0 +1,144 @@
#nullable enable
using System.Threading.Tasks;
using Content.Shared.Chemistry;
using Content.Shared.GameObjects.Components.Chemistry;
using Content.Shared.Interfaces;
using Content.Shared.Interfaces.GameObjects.Components;
using Robust.Shared.GameObjects;
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.Localization;
using Robust.Shared.Serialization;
using Robust.Shared.ViewVariables;
namespace Content.Server.GameObjects.Components.Chemistry
{
/// <summary>
/// Gives click behavior for transferring to/from other reagent containers.
/// </summary>
[RegisterComponent]
public sealed class SolutionTransferComponent : Component, IAfterInteract
{
// Behavior is as such:
// If it's a reagent tank, TAKE reagent.
// If it's anything else, GIVE reagent.
// Of course, only if possible.
public override string Name => "SolutionTransfer";
private ReagentUnit _transferAmount;
private bool _canReceive;
private bool _canSend;
/// <summary>
/// The amount of solution to be transferred from this solution when clicking on other solutions with it.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
public ReagentUnit TransferAmount
{
get => _transferAmount;
set => _transferAmount = value;
}
/// <summary>
/// Can this entity take reagent from reagent tanks?
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
public bool CanReceive
{
get => _canReceive;
set => _canReceive = value;
}
/// <summary>
/// Can this entity give reagent to other reagent containers?
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
public bool CanSend
{
get => _canSend;
set => _canSend = value;
}
public override void ExposeData(ObjectSerializer serializer)
{
base.ExposeData(serializer);
serializer.DataField(ref _transferAmount, "transferAmount", ReagentUnit.New(5));
serializer.DataField(ref _canReceive, "canReceive", true);
serializer.DataField(ref _canSend, "canSend", true);
}
async Task<bool> IAfterInteract.AfterInteract(AfterInteractEventArgs eventArgs)
{
if (!eventArgs.CanReach || eventArgs.Target == null)
return false;
if (!Owner.TryGetComponent(out ISolutionInteractionsComponent? ownerSolution))
return false;
var target = eventArgs.Target;
if (!target.TryGetComponent(out ISolutionInteractionsComponent? targetSolution))
{
return false;
}
if (CanReceive && target.TryGetComponent(out ReagentTankComponent? tank)
&& ownerSolution.CanRefill && targetSolution.CanDrain)
{
var transferred = DoTransfer(targetSolution, ownerSolution, tank.TransferAmount, eventArgs.User);
if (transferred > 0)
{
var toTheBrim = ownerSolution.RefillSpaceAvailable == 0;
var msg = toTheBrim
? "You fill {0:TheName} to the brim with {1}u from {2:theName}"
: "You fill {0:TheName} with {1}u from {2:theName}";
target.PopupMessage(eventArgs.User, Loc.GetString(msg, Owner, transferred, target));
return true;
}
}
if (CanSend && targetSolution.CanRefill && ownerSolution.CanDrain)
{
var transferred = DoTransfer(ownerSolution, targetSolution, TransferAmount, eventArgs.User);
if (transferred > 0)
{
Owner.PopupMessage(eventArgs.User, Loc.GetString("You transfer {0}u to {1:theName}.",
transferred, target));
return true;
}
}
return true;
}
/// <returns>The actual amount transferred.</returns>
private static ReagentUnit DoTransfer(
ISolutionInteractionsComponent source,
ISolutionInteractionsComponent target,
ReagentUnit amount,
IEntity user)
{
if (source.DrainAvailable == 0)
{
source.Owner.PopupMessage(user, Loc.GetString("{0:TheName} is empty!", source.Owner));
return ReagentUnit.Zero;
}
if (target.RefillSpaceAvailable == 0)
{
target.Owner.PopupMessage(user, Loc.GetString("{0:TheName} is full!", target.Owner));
return ReagentUnit.Zero;
}
var actualAmount =
ReagentUnit.Min(amount, ReagentUnit.Min(source.DrainAvailable, target.RefillSpaceAvailable));
var solution = source.Drain(actualAmount);
target.Refill(solution);
return actualAmount;
}
}
}

View File

@@ -0,0 +1,282 @@
using System.Diagnostics.CodeAnalysis;
using Content.Server.Administration;
using Content.Server.Eui;
using Content.Server.GameObjects.Components.GUI;
using Content.Shared.Administration;
using Content.Shared.Chemistry;
using Content.Shared.Eui;
using Content.Shared.GameObjects.Components.Chemistry;
using Content.Shared.GameObjects.EntitySystems.ActionBlocker;
using Content.Shared.GameObjects.Verbs;
using Robust.Server.Interfaces.GameObjects;
using Robust.Server.Interfaces.Player;
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
#nullable enable
namespace Content.Server.GameObjects.Components.Chemistry
{
internal abstract class SolutionTransferVerbBase : GlobalVerb
{
protected static bool GetHeldSolution(
IEntity holder,
[NotNullWhen(true)]
out IEntity? held,
[NotNullWhen(true)]
out ISolutionInteractionsComponent? heldSolution)
{
if (!holder.TryGetComponent(out HandsComponent? hands)
|| hands.GetActiveHand == null
|| !hands.GetActiveHand.Owner.TryGetComponent(out heldSolution))
{
held = null;
heldSolution = null;
return false;
}
held = heldSolution.Owner;
return true;
}
}
/// <summary>
/// Transfers solution from the held container to the target container.
/// </summary>
[GlobalVerb]
internal sealed class SolutionFillTargetVerb : SolutionTransferVerbBase
{
public override void GetData(IEntity user, IEntity target, VerbData data)
{
if (!target.TryGetComponent(out ISolutionInteractionsComponent? targetSolution) ||
!ActionBlockerSystem.CanInteract(user) ||
!GetHeldSolution(user, out var source, out var sourceSolution) ||
source != target ||
!sourceSolution.CanDrain ||
!targetSolution.CanRefill)
{
data.Visibility = VerbVisibility.Invisible;
return;
}
data.Visibility = VerbVisibility.Visible;
data.Text = Loc.GetString("Transfer liquid from [{0}] to [{1}].", source.Name, target.Name);
}
public override void Activate(IEntity user, IEntity target)
{
if (!GetHeldSolution(user, out _, out var handSolutionComp))
{
return;
}
if (!handSolutionComp.CanDrain ||
!target.TryGetComponent(out ISolutionInteractionsComponent? targetComp) ||
!targetComp.CanRefill)
{
return;
}
var transferQuantity = ReagentUnit.Min(
targetComp.RefillSpaceAvailable,
handSolutionComp.DrainAvailable,
ReagentUnit.New(10));
if (transferQuantity <= 0)
{
return;
}
var transferSolution = handSolutionComp.Drain(transferQuantity);
targetComp.Refill(transferSolution);
}
}
/// <summary>
/// Transfers solution from a target container to the held container.
/// </summary>
[GlobalVerb]
internal sealed class SolutionDrainTargetVerb : SolutionTransferVerbBase
{
public override void GetData(IEntity user, IEntity target, VerbData data)
{
if (!target.TryGetComponent(out ISolutionInteractionsComponent? sourceSolution) ||
!ActionBlockerSystem.CanInteract(user) ||
!GetHeldSolution(user, out var held, out var targetSolution) ||
!sourceSolution.CanDrain ||
!targetSolution.CanRefill)
{
data.Visibility = VerbVisibility.Invisible;
return;
}
data.Visibility = VerbVisibility.Visible;
data.Text = Loc.GetString("Transfer liquid from [{0}] to [{1}].", held.Name, target.Name);
}
public override void Activate(IEntity user, IEntity target)
{
if (!GetHeldSolution(user, out _, out var targetComp))
{
return;
}
if (!targetComp.CanRefill ||
!target.TryGetComponent(out ISolutionInteractionsComponent? sourceComp) ||
!sourceComp.CanDrain)
{
return;
}
var transferQuantity = ReagentUnit.Min(
targetComp.RefillSpaceAvailable,
sourceComp.DrainAvailable,
ReagentUnit.New(10));
if (transferQuantity <= 0)
{
return;
}
var transferSolution = sourceComp.Drain(transferQuantity);
targetComp.Refill(transferSolution);
}
}
[GlobalVerb]
internal sealed class AdminAddReagentVerb : GlobalVerb
{
public override bool RequireInteractionRange => false;
public override bool BlockedByContainers => false;
private const AdminFlags ReqFlags = AdminFlags.Fun;
private static void OpenAddReagentMenu(IPlayerSession player, IEntity target)
{
var euiMgr = IoCManager.Resolve<EuiManager>();
euiMgr.OpenEui(new AdminAddReagentEui(target), player);
}
public override void GetData(IEntity user, IEntity target, VerbData data)
{
// ISolutionInteractionsComponent doesn't exactly have an interface for "admin tries to refill this", so...
// Still have a path for SolutionContainerComponent in case it doesn't allow direct refilling.
if (!target.HasComponent<SolutionContainerComponent>()
&& !(target.TryGetComponent(out ISolutionInteractionsComponent? interactions)
&& interactions.CanInject))
{
data.Visibility = VerbVisibility.Invisible;
return;
}
data.Text = Loc.GetString("Add Reagent...");
data.CategoryData = VerbCategories.Debug;
data.Visibility = VerbVisibility.Invisible;
var adminManager = IoCManager.Resolve<IAdminManager>();
if (user.TryGetComponent<IActorComponent>(out var player))
{
if (adminManager.HasAdminFlag(player.playerSession, ReqFlags))
{
data.Visibility = VerbVisibility.Visible;
}
}
}
public override void Activate(IEntity user, IEntity target)
{
var groupController = IoCManager.Resolve<IAdminManager>();
if (user.TryGetComponent<IActorComponent>(out var player))
{
if (groupController.HasAdminFlag(player.playerSession, ReqFlags))
OpenAddReagentMenu(player.playerSession, target);
}
}
private sealed class AdminAddReagentEui : BaseEui
{
private readonly IEntity _target;
[Dependency] private readonly IAdminManager _adminManager = default!;
public AdminAddReagentEui(IEntity target)
{
_target = target;
IoCManager.InjectDependencies(this);
}
public override void Opened()
{
StateDirty();
}
public override EuiStateBase GetNewState()
{
if (_target.TryGetComponent(out SolutionContainerComponent? container))
{
return new AdminAddReagentEuiState
{
CurVolume = container.CurrentVolume,
MaxVolume = container.MaxVolume
};
}
if (_target.TryGetComponent(out ISolutionInteractionsComponent? interactions))
{
return new AdminAddReagentEuiState
{
// We don't exactly have an absolute total volume so good enough.
CurVolume = ReagentUnit.Zero,
MaxVolume = interactions.InjectSpaceAvailable
};
}
return new AdminAddReagentEuiState
{
CurVolume = ReagentUnit.Zero,
MaxVolume = ReagentUnit.Zero
};
}
public override void HandleMessage(EuiMessageBase msg)
{
switch (msg)
{
case AdminAddReagentEuiMsg.Close:
Close();
break;
case AdminAddReagentEuiMsg.DoAdd doAdd:
// Double check that user wasn't de-adminned in the mean time...
// Or the target was deleted.
if (!_adminManager.HasAdminFlag(Player, ReqFlags) || _target.Deleted)
{
Close();
return;
}
var id = doAdd.ReagentId;
var amount = doAdd.Amount;
if (_target.TryGetComponent(out SolutionContainerComponent? container))
{
container.TryAddReagent(id, amount, out _);
}
else if (_target.TryGetComponent(out ISolutionInteractionsComponent? interactions))
{
var solution = new Solution(id, amount);
interactions.Inject(solution);
}
StateDirty();
if (doAdd.CloseAfter)
Close();
break;
}
}
}
}
}

View File

@@ -109,19 +109,22 @@ namespace Content.Server.GameObjects.Components
return false;
}
async Task IAfterInteract.AfterInteract(AfterInteractEventArgs eventArgs)
async Task<bool> IAfterInteract.AfterInteract(AfterInteractEventArgs eventArgs)
{
if (!eventArgs.InRangeUnobstructed(ignoreInsideBlocker: false, popup: true,
collisionMask: Shared.Physics.CollisionGroup.MobImpassable)) return;
collisionMask: Shared.Physics.CollisionGroup.MobImpassable))
{
return true;
}
if (Charges <= 0)
{
eventArgs.User.PopupMessage(Loc.GetString("Not enough left."));
return;
return true;
}
var entityManager = IoCManager.Resolve<IServerEntityManager>();
var entity = entityManager.SpawnEntity("CrayonDecal", eventArgs.ClickLocation);
if (entity.TryGetComponent(out AppearanceComponent? appearance))
{
@@ -138,6 +141,7 @@ namespace Content.Server.GameObjects.Components
// Decrease "Ammo"
Charges--;
Dirty();
return true;
}
void IDropped.Dropped(DroppedEventArgs eventArgs)

View File

@@ -107,9 +107,10 @@ namespace Content.Server.GameObjects.Components.Culinary
serializer.DataField(ref _breakSound, "breakSound", "/Audio/Items/snap.ogg");
}
async Task IAfterInteract.AfterInteract(AfterInteractEventArgs eventArgs)
async Task<bool> IAfterInteract.AfterInteract(AfterInteractEventArgs eventArgs)
{
TryUseUtensil(eventArgs.User, eventArgs.Target);
return true;
}
private void TryUseUtensil(IEntity user, IEntity? target)

View File

@@ -62,14 +62,16 @@ namespace Content.Server.GameObjects.Components.Fluids
Owner.EnsureComponentWarn(out SolutionContainerComponent _);
}
async Task IAfterInteract.AfterInteract(AfterInteractEventArgs eventArgs)
async Task<bool> IAfterInteract.AfterInteract(AfterInteractEventArgs eventArgs)
{
if (!Owner.TryGetComponent(out SolutionContainerComponent? contents)) return;
if (!eventArgs.InRangeUnobstructed(ignoreInsideBlocker: true, popup: true)) return;
if (!Owner.TryGetComponent(out SolutionContainerComponent? contents))
return false;
if (!eventArgs.InRangeUnobstructed(ignoreInsideBlocker: true, popup: true))
return false;
if (CurrentVolume <= 0)
{
return;
return true;
}
if (eventArgs.Target == null)
@@ -77,12 +79,12 @@ namespace Content.Server.GameObjects.Components.Fluids
// Drop the liquid on the mop on to the ground
contents.SplitSolution(CurrentVolume).SpillAt(eventArgs.ClickLocation, "PuddleSmear");
return;
return true;
}
if (!eventArgs.Target.TryGetComponent(out PuddleComponent? puddleComponent))
{
return;
return true;
}
// Essentially pickup either:
// - _pickupAmount,
@@ -101,7 +103,7 @@ namespace Content.Server.GameObjects.Components.Fluids
}
else
{
return;
return true;
}
}
else
@@ -121,12 +123,12 @@ namespace Content.Server.GameObjects.Components.Fluids
// Give some visual feedback shit's happening (for anyone who can't hear sound)
Owner.PopupMessage(eventArgs.User, Loc.GetString("Swish"));
if (string.IsNullOrWhiteSpace(_pickupSound))
if (!string.IsNullOrWhiteSpace(_pickupSound))
{
return;
EntitySystem.Get<AudioSystem>().PlayFromEntity(_pickupSound, Owner);
}
EntitySystem.Get<AudioSystem>().PlayFromEntity(_pickupSound, Owner);
return true;
}
}
}

View File

@@ -1,6 +1,5 @@
using Content.Server.GameObjects.Components.Chemistry;
using Content.Shared.Chemistry;
using Content.Shared.GameObjects.EntitySystems;
using Content.Shared.Chemistry;
using Content.Shared.GameObjects.Components.Chemistry;
using Content.Shared.GameObjects.EntitySystems.ActionBlocker;
using Content.Shared.GameObjects.Verbs;
using Content.Shared.Interfaces;
@@ -24,34 +23,37 @@ namespace Content.Server.GameObjects.Components.Fluids
protected override void GetData(IEntity user, SpillableComponent component, VerbData data)
{
if (!ActionBlockerSystem.CanInteract(user) ||
!component.Owner.TryGetComponent(out SolutionContainerComponent solutionComponent) ||
!solutionComponent.CanRemoveSolutions)
!component.Owner.TryGetComponent(out ISolutionInteractionsComponent solutionComponent) ||
!solutionComponent.CanDrain)
{
data.Visibility = VerbVisibility.Invisible;
return;
}
data.Text = Loc.GetString("Spill liquid");
data.Visibility = solutionComponent.CurrentVolume > ReagentUnit.Zero ? VerbVisibility.Visible : VerbVisibility.Disabled;
data.Visibility = solutionComponent.DrainAvailable > ReagentUnit.Zero
? VerbVisibility.Visible
: VerbVisibility.Disabled;
}
protected override void Activate(IEntity user, SpillableComponent component)
{
if (component.Owner.TryGetComponent<SolutionContainerComponent>(out var solutionComponent))
if (component.Owner.TryGetComponent<ISolutionInteractionsComponent>(out var solutionComponent))
{
if (!solutionComponent.CanRemoveSolutions)
if (!solutionComponent.CanDrain)
{
user.PopupMessage(user, Loc.GetString("You can't pour anything from {0:theName}!", component.Owner));
user.PopupMessage(user,
Loc.GetString("You can't pour anything from {0:theName}!", component.Owner));
}
if (solutionComponent.CurrentVolume.Float() <= 0)
if (solutionComponent.DrainAvailable <= 0)
{
user.PopupMessage(user, Loc.GetString("{0:theName} is empty!", component.Owner));
}
// Need this as when we split the component's owner may be deleted
var entityLocation = component.Owner.Transform.Coordinates;
var solution = solutionComponent.SplitSolution(solutionComponent.CurrentVolume);
var solution = solutionComponent.Drain(solutionComponent.DrainAvailable);
solution.SpillAt(entityLocation, "PuddleSmear");
}
}

View File

@@ -100,34 +100,34 @@ namespace Content.Server.GameObjects.Components.Fluids
serializer.DataField(ref _safety, "safety", true);
}
async Task IAfterInteract.AfterInteract(AfterInteractEventArgs eventArgs)
async Task<bool> IAfterInteract.AfterInteract(AfterInteractEventArgs eventArgs)
{
if (!ActionBlockerSystem.CanInteract(eventArgs.User))
return;
return false;
if (_hasSafety && _safety)
{
Owner.PopupMessage(eventArgs.User, Loc.GetString("Its safety is on!"));
return;
return true;
}
if (CurrentVolume <= 0)
{
Owner.PopupMessage(eventArgs.User, Loc.GetString("It's empty!"));
return;
return true;
}
var curTime = _gameTiming.CurTime;
if(curTime < _cooldownEnd)
return;
return true;
var playerPos = eventArgs.User.Transform.Coordinates;
if (eventArgs.ClickLocation.GetGridId(_serverEntityManager) != playerPos.GetGridId(_serverEntityManager))
return;
return true;
if (!Owner.TryGetComponent(out SolutionContainerComponent contents))
return;
return true;
var direction = (eventArgs.ClickLocation.Position - playerPos.Position).Normalized;
var threeQuarters = direction * 0.75f;
@@ -183,6 +183,8 @@ namespace Content.Server.GameObjects.Components.Fluids
cooldown.CooldownStart = _lastUseTime;
cooldown.CooldownEnd = _cooldownEnd;
}
return true;
}
public bool UseEntity(UseEntityEventArgs eventArgs)

View File

@@ -21,9 +21,10 @@ namespace Content.Server.GameObjects.Components.Interactable
public override string Name => "TilePrying";
private bool _toolComponentNeeded = true;
public async Task AfterInteract(AfterInteractEventArgs eventArgs)
public async Task<bool> AfterInteract(AfterInteractEventArgs eventArgs)
{
TryPryTile(eventArgs.User, eventArgs.ClickLocation);
return true;
}
public override void ExposeData(ObjectSerializer serializer)

View File

@@ -2,6 +2,7 @@
using System;
using System.Threading.Tasks;
using Content.Server.Atmos;
using Content.Server.Explosions;
using Content.Server.GameObjects.Components.Chemistry;
using Content.Server.GameObjects.Components.Items.Storage;
using Content.Server.GameObjects.EntitySystems;
@@ -10,12 +11,15 @@ using Content.Server.Interfaces.GameObjects;
using Content.Server.Utility;
using Content.Shared.Chemistry;
using Content.Shared.GameObjects;
using Content.Shared.GameObjects.Components.Chemistry;
using Content.Shared.GameObjects.Components.Interactable;
using Content.Shared.GameObjects.EntitySystems;
using Content.Shared.Interfaces;
using Content.Shared.Interfaces.GameObjects.Components;
using Robust.Server.GameObjects;
using Robust.Server.GameObjects.EntitySystems;
using Robust.Shared.GameObjects;
using Robust.Shared.GameObjects.Systems;
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
@@ -29,7 +33,7 @@ namespace Content.Server.GameObjects.Components.Interactable
[ComponentReference(typeof(ToolComponent))]
[ComponentReference(typeof(IToolComponent))]
[ComponentReference(typeof(IHotItem))]
public class WelderComponent : ToolComponent, IExamine, IUse, ISuicideAct, ISolutionChange, IHotItem
public class WelderComponent : ToolComponent, IExamine, IUse, ISuicideAct, ISolutionChange, IHotItem, IAfterInteract
{
[Dependency] private readonly IEntitySystemManager _entitySystemManager = default!;
@@ -293,5 +297,38 @@ namespace Content.Server.GameObjects.Components.Interactable
}
async Task<bool> IAfterInteract.AfterInteract(AfterInteractEventArgs eventArgs)
{
if (eventArgs.Target == null || !eventArgs.CanReach)
{
return false;
}
if (eventArgs.Target.TryGetComponent(out ReagentTankComponent? tank)
&& tank.TankType == ReagentTankType.Fuel
&& eventArgs.Target.TryGetComponent(out ISolutionInteractionsComponent? targetSolution)
&& targetSolution.CanDrain
&& _solutionComponent != null)
{
if (WelderLit)
{
// Oh no no
eventArgs.Target.SpawnExplosion();
return true;
}
var trans = ReagentUnit.Min(_solutionComponent.EmptyVolume, targetSolution.DrainAvailable);
if (trans > 0)
{
var drained = targetSolution.Drain(trans);
_solutionComponent.TryAddSolution(drained);
EntitySystem.Get<AudioSystem>().PlayFromEntity("/Audio/Effects/refill.ogg", Owner);
eventArgs.Target.PopupMessage(eventArgs.User, Loc.GetString("Welder refueled"));
}
}
return true;
}
}
}

View File

@@ -1,10 +1,52 @@
using Robust.Shared.GameObjects;
using System.Threading.Tasks;
using Content.Server.GameObjects.Components.Chemistry;
using Content.Shared.Chemistry;
using Content.Shared.GameObjects.Components.Chemistry;
using Content.Shared.Interfaces;
using Content.Shared.Interfaces.GameObjects.Components;
using Robust.Server.GameObjects.EntitySystems;
using Robust.Shared.GameObjects;
using Robust.Shared.GameObjects.Systems;
using Robust.Shared.Localization;
#nullable enable
namespace Content.Server.GameObjects.Components.Items
{
[RegisterComponent]
public class FireExtinguisherComponent : Component
public class FireExtinguisherComponent : Component, IAfterInteract
{
public override string Name => "FireExtinguisher";
// Higher priority than sprays.
int IAfterInteract.Priority => 1;
async Task<bool> IAfterInteract.AfterInteract(AfterInteractEventArgs eventArgs)
{
if (eventArgs.Target == null || !eventArgs.CanReach)
{
return false;
}
if (eventArgs.Target.TryGetComponent(out ReagentTankComponent? tank)
&& eventArgs.Target.TryGetComponent(out ISolutionInteractionsComponent? targetSolution)
&& targetSolution.CanDrain
&& Owner.TryGetComponent(out SolutionContainerComponent? container))
{
var trans = ReagentUnit.Min(container.EmptyVolume, targetSolution.DrainAvailable);
if (trans > 0)
{
var drained = targetSolution.Drain(trans);
container.TryAddSolution(drained);
EntitySystem.Get<AudioSystem>().PlayFromEntity("/Audio/Effects/refill.ogg", Owner);
eventArgs.Target.PopupMessage(eventArgs.User, Loc.GetString("{0:TheName} is now refilled", Owner));
}
return true;
}
return false;
}
}
}

View File

@@ -56,10 +56,14 @@ namespace Content.Server.GameObjects.Components.Items
EntitySystem.Get<AudioSystem>().PlayAtCoords("/Audio/Items/genhit.ogg", location, AudioHelpers.WithVariation(0.125f));
}
public async Task AfterInteract(AfterInteractEventArgs eventArgs)
public async Task<bool> AfterInteract(AfterInteractEventArgs eventArgs)
{
if (!eventArgs.InRangeUnobstructed(ignoreInsideBlocker: true, popup: true)) return;
if (!Owner.TryGetComponent(out StackComponent stack)) return;
if (!eventArgs.InRangeUnobstructed(ignoreInsideBlocker: true, popup: true))
return true;
if (!Owner.TryGetComponent(out StackComponent stack))
return true;
var mapManager = IoCManager.Resolve<IMapManager>();
var location = eventArgs.ClickLocation.AlignWithClosestGridTile();
@@ -88,10 +92,9 @@ namespace Content.Server.GameObjects.Components.Items
PlaceAt(mapGrid, location, _tileDefinitionManager[_outputTiles[0]].TileId, mapGrid.TileSize / 2f);
break;
}
}
return true;
}
}
}

View File

@@ -32,17 +32,17 @@ namespace Content.Server.GameObjects.Components.Items.RCD
message.AddMarkup(Loc.GetString("It holds {0} charges.", refillAmmo));
}
public async Task AfterInteract(AfterInteractEventArgs eventArgs)
public async Task<bool> AfterInteract(AfterInteractEventArgs eventArgs)
{
if (eventArgs.Target == null || !eventArgs.Target.TryGetComponent(out RCDComponent rcdComponent) || !eventArgs.User.TryGetComponent(out IHandsComponent hands))
{
return;
return false;
}
if (rcdComponent.maxAmmo - rcdComponent._ammo < refillAmmo)
{
rcdComponent.Owner.PopupMessage(eventArgs.User, Loc.GetString("The RCD is full!"));
return;
return true;
}
rcdComponent._ammo = Math.Min(rcdComponent.maxAmmo, rcdComponent._ammo + refillAmmo);
@@ -51,6 +51,7 @@ namespace Content.Server.GameObjects.Components.Items.RCD
//Deleting a held item causes a lot of errors
hands.Drop(Owner, false);
Owner.Delete();
return true;
}
}
}

View File

@@ -94,7 +94,7 @@ namespace Content.Server.GameObjects.Components.Items.RCD
message.AddMarkup(Loc.GetString("It's currently on {0} mode, and holds {1} charges.",_mode.ToString(), _ammo));
}
public async Task AfterInteract(AfterInteractEventArgs eventArgs)
public async Task<bool> AfterInteract(AfterInteractEventArgs eventArgs)
{
//No changing mode mid-RCD
var startingMode = _mode;
@@ -116,7 +116,7 @@ namespace Content.Server.GameObjects.Components.Items.RCD
var result = await doAfterSystem.DoAfter(doAfterEventArgs);
if (result == DoAfterStatus.Cancelled)
{
return;
return true;
}
switch (_mode)
@@ -146,12 +146,12 @@ namespace Content.Server.GameObjects.Components.Items.RCD
airlock.Transform.LocalRotation = Owner.Transform.LocalRotation; //Now apply icon smoothing.
break;
default:
return; //I don't know why this would happen, but sure I guess. Get out of here invalid state!
return true; //I don't know why this would happen, but sure I guess. Get out of here invalid state!
}
_entitySystemManager.GetEntitySystem<AudioSystem>().PlayFromEntity("/Audio/Items/deconstruct.ogg", Owner);
_ammo--;
return true;
}
private bool IsRCDStillValid(AfterInteractEventArgs eventArgs, IMapGrid mapGrid, TileRef tile, Vector2i snapPos, RcdMode startingMode)

View File

@@ -13,6 +13,7 @@ using Content.Server.Utility;
using Content.Shared.Chemistry;
using Content.Shared.GameObjects.Components.Body;
using Content.Shared.GameObjects.Components.Body.Part;
using Content.Shared.GameObjects.Components.Chemistry;
using Content.Shared.GameObjects.Components.Power;
using Content.Shared.GameObjects.EntitySystems;
using Content.Shared.Interfaces;
@@ -213,10 +214,10 @@ namespace Content.Server.GameObjects.Components.Kitchen
return false;
}
if (itemEntity.TryGetComponent<PourableComponent>(out var attackPourable))
if (itemEntity.TryGetComponent<SolutionTransferComponent>(out var attackPourable))
{
if (!itemEntity.TryGetComponent<SolutionContainerComponent>(out var attackSolution)
|| !attackSolution.CanRemoveSolutions)
if (!itemEntity.TryGetComponent<ISolutionInteractionsComponent>(out var attackSolution)
|| !attackSolution.CanDrain)
{
return false;
}
@@ -235,7 +236,7 @@ namespace Content.Server.GameObjects.Components.Kitchen
}
//Move units from attackSolution to targetSolution
var removedSolution = attackSolution.SplitSolution(realTransferAmount);
var removedSolution = attackSolution.Drain(realTransferAmount);
if (!solution.TryAddSolution(removedSolution))
{
return false;

View File

@@ -26,39 +26,41 @@ namespace Content.Server.GameObjects.Components.Medical
serializer.DataField(this, h => h.Heal, "heal", new Dictionary<DamageType, int>());
}
public async Task AfterInteract(AfterInteractEventArgs eventArgs)
public async Task<bool> AfterInteract(AfterInteractEventArgs eventArgs)
{
if (eventArgs.Target == null)
{
return;
return false;
}
if (!eventArgs.Target.TryGetComponent(out IDamageableComponent damageable))
{
return;
return true;
}
if (!ActionBlockerSystem.CanInteract(eventArgs.User))
{
return;
return true;
}
if (eventArgs.User != eventArgs.Target &&
!eventArgs.InRangeUnobstructed(ignoreInsideBlocker: true, popup: true))
{
return;
return true;
}
if (Owner.TryGetComponent(out StackComponent stack) &&
!stack.Use(1))
{
return;
return true;
}
foreach (var (type, amount) in Heal)
{
damageable.ChangeDamage(type, -amount, true);
}
return true;
}
}
}

View File

@@ -3,11 +3,9 @@ using System.Threading.Tasks;
using Content.Server.GameObjects.Components.Body.Behavior;
using Content.Server.GameObjects.Components.Chemistry;
using Content.Server.GameObjects.Components.Fluids;
using Content.Server.GameObjects.EntitySystems;
using Content.Shared.Audio;
using Content.Shared.Chemistry;
using Content.Shared.GameObjects.Components.Body;
using Content.Shared.GameObjects.Components.Body.Mechanism;
using Content.Shared.GameObjects.Components.Nutrition;
using Content.Shared.GameObjects.EntitySystems;
using Content.Shared.Interfaces;
@@ -75,7 +73,7 @@ namespace Content.Server.GameObjects.Components.Nutrition
_contents = Owner.AddComponent<SolutionContainerComponent>();
}
_contents.Capabilities = SolutionContainerCaps.AddTo | SolutionContainerCaps.RemoveFrom;
_contents.Capabilities = SolutionContainerCaps.Refillable | SolutionContainerCaps.Drainable;
Opened = _defaultToOpened;
UpdateAppearance();
}
@@ -113,9 +111,10 @@ namespace Content.Server.GameObjects.Components.Nutrition
}
//Force feeding a drink to someone.
async Task IAfterInteract.AfterInteract(AfterInteractEventArgs eventArgs)
async Task<bool> IAfterInteract.AfterInteract(AfterInteractEventArgs eventArgs)
{
TryUseDrink(eventArgs.Target, forced: true);
return true;
}
public void Examine(FormattedMessage message, bool inDetailsRange)
@@ -131,7 +130,7 @@ namespace Content.Server.GameObjects.Components.Nutrition
private bool TryUseDrink(IEntity target, bool forced = false)
{
if (target == null || !_contents.CanRemoveSolutions)
if (target == null || !_contents.CanDrain)
{
return false;
}

View File

@@ -100,14 +100,15 @@ namespace Content.Server.GameObjects.Components.Nutrition
}
// Feeding someone else
async Task IAfterInteract.AfterInteract(AfterInteractEventArgs eventArgs)
async Task<bool> IAfterInteract.AfterInteract(AfterInteractEventArgs eventArgs)
{
if (eventArgs.Target == null)
{
return;
return false;
}
TryUseFood(eventArgs.User, eventArgs.Target);
return true;
}
public virtual bool TryUseFood(IEntity? user, IEntity? target, UtensilComponent? utensilUsed = null)

View File

@@ -23,7 +23,7 @@ using Robust.Shared.ViewVariables;
namespace Content.Server.GameObjects.Components.Portal
{
[RegisterComponent]
public class TeleporterComponent : Component, IAfterInteract
public class TeleporterComponent : Component, IAfterInteract
{
[Dependency] private readonly IServerEntityManager _serverEntityManager = default!;
[Dependency] private readonly IRobustRandom _spreadRandom = default!;
@@ -78,7 +78,7 @@ namespace Content.Server.GameObjects.Components.Portal
_state = newState;
}
async Task IAfterInteract.AfterInteract(AfterInteractEventArgs eventArgs)
async Task<bool> IAfterInteract.AfterInteract(AfterInteractEventArgs eventArgs)
{
if (_teleporterType == TeleporterType.Directed)
{
@@ -89,6 +89,8 @@ namespace Content.Server.GameObjects.Components.Portal
{
TryRandomTeleport(eventArgs.User);
}
return true;
}
public void TryDirectedTeleport(IEntity user, MapCoordinates mapCoords)

View File

@@ -34,28 +34,29 @@ namespace Content.Server.GameObjects.Components.Power
}
/// <inheritdoc />
public async Task AfterInteract(AfterInteractEventArgs eventArgs)
public async Task<bool> AfterInteract(AfterInteractEventArgs eventArgs)
{
if (_wirePrototypeID == null)
return;
if (!eventArgs.InRangeUnobstructed(ignoreInsideBlocker: true, popup: true)) return;
return true;
if (!eventArgs.InRangeUnobstructed(ignoreInsideBlocker: true, popup: true))
return true;
if(!_mapManager.TryGetGrid(eventArgs.ClickLocation.GetGridId(Owner.EntityManager), out var grid))
return;
return true;
var snapPos = grid.SnapGridCellFor(eventArgs.ClickLocation, SnapGridOffset.Center);
var snapCell = grid.GetSnapGridCell(snapPos, SnapGridOffset.Center);
if(grid.GetTileRef(snapPos).Tile.IsEmpty)
return;
return true;
foreach (var snapComp in snapCell)
{
if (snapComp.Owner.TryGetComponent<WireComponent>(out var wire) && wire.WireType == _blockingWireType)
{
return;
return true;
}
}
if (Owner.TryGetComponent<StackComponent>(out var stack) && !stack.Use(1))
return;
return true;
Owner.EntityManager.SpawnEntity(_wirePrototypeID, grid.GridTileToLocal(snapPos));
return true;
}
}
}

View File

@@ -146,11 +146,11 @@ namespace Content.Server.GameObjects.Components.Weapon.Ranged.Ammunition
return entity;
}
async Task IAfterInteract.AfterInteract(AfterInteractEventArgs eventArgs)
async Task<bool> IAfterInteract.AfterInteract(AfterInteractEventArgs eventArgs)
{
if (eventArgs.Target == null)
{
return;
return false;
}
// This area is dirty but not sure of an easier way to do it besides add an interface or somethin
@@ -203,6 +203,8 @@ namespace Content.Server.GameObjects.Components.Weapon.Ranged.Ammunition
{
UpdateAppearance();
}
return true;
}
async Task<bool> IInteractUsing.InteractUsing(InteractUsingEventArgs eventArgs)

View File

@@ -416,13 +416,8 @@ namespace Content.Server.GameObjects.EntitySystems.Click
return;
}
var afterInteracts = weapon.GetAllComponents<IAfterInteract>().ToList();
var afterInteractEventArgs = new AfterInteractEventArgs(user, clickLocation, null, canReach);
foreach (var afterInteract in afterInteracts)
{
await afterInteract.AfterInteract(afterInteractEventArgs);
}
await DoAfterInteract(weapon, afterInteractEventArgs);
}
/// <summary>
@@ -465,13 +460,9 @@ namespace Content.Server.GameObjects.EntitySystems.Click
}
// If we aren't directly attacking the nearby object, lets see if our item has an after attack we can do
var afterAttacks = weapon.GetAllComponents<IAfterInteract>().ToList();
var afterAttackEventArgs = new AfterInteractEventArgs(user, clickLocation, attacked, canReach: true);
foreach (var afterAttack in afterAttacks)
{
await afterAttack.AfterInteract(afterAttackEventArgs);
}
await DoAfterInteract(weapon, afterAttackEventArgs);
}
/// <summary>
@@ -835,13 +826,21 @@ namespace Content.Server.GameObjects.EntitySystems.Click
if (afterAtkMsg.Handled)
return;
var afterAttacks = weapon.GetAllComponents<IAfterInteract>().ToList();
// See if we have a ranged attack interaction
var afterAttackEventArgs = new AfterInteractEventArgs(user, clickLocation, attacked, canReach: false);
await DoAfterInteract(weapon, afterAttackEventArgs);
}
private static async Task DoAfterInteract(IEntity weapon, AfterInteractEventArgs afterAttackEventArgs)
{
var afterAttacks = weapon.GetAllComponents<IAfterInteract>().OrderByDescending(x => x.Priority).ToList();
//See if we have a ranged attack interaction
foreach (var afterAttack in afterAttacks)
{
await afterAttack.AfterInteract(afterAttackEventArgs);
if (await afterAttack.AfterInteract(afterAttackEventArgs))
{
return;
}
}
}

View File

@@ -92,7 +92,7 @@ namespace Content.Shared.Chemistry
return "";
}
var majorReagent = Contents.OrderByDescending(reagent => reagent.Quantity).First(); ;
var majorReagent = Contents.OrderByDescending(reagent => reagent.Quantity).First();
return majorReagent.ReagentId;
}

View File

@@ -1,46 +0,0 @@
using System;
using Robust.Shared.Serialization;
namespace Content.Shared.Chemistry
{
/// <summary>
/// These are the defined capabilities of a container of a solution.
/// </summary>
[Flags]
[Serializable, NetSerializable]
public enum SolutionContainerCaps
{
None = 0,
/// <summary>
/// Can solutions be added into the container?
/// </summary>
AddTo = 1,
/// <summary>
/// Can solutions be removed from the container?
/// </summary>
RemoveFrom = 2,
/// <summary>
/// Allows the container to be placed in a <c>ReagentDispenserComponent</c>.
/// <para>Otherwise it's considered to be too large or the improper shape to fit.</para>
/// <para>Allows us to have obscenely large containers that are harder to abuse in chem dispensers
/// since they can't be placed directly in them.</para>
/// </summary>
FitsInDispenser = 4,
/// <summary>
/// Can people examine the solution in the container or is it impossible to see?
/// </summary>
CanExamine = 8,
}
public static class SolutionContainerCapsHelpers
{
public static bool HasCap(this SolutionContainerCaps cap, SolutionContainerCaps other)
{
return (cap & other) == other;
}
}
}

View File

@@ -0,0 +1,62 @@
using System;
using Content.Shared.GameObjects.Components.Chemistry;
using Robust.Shared.Serialization;
namespace Content.Shared.Chemistry
{
/// <summary>
/// Define common interaction behaviors for <see cref="SharedSolutionContainerComponent"/>
/// </summary>
/// <seealso cref="ISolutionInteractionsComponent"/>
[Flags]
[Serializable, NetSerializable]
public enum SolutionContainerCaps : ushort
{
None = 0,
/// <summary>
/// Reagents can be added with syringes.
/// </summary>
Injectable = 1 << 0,
/// <summary>
/// Reagents can be removed with syringes.
/// </summary>
Drawable = 1 << 1,
/// <summary>
/// Reagents can be easily added via all reagent containers.
/// Think pouring something into another beaker or into the gas tank of a car.
/// </summary>
Refillable = 1 << 2,
/// <summary>
/// Reagents can be easily removed through any reagent container.
/// Think pouring this or draining from a water tank.
/// </summary>
Drainable = 1 << 3,
/// <summary>
/// The contents of the solution can be examined directly.
/// </summary>
CanExamine = 1 << 4,
/// <summary>
/// Allows the container to be placed in a <c>ReagentDispenserComponent</c>.
/// <para>Otherwise it's considered to be too large or the improper shape to fit.</para>
/// <para>Allows us to have obscenely large containers that are harder to abuse in chem dispensers
/// since they can't be placed directly in them.</para>
/// </summary>
FitsInDispenser = 1 << 5,
OpenContainer = Refillable | Drainable | CanExamine
}
public static class SolutionContainerCapsHelpers
{
public static bool HasCap(this SolutionContainerCaps cap, SolutionContainerCaps other)
{
return (cap & other) == other;
}
}
}

View File

@@ -0,0 +1,84 @@
using Content.Shared.Chemistry;
using Robust.Shared.Interfaces.GameObjects;
namespace Content.Shared.GameObjects.Components.Chemistry
{
/// <summary>
/// High-level solution transferring operations like "what happens when a syringe tries to inject this entity."
/// </summary>
/// <remarks>
/// This interface is most often implemented by using <see cref="SharedSolutionContainerComponent"/>
/// and setting the appropriate <see cref="SolutionContainerCaps"/>
/// </remarks>
public interface ISolutionInteractionsComponent : IComponent
{
//
// INJECTING
//
/// <summary>
/// Whether we CAN POTENTIALLY be injected with solutions by items like syringes.
/// </summary>
/// <remarks>
/// <para>
/// This should NOT change to communicate behavior like "the container is full".
/// Change <see cref="InjectSpaceAvailable"/> to 0 for that.
/// </para>
/// <para>
/// If refilling is allowed (<see cref="CanRefill"/>) you should also always allow injecting.
/// </para>
/// </remarks>
bool CanInject => false;
/// <summary>
/// The amount of solution space available for injecting.
/// </summary>
ReagentUnit InjectSpaceAvailable => ReagentUnit.Zero;
/// <summary>
/// Actually inject reagents.
/// </summary>
void Inject(Solution solution)
{
}
//
// DRAWING
//
bool CanDraw => false;
ReagentUnit DrawAvailable => ReagentUnit.Zero;
Solution Draw(ReagentUnit amount)
{
return new();
}
//
// REFILLING
//
bool CanRefill => false;
ReagentUnit RefillSpaceAvailable => ReagentUnit.Zero;
void Refill(Solution solution)
{
}
//
// DRAINING
//
bool CanDrain => false;
ReagentUnit DrainAvailable => ReagentUnit.Zero;
Solution Drain(ReagentUnit amount)
{
return new();
}
}
}

View File

@@ -20,7 +20,7 @@ namespace Content.Shared.GameObjects.Components.Chemistry
/// <summary>
/// Holds a <see cref="Solution"/> with a limited volume.
/// </summary>
public abstract class SharedSolutionContainerComponent : Component, IExamine
public abstract class SharedSolutionContainerComponent : Component, IExamine, ISolutionInteractionsComponent
{
public override string Name => "SolutionContainer";
@@ -60,9 +60,11 @@ namespace Content.Shared.GameObjects.Components.Chemistry
public bool CanUseWithChemDispenser => Capabilities.HasCap(SolutionContainerCaps.FitsInDispenser);
public bool CanAddSolutions => Capabilities.HasCap(SolutionContainerCaps.AddTo);
public bool CanInject => Capabilities.HasCap(SolutionContainerCaps.Injectable) || CanRefill;
public bool CanDraw => Capabilities.HasCap(SolutionContainerCaps.Drawable) || CanDrain;
public bool CanRemoveSolutions => Capabilities.HasCap(SolutionContainerCaps.RemoveFrom);
public bool CanRefill => Capabilities.HasCap(SolutionContainerCaps.Refillable);
public bool CanDrain => Capabilities.HasCap(SolutionContainerCaps.Drainable);
public override void ExposeData(ObjectSerializer serializer)
{
@@ -71,7 +73,7 @@ namespace Content.Shared.GameObjects.Components.Chemistry
serializer.DataField(this, x => x.CanReact, "canReact", true);
serializer.DataField(this, x => x.MaxVolume, "maxVol", ReagentUnit.New(0));
serializer.DataField(this, x => x.Solution, "contents", new Solution());
serializer.DataField(this, x => x.Capabilities, "caps", SolutionContainerCaps.AddTo | SolutionContainerCaps.RemoveFrom | SolutionContainerCaps.CanExamine);
serializer.DataField(this, x => x.Capabilities, "caps", SolutionContainerCaps.None);
}
public void RemoveAllSolution()
@@ -209,6 +211,43 @@ namespace Content.Shared.GameObjects.Components.Chemistry
message.AddMarkup(Loc.GetString(messageString, colorHex, Loc.GetString(proto.PhysicalDescription)));
}
ReagentUnit ISolutionInteractionsComponent.RefillSpaceAvailable => EmptyVolume;
ReagentUnit ISolutionInteractionsComponent.InjectSpaceAvailable => EmptyVolume;
ReagentUnit ISolutionInteractionsComponent.DrawAvailable => CurrentVolume;
ReagentUnit ISolutionInteractionsComponent.DrainAvailable => CurrentVolume;
void ISolutionInteractionsComponent.Refill(Solution solution)
{
if (!CanRefill)
return;
TryAddSolution(solution);
}
void ISolutionInteractionsComponent.Inject(Solution solution)
{
if (!CanInject)
return;
TryAddSolution(solution);
}
Solution ISolutionInteractionsComponent.Draw(ReagentUnit amount)
{
if (!CanDraw)
return new Solution();
return SplitSolution(amount);
}
Solution ISolutionInteractionsComponent.Drain(ReagentUnit amount)
{
if (!CanDrain)
return new Solution();
return SplitSolution(amount);
}
private void UpdateAppearance()
{
if (Owner.Deleted || !Owner.TryGetComponent<SharedAppearanceComponent>(out var appearance))

View File

@@ -16,10 +16,16 @@ namespace Content.Shared.Interfaces.GameObjects.Components
/// </summary>
public interface IAfterInteract
{
/// <summary>
/// The interaction priority. Higher numbers get called first.
/// </summary>
/// <value>Priority defaults to 0</value>
int Priority => 0;
/// <summary>
/// Called when we interact with nothing, or when we interact with an entity out of range that has no behavior
/// </summary>
Task AfterInteract(AfterInteractEventArgs eventArgs);
Task<bool> AfterInteract(AfterInteractEventArgs eventArgs);
}
public class AfterInteractEventArgs : EventArgs

Binary file not shown.

View File

@@ -534,8 +534,6 @@ entities:
pos: -15.694785,24.608267
rot: -1.5707963267948966 rad
type: Transform
- caps: AddTo, RemoveFrom
type: SolutionContainer
- uid: 60
type: DisgustingSweptSoup
components:
@@ -652,8 +650,6 @@ entities:
pos: -3.470539,16.956116
rot: -1.5707963267948966 rad
type: Transform
- caps: AddTo, RemoveFrom
type: SolutionContainer
- uid: 74
type: Carpet
components:
@@ -703,8 +699,6 @@ entities:
pos: 8.439846,26.712742
rot: 1.5707963267948966 rad
type: Transform
- caps: AddTo, RemoveFrom
type: SolutionContainer
- uid: 81
type: Table
components:
@@ -929,8 +923,6 @@ entities:
- parent: 853
pos: 8.661116,25.513401
type: Transform
- caps: AddTo, RemoveFrom
type: SolutionContainer
- uid: 114
type: BoxDonkpocket
components:
@@ -32236,8 +32228,6 @@ entities:
pos: -8.476567,-17.420076
rot: -1.5707963267948966 rad
type: Transform
- caps: AddTo, RemoveFrom
type: SolutionContainer
- uid: 2443
type: TableWood
components:

View File

@@ -12,7 +12,7 @@
- type: InteractionOutline
- type: SolutionContainer
maxVol: 100
caps: AddTo
caps: Refillable
- type: Appearance
visuals:
- type: MicrowaveVisualizer

View File

@@ -38,8 +38,7 @@
drawWarnings: false
- type: SolutionContainer
maxVol: 200
caps: AddTo
- type: Pourable
caps: Refillable
- type: SnapGrid
offset: Center
- type: Appearance

View File

@@ -33,9 +33,9 @@
acts: ["Destruction"]
- type: SolutionContainer
maxVol: 1500
caps: RemoveFrom
- type: Pourable
transferAmount: 100.0
caps: Drainable
- type: ReagentTank
placement:
snap:
- Wall

View File

@@ -17,9 +17,7 @@
reagents:
- ReagentId: chem.WeldingFuel
Quantity: 1500
- type: DamageOnToolInteract
damage: 200
tools:
- Welding
- type: Anchorable
- type: Pullable
- type: ReagentTank
tankType: Fuel

View File

@@ -20,7 +20,7 @@
# Organs
- type: SolutionContainer
maxVol: 250
caps: AddTo, RemoveFrom
caps: Injectable, Drawable
- type: Bloodstream
max_volume: 100
# StatusEffects

View File

@@ -1,4 +1,4 @@
# TODO: Find remaining cans and move to drinks_cans
# TODO: Find remaining cans and move to drinks_cans
# TODO: Find empty containers (e.g. mug, pitcher) and move to their own yml
# TODO: Move bottles to their own yml
- type: entity
@@ -8,7 +8,7 @@
components:
- type: SolutionContainer
maxVol: 50
- type: Pourable
- type: SolutionTransfer
transferAmount: 5
- type: Drink
- type: Sprite
@@ -55,7 +55,7 @@
- type: SolutionContainer
fillingState: glass
maxVol: 50
- type: Pourable
- type: SolutionTransfer
transferAmount: 5
- type: TransformableContainer

View File

@@ -7,7 +7,7 @@
openSounds: bottleOpenSounds
- type: SolutionContainer
maxVol: 100
- type: Pourable
- type: SolutionTransfer
transferAmount: 5
- type: Sprite
state: icon

View File

@@ -8,12 +8,12 @@
pressurized: true
- type: SolutionContainer
maxVol: 20
caps: AddTo, RemoveFrom
caps: None
contents:
reagents:
- ReagentId: chem.Cola
Quantity: 20
- type: Pourable
- type: SolutionTransfer
transferAmount: 5
- type: Sprite
state: icon

View File

@@ -7,7 +7,7 @@
components:
- type: SolutionContainer
maxVol: 20
- type: Pourable
- type: SolutionTransfer
transferAmount: 5
- type: Drink
isOpen: true

View File

@@ -10,7 +10,7 @@
reagents:
- ReagentId: chem.Flour
Quantity: 50
- type: Pourable
- type: SolutionTransfer
transferAmount: 5
- type: Drink
- type: Sprite

View File

@@ -12,7 +12,7 @@
- type: SolutionContainer
maxVol: 10
- type: Pourable
- type: SolutionTransfer
transferAmount: 5
- type: Drink
isOpen: true

View File

@@ -14,7 +14,7 @@
size: 10
- type: SolutionContainer
maxVol: 100
caps: AddTo, RemoveFrom
caps: Refillable, Drainable
contents:
reagents:
- ReagentId: chem.Water

View File

@@ -17,7 +17,7 @@
netsync: false
- type: SolutionContainer
maxVol: 5
caps: AddTo, RemoveFrom
caps: Injectable, Drawable
- type: entity

View File

@@ -13,8 +13,8 @@
- type: SolutionContainer
fillingState: beaker
maxVol: 50
caps: CanExamine, AddTo, RemoveFrom, FitsInDispenser # can add and remove solutions and fits in the chemmaster.
- type: Pourable
caps: OpenContainer, FitsInDispenser # can add and remove solutions and fits in the chemmaster.
- type: SolutionTransfer
transferAmount: 5.0
- type: Spillable
- type: GlassBeaker
@@ -34,8 +34,8 @@
- type: SolutionContainer
fillingState: beakerlarge
maxVol: 100
caps: CanExamine, AddTo, RemoveFrom, FitsInDispenser
- type: Pourable
caps: OpenContainer, FitsInDispenser
- type: SolutionTransfer
transferAmount: 5.0
- type: Spillable
- type: GlassBeaker
@@ -53,7 +53,7 @@
fillingState: dropper
fillingSteps: 2
maxVol: 5
- type: Pourable
- type: SolutionTransfer
transferAmount: 5.0
- type: Spillable
@@ -85,7 +85,7 @@
- type: Drink
- type: SolutionContainer
maxVol: 30
- type: Pourable
- type: SolutionTransfer
transferAmount: 5
- type: Spillable

View File

@@ -198,8 +198,8 @@
state: cleaner
- type: SolutionContainer
maxVol: 100
caps: AddTo, RemoveFrom
- type: Pourable
caps: Refillable, Drainable
- type: SolutionTransfer
transferAmount: 5.0
- type: Spillable
- type: ItemCooldown

View File

@@ -10,7 +10,7 @@
- ReagentId: chem.Nutriment
Quantity: 10
maxVol: 11 # needs room for water
caps: AddTo
caps: Refillable
- type: Sprite
sprite: Objects/Consumable/Food/monkeycube.rsi
- type: Rehydratable
@@ -28,6 +28,6 @@
- ReagentId: chem.Nutriment
Quantity: 10
maxVol: 11 # needs room for water
caps: AddTo
caps: Refillable
- type: Rehydratable
target: CarpMob_Content

View File

@@ -1,4 +1,4 @@
- type: entity
- type: entity
name: mini hoe
parent: BaseItem
id: MiniHoe
@@ -24,7 +24,7 @@
state: plantbgone
- type: SolutionContainer
maxVol: 100
caps: RemoveFrom
caps: Drainable
contents:
reagents:
- ReagentId: chem.PlantBGone
@@ -42,12 +42,12 @@
state: weedspray
- type: SolutionContainer
maxVol: 50
caps: RemoveFrom
caps: Drainable
contents:
reagents:
- ReagentId: chem.WeedKiller
Quantity: 50
- type: Pourable
- type: SolutionTransfer
transferAmount: 1.0
- type: Spillable
- type: ItemCooldown
@@ -65,7 +65,7 @@
state: pestspray
- type: SolutionContainer
maxVol: 50
caps: RemoveFrom
caps: Drainable
contents:
reagents:
- ReagentId: chem.PestKiller

View File

@@ -1,4 +1,4 @@
- type: entity
- type: entity
name: haycutters
parent: BaseItem
id: Haycutters
@@ -114,7 +114,7 @@
- type: ItemStatus
- type: SolutionContainer
maxVol: 50
caps: AddTo
caps: Refillable
contents:
reagents:
- ReagentId: chem.WeldingFuel

View File

@@ -20,7 +20,7 @@
- type: ItemStatus
- type: SolutionContainer
maxVol: 100
caps: AddTo
caps: Refillable
contents:
reagents:
- ReagentId: chem.WeldingFuel
@@ -44,7 +44,7 @@
sprite: Objects/Tools/welder_experimental.rsi
- type: SolutionContainer
maxVol: 1000
caps: AddTo
caps: Refillable
contents:
reagents:
- ReagentId: chem.WeldingFuel
@@ -66,7 +66,7 @@
sprite: Objects/Tools/welder_mini.rsi
- type: SolutionContainer
maxVol: 25
caps: AddTo
caps: Refillable
contents:
reagents:
- ReagentId: chem.WeldingFuel

View File

@@ -102,5 +102,5 @@
- type: ChemicalAmmo
- type: SolutionContainer
maxVol: 15
caps: AddTo, RemoveFrom
- type: Pourable
caps: Refillable, Drainable
- type: SolutionTransfer

View File

@@ -91,6 +91,6 @@
Blunt: 1
- type: SolutionContainer
maxVol: 15
caps: AddTo, RemoveFrom
caps: Refillable, Drainable
- type: ChemicalInjectionProjectile
transferAmount: 15

View File

@@ -1,4 +1,4 @@
- type: entity
- type: entity
name: spear
parent: BaseItem
id: Spear
@@ -22,7 +22,7 @@
- type: MeleeChemicalInjector
- type: SolutionContainer
maxVol: 5
- type: Pourable
- type: SolutionTransfer
- type: MeleeWeaponAnimation
id: spear

View File

@@ -11,5 +11,5 @@
sprite: Objects/Specific/Medical/hypospray.rsi
- type: SolutionContainer
maxVol: 30
caps: AddTo, CanExamine
caps: Refillable, CanExamine
- type: Hypospray

View File

@@ -89,6 +89,7 @@
<s:Boolean x:Key="/Default/UserDictionary/Words/=Constructible/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Cooldowns/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Diethylamine/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Drainable/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Firelock/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Cuffable/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=cvar/@EntryIndexedValue">True</s:Boolean>