2025-06-17 03:08:08 +03:00
using Content.Shared.Administration.Logs ;
2025-07-03 01:20:31 +02:00
using Content.Shared.Body.Components ;
using Content.Shared.Chemistry.EntitySystems ;
2025-06-17 03:08:08 +03:00
using Content.Shared.Chemistry.Components ;
2025-07-03 01:20:31 +02:00
using Content.Shared.Chemistry.Components.SolutionManager ;
2024-11-15 15:46:01 -08:00
using Content.Shared.Chemistry.Hypospray.Events ;
2024-03-29 20:59:16 -07:00
using Content.Shared.Database ;
using Content.Shared.FixedPoint ;
using Content.Shared.Forensics ;
using Content.Shared.IdentityManagement ;
using Content.Shared.Interaction.Events ;
2025-06-17 03:08:08 +03:00
using Content.Shared.Interaction ;
2024-03-29 20:59:16 -07:00
using Content.Shared.Mobs.Components ;
2025-06-17 03:08:08 +03:00
using Content.Shared.Popups ;
2024-03-29 20:59:16 -07:00
using Content.Shared.Timing ;
2025-06-17 03:08:08 +03:00
using Content.Shared.Verbs ;
2024-03-29 20:59:16 -07:00
using Content.Shared.Weapons.Melee.Events ;
2025-06-17 03:08:08 +03:00
using Robust.Shared.Audio.Systems ;
2024-03-29 20:59:16 -07:00
2025-06-17 03:08:08 +03:00
namespace Content.Shared.Chemistry.EntitySystems ;
2024-03-29 20:59:16 -07:00
2025-06-17 03:08:08 +03:00
public sealed class HypospraySystem : EntitySystem
2024-03-29 20:59:16 -07:00
{
2025-06-17 03:08:08 +03:00
[Dependency] private readonly ISharedAdminLogManager _adminLogger = default ! ;
[Dependency] private readonly ReactiveSystem _reactiveSystem = default ! ;
[Dependency] private readonly SharedAudioSystem _audio = default ! ;
[Dependency] private readonly SharedPopupSystem _popup = default ! ;
[Dependency] private readonly SharedSolutionContainerSystem _solutionContainers = default ! ;
[Dependency] private readonly UseDelaySystem _useDelay = default ! ;
2024-03-29 20:59:16 -07:00
public override void Initialize ( )
{
base . Initialize ( ) ;
SubscribeLocalEvent < HyposprayComponent , AfterInteractEvent > ( OnAfterInteract ) ;
SubscribeLocalEvent < HyposprayComponent , MeleeHitEvent > ( OnAttack ) ;
SubscribeLocalEvent < HyposprayComponent , UseInHandEvent > ( OnUseInHand ) ;
2025-06-17 03:08:08 +03:00
SubscribeLocalEvent < HyposprayComponent , GetVerbsEvent < AlternativeVerb > > ( AddToggleModeVerb ) ;
2024-03-29 20:59:16 -07:00
}
2025-06-17 03:08:08 +03:00
#region Ref events
2024-03-29 20:59:16 -07:00
private void OnUseInHand ( Entity < HyposprayComponent > entity , ref UseInHandEvent args )
{
if ( args . Handled )
return ;
2024-05-30 00:18:07 -04:00
args . Handled = TryDoInject ( entity , args . User , args . User ) ;
2024-03-29 20:59:16 -07:00
}
2025-06-17 03:08:08 +03:00
private void OnAfterInteract ( Entity < HyposprayComponent > entity , ref AfterInteractEvent args )
2024-03-29 20:59:16 -07:00
{
if ( args . Handled | | ! args . CanReach | | args . Target = = null )
return ;
2024-05-30 00:18:07 -04:00
args . Handled = TryUseHypospray ( entity , args . Target . Value , args . User ) ;
2024-03-29 20:59:16 -07:00
}
2025-06-17 03:08:08 +03:00
private void OnAttack ( Entity < HyposprayComponent > entity , ref MeleeHitEvent args )
2024-03-29 20:59:16 -07:00
{
2025-06-17 03:08:08 +03:00
if ( args . HitEntities is [ ] )
2024-03-29 20:59:16 -07:00
return ;
2025-06-17 03:08:08 +03:00
TryDoInject ( entity , args . HitEntities [ 0 ] , args . User ) ;
}
#endregion
#region Draw / Inject
private bool TryUseHypospray ( Entity < HyposprayComponent > entity , EntityUid target , EntityUid user )
{
// if target is ineligible but is a container, try to draw from the container if allowed
if ( entity . Comp . CanContainerDraw
& & ! EligibleEntity ( target , entity )
& & _solutionContainers . TryGetDrawableSolution ( target , out var drawableSolution , out _ ) )
{
return TryDraw ( entity , target , drawableSolution . Value , user ) ;
}
return TryDoInject ( entity , target , user ) ;
2024-03-29 20:59:16 -07:00
}
public bool TryDoInject ( Entity < HyposprayComponent > entity , EntityUid target , EntityUid user )
{
var ( uid , component ) = entity ;
2025-06-17 03:08:08 +03:00
if ( ! EligibleEntity ( target , component ) )
2024-03-29 20:59:16 -07:00
return false ;
if ( TryComp ( uid , out UseDelayComponent ? delayComp ) )
{
if ( _useDelay . IsDelayed ( ( uid , delayComp ) ) )
return false ;
}
string? msgFormat = null ;
2024-11-15 15:46:01 -08:00
// Self event
var selfEvent = new SelfBeforeHyposprayInjectsEvent ( user , entity . Owner , target ) ;
RaiseLocalEvent ( user , selfEvent ) ;
if ( selfEvent . Cancelled )
{
2025-06-17 03:08:08 +03:00
_popup . PopupClient ( Loc . GetString ( selfEvent . InjectMessageOverride ? ? "hypospray-cant-inject" , ( "owner" , Identity . Entity ( target , EntityManager ) ) ) , target , user ) ;
2024-11-15 15:46:01 -08:00
return false ;
}
target = selfEvent . TargetGettingInjected ;
2025-06-17 03:08:08 +03:00
if ( ! EligibleEntity ( target , component ) )
2024-11-15 15:46:01 -08:00
return false ;
// Target event
var targetEvent = new TargetBeforeHyposprayInjectsEvent ( user , entity . Owner , target ) ;
RaiseLocalEvent ( target , targetEvent ) ;
if ( targetEvent . Cancelled )
2024-03-29 20:59:16 -07:00
{
2025-06-17 03:08:08 +03:00
_popup . PopupClient ( Loc . GetString ( targetEvent . InjectMessageOverride ? ? "hypospray-cant-inject" , ( "owner" , Identity . Entity ( target , EntityManager ) ) ) , target , user ) ;
2024-11-15 15:46:01 -08:00
return false ;
2024-03-29 20:59:16 -07:00
}
2024-11-15 15:46:01 -08:00
target = targetEvent . TargetGettingInjected ;
2025-06-17 03:08:08 +03:00
if ( ! EligibleEntity ( target , component ) )
2024-11-15 15:46:01 -08:00
return false ;
// The target event gets priority for the overriden message.
if ( targetEvent . InjectMessageOverride ! = null )
msgFormat = targetEvent . InjectMessageOverride ;
else if ( selfEvent . InjectMessageOverride ! = null )
msgFormat = selfEvent . InjectMessageOverride ;
else if ( target = = user )
msgFormat = "hypospray-component-inject-self-message" ;
2024-03-29 20:59:16 -07:00
if ( ! _solutionContainers . TryGetSolution ( uid , component . SolutionName , out var hypoSpraySoln , out var hypoSpraySolution ) | | hypoSpraySolution . Volume = = 0 )
{
2025-06-17 03:08:08 +03:00
_popup . PopupClient ( Loc . GetString ( "hypospray-component-empty-message" ) , target , user ) ;
2024-03-29 20:59:16 -07:00
return true ;
}
if ( ! _solutionContainers . TryGetInjectableSolution ( target , out var targetSoln , out var targetSolution ) )
{
2025-06-17 03:08:08 +03:00
_popup . PopupClient ( Loc . GetString ( "hypospray-cant-inject" , ( "target" , Identity . Entity ( target , EntityManager ) ) ) , target , user ) ;
2024-03-29 20:59:16 -07:00
return false ;
}
2025-08-18 22:42:40 +02:00
_popup . PopupClient ( Loc . GetString ( msgFormat ? ? "hypospray-component-inject-other-message" , ( "other" , Identity . Entity ( target , EntityManager ) ) ) , target , user ) ;
2024-03-29 20:59:16 -07:00
if ( target ! = user )
{
_popup . PopupEntity ( Loc . GetString ( "hypospray-component-feel-prick-message" ) , target , target ) ;
// TODO: This should just be using melee attacks...
// meleeSys.SendLunge(angle, user);
}
2025-06-17 03:08:08 +03:00
_audio . PlayPredicted ( component . InjectSound , target , user ) ;
2024-03-29 20:59:16 -07:00
// Medipens and such use this system and don't have a delay, requiring extra checks
// BeginDelay function returns if item is already on delay
if ( delayComp ! = null )
_useDelay . TryResetDelay ( ( uid , delayComp ) ) ;
// Get transfer amount. May be smaller than component.TransferAmount if not enough room
var realTransferAmount = FixedPoint2 . Min ( component . TransferAmount , targetSolution . AvailableVolume ) ;
if ( realTransferAmount < = 0 )
{
2025-06-17 03:08:08 +03:00
_popup . PopupClient ( Loc . GetString ( "hypospray-component-transfer-already-full-message" , ( "owner" , target ) ) , target , user ) ;
2024-03-29 20:59:16 -07:00
return true ;
}
// Move units from attackSolution to targetSolution
var removedSolution = _solutionContainers . SplitSolution ( hypoSpraySoln . Value , realTransferAmount ) ;
if ( ! targetSolution . CanAddSolution ( removedSolution ) )
return true ;
_reactiveSystem . DoEntityReaction ( target , removedSolution , ReactionMethod . Injection ) ;
_solutionContainers . TryAddSolution ( targetSoln . Value , removedSolution ) ;
var ev = new TransferDnaEvent { Donor = target , Recipient = uid } ;
RaiseLocalEvent ( target , ref ev ) ;
// same LogType as syringes...
2025-06-26 19:50:49 -04:00
_adminLogger . Add ( LogType . ForceFeed , $"{ToPrettyString(user):user} injected {ToPrettyString(target):target} with a solution {SharedSolutionContainerSystem.ToPrettyString(removedSolution):removedSolution} using a {ToPrettyString(uid):using}" ) ;
2024-03-29 20:59:16 -07:00
return true ;
}
2025-06-17 03:08:08 +03:00
private bool TryDraw ( Entity < HyposprayComponent > entity , EntityUid target , Entity < SolutionComponent > targetSolution , EntityUid user )
2024-03-29 20:59:16 -07:00
{
if ( ! _solutionContainers . TryGetSolution ( entity . Owner , entity . Comp . SolutionName , out var soln ,
out var solution ) | | solution . AvailableVolume = = 0 )
{
2024-05-30 00:18:07 -04:00
return false ;
2024-03-29 20:59:16 -07:00
}
// Get transfer amount. May be smaller than _transferAmount if not enough room, also make sure there's room in the injector
var realTransferAmount = FixedPoint2 . Min ( entity . Comp . TransferAmount , targetSolution . Comp . Solution . Volume ,
solution . AvailableVolume ) ;
if ( realTransferAmount < = 0 )
{
2025-06-17 03:08:08 +03:00
_popup . PopupClient (
2024-03-29 20:59:16 -07:00
Loc . GetString ( "injector-component-target-is-empty-message" ,
( "target" , Identity . Entity ( target , EntityManager ) ) ) ,
entity . Owner , user ) ;
2024-05-30 00:18:07 -04:00
return false ;
2024-03-29 20:59:16 -07:00
}
2025-06-17 03:08:08 +03:00
var removedSolution = _solutionContainers . Draw ( target , targetSolution , realTransferAmount ) ;
2024-03-29 20:59:16 -07:00
if ( ! _solutionContainers . TryAddSolution ( soln . Value , removedSolution ) )
{
2024-05-30 00:18:07 -04:00
return false ;
2024-03-29 20:59:16 -07:00
}
2025-06-17 03:08:08 +03:00
_popup . PopupClient ( Loc . GetString ( "injector-component-draw-success-message" ,
2024-03-29 20:59:16 -07:00
( "amount" , removedSolution . Volume ) ,
( "target" , Identity . Entity ( target , EntityManager ) ) ) , entity . Owner , user ) ;
2024-05-30 00:18:07 -04:00
return true ;
2024-03-29 20:59:16 -07:00
}
2025-06-17 03:08:08 +03:00
private bool EligibleEntity ( EntityUid entity , HyposprayComponent component )
2024-03-29 20:59:16 -07:00
{
// TODO: Does checking for BodyComponent make sense as a "can be hypospray'd" tag?
// In SS13 the hypospray ONLY works on mobs, NOT beakers or anything else.
// But this is 14, we dont do what SS13 does just because SS13 does it.
return component . OnlyAffectsMobs
2025-06-17 03:08:08 +03:00
? HasComp < SolutionContainerManagerComponent > ( entity ) & &
HasComp < MobStateComponent > ( entity )
: HasComp < SolutionContainerManagerComponent > ( entity ) ;
2024-03-29 20:59:16 -07:00
}
2025-06-17 03:08:08 +03:00
#endregion
#region Verbs
// <summary>
// Uses the OnlyMobs field as a check to implement the ability
// to draw from jugs and containers with the hypospray
// Toggleable to allow people to inject containers if they prefer it over drawing
// </summary>
private void AddToggleModeVerb ( Entity < HyposprayComponent > entity , ref GetVerbsEvent < AlternativeVerb > args )
{
if ( ! args . CanAccess | | ! args . CanInteract | | args . Hands = = null | | entity . Comp . InjectOnly )
return ;
var user = args . User ;
var verb = new AlternativeVerb
{
Text = Loc . GetString ( "hypospray-verb-mode-label" ) ,
Act = ( ) = >
{
ToggleMode ( entity , user ) ;
}
} ;
args . Verbs . Add ( verb ) ;
}
private void ToggleMode ( Entity < HyposprayComponent > entity , EntityUid user )
{
SetMode ( entity , ! entity . Comp . OnlyAffectsMobs ) ;
var msg = ( entity . Comp . OnlyAffectsMobs & & entity . Comp . CanContainerDraw ) ? "hypospray-verb-mode-inject-mobs-only" : "hypospray-verb-mode-inject-all" ;
_popup . PopupClient ( Loc . GetString ( msg ) , entity , user ) ;
}
public void SetMode ( Entity < HyposprayComponent > entity , bool onlyAffectsMobs )
{
if ( entity . Comp . OnlyAffectsMobs = = onlyAffectsMobs )
return ;
entity . Comp . OnlyAffectsMobs = onlyAffectsMobs ;
Dirty ( entity ) ;
}
#endregion
2024-03-29 20:59:16 -07:00
}