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); 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)) if (eventArgs.Target == null || !ActionBlockerSystem.CanUse(eventArgs.User) || !eventArgs.Target.TryGetComponent<CuffableComponent>(out var cuffed))
{ {
return; return false;
} }
if (eventArgs.Target == eventArgs.User) if (eventArgs.Target == eventArgs.User)
{ {
eventArgs.User.PopupMessage(Loc.GetString("You can't cuff yourself!")); eventArgs.User.PopupMessage(Loc.GetString("You can't cuff yourself!"));
return; return true;
} }
if (Broken) if (Broken)
{ {
eventArgs.User.PopupMessage(Loc.GetString("The cuffs are broken!")); eventArgs.User.PopupMessage(Loc.GetString("The cuffs are broken!"));
return; return true;
} }
if (!eventArgs.Target.TryGetComponent<HandsComponent>(out var hands)) if (!eventArgs.Target.TryGetComponent<HandsComponent>(out var hands))
{ {
eventArgs.User.PopupMessage(Loc.GetString("{0:theName} has no hands!", eventArgs.Target)); eventArgs.User.PopupMessage(Loc.GetString("{0:theName} has no hands!", eventArgs.Target));
return; return true;
} }
if (cuffed.CuffedHandCount == hands.Count) if (cuffed.CuffedHandCount == hands.Count)
{ {
eventArgs.User.PopupMessage(Loc.GetString("{0:theName} has no free hands to handcuff!", eventArgs.Target)); eventArgs.User.PopupMessage(Loc.GetString("{0:theName} has no free hands to handcuff!", eventArgs.Target));
return; return true;
} }
if (!eventArgs.InRangeUnobstructed(_interactRange, ignoreInsideBlocker: true)) if (!eventArgs.InRangeUnobstructed(_interactRange, ignoreInsideBlocker: true))
{ {
eventArgs.User.PopupMessage(Loc.GetString("You are too far away to use the cuffs!")); 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)); 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); _audioSystem.PlayFromEntity(StartCuffSound, Owner);
TryUpdateCuff(eventArgs.User, eventArgs.Target, cuffed); TryUpdateCuff(eventArgs.User, eventArgs.Target, cuffed);
return true;
} }
/// <summary> /// <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) if (!eventArgs.CanReach)
{ {
eventArgs.User.PopupMessage(Loc.GetString("You can't reach there!")); eventArgs.User.PopupMessage(Loc.GetString("You can't reach there!"));
return; return true;
} }
if (eventArgs.User.TryGetComponent(out IActorComponent? actor)) if (eventArgs.User.TryGetComponent(out IActorComponent? actor))
{ {
OpenInterface(actor.playerSession, eventArgs.ClickLocation); 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) if (eventArgs.Target == null)
{ {
return; return false;
} }
CloseAllSurgeryUIs(); CloseAllSurgeryUIs();
@@ -61,6 +61,8 @@ namespace Content.Server.GameObjects.Components.Body
eventArgs.Target.PopupMessage(eventArgs.User, Loc.GetString("You can't fit it in!")); eventArgs.Target.PopupMessage(eventArgs.User, Loc.GetString("You can't fit it in!"));
} }
} }
return true;
} }
private void SendBodyPartListToUser(AfterInteractEventArgs eventArgs, IBody body) 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 // TODO BODY
if (eventArgs.Target == null) if (eventArgs.Target == null)
{ {
return; return false;
} }
CloseAllSurgeryUIs(); CloseAllSurgeryUIs();
@@ -116,6 +116,8 @@ namespace Content.Server.GameObjects.Components.Body.Part
{ {
SendSlots(eventArgs, body); SendSlots(eventArgs, body);
} }
return true;
} }
private void SendSlots(AfterInteractEventArgs eventArgs, IBody body) 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; } public IEntity? PerformerCache { get; private set; }
async Task IAfterInteract.AfterInteract(AfterInteractEventArgs eventArgs) async Task<bool> IAfterInteract.AfterInteract(AfterInteractEventArgs eventArgs)
{ {
if (eventArgs.Target == null) if (eventArgs.Target == null)
{ {
return; return false;
} }
if (!eventArgs.User.TryGetComponent(out IActorComponent? actor)) if (!eventArgs.User.TryGetComponent(out IActorComponent? actor))
{ {
return; return false;
} }
CloseAllSurgeryUIs(); CloseAllSurgeryUIs();
@@ -101,20 +101,22 @@ namespace Content.Server.GameObjects.Components.Body.Surgery
if (!part.SurgeryCheck(_surgeryType)) if (!part.SurgeryCheck(_surgeryType))
{ {
NotUsefulPopup(); NotUsefulPopup();
return; return true;
} }
// ...do the surgery. // ...do the surgery.
if (part.AttemptSurgery(_surgeryType, part, this, if (part.AttemptSurgery(_surgeryType, part, this,
eventArgs.User)) eventArgs.User))
{ {
return; return true;
} }
// Log error if the surgery fails somehow. // Log error if the surgery fails somehow.
Logger.Debug($"Error when trying to perform surgery on ${nameof(IBodyPart)} {eventArgs.User.Name}"); Logger.Debug($"Error when trying to perform surgery on ${nameof(IBodyPart)} {eventArgs.User.Name}");
throw new InvalidOperationException(); throw new InvalidOperationException();
} }
return true;
} }
public float BaseOperationTime { get => _baseOperateTime; set => _baseOperateTime = value; } 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.Audio;
using Content.Shared.Chemistry; using Content.Shared.Chemistry;
using Content.Shared.GameObjects.Components.Botany; using Content.Shared.GameObjects.Components.Botany;
using Content.Shared.GameObjects.Components.Chemistry;
using Content.Shared.GameObjects.EntitySystems; using Content.Shared.GameObjects.EntitySystems;
using Content.Shared.GameObjects.EntitySystems.ActionBlocker; using Content.Shared.GameObjects.EntitySystems.ActionBlocker;
using Content.Shared.Interfaces; using Content.Shared.Interfaces;
@@ -718,23 +719,28 @@ namespace Content.Server.GameObjects.Components.Botany
return true; 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; var sprayed = false;
if (usingItem.TryGetComponent(out SprayComponent? spray)) if (usingItem.TryGetComponent(out SprayComponent? spray))
{ {
sprayed = true; sprayed = true;
amount = 1f; amount = ReagentUnit.New(1);
EntitySystem.Get<AudioSystem>().PlayFromEntity(spray.SpraySound, usingItem, AudioHelpers.WithVariation(0.125f)); 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 {0:TheName}" : "You transfer {1}u to {0:TheName}",
user.PopupMessageCursor(Loc.GetString(sprayed ? $"You spray {Owner.Name} with {usingItem.Name}." : $"You transfer {split.TotalVolume.ToString()}u to {Owner.Name}")); Owner, split.TotalVolume));
_solutionContainer?.TryAddSolution(split); _solutionContainer?.TryAddSolution(split);

