2024-03-03 21:07:11 -08:00
using Content.Server.Body.Components ;
2024-02-15 00:05:01 +01:00
using Content.Server.Body.Systems ;
2024-09-18 15:56:50 +03:00
using Content.Shared._CP14.Farming ;
2024-06-15 16:55:04 +03:00
using Content.Shared._CP14.Skills ;
2024-02-15 00:05:01 +01:00
using Content.Shared.Chemistry ;
using Content.Shared.Chemistry.Components ;
using Content.Shared.Chemistry.Components.SolutionManager ;
using Content.Shared.Chemistry.EntitySystems ;
using Content.Shared.Chemistry.Reagent ;
using Content.Shared.Database ;
using Content.Shared.DoAfter ;
using Content.Shared.FixedPoint ;
using Content.Shared.Forensics ;
using Content.Shared.IdentityManagement ;
using Content.Shared.Interaction ;
using Content.Shared.Mobs.Components ;
using Content.Shared.Stacks ;
2024-11-12 06:06:43 -06:00
using Content.Shared.Nutrition.EntitySystems ;
2024-02-15 00:05:01 +01:00
namespace Content.Server.Chemistry.EntitySystems ;
public sealed class InjectorSystem : SharedInjectorSystem
{
[Dependency] private readonly BloodstreamSystem _blood = default ! ;
[Dependency] private readonly ReactiveSystem _reactiveSystem = default ! ;
2024-11-12 06:06:43 -06:00
[Dependency] private readonly OpenableSystem _openable = default ! ;
2024-02-15 00:05:01 +01:00
public override void Initialize ( )
{
base . Initialize ( ) ;
SubscribeLocalEvent < InjectorComponent , InjectorDoAfterEvent > ( OnInjectDoAfter ) ;
SubscribeLocalEvent < InjectorComponent , AfterInteractEvent > ( OnInjectorAfterInteract ) ;
}
2024-05-30 00:18:07 -04:00
private bool TryUseInjector ( Entity < InjectorComponent > injector , EntityUid target , EntityUid user )
2024-02-15 00:05:01 +01:00
{
2024-06-15 16:55:04 +03:00
RaiseLocalEvent ( injector , new CP14TrySkillIssueEvent ( user ) ) ; //CP14 Skill issue event
2024-11-12 06:06:43 -06:00
var isOpenOrIgnored = injector . Comp . IgnoreClosed | | ! _openable . IsClosed ( target ) ;
2024-02-15 00:05:01 +01:00
// Handle injecting/drawing for solutions
if ( injector . Comp . ToggleState = = InjectorToggleMode . Inject )
{
2024-11-12 06:06:43 -06:00
if ( isOpenOrIgnored & & SolutionContainers . TryGetInjectableSolution ( target , out var injectableSolution , out _ ) )
2024-05-30 00:18:07 -04:00
return TryInject ( injector , target , injectableSolution . Value , user , false ) ;
2024-11-12 06:06:43 -06:00
if ( isOpenOrIgnored & & SolutionContainers . TryGetRefillableSolution ( target , out var refillableSolution , out _ ) )
2024-05-30 00:18:07 -04:00
return TryInject ( injector , target , refillableSolution . Value , user , true ) ;
if ( TryComp < BloodstreamComponent > ( target , out var bloodstream ) )
return TryInjectIntoBloodstream ( injector , ( target , bloodstream ) , user ) ;
Popup . PopupEntity ( Loc . GetString ( "injector-component-cannot-transfer-message" ,
( "target" , Identity . Entity ( target , EntityManager ) ) ) , injector , user ) ;
return false ;
2024-02-15 00:05:01 +01:00
}
2024-05-30 00:18:07 -04:00
if ( injector . Comp . ToggleState = = InjectorToggleMode . Draw )
2024-02-15 00:05:01 +01:00
{
// Draw from a bloodstream, if the target has that
if ( TryComp < BloodstreamComponent > ( target , out var stream ) & &
SolutionContainers . ResolveSolution ( target , stream . BloodSolutionName , ref stream . BloodSolution ) )
{
2024-05-30 00:18:07 -04:00
return TryDraw ( injector , ( target , stream ) , stream . BloodSolution . Value , user ) ;
2024-02-15 00:05:01 +01:00
}
// Draw from an object (food, beaker, etc)
2024-11-12 06:06:43 -06:00
if ( isOpenOrIgnored & & SolutionContainers . TryGetDrawableSolution ( target , out var drawableSolution , out _ ) )
2024-05-30 00:18:07 -04:00
return TryDraw ( injector , target , drawableSolution . Value , user ) ;
Popup . PopupEntity ( Loc . GetString ( "injector-component-cannot-draw-message" ,
( "target" , Identity . Entity ( target , EntityManager ) ) ) , injector . Owner , user ) ;
return false ;
2024-02-15 00:05:01 +01:00
}
2024-05-30 00:18:07 -04:00
return false ;
2024-02-15 00:05:01 +01:00
}
private void OnInjectDoAfter ( Entity < InjectorComponent > entity , ref InjectorDoAfterEvent args )
{
if ( args . Cancelled | | args . Handled | | args . Args . Target = = null )
return ;
2024-05-30 00:18:07 -04:00
args . Handled = TryUseInjector ( entity , args . Args . Target . Value , args . Args . User ) ;
2024-02-15 00:05:01 +01:00
}
private void OnInjectorAfterInteract ( Entity < InjectorComponent > entity , ref AfterInteractEvent args )
{
if ( args . Handled | | ! args . CanReach )
return ;
//Make sure we have the attacking entity
if ( args . Target is not { Valid : true } target | | ! HasComp < SolutionContainerManagerComponent > ( entity ) )
return ;
2024-09-18 15:56:50 +03:00
//CP14 - Shitcode retarget plant -> soil
//TODO: fix it
if ( TryComp < CP14PlantComponent > ( args . Target , out var plant ) & & plant . SoilUid is not null )
target = plant . SoilUid . Value ;
//CP14 - end shitcode
2024-02-15 00:05:01 +01:00
// Is the target a mob? If yes, use a do-after to give them time to respond.
if ( HasComp < MobStateComponent > ( target ) | | HasComp < BloodstreamComponent > ( target ) )
{
// Are use using an injector capible of targeting a mob?
if ( entity . Comp . IgnoreMobs )
return ;
InjectDoAfter ( entity , target , args . User ) ;
args . Handled = true ;
return ;
}
2024-05-30 00:18:07 -04:00
args . Handled = TryUseInjector ( entity , target , args . User ) ;
2024-02-15 00:05:01 +01:00
}
/// <summary>
/// Send informative pop-up messages and wait for a do-after to complete.
/// </summary>
private void InjectDoAfter ( Entity < InjectorComponent > injector , EntityUid target , EntityUid user )
{
// Create a pop-up for the user
2024-03-03 21:07:11 -08:00
if ( injector . Comp . ToggleState = = InjectorToggleMode . Draw )
{
Popup . PopupEntity ( Loc . GetString ( "injector-component-drawing-user" ) , target , user ) ;
}
else
{
Popup . PopupEntity ( Loc . GetString ( "injector-component-injecting-user" ) , target , user ) ;
}
2024-02-15 00:05:01 +01:00
2024-04-30 00:29:15 +03:00
if ( ! SolutionContainers . TryGetSolution ( injector . Owner , injector . Comp . SolutionName , out _ , out var solution ) )
2024-02-15 00:05:01 +01:00
return ;
2024-07-22 13:20:36 +03:00
var actualDelay = injector . Comp . Delay ;
FixedPoint2 amountToInject ;
2024-03-13 02:35:48 -07:00
if ( injector . Comp . ToggleState = = InjectorToggleMode . Draw )
{
// additional delay is based on actual volume left to draw in syringe when smaller than transfer amount
2024-07-22 13:20:36 +03:00
amountToInject = FixedPoint2 . Min ( injector . Comp . TransferAmount , ( solution . MaxVolume - solution . Volume ) ) ;
2024-03-13 02:35:48 -07:00
}
else
{
// additional delay is based on actual volume left to inject in syringe when smaller than transfer amount
2024-07-22 13:20:36 +03:00
amountToInject = FixedPoint2 . Min ( injector . Comp . TransferAmount , solution . Volume ) ;
2024-03-13 02:35:48 -07:00
}
// Injections take 0.5 seconds longer per 5u of possible space/content
2024-07-22 13:20:36 +03:00
// First 5u(MinimumTransferAmount) doesn't incur delay
actualDelay + = injector . Comp . DelayPerVolume * FixedPoint2 . Max ( 0 , amountToInject - injector . Comp . MinimumTransferAmount ) . Double ( ) ;
// Ensure that minimum delay before incapacitation checks is 1 seconds
actualDelay = MathHelper . Max ( actualDelay , TimeSpan . FromSeconds ( 1 ) ) ;
2024-02-15 00:05:01 +01:00
var isTarget = user ! = target ;
if ( isTarget )
{
// Create a pop-up for the target
var userName = Identity . Entity ( user , EntityManager ) ;
2024-03-03 21:07:11 -08:00
if ( injector . Comp . ToggleState = = InjectorToggleMode . Draw )
{
Popup . PopupEntity ( Loc . GetString ( "injector-component-drawing-target" ,
( "user" , userName ) ) , user , target ) ;
}
else
{
Popup . PopupEntity ( Loc . GetString ( "injector-component-injecting-target" ,
( "user" , userName ) ) , user , target ) ;
}
2024-02-15 00:05:01 +01:00
// Check if the target is incapacitated or in combat mode and modify time accordingly.
if ( MobState . IsIncapacitated ( target ) )
{
actualDelay / = 2.5f ;
}
else if ( Combat . IsInCombatMode ( target ) )
{
// Slightly increase the delay when the target is in combat mode. Helps prevents cheese injections in
// combat with fast syringes & lag.
actualDelay + = TimeSpan . FromSeconds ( 1 ) ;
}
// Add an admin log, using the "force feed" log type. It's not quite feeding, but the effect is the same.
if ( injector . Comp . ToggleState = = InjectorToggleMode . Inject )
{
AdminLogger . Add ( LogType . ForceFeed ,
$"{EntityManager.ToPrettyString(user):user} is attempting to inject {EntityManager.ToPrettyString(target):target} with a solution {SharedSolutionContainerSystem.ToPrettyString(solution):solution}" ) ;
}
else
{
AdminLogger . Add ( LogType . ForceFeed ,
$"{EntityManager.ToPrettyString(user):user} is attempting to draw {injector.Comp.TransferAmount.ToString()} units from {EntityManager.ToPrettyString(target):target}" ) ;
}
}
else
{
// Self-injections take half as long.
actualDelay / = 2 ;
if ( injector . Comp . ToggleState = = InjectorToggleMode . Inject )
{
AdminLogger . Add ( LogType . Ingestion ,
$"{EntityManager.ToPrettyString(user):user} is attempting to inject themselves with a solution {SharedSolutionContainerSystem.ToPrettyString(solution):solution}." ) ;
}
else
{
AdminLogger . Add ( LogType . ForceFeed ,
$"{EntityManager.ToPrettyString(user):user} is attempting to draw {injector.Comp.TransferAmount.ToString()} units from themselves." ) ;
}
}
DoAfter . TryStartDoAfter ( new DoAfterArgs ( EntityManager , user , actualDelay , new InjectorDoAfterEvent ( ) , injector . Owner , target : target , used : injector . Owner )
{
2024-03-19 12:09:00 +02:00
BreakOnMove = true ,
BreakOnWeightlessMove = false ,
2024-02-15 00:05:01 +01:00
BreakOnDamage = true ,
2024-07-24 22:26:52 +03:00
NeedHand = injector . Comp . NeedHand ,
BreakOnHandChange = injector . Comp . BreakOnHandChange ,
MovementThreshold = injector . Comp . MovementThreshold ,
2024-02-15 00:05:01 +01:00
} ) ;
}
2024-05-30 00:18:07 -04:00
private bool TryInjectIntoBloodstream ( Entity < InjectorComponent > injector , Entity < BloodstreamComponent > target ,
2024-02-15 00:05:01 +01:00
EntityUid user )
{
// Get transfer amount. May be smaller than _transferAmount if not enough room
if ( ! SolutionContainers . ResolveSolution ( target . Owner , target . Comp . ChemicalSolutionName ,
ref target . Comp . ChemicalSolution , out var chemSolution ) )
{
Popup . PopupEntity (
Loc . GetString ( "injector-component-cannot-inject-message" ,
( "target" , Identity . Entity ( target , EntityManager ) ) ) , injector . Owner , user ) ;
2024-05-30 00:18:07 -04:00
return false ;
2024-02-15 00:05:01 +01:00
}
var realTransferAmount = FixedPoint2 . Min ( injector . Comp . TransferAmount , chemSolution . AvailableVolume ) ;
if ( realTransferAmount < = 0 )
{
Popup . PopupEntity (
Loc . GetString ( "injector-component-cannot-inject-message" ,
( "target" , Identity . Entity ( target , EntityManager ) ) ) , injector . Owner , user ) ;
2024-05-30 00:18:07 -04:00
return false ;
2024-02-15 00:05:01 +01:00
}
// Move units from attackSolution to targetSolution
var removedSolution = SolutionContainers . SplitSolution ( target . Comp . ChemicalSolution . Value , realTransferAmount ) ;
_blood . TryAddToChemicals ( target , removedSolution , target . Comp ) ;
_reactiveSystem . DoEntityReaction ( target , removedSolution , ReactionMethod . Injection ) ;
Popup . PopupEntity ( Loc . GetString ( "injector-component-inject-success-message" ,
( "amount" , removedSolution . Volume ) ,
( "target" , Identity . Entity ( target , EntityManager ) ) ) , injector . Owner , user ) ;
Dirty ( injector ) ;
AfterInject ( injector , target ) ;
2024-05-30 00:18:07 -04:00
return true ;
2024-02-15 00:05:01 +01:00
}
2024-05-30 00:18:07 -04:00
private bool TryInject ( Entity < InjectorComponent > injector , EntityUid targetEntity ,
2024-02-15 00:05:01 +01:00
Entity < SolutionComponent > targetSolution , EntityUid user , bool asRefill )
{
2024-04-30 00:29:15 +03:00
if ( ! SolutionContainers . TryGetSolution ( injector . Owner , injector . Comp . SolutionName , out var soln ,
2024-02-15 00:05:01 +01:00
out var solution ) | | solution . Volume = = 0 )
2024-05-30 00:18:07 -04:00
return false ;
2024-02-15 00:05:01 +01:00
// Get transfer amount. May be smaller than _transferAmount if not enough room
var realTransferAmount =
FixedPoint2 . Min ( injector . Comp . TransferAmount , targetSolution . Comp . Solution . AvailableVolume ) ;
if ( realTransferAmount < = 0 )
{
Popup . PopupEntity (
Loc . GetString ( "injector-component-target-already-full-message" ,
( "target" , Identity . Entity ( targetEntity , EntityManager ) ) ) ,
injector . Owner , user ) ;
2024-05-30 00:18:07 -04:00
return false ;
2024-02-15 00:05:01 +01:00
}
// Move units from attackSolution to targetSolution
Solution removedSolution ;
if ( TryComp < StackComponent > ( targetEntity , out var stack ) )
removedSolution = SolutionContainers . SplitStackSolution ( soln . Value , realTransferAmount , stack . Count ) ;
else
removedSolution = SolutionContainers . SplitSolution ( soln . Value , realTransferAmount ) ;
_reactiveSystem . DoEntityReaction ( targetEntity , removedSolution , ReactionMethod . Injection ) ;
if ( ! asRefill )
SolutionContainers . Inject ( targetEntity , targetSolution , removedSolution ) ;
else
SolutionContainers . Refill ( targetEntity , targetSolution , removedSolution ) ;
Popup . PopupEntity ( Loc . GetString ( "injector-component-transfer-success-message" ,
( "amount" , removedSolution . Volume ) ,
( "target" , Identity . Entity ( targetEntity , EntityManager ) ) ) , injector . Owner , user ) ;
Dirty ( injector ) ;
AfterInject ( injector , targetEntity ) ;
2024-05-30 00:18:07 -04:00
return true ;
2024-02-15 00:05:01 +01:00
}
private void AfterInject ( Entity < InjectorComponent > injector , EntityUid target )
{
// Automatically set syringe to draw after completely draining it.
2024-04-30 00:29:15 +03:00
if ( SolutionContainers . TryGetSolution ( injector . Owner , injector . Comp . SolutionName , out _ ,
2024-02-15 00:05:01 +01:00
out var solution ) & & solution . Volume = = 0 )
{
SetMode ( injector , InjectorToggleMode . Draw ) ;
}
// Leave some DNA from the injectee on it
var ev = new TransferDnaEvent { Donor = target , Recipient = injector } ;
RaiseLocalEvent ( target , ref ev ) ;
}
private void AfterDraw ( Entity < InjectorComponent > injector , EntityUid target )
{
// Automatically set syringe to inject after completely filling it.
2024-04-30 00:29:15 +03:00
if ( SolutionContainers . TryGetSolution ( injector . Owner , injector . Comp . SolutionName , out _ ,
2024-02-15 00:05:01 +01:00
out var solution ) & & solution . AvailableVolume = = 0 )
{
SetMode ( injector , InjectorToggleMode . Inject ) ;
}
// Leave some DNA from the drawee on it
var ev = new TransferDnaEvent { Donor = target , Recipient = injector } ;
RaiseLocalEvent ( target , ref ev ) ;
}
2024-05-30 00:18:07 -04:00
private bool TryDraw ( Entity < InjectorComponent > injector , Entity < BloodstreamComponent ? > target ,
2024-02-15 00:05:01 +01:00
Entity < SolutionComponent > targetSolution , EntityUid user )
{
2024-04-30 00:29:15 +03:00
if ( ! SolutionContainers . TryGetSolution ( injector . Owner , injector . Comp . SolutionName , out var soln ,
2024-02-15 00:05:01 +01:00
out var solution ) | | solution . AvailableVolume = = 0 )
{
2024-05-30 00:18:07 -04:00
return false ;
2024-02-15 00:05:01 +01:00
}
2024-09-12 06:29:11 -04:00
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 )
{
string [ ] reagentPrototypeWhitelistArray = new string [ reagentWhitelist . Count ] ;
var i = 0 ;
foreach ( var reagent in reagentWhitelist )
{
reagentPrototypeWhitelistArray [ i ] = reagent ;
+ + i ;
}
temporarilyRemovedSolution = applicableTargetSolution . SplitSolutionWithout ( applicableTargetSolution . Volume , reagentPrototypeWhitelistArray ) ;
}
2024-02-15 00:05:01 +01:00
// Get transfer amount. May be smaller than _transferAmount if not enough room, also make sure there's room in the injector
2024-09-12 06:29:11 -04:00
var realTransferAmount = FixedPoint2 . Min ( injector . Comp . TransferAmount , applicableTargetSolution . Volume ,
2024-02-15 00:05:01 +01:00
solution . AvailableVolume ) ;
if ( realTransferAmount < = 0 )
{
Popup . PopupEntity (
Loc . GetString ( "injector-component-target-is-empty-message" ,
( "target" , Identity . Entity ( target , EntityManager ) ) ) ,
injector . Owner , user ) ;
2024-05-30 00:18:07 -04:00
return false ;
2024-02-15 00:05:01 +01:00
}
// We have some snowflaked behavior for streams.
if ( target . Comp ! = null )
{
DrawFromBlood ( injector , ( target . Owner , target . Comp ) , soln . Value , realTransferAmount , user ) ;
2024-05-30 00:18:07 -04:00
return true ;
2024-02-15 00:05:01 +01:00
}
// Move units from attackSolution to targetSolution
var removedSolution = SolutionContainers . Draw ( target . Owner , targetSolution , realTransferAmount ) ;
2024-09-12 06:29:11 -04:00
// Add back non-whitelisted reagents to the target solution
applicableTargetSolution . AddSolution ( temporarilyRemovedSolution , null ) ;
2024-02-15 00:05:01 +01:00
if ( ! SolutionContainers . TryAddSolution ( soln . Value , removedSolution ) )
{
2024-05-30 00:18:07 -04:00
return false ;
2024-02-15 00:05:01 +01:00
}
Popup . PopupEntity ( Loc . GetString ( "injector-component-draw-success-message" ,
( "amount" , removedSolution . Volume ) ,
( "target" , Identity . Entity ( target , EntityManager ) ) ) , injector . Owner , user ) ;
Dirty ( injector ) ;
AfterDraw ( injector , target ) ;
2024-05-30 00:18:07 -04:00
return true ;
2024-02-15 00:05:01 +01:00
}
private void DrawFromBlood ( Entity < InjectorComponent > injector , Entity < BloodstreamComponent > target ,
Entity < SolutionComponent > injectorSolution , FixedPoint2 transferAmount , EntityUid user )
{
var drawAmount = ( float ) transferAmount ;
if ( SolutionContainers . ResolveSolution ( target . Owner , target . Comp . ChemicalSolutionName ,
ref target . Comp . ChemicalSolution ) )
{
var chemTemp = SolutionContainers . SplitSolution ( target . Comp . ChemicalSolution . Value , drawAmount * 0.15f ) ;
SolutionContainers . TryAddSolution ( injectorSolution , chemTemp ) ;
drawAmount - = ( float ) chemTemp . Volume ;
}
if ( SolutionContainers . ResolveSolution ( target . Owner , target . Comp . BloodSolutionName ,
ref target . Comp . BloodSolution ) )
{
var bloodTemp = SolutionContainers . SplitSolution ( target . Comp . BloodSolution . Value , drawAmount ) ;
SolutionContainers . TryAddSolution ( injectorSolution , bloodTemp ) ;
}
Popup . PopupEntity ( Loc . GetString ( "injector-component-draw-success-message" ,
( "amount" , transferAmount ) ,
( "target" , Identity . Entity ( target , EntityManager ) ) ) , injector . Owner , user ) ;
Dirty ( injector ) ;
AfterDraw ( injector , target ) ;
}
}