2020-08-22 22:29:20 +02:00
using System ;
2021-12-07 19:19:26 +13:00
using System.Threading ;
2020-12-17 10:45:04 +03:00
using System.Threading.Tasks ;
2021-12-05 00:33:21 +13:00
using Content.Server.Administration.Logs ;
2021-11-11 16:10:57 -07:00
using Content.Server.Body.Components ;
2021-11-30 16:47:21 -07:00
using Content.Server.Body.Systems ;
2021-10-29 13:40:15 +01:00
using Content.Server.Chemistry.Components.SolutionManager ;
using Content.Server.Chemistry.EntitySystems ;
2021-12-05 00:33:21 +13:00
using Content.Server.CombatMode ;
using Content.Server.DoAfter ;
using Content.Shared.ActionBlocker ;
2021-11-11 16:10:57 -07:00
using Content.Shared.Body.Components ;
2021-06-09 22:19:39 +02:00
using Content.Shared.Chemistry.Components ;
using Content.Shared.Chemistry.Reagent ;
2021-12-05 00:33:21 +13:00
using Content.Shared.Database ;
2021-11-03 16:48:03 -07:00
using Content.Shared.FixedPoint ;
2021-06-09 22:19:39 +02:00
using Content.Shared.Interaction ;
using Content.Shared.Interaction.Helpers ;
2021-12-05 00:33:21 +13:00
using Content.Shared.MobState.Components ;
2021-09-26 15:18:45 +02:00
using Content.Shared.Popups ;
2020-02-23 19:47:33 -05:00
using Robust.Shared.GameObjects ;
2021-12-03 11:11:52 +01:00
using Robust.Shared.IoC ;
2020-02-23 19:47:33 -05:00
using Robust.Shared.Localization ;
2021-12-05 00:33:21 +13:00
using Robust.Shared.Player ;
2021-02-18 09:09:07 +01:00
using Robust.Shared.Players ;
2021-03-05 01:08:38 +01:00
using Robust.Shared.Serialization.Manager.Attributes ;
2020-02-23 19:47:33 -05:00
using Robust.Shared.ViewVariables ;
2021-06-09 22:19:39 +02:00
namespace Content.Server.Chemistry.Components
2020-02-23 19:47:33 -05:00
{
/// <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]
2021-09-06 15:49:44 +02:00
public class InjectorComponent : SharedInjectorComponent , IAfterInteract , IUse
2020-02-23 19:47:33 -05:00
{
2021-12-05 18:09:01 +01:00
[Dependency] private readonly IEntityManager _entities = default ! ;
2021-09-06 15:49:44 +02:00
public const string SolutionName = "injector" ;
2020-02-23 19:47:33 -05:00
/// <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>
2021-03-05 01:08:38 +01:00
[ViewVariables]
[DataField("injectOnly")]
private bool _injectOnly ;
2020-02-23 19:47:33 -05:00
/// <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>
2021-12-05 00:33:21 +13:00
[ViewVariables(VVAccess.ReadWrite)]
2021-03-05 01:08:38 +01:00
[DataField("transferAmount")]
2021-11-03 16:48:03 -07:00
private FixedPoint2 _transferAmount = FixedPoint2 . New ( 5 ) ;
2020-02-23 19:47:33 -05:00
/// <summary>
2021-12-05 00:33:21 +13:00
/// Injection delay (seconds) when the target is a mob.
2020-02-23 19:47:33 -05:00
/// </summary>
2021-12-05 00:33:21 +13:00
/// <remarks>
/// The base delay has a minimum of 1 second, but this will still be modified if the target is incapacitated or
/// in combat mode.
/// </remarks>
[ViewVariables(VVAccess.ReadWrite)]
[DataField("delay")]
public float Delay = 5 ;
/// <summary>
2021-12-07 19:19:26 +13:00
/// Token for interrupting a do-after action (e.g., injection another player). If not null, implies
/// component is currently "in use".
2021-12-05 00:33:21 +13:00
/// </summary>
2021-12-07 19:19:26 +13:00
public CancellationTokenSource ? CancelToken ;
2020-02-23 19:47:33 -05:00
2021-01-25 02:10:23 +01:00
private InjectorToggleMode _toggleState ;
2020-02-23 19:47:33 -05:00
/// <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)]
2021-01-25 02:10:23 +01:00
public InjectorToggleMode ToggleState
{
get = > _toggleState ;
set
{
_toggleState = value ;
Dirty ( ) ;
}
}
2020-02-23 19:47:33 -05:00
2020-04-08 15:53:15 +05:00
protected override void Startup ( )
2020-02-23 19:47:33 -05:00
{
2020-04-08 15:53:15 +05:00
base . Startup ( ) ;
2020-08-22 22:29:20 +02:00
2021-01-10 16:21:09 +01:00
Dirty ( ) ;
2020-02-23 19:47:33 -05:00
}
/// <summary>
/// Toggle between draw/inject state if applicable
/// </summary>
2021-12-05 18:09:01 +01:00
private void Toggle ( EntityUid user )
2020-02-23 19:47:33 -05:00
{
if ( _injectOnly )
{
return ;
}
2020-02-24 17:40:24 +01:00
string msg ;
2021-01-25 02:10:23 +01:00
switch ( ToggleState )
2020-02-23 19:47:33 -05:00
{
2020-02-24 17:40:24 +01:00
case InjectorToggleMode . Inject :
2021-01-25 02:10:23 +01:00
ToggleState = InjectorToggleMode . Draw ;
2021-06-21 02:13:54 +02:00
msg = "injector-component-drawing-text" ;
2020-02-24 17:40:24 +01:00
break ;
case InjectorToggleMode . Draw :
2021-01-25 02:10:23 +01:00
ToggleState = InjectorToggleMode . Inject ;
2021-06-21 02:13:54 +02:00
msg = "injector-component-injecting-text" ;
2020-02-24 17:40:24 +01:00
break ;
default :
throw new ArgumentOutOfRangeException ( ) ;
}
2020-09-01 12:34:53 +02:00
Owner . PopupMessage ( user , Loc . GetString ( msg ) ) ;
2020-02-23 19:47:33 -05:00
}
/// <summary>
/// Called when clicking on entities while holding in active hand
/// </summary>
/// <param name="eventArgs"></param>
2021-02-03 14:05:31 +01:00
async Task < bool > IAfterInteract . AfterInteract ( AfterInteractEventArgs eventArgs )
2020-02-23 19:47:33 -05:00
{
2021-12-07 19:19:26 +13:00
if ( CancelToken ! = null )
{
CancelToken . Cancel ( ) ;
return true ;
}
2021-12-05 00:33:21 +13:00
2021-02-03 14:05:31 +01:00
if ( ! eventArgs . InRangeUnobstructed ( ignoreInsideBlocker : true , popup : true ) )
return false ;
2020-05-23 02:27:31 -07:00
2021-12-07 17:48:49 +01:00
if ( ! EntitySystem . Get < ActionBlockerSystem > ( ) . CanInteract ( eventArgs . User ) )
2021-12-05 00:33:21 +13:00
return false ;
2021-09-06 15:49:44 +02:00
var solutionsSys = EntitySystem . Get < SolutionContainerSystem > ( ) ;
2020-02-23 19:47:33 -05:00
//Make sure we have the attacking entity
2021-12-05 18:09:01 +01:00
if ( eventArgs . Target is not { Valid : true } target | |
! _entities . HasComponent < SolutionContainerManagerComponent > ( Owner ) )
2020-02-23 19:47:33 -05:00
{
2021-02-03 14:05:31 +01:00
return false ;
2020-02-23 19:47:33 -05:00
}
2021-12-05 00:33:21 +13:00
// Is the target a mob? If yes, use a do-after to give them time to respond.
2021-12-07 17:48:49 +01:00
if ( _entities . HasComponent < MobStateComponent > ( target ) | |
_entities . HasComponent < BloodstreamComponent > ( target ) )
2021-12-05 00:33:21 +13:00
{
2021-12-07 17:48:49 +01:00
if ( ! await TryInjectDoAfter ( eventArgs . User , target ) )
2021-12-05 00:33:21 +13:00
return true ;
}
2021-09-06 15:49:44 +02:00
2020-09-09 18:32:31 -04:00
// Handle injecting/drawing for solutions
2021-09-06 15:49:44 +02:00
if ( ToggleState = = InjectorToggleMode . Inject )
2020-02-23 19:47:33 -05:00
{
2021-12-05 18:09:01 +01:00
if ( solutionsSys . TryGetInjectableSolution ( target , out var injectableSolution ) )
2021-09-06 15:49:44 +02:00
{
2021-12-05 18:09:01 +01:00
TryInject ( target , injectableSolution , eventArgs . User , false ) ;
2021-10-01 11:06:50 +01:00
}
2021-12-05 18:09:01 +01:00
else if ( solutionsSys . TryGetRefillableSolution ( target , out var refillableSolution ) )
2021-10-01 11:06:50 +01:00
{
2021-12-05 18:09:01 +01:00
TryInject ( target , refillableSolution , eventArgs . User , true ) ;
2021-09-06 15:49:44 +02:00
}
2021-12-05 18:09:01 +01:00
else if ( _entities . TryGetComponent ( target , out BloodstreamComponent ? bloodstream ) )
2020-02-23 19:47:33 -05:00
{
2021-09-06 15:49:44 +02:00
TryInjectIntoBloodstream ( bloodstream , eventArgs . User ) ;
2020-02-23 19:47:33 -05:00
}
2021-09-06 15:49:44 +02:00
else
2020-02-23 19:47:33 -05:00
{
2021-09-06 15:49:44 +02:00
eventArgs . User . PopupMessage ( eventArgs . User ,
Loc . GetString ( "injector-component-cannot-transfer-message" ,
2021-12-05 18:09:01 +01:00
( "target" , target ) ) ) ;
2020-02-23 19:47:33 -05:00
}
}
2021-09-06 15:49:44 +02:00
else if ( ToggleState = = InjectorToggleMode . Draw )
2020-02-23 19:47:33 -05:00
{
2021-12-05 18:09:01 +01:00
if ( solutionsSys . TryGetDrawableSolution ( target , out var drawableSolution ) )
2021-09-06 15:49:44 +02:00
{
2021-12-05 18:09:01 +01:00
TryDraw ( target , drawableSolution , eventArgs . User ) ;
2021-09-06 15:49:44 +02:00
}
else
{
eventArgs . User . PopupMessage ( eventArgs . User ,
Loc . GetString ( "injector-component-cannot-draw-message" ,
2021-12-05 18:09:01 +01:00
( "target" , target ) ) ) ;
2021-09-06 15:49:44 +02:00
}
2020-02-23 19:47:33 -05:00
}
2021-02-03 14:05:31 +01:00
return true ;
2020-02-23 19:47:33 -05:00
}
2021-12-05 00:33:21 +13:00
/// <summary>
/// Send informative pop-up messages and wait for a do-after to complete.
/// </summary>
public async Task < bool > TryInjectDoAfter ( EntityUid user , EntityUid target )
{
var popupSys = EntitySystem . Get < SharedPopupSystem > ( ) ;
// Create a pop-up for the user
popupSys . PopupEntity ( Loc . GetString ( "injector-component-injecting-user" ) , target , Filter . Entities ( user ) ) ;
2021-12-14 00:22:58 +13:00
if ( ! EntitySystem . Get < SolutionContainerSystem > ( ) . TryGetSolution ( Owner , SolutionName , out var solution ) )
return false ;
2021-12-05 00:33:21 +13:00
// Get entity for logging. Log with EntityUids when?
var logSys = EntitySystem . Get < AdminLogSystem > ( ) ;
var actualDelay = MathF . Max ( Delay , 1f ) ;
if ( user ! = target )
{
// Create a pop-up for the target
2021-12-07 17:48:49 +01:00
var userName = _entities . GetComponent < MetaDataComponent > ( user ) . EntityName ;
2021-12-05 00:33:21 +13:00
popupSys . PopupEntity ( Loc . GetString ( "injector-component-injecting-target" ,
( "user" , userName ) ) , user , Filter . Entities ( target ) ) ;
// Check if the target is incapacitated or in combat mode and modify time accordingly.
2021-12-07 17:48:49 +01:00
if ( _entities . TryGetComponent < MobStateComponent > ( target , out var mobState ) & &
2021-12-05 00:33:21 +13:00
mobState . IsIncapacitated ( ) )
{
actualDelay / = 2 ;
}
2021-12-07 17:48:49 +01:00
else if ( _entities . TryGetComponent < CombatModeComponent > ( target , out var combat ) & &
combat . IsInCombatMode )
2021-12-05 00:33:21 +13:00
{
// Slightly increase the delay when the target is in combat mode. Helps prevents cheese injections in
// combat with fast syringes & lag.
actualDelay + = 1 ;
}
// Add an admin log, using the "force feed" log type. It's not quite feeding, but the effect is the same.
if ( ToggleState = = InjectorToggleMode . Inject )
{
logSys . Add ( LogType . ForceFeed ,
2021-12-14 00:22:58 +13:00
$"{_entities.ToPrettyString(user):user} is attempting to inject {_entities.ToPrettyString(target):target} with a solution {SolutionContainerSystem.ToPrettyString(solution):solution}" ) ;
2021-12-05 00:33:21 +13:00
// TODO solution pretty string.
}
}
else
{
// Self-injections take half as long.
actualDelay / = 2 ;
if ( ToggleState = = InjectorToggleMode . Inject )
logSys . Add ( LogType . Ingestion ,
2021-12-14 00:22:58 +13:00
$"{_entities.ToPrettyString(user):user} is attempting to inject themselves with a solution {SolutionContainerSystem.ToPrettyString(solution):solution}." ) ;
2021-12-05 00:33:21 +13:00
//TODO solution pretty string.
}
2021-12-07 19:19:26 +13:00
CancelToken = new ( ) ;
2021-12-05 00:33:21 +13:00
var status = await EntitySystem . Get < DoAfterSystem > ( ) . WaitDoAfter (
2021-12-07 19:19:26 +13:00
new DoAfterEventArgs ( user , actualDelay , CancelToken . Token , target )
2021-12-05 00:33:21 +13:00
{
BreakOnUserMove = true ,
BreakOnDamage = true ,
BreakOnStun = true ,
BreakOnTargetMove = true ,
MovementThreshold = 1.0f
} ) ;
2021-12-07 19:19:26 +13:00
CancelToken = null ;
2021-12-05 00:33:21 +13:00
return status = = DoAfterStatus . Finished ;
2020-02-23 19:47:33 -05:00
}
/// <summary>
/// Called when use key is pressed when held in active hand
/// </summary>
/// <param name="eventArgs"></param>
/// <returns></returns>
bool IUse . UseEntity ( UseEntityEventArgs eventArgs )
{
2020-02-24 17:40:24 +01:00
Toggle ( eventArgs . User ) ;
2020-02-23 19:47:33 -05:00
return true ;
}
2021-12-05 18:09:01 +01:00
private void TryInjectIntoBloodstream ( BloodstreamComponent targetBloodstream , EntityUid user )
2020-02-23 19:47:33 -05:00
{
2020-09-09 18:32:31 -04:00
// Get transfer amount. May be smaller than _transferAmount if not enough room
2021-11-30 16:47:21 -07:00
var realTransferAmount = FixedPoint2 . Min ( _transferAmount , targetBloodstream . Solution . AvailableVolume ) ;
2020-09-09 18:32:31 -04:00
2020-02-23 19:47:33 -05:00
if ( realTransferAmount < = 0 )
{
2021-02-03 14:05:31 +01:00
Owner . PopupMessage ( user ,
2021-10-01 11:06:50 +01:00
Loc . GetString ( "injector-component-cannot-inject-message" , ( "target" , targetBloodstream . Owner ) ) ) ;
2020-02-23 19:47:33 -05:00
return ;
}
2020-09-09 18:32:31 -04:00
// Move units from attackSolution to targetSolution
2021-09-06 15:49:44 +02:00
var removedSolution =
2021-12-03 15:53:09 +01:00
EntitySystem . Get < SolutionContainerSystem > ( ) . SplitSolution ( user , targetBloodstream . Solution , realTransferAmount ) ;
2020-09-26 14:48:24 +02:00
2021-11-30 16:47:21 -07:00
var bloodstreamSys = EntitySystem . Get < BloodstreamSystem > ( ) ;
2021-12-07 22:22:34 +11:00
bloodstreamSys . TryAddToBloodstream ( ( targetBloodstream ) . Owner , removedSolution , targetBloodstream ) ;
2020-09-26 14:48:24 +02:00
2021-12-03 15:53:09 +01:00
removedSolution . DoEntityReaction ( targetBloodstream . Owner , ReactionMethod . Injection ) ;
2020-09-26 14:48:24 +02:00
2021-02-03 14:05:31 +01:00
Owner . PopupMessage ( user ,
2021-06-21 02:13:54 +02:00
Loc . GetString ( "injector-component-inject-success-message" ,
2021-09-06 15:49:44 +02:00
( "amount" , removedSolution . TotalVolume ) ,
( "target" , targetBloodstream . Owner ) ) ) ;
2020-02-23 19:47:33 -05:00
Dirty ( ) ;
2021-01-25 02:10:23 +01:00
AfterInject ( ) ;
2020-02-23 19:47:33 -05:00
}
2021-12-05 18:09:01 +01:00
private void TryInject ( EntityUid targetEntity , Solution targetSolution , EntityUid user , bool asRefill )
2020-02-23 19:47:33 -05:00
{
2021-12-03 15:53:09 +01:00
if ( ! EntitySystem . Get < SolutionContainerSystem > ( ) . TryGetSolution ( Owner , SolutionName , out var solution )
2021-09-06 15:49:44 +02:00
| | solution . CurrentVolume = = 0 )
2020-02-23 19:47:33 -05:00
{
return ;
}
2020-09-09 18:32:31 -04:00
// Get transfer amount. May be smaller than _transferAmount if not enough room
2021-11-03 16:48:03 -07:00
var realTransferAmount = FixedPoint2 . Min ( _transferAmount , targetSolution . AvailableVolume ) ;
2020-09-09 18:32:31 -04:00
2020-02-23 19:47:33 -05:00
if ( realTransferAmount < = 0 )
{
2021-09-06 15:49:44 +02:00
Owner . PopupMessage ( user ,
Loc . GetString ( "injector-component-target-already-full-message" , ( "target" , targetEntity ) ) ) ;
2020-02-23 19:47:33 -05:00
return ;
}
2020-09-09 18:32:31 -04:00
// Move units from attackSolution to targetSolution
2021-12-03 15:53:09 +01:00
var removedSolution = EntitySystem . Get < SolutionContainerSystem > ( ) . SplitSolution ( Owner , solution , realTransferAmount ) ;
2020-09-09 18:32:31 -04:00
2021-12-03 15:53:09 +01:00
removedSolution . DoEntityReaction ( targetEntity , ReactionMethod . Injection ) ;
2020-09-26 14:48:24 +02:00
2021-10-01 11:06:50 +01:00
if ( ! asRefill )
{
EntitySystem . Get < SolutionContainerSystem > ( )
2021-12-03 15:53:09 +01:00
. Inject ( targetEntity , targetSolution , removedSolution ) ;
2021-10-01 11:06:50 +01:00
}
else
{
EntitySystem . Get < SolutionContainerSystem > ( )
2021-12-03 15:53:09 +01:00
. Refill ( targetEntity , targetSolution , removedSolution ) ;
2021-10-01 11:06:50 +01:00
}
2020-09-26 14:48:24 +02:00
2021-02-03 14:05:31 +01:00
Owner . PopupMessage ( user ,
2021-06-21 02:13:54 +02:00
Loc . GetString ( "injector-component-transfer-success-message" ,
2021-09-06 15:49:44 +02:00
( "amount" , removedSolution . TotalVolume ) ,
( "target" , targetEntity ) ) ) ;
2020-02-23 19:47:33 -05:00
Dirty ( ) ;
2021-01-25 02:10:23 +01:00
AfterInject ( ) ;
}
private void AfterInject ( )
{
// Automatically set syringe to draw after completely draining it.
2021-12-03 15:53:09 +01:00
if ( EntitySystem . Get < SolutionContainerSystem > ( ) . TryGetSolution ( Owner , SolutionName , out var solution )
2021-09-06 15:49:44 +02:00
& & solution . CurrentVolume = = 0 )
2021-01-25 02:10:23 +01:00
{
ToggleState = InjectorToggleMode . Draw ;
}
2020-02-23 19:47:33 -05:00
}
2021-09-06 15:49:44 +02:00
private void AfterDraw ( )
{
// Automatically set syringe to inject after completely filling it.
2021-12-03 15:53:09 +01:00
if ( EntitySystem . Get < SolutionContainerSystem > ( ) . TryGetSolution ( Owner , SolutionName , out var solution )
2021-09-06 15:49:44 +02:00
& & solution . AvailableVolume = = 0 )
{
ToggleState = InjectorToggleMode . Inject ;
}
}
2021-12-05 18:09:01 +01:00
private void TryDraw ( EntityUid targetEntity , Solution targetSolution , EntityUid user )
2020-02-23 19:47:33 -05:00
{
2021-12-03 15:53:09 +01:00
if ( ! EntitySystem . Get < SolutionContainerSystem > ( ) . TryGetSolution ( Owner , SolutionName , out var solution )
2021-09-06 15:49:44 +02:00
| | solution . AvailableVolume = = 0 )
2020-02-23 19:47:33 -05:00
{
return ;
}
2020-09-09 18:32:31 -04:00
// Get transfer amount. May be smaller than _transferAmount if not enough room
2021-11-03 16:48:03 -07:00
var realTransferAmount = FixedPoint2 . Min ( _transferAmount , targetSolution . DrawAvailable ) ;
2020-09-09 18:32:31 -04:00
2020-02-23 19:47:33 -05:00
if ( realTransferAmount < = 0 )
{
2021-09-06 15:49:44 +02:00
Owner . PopupMessage ( user ,
Loc . GetString ( "injector-component-target-is-empty-message" , ( "target" , targetEntity ) ) ) ;
2020-02-23 19:47:33 -05:00
return ;
}
2020-09-09 18:32:31 -04:00
// Move units from attackSolution to targetSolution
2021-09-06 15:49:44 +02:00
var removedSolution = EntitySystem . Get < SolutionContainerSystem > ( )
2021-12-03 15:53:09 +01:00
. Draw ( targetEntity , targetSolution , realTransferAmount ) ;
2020-09-09 18:32:31 -04:00
2021-12-03 15:53:09 +01:00
if ( ! EntitySystem . Get < SolutionContainerSystem > ( ) . TryAddSolution ( targetEntity , solution , removedSolution ) )
2020-02-23 19:47:33 -05:00
{
return ;
}
2021-02-03 14:05:31 +01:00
Owner . PopupMessage ( user ,
2021-06-21 02:13:54 +02:00
Loc . GetString ( "injector-component-draw-success-message" ,
2021-09-06 15:49:44 +02:00
( "amount" , removedSolution . TotalVolume ) ,
( "target" , targetEntity ) ) ) ;
2020-02-23 19:47:33 -05:00
Dirty ( ) ;
2021-01-25 02:10:23 +01:00
AfterDraw ( ) ;
}
2021-01-10 16:21:09 +01:00
2021-11-30 15:20:38 +01:00
public override ComponentState GetComponentState ( )
2020-02-23 19:47:33 -05:00
{
2021-12-05 18:09:01 +01:00
_entities . EntitySysManager . GetEntitySystem < SolutionContainerSystem > ( )
2021-12-03 15:53:09 +01:00
. TryGetSolution ( Owner , SolutionName , out var solution ) ;
2020-08-22 22:29:20 +02:00
2021-11-03 16:48:03 -07:00
var currentVolume = solution ? . CurrentVolume ? ? FixedPoint2 . Zero ;
var maxVolume = solution ? . MaxVolume ? ? FixedPoint2 . Zero ;
2020-08-22 22:29:20 +02:00
2021-01-25 02:10:23 +01:00
return new InjectorComponentState ( currentVolume , maxVolume , ToggleState ) ;
2020-02-23 19:47:33 -05:00
}
}
}