View File

@@ -52,10 +52,12 @@ namespace Content.Server.GameObjects.Components.Chemistry
return TryDoInject(target, user); return TryDoInject(target, user);
} }
Task IAfterInteract.AfterInteract(AfterInteractEventArgs eventArgs) async Task<bool> IAfterInteract.AfterInteract(AfterInteractEventArgs eventArgs)
{ {
TryDoInject(eventArgs.Target, eventArgs.User); if (!eventArgs.CanReach)
return Task.CompletedTask; return false;
return TryDoInject(eventArgs.Target, eventArgs.User);
} }
private bool TryDoInject(IEntity? target, IEntity 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 /// Whether or not the injector is able to draw from containers or if it's a single use
/// device that can only inject. /// device that can only inject.
/// </summary> /// </summary>
[ViewVariables] [ViewVariables] private bool _injectOnly;
private bool _injectOnly;
/// <summary> /// <summary>
/// Amount to inject or draw on each usage. If the injector is inject only, it will /// 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. /// attempt to inject it's entire contents upon use.
/// </summary> /// </summary>
[ViewVariables] [ViewVariables] private ReagentUnit _transferAmount;
private ReagentUnit _transferAmount;
/// <summary> /// <summary>
/// Initial storage volume of the injector /// Initial storage volume of the injector
/// </summary> /// </summary>
[ViewVariables] [ViewVariables] private ReagentUnit _initialMaxVolume;
private ReagentUnit _initialMaxVolume;
private InjectorToggleMode _toggleState; private InjectorToggleMode _toggleState;
@@ -68,19 +65,14 @@ namespace Content.Server.GameObjects.Components.Chemistry
serializer.DataField(ref _injectOnly, "injectOnly", false); serializer.DataField(ref _injectOnly, "injectOnly", false);
serializer.DataField(ref _initialMaxVolume, "initialMaxVolume", ReagentUnit.New(15)); serializer.DataField(ref _initialMaxVolume, "initialMaxVolume", ReagentUnit.New(15));
serializer.DataField(ref _transferAmount, "transferAmount", ReagentUnit.New(5)); 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() protected override void Startup()
{ {
base.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(); Dirty();
} }
@@ -116,58 +108,55 @@ namespace Content.Server.GameObjects.Components.Chemistry
/// Called when clicking on entities while holding in active hand /// Called when clicking on entities while holding in active hand
/// </summary> /// </summary>
/// <param name="eventArgs"></param> /// <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 //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; var targetEntity = eventArgs.Target;
// Handle injecting/drawing for solutions // Handle injecting/drawing for solutions
if (targetEntity.TryGetComponent<SolutionContainerComponent>(out var targetSolution)) if (targetEntity.TryGetComponent<ISolutionInteractionsComponent>(out var targetSolution))
{ {
if (ToggleState == InjectorToggleMode.Inject) if (ToggleState == InjectorToggleMode.Inject)
{ {
if (solution.CanRemoveSolutions && targetSolution.CanAddSolutions) if (targetSolution.CanInject)
{ {
TryInject(targetSolution, eventArgs.User); TryInject(targetSolution, eventArgs.User);
} }
else 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) else if (ToggleState == InjectorToggleMode.Draw)
{ {
if (targetSolution.CanRemoveSolutions && solution.CanAddSolutions) if (targetSolution.CanDraw)
{ {
TryDraw(targetSolution, eventArgs.User); TryDraw(targetSolution, eventArgs.User);
} }
else 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) &&
if (targetEntity.TryGetComponent(out BloodstreamComponent? bloodstream) && ToggleState == InjectorToggleMode.Inject) ToggleState == InjectorToggleMode.Inject)
{
if (solution.CanRemoveSolutions)
{ {
TryInjectIntoBloodstream(bloodstream, eventArgs.User); TryInjectIntoBloodstream(bloodstream, eventArgs.User);
} }
else
{ return true;
eventArgs.User.PopupMessage(eventArgs.User, Loc.GetString("You aren't able to inject {0:theName}!", targetEntity));
}
}
}
} }
/// <summary> /// <summary>
@@ -193,7 +182,8 @@ namespace Content.Server.GameObjects.Components.Chemistry
if (realTransferAmount <= 0) 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; return;
} }
@@ -213,12 +203,14 @@ namespace Content.Server.GameObjects.Components.Chemistry
removedSolution.DoEntityReaction(targetBloodstream.Owner, ReactionMethod.Injection); 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(); Dirty();
AfterInject(); AfterInject();
} }
private void TryInject(SolutionContainerComponent targetSolution, IEntity user) private void TryInject(ISolutionInteractionsComponent targetSolution, IEntity user)
{ {
if (!Owner.TryGetComponent(out SolutionContainerComponent? solution) || solution.CurrentVolume == 0) 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 // 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) if (realTransferAmount <= 0)
{ {
@@ -237,16 +229,12 @@ namespace Content.Server.GameObjects.Components.Chemistry
// Move units from attackSolution to targetSolution // Move units from attackSolution to targetSolution
var removedSolution = solution.SplitSolution(realTransferAmount); var removedSolution = solution.SplitSolution(realTransferAmount);
if (!targetSolution.CanAddSolution(removedSolution))
{
return;
}
removedSolution.DoEntityReaction(targetSolution.Owner, ReactionMethod.Injection); 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(); Dirty();
AfterInject(); 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) 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 // 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) if (realTransferAmount <= 0)
{ {
@@ -277,14 +265,15 @@ namespace Content.Server.GameObjects.Components.Chemistry
} }
// Move units from attackSolution to targetSolution // Move units from attackSolution to targetSolution
var removedSolution = targetSolution.SplitSolution(realTransferAmount); var removedSolution = targetSolution.Draw(realTransferAmount);
if (!solution.TryAddSolution(removedSolution)) if (!solution.TryAddSolution(removedSolution))
{ {
return; 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(); Dirty();
AfterDraw(); AfterDraw();
} }

View File

@@ -59,14 +59,15 @@ namespace Content.Server.GameObjects.Components.Chemistry
} }
// Feeding someone else // Feeding someone else
public async Task AfterInteract(AfterInteractEventArgs eventArgs) public async Task<bool> AfterInteract(AfterInteractEventArgs eventArgs)
{ {
if (eventArgs.Target == null) if (eventArgs.Target == null)
{ {
return; return false;
} }
TryUseFood(eventArgs.User, eventArgs.Target); TryUseFood(eventArgs.User, eventArgs.Target);
return true;
} }
public override bool TryUseFood(IEntity user, IEntity target, UtensilComponent utensilUsed = null) 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.Administration;
using Content.Server.Eui; using Content.Server.Eui;
using Content.Server.GameObjects.Components.GUI; using Content.Server.GameObjects.Components.GUI;
@@ -6,6 +6,7 @@ using Content.Shared.Administration;
using Content.Shared.Chemistry; using Content.Shared.Chemistry;
using Content.Shared.Eui; using Content.Shared.Eui;
using Content.Shared.GameObjects.Components.Chemistry; using Content.Shared.GameObjects.Components.Chemistry;
using Content.Shared.GameObjects.EntitySystems;
using Content.Shared.GameObjects.EntitySystems.ActionBlocker; using Content.Shared.GameObjects.EntitySystems.ActionBlocker;
using Content.Shared.GameObjects.Verbs; using Content.Shared.GameObjects.Verbs;
using Robust.Server.Interfaces.GameObjects; using Robust.Server.Interfaces.GameObjects;
@@ -19,213 +20,8 @@ namespace Content.Server.GameObjects.Components.Chemistry
{ {
[RegisterComponent] [RegisterComponent]
[ComponentReference(typeof(SharedSolutionContainerComponent))] [ComponentReference(typeof(SharedSolutionContainerComponent))]
[ComponentReference(typeof(ISolutionInteractionsComponent))]
public class SolutionContainerComponent : SharedSolutionContainerComponent 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,15 +109,18 @@ namespace Content.Server.GameObjects.Components
return false; return false;
} }
async Task IAfterInteract.AfterInteract(AfterInteractEventArgs eventArgs) async Task<bool> IAfterInteract.AfterInteract(AfterInteractEventArgs eventArgs)
{ {
if (!eventArgs.InRangeUnobstructed(ignoreInsideBlocker: false, popup: true, if (!eventArgs.InRangeUnobstructed(ignoreInsideBlocker: false, popup: true,
collisionMask: Shared.Physics.CollisionGroup.MobImpassable)) return; collisionMask: Shared.Physics.CollisionGroup.MobImpassable))
{
return true;
}
if (Charges <= 0) if (Charges <= 0)
{ {
eventArgs.User.PopupMessage(Loc.GetString("Not enough left.")); eventArgs.User.PopupMessage(Loc.GetString("Not enough left."));
return; return true;
} }
var entityManager = IoCManager.Resolve<IServerEntityManager>(); var entityManager = IoCManager.Resolve<IServerEntityManager>();
@@ -138,6 +141,7 @@ namespace Content.Server.GameObjects.Components
// Decrease "Ammo" // Decrease "Ammo"
Charges--; Charges--;
Dirty(); Dirty();
return true;
} }
void IDropped.Dropped(DroppedEventArgs eventArgs) 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"); 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); TryUseUtensil(eventArgs.User, eventArgs.Target);
return true;
} }
private void TryUseUtensil(IEntity user, IEntity? target) private void TryUseUtensil(IEntity user, IEntity? target)

