diff --git a/Content.Client/Doors/AirlockSystem.cs b/Content.Client/Doors/AirlockSystem.cs index 18b9eae5f3..cc68d09039 100644 --- a/Content.Client/Doors/AirlockSystem.cs +++ b/Content.Client/Doors/AirlockSystem.cs @@ -1,6 +1,7 @@ using Content.Client.Wires.Visualizers; using Content.Shared.Doors.Components; using Content.Shared.Doors.Systems; +using Content.Shared.Prying.Components; using Robust.Client.Animations; using Robust.Client.GameObjects; @@ -15,6 +16,13 @@ public sealed class AirlockSystem : SharedAirlockSystem base.Initialize(); SubscribeLocalEvent(OnComponentStartup); SubscribeLocalEvent(OnAppearanceChange); + SubscribeLocalEvent(OnAirlockPryAttempt); + } + + private void OnAirlockPryAttempt(EntityUid uid, AirlockComponent component, ref BeforePryEvent args) + { + // TODO: Temporary until airlocks predicted. + args.Cancelled = true; } private void OnComponentStartup(EntityUid uid, AirlockComponent comp, ComponentStartup args) diff --git a/Content.Server/Doors/Systems/AirlockSystem.cs b/Content.Server/Doors/Systems/AirlockSystem.cs index bb75fc7d47..0ea2755ab6 100644 --- a/Content.Server/Doors/Systems/AirlockSystem.cs +++ b/Content.Server/Doors/Systems/AirlockSystem.cs @@ -1,7 +1,6 @@ using Content.Server.DeviceLinking.Events; using Content.Server.Power.Components; using Content.Server.Power.EntitySystems; -using Content.Shared.Tools.Components; using Content.Server.Wires; using Content.Shared.Doors; using Content.Shared.Doors.Components; @@ -9,6 +8,7 @@ using Content.Shared.Doors.Systems; using Content.Shared.Interaction; using Robust.Server.GameObjects; using Content.Shared.Wires; +using Content.Shared.Prying.Components; using Robust.Shared.Prototypes; namespace Content.Server.Doors.Systems; @@ -31,9 +31,9 @@ public sealed class AirlockSystem : SharedAirlockSystem SubscribeLocalEvent(OnStateChanged); SubscribeLocalEvent(OnBeforeDoorOpened); SubscribeLocalEvent(OnBeforeDoorDenied); - SubscribeLocalEvent(OnActivate, before: new [] {typeof(DoorSystem)}); - SubscribeLocalEvent(OnGetPryMod); - SubscribeLocalEvent(OnDoorPry); + SubscribeLocalEvent(OnActivate, before: new[] { typeof(DoorSystem) }); + SubscribeLocalEvent(OnGetPryMod); + SubscribeLocalEvent(OnBeforePry); } @@ -169,20 +169,18 @@ public sealed class AirlockSystem : SharedAirlockSystem } } - private void OnGetPryMod(EntityUid uid, AirlockComponent component, DoorGetPryTimeModifierEvent args) + private void OnGetPryMod(EntityUid uid, AirlockComponent component, ref GetPryTimeModifierEvent args) { if (_power.IsPowered(uid)) args.PryTimeModifier *= component.PoweredPryModifier; } - private void OnDoorPry(EntityUid uid, AirlockComponent component, BeforeDoorPryEvent args) + private void OnBeforePry(EntityUid uid, AirlockComponent component, ref BeforePryEvent args) { - if (this.IsPowered(uid, EntityManager)) + if (this.IsPowered(uid, EntityManager) && !args.PryPowered) { - if (HasComp(args.Tool)) - return; - Popup.PopupEntity(Loc.GetString("airlock-component-cannot-pry-is-powered-message"), uid, args.User); - args.Cancel(); + Popup.PopupClient(Loc.GetString("airlock-component-cannot-pry-is-powered-message"), uid, args.User); + args.Cancelled = true; } } diff --git a/Content.Server/Doors/Systems/DoorSystem.cs b/Content.Server/Doors/Systems/DoorSystem.cs index f9918dfb0a..aa4de6b86b 100644 --- a/Content.Server/Doors/Systems/DoorSystem.cs +++ b/Content.Server/Doors/Systems/DoorSystem.cs @@ -1,15 +1,12 @@ using Content.Server.Access; using Content.Server.Atmos.Components; using Content.Server.Atmos.EntitySystems; -using Content.Server.Construction; using Content.Shared.Database; using Content.Shared.Doors; using Content.Shared.Doors.Components; using Content.Shared.Doors.Systems; using Content.Shared.Emag.Systems; using Content.Shared.Interaction; -using Content.Shared.Tools.Components; -using Content.Shared.Verbs; using Robust.Shared.Audio; using Content.Server.Administration.Logs; using Content.Server.Power.EntitySystems; @@ -17,6 +14,8 @@ using Content.Shared.Tools; using Robust.Shared.Physics.Components; using Robust.Shared.Physics.Events; using Content.Shared.DoAfter; +using Content.Shared.Prying.Systems; +using Content.Shared.Prying.Components; using Content.Shared.Tools.Systems; namespace Content.Server.Doors.Systems; @@ -27,20 +26,19 @@ public sealed class DoorSystem : SharedDoorSystem [Dependency] private readonly DoorBoltSystem _bolts = default!; [Dependency] private readonly AirtightSystem _airtightSystem = default!; [Dependency] private readonly SharedToolSystem _toolSystem = default!; + [Dependency] private readonly SharedDoAfterSystem _doAfterSystem = default!; + [Dependency] private readonly PryingSystem _pryingSystem = default!; public override void Initialize() { base.Initialize(); - SubscribeLocalEvent(OnInteractUsing, after: new[] { typeof(ConstructionSystem) }); - // Mob prying doors - SubscribeLocalEvent>(OnDoorAltVerb); - - SubscribeLocalEvent(OnPryFinished); + SubscribeLocalEvent(OnBeforeDoorPry); SubscribeLocalEvent(OnWeldAttempt); SubscribeLocalEvent(OnWeldChanged); SubscribeLocalEvent(OnEmagged); + SubscribeLocalEvent(OnAfterPry); } protected override void OnActivate(EntityUid uid, DoorComponent door, ActivateInWorldEvent args) @@ -49,7 +47,9 @@ public sealed class DoorSystem : SharedDoorSystem if (args.Handled || !door.ClickOpen) return; - TryToggleDoor(uid, door, args.User); + if (!TryToggleDoor(uid, door, args.User)) + _pryingSystem.TryPry(uid, args.User, out _); + args.Handled = true; } @@ -108,24 +108,7 @@ public sealed class DoorSystem : SharedDoorSystem Audio.PlayPvs(soundSpecifier, uid, audioParams); } -#region DoAfters - /// - /// Weld or pry open a door. - /// - 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)) - { - args.Handled = TryPryDoor(uid, args.Used, args.User, door, out _); - } - } - + #region DoAfters private void OnWeldAttempt(EntityUid uid, DoorComponent component, WeldableAttemptEvent args) { if (component.CurrentlyCrushing.Count > 0) @@ -147,69 +130,12 @@ public sealed class DoorSystem : SharedDoorSystem SetState(uid, DoorState.Closed, component); } - private void OnDoorAltVerb(EntityUid uid, DoorComponent component, GetVerbsEvent args) + private void OnBeforeDoorPry(EntityUid id, DoorComponent door, ref BeforePryEvent args) { - if (!args.CanInteract || !args.CanAccess) - return; - - if (!TryComp(args.User, out var tool) || !tool.Qualities.Contains(component.PryingQuality)) - return; - - args.Verbs.Add(new AlternativeVerb() - { - Text = Loc.GetString("door-pry"), - Impact = LogImpact.Low, - Act = () => TryPryDoor(uid, args.User, args.User, component, out _, force: true), - }); + if (door.State == DoorState.Welded || !door.CanPry) + args.Cancelled = true; } - - - /// - /// Pry open a door. This does not check if the user is holding the required tool. - /// - public bool TryPryDoor(EntityUid target, EntityUid tool, EntityUid user, DoorComponent door, out DoAfterId? id, bool force = false) - { - id = null; - - if (door.State == DoorState.Welded) - return false; - - if (!force) - { - var canEv = new BeforeDoorPryEvent(user, tool); - RaiseLocalEvent(target, canEv, false); - - if (!door.CanPry || canEv.Cancelled) - // 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; - } - - var modEv = new DoorGetPryTimeModifierEvent(user); - RaiseLocalEvent(target, modEv, false); - - _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 - _toolSystem.UseTool(tool, user, target, TimeSpan.FromSeconds(modEv.PryTimeModifier * door.PryTime), new[] {door.PryingQuality}, new DoorPryDoAfterEvent(), out id); - return true; // we might not actually succeeded, but a do-after has started - } - - private void OnPryFinished(EntityUid uid, DoorComponent door, DoAfterEvent args) - { - if (args.Cancelled) - return; - - if (door.State == DoorState.Closed) - { - _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 - StartOpening(uid, door); - } - else if (door.State == DoorState.Open) - { - _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 - StartClosing(uid, door); - } - } -#endregion + #endregion /// @@ -233,7 +159,7 @@ public sealed class DoorSystem : SharedDoorSystem } private void OnEmagged(EntityUid uid, DoorComponent door, ref GotEmaggedEvent args) { - if(TryComp(uid, out var airlockComponent)) + if (TryComp(uid, out var airlockComponent)) { if (_bolts.IsBolted(uid) || !this.IsPowered(uid, EntityManager)) return; @@ -259,10 +185,27 @@ public sealed class DoorSystem : SharedDoorSystem if (door.OpenSound != null) PlaySound(uid, door.OpenSound, AudioParams.Default.WithVolume(-5), user, predicted); - if(lastState == DoorState.Emagging && TryComp(uid, out var doorBoltComponent)) + if (lastState == DoorState.Emagging && TryComp(uid, out var doorBoltComponent)) _bolts.SetBoltsWithAudio(uid, doorBoltComponent, !doorBoltComponent.BoltsDown); } + /// + /// Open or close a door after it has been successfuly pried. + /// + private void OnAfterPry(EntityUid uid, DoorComponent door, ref PriedEvent args) + { + if (door.State == DoorState.Closed) + { + _adminLog.Add(LogType.Action, LogImpact.Medium, $"{ToPrettyString(args.User)} pried {ToPrettyString(uid)} open"); + StartOpening(uid, door, args.User); + } + else if (door.State == DoorState.Open) + { + _adminLog.Add(LogType.Action, LogImpact.Medium, $"{ToPrettyString(args.User)} pried {ToPrettyString(uid)} closed"); + StartClosing(uid, door, args.User); + } + } + protected override void CheckDoorBump(DoorComponent component, PhysicsComponent body) { var uid = body.Owner; diff --git a/Content.Server/Doors/Systems/FirelockSystem.cs b/Content.Server/Doors/Systems/FirelockSystem.cs index 7147aa4f24..e2f25c63ab 100644 --- a/Content.Server/Doors/Systems/FirelockSystem.cs +++ b/Content.Server/Doors/Systems/FirelockSystem.cs @@ -18,6 +18,7 @@ using Microsoft.Extensions.Options; using Robust.Server.GameObjects; using Robust.Shared.Map.Components; using Robust.Shared.Player; +using Content.Shared.Prying.Components; namespace Content.Server.Doors.Systems { @@ -38,7 +39,7 @@ namespace Content.Server.Doors.Systems base.Initialize(); SubscribeLocalEvent(OnBeforeDoorOpened); - SubscribeLocalEvent(OnDoorGetPryTimeModifier); + SubscribeLocalEvent(OnDoorGetPryTimeModifier); SubscribeLocalEvent(OnUpdateState); SubscribeLocalEvent(OnBeforeDoorAutoclose); @@ -144,7 +145,7 @@ namespace Content.Server.Doors.Systems args.Cancel(); } - private void OnDoorGetPryTimeModifier(EntityUid uid, FirelockComponent component, DoorGetPryTimeModifierEvent args) + private void OnDoorGetPryTimeModifier(EntityUid uid, FirelockComponent component, ref GetPryTimeModifierEvent args) { var state = CheckPressureAndFire(uid, component); @@ -261,7 +262,7 @@ namespace Content.Server.Doors.Systems List directions = new(4); for (var i = 0; i < Atmospherics.Directions; i++) { - var dir = (AtmosDirection) (1 << i); + var dir = (AtmosDirection)(1 << i); if (airtight.AirBlockedDirection.HasFlag(dir)) { directions.Add(dir); diff --git a/Content.Server/NPC/Systems/NPCSteeringSystem.Obstacles.cs b/Content.Server/NPC/Systems/NPCSteeringSystem.Obstacles.cs index 1d9c19de02..87deec9ea9 100644 --- a/Content.Server/NPC/Systems/NPCSteeringSystem.Obstacles.cs +++ b/Content.Server/NPC/Systems/NPCSteeringSystem.Obstacles.cs @@ -113,7 +113,7 @@ public sealed partial class NPCSteeringSystem // TODO: Use the verb. if (door.State != DoorState.Opening) - _doors.TryPryDoor(ent, uid, uid, door, out id, force: true); + _pryingSystem.TryPry(ent, uid, out id, uid); component.DoAfterId = id; return SteeringObstacleStatus.Continuing; diff --git a/Content.Server/NPC/Systems/NPCSteeringSystem.cs b/Content.Server/NPC/Systems/NPCSteeringSystem.cs index cbc2ba6d2c..0fa28f6af7 100644 --- a/Content.Server/NPC/Systems/NPCSteeringSystem.cs +++ b/Content.Server/NPC/Systems/NPCSteeringSystem.cs @@ -31,6 +31,7 @@ using Robust.Shared.Random; using Robust.Shared.Threading; using Robust.Shared.Timing; using Robust.Shared.Utility; +using Content.Shared.Prying.Systems; namespace Content.Server.NPC.Systems; @@ -63,6 +64,7 @@ public sealed partial class NPCSteeringSystem : SharedNPCSteeringSystem [Dependency] private readonly SharedPhysicsSystem _physics = default!; [Dependency] private readonly SharedTransformSystem _transform = default!; [Dependency] private readonly SharedCombatModeSystem _combat = default!; + [Dependency] private readonly PryingSystem _pryingSystem = default!; private EntityQuery _fixturesQuery; private EntityQuery _modifierQuery; @@ -148,7 +150,7 @@ public sealed partial class NPCSteeringSystem : SharedNPCSteeringSystem private void OnDebugRequest(RequestNPCSteeringDebugEvent msg, EntitySessionEventArgs args) { - if (!_admin.IsAdmin((IPlayerSession) args.SenderSession)) + if (!_admin.IsAdmin((IPlayerSession)args.SenderSession)) return; if (msg.Enabled) @@ -440,7 +442,7 @@ public sealed partial class NPCSteeringSystem : SharedNPCSteeringSystem if (targetPoly != null && steering.Coordinates.Position.Equals(Vector2.Zero) && TryComp(uid, out var physics) && - _interaction.InRangeUnobstructed(uid, steering.Coordinates.EntityId, range: 30f, (CollisionGroup) physics.CollisionMask)) + _interaction.InRangeUnobstructed(uid, steering.Coordinates.EntityId, range: 30f, (CollisionGroup)physics.CollisionMask)) { steering.CurrentPath.Clear(); steering.CurrentPath.Enqueue(targetPoly); diff --git a/Content.Server/Zombies/ZombieSystem.Transform.cs b/Content.Server/Zombies/ZombieSystem.Transform.cs index 5da7c6e8cd..19e8013220 100644 --- a/Content.Server/Zombies/ZombieSystem.Transform.cs +++ b/Content.Server/Zombies/ZombieSystem.Transform.cs @@ -32,6 +32,7 @@ using Content.Shared.Tools.Components; using Content.Shared.Weapons.Melee; using Content.Shared.Zombies; using Robust.Shared.Audio; +using Content.Shared.Prying.Components; namespace Content.Server.Zombies { @@ -162,11 +163,12 @@ namespace Content.Server.Zombies melee.Damage = dspec; // humanoid zombies get to pry open doors and shit - var tool = EnsureComp(target); - tool.SpeedModifier = 0.75f; - tool.Qualities = new ("Prying"); - tool.UseSound = new SoundPathSpecifier("/Audio/Items/crowbar.ogg"); - Dirty(tool); + var pryComp = EnsureComp(target); + pryComp.SpeedModifier = 0.75f; + pryComp.PryPowered = true; + pryComp.Force = true; + + Dirty(target, pryComp); } Dirty(melee); @@ -232,7 +234,7 @@ namespace Content.Server.Zombies else { var htn = EnsureComp(target); - htn.RootTask = new HTNCompoundTask() {Task = "SimpleHostileCompound"}; + htn.RootTask = new HTNCompoundTask() { Task = "SimpleHostileCompound" }; htn.Blackboard.SetValue(NPCBlackboard.Owner, target); _npc.WakeNPC(target, htn); } diff --git a/Content.Shared/Doors/Components/DoorComponent.cs b/Content.Shared/Doors/Components/DoorComponent.cs index 567afa0770..7cfcba8c5b 100644 --- a/Content.Shared/Doors/Components/DoorComponent.cs +++ b/Content.Shared/Doors/Components/DoorComponent.cs @@ -249,7 +249,7 @@ public sealed partial class DoorComponent : Component } var curTime = IoCManager.Resolve().CurTime; - return (float) (NextStateChange.Value - curTime).TotalSeconds; + return (float)(NextStateChange.Value - curTime).TotalSeconds; } set { @@ -299,10 +299,10 @@ public sealed partial class DoorComponent : Component public bool ClickOpen = true; [DataField("openDrawDepth", customTypeSerializer: typeof(ConstantSerializer))] - public int OpenDrawDepth = (int) DrawDepth.DrawDepth.Doors; + public int OpenDrawDepth = (int)DrawDepth.DrawDepth.Doors; [DataField("closedDrawDepth", customTypeSerializer: typeof(ConstantSerializer))] - public int ClosedDrawDepth = (int) DrawDepth.DrawDepth.Doors; + public int ClosedDrawDepth = (int)DrawDepth.DrawDepth.Doors; } [Serializable, NetSerializable] diff --git a/Content.Shared/Doors/DoorEvents.cs b/Content.Shared/Doors/DoorEvents.cs index 5b0ca71ede..08a2c8b18b 100644 --- a/Content.Shared/Doors/DoorEvents.cs +++ b/Content.Shared/Doors/DoorEvents.cs @@ -62,35 +62,4 @@ namespace Content.Shared.Doors public sealed class BeforeDoorAutoCloseEvent : CancellableEntityEventArgs { } - - /// - /// Raised to determine how long the door's pry time should be modified by. - /// Multiply PryTimeModifier by the desired amount. - /// - public sealed class DoorGetPryTimeModifierEvent : EntityEventArgs - { - public readonly EntityUid User; - public float PryTimeModifier = 1.0f; - - public DoorGetPryTimeModifierEvent(EntityUid user) - { - User = user; - } - } - - /// - /// Raised when an attempt to pry open the door is made. - /// Cancel to stop the door from being pried open. - /// - public sealed class BeforeDoorPryEvent : CancellableEntityEventArgs - { - public readonly EntityUid User; - public readonly EntityUid Tool; - - public BeforeDoorPryEvent(EntityUid user, EntityUid tool) - { - User = user; - Tool = tool; - } - } } diff --git a/Content.Shared/Doors/Systems/SharedDoorBoltSystem.cs b/Content.Shared/Doors/Systems/SharedDoorBoltSystem.cs index e8be596b06..1deb6e3f7c 100644 --- a/Content.Shared/Doors/Systems/SharedDoorBoltSystem.cs +++ b/Content.Shared/Doors/Systems/SharedDoorBoltSystem.cs @@ -1,5 +1,6 @@ using Content.Shared.Doors.Components; using Content.Shared.Popups; +using Content.Shared.Prying.Components; namespace Content.Shared.Doors.Systems; @@ -16,16 +17,16 @@ public abstract class SharedDoorBoltSystem : EntitySystem SubscribeLocalEvent(OnBeforeDoorOpened); SubscribeLocalEvent(OnBeforeDoorClosed); SubscribeLocalEvent(OnBeforeDoorDenied); - SubscribeLocalEvent(OnDoorPry); + SubscribeLocalEvent(OnDoorPry); } - private void OnDoorPry(EntityUid uid, DoorBoltComponent component, BeforeDoorPryEvent args) + private void OnDoorPry(EntityUid uid, DoorBoltComponent component, ref BeforePryEvent args) { - if (component.BoltsDown) + if (component.BoltsDown && !args.Force) { - Popup.PopupEntity(Loc.GetString("airlock-component-cannot-pry-is-bolted-message"), uid, args.User); - args.Cancel(); + Popup.PopupClient(Loc.GetString("airlock-component-cannot-pry-is-bolted-message"), uid, args.User); + args.Cancelled = true; } } diff --git a/Content.Shared/Doors/Systems/SharedDoorSystem.cs b/Content.Shared/Doors/Systems/SharedDoorSystem.cs index 3fc912deba..e551517149 100644 --- a/Content.Shared/Doors/Systems/SharedDoorSystem.cs +++ b/Content.Shared/Doors/Systems/SharedDoorSystem.cs @@ -16,6 +16,7 @@ using Robust.Shared.Physics.Events; using Robust.Shared.Physics.Systems; using Robust.Shared.Serialization; using Robust.Shared.Timing; +using Content.Shared.Prying.Components; namespace Content.Shared.Doors.Systems; @@ -23,14 +24,14 @@ public abstract partial class SharedDoorSystem : EntitySystem { [Dependency] protected readonly IGameTiming GameTiming = default!; [Dependency] protected readonly SharedPhysicsSystem PhysicsSystem = default!; - [Dependency] private readonly DamageableSystem _damageableSystem = default!; - [Dependency] private readonly SharedStunSystem _stunSystem = default!; + [Dependency] private readonly DamageableSystem _damageableSystem = default!; + [Dependency] private readonly SharedStunSystem _stunSystem = default!; [Dependency] protected readonly TagSystem Tags = default!; [Dependency] protected readonly SharedAudioSystem Audio = default!; - [Dependency] private readonly EntityLookupSystem _entityLookup = default!; + [Dependency] private readonly EntityLookupSystem _entityLookup = default!; [Dependency] protected readonly SharedAppearanceSystem AppearanceSystem = default!; - [Dependency] private readonly OccluderSystem _occluder = default!; - [Dependency] private readonly AccessReaderSystem _accessReaderSystem = default!; + [Dependency] private readonly OccluderSystem _occluder = default!; + [Dependency] private readonly AccessReaderSystem _accessReaderSystem = default!; /// /// A body must have an intersection percentage larger than this in order to be considered as colliding with a @@ -61,6 +62,8 @@ public abstract partial class SharedDoorSystem : EntitySystem SubscribeLocalEvent(HandleCollide); SubscribeLocalEvent(PreventCollision); + SubscribeLocalEvent(OnPryTimeModifier); + } protected virtual void OnComponentInit(EntityUid uid, DoorComponent door, ComponentInit args) @@ -182,6 +185,11 @@ public abstract partial class SharedDoorSystem : EntitySystem args.Handled = true; } + private void OnPryTimeModifier(EntityUid uid, DoorComponent door, ref GetPryTimeModifierEvent args) + { + args.BaseTime = door.PryTime; + } + /// /// Update the door state/visuals and play an access denied sound when a user without access interacts with the /// door. @@ -206,6 +214,7 @@ public abstract partial class SharedDoorSystem : EntitySystem PlaySound(uid, door.DenySound, AudioParams.Default.WithVolume(-3), user, predicted); } + public bool TryToggleDoor(EntityUid uid, DoorComponent? door = null, EntityUid? user = null, bool predicted = false) { if (!Resolve(uid, ref door)) @@ -246,7 +255,7 @@ public abstract partial class SharedDoorSystem : EntitySystem if (door.State == DoorState.Welded) return false; - var ev = new BeforeDoorOpenedEvent(){User=user}; + var ev = new BeforeDoorOpenedEvent() { User = user }; RaiseLocalEvent(uid, ev, false); if (ev.Cancelled) return false; @@ -261,6 +270,14 @@ public abstract partial class SharedDoorSystem : EntitySystem return true; } + /// + /// Immediately start opening a door + /// + /// The uid of the door + /// The doorcomponent of the door + /// The user (if any) opening the door + /// Whether the interaction would have been + /// predicted. See comments in the PlaySound method on the Server system for details public virtual void StartOpening(EntityUid uid, DoorComponent? door = null, EntityUid? user = null, bool predicted = false) { if (!Resolve(uid, ref door)) @@ -309,6 +326,14 @@ public abstract partial class SharedDoorSystem : EntitySystem return true; } + /// + /// Immediately start closing a door + /// + /// The uid of the door + /// The doorcomponent of the door + /// The user (if any) opening the door + /// Whether the interaction would have been + /// predicted. See comments in the PlaySound method on the Server system for details public bool CanClose(EntityUid uid, DoorComponent? door = null, EntityUid? user = null, bool quiet = true) { if (!Resolve(uid, ref door)) @@ -444,11 +469,11 @@ public abstract partial class SharedDoorSystem : EntitySystem //TODO: Make only shutters ignore these objects upon colliding instead of all airlocks // Excludes Glasslayer for windows, GlassAirlockLayer for windoors, TableLayer for tables - if (!otherPhysics.CanCollide || otherPhysics.CollisionLayer == (int) CollisionGroup.GlassLayer || otherPhysics.CollisionLayer == (int) CollisionGroup.GlassAirlockLayer || otherPhysics.CollisionLayer == (int) CollisionGroup.TableLayer) + if (!otherPhysics.CanCollide || otherPhysics.CollisionLayer == (int)CollisionGroup.GlassLayer || otherPhysics.CollisionLayer == (int)CollisionGroup.GlassAirlockLayer || otherPhysics.CollisionLayer == (int)CollisionGroup.TableLayer) continue; //If the colliding entity is a slippable item ignore it by the airlock - if (otherPhysics.CollisionLayer == (int) CollisionGroup.SlipLayer && otherPhysics.CollisionMask == (int) CollisionGroup.ItemMask) + if (otherPhysics.CollisionLayer == (int)CollisionGroup.SlipLayer && otherPhysics.CollisionMask == (int)CollisionGroup.ItemMask) continue; if ((physics.CollisionMask & otherPhysics.CollisionLayer) == 0 && (otherPhysics.CollisionMask & physics.CollisionLayer) == 0) @@ -598,7 +623,7 @@ public abstract partial class SharedDoorSystem : EntitySystem } } - protected virtual void CheckDoorBump(DoorComponent component, PhysicsComponent body) {} + protected virtual void CheckDoorBump(DoorComponent component, PhysicsComponent body) { } /// /// Makes a door proceed to the next state (if applicable). @@ -659,9 +684,4 @@ public abstract partial class SharedDoorSystem : EntitySystem #endregion protected abstract void PlaySound(EntityUid uid, SoundSpecifier soundSpecifier, AudioParams audioParams, EntityUid? predictingPlayer, bool predicted); - - [Serializable, NetSerializable] - protected sealed partial class DoorPryDoAfterEvent : SimpleDoAfterEvent - { - } } diff --git a/Content.Shared/Prying/Components/PryUnpoweredComponent.cs b/Content.Shared/Prying/Components/PryUnpoweredComponent.cs new file mode 100644 index 0000000000..f0e61dc968 --- /dev/null +++ b/Content.Shared/Prying/Components/PryUnpoweredComponent.cs @@ -0,0 +1,11 @@ +using Robust.Shared.GameStates; + +namespace Content.Shared.Prying.Components; + +/// +/// Applied to entities that can be pried open without tools while unpowered +/// +[RegisterComponent, NetworkedComponent] +public sealed partial class PryUnpoweredComponent : Component +{ +} diff --git a/Content.Shared/Prying/Components/PryingComponent.cs b/Content.Shared/Prying/Components/PryingComponent.cs new file mode 100644 index 0000000000..4442481dce --- /dev/null +++ b/Content.Shared/Prying/Components/PryingComponent.cs @@ -0,0 +1,82 @@ +using Robust.Shared.Audio; +using Robust.Shared.GameStates; + +namespace Content.Shared.Prying.Components; + +[RegisterComponent, NetworkedComponent] +public sealed partial class PryingComponent : Component +{ + /// + /// Whether the entity can pry open powered doors + /// + [DataField("pryPowered")] + public bool PryPowered = false; + + /// + /// Whether the tool can bypass certain restrictions when prying. + /// For example door bolts. + /// + [DataField("force")] + public bool Force = false; + /// + /// Modifier on the prying time. + /// Lower values result in more time. + /// + [DataField("speedModifier")] + public float SpeedModifier = 1.0f; + + /// + /// What sound to play when prying is finished. + /// + [DataField("useSound")] + public SoundSpecifier UseSound = new SoundPathSpecifier("/Audio/Items/crowbar.ogg"); + + /// + /// Whether the entity can currently pry things. + /// + [DataField("enabled")] + public bool Enabled = true; +} + +/// +/// Raised directed on an entity before prying it. +/// Cancel to stop the entity from being pried open. +/// +[ByRefEvent] +public record struct BeforePryEvent(EntityUid User, bool PryPowered, bool Force) +{ + public readonly EntityUid User = User; + + public readonly bool PryPowered = PryPowered; + + public readonly bool Force = Force; + + public bool Cancelled; +} + +/// +/// Raised directed on an entity that has been pried. +/// +[ByRefEvent] +public readonly record struct PriedEvent(EntityUid User) +{ + public readonly EntityUid User = User; +} + +/// +/// Raised to determine how long the door's pry time should be modified by. +/// Multiply PryTimeModifier by the desired amount. +/// +[ByRefEvent] +public record struct GetPryTimeModifierEvent +{ + public readonly EntityUid User; + public float PryTimeModifier = 1.0f; + public float BaseTime = 5.0f; + + public GetPryTimeModifierEvent(EntityUid user) + { + User = user; + } +} + diff --git a/Content.Shared/Prying/Systems/PryingSystem.cs b/Content.Shared/Prying/Systems/PryingSystem.cs new file mode 100644 index 0000000000..3bb3a9bc9b --- /dev/null +++ b/Content.Shared/Prying/Systems/PryingSystem.cs @@ -0,0 +1,162 @@ +using Content.Shared.Prying.Components; +using Content.Shared.Verbs; +using Content.Shared.DoAfter; +using Robust.Shared.Serialization; +using Content.Shared.Administration.Logs; +using Content.Shared.Database; +using Content.Shared.Doors.Components; +using System.Diagnostics.CodeAnalysis; +using Content.Shared.Interaction; +using PryUnpoweredComponent = Content.Shared.Prying.Components.PryUnpoweredComponent; + +namespace Content.Shared.Prying.Systems; + +/// +/// Handles prying of entities (e.g. doors) +/// +public sealed class PryingSystem : EntitySystem +{ + [Dependency] private readonly ISharedAdminLogManager _adminLog = default!; + [Dependency] private readonly SharedDoAfterSystem _doAfterSystem = default!; + [Dependency] private readonly SharedAudioSystem _audioSystem = default!; + + public override void Initialize() + { + base.Initialize(); + + // Mob prying doors + SubscribeLocalEvent>(OnDoorAltVerb); + SubscribeLocalEvent(OnDoAfter); + SubscribeLocalEvent(TryPryDoor); + } + + private void TryPryDoor(EntityUid uid, DoorComponent comp, InteractUsingEvent args) + { + if (args.Handled) + return; + + args.Handled = TryPry(uid, args.User, out _, args.Used); + } + + private void OnDoorAltVerb(EntityUid uid, DoorComponent component, GetVerbsEvent args) + { + if (!args.CanInteract || !args.CanAccess) + return; + + if (!TryComp(args.User, out var tool)) + return; + + args.Verbs.Add(new AlternativeVerb() + { + Text = Loc.GetString("door-pry"), + Impact = LogImpact.Low, + Act = () => TryPry(uid, args.User, out _, args.User), + }); + } + + /// + /// Attempt to pry an entity. + /// + public bool TryPry(EntityUid target, EntityUid user, out DoAfterId? id, EntityUid tool) + { + id = null; + + PryingComponent? comp = null; + if (!Resolve(tool, ref comp, false)) + return false; + + if (!comp.Enabled) + return false; + + if (!CanPry(target, user, comp)) + { + // If we have reached this point we want the event that caused this + // to be marked as handled as a popup would be generated on failure. + return true; + } + + StartPry(target, user, tool, comp.SpeedModifier, out id); + + return true; + } + + /// + /// Try to pry an entity. + /// + public bool TryPry(EntityUid target, EntityUid user, out DoAfterId? id) + { + id = null; + + if (!CanPry(target, user)) + // If we have reached this point we want the event that caused this + // to be marked as handled as a popup would be generated on failure. + return true; + + return StartPry(target, user, null, 1.0f, out id); + } + + private bool CanPry(EntityUid target, EntityUid user, PryingComponent? comp = null) + { + BeforePryEvent canev; + + if (comp != null) + { + canev = new BeforePryEvent(user, comp.PryPowered, comp.Force); + } + else + { + if (!TryComp(target, out _)) + return false; + canev = new BeforePryEvent(user, false, false); + } + + RaiseLocalEvent(target, ref canev); + + if (canev.Cancelled) + return false; + return true; + } + + private bool StartPry(EntityUid target, EntityUid user, EntityUid? tool, float toolModifier, [NotNullWhen(true)] out DoAfterId? id) + { + var modEv = new GetPryTimeModifierEvent(user); + + RaiseLocalEvent(target, ref modEv); + var doAfterArgs = new DoAfterArgs(EntityManager, user, TimeSpan.FromSeconds(modEv.BaseTime * modEv.PryTimeModifier / toolModifier), new DoorPryDoAfterEvent(), target, target, tool) + { + BreakOnDamage = true, + BreakOnUserMove = true, + }; + + if (tool != null) + { + _adminLog.Add(LogType.Action, LogImpact.Low, $"{ToPrettyString(user)} is using {ToPrettyString(tool.Value)} to pry {ToPrettyString(target)}"); + } + else + { + _adminLog.Add(LogType.Action, LogImpact.Low, $"{ToPrettyString(user)} is prying {ToPrettyString(target)}"); + } + return _doAfterSystem.TryStartDoAfter(doAfterArgs, out id); + } + + private void OnDoAfter(EntityUid uid, DoorComponent door, DoorPryDoAfterEvent args) + { + if (args.Cancelled) + return; + if (args.Target is null) + return; + + PryingComponent? comp = null; + + if (args.Used != null && Resolve(args.Used.Value, ref comp)) + _audioSystem.PlayPredicted(comp.UseSound, args.Used.Value, args.User); + + var ev = new PriedEvent(args.User); + RaiseLocalEvent(uid, ref ev); + } +} + +[Serializable, NetSerializable] +public sealed partial class DoorPryDoAfterEvent : SimpleDoAfterEvent +{ +} diff --git a/Content.Shared/Tools/Systems/SharedToolSystem.MultipleTool.cs b/Content.Shared/Tools/Systems/SharedToolSystem.MultipleTool.cs index b198f6d779..d528c1be7d 100644 --- a/Content.Shared/Tools/Systems/SharedToolSystem.MultipleTool.cs +++ b/Content.Shared/Tools/Systems/SharedToolSystem.MultipleTool.cs @@ -2,6 +2,7 @@ using System.Linq; using Content.Shared.Interaction; using Content.Shared.Tools.Components; using Robust.Shared.GameStates; +using Content.Shared.Prying.Components; namespace Content.Shared.Tools; @@ -27,7 +28,7 @@ public abstract partial class SharedToolSystem : EntitySystem private void OnMultipleToolStartup(EntityUid uid, MultipleToolComponent multiple, ComponentStartup args) { // Only set the multiple tool if we have a tool component. - if(EntityManager.TryGetComponent(uid, out ToolComponent? tool)) + if (EntityManager.TryGetComponent(uid, out ToolComponent? tool)) SetMultipleTool(uid, multiple, tool); } @@ -52,7 +53,7 @@ public abstract partial class SharedToolSystem : EntitySystem if (multiple.Entries.Length == 0) return false; - multiple.CurrentEntry = (uint) ((multiple.CurrentEntry + 1) % multiple.Entries.Length); + multiple.CurrentEntry = (uint)((multiple.CurrentEntry + 1) % multiple.Entries.Length); SetMultipleTool(uid, multiple, playSound: true, user: user); return true; @@ -79,6 +80,19 @@ public abstract partial class SharedToolSystem : EntitySystem tool.UseSound = current.Sound; tool.Qualities = current.Behavior; + // TODO: Replace this with a better solution later + if (TryComp(uid, out var pcomp)) + { + if (current.Behavior.Contains("Prying")) + { + pcomp.Enabled = true; + } + else + { + pcomp.Enabled = false; + } + } + if (playSound && current.ChangeSound != null) _audioSystem.PlayPredicted(current.ChangeSound, uid, user); diff --git a/Resources/Prototypes/Entities/Debugging/spanisharmyknife.yml b/Resources/Prototypes/Entities/Debugging/spanisharmyknife.yml index 66e2b35ad8..94464f2535 100644 --- a/Resources/Prototypes/Entities/Debugging/spanisharmyknife.yml +++ b/Resources/Prototypes/Entities/Debugging/spanisharmyknife.yml @@ -24,6 +24,7 @@ - type: Tool qualities: - Prying + - type: Prying - type: MultipleTool statusShowBehavior: true entries: diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/xeno.yml b/Resources/Prototypes/Entities/Mobs/NPCs/xeno.yml index 47ea55278f..9831d20e27 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/xeno.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/xeno.yml @@ -25,6 +25,11 @@ speed: 1.5 qualities: - Prying + - type: Prying + pryPowered: !type:Bool + true + force: !type:Bool + true useSound: path: /Audio/Items/crowbar.ogg - type: Reactive diff --git a/Resources/Prototypes/Entities/Objects/Tools/cowtools.yml b/Resources/Prototypes/Entities/Objects/Tools/cowtools.yml index d500ef141c..24eb0b02b2 100644 --- a/Resources/Prototypes/Entities/Objects/Tools/cowtools.yml +++ b/Resources/Prototypes/Entities/Objects/Tools/cowtools.yml @@ -1,4 +1,4 @@ -- type: entity +- type: entity name: haycutters parent: BaseItem id: Haycutters @@ -98,6 +98,7 @@ path: /Audio/Items/crowbar.ogg speed: 0.05 - type: TilePrying + - type: Prying - type: entity name: mooltitool diff --git a/Resources/Prototypes/Entities/Objects/Tools/jaws_of_life.yml b/Resources/Prototypes/Entities/Objects/Tools/jaws_of_life.yml index a7f74a542f..36f96f61af 100644 --- a/Resources/Prototypes/Entities/Objects/Tools/jaws_of_life.yml +++ b/Resources/Prototypes/Entities/Objects/Tools/jaws_of_life.yml @@ -24,6 +24,12 @@ - Prying speed: 1.5 useSound: /Audio/Items/jaws_pry.ogg + - type: Prying + pryPowered: !type:Bool + true + force: !type:Bool + true + useSound: /Audio/Items/jaws_pry.ogg - type: ToolForcePowered - type: MultipleTool statusShowBehavior: true @@ -77,4 +83,4 @@ - type: MeleeWeapon damage: types: - Blunt: 14 \ No newline at end of file + Blunt: 14 diff --git a/Resources/Prototypes/Entities/Objects/Tools/tools.yml b/Resources/Prototypes/Entities/Objects/Tools/tools.yml index 3d58b2b079..82bef616de 100644 --- a/Resources/Prototypes/Entities/Objects/Tools/tools.yml +++ b/Resources/Prototypes/Entities/Objects/Tools/tools.yml @@ -155,6 +155,7 @@ Steel: 100 - type: StaticPrice price: 22 + - type: Prying - type: entity parent: Crowbar diff --git a/Resources/Prototypes/Entities/Objects/Weapons/Melee/armblade.yml b/Resources/Prototypes/Entities/Objects/Weapons/Melee/armblade.yml index cb6f862745..764683183d 100644 --- a/Resources/Prototypes/Entities/Objects/Weapons/Melee/armblade.yml +++ b/Resources/Prototypes/Entities/Objects/Weapons/Melee/armblade.yml @@ -20,3 +20,4 @@ - type: Tool qualities: - Prying + - type: Prying diff --git a/Resources/Prototypes/Entities/Objects/Weapons/Melee/fireaxe.yml b/Resources/Prototypes/Entities/Objects/Weapons/Melee/fireaxe.yml index 9e747328f9..fcc2129a51 100644 --- a/Resources/Prototypes/Entities/Objects/Weapons/Melee/fireaxe.yml +++ b/Resources/Prototypes/Entities/Objects/Weapons/Melee/fireaxe.yml @@ -38,6 +38,7 @@ - Prying - type: TilePrying advanced: true + - type: Prying - type: entity id: FireAxeFlaming diff --git a/Resources/Prototypes/Entities/Objects/Weapons/Melee/mining.yml b/Resources/Prototypes/Entities/Objects/Weapons/Melee/mining.yml index d182d9a00e..3073ed7ab5 100644 --- a/Resources/Prototypes/Entities/Objects/Weapons/Melee/mining.yml +++ b/Resources/Prototypes/Entities/Objects/Weapons/Melee/mining.yml @@ -61,6 +61,7 @@ - type: Tool qualities: - Prying + - type: Prying - type: entity name: crusher dagger diff --git a/Resources/Prototypes/Entities/Structures/Doors/Airlocks/easy_pry.yml b/Resources/Prototypes/Entities/Structures/Doors/Airlocks/easy_pry.yml new file mode 100644 index 0000000000..04a58eebe0 --- /dev/null +++ b/Resources/Prototypes/Entities/Structures/Doors/Airlocks/easy_pry.yml @@ -0,0 +1,63 @@ +- type: entity + parent: AirlockExternal + id: AirlockExternalEasyPry + suffix: External, EasyPry + description: It opens, it closes, it might crush you, and there might be only space behind it. Has to be manually activated. Has a valve labelled "TURN TO OPEN" + components: + - type: PryUnpowered + +- type: entity + parent: AirlockExternalGlass + id: AirlockExternalGlassEasyPry + suffix: External, Glass, EasyPry + description: It opens, it closes, it might crush you, and there might be only space behind it. Has to be manually activated. Has a valve labelled "TURN TO OPEN" + components: + - type: PryUnpowered + +- type: entity + parent: AirlockGlassShuttle + id: AirlockGlassShuttleEasyPry + suffix: EasyPry, Docking + description: Necessary for connecting two space craft together. Has a valve labelled "TURN TO OPEN" + components: + - type: PryUnpowered + +- type: entity + parent: AirlockShuttle + id: AirlockShuttleEasyPry + suffix: EasyPry, Docking + description: Necessary for connecting two space craft together. Has a valve labelled "TURN TO OPEN" + components: + - type: PryUnpowered + +- type: entity + parent: AirlockExternalLocked + id: AirlockExternalEasyPryLocked + suffix: External, EasyPry, Locked + description: It opens, it closes, it might crush you, and there might be only space behind it. Has to be manually activated. Has a valve labelled "TURN TO OPEN" + components: + - type: PryUnpowered + +- type: entity + parent: AirlockExternalGlassLocked + id: AirlockExternalGlassEasyPryLocked + suffix: External, Glass, EasyPry, Locked + description: It opens, it closes, it might crush you, and there might be only space behind it. Has to be manually activated. Has a valve labelled "TURN TO OPEN" + components: + - type: PryUnpowered + +- type: entity + parent: AirlockExternalGlassShuttleLocked + id: AirlockGlassShuttleEasyPryLocked + suffix: EasyPry, Docking, Locked + description: Necessary for connecting two space craft together. Has a valve labelled "TURN TO OPEN" + components: + - type: PryUnpowered + +- type: entity + parent: AirlockExternalShuttleLocked + id: AirlockShuttleEasyPryLocked + suffix: EasyPry, Docking, Locked + description: Necessary for connecting two space craft together. Has a valve labelled "TURN TO OPEN" + components: + - type: PryUnpowered