2021-12-05 18:09:01 +01:00
using System ;
using System.Threading ;
2021-10-24 06:34:00 +03:00
using Content.Server.DoAfter ;
using Content.Server.RCD.Components ;
using Content.Shared.Coordinates ;
using Content.Shared.Examine ;
using Content.Shared.Interaction ;
using Content.Shared.Interaction.Helpers ;
using Content.Shared.Maps ;
using Content.Shared.Popups ;
2021-12-06 00:52:58 +01:00
using Content.Shared.Tag ;
2021-10-24 06:34:00 +03:00
using Robust.Shared.Audio ;
using Robust.Shared.GameObjects ;
using Robust.Shared.IoC ;
using Robust.Shared.Localization ;
using Robust.Shared.Map ;
using Robust.Shared.Maths ;
using Robust.Shared.Player ;
namespace Content.Server.RCD.Systems
{
2022-02-16 00:23:23 -07:00
public sealed class RCDSystem : EntitySystem
2021-10-24 06:34:00 +03:00
{
[Dependency] private readonly ITileDefinitionManager _tileDefinitionManager = default ! ;
[Dependency] private readonly IMapManager _mapManager = default ! ;
[Dependency] private readonly DoAfterSystem _doAfterSystem = default ! ;
2022-02-05 15:39:01 +13:00
[Dependency] private readonly SharedInteractionSystem _interactionSystem = default ! ;
2022-02-08 14:08:11 +11:00
[Dependency] private readonly TagSystem _tagSystem = default ! ;
2021-10-24 06:34:00 +03:00
private readonly RcdMode [ ] _modes = ( RcdMode [ ] ) Enum . GetValues ( typeof ( RcdMode ) ) ;
public override void Initialize ( )
{
base . Initialize ( ) ;
SubscribeLocalEvent < RCDComponent , MapInitEvent > ( OnMapInit ) ;
SubscribeLocalEvent < RCDComponent , ExaminedEvent > ( OnExamine ) ;
SubscribeLocalEvent < RCDComponent , UseInHandEvent > ( OnUseInHand ) ;
SubscribeLocalEvent < RCDComponent , AfterInteractEvent > ( OnAfterInteract ) ;
}
private void OnMapInit ( EntityUid uid , RCDComponent component , MapInitEvent args )
{
component . CurrentAmmo = component . StartingAmmo ;
}
private void OnExamine ( EntityUid uid , RCDComponent component , ExaminedEvent args )
{
var msg = Loc . GetString ( "rcd-component-examine-detail-count" ,
( "mode" , component . Mode ) , ( "ammoCount" , component . CurrentAmmo ) ) ;
args . PushMarkup ( msg ) ;
}
private void OnUseInHand ( EntityUid uid , RCDComponent component , UseInHandEvent args )
{
if ( args . Handled )
return ;
NextMode ( uid , component , args . User ) ;
args . Handled = true ;
}
private async void OnAfterInteract ( EntityUid uid , RCDComponent rcd , AfterInteractEvent args )
{
2022-02-05 15:39:01 +13:00
if ( args . Handled | | ! args . CanReach )
2021-10-24 06:34:00 +03:00
return ;
// FIXME: Make this work properly. Right now it relies on the click location being on a grid, which is bad.
2021-10-25 04:33:49 +01:00
var clickLocationMod = args . ClickLocation ;
// Initial validity check
if ( ! clickLocationMod . IsValid ( EntityManager ) )
2021-10-24 06:34:00 +03:00
return ;
2021-10-25 04:33:49 +01:00
// Try to fix it (i.e. if clicking on space)
// Note: Ideally there'd be a better way, but there isn't right now.
var gridID = clickLocationMod . GetGridId ( EntityManager ) ;
if ( ! gridID . IsValid ( ) )
{
clickLocationMod = clickLocationMod . AlignWithClosestGridTile ( ) ;
gridID = clickLocationMod . GetGridId ( EntityManager ) ;
}
// Check if fixing it failed / get final grid ID
2021-10-24 06:34:00 +03:00
if ( ! gridID . IsValid ( ) )
return ;
var mapGrid = _mapManager . GetGrid ( gridID ) ;
2021-10-25 04:33:49 +01:00
var tile = mapGrid . GetTileRef ( clickLocationMod ) ;
var snapPos = mapGrid . TileIndicesFor ( clickLocationMod ) ;
2021-10-24 06:34:00 +03:00
//No changing mode mid-RCD
var startingMode = rcd . Mode ;
//Using an RCD isn't instantaneous
var cancelToken = new CancellationTokenSource ( ) ;
var doAfterEventArgs = new DoAfterEventArgs ( args . User , rcd . Delay , cancelToken . Token , args . Target )
{
BreakOnDamage = true ,
BreakOnStun = true ,
NeedHand = true ,
ExtraCheck = ( ) = > IsRCDStillValid ( rcd , args , mapGrid , tile , snapPos , startingMode ) //All of the sanity checks are here
} ;
var result = await _doAfterSystem . WaitDoAfter ( doAfterEventArgs ) ;
if ( result = = DoAfterStatus . Cancelled )
{
args . Handled = true ;
return ;
}
switch ( rcd . Mode )
{
//Floor mode just needs the tile to be a space tile (subFloor)
case RcdMode . Floors :
2021-10-25 04:33:49 +01:00
mapGrid . SetTile ( clickLocationMod , new Tile ( _tileDefinitionManager [ "floor_steel" ] . TileId ) ) ;
2021-10-24 06:34:00 +03:00
break ;
//We don't want to place a space tile on something that's already a space tile. Let's do the inverse of the last check.
case RcdMode . Deconstruct :
if ( ! tile . IsBlockedTurf ( true ) ) //Delete the turf
{
mapGrid . SetTile ( snapPos , Tile . Empty ) ;
}
else //Delete what the user targeted
{
2021-12-06 00:52:58 +01:00
if ( args . Target is { Valid : true } target )
2021-12-03 11:43:03 +01:00
{
2021-12-06 00:52:58 +01:00
EntityManager . DeleteEntity ( target ) ;
2021-12-03 11:43:03 +01:00
}
2021-10-24 06:34:00 +03:00
}
break ;
//Walls are a special behaviour, and require us to build a new object with a transform rather than setting a grid tile,
// thus we early return to avoid the tile set code.
case RcdMode . Walls :
var ent = EntityManager . SpawnEntity ( "WallSolid" , mapGrid . GridTileToLocal ( snapPos ) ) ;
2021-12-08 13:00:43 +01:00
EntityManager . GetComponent < TransformComponent > ( ent ) . LocalRotation = Angle . Zero ; // Walls always need to point south.
2021-10-24 06:34:00 +03:00
break ;
case RcdMode . Airlock :
var airlock = EntityManager . SpawnEntity ( "Airlock" , mapGrid . GridTileToLocal ( snapPos ) ) ;
2021-12-08 13:00:43 +01:00
EntityManager . GetComponent < TransformComponent > ( airlock ) . LocalRotation = EntityManager . GetComponent < TransformComponent > ( rcd . Owner ) . LocalRotation ; //Now apply icon smoothing.
2021-10-24 06:34:00 +03:00
break ;
default :
args . Handled = true ;
return ; //I don't know why this would happen, but sure I guess. Get out of here invalid state!
}
SoundSystem . Play ( Filter . Pvs ( uid ) , rcd . SuccessSound . GetSound ( ) , rcd . Owner ) ;
rcd . CurrentAmmo - - ;
args . Handled = true ;
}
private bool IsRCDStillValid ( RCDComponent rcd , AfterInteractEvent eventArgs , IMapGrid mapGrid , TileRef tile , Vector2i snapPos , RcdMode startingMode )
{
//Less expensive checks first. Failing those ones, we need to check that the tile isn't obstructed.
if ( rcd . CurrentAmmo < = 0 )
{
rcd . Owner . PopupMessage ( eventArgs . User , Loc . GetString ( "rcd-component-no-ammo-message" ) ) ;
return false ;
}
if ( rcd . Mode ! = startingMode )
{
return false ;
}
var coordinates = mapGrid . ToCoordinates ( tile . GridIndices ) ;
2022-02-22 18:01:01 +13:00
if ( coordinates = = EntityCoordinates . Invalid )
2021-10-24 06:34:00 +03:00
{
return false ;
}
2022-02-22 18:01:01 +13:00
var unobstructed = eventArgs . Target = = null
? _interactionSystem . InRangeUnobstructed ( eventArgs . User , coordinates , popup : true )
: _interactionSystem . InRangeUnobstructed ( eventArgs . User , eventArgs . Target . Value , popup : true ) ;
if ( ! unobstructed )
return false ;
2021-10-24 06:34:00 +03:00
switch ( rcd . Mode )
{
//Floor mode just needs the tile to be a space tile (subFloor)
case RcdMode . Floors :
if ( ! tile . Tile . IsEmpty )
{
rcd . Owner . PopupMessage ( eventArgs . User , Loc . GetString ( "rcd-component-cannot-build-floor-tile-not-empty-message" ) ) ;
return false ;
}
return true ;
//We don't want to place a space tile on something that's already a space tile. Let's do the inverse of the last check.
case RcdMode . Deconstruct :
if ( tile . Tile . IsEmpty )
{
return false ;
}
//They tried to decon a turf but the turf is blocked
if ( eventArgs . Target = = null & & tile . IsBlockedTurf ( true ) )
{
rcd . Owner . PopupMessage ( eventArgs . User , Loc . GetString ( "rcd-component-tile-obstructed-message" ) ) ;
return false ;
}
//They tried to decon a non-turf but it's not in the whitelist
2022-02-08 14:08:11 +11:00
if ( eventArgs . Target ! = null & & ! _tagSystem . HasTag ( eventArgs . Target . Value , "RCDDeconstructWhitelist" ) )
2021-10-24 06:34:00 +03:00
{
rcd . Owner . PopupMessage ( eventArgs . User , Loc . GetString ( "rcd-component-deconstruct-target-not-on-whitelist-message" ) ) ;
return false ;
}
return true ;
//Walls are a special behaviour, and require us to build a new object with a transform rather than setting a grid tile, thus we early return to avoid the tile set code.
case RcdMode . Walls :
if ( tile . Tile . IsEmpty )
{
rcd . Owner . PopupMessage ( eventArgs . User , Loc . GetString ( "rcd-component-cannot-build-wall-tile-not-empty-message" ) ) ;
return false ;
}
if ( tile . IsBlockedTurf ( true ) )
{
rcd . Owner . PopupMessage ( eventArgs . User , Loc . GetString ( "rcd-component-tile-obstructed-message" ) ) ;
return false ;
}
return true ;
case RcdMode . Airlock :
if ( tile . Tile . IsEmpty )
{
rcd . Owner . PopupMessage ( eventArgs . User , Loc . GetString ( "rcd-component-cannot-build-airlock-tile-not-empty-message" ) ) ;
return false ;
}
if ( tile . IsBlockedTurf ( true ) )
{
rcd . Owner . PopupMessage ( eventArgs . User , Loc . GetString ( "rcd-component-tile-obstructed-message" ) ) ;
return false ;
}
return true ;
default :
return false ; //I don't know why this would happen, but sure I guess. Get out of here invalid state!
}
}
2021-12-05 18:09:01 +01:00
private void NextMode ( EntityUid uid , RCDComponent rcd , EntityUid user )
2021-10-24 06:34:00 +03:00
{
SoundSystem . Play ( Filter . Pvs ( uid ) , rcd . SwapModeSound . GetSound ( ) , uid ) ;
var mode = ( int ) rcd . Mode ; //Firstly, cast our RCDmode mode to an int (enums are backed by ints anyway by default)
mode = ( + + mode ) % _modes . Length ; //Then, do a rollover on the value so it doesnt hit an invalid state
rcd . Mode = ( RcdMode ) mode ; //Finally, cast the newly acquired int mode to an RCDmode so we can use it.
2021-12-26 15:32:45 +13:00
var msg = Loc . GetString ( "rcd-component-change-mode" , ( "mode" , rcd . Mode . ToString ( ) ) ) ;
rcd . Owner . PopupMessage ( user , msg ) ; //Prints an overhead message above the RCD
2021-10-24 06:34:00 +03:00
}
}
}