View File

@@ -62,14 +62,16 @@ namespace Content.Server.GameObjects.Components.Fluids
Owner.EnsureComponentWarn(out SolutionContainerComponent _); 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 (!Owner.TryGetComponent(out SolutionContainerComponent? contents))
if (!eventArgs.InRangeUnobstructed(ignoreInsideBlocker: true, popup: true)) return; return false;
if (!eventArgs.InRangeUnobstructed(ignoreInsideBlocker: true, popup: true))
return false;
if (CurrentVolume <= 0) if (CurrentVolume <= 0)
{ {
return; return true;
} }
if (eventArgs.Target == null) if (eventArgs.Target == null)
@@ -77,12 +79,12 @@ namespace Content.Server.GameObjects.Components.Fluids
// Drop the liquid on the mop on to the ground // Drop the liquid on the mop on to the ground
contents.SplitSolution(CurrentVolume).SpillAt(eventArgs.ClickLocation, "PuddleSmear"); contents.SplitSolution(CurrentVolume).SpillAt(eventArgs.ClickLocation, "PuddleSmear");
return; return true;
} }
if (!eventArgs.Target.TryGetComponent(out PuddleComponent? puddleComponent)) if (!eventArgs.Target.TryGetComponent(out PuddleComponent? puddleComponent))
{ {
return; return true;
} }
// Essentially pickup either: // Essentially pickup either:
// - _pickupAmount, // - _pickupAmount,
@@ -101,7 +103,7 @@ namespace Content.Server.GameObjects.Components.Fluids
} }
else else
{ {
return; return true;
} }
} }
else 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) // Give some visual feedback shit's happening (for anyone who can't hear sound)
Owner.PopupMessage(eventArgs.User, Loc.GetString("Swish")); 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.Chemistry; using Content.Shared.GameObjects.Components.Chemistry;
using Content.Shared.GameObjects.EntitySystems;
using Content.Shared.GameObjects.EntitySystems.ActionBlocker; using Content.Shared.GameObjects.EntitySystems.ActionBlocker;
using Content.Shared.GameObjects.Verbs; using Content.Shared.GameObjects.Verbs;
using Content.Shared.Interfaces; using Content.Shared.Interfaces;
@@ -24,34 +23,37 @@ namespace Content.Server.GameObjects.Components.Fluids
protected override void GetData(IEntity user, SpillableComponent component, VerbData data) protected override void GetData(IEntity user, SpillableComponent component, VerbData data)
{ {
if (!ActionBlockerSystem.CanInteract(user) || if (!ActionBlockerSystem.CanInteract(user) ||
!component.Owner.TryGetComponent(out SolutionContainerComponent solutionComponent) || !component.Owner.TryGetComponent(out ISolutionInteractionsComponent solutionComponent) ||
!solutionComponent.CanRemoveSolutions) !solutionComponent.CanDrain)
{ {
data.Visibility = VerbVisibility.Invisible; data.Visibility = VerbVisibility.Invisible;
return; return;
} }
data.Text = Loc.GetString("Spill liquid"); 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) 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)); user.PopupMessage(user, Loc.GetString("{0:theName} is empty!", component.Owner));
} }
// Need this as when we split the component's owner may be deleted // Need this as when we split the component's owner may be deleted
var entityLocation = component.Owner.Transform.Coordinates; var entityLocation = component.Owner.Transform.Coordinates;
var solution = solutionComponent.SplitSolution(solutionComponent.CurrentVolume); var solution = solutionComponent.Drain(solutionComponent.DrainAvailable);
solution.SpillAt(entityLocation, "PuddleSmear"); solution.SpillAt(entityLocation, "PuddleSmear");
} }
} }

