2022-01-30 13:49:56 +13:00
using Content.Server.Access ;
using Content.Server.Atmos.Components ;
using Content.Server.Atmos.EntitySystems ;
using Content.Server.Construction ;
2022-07-29 14:13:12 +12:00
using Content.Server.Tools.Systems ;
using Content.Shared.Database ;
2021-06-09 22:19:39 +02:00
using Content.Shared.Doors ;
2022-01-30 13:49:56 +13:00
using Content.Shared.Doors.Components ;
using Content.Shared.Doors.Systems ;
2022-02-17 21:43:24 -05:00
using Content.Shared.Emag.Systems ;
2022-01-30 13:49:56 +13:00
using Content.Shared.Interaction ;
2022-07-29 14:13:12 +12:00
using Content.Shared.Tools.Components ;
using Content.Shared.Verbs ;
2022-01-30 13:49:56 +13:00
using Robust.Shared.Audio ;
2023-06-16 05:58:29 -05:00
using Content.Server.Administration.Logs ;
2023-01-18 05:44:32 +11:00
using Content.Server.Power.EntitySystems ;
2023-02-24 19:01:25 -05:00
using Content.Shared.Tools ;
2022-09-14 17:26:26 +10:00
using Robust.Shared.Physics.Components ;
using Robust.Shared.Physics.Events ;
2023-04-03 13:13:48 +12:00
using Content.Shared.DoAfter ;
2021-02-12 07:02:14 -08:00
2022-01-30 13:49:56 +13:00
namespace Content.Server.Doors.Systems ;
public sealed class DoorSystem : SharedDoorSystem
2021-02-12 07:02:14 -08:00
{
2023-06-16 05:58:29 -05:00
[Dependency] private readonly IAdminLogManager _adminLog = default ! ;
2023-06-01 02:23:35 +12:00
[Dependency] private readonly DoorBoltSystem _bolts = default ! ;
2022-06-12 11:15:53 +10:00
[Dependency] private readonly AirtightSystem _airtightSystem = default ! ;
2023-02-24 19:01:25 -05:00
[Dependency] private readonly SharedToolSystem _toolSystem = default ! ;
2022-01-30 13:49:56 +13:00
public override void Initialize ( )
{
base . Initialize ( ) ;
2022-02-05 10:14:21 +13:00
SubscribeLocalEvent < DoorComponent , InteractUsingEvent > ( OnInteractUsing , after : new [ ] { typeof ( ConstructionSystem ) } ) ;
2022-01-30 13:49:56 +13:00
2022-06-28 20:50:58 +10:00
// Mob prying doors
SubscribeLocalEvent < DoorComponent , GetVerbsEvent < AlternativeVerb > > ( OnDoorAltVerb ) ;
2023-04-03 13:13:48 +12:00
SubscribeLocalEvent < DoorComponent , DoorPryDoAfterEvent > ( OnPryFinished ) ;
2022-05-09 08:51:52 +03:00
SubscribeLocalEvent < DoorComponent , WeldableAttemptEvent > ( OnWeldAttempt ) ;
SubscribeLocalEvent < DoorComponent , WeldableChangedEvent > ( OnWeldChanged ) ;
2022-02-17 21:43:24 -05:00
SubscribeLocalEvent < DoorComponent , GotEmaggedEvent > ( OnEmagged ) ;
2022-01-30 13:49:56 +13:00
}
2023-06-17 12:09:49 +10:00
protected override void OnActivate ( EntityUid uid , DoorComponent door , ActivateInWorldEvent args )
{
// TODO once access permissions are shared, move this back to shared.
if ( args . Handled | | ! door . ClickOpen )
return ;
TryToggleDoor ( uid , door , args . User ) ;
args . Handled = true ;
}
2023-01-15 15:38:59 +11:00
protected override void SetCollidable (
EntityUid uid ,
bool collidable ,
2022-02-20 08:34:01 +13:00
DoorComponent ? door = null ,
PhysicsComponent ? physics = null ,
OccluderComponent ? occluder = null )
2022-01-30 20:30:24 +13:00
{
2022-02-20 08:34:01 +13:00
if ( ! Resolve ( uid , ref door ) )
return ;
2022-01-30 20:30:24 +13:00
2022-02-20 08:34:01 +13:00
if ( door . ChangeAirtight & & TryComp ( uid , out AirtightComponent ? airtight ) )
2023-01-11 19:18:26 +11:00
_airtightSystem . SetAirblocked ( uid , airtight , collidable ) ;
2022-02-20 08:34:01 +13:00
// Pathfinding / AI stuff.
2022-05-16 13:21:00 +10:00
RaiseLocalEvent ( new AccessReaderChangeEvent ( uid , collidable ) ) ;
2022-02-20 08:34:01 +13:00
base . SetCollidable ( uid , collidable , door , physics , occluder ) ;
2022-01-30 20:30:24 +13:00
}
2022-01-30 13:49:56 +13:00
// TODO AUDIO PREDICT Figure out a better way to handle sound and prediction. For now, this works well enough?
//
// Currently a client will predict when a door is going to close automatically. So any client in PVS range can just
// play their audio locally. Playing it server-side causes an odd delay, while in shared it causes double-audio.
//
// But if we just do that, then if a door is closed prematurely as the result of an interaction (i.e., using "E" on
// an open door), then the audio would only be played for the client performing the interaction.
//
// So we do this:
// - Play audio client-side IF the closing is being predicted (auto-close or predicted interaction)
// - Server assumes automated closing is predicted by clients and does not play audio unless otherwise specified.
// - Major exception is player interactions, which other players cannot predict
// - In that case, send audio to all players, except possibly the interacting player if it was a predicted
// interaction.
2021-02-12 07:02:14 -08:00
/// <summary>
2022-01-30 13:49:56 +13:00
/// Selectively send sound to clients, taking care to not send the double-audio.
2021-02-12 07:02:14 -08:00
/// </summary>
2022-01-30 13:49:56 +13:00
/// <param name="uid">The audio source</param>
2022-08-23 11:31:54 +01:00
/// <param name="soundSpecifier">The sound</param>
/// <param name="audioParams">The audio parameters.</param>
2022-01-30 13:49:56 +13:00
/// <param name="predictingPlayer">The user (if any) that instigated an interaction</param>
/// <param name="predicted">Whether this interaction would have been predicted. If the predicting player is null,
/// this assumes it would have been predicted by all players in PVS range.</param>
2022-08-23 11:31:54 +01:00
protected override void PlaySound ( EntityUid uid , SoundSpecifier soundSpecifier , AudioParams audioParams , EntityUid ? predictingPlayer , bool predicted )
2021-02-12 07:02:14 -08:00
{
2022-01-30 13:49:56 +13:00
// If this sound would have been predicted by all clients, do not play any audio.
if ( predicted & & predictingPlayer = = null )
return ;
if ( predicted )
2022-11-22 13:49:48 +13:00
Audio . PlayPredicted ( soundSpecifier , uid , predictingPlayer , audioParams ) ;
else
Audio . PlayPvs ( soundSpecifier , uid , audioParams ) ;
2022-01-30 13:49:56 +13:00
}
2022-02-09 03:13:35 +00:00
#region DoAfters
2022-01-30 13:49:56 +13:00
/// <summary>
/// Weld or pry open a door.
/// </summary>
private void OnInteractUsing ( EntityUid uid , DoorComponent door , InteractUsingEvent args )
{
if ( args . Handled )
return ;
if ( ! TryComp ( args . Used , out ToolComponent ? tool ) )
return ;
if ( tool . Qualities . Contains ( door . PryingQuality ) )
2021-02-12 07:02:14 -08:00
{
2023-04-21 15:05:29 +10:00
args . Handled = TryPryDoor ( uid , args . Used , args . User , door , out _ ) ;
2022-01-30 13:49:56 +13:00
}
2022-05-09 08:51:52 +03:00
}
2021-02-12 07:02:14 -08:00
2022-05-09 08:51:52 +03:00
private void OnWeldAttempt ( EntityUid uid , DoorComponent component , WeldableAttemptEvent args )
{
if ( component . CurrentlyCrushing . Count > 0 )
{
args . Cancel ( ) ;
return ;
}
if ( component . State ! = DoorState . Closed & & component . State ! = DoorState . Welded )
2022-01-30 13:49:56 +13:00
{
2022-05-09 08:51:52 +03:00
args . Cancel ( ) ;
2021-07-21 20:28:37 +10:00
}
2022-01-30 13:49:56 +13:00
}
2021-07-21 20:28:37 +10:00
2022-05-09 08:51:52 +03:00
private void OnWeldChanged ( EntityUid uid , DoorComponent component , WeldableChangedEvent args )
2022-01-30 13:49:56 +13:00
{
2022-05-09 08:51:52 +03:00
if ( component . State = = DoorState . Closed )
SetState ( uid , DoorState . Welded , component ) ;
else if ( component . State = = DoorState . Welded )
SetState ( uid , DoorState . Closed , component ) ;
2022-01-30 13:49:56 +13:00
}
2022-06-28 20:50:58 +10:00
private void OnDoorAltVerb ( EntityUid uid , DoorComponent component , GetVerbsEvent < AlternativeVerb > args )
{
2022-08-23 11:31:54 +01:00
if ( ! args . CanInteract | | ! args . CanAccess )
return ;
if ( ! TryComp < ToolComponent > ( args . User , out var tool ) | | ! tool . Qualities . Contains ( component . PryingQuality ) )
return ;
2022-06-28 20:50:58 +10:00
args . Verbs . Add ( new AlternativeVerb ( )
{
2022-09-30 14:39:48 +10:00
Text = Loc . GetString ( "door-pry" ) ,
2022-06-28 20:50:58 +10:00
Impact = LogImpact . Low ,
2023-04-21 15:05:29 +10:00
Act = ( ) = > TryPryDoor ( uid , args . User , args . User , component , out _ , force : true ) ,
2022-06-28 20:50:58 +10:00
} ) ;
}
2022-08-31 23:22:25 -04:00
2022-01-30 13:49:56 +13:00
/// <summary>
/// Pry open a door. This does not check if the user is holding the required tool.
/// </summary>
2023-04-21 15:05:29 +10:00
public bool TryPryDoor ( EntityUid target , EntityUid tool , EntityUid user , DoorComponent door , out DoAfterId ? id , bool force = false )
2022-01-30 13:49:56 +13:00
{
2023-04-21 15:05:29 +10:00
id = null ;
2022-01-30 13:49:56 +13:00
if ( door . State = = DoorState . Welded )
2022-02-05 10:14:21 +13:00
return false ;
2022-01-30 13:49:56 +13:00
2022-06-28 20:50:58 +10:00
if ( ! force )
{
2022-08-31 23:22:25 -04:00
var canEv = new BeforeDoorPryEvent ( user , tool ) ;
2022-06-28 20:50:58 +10:00
RaiseLocalEvent ( target , canEv , false ) ;
2022-01-30 13:49:56 +13:00
2023-05-05 16:16:23 +03:00
if ( ! door . CanPry | | canEv . Cancelled )
2022-06-28 20:50:58 +10:00
// mark handled, as airlock component will cancel after generating a pop-up & you don't want to pry a tile
// under a windoor.
return true ;
}
2022-01-30 13:49:56 +13:00
2023-01-01 14:42:21 -05:00
var modEv = new DoorGetPryTimeModifierEvent ( user ) ;
2022-01-30 13:49:56 +13:00
RaiseLocalEvent ( target , modEv , false ) ;
2023-06-16 05:58:29 -05:00
_adminLog . Add ( LogType . Action , LogImpact . Low , $"{ToPrettyString(user)} is using {ToPrettyString(tool)} to pry {ToPrettyString(target)} while it is {door.State}" ) ; // TODO move to generic tool use logging in a way that includes door state
2023-04-21 15:05:29 +10:00
_toolSystem . UseTool ( tool , user , target , TimeSpan . FromSeconds ( modEv . PryTimeModifier * door . PryTime ) , new [ ] { door . PryingQuality } , new DoorPryDoAfterEvent ( ) , out id ) ;
2022-02-05 10:14:21 +13:00
return true ; // we might not actually succeeded, but a do-after has started
2022-01-30 13:49:56 +13:00
}
2023-04-03 13:13:48 +12:00
private void OnPryFinished ( EntityUid uid , DoorComponent door , DoAfterEvent args )
2022-01-30 13:49:56 +13:00
{
2023-04-03 13:13:48 +12:00
if ( args . Cancelled )
return ;
2022-01-30 13:49:56 +13:00
if ( door . State = = DoorState . Closed )
2023-06-16 05:58:29 -05:00
{
_adminLog . Add ( LogType . Action , LogImpact . Medium , $"{ToPrettyString(args.User)} pried {ToPrettyString(uid)} open" ) ; // TODO move to generic tool use logging in a way that includes door state
2022-01-30 13:49:56 +13:00
StartOpening ( uid , door ) ;
2023-06-16 05:58:29 -05:00
}
2022-01-30 13:49:56 +13:00
else if ( door . State = = DoorState . Open )
2023-06-16 05:58:29 -05:00
{
_adminLog . Add ( LogType . Action , LogImpact . Medium , $"{ToPrettyString(args.User)} pried {ToPrettyString(uid)} closed" ) ; // TODO move to generic tool use logging in a way that includes door state
2022-01-30 13:49:56 +13:00
StartClosing ( uid , door ) ;
2023-06-16 05:58:29 -05:00
}
2022-01-30 13:49:56 +13:00
}
2022-02-09 03:13:35 +00:00
#endregion
2022-01-30 13:49:56 +13:00
2021-10-01 13:47:38 +02:00
2022-01-30 13:49:56 +13:00
/// <summary>
/// Open a door if a player or door-bumper (PDA, ID-card) collide with the door. Sadly, bullets no longer
/// generate "access denied" sounds as you fire at a door.
/// </summary>
2022-09-14 17:26:26 +10:00
protected override void HandleCollide ( EntityUid uid , DoorComponent door , ref StartCollideEvent args )
2022-01-30 13:49:56 +13:00
{
// TODO ACCESS READER move access reader to shared and predict door opening/closing
// Then this can be moved to the shared system without mispredicting.
if ( ! door . BumpOpen )
return ;
2021-07-21 20:28:37 +10:00
2022-01-30 13:49:56 +13:00
if ( door . State ! = DoorState . Closed )
return ;
2023-05-09 19:21:26 +12:00
var otherUid = args . OtherEntity ;
2022-02-08 14:08:11 +11:00
2022-06-12 11:15:53 +10:00
if ( Tags . HasTag ( otherUid , "DoorBumpOpener" ) )
2022-02-08 14:08:11 +11:00
TryOpen ( uid , door , otherUid ) ;
2022-01-30 13:49:56 +13:00
}
2023-01-21 10:12:45 -05:00
private void OnEmagged ( EntityUid uid , DoorComponent door , ref GotEmaggedEvent args )
2022-02-17 21:43:24 -05:00
{
if ( TryComp < AirlockComponent > ( uid , out var airlockComponent ) )
{
2023-06-01 02:23:35 +12:00
if ( _bolts . IsBolted ( uid ) | | ! this . IsPowered ( uid , EntityManager ) )
2022-04-23 20:37:49 -04:00
return ;
2022-02-17 21:43:24 -05:00
if ( door . State = = DoorState . Closed )
{
2022-03-19 14:00:01 -05:00
SetState ( uid , DoorState . Emagging , door ) ;
2022-08-23 11:31:54 +01:00
PlaySound ( uid , door . SparkSound , AudioParams . Default . WithVolume ( 8 ) , args . UserUid , false ) ;
2022-02-17 21:43:24 -05:00
args . Handled = true ;
}
}
}
2022-03-19 14:00:01 -05:00
public override void StartOpening ( EntityUid uid , DoorComponent ? door = null , EntityUid ? user = null , bool predicted = false )
{
if ( ! Resolve ( uid , ref door ) )
return ;
2022-08-23 11:31:54 +01:00
var lastState = door . State ;
2022-03-19 14:00:01 -05:00
SetState ( uid , DoorState . Opening , door ) ;
if ( door . OpenSound ! = null )
2022-08-23 11:31:54 +01:00
PlaySound ( uid , door . OpenSound , AudioParams . Default . WithVolume ( - 5 ) , user , predicted ) ;
2022-03-19 14:00:01 -05:00
2023-06-01 02:23:35 +12:00
if ( lastState = = DoorState . Emagging & & TryComp < DoorBoltComponent > ( uid , out var doorBoltComponent ) )
_bolts . SetBoltsWithAudio ( uid , doorBoltComponent , ! doorBoltComponent . BoltsDown ) ;
2022-03-19 14:00:01 -05:00
}
2022-06-12 11:15:53 +10:00
protected override void CheckDoorBump ( DoorComponent component , PhysicsComponent body )
{
2023-05-09 19:21:26 +12:00
var uid = body . Owner ;
2022-06-12 11:15:53 +10:00
if ( component . BumpOpen )
{
2023-05-09 19:21:26 +12:00
foreach ( var other in PhysicsSystem . GetContactingEntities ( uid , body , approximate : true ) )
2022-06-12 11:15:53 +10:00
{
2023-05-09 19:21:26 +12:00
if ( Tags . HasTag ( other , "DoorBumpOpener" ) & & TryOpen ( uid , component , other , false , quiet : true ) )
break ;
2022-06-12 11:15:53 +10:00
}
}
}
2021-02-12 07:02:14 -08:00
}
2022-01-30 13:49:56 +13:00