780 lines
35 KiB
C#
780 lines
35 KiB
C#
using System.Linq;
|
|
using Content.Shared.Administration.Logs;
|
|
using Content.Shared.Body.Components;
|
|
using Content.Shared.Chemistry.Components;
|
|
using Content.Shared.Chemistry.Events;
|
|
using Content.Shared.Chemistry.Prototypes;
|
|
using Content.Shared.Chemistry.Reagent;
|
|
using Content.Shared.Database;
|
|
using Content.Shared.DoAfter;
|
|
using Content.Shared.FixedPoint;
|
|
using Content.Shared.Forensics.Systems;
|
|
using Content.Shared.IdentityManagement;
|
|
using Content.Shared.Interaction;
|
|
using Content.Shared.Interaction.Events;
|
|
using Content.Shared.Nutrition.EntitySystems;
|
|
using Content.Shared.Popups;
|
|
using Content.Shared.Stacks;
|
|
using Content.Shared.Standing;
|
|
using Content.Shared.Timing;
|
|
using Content.Shared.Verbs;
|
|
using Content.Shared.Weapons.Melee.Events;
|
|
using JetBrains.Annotations;
|
|
using Robust.Shared.Audio.Systems;
|
|
using Robust.Shared.Prototypes;
|
|
|
|
namespace Content.Shared.Chemistry.EntitySystems;
|
|
|
|
/// <summary>
|
|
/// This handles toggling injection modes, injections and drawings for all kinds of injectors.
|
|
/// </summary>
|
|
/// <seealso cref="InjectorComponent"/>
|
|
/// <seealso cref="InjectorModePrototype"/>
|
|
public sealed partial class InjectorSystem : EntitySystem
|
|
{
|
|
[Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
|
|
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
|
[Dependency] private readonly SharedAudioSystem _audio = default!;
|
|
[Dependency] private readonly SharedForensicsSystem _forensics = default!;
|
|
[Dependency] private readonly SharedDoAfterSystem _doAfter = default!;
|
|
[Dependency] private readonly OpenableSystem _openable = default!;
|
|
[Dependency] private readonly SharedPopupSystem _popup = default!;
|
|
[Dependency] private readonly ReactiveSystem _reactiveSystem = default!;
|
|
[Dependency] private readonly SharedSolutionContainerSystem _solutionContainer = default!;
|
|
[Dependency] private readonly StandingStateSystem _standingState = default!;
|
|
[Dependency] private readonly UseDelaySystem _useDelay = default!;
|
|
|
|
public override void Initialize()
|
|
{
|
|
SubscribeLocalEvent<InjectorComponent, UseInHandEvent>(OnInjectorUse);
|
|
SubscribeLocalEvent<InjectorComponent, AfterInteractEvent>(OnInjectorAfterInteract);
|
|
SubscribeLocalEvent<InjectorComponent, InjectorDoAfterEvent>(OnInjectDoAfter);
|
|
SubscribeLocalEvent<InjectorComponent, MeleeHitEvent>(OnAttack);
|
|
SubscribeLocalEvent<InjectorComponent, GetVerbsEvent<AlternativeVerb>>(AddVerbs);
|
|
}
|
|
|
|
#region Events Handling
|
|
private void OnInjectorUse(Entity<InjectorComponent> injector, ref UseInHandEvent args)
|
|
{
|
|
if (args.Handled
|
|
|| !_prototypeManager.Resolve(injector.Comp.ActiveModeProtoId, out var activeProto))
|
|
return;
|
|
|
|
if (activeProto.InjectOnUse) // Injectors that can't toggle transferAmounts will be used.
|
|
TryMobsDoAfter(injector, args.User, args.User);
|
|
else // Syringes toggle Draw/Inject.
|
|
ToggleMode(injector, args.User);
|
|
|
|
args.Handled = true;
|
|
args.ApplyDelay = false;
|
|
}
|
|
|
|
private void OnInjectorAfterInteract(Entity<InjectorComponent> injector, ref AfterInteractEvent args)
|
|
{
|
|
if (args.Handled || !args.CanReach || args.Target is not { Valid: true } target)
|
|
return;
|
|
|
|
// Is the target a mob? If yes, use a do-after to give them time to respond.
|
|
if (HasComp<BloodstreamComponent>(target))
|
|
{
|
|
// Are use using an injector capable of targeting a mob?
|
|
if (injector.Comp.IgnoreMobs)
|
|
{
|
|
_popup.PopupClient(Loc.GetString("injector-component-ignore-mobs"), args.Target.Value, args.User);
|
|
return;
|
|
}
|
|
|
|
args.Handled |= TryMobsDoAfter(injector, args.User, target);
|
|
return;
|
|
}
|
|
|
|
// Draw from or inject into jugs, bottles, etc.
|
|
args.Handled |= TryContainerDoAfter(injector, args.User, target);
|
|
}
|
|
|
|
private void OnInjectDoAfter(Entity<InjectorComponent> injector, ref InjectorDoAfterEvent args)
|
|
{
|
|
if (args.Cancelled || args.Handled || args.Args.Target == null)
|
|
return;
|
|
|
|
args.Handled |= TryUseInjector(injector, args.Args.User, args.Args.Target.Value);
|
|
}
|
|
|
|
private void OnAttack(Entity<InjectorComponent> injector, ref MeleeHitEvent args)
|
|
{
|
|
if (args.HitEntities is [])
|
|
return;
|
|
|
|
TryMobsDoAfter(injector, args.User, args.HitEntities[0]);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Give the user interaction verbs for their injector.
|
|
/// </summary>
|
|
/// <param name="injector"></param>
|
|
/// <param name="args"></param>
|
|
/// <remarks>
|
|
/// If they have multiple transferAmounts, they'll be able to switch between them via the verbs.
|
|
/// If they have multiple injector modes and don't toggle when used in hand, they can toggle the mode with the verbs too.
|
|
/// </remarks>
|
|
private void AddVerbs(Entity<InjectorComponent> injector, ref GetVerbsEvent<AlternativeVerb> args)
|
|
{
|
|
if (!args.CanAccess || !args.CanInteract || args.Hands == null
|
|
|| !_prototypeManager.Resolve(injector.Comp.ActiveModeProtoId, out var activeMode))
|
|
return;
|
|
|
|
var user = args.User;
|
|
var min = activeMode.TransferAmounts.Min();
|
|
var max = activeMode.TransferAmounts.Max();
|
|
var cur = injector.Comp.CurrentTransferAmount;
|
|
var toggleAmount = cur == max ? min : max;
|
|
|
|
var priority = 0;
|
|
|
|
if (activeMode.TransferAmounts.Count > 1)
|
|
{
|
|
AlternativeVerb toggleVerb = new()
|
|
{
|
|
Text = Loc.GetString("comp-solution-transfer-verb-toggle", ("amount", toggleAmount)),
|
|
Category = VerbCategory.SetTransferAmount,
|
|
Act = () =>
|
|
{
|
|
injector.Comp.CurrentTransferAmount = toggleAmount;
|
|
_popup.PopupClient(Loc.GetString("comp-solution-transfer-set-amount", ("amount", toggleAmount)), user, user);
|
|
Dirty(injector);
|
|
},
|
|
|
|
Priority = priority
|
|
};
|
|
args.Verbs.Add(toggleVerb);
|
|
|
|
priority -= 1;
|
|
|
|
// Add specific transfer verbs for amounts defined in the component
|
|
foreach (var amount in activeMode.TransferAmounts)
|
|
{
|
|
AlternativeVerb verb = new()
|
|
{
|
|
Text = Loc.GetString("comp-solution-transfer-verb-amount", ("amount", amount)),
|
|
Category = VerbCategory.SetTransferAmount,
|
|
Act = () =>
|
|
{
|
|
injector.Comp.CurrentTransferAmount = amount;
|
|
_popup.PopupClient(Loc.GetString("comp-solution-transfer-set-amount", ("amount", amount)), user, user);
|
|
Dirty(injector);
|
|
},
|
|
|
|
// we want to sort by size, not alphabetically by the verb text.
|
|
Priority = priority
|
|
};
|
|
args.Verbs.Add(verb);
|
|
}
|
|
}
|
|
|
|
// If the injector cannot toggle via using in hand, allow toggling via verb.
|
|
if (!activeMode.InjectOnUse || injector.Comp.AllowedModes.Count <= 1)
|
|
return;
|
|
|
|
var toggleModeVerb = new AlternativeVerb
|
|
{
|
|
Text = Loc.GetString("injector-toggle-verb-text"),
|
|
Act = () =>
|
|
{
|
|
ToggleMode(injector, user);
|
|
},
|
|
Priority = priority,
|
|
};
|
|
|
|
args.Verbs.Add(toggleModeVerb);
|
|
}
|
|
#endregion Events Handling
|
|
|
|
#region Mob Interaction
|
|
/// <summary>
|
|
/// Send informative pop-up messages and wait for a do-after to complete.
|
|
/// </summary>
|
|
private bool TryMobsDoAfter(Entity<InjectorComponent> injector, EntityUid user, EntityUid target)
|
|
{
|
|
if (_useDelay.IsDelayed(injector.Owner) // Check for Delay.
|
|
|| !GetMobsDoAfterTime(injector, user, target, out var doAfterTime, out var amount)) // Get the DoAfter time.
|
|
return false;
|
|
|
|
if (!_doAfter.TryStartDoAfter(new DoAfterArgs(EntityManager, user, doAfterTime, new InjectorDoAfterEvent(), injector.Owner, target: target, used: injector.Owner)
|
|
{
|
|
BreakOnMove = true,
|
|
BreakOnWeightlessMove = false,
|
|
BreakOnDamage = true,
|
|
NeedHand = injector.Comp.NeedHand,
|
|
BreakOnHandChange = injector.Comp.BreakOnHandChange,
|
|
MovementThreshold = injector.Comp.MovementThreshold,
|
|
}))
|
|
return false;
|
|
|
|
// If the DoAfter was instant, don't send popups and logs indicating an attempt.
|
|
if (doAfterTime == TimeSpan.Zero)
|
|
return true;
|
|
|
|
if (!_solutionContainer.ResolveSolution(injector.Owner, injector.Comp.SolutionName, ref injector.Comp.Solution, out var injectorSolution)
|
|
|| !_prototypeManager.Resolve(injector.Comp.ActiveModeProtoId, out var activeMode))
|
|
return false;
|
|
|
|
// Create a pop-up for the user.
|
|
_popup.PopupClient(Loc.GetString(activeMode.PopupUserAttempt), target, user);
|
|
|
|
if (user == target)
|
|
{
|
|
if (activeMode.Behavior.HasFlag(InjectorBehavior.Draw))
|
|
{
|
|
_adminLogger.Add(LogType.ForceFeed,
|
|
$"{ToPrettyString(user):user} is attempting to draw {amount} units from themselves.");
|
|
}
|
|
else
|
|
{
|
|
_adminLogger.Add(LogType.Ingestion,
|
|
$"{ToPrettyString(user):user} is attempting to inject themselves with a solution {SharedSolutionContainerSystem.ToPrettyString(injectorSolution):solution}.");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Create a popup to the target.
|
|
var userName = Identity.Entity(user, EntityManager);
|
|
var popup = Loc.GetString(activeMode.PopupTargetAttempt, ("user", userName));
|
|
_popup.PopupEntity(popup, user, target);
|
|
|
|
if (activeMode.Behavior.HasFlag(InjectorBehavior.Draw))
|
|
{
|
|
_adminLogger.Add(LogType.ForceFeed,
|
|
$"{ToPrettyString(user):user} is attempting to draw {amount} units from {ToPrettyString(target):target}");
|
|
}
|
|
else
|
|
{
|
|
_adminLogger.Add(LogType.ForceFeed,
|
|
$"{ToPrettyString(user):user} is attempting to inject {ToPrettyString(target):target} with a solution {SharedSolutionContainerSystem.ToPrettyString(injectorSolution):solution}");
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get the DoAfter Time for Mobs.
|
|
/// </summary>
|
|
/// <param name="injector">The injector that is interacting with the mob.</param>
|
|
/// <param name="user">The user using the injector.</param>
|
|
/// <param name="target">The target mob.</param>
|
|
/// <param name="doAfterTime">The duration of the resulting doAfter.</param>
|
|
/// <param name="amount">The amount of the reagents transferred.</param>
|
|
/// <returns>True if calculating the time was successful, false if not.</returns>
|
|
private bool GetMobsDoAfterTime(Entity<InjectorComponent> injector, EntityUid user, EntityUid target, out TimeSpan doAfterTime, out FixedPoint2 amount)
|
|
{
|
|
doAfterTime = TimeSpan.Zero;
|
|
amount = FixedPoint2.Zero;
|
|
|
|
if (!_solutionContainer.ResolveSolution(injector.Owner, injector.Comp.SolutionName, ref injector.Comp.Solution, out var injectorSolution)
|
|
|| !_prototypeManager.Resolve(injector.Comp.ActiveModeProtoId, out var activeMode))
|
|
return false;
|
|
|
|
doAfterTime = activeMode.MobTime;
|
|
|
|
// Can only draw blood with a draw mode and a transferAmount.
|
|
if (activeMode.Behavior.HasFlag(InjectorBehavior.Draw) && injector.Comp.CurrentTransferAmount != null)
|
|
{
|
|
// additional delay is based on actual volume left to draw in syringe when smaller than transfer amount
|
|
amount = FixedPoint2.Min(injector.Comp.CurrentTransferAmount.Value, injectorSolution.AvailableVolume);
|
|
}
|
|
else
|
|
{
|
|
// Check if we have anything to inject.
|
|
if (injectorSolution.Volume == 0)
|
|
{
|
|
_popup.PopupClient(Loc.GetString("injector-component-empty-message", ("injector", injector)), target, user);
|
|
return false;
|
|
}
|
|
|
|
// additional delay is based on actual volume left to inject in syringe when smaller than transfer amount
|
|
// If CurrentTransferAmount is null, it'll want to inject its entire contents, e.g., epipens.
|
|
amount = injector.Comp.CurrentTransferAmount ?? injectorSolution.Volume;
|
|
amount = FixedPoint2.Min(amount, injectorSolution.Volume);
|
|
}
|
|
|
|
// Transfers over the IgnoreDelayForVolume amount take Xu times DelayPerVolume longer.
|
|
doAfterTime += activeMode.DelayPerVolume * FixedPoint2.Max(0, amount - activeMode.IgnoreDelayForVolume).Double();
|
|
|
|
// Check if the target is either the user or downed.
|
|
if (user == target) // Self-injections take priority.
|
|
doAfterTime *= activeMode.SelfModifier;
|
|
// Technically, both can be true, but that is probably a balance nightmare.
|
|
else if (_standingState.IsDown(target))
|
|
doAfterTime *= activeMode.DownedModifier;
|
|
|
|
return true;
|
|
}
|
|
#endregion Mob Interaction
|
|
|
|
#region Container Interaction
|
|
private bool TryContainerDoAfter(Entity<InjectorComponent> injector, EntityUid user, EntityUid target)
|
|
{
|
|
if (!GetContainerDoAfterTime(injector, user, target, out var doAfterTime))
|
|
return false;
|
|
|
|
return _doAfter.TryStartDoAfter(new DoAfterArgs(EntityManager, user, doAfterTime, new InjectorDoAfterEvent(), injector.Owner, target: target, used: injector.Owner)
|
|
{
|
|
BreakOnMove = true,
|
|
BreakOnWeightlessMove = false,
|
|
BreakOnDamage = true,
|
|
NeedHand = injector.Comp.NeedHand,
|
|
BreakOnHandChange = injector.Comp.BreakOnHandChange,
|
|
MovementThreshold = injector.Comp.MovementThreshold,
|
|
});
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get the DoAfter Time for Containers and check if it is possible.
|
|
/// </summary>
|
|
/// <param name="injector">The injector that is interacting with the container.</param>
|
|
/// <param name="user">The user using the injector.</param>
|
|
/// <param name="target">The target container,</param>
|
|
/// <param name="doAfterTime">The duration of the resulting DoAfter.</param>
|
|
/// <returns>True if calculating the time was successful, false if not.</returns>
|
|
private bool GetContainerDoAfterTime(Entity<InjectorComponent> injector, EntityUid user, EntityUid target, out TimeSpan doAfterTime)
|
|
{
|
|
doAfterTime = TimeSpan.Zero;
|
|
|
|
if (!_prototypeManager.Resolve(injector.Comp.ActiveModeProtoId, out var activeMode))
|
|
return false;
|
|
|
|
// Check if the Injector has a draw time, but only when drawing.
|
|
if (!activeMode.Behavior.HasAnyFlag(InjectorBehavior.Draw | InjectorBehavior.Dynamic))
|
|
return true;
|
|
|
|
if (!_solutionContainer.ResolveSolution(injector.Owner, injector.Comp.SolutionName, ref injector.Comp.Solution, out var solution)
|
|
|| solution.AvailableVolume == 0)
|
|
{
|
|
_popup.PopupClient(Loc.GetString("injector-component-cannot-toggle-draw-message"), user, user);
|
|
return false; // If already full, fail drawing.
|
|
}
|
|
|
|
if (!_solutionContainer.TryGetDrawableSolution(target, out _, out var drawableSol))
|
|
{
|
|
_popup.PopupClient(Loc.GetString("injector-component-cannot-draw-message", ("target", Identity.Entity(target, EntityManager))), injector, user);
|
|
return false;
|
|
}
|
|
|
|
if (drawableSol.Volume == 0)
|
|
{
|
|
_popup.PopupClient(Loc.GetString("injector-component-target-is-empty-message", ("target", Identity.Entity(target, EntityManager))), injector, user);
|
|
return false;
|
|
}
|
|
|
|
doAfterTime = activeMode.ContainerDrawTime;
|
|
return true;
|
|
}
|
|
#endregion Container Interaction
|
|
|
|
#region Injecting/Drawing
|
|
/// <summary>
|
|
/// Depending on the <see cref="InjectorBehavior"/>, this will deal with the result of the DoAfter and draw/inject accordingly.
|
|
/// </summary>
|
|
/// <param name="injector">The injector used.</param>
|
|
/// <param name="user">The entity using the injector.</param>
|
|
/// <param name="target">The entity targeted by the user.</param>
|
|
/// <returns>True if the injection/drawing was successful, false if not.</returns>
|
|
/// <exception cref="ArgumentOutOfRangeException">The injector has a different <see cref="InjectorBehavior"/>.</exception>
|
|
private bool TryUseInjector(Entity<InjectorComponent> injector, EntityUid user, EntityUid target)
|
|
{
|
|
if (!_prototypeManager.Resolve(injector.Comp.ActiveModeProtoId, out var activeMode))
|
|
return false;
|
|
|
|
var isOpenOrIgnored = injector.Comp.IgnoreClosed || !_openable.IsClosed(target);
|
|
|
|
LocId msg = target == user ? "injector-component-cannot-transfer-message-self" : "injector-component-cannot-transfer-message";
|
|
|
|
switch (activeMode.Behavior)
|
|
{
|
|
// Handle injecting/drawing for solutions
|
|
case InjectorBehavior.Inject:
|
|
{
|
|
if (isOpenOrIgnored && _solutionContainer.TryGetInjectableSolution(target, out var injectableSolution, out _))
|
|
return TryInject(injector, user, target, injectableSolution.Value, false);
|
|
|
|
if (isOpenOrIgnored && _solutionContainer.TryGetRefillableSolution(target, out var refillableSolution, out _))
|
|
return TryInject(injector, user, target, refillableSolution.Value, true);
|
|
break;
|
|
}
|
|
case InjectorBehavior.Draw:
|
|
{
|
|
// Draw from a bloodstream if the target has that
|
|
if (TryComp<BloodstreamComponent>(target, out var stream) &&
|
|
_solutionContainer.ResolveSolution(target, stream.BloodSolutionName, ref stream.BloodSolution))
|
|
{
|
|
return TryDraw(injector, user, (target, stream), stream.BloodSolution.Value);
|
|
}
|
|
|
|
// Draw from an object (food, beaker, etc)
|
|
if (isOpenOrIgnored && _solutionContainer.TryGetDrawableSolution(target, out var drawableSolution, out _))
|
|
return TryDraw(injector, user, target, drawableSolution.Value);
|
|
|
|
msg = target == user ? "injector-component-cannot-draw-message-self" : "injector-component-cannot-draw-message";
|
|
_popup.PopupClient(Loc.GetString(msg, ("target", Identity.Entity(target, EntityManager))), injector, user);
|
|
break;
|
|
}
|
|
case InjectorBehavior.Dynamic:
|
|
{
|
|
// If it's a mob, inject. We're using injectableSolution so I don't have to code a sole method for injecting into bloodstreams.
|
|
if (HasComp<BloodstreamComponent>(target)
|
|
&& _solutionContainer.TryGetInjectableSolution(target, out var injectableSolution, out _))
|
|
{
|
|
return TryInject(injector, user, target, injectableSolution.Value, false);
|
|
}
|
|
|
|
// Draw from an object (food, beaker, etc.)
|
|
if (isOpenOrIgnored && _solutionContainer.TryGetDrawableSolution(target, out var drawableSolution, out _))
|
|
return TryDraw(injector, user, target, drawableSolution.Value);
|
|
break;
|
|
}
|
|
default:
|
|
throw new ArgumentOutOfRangeException();
|
|
}
|
|
|
|
_popup.PopupClient(Loc.GetString(msg, ("target", Identity.Entity(target, EntityManager))), injector, user);
|
|
return false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Attempt to inject the solution of the injector into the target.
|
|
/// </summary>
|
|
/// <param name="injector">The injector used.</param>
|
|
/// <param name="user">The entity using the injector.</param>
|
|
/// <param name="target">The entity targeted by the user.</param>
|
|
/// <param name="targetSolution">The solution of the target.</param>
|
|
/// <param name="asRefill">Whether or not the solution is refillable or injectable.</param>
|
|
/// <returns>True if the injection was successful, false if not.</returns>
|
|
private bool TryInject(Entity<InjectorComponent> injector, EntityUid user, EntityUid target, Entity<SolutionComponent> targetSolution, bool asRefill)
|
|
{
|
|
if (!_solutionContainer.ResolveSolution(injector.Owner,
|
|
injector.Comp.SolutionName,
|
|
ref injector.Comp.Solution,
|
|
out var injectorSolution) || injectorSolution.Volume == 0)
|
|
{
|
|
// If empty, show a popup.
|
|
_popup.PopupClient(Loc.GetString("injector-component-empty-message", ("injector", injector)), user, user);
|
|
return false;
|
|
}
|
|
|
|
if (!_prototypeManager.Resolve(injector.Comp.ActiveModeProtoId, out var activeMode))
|
|
return false;
|
|
|
|
var selfEv = new SelfBeforeInjectEvent(user, injector, target);
|
|
RaiseLocalEvent(user, selfEv);
|
|
|
|
if (selfEv.Cancelled)
|
|
{
|
|
// Clowns will now also fumble Syringes.
|
|
if (selfEv.OverrideMessage != null)
|
|
_popup.PopupPredicted(selfEv.OverrideMessage, user, user);
|
|
return true;
|
|
}
|
|
|
|
target = selfEv.TargetGettingInjected;
|
|
|
|
var ev = new TargetBeforeInjectEvent(user, injector, target);
|
|
RaiseLocalEvent(target, ref ev);
|
|
|
|
// Jugsuit blocking Hyposprays when
|
|
if (ev.Cancelled)
|
|
{
|
|
var userMessage = Loc.GetString("injector-component-blocked-user");
|
|
var otherMessage = Loc.GetString("injector-component-blocked-other", ("target", target), ("user", user));
|
|
_popup.PopupPredicted(userMessage, otherMessage, target, user, PopupType.SmallCaution);
|
|
return true;
|
|
}
|
|
|
|
// Get transfer amount. It may be smaller than _transferAmount if not enough room
|
|
var plannedTransferAmount = FixedPoint2.Min(injector.Comp.CurrentTransferAmount ?? injectorSolution.Volume, injectorSolution.Volume);
|
|
var realTransferAmount = FixedPoint2.Min(plannedTransferAmount, targetSolution.Comp.Solution.AvailableVolume);
|
|
|
|
if (realTransferAmount <= 0)
|
|
{
|
|
LocId msg = target == user ? "injector-component-target-already-full-message-self" : "injector-component-target-already-full-message";
|
|
_popup.PopupClient(
|
|
Loc.GetString(msg,
|
|
("target", Identity.Entity(target, EntityManager))),
|
|
injector.Owner,
|
|
user);
|
|
return false;
|
|
}
|
|
|
|
// Move units from attackSolution to targetSolution
|
|
Solution removedSolution;
|
|
if (TryComp<StackComponent>(target, out var stack))
|
|
removedSolution = _solutionContainer.SplitStackSolution(injector.Comp.Solution.Value, realTransferAmount, stack.Count);
|
|
else
|
|
removedSolution = _solutionContainer.SplitSolution(injector.Comp.Solution.Value, realTransferAmount);
|
|
|
|
_reactiveSystem.DoEntityReaction(target, removedSolution, ReactionMethod.Injection);
|
|
|
|
if (!asRefill)
|
|
_solutionContainer.Inject(target, targetSolution, removedSolution);
|
|
else
|
|
_solutionContainer.Refill(target, targetSolution, removedSolution);
|
|
|
|
LocId msgSuccess = target == user ? "injector-component-inject-success-message-self" : "injector-component-inject-success-message";
|
|
|
|
if (selfEv.OverrideMessage != null)
|
|
msgSuccess = selfEv.OverrideMessage;
|
|
else if (ev.OverrideMessage != null)
|
|
msgSuccess = ev.OverrideMessage;
|
|
|
|
_popup.PopupClient(Loc.GetString(msgSuccess, ("amount", removedSolution.Volume), ("target", Identity.Entity(target, EntityManager))), target, user);
|
|
|
|
// it is IMPERATIVE that when an injector is instant, that it has a pop-up.
|
|
if (activeMode.InjectPopupTarget != null && target != user)
|
|
_popup.PopupClient(Loc.GetString(activeMode.InjectPopupTarget), target, target);
|
|
|
|
// Some injectors like hyposprays have sound, some like syringes have not.
|
|
if (activeMode.InjectSound != null)
|
|
_audio.PlayPredicted(activeMode.InjectSound, injector, user);
|
|
|
|
// Log what happened.
|
|
_adminLogger.Add(LogType.ForceFeed, $"{ToPrettyString(user):user} injected {ToPrettyString(target):target} with a solution {SharedSolutionContainerSystem.ToPrettyString(removedSolution):removedSolution} using a {ToPrettyString(injector):using}");
|
|
|
|
AfterInject(injector, user, target);
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Attempt to draw reagents from a container.
|
|
/// </summary>
|
|
/// <param name="injector">The injector used.</param>
|
|
/// <param name="user">The entity using the injector.</param>
|
|
/// <param name="target">The entity targeted by the user.</param>
|
|
/// <param name="targetSolution">The solution of the target.</param>
|
|
/// <returns>True if the drawing was successful, false if not.</returns>
|
|
private bool TryDraw(Entity<InjectorComponent> injector, EntityUid user, Entity<BloodstreamComponent?> target, Entity<SolutionComponent> targetSolution)
|
|
{
|
|
if (!_solutionContainer.ResolveSolution(injector.Owner, injector.Comp.SolutionName, ref injector.Comp.Solution, out var solution) || solution.AvailableVolume == 0)
|
|
{
|
|
_popup.PopupClient("injector-component-cannot-toggle-draw-message", user, user);
|
|
return false;
|
|
}
|
|
|
|
var applicableTargetSolution = targetSolution.Comp.Solution;
|
|
// If a whitelist exists, remove all non-whitelisted reagents from the target solution temporarily
|
|
var temporarilyRemovedSolution = new Solution();
|
|
if (injector.Comp.ReagentWhitelist is { } reagentWhitelist)
|
|
{
|
|
temporarilyRemovedSolution = applicableTargetSolution.SplitSolutionWithout(applicableTargetSolution.Volume, reagentWhitelist.ToArray());
|
|
}
|
|
|
|
// Begin DeltaV Additions - skimmer functionality
|
|
else if (injector.Comp.TargetSmallest && applicableTargetSolution.Any())
|
|
{
|
|
var smallest = applicableTargetSolution.MinBy(soln => soln.Quantity);
|
|
ProtoId<ReagentPrototype> smallestReagent = smallest.Reagent.Prototype;
|
|
temporarilyRemovedSolution = applicableTargetSolution.SplitSolutionWithout(applicableTargetSolution.Volume, smallestReagent);
|
|
}
|
|
// End DeltaV Additions - skimmer functionality
|
|
|
|
// If transferAmount is null, fallback to 5 units.
|
|
var plannedTransferAmount = injector.Comp.CurrentTransferAmount ?? FixedPoint2.New(5);
|
|
// Get transfer amount. It may be smaller than _transferAmount if not enough room, also make sure there's room in the injector
|
|
var realTransferAmount = FixedPoint2.Min(plannedTransferAmount,
|
|
applicableTargetSolution.Volume,
|
|
solution.AvailableVolume);
|
|
|
|
if (realTransferAmount <= 0)
|
|
{
|
|
LocId msg = target.Owner == user ? "injector-component-target-is-empty-message-self" : "injector-component-target-is-empty-message";
|
|
var targetIdentity = Identity.Entity(target, EntityManager);
|
|
_popup.PopupClient(Loc.GetString(msg, ("target", targetIdentity)), injector.Owner, user);
|
|
return false;
|
|
}
|
|
|
|
// We have some snowflaked behavior for streams.
|
|
if (target.Comp != null)
|
|
{
|
|
DrawFromBlood(injector, user, (target.Owner, target.Comp), injector.Comp.Solution.Value, realTransferAmount);
|
|
return true;
|
|
}
|
|
|
|
// Move units from attackSolution to targetSolution
|
|
var removedSolution = _solutionContainer.Draw(target.Owner, targetSolution, realTransferAmount);
|
|
|
|
// Add back non-whitelisted reagents to the target solution
|
|
_solutionContainer.TryAddSolution(targetSolution, temporarilyRemovedSolution);
|
|
|
|
if (!_solutionContainer.TryAddSolution(injector.Comp.Solution.Value, removedSolution))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
LocId msgSuccess = target.Owner == user ? "injector-component-draw-success-message-self" : "injector-component-draw-success-message";
|
|
var targetIdentitySuccess = Identity.Entity(target, EntityManager);
|
|
_popup.PopupClient(
|
|
Loc.GetString(msgSuccess, ("amount", removedSolution.Volume), ("target", targetIdentitySuccess)),
|
|
target,
|
|
user);
|
|
|
|
AfterDraw(injector, user, target);
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Attempt to draw blood from a mob.
|
|
/// </summary>
|
|
/// <param name="injector">The injector used.</param>
|
|
/// <param name="user">The entity using the injector.</param>
|
|
/// <param name="target">The entity targeted by the user.</param>
|
|
/// <param name="injectorSolution">The solution of the injector.</param>
|
|
/// <param name="transferAmount">The amount of blood to draw.</param>
|
|
private void DrawFromBlood(Entity<InjectorComponent> injector,
|
|
EntityUid user,
|
|
Entity<BloodstreamComponent> target,
|
|
Entity<SolutionComponent> injectorSolution,
|
|
FixedPoint2 transferAmount)
|
|
{
|
|
if (_solutionContainer.ResolveSolution(target.Owner, target.Comp.BloodSolutionName, ref target.Comp.BloodSolution))
|
|
{
|
|
var bloodTemp = _solutionContainer.SplitSolution(target.Comp.BloodSolution.Value, transferAmount);
|
|
_solutionContainer.TryAddSolution(injectorSolution, bloodTemp);
|
|
}
|
|
|
|
LocId msg = target.Owner == user ? "injector-component-draw-success-message-self" : "injector-component-draw-success-message";
|
|
var targetIdentity = Identity.Entity(target, EntityManager);
|
|
var finalMessage = Loc.GetString(msg, ("amount", transferAmount), ("target", targetIdentity));
|
|
_popup.PopupClient(finalMessage, target, user);
|
|
|
|
AfterDraw(injector, user, target);
|
|
}
|
|
|
|
/// <summary>
|
|
/// This handles logic like DNA and Delays after injection.
|
|
/// </summary>
|
|
/// <param name="injector">The injector used.</param>
|
|
/// <param name="user">The entity using the injector.</param>
|
|
/// <param name="target">The entity targeted by the user.</param>
|
|
private void AfterInject(Entity<InjectorComponent> injector, EntityUid user, EntityUid target)
|
|
{
|
|
// Leave some DNA from the injectee on it
|
|
_forensics.TransferDna(injector, target);
|
|
// Reset the delay, if present.
|
|
|
|
_useDelay.TryResetDelay(injector);
|
|
|
|
// Automatically set syringe to draw after completely draining it.
|
|
if (!_solutionContainer.ResolveSolution(injector.Owner, injector.Comp.SolutionName, ref injector.Comp.Solution, out var solution)
|
|
|| solution.Volume != 0)
|
|
return;
|
|
|
|
if (!_prototypeManager.Resolve(injector.Comp.ActiveModeProtoId, out var activeMode)
|
|
|| activeMode.Behavior.HasFlag(InjectorBehavior.Dynamic))
|
|
return;
|
|
|
|
foreach (var mode in injector.Comp.AllowedModes)
|
|
{
|
|
if (!_prototypeManager.Resolve(mode, out var proto)
|
|
|| !proto.Behavior.HasFlag(InjectorBehavior.Draw))
|
|
continue;
|
|
|
|
ToggleMode(injector, user, proto);
|
|
return;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// This handles logic like DNA after drawing.
|
|
/// </summary>
|
|
/// <param name="injector">The injector used.</param>
|
|
/// <param name="user">The entity using the injector.</param>
|
|
/// <param name="target">The entity targeted by the user.</param>
|
|
private void AfterDraw(Entity<InjectorComponent> injector, EntityUid user, EntityUid target)
|
|
{
|
|
// Leave some DNA from the drawee on it
|
|
_forensics.TransferDna(injector, target);
|
|
|
|
// Automatically set the syringe to inject after completely filling it.
|
|
if (!_solutionContainer.ResolveSolution(injector.Owner, injector.Comp.SolutionName, ref injector.Comp.Solution, out var solution)
|
|
|| solution.AvailableVolume != 0)
|
|
return;
|
|
|
|
if (!_prototypeManager.Resolve(injector.Comp.ActiveModeProtoId, out var activeMode)
|
|
|| activeMode.Behavior.HasFlag(InjectorBehavior.Dynamic))
|
|
return;
|
|
|
|
foreach (var mode in injector.Comp.AllowedModes)
|
|
{
|
|
if (!_prototypeManager.Resolve(mode, out var proto)
|
|
|| !proto.Behavior.HasFlag(InjectorBehavior.Inject))
|
|
continue;
|
|
|
|
ToggleMode(injector, user, proto);
|
|
return;
|
|
}
|
|
}
|
|
#endregion Injecting/Drawing
|
|
|
|
#region Mode Toggling
|
|
/// <summary>
|
|
/// Toggle modes of the injector if possible.
|
|
/// </summary>
|
|
/// <param name="injector">The injector whose mode is to be toggled.</param>
|
|
/// <param name="user">The user toggling the mode.</param>
|
|
/// <param name="mode">The desired mode.</param>
|
|
/// <remarks>This will still check if the injector can use that mode.</remarks>
|
|
[PublicAPI]
|
|
public void ToggleMode(Entity<InjectorComponent> injector, EntityUid user, InjectorModePrototype mode)
|
|
{
|
|
var index = injector.Comp.AllowedModes.FindIndex(nextMode => mode == nextMode);
|
|
|
|
injector.Comp.ActiveModeProtoId = injector.Comp.AllowedModes[index];
|
|
|
|
if (!_prototypeManager.Resolve(injector.Comp.ActiveModeProtoId, out var newMode))
|
|
return;
|
|
|
|
var modeName = Loc.GetString(newMode.Name);
|
|
var message = Loc.GetString("injector-component-mode-changed-text", ("mode", modeName));
|
|
_popup.PopupClient(message, user, user);
|
|
Dirty(injector);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Toggle the mode of the injector to the next allowed mode.
|
|
/// </summary>
|
|
/// <param name="injector">The injector whose mode is to be toggled.</param>
|
|
/// <param name="user">The user toggling the mode.</param>
|
|
[PublicAPI]
|
|
public void ToggleMode(Entity<InjectorComponent> injector, EntityUid user)
|
|
{
|
|
if (!_prototypeManager.Resolve(injector.Comp.ActiveModeProtoId, out var activeProto))
|
|
return;
|
|
|
|
string? errorMessage = null;
|
|
|
|
foreach (var allowedMode in injector.Comp.AllowedModes)
|
|
{
|
|
if (!_prototypeManager.Resolve(allowedMode, out var proto)
|
|
|| proto.Behavior.HasFlag(activeProto.Behavior)
|
|
|| !_solutionContainer.ResolveSolution(injector.Owner, injector.Comp.SolutionName, ref injector.Comp.Solution, out var solution))
|
|
continue;
|
|
|
|
if (proto.Behavior.HasFlag(InjectorBehavior.Inject) && solution.Volume == 0)
|
|
{
|
|
errorMessage = "injector-component-cannot-toggle-inject-message";
|
|
continue;
|
|
}
|
|
|
|
if (proto.Behavior.HasFlag(InjectorBehavior.Draw) && solution.AvailableVolume == 0)
|
|
{
|
|
errorMessage = "injector-component-cannot-toggle-draw-message";
|
|
continue;
|
|
}
|
|
|
|
ToggleMode(injector, user, proto);
|
|
return;
|
|
}
|
|
if (errorMessage != null)
|
|
_popup.PopupClient(Loc.GetString(errorMessage), user, user);
|
|
}
|
|
#endregion Mode Toggling
|
|
}
|