View File

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

View File

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

View File

@@ -2,6 +2,7 @@
using System; using System;
using System.Threading.Tasks; using System.Threading.Tasks;
using Content.Server.Atmos; using Content.Server.Atmos;
using Content.Server.Explosions;
using Content.Server.GameObjects.Components.Chemistry; using Content.Server.GameObjects.Components.Chemistry;
using Content.Server.GameObjects.Components.Items.Storage; using Content.Server.GameObjects.Components.Items.Storage;
using Content.Server.GameObjects.EntitySystems; using Content.Server.GameObjects.EntitySystems;
@@ -10,12 +11,15 @@ using Content.Server.Interfaces.GameObjects;
using Content.Server.Utility; using Content.Server.Utility;
using Content.Shared.Chemistry; using Content.Shared.Chemistry;
using Content.Shared.GameObjects; using Content.Shared.GameObjects;
using Content.Shared.GameObjects.Components.Chemistry;
using Content.Shared.GameObjects.Components.Interactable; using Content.Shared.GameObjects.Components.Interactable;
using Content.Shared.GameObjects.EntitySystems; using Content.Shared.GameObjects.EntitySystems;
using Content.Shared.Interfaces; using Content.Shared.Interfaces;
using Content.Shared.Interfaces.GameObjects.Components; using Content.Shared.Interfaces.GameObjects.Components;
using Robust.Server.GameObjects; using Robust.Server.GameObjects;
using Robust.Server.GameObjects.EntitySystems;
using Robust.Shared.GameObjects; using Robust.Shared.GameObjects;
using Robust.Shared.GameObjects.Systems;
using Robust.Shared.Interfaces.GameObjects; using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.IoC; using Robust.Shared.IoC;
using Robust.Shared.Localization; using Robust.Shared.Localization;
@@ -29,7 +33,7 @@ namespace Content.Server.GameObjects.Components.Interactable
[ComponentReference(typeof(ToolComponent))] [ComponentReference(typeof(ToolComponent))]
[ComponentReference(typeof(IToolComponent))] [ComponentReference(typeof(IToolComponent))]
[ComponentReference(typeof(IHotItem))] [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!; [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 namespace Content.Server.GameObjects.Components.Items
{ {
[RegisterComponent] [RegisterComponent]
public class FireExtinguisherComponent : Component public class FireExtinguisherComponent : Component, IAfterInteract
{ {
public override string Name => "FireExtinguisher"; 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)); 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 (!eventArgs.InRangeUnobstructed(ignoreInsideBlocker: true, popup: true))
if (!Owner.TryGetComponent(out StackComponent stack)) return; return true;
if (!Owner.TryGetComponent(out StackComponent stack))
return true;
var mapManager = IoCManager.Resolve<IMapManager>(); var mapManager = IoCManager.Resolve<IMapManager>();
var location = eventArgs.ClickLocation.AlignWithClosestGridTile(); 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); PlaceAt(mapGrid, location, _tileDefinitionManager[_outputTiles[0]].TileId, mapGrid.TileSize / 2f);
break; 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)); 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)) 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) if (rcdComponent.maxAmmo - rcdComponent._ammo < refillAmmo)
{ {
rcdComponent.Owner.PopupMessage(eventArgs.User, Loc.GetString("The RCD is full!")); rcdComponent.Owner.PopupMessage(eventArgs.User, Loc.GetString("The RCD is full!"));
return; return true;
} }
rcdComponent._ammo = Math.Min(rcdComponent.maxAmmo, rcdComponent._ammo + refillAmmo); 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 //Deleting a held item causes a lot of errors
hands.Drop(Owner, false); hands.Drop(Owner, false);
Owner.Delete(); 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)); 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 //No changing mode mid-RCD
var startingMode = _mode; var startingMode = _mode;
@@ -116,7 +116,7 @@ namespace Content.Server.GameObjects.Components.Items.RCD
var result = await doAfterSystem.DoAfter(doAfterEventArgs); var result = await doAfterSystem.DoAfter(doAfterEventArgs);
if (result == DoAfterStatus.Cancelled) if (result == DoAfterStatus.Cancelled)
{ {
return; return true;
} }
switch (_mode) switch (_mode)
@@ -146,12 +146,12 @@ namespace Content.Server.GameObjects.Components.Items.RCD
airlock.Transform.LocalRotation = Owner.Transform.LocalRotation; //Now apply icon smoothing. airlock.Transform.LocalRotation = Owner.Transform.LocalRotation; //Now apply icon smoothing.
break; break;
default: 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); _entitySystemManager.GetEntitySystem<AudioSystem>().PlayFromEntity("/Audio/Items/deconstruct.ogg", Owner);
_ammo--; _ammo--;
return true;
} }
private bool IsRCDStillValid(AfterInteractEventArgs eventArgs, IMapGrid mapGrid, TileRef tile, Vector2i snapPos, RcdMode startingMode) 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.Chemistry;
using Content.Shared.GameObjects.Components.Body; using Content.Shared.GameObjects.Components.Body;
using Content.Shared.GameObjects.Components.Body.Part; using Content.Shared.GameObjects.Components.Body.Part;
using Content.Shared.GameObjects.Components.Chemistry;
using Content.Shared.GameObjects.Components.Power; using Content.Shared.GameObjects.Components.Power;
using Content.Shared.GameObjects.EntitySystems; using Content.Shared.GameObjects.EntitySystems;
using Content.Shared.Interfaces; using Content.Shared.Interfaces;
@@ -213,10 +214,10 @@ namespace Content.Server.GameObjects.Components.Kitchen
return false; return false;
} }
if (itemEntity.TryGetComponent<PourableComponent>(out var attackPourable)) if (itemEntity.TryGetComponent<SolutionTransferComponent>(out var attackPourable))
{ {
if (!itemEntity.TryGetComponent<SolutionContainerComponent>(out var attackSolution) if (!itemEntity.TryGetComponent<ISolutionInteractionsComponent>(out var attackSolution)
|| !attackSolution.CanRemoveSolutions) || !attackSolution.CanDrain)
{ {
return false; return false;
} }
@@ -235,7 +236,7 @@ namespace Content.Server.GameObjects.Components.Kitchen
} }
//Move units from attackSolution to targetSolution //Move units from attackSolution to targetSolution
var removedSolution = attackSolution.SplitSolution(realTransferAmount); var removedSolution = attackSolution.Drain(realTransferAmount);
if (!solution.TryAddSolution(removedSolution)) if (!solution.TryAddSolution(removedSolution))
{ {
return false; 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>()); 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) if (eventArgs.Target == null)
{ {
return; return false;
} }
if (!eventArgs.Target.TryGetComponent(out IDamageableComponent damageable)) if (!eventArgs.Target.TryGetComponent(out IDamageableComponent damageable))
{ {
return; return true;
} }
if (!ActionBlockerSystem.CanInteract(eventArgs.User)) if (!ActionBlockerSystem.CanInteract(eventArgs.User))
{ {
return; return true;
} }
if (eventArgs.User != eventArgs.Target && if (eventArgs.User != eventArgs.Target &&
!eventArgs.InRangeUnobstructed(ignoreInsideBlocker: true, popup: true)) !eventArgs.InRangeUnobstructed(ignoreInsideBlocker: true, popup: true))
{ {
return; return true;
} }
if (Owner.TryGetComponent(out StackComponent stack) && if (Owner.TryGetComponent(out StackComponent stack) &&
!stack.Use(1)) !stack.Use(1))
{ {
return; return true;
} }
foreach (var (type, amount) in Heal) foreach (var (type, amount) in Heal)
{ {
damageable.ChangeDamage(type, -amount, true); 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.Body.Behavior;
using Content.Server.GameObjects.Components.Chemistry; using Content.Server.GameObjects.Components.Chemistry;
using Content.Server.GameObjects.Components.Fluids; using Content.Server.GameObjects.Components.Fluids;
using Content.Server.GameObjects.EntitySystems;
using Content.Shared.Audio; using Content.Shared.Audio;
using Content.Shared.Chemistry; using Content.Shared.Chemistry;
using Content.Shared.GameObjects.Components.Body; using Content.Shared.GameObjects.Components.Body;
using Content.Shared.GameObjects.Components.Body.Mechanism;
using Content.Shared.GameObjects.Components.Nutrition; using Content.Shared.GameObjects.Components.Nutrition;
using Content.Shared.GameObjects.EntitySystems; using Content.Shared.GameObjects.EntitySystems;
using Content.Shared.Interfaces; using Content.Shared.Interfaces;
@@ -75,7 +73,7 @@ namespace Content.Server.GameObjects.Components.Nutrition
_contents = Owner.AddComponent<SolutionContainerComponent>(); _contents = Owner.AddComponent<SolutionContainerComponent>();
} }
_contents.Capabilities = SolutionContainerCaps.AddTo | SolutionContainerCaps.RemoveFrom; _contents.Capabilities = SolutionContainerCaps.Refillable | SolutionContainerCaps.Drainable;
Opened = _defaultToOpened; Opened = _defaultToOpened;
UpdateAppearance(); UpdateAppearance();
} }
@@ -113,9 +111,10 @@ namespace Content.Server.GameObjects.Components.Nutrition
} }
//Force feeding a drink to someone. //Force feeding a drink to someone.
async Task IAfterInteract.AfterInteract(AfterInteractEventArgs eventArgs) async Task<bool> IAfterInteract.AfterInteract(AfterInteractEventArgs eventArgs)
{ {
TryUseDrink(eventArgs.Target, forced: true); TryUseDrink(eventArgs.Target, forced: true);
return true;
} }
public void Examine(FormattedMessage message, bool inDetailsRange) 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) private bool TryUseDrink(IEntity target, bool forced = false)
{ {
if (target == null || !_contents.CanRemoveSolutions) if (target == null || !_contents.CanDrain)
{ {
return false; return false;
} }

View File

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

View File

@@ -78,7 +78,7 @@ namespace Content.Server.GameObjects.Components.Portal
_state = newState; _state = newState;
} }
async Task IAfterInteract.AfterInteract(AfterInteractEventArgs eventArgs) async Task<bool> IAfterInteract.AfterInteract(AfterInteractEventArgs eventArgs)
{ {
if (_teleporterType == TeleporterType.Directed) if (_teleporterType == TeleporterType.Directed)
{ {
@@ -89,6 +89,8 @@ namespace Content.Server.GameObjects.Components.Portal
{ {
TryRandomTeleport(eventArgs.User); TryRandomTeleport(eventArgs.User);
} }
return true;
} }
public void TryDirectedTeleport(IEntity user, MapCoordinates mapCoords) public void TryDirectedTeleport(IEntity user, MapCoordinates mapCoords)

View File

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

View File

@@ -416,13 +416,8 @@ namespace Content.Server.GameObjects.EntitySystems.Click
return; return;
} }
var afterInteracts = weapon.GetAllComponents<IAfterInteract>().ToList();
var afterInteractEventArgs = new AfterInteractEventArgs(user, clickLocation, null, canReach); var afterInteractEventArgs = new AfterInteractEventArgs(user, clickLocation, null, canReach);
await DoAfterInteract(weapon, afterInteractEventArgs);
foreach (var afterInteract in afterInteracts)
{
await afterInteract.AfterInteract(afterInteractEventArgs);
}
} }
/// <summary> /// <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 // 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); var afterAttackEventArgs = new AfterInteractEventArgs(user, clickLocation, attacked, canReach: true);
foreach (var afterAttack in afterAttacks) await DoAfterInteract(weapon, afterAttackEventArgs);
{
await afterAttack.AfterInteract(afterAttackEventArgs);
}
} }
/// <summary> /// <summary>
@@ -835,13 +826,21 @@ namespace Content.Server.GameObjects.EntitySystems.Click
if (afterAtkMsg.Handled) if (afterAtkMsg.Handled)
return; return;
var afterAttacks = weapon.GetAllComponents<IAfterInteract>().ToList(); // See if we have a ranged attack interaction
var afterAttackEventArgs = new AfterInteractEventArgs(user, clickLocation, attacked, canReach: false); 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) 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 ""; return "";
} }
var majorReagent = Contents.OrderByDescending(reagent => reagent.Quantity).First(); ; var majorReagent = Contents.OrderByDescending(reagent => reagent.Quantity).First();
return majorReagent.ReagentId; 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> /// <summary>
/// Holds a <see cref="Solution"/> with a limited volume. /// Holds a <see cref="Solution"/> with a limited volume.
/// </summary> /// </summary>
public abstract class SharedSolutionContainerComponent : Component, IExamine public abstract class SharedSolutionContainerComponent : Component, IExamine, ISolutionInteractionsComponent
{ {
public override string Name => "SolutionContainer"; public override string Name => "SolutionContainer";
@@ -60,9 +60,11 @@ namespace Content.Shared.GameObjects.Components.Chemistry
public bool CanUseWithChemDispenser => Capabilities.HasCap(SolutionContainerCaps.FitsInDispenser); 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) 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.CanReact, "canReact", true);
serializer.DataField(this, x => x.MaxVolume, "maxVol", ReagentUnit.New(0)); serializer.DataField(this, x => x.MaxVolume, "maxVol", ReagentUnit.New(0));
serializer.DataField(this, x => x.Solution, "contents", new Solution()); 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() public void RemoveAllSolution()
@@ -209,6 +211,43 @@ namespace Content.Shared.GameObjects.Components.Chemistry
message.AddMarkup(Loc.GetString(messageString, colorHex, Loc.GetString(proto.PhysicalDescription))); 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() private void UpdateAppearance()
{ {
if (Owner.Deleted || !Owner.TryGetComponent<SharedAppearanceComponent>(out var appearance)) if (Owner.Deleted || !Owner.TryGetComponent<SharedAppearanceComponent>(out var appearance))

View File

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

Binary file not shown.

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -20,7 +20,7 @@
# Organs # Organs
- type: SolutionContainer - type: SolutionContainer
maxVol: 250 maxVol: 250
caps: AddTo, RemoveFrom caps: Injectable, Drawable
- type: Bloodstream - type: Bloodstream
max_volume: 100 max_volume: 100
# StatusEffects # 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: Find empty containers (e.g. mug, pitcher) and move to their own yml
# TODO: Move bottles to their own yml # TODO: Move bottles to their own yml
- type: entity - type: entity
@@ -8,7 +8,7 @@
components: components:
- type: SolutionContainer - type: SolutionContainer
maxVol: 50 maxVol: 50
- type: Pourable - type: SolutionTransfer
transferAmount: 5 transferAmount: 5
- type: Drink - type: Drink
- type: Sprite - type: Sprite
@@ -55,7 +55,7 @@
- type: SolutionContainer - type: SolutionContainer
fillingState: glass fillingState: glass
maxVol: 50 maxVol: 50
- type: Pourable - type: SolutionTransfer
transferAmount: 5 transferAmount: 5
- type: TransformableContainer - type: TransformableContainer

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -11,5 +11,5 @@
sprite: Objects/Specific/Medical/hypospray.rsi sprite: Objects/Specific/Medical/hypospray.rsi
- type: SolutionContainer - type: SolutionContainer
maxVol: 30 maxVol: 30
caps: AddTo, CanExamine caps: Refillable, CanExamine
- type: Hypospray - 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/=Constructible/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Cooldowns/@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/=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/=Firelock/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Cuffable/@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> <s:Boolean x:Key="/Default/UserDictionary/Words/=cvar/@EntryIndexedValue">True</s:Boolean>