2023-12-20 18:56:57 +01:00
using System.Linq ;
2022-09-18 11:10:10 +12:00
using Content.Server.Atmos ;
2022-09-08 14:22:14 +00:00
using Content.Server.Atmos.Components ;
using Content.Server.NodeContainer ;
using Content.Server.NodeContainer.Nodes ;
2022-07-14 04:45:31 -07:00
using Content.Server.Popups ;
2022-09-08 14:22:14 +00:00
using Content.Shared.Atmos ;
using Content.Shared.Atmos.Components ;
2022-07-14 04:45:31 -07:00
using Content.Shared.Interaction ;
2022-09-08 14:22:14 +00:00
using Content.Shared.Interaction.Events ;
2020-12-08 11:56:10 +01:00
using JetBrains.Annotations ;
2022-07-14 04:45:31 -07:00
using Robust.Server.GameObjects ;
2023-10-29 04:21:02 +11:00
using Robust.Shared.Player ;
2023-07-12 15:47:45 -04:00
using static Content . Shared . Atmos . Components . GasAnalyzerComponent ;
2020-08-08 18:24:41 +02:00
2021-06-19 13:25:05 +02:00
namespace Content.Server.Atmos.EntitySystems
2020-08-08 18:24:41 +02:00
{
2020-12-08 11:56:10 +01:00
[UsedImplicitly]
2022-02-16 00:23:23 -07:00
public sealed class GasAnalyzerSystem : EntitySystem
2020-08-08 18:24:41 +02:00
{
2022-07-14 04:45:31 -07:00
[Dependency] private readonly PopupSystem _popup = default ! ;
2022-09-08 14:22:14 +00:00
[Dependency] private readonly AtmosphereSystem _atmo = default ! ;
[Dependency] private readonly SharedAppearanceSystem _appearance = default ! ;
[Dependency] private readonly UserInterfaceSystem _userInterface = default ! ;
2023-07-12 15:47:45 -04:00
[Dependency] private readonly TransformSystem _transform = default ! ;
2022-07-14 04:45:31 -07:00
2023-12-20 18:56:57 +01:00
/// <summary>
/// Minimum moles of a gas to be sent to the client.
/// </summary>
private const float UIMinMoles = 0.01f ;
2022-07-14 04:45:31 -07:00
public override void Initialize ( )
{
base . Initialize ( ) ;
SubscribeLocalEvent < GasAnalyzerComponent , AfterInteractEvent > ( OnAfterInteract ) ;
2022-09-08 14:22:14 +00:00
SubscribeLocalEvent < GasAnalyzerComponent , GasAnalyzerDisableMessage > ( OnDisabledMessage ) ;
SubscribeLocalEvent < GasAnalyzerComponent , DroppedEvent > ( OnDropped ) ;
SubscribeLocalEvent < GasAnalyzerComponent , UseInHandEvent > ( OnUseInHand ) ;
2022-07-14 04:45:31 -07:00
}
2020-08-08 18:24:41 +02:00
public override void Update ( float frameTime )
{
2023-07-12 15:47:45 -04:00
var query = EntityQueryEnumerator < ActiveGasAnalyzerComponent > ( ) ;
while ( query . MoveNext ( out var uid , out var analyzer ) )
2020-08-08 18:24:41 +02:00
{
2022-09-08 14:22:14 +00:00
// Don't update every tick
analyzer . AccumulatedFrametime + = frameTime ;
if ( analyzer . AccumulatedFrametime < analyzer . UpdateInterval )
continue ;
analyzer . AccumulatedFrametime - = analyzer . UpdateInterval ;
2023-07-12 15:47:45 -04:00
if ( ! UpdateAnalyzer ( uid ) )
RemCompDeferred < ActiveGasAnalyzerComponent > ( uid ) ;
2020-08-08 18:24:41 +02:00
}
}
2022-07-14 04:45:31 -07:00
2022-09-08 14:22:14 +00:00
/// <summary>
/// Activates the analyzer when used in the world, scanning either the target entity or the tile clicked
/// </summary>
2022-07-14 04:45:31 -07:00
private void OnAfterInteract ( EntityUid uid , GasAnalyzerComponent component , AfterInteractEvent args )
{
if ( ! args . CanReach )
{
2022-12-19 10:41:47 +13:00
_popup . PopupEntity ( Loc . GetString ( "gas-analyzer-component-player-cannot-reach-message" ) , args . User , args . User ) ;
2022-07-14 04:45:31 -07:00
return ;
}
2022-09-08 14:22:14 +00:00
ActivateAnalyzer ( uid , component , args . User , args . Target ) ;
2023-07-12 15:47:45 -04:00
OpenUserInterface ( uid , args . User , component ) ;
2022-09-08 14:22:14 +00:00
args . Handled = true ;
}
/// <summary>
/// Activates the analyzer with no target, so it only scans the tile the user was on when activated
/// </summary>
private void OnUseInHand ( EntityUid uid , GasAnalyzerComponent component , UseInHandEvent args )
{
ActivateAnalyzer ( uid , component , args . User ) ;
args . Handled = true ;
}
/// <summary>
/// Handles analyzer activation logic
/// </summary>
private void ActivateAnalyzer ( EntityUid uid , GasAnalyzerComponent component , EntityUid user , EntityUid ? target = null )
{
component . Target = target ;
component . User = user ;
2022-09-27 16:55:44 -04:00
if ( target ! = null )
component . LastPosition = Transform ( target . Value ) . Coordinates ;
else
component . LastPosition = null ;
2022-09-08 14:22:14 +00:00
component . Enabled = true ;
Dirty ( component ) ;
2023-07-12 15:47:45 -04:00
UpdateAppearance ( uid , component ) ;
2022-09-08 14:22:14 +00:00
if ( ! HasComp < ActiveGasAnalyzerComponent > ( uid ) )
AddComp < ActiveGasAnalyzerComponent > ( uid ) ;
2022-09-18 11:10:10 +12:00
UpdateAnalyzer ( uid , component ) ;
2022-09-08 14:22:14 +00:00
}
/// <summary>
/// Close the UI, turn the analyzer off, and don't update when it's dropped
/// </summary>
private void OnDropped ( EntityUid uid , GasAnalyzerComponent component , DroppedEvent args )
{
2023-07-12 15:47:45 -04:00
if ( args . User is var userId & & component . Enabled )
2022-12-19 10:41:47 +13:00
_popup . PopupEntity ( Loc . GetString ( "gas-analyzer-shutoff" ) , userId , userId ) ;
2022-09-08 14:22:14 +00:00
DisableAnalyzer ( uid , component , args . User ) ;
}
/// <summary>
/// Closes the UI, sets the icon to off, and removes it from the update list
/// </summary>
private void DisableAnalyzer ( EntityUid uid , GasAnalyzerComponent ? component = null , EntityUid ? user = null )
{
if ( ! Resolve ( uid , ref component ) )
return ;
if ( user ! = null & & TryComp < ActorComponent > ( user , out var actor ) )
_userInterface . TryClose ( uid , GasAnalyzerUiKey . Key , actor . PlayerSession ) ;
component . Enabled = false ;
Dirty ( component ) ;
2023-07-12 15:47:45 -04:00
UpdateAppearance ( uid , component ) ;
2022-09-08 14:22:14 +00:00
RemCompDeferred < ActiveGasAnalyzerComponent > ( uid ) ;
}
/// <summary>
/// Disables the analyzer when the user closes the UI
/// </summary>
private void OnDisabledMessage ( EntityUid uid , GasAnalyzerComponent component , GasAnalyzerDisableMessage message )
{
if ( message . Session . AttachedEntity is not { Valid : true } )
return ;
DisableAnalyzer ( uid , component ) ;
}
2023-07-12 15:47:45 -04:00
private void OpenUserInterface ( EntityUid uid , EntityUid user , GasAnalyzerComponent ? component = null )
2022-09-08 14:22:14 +00:00
{
2023-07-12 15:47:45 -04:00
if ( ! Resolve ( uid , ref component , false ) )
return ;
2022-09-08 14:22:14 +00:00
if ( ! TryComp < ActorComponent > ( user , out var actor ) )
return ;
2023-07-12 15:47:45 -04:00
_userInterface . TryOpen ( uid , GasAnalyzerUiKey . Key , actor . PlayerSession ) ;
2022-09-08 14:22:14 +00:00
}
/// <summary>
/// Fetches fresh data for the analyzer. Should only be called by Update or when the user requests an update via refresh button
/// </summary>
private bool UpdateAnalyzer ( EntityUid uid , GasAnalyzerComponent ? component = null )
{
if ( ! Resolve ( uid , ref component ) )
return false ;
2022-09-18 11:10:10 +12:00
if ( ! TryComp ( component . User , out TransformComponent ? xform ) )
{
DisableAnalyzer ( uid , component ) ;
return false ;
}
2022-09-08 14:22:14 +00:00
// check if the user has walked away from what they scanned
2022-09-18 11:10:10 +12:00
var userPos = xform . Coordinates ;
2022-09-08 14:22:14 +00:00
if ( component . LastPosition . HasValue )
{
// Check if position is out of range => don't update and disable
2023-07-12 15:47:45 -04:00
if ( ! component . LastPosition . Value . InRange ( EntityManager , _transform , userPos , SharedInteractionSystem . InteractionRange ) )
2022-09-08 14:22:14 +00:00
{
if ( component . User is { } userId & & component . Enabled )
2022-12-19 10:41:47 +13:00
_popup . PopupEntity ( Loc . GetString ( "gas-analyzer-shutoff" ) , userId , userId ) ;
2022-09-08 14:22:14 +00:00
DisableAnalyzer ( uid , component , component . User ) ;
return false ;
}
}
var gasMixList = new List < GasMixEntry > ( ) ;
// Fetch the environmental atmosphere around the scanner. This must be the first entry
2023-07-12 15:47:45 -04:00
var tileMixture = _atmo . GetContainingMixture ( uid , true ) ;
2022-09-08 14:22:14 +00:00
if ( tileMixture ! = null )
{
gasMixList . Add ( new GasMixEntry ( Loc . GetString ( "gas-analyzer-window-environment-tab-label" ) , tileMixture . Pressure , tileMixture . Temperature ,
GenerateGasEntryArray ( tileMixture ) ) ) ;
}
else
{
// No gases were found
gasMixList . Add ( new GasMixEntry ( Loc . GetString ( "gas-analyzer-window-environment-tab-label" ) , 0f , 0f ) ) ;
}
2022-07-14 04:45:31 -07:00
2022-09-08 14:22:14 +00:00
var deviceFlipped = false ;
if ( component . Target ! = null )
2022-07-14 04:45:31 -07:00
{
2022-09-18 11:10:10 +12:00
if ( Deleted ( component . Target ) )
{
component . Target = null ;
DisableAnalyzer ( uid , component , component . User ) ;
return false ;
}
2022-09-08 14:22:14 +00:00
// gas analyzed was used on an entity, try to request gas data via event for override
var ev = new GasAnalyzerScanEvent ( ) ;
2023-07-12 15:47:45 -04:00
RaiseLocalEvent ( component . Target . Value , ev ) ;
2022-09-08 14:22:14 +00:00
if ( ev . GasMixtures ! = null )
{
foreach ( var mixes in ev . GasMixtures )
{
if ( mixes . Value ! = null )
gasMixList . Add ( new GasMixEntry ( mixes . Key , mixes . Value . Pressure , mixes . Value . Temperature , GenerateGasEntryArray ( mixes . Value ) ) ) ;
}
deviceFlipped = ev . DeviceFlipped ;
}
else
{
// No override, fetch manually, to handle flippable devices you must subscribe to GasAnalyzerScanEvent
if ( TryComp ( component . Target , out NodeContainerComponent ? node ) )
{
foreach ( var pair in node . Nodes )
{
if ( pair . Value is PipeNode pipeNode )
gasMixList . Add ( new GasMixEntry ( pair . Key , pipeNode . Air . Pressure , pipeNode . Air . Temperature , GenerateGasEntryArray ( pipeNode . Air ) ) ) ;
}
}
}
2022-07-14 04:45:31 -07:00
}
2022-09-08 14:22:14 +00:00
// Don't bother sending a UI message with no content, and stop updating I guess?
if ( gasMixList . Count = = 0 )
return false ;
2023-07-12 15:47:45 -04:00
_userInterface . TrySendUiMessage ( uid , GasAnalyzerUiKey . Key ,
2022-09-08 14:22:14 +00:00
new GasAnalyzerUserMessage ( gasMixList . ToArray ( ) ,
component . Target ! = null ? Name ( component . Target . Value ) : string . Empty ,
2023-09-11 09:42:41 +10:00
GetNetEntity ( component . Target ) ? ? NetEntity . Invalid ,
2022-09-08 14:22:14 +00:00
deviceFlipped ) ) ;
return true ;
}
/// <summary>
/// Sets the appearance based on the analyzers Enabled state
/// </summary>
2023-07-12 15:47:45 -04:00
private void UpdateAppearance ( EntityUid uid , GasAnalyzerComponent analyzer )
2022-09-08 14:22:14 +00:00
{
2023-07-12 15:47:45 -04:00
_appearance . SetData ( uid , GasAnalyzerVisuals . Enabled , analyzer . Enabled ) ;
2022-09-08 14:22:14 +00:00
}
/// <summary>
/// Generates a GasEntry array for a given GasMixture
/// </summary>
private GasEntry [ ] GenerateGasEntryArray ( GasMixture ? mixture )
{
var gases = new List < GasEntry > ( ) ;
for ( var i = 0 ; i < Atmospherics . TotalNumberOfGases ; i + + )
{
var gas = _atmo . GetGas ( i ) ;
2023-12-20 18:56:57 +01:00
if ( mixture ? . Moles [ i ] < = UIMinMoles )
2022-09-08 14:22:14 +00:00
continue ;
if ( mixture ! = null )
2022-12-20 23:25:34 +01:00
{
var gasName = Loc . GetString ( gas . Name ) ;
gases . Add ( new GasEntry ( gasName , mixture . Moles [ i ] , gas . Color ) ) ;
}
2022-09-08 14:22:14 +00:00
}
2023-12-20 18:56:57 +01:00
var gasesOrdered = gases . OrderByDescending ( gas = > gas . Amount ) ;
return gasesOrdered . ToArray ( ) ;
2022-07-14 04:45:31 -07:00
}
2020-08-08 18:24:41 +02:00
}
}
2022-09-08 14:22:14 +00:00
/// <summary>
/// Raised when the analyzer is used. An atmospherics device that does not rely on a NodeContainer or
/// wishes to override the default analyzer behaviour of fetching all nodes in the attached NodeContainer
/// should subscribe to this and return the GasMixtures as desired. A device that is flippable should subscribe
/// to this event to report if it is flipped or not. See GasFilterSystem or GasMixerSystem for an example.
/// </summary>
public sealed class GasAnalyzerScanEvent : EntityEventArgs
{
/// <summary>
/// Key is the mix name (ex "pipe", "inlet", "filter"), value is the pipe direction and GasMixture. Add all mixes that should be reported when scanned.
/// </summary>
public Dictionary < string , GasMixture ? > ? GasMixtures ;
/// <summary>
/// If the device is flipped. Flipped is defined as when the inline input is 90 degrees CW to the side input
/// </summary>
public bool DeviceFlipped ;
}