Files
tbd-station-14/Content.Server/GameObjects/Components/Chemistry/InjectorComponent.cs
Pieter-Jan Briers c40ac26ced 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.
2021-02-04 00:05:31 +11:00

306 lines
11 KiB
C#

#nullable enable
using System;
using System.Threading.Tasks;
using Content.Server.GameObjects.Components.Body.Circulatory;
using Content.Shared.Chemistry;
using Content.Shared.GameObjects.Components.Chemistry;
using Content.Shared.GameObjects.EntitySystems;
using Content.Shared.Interfaces;
using Content.Shared.Interfaces.GameObjects.Components;
using Content.Shared.Utility;
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>
/// Server behavior for reagent injectors and syringes. Can optionally support both
/// injection and drawing or just injection. Can inject/draw reagents from solution
/// containers, and can directly inject into a mobs bloodstream.
/// </summary>
[RegisterComponent]
public class InjectorComponent : SharedInjectorComponent, IAfterInteract, IUse, ISolutionChange
{
/// <summary>
/// Whether or not the injector is able to draw from containers or if it's a single use
/// device that can only inject.
/// </summary>
[ViewVariables] private bool _injectOnly;
/// <summary>
/// Amount to inject or draw on each usage. If the injector is inject only, it will
/// attempt to inject it's entire contents upon use.
/// </summary>
[ViewVariables] private ReagentUnit _transferAmount;
/// <summary>
/// Initial storage volume of the injector
/// </summary>
[ViewVariables] private ReagentUnit _initialMaxVolume;
private InjectorToggleMode _toggleState;
/// <summary>
/// The state of the injector. Determines it's attack behavior. Containers must have the
/// right SolutionCaps to support injection/drawing. For InjectOnly injectors this should
/// only ever be set to Inject
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
public InjectorToggleMode ToggleState
{
get => _toggleState;
set
{
_toggleState = value;
Dirty();
}
}
public override void ExposeData(ObjectSerializer serializer)
{
base.ExposeData(serializer);
serializer.DataField(ref _injectOnly, "injectOnly", false);
serializer.DataField(ref _initialMaxVolume, "initialMaxVolume", ReagentUnit.New(15));
serializer.DataField(ref _transferAmount, "transferAmount", ReagentUnit.New(5));
serializer.DataField(ref _toggleState, "toggleState",
_injectOnly ? InjectorToggleMode.Inject : InjectorToggleMode.Draw);
}
protected override void Startup()
{
base.Startup();
Dirty();
}
/// <summary>
/// Toggle between draw/inject state if applicable
/// </summary>
private void Toggle(IEntity user)
{
if (_injectOnly)
{
return;
}
string msg;
switch (ToggleState)
{
case InjectorToggleMode.Inject:
ToggleState = InjectorToggleMode.Draw;
msg = "Now drawing";
break;
case InjectorToggleMode.Draw:
ToggleState = InjectorToggleMode.Inject;
msg = "Now injecting";
break;
default:
throw new ArgumentOutOfRangeException();
}
Owner.PopupMessage(user, Loc.GetString(msg));
}
/// <summary>
/// Called when clicking on entities while holding in active hand
/// </summary>
/// <param name="eventArgs"></param>
async Task<bool> IAfterInteract.AfterInteract(AfterInteractEventArgs eventArgs)
{
if (!eventArgs.InRangeUnobstructed(ignoreInsideBlocker: true, popup: true))
return false;
//Make sure we have the attacking entity
if (eventArgs.Target == null || !Owner.HasComponent<SolutionContainerComponent>())
{
return false;
}
var targetEntity = eventArgs.Target;
// Handle injecting/drawing for solutions
if (targetEntity.TryGetComponent<ISolutionInteractionsComponent>(out var targetSolution))
{
if (ToggleState == InjectorToggleMode.Inject)
{
if (targetSolution.CanInject)
{
TryInject(targetSolution, eventArgs.User);
}
else
{
eventArgs.User.PopupMessage(eventArgs.User,
Loc.GetString("You aren't able to transfer to {0:theName}!", targetSolution.Owner));
}
}
else if (ToggleState == InjectorToggleMode.Draw)
{
if (targetSolution.CanDraw)
{
TryDraw(targetSolution, eventArgs.User);
}
else
{
eventArgs.User.PopupMessage(eventArgs.User,
Loc.GetString("You aren't able to draw from {0:theName}!", targetSolution.Owner));
}
}
}
// Handle injecting into bloodstream
else if (targetEntity.TryGetComponent(out BloodstreamComponent? bloodstream) &&
ToggleState == InjectorToggleMode.Inject)
{
TryInjectIntoBloodstream(bloodstream, eventArgs.User);
}
return true;
}
/// <summary>
/// Called when use key is pressed when held in active hand
/// </summary>
/// <param name="eventArgs"></param>
/// <returns></returns>
bool IUse.UseEntity(UseEntityEventArgs eventArgs)
{
Toggle(eventArgs.User);
return true;
}
private void TryInjectIntoBloodstream(BloodstreamComponent targetBloodstream, IEntity user)
{
if (!Owner.TryGetComponent(out SolutionContainerComponent? solution) || solution.CurrentVolume == 0)
{
return;
}
// Get transfer amount. May be smaller than _transferAmount if not enough room
var realTransferAmount = ReagentUnit.Min(_transferAmount, targetBloodstream.EmptyVolume);
if (realTransferAmount <= 0)
{
Owner.PopupMessage(user,
Loc.GetString("You aren't able to inject {0:theName}!", targetBloodstream.Owner));
return;
}
// Move units from attackSolution to targetSolution
var removedSolution = solution.SplitSolution(realTransferAmount);
if (!solution.CanAddSolution(removedSolution))
{
return;
}
// TODO: Account for partial transfer.
removedSolution.DoEntityReaction(solution.Owner, ReactionMethod.Injection);
solution.TryAddSolution(removedSolution);
removedSolution.DoEntityReaction(targetBloodstream.Owner, ReactionMethod.Injection);
Owner.PopupMessage(user,
Loc.GetString("You inject {0}u into {1:theName}!", removedSolution.TotalVolume,
targetBloodstream.Owner));
Dirty();
AfterInject();
}
private void TryInject(ISolutionInteractionsComponent targetSolution, IEntity user)
{
if (!Owner.TryGetComponent(out SolutionContainerComponent? solution) || solution.CurrentVolume == 0)
{
return;
}
// Get transfer amount. May be smaller than _transferAmount if not enough room
var realTransferAmount = ReagentUnit.Min(_transferAmount, targetSolution.InjectSpaceAvailable);
if (realTransferAmount <= 0)
{
Owner.PopupMessage(user, Loc.GetString("{0:theName} is already full!", targetSolution.Owner));
return;
}
// Move units from attackSolution to targetSolution
var removedSolution = solution.SplitSolution(realTransferAmount);
removedSolution.DoEntityReaction(targetSolution.Owner, ReactionMethod.Injection);
targetSolution.Inject(removedSolution);
Owner.PopupMessage(user,
Loc.GetString("You transfer {0}u to {1:theName}", removedSolution.TotalVolume, targetSolution.Owner));
Dirty();
AfterInject();
}
private void AfterInject()
{
// Automatically set syringe to draw after completely draining it.
if (Owner.GetComponent<SolutionContainerComponent>().CurrentVolume == 0)
{
ToggleState = InjectorToggleMode.Draw;
}
}
private void TryDraw(ISolutionInteractionsComponent targetSolution, IEntity user)
{
if (!Owner.TryGetComponent(out SolutionContainerComponent? solution) || solution.EmptyVolume == 0)
{
return;
}
// Get transfer amount. May be smaller than _transferAmount if not enough room
var realTransferAmount = ReagentUnit.Min(_transferAmount, targetSolution.DrawAvailable);
if (realTransferAmount <= 0)
{
Owner.PopupMessage(user, Loc.GetString("{0:theName} is empty!", targetSolution.Owner));
return;
}
// Move units from attackSolution to targetSolution
var removedSolution = targetSolution.Draw(realTransferAmount);
if (!solution.TryAddSolution(removedSolution))
{
return;
}
Owner.PopupMessage(user,
Loc.GetString("Drew {0}u from {1:theName}", removedSolution.TotalVolume, targetSolution.Owner));
Dirty();
AfterDraw();
}
private void AfterDraw()
{
// Automatically set syringe to inject after completely filling it.
if (Owner.GetComponent<SolutionContainerComponent>().EmptyVolume == 0)
{
ToggleState = InjectorToggleMode.Inject;
}
}
void ISolutionChange.SolutionChanged(SolutionChangeEventArgs eventArgs)
{
Dirty();
}
public override ComponentState GetComponentState()
{
Owner.TryGetComponent(out SolutionContainerComponent? solution);
var currentVolume = solution?.CurrentVolume ?? ReagentUnit.Zero;
var maxVolume = solution?.MaxVolume ?? ReagentUnit.Zero;
return new InjectorComponentState(currentVolume, maxVolume, ToggleState);
}
}
}