2023-05-01 13:46:59 +00:00
using Content.Shared.Administration.Logs ;
using Content.Shared.Charges.Components ;
using Content.Shared.Charges.Systems ;
using Content.Shared.Database ;
using Content.Shared.DoAfter ;
using Content.Shared.Examine ;
using Content.Shared.Interaction ;
using Content.Shared.Interaction.Events ;
using Content.Shared.Maps ;
using Content.Shared.Physics ;
using Content.Shared.Popups ;
using Content.Shared.RCD.Components ;
using Content.Shared.Tag ;
2023-10-22 16:53:39 +11:00
using Content.Shared.Tiles ;
2023-11-26 14:20:07 +11:00
using Robust.Shared.Audio ;
2023-11-27 22:12:34 +11:00
using Robust.Shared.Audio.Systems ;
2023-05-01 13:46:59 +00:00
using Robust.Shared.Map ;
using Robust.Shared.Map.Components ;
using Robust.Shared.Network ;
using Robust.Shared.Serialization ;
using Robust.Shared.Timing ;
namespace Content.Shared.RCD.Systems ;
public sealed class RCDSystem : EntitySystem
{
2023-10-22 16:53:39 +11:00
[Dependency] private readonly IGameTiming _timing = default ! ;
[Dependency] private readonly IMapManager _mapMan = default ! ;
[Dependency] private readonly INetManager _net = default ! ;
2023-05-01 13:46:59 +00:00
[Dependency] private readonly ISharedAdminLogManager _adminLogger = default ! ;
2023-10-22 16:53:39 +11:00
[Dependency] private readonly ITileDefinitionManager _tileDefMan = default ! ;
[Dependency] private readonly FloorTileSystem _floors = default ! ;
2023-05-01 13:46:59 +00:00
[Dependency] private readonly SharedAudioSystem _audio = default ! ;
[Dependency] private readonly SharedChargesSystem _charges = default ! ;
[Dependency] private readonly SharedDoAfterSystem _doAfter = default ! ;
[Dependency] private readonly SharedInteractionSystem _interaction = default ! ;
[Dependency] private readonly SharedPopupSystem _popup = default ! ;
[Dependency] private readonly TagSystem _tag = default ! ;
[Dependency] private readonly TurfSystem _turf = default ! ;
2023-12-17 10:08:59 -06:00
[Dependency] private readonly IGameTiming _gameTiming = default ! ;
2023-05-01 13:46:59 +00:00
private readonly int RcdModeCount = Enum . GetValues ( typeof ( RcdMode ) ) . Length ;
public override void Initialize ( )
{
base . Initialize ( ) ;
SubscribeLocalEvent < RCDComponent , ExaminedEvent > ( OnExamine ) ;
SubscribeLocalEvent < RCDComponent , UseInHandEvent > ( OnUseInHand ) ;
SubscribeLocalEvent < RCDComponent , AfterInteractEvent > ( OnAfterInteract ) ;
SubscribeLocalEvent < RCDComponent , RCDDoAfterEvent > ( OnDoAfter ) ;
SubscribeLocalEvent < RCDComponent , DoAfterAttemptEvent < RCDDoAfterEvent > > ( OnDoAfterAttempt ) ;
}
private void OnExamine ( EntityUid uid , RCDComponent comp , ExaminedEvent args )
{
if ( ! args . IsInDetailsRange )
return ;
var msg = Loc . GetString ( "rcd-component-examine-detail" , ( "mode" , comp . Mode ) ) ;
args . PushMarkup ( msg ) ;
}
private void OnUseInHand ( EntityUid uid , RCDComponent comp , UseInHandEvent args )
{
if ( args . Handled )
return ;
NextMode ( uid , comp , args . User ) ;
args . Handled = true ;
}
private void OnAfterInteract ( EntityUid uid , RCDComponent comp , AfterInteractEvent args )
{
if ( args . Handled | | ! args . CanReach )
return ;
var user = args . User ;
TryComp < LimitedChargesComponent > ( uid , out var charges ) ;
if ( _charges . IsEmpty ( uid , charges ) )
{
_popup . PopupClient ( Loc . GetString ( "rcd-component-no-ammo-message" ) , uid , user ) ;
return ;
}
var location = args . ClickLocation ;
// Initial validity check
if ( ! location . IsValid ( EntityManager ) )
return ;
var gridId = location . GetGridUid ( EntityManager ) ;
if ( ! HasComp < MapGridComponent > ( gridId ) )
{
location = location . AlignWithClosestGridTile ( ) ;
gridId = location . GetGridUid ( EntityManager ) ;
// Check if fixing it failed / get final grid ID
if ( ! HasComp < MapGridComponent > ( gridId ) )
return ;
}
2023-09-11 09:42:41 +10:00
var doAfterArgs = new DoAfterArgs ( EntityManager , user , comp . Delay , new RCDDoAfterEvent ( GetNetCoordinates ( location ) , comp . Mode ) , uid , target : args . Target , used : uid )
2023-05-01 13:46:59 +00:00
{
BreakOnDamage = true ,
NeedHand = true ,
BreakOnHandChange = true ,
BreakOnUserMove = true ,
BreakOnTargetMove = args . Target ! = null ,
AttemptFrequency = AttemptFrequency . EveryTick
} ;
args . Handled = true ;
2023-12-17 10:08:59 -06:00
if ( _doAfter . TryStartDoAfter ( doAfterArgs ) & & _gameTiming . IsFirstTimePredicted )
Spawn ( "EffectRCDConstruction" , location ) ;
2023-05-01 13:46:59 +00:00
}
private void OnDoAfterAttempt ( EntityUid uid , RCDComponent comp , DoAfterAttemptEvent < RCDDoAfterEvent > args )
{
// sus client crash why
if ( args . Event ? . DoAfter ? . Args = = null )
return ;
2023-09-11 09:42:41 +10:00
var location = GetCoordinates ( args . Event . Location ) ;
2023-05-01 13:46:59 +00:00
var gridId = location . GetGridUid ( EntityManager ) ;
if ( ! HasComp < MapGridComponent > ( gridId ) )
{
location = location . AlignWithClosestGridTile ( ) ;
gridId = location . GetGridUid ( EntityManager ) ;
// Check if fixing it failed / get final grid ID
if ( ! HasComp < MapGridComponent > ( gridId ) )
return ;
}
var mapGrid = _mapMan . GetGrid ( gridId . Value ) ;
var tile = mapGrid . GetTileRef ( location ) ;
if ( ! IsRCDStillValid ( uid , comp , args . Event . User , args . Event . Target , mapGrid , tile , args . Event . StartingMode ) )
args . Cancel ( ) ;
}
private void OnDoAfter ( EntityUid uid , RCDComponent comp , RCDDoAfterEvent args )
{
if ( args . Handled | | args . Cancelled | | ! _timing . IsFirstTimePredicted )
return ;
var user = args . User ;
2023-09-11 09:42:41 +10:00
var location = GetCoordinates ( args . Location ) ;
2023-05-01 13:46:59 +00:00
var gridId = location . GetGridUid ( EntityManager ) ;
if ( ! HasComp < MapGridComponent > ( gridId ) )
{
location = location . AlignWithClosestGridTile ( ) ;
gridId = location . GetGridUid ( EntityManager ) ;
// Check if fixing it failed / get final grid ID
if ( ! HasComp < MapGridComponent > ( gridId ) )
return ;
}
var mapGrid = _mapMan . GetGrid ( gridId . Value ) ;
var tile = mapGrid . GetTileRef ( location ) ;
var snapPos = mapGrid . TileIndicesFor ( location ) ;
2023-10-22 16:53:39 +11:00
// I love that this uses entirely separate code to construction and tile placement!!!
2023-05-01 13:46:59 +00:00
switch ( comp . Mode )
{
//Floor mode just needs the tile to be a space tile (subFloor)
case RcdMode . Floors :
2023-10-22 16:53:39 +11:00
if ( ! _floors . CanPlaceTile ( gridId . Value , mapGrid , out var reason ) )
{
_popup . PopupClient ( reason , user , user ) ;
return ;
}
2023-05-01 13:46:59 +00:00
mapGrid . SetTile ( snapPos , new Tile ( _tileDefMan [ comp . Floor ] . TileId ) ) ;
_adminLogger . Add ( LogType . RCD , LogImpact . High , $"{ToPrettyString(args.User):user} used RCD to set grid: {tile.GridUid} {snapPos} to {comp.Floor}" ) ;
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 ( ! IsTileBlocked ( tile ) ) // Delete the turf
{
mapGrid . SetTile ( snapPos , Tile . Empty ) ;
_adminLogger . Add ( LogType . RCD , LogImpact . High , $"{ToPrettyString(args.User):user} used RCD to set grid: {tile.GridUid} tile: {snapPos} to space" ) ;
}
else // Delete the targeted thing
{
var target = args . Target ! . Value ;
_adminLogger . Add ( LogType . RCD , LogImpact . High , $"{ToPrettyString(args.User):user} used RCD to delete {ToPrettyString(target):target}" ) ;
QueueDel ( target ) ;
}
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 :
// only spawn on the server
if ( _net . IsServer )
{
var ent = Spawn ( "WallSolid" , mapGrid . GridTileToLocal ( snapPos ) ) ;
2024-02-28 00:51:20 +11:00
Transform ( ent ) . LocalRotation = Angle . Zero ; // Walls always need to point south.
2023-05-01 13:46:59 +00:00
_adminLogger . Add ( LogType . RCD , LogImpact . High , $"{ToPrettyString(args.User):user} used RCD to spawn {ToPrettyString(ent)} at {snapPos} on grid {tile.GridUid}" ) ;
}
break ;
case RcdMode . Airlock :
// only spawn on the server
if ( _net . IsServer )
{
var airlock = Spawn ( "Airlock" , mapGrid . GridTileToLocal ( snapPos ) ) ;
2024-02-28 00:51:20 +11:00
Transform ( airlock ) . LocalRotation = Transform ( uid ) . LocalRotation ; //Now apply icon smoothing.
2023-05-01 13:46:59 +00:00
_adminLogger . Add ( LogType . RCD , LogImpact . High , $"{ToPrettyString(args.User):user} used RCD to spawn {ToPrettyString(airlock)} at {snapPos} on grid {tile.GridUid}" ) ;
}
break ;
default :
args . Handled = true ;
return ; //I don't know why this would happen, but sure I guess. Get out of here invalid state!
}
_audio . PlayPredicted ( comp . SuccessSound , uid , user ) ;
_charges . UseCharge ( uid ) ;
args . Handled = true ;
}
private bool IsRCDStillValid ( EntityUid uid , RCDComponent comp , EntityUid user , EntityUid ? target , MapGridComponent mapGrid , TileRef tile , RcdMode startingMode )
{
//Less expensive checks first. Failing those ones, we need to check that the tile isn't obstructed.
if ( comp . Mode ! = startingMode )
return false ;
var unobstructed = target = = null
? _interaction . InRangeUnobstructed ( user , mapGrid . GridTileToWorld ( tile . GridIndices ) , popup : true )
: _interaction . InRangeUnobstructed ( user , target . Value , popup : true ) ;
if ( ! unobstructed )
return false ;
switch ( comp . Mode )
{
//Floor mode just needs the tile to be a space tile (subFloor)
case RcdMode . Floors :
if ( ! tile . Tile . IsEmpty )
{
_popup . PopupClient ( Loc . GetString ( "rcd-component-cannot-build-floor-tile-not-empty-message" ) , uid , user ) ;
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 ;
2023-07-16 13:46:35 +00:00
//They tried to decon a turf but...
if ( target = = null )
2023-05-01 13:46:59 +00:00
{
2023-07-16 13:46:35 +00:00
// the turf is blocked
if ( IsTileBlocked ( tile ) )
{
_popup . PopupClient ( Loc . GetString ( "rcd-component-tile-obstructed-message" ) , uid , user ) ;
return false ;
}
// the turf can't be destroyed (planet probably)
var tileDef = ( ContentTileDefinition ) _tileDefMan [ tile . Tile . TypeId ] ;
if ( tileDef . Indestructible )
{
_popup . PopupClient ( Loc . GetString ( "rcd-component-tile-indestructible-message" ) , uid , user ) ;
return false ;
}
2023-05-01 13:46:59 +00:00
}
//They tried to decon a non-turf but it's not in the whitelist
2023-07-16 13:46:35 +00:00
else if ( ! _tag . HasTag ( target . Value , "RCDDeconstructWhitelist" ) )
2023-05-01 13:46:59 +00:00
{
_popup . PopupClient ( Loc . GetString ( "rcd-component-deconstruct-target-not-on-whitelist-message" ) , uid , user ) ;
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 )
{
_popup . PopupClient ( Loc . GetString ( "rcd-component-cannot-build-wall-tile-not-empty-message" ) , uid , user ) ;
return false ;
}
if ( IsTileBlocked ( tile ) )
{
_popup . PopupClient ( Loc . GetString ( "rcd-component-tile-obstructed-message" ) , uid , user ) ;
return false ;
}
return true ;
case RcdMode . Airlock :
if ( tile . Tile . IsEmpty )
{
_popup . PopupClient ( Loc . GetString ( "rcd-component-cannot-build-airlock-tile-not-empty-message" ) , uid , user ) ;
return false ;
}
if ( IsTileBlocked ( tile ) )
{
_popup . PopupClient ( Loc . GetString ( "rcd-component-tile-obstructed-message" ) , uid , user ) ;
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!
}
}
private void NextMode ( EntityUid uid , RCDComponent comp , EntityUid user )
{
_audio . PlayPredicted ( comp . SwapModeSound , uid , user ) ;
var mode = ( int ) comp . Mode ;
mode = + + mode % RcdModeCount ;
comp . Mode = ( RcdMode ) mode ;
Dirty ( comp ) ;
var msg = Loc . GetString ( "rcd-component-change-mode" , ( "mode" , comp . Mode . ToString ( ) ) ) ;
_popup . PopupClient ( msg , uid , user ) ;
}
private bool IsTileBlocked ( TileRef tile )
{
return _turf . IsTileBlocked ( tile , CollisionGroup . MobMask ) ;
}
}
[Serializable, NetSerializable]
2023-08-22 18:14:33 -07:00
public sealed partial class RCDDoAfterEvent : DoAfterEvent
2023-05-01 13:46:59 +00:00
{
[DataField("location", required: true)]
2023-09-11 09:42:41 +10:00
public NetCoordinates Location = default ! ;
2023-05-01 13:46:59 +00:00
[DataField("startingMode", required: true)]
2023-08-22 18:14:33 -07:00
public RcdMode StartingMode = default ! ;
2023-05-01 13:46:59 +00:00
private RCDDoAfterEvent ( )
{
}
2023-09-11 09:42:41 +10:00
public RCDDoAfterEvent ( NetCoordinates location , RcdMode startingMode )
2023-05-01 13:46:59 +00:00
{
Location = location ;
StartingMode = startingMode ;
}
public override DoAfterEvent Clone ( ) = > this ;
}