From d6d2aebf7b0807921b70d32cb492e2396803c73e Mon Sep 17 00:00:00 2001 From: Leon Friedrich <60421075+ElectroJr@users.noreply.github.com> Date: Thu, 20 Jun 2024 03:14:18 +1200 Subject: [PATCH] Partial buckling refactor (#29031) * partial buckling refactor * git mv test * change test namespace * git mv test * Update test namespace * Add pulling test * Network BuckleTime * Add two more tests * smelly --- Content.Client/Buckle/BuckleSystem.cs | 66 ++- .../Tests/Buckle/BuckleDragTest.cs | 56 ++ .../Tests/Buckle/BuckleTest.cs | 20 +- .../Tests/Climbing/ClimbingTest.cs | 1 + .../Construction/Interaction/CraftingTests.cs | 56 +- .../Interaction/InteractionTest.Helpers.cs | 20 +- .../Tests/Interaction/InteractionTest.cs | 8 +- .../Tests/Movement/BuckleMovementTest.cs | 63 ++ .../{Interaction => Movement}/MovementTest.cs | 3 +- .../Tests/Movement/PullingTest.cs | 73 +++ .../{Slipping => Movement}/SlippingTest.cs | 6 +- Content.Server/Bed/BedSystem.cs | 49 +- .../Bed/Components/HealOnBuckleComponent.cs | 21 +- .../Bed/Components/HealOnBuckleHealing.cs | 1 + .../Bed/Components/StasisBedComponent.cs | 3 +- .../Body/Systems/BloodstreamSystem.cs | 3 + .../Body/Systems/MetabolizerSystem.cs | 6 + .../Body/Systems/RespiratorSystem.cs | 3 + .../Operators/Combat/UnbuckleOperator.cs | 7 +- .../Buckle/Components/BuckleComponent.cs | 113 +++- .../Buckle/Components/StrapComponent.cs | 50 +- .../Buckle/SharedBuckleSystem.Buckle.cs | 558 +++++++++--------- .../Buckle/SharedBuckleSystem.Interaction.cs | 171 ++++++ .../Buckle/SharedBuckleSystem.Strap.cs | 245 +------- Content.Shared/Buckle/SharedBuckleSystem.cs | 49 +- .../Climbing/Systems/ClimbSystem.cs | 6 +- Content.Shared/Cuffs/SharedCuffableSystem.cs | 27 +- Content.Shared/Foldable/FoldableSystem.cs | 6 +- .../Interaction/RotateToFaceSystem.cs | 32 +- .../Systems/MobStateSystem.Subscribers.cs | 14 +- .../Pulling/Events/PullStartedMessage.cs | 11 +- .../Pulling/Events/PullStoppedMessage.cs | 13 +- .../Movement/Pulling/Systems/PullingSystem.cs | 58 +- .../Entities/Structures/Furniture/chairs.yml | 2 + .../Structures/Furniture/rollerbeds.yml | 6 + 35 files changed, 993 insertions(+), 833 deletions(-) create mode 100644 Content.IntegrationTests/Tests/Buckle/BuckleDragTest.cs create mode 100644 Content.IntegrationTests/Tests/Movement/BuckleMovementTest.cs rename Content.IntegrationTests/Tests/{Interaction => Movement}/MovementTest.cs (95%) create mode 100644 Content.IntegrationTests/Tests/Movement/PullingTest.cs rename Content.IntegrationTests/Tests/{Slipping => Movement}/SlippingTest.cs (92%) create mode 100644 Content.Shared/Buckle/SharedBuckleSystem.Interaction.cs diff --git a/Content.Client/Buckle/BuckleSystem.cs b/Content.Client/Buckle/BuckleSystem.cs index d4614210d9f..4429996aca3 100644 --- a/Content.Client/Buckle/BuckleSystem.cs +++ b/Content.Client/Buckle/BuckleSystem.cs @@ -3,6 +3,7 @@ using Content.Shared.Buckle.Components; using Content.Shared.Rotation; using Robust.Client.GameObjects; +using Robust.Shared.GameStates; namespace Content.Client.Buckle; @@ -14,40 +15,63 @@ public override void Initialize() { base.Initialize(); - SubscribeLocalEvent(OnBuckleAfterAutoHandleState); + SubscribeLocalEvent(OnHandleState); SubscribeLocalEvent(OnAppearanceChange); + SubscribeLocalEvent(OnStrapMoveEvent); } - private void OnBuckleAfterAutoHandleState(EntityUid uid, BuckleComponent component, ref AfterAutoHandleStateEvent args) + private void OnStrapMoveEvent(EntityUid uid, StrapComponent component, ref MoveEvent args) { - ActionBlocker.UpdateCanMove(uid); + // I'm moving this to the client-side system, but for the sake of posterity let's keep this comment: + // > This is mega cursed. Please somebody save me from Mr Buckle's wild ride - if (!TryComp(uid, out var ownerSprite)) + // The nice thing is its still true, this is quite cursed, though maybe not omega cursed anymore. + // This code is garbage, it doesn't work with rotated viewports. I need to finally get around to reworking + // sprite rendering for entity layers & direction dependent sorting. + + if (args.NewRotation == args.OldRotation) return; - // Adjust draw depth when the chair faces north so that the seat back is drawn over the player. - // Reset the draw depth when rotated in any other direction. - // TODO when ECSing, make this a visualizer - // This code was written before rotatable viewports were introduced, so hard-coding Direction.North - // and comparing it against LocalRotation now breaks this in other rotations. This is a FIXME, but - // better to get it working for most people before we look at a more permanent solution. - if (component is { Buckled: true, LastEntityBuckledTo: { } } && - Transform(component.LastEntityBuckledTo.Value).LocalRotation.GetCardinalDir() == Direction.North && - TryComp(component.LastEntityBuckledTo, out var buckledSprite)) - { - component.OriginalDrawDepth ??= ownerSprite.DrawDepth; - ownerSprite.DrawDepth = buckledSprite.DrawDepth - 1; + if (!TryComp(uid, out var strapSprite)) return; - } - // If here, we're not turning north and should restore the saved draw depth. - if (component.OriginalDrawDepth.HasValue) + var isNorth = Transform(uid).LocalRotation.GetCardinalDir() == Direction.North; + foreach (var buckledEntity in component.BuckledEntities) { - ownerSprite.DrawDepth = component.OriginalDrawDepth.Value; - component.OriginalDrawDepth = null; + if (!TryComp(buckledEntity, out var buckle)) + continue; + + if (!TryComp(buckledEntity, out var buckledSprite)) + continue; + + if (isNorth) + { + buckle.OriginalDrawDepth ??= buckledSprite.DrawDepth; + buckledSprite.DrawDepth = strapSprite.DrawDepth - 1; + } + else if (buckle.OriginalDrawDepth.HasValue) + { + buckledSprite.DrawDepth = buckle.OriginalDrawDepth.Value; + buckle.OriginalDrawDepth = null; + } } } + private void OnHandleState(Entity ent, ref ComponentHandleState args) + { + if (args.Current is not BuckleState state) + return; + + ent.Comp.DontCollide = state.DontCollide; + ent.Comp.BuckleTime = state.BuckleTime; + var strapUid = EnsureEntity(state.BuckledTo, ent); + + SetBuckledTo(ent, strapUid == null ? null : new (strapUid.Value, null)); + + var (uid, component) = ent; + + } + private void OnAppearanceChange(EntityUid uid, BuckleComponent component, ref AppearanceChangeEvent args) { if (!TryComp(uid, out var rotVisuals) diff --git a/Content.IntegrationTests/Tests/Buckle/BuckleDragTest.cs b/Content.IntegrationTests/Tests/Buckle/BuckleDragTest.cs new file mode 100644 index 00000000000..8df151d5a0e --- /dev/null +++ b/Content.IntegrationTests/Tests/Buckle/BuckleDragTest.cs @@ -0,0 +1,56 @@ +using Content.IntegrationTests.Tests.Interaction; +using Content.Shared.Buckle; +using Content.Shared.Buckle.Components; +using Content.Shared.Input; +using Content.Shared.Movement.Pulling.Components; + +namespace Content.IntegrationTests.Tests.Buckle; + +public sealed class BuckleDragTest : InteractionTest +{ + // Check that dragging a buckled player unbuckles them. + [Test] + public async Task BucklePullTest() + { + var urist = await SpawnTarget("MobHuman"); + var sUrist = ToServer(urist); + await SpawnTarget("Chair"); + + var buckle = Comp(urist); + var strap = Comp(Target); + var puller = Comp(Player); + var pullable = Comp(urist); + +#pragma warning disable RA0002 + buckle.Delay = TimeSpan.Zero; +#pragma warning restore RA0002 + + // Initially not buckled to the chair and not pulling anything + Assert.That(buckle.Buckled, Is.False); + Assert.That(buckle.BuckledTo, Is.Null); + Assert.That(strap.BuckledEntities, Is.Empty); + Assert.That(puller.Pulling, Is.Null); + Assert.That(pullable.Puller, Is.Null); + Assert.That(pullable.BeingPulled, Is.False); + + // Strap the human to the chair + Assert.That(Server.System().TryBuckle(sUrist, SPlayer, STarget.Value)); + await RunTicks(5); + Assert.That(buckle.Buckled, Is.True); + Assert.That(buckle.BuckledTo, Is.EqualTo(STarget)); + Assert.That(strap.BuckledEntities, Is.EquivalentTo(new[]{sUrist})); + Assert.That(puller.Pulling, Is.Null); + Assert.That(pullable.Puller, Is.Null); + Assert.That(pullable.BeingPulled, Is.False); + + // Start pulling, and thus unbuckle them + await PressKey(ContentKeyFunctions.TryPullObject, cursorEntity:urist); + await RunTicks(5); + Assert.That(buckle.Buckled, Is.False); + Assert.That(buckle.BuckledTo, Is.Null); + Assert.That(strap.BuckledEntities, Is.Empty); + Assert.That(puller.Pulling, Is.EqualTo(sUrist)); + Assert.That(pullable.Puller, Is.EqualTo(SPlayer)); + Assert.That(pullable.BeingPulled, Is.True); + } +} diff --git a/Content.IntegrationTests/Tests/Buckle/BuckleTest.cs b/Content.IntegrationTests/Tests/Buckle/BuckleTest.cs index 7c700d9fb8a..5681013d7b0 100644 --- a/Content.IntegrationTests/Tests/Buckle/BuckleTest.cs +++ b/Content.IntegrationTests/Tests/Buckle/BuckleTest.cs @@ -90,7 +90,6 @@ await server.WaitAssertion(() => { Assert.That(strap, Is.Not.Null); Assert.That(strap.BuckledEntities, Is.Empty); - Assert.That(strap.OccupiedSize, Is.Zero); }); // Side effects of buckling @@ -110,8 +109,6 @@ await server.WaitAssertion(() => // Side effects of buckling for the strap Assert.That(strap.BuckledEntities, Does.Contain(human)); - Assert.That(strap.OccupiedSize, Is.EqualTo(buckle.Size)); - Assert.That(strap.OccupiedSize, Is.Positive); }); #pragma warning disable NUnit2045 // Interdependent asserts. @@ -121,7 +118,7 @@ await server.WaitAssertion(() => // Trying to unbuckle too quickly fails Assert.That(buckleSystem.TryUnbuckle(human, human, buckleComp: buckle), Is.False); Assert.That(buckle.Buckled); - Assert.That(buckleSystem.ToggleBuckle(human, human, chair, buckle: buckle), Is.False); + Assert.That(buckleSystem.TryUnbuckle(human, human), Is.False); Assert.That(buckle.Buckled); #pragma warning restore NUnit2045 }); @@ -148,7 +145,6 @@ await server.WaitAssertion(() => // Unbuckle, strap Assert.That(strap.BuckledEntities, Is.Empty); - Assert.That(strap.OccupiedSize, Is.Zero); }); #pragma warning disable NUnit2045 // Interdependent asserts. @@ -159,9 +155,9 @@ await server.WaitAssertion(() => // On cooldown Assert.That(buckleSystem.TryUnbuckle(human, human, buckleComp: buckle), Is.False); Assert.That(buckle.Buckled); - Assert.That(buckleSystem.ToggleBuckle(human, human, chair, buckle: buckle), Is.False); + Assert.That(buckleSystem.TryUnbuckle(human, human), Is.False); Assert.That(buckle.Buckled); - Assert.That(buckleSystem.ToggleBuckle(human, human, chair, buckle: buckle), Is.False); + Assert.That(buckleSystem.TryUnbuckle(human, human), Is.False); Assert.That(buckle.Buckled); #pragma warning restore NUnit2045 }); @@ -188,7 +184,6 @@ await server.WaitAssertion(() => #pragma warning disable NUnit2045 // Interdependent asserts. Assert.That(buckleSystem.TryBuckle(human, human, chair, buckleComp: buckle), Is.False); Assert.That(buckleSystem.TryUnbuckle(human, human, buckleComp: buckle), Is.False); - Assert.That(buckleSystem.ToggleBuckle(human, human, chair, buckle: buckle), Is.False); #pragma warning restore NUnit2045 // Move near the chair @@ -201,12 +196,10 @@ await server.WaitAssertion(() => Assert.That(buckle.Buckled); Assert.That(buckleSystem.TryUnbuckle(human, human, buckleComp: buckle), Is.False); Assert.That(buckle.Buckled); - Assert.That(buckleSystem.ToggleBuckle(human, human, chair, buckle: buckle), Is.False); - Assert.That(buckle.Buckled); #pragma warning restore NUnit2045 // Force unbuckle - Assert.That(buckleSystem.TryUnbuckle(human, human, true, buckleComp: buckle)); + buckleSystem.Unbuckle(human, human); Assert.Multiple(() => { Assert.That(buckle.Buckled, Is.False); @@ -310,7 +303,7 @@ await server.WaitAssertion(() => // Break our guy's kneecaps foreach (var leg in legs) { - xformSystem.DetachParentToNull(leg.Id, entityManager.GetComponent(leg.Id)); + entityManager.DeleteEntity(leg.Id); } }); @@ -327,7 +320,8 @@ await server.WaitAssertion(() => Assert.That(hand.HeldEntity, Is.Null); } - buckleSystem.TryUnbuckle(human, human, true, buckleComp: buckle); + buckleSystem.Unbuckle(human, human); + Assert.That(buckle.Buckled, Is.False); }); await pair.CleanReturnAsync(); diff --git a/Content.IntegrationTests/Tests/Climbing/ClimbingTest.cs b/Content.IntegrationTests/Tests/Climbing/ClimbingTest.cs index d8d3086520e..2db0a9acd3d 100644 --- a/Content.IntegrationTests/Tests/Climbing/ClimbingTest.cs +++ b/Content.IntegrationTests/Tests/Climbing/ClimbingTest.cs @@ -1,5 +1,6 @@ #nullable enable using Content.IntegrationTests.Tests.Interaction; +using Content.IntegrationTests.Tests.Movement; using Robust.Shared.Maths; using ClimbingComponent = Content.Shared.Climbing.Components.ClimbingComponent; using ClimbSystem = Content.Shared.Climbing.Systems.ClimbSystem; diff --git a/Content.IntegrationTests/Tests/Construction/Interaction/CraftingTests.cs b/Content.IntegrationTests/Tests/Construction/Interaction/CraftingTests.cs index 76911eba5f7..74d0e924217 100644 --- a/Content.IntegrationTests/Tests/Construction/Interaction/CraftingTests.cs +++ b/Content.IntegrationTests/Tests/Construction/Interaction/CraftingTests.cs @@ -59,11 +59,6 @@ public async Task CraftSpear() await AssertEntityLookup((Rod, 2), (Cable, 7), (ShardGlass, 2), (Spear, 1)); } - // The following is wrapped in an if DEBUG. This is because of cursed state handling bugs. Tests don't (de)serialize - // net messages and just copy objects by reference. This means that the server will directly modify cached server - // states on the client's end. Crude fix at the moment is to used modified state handling while in debug mode - // Otherwise, this test cannot work. -#if DEBUG /// /// Cancel crafting a complex recipe. /// @@ -93,28 +88,22 @@ public async Task CancelCraft() await RunTicks(1); // DoAfter is in progress. Entity not spawned, stacks have been split and someingredients are in a container. - Assert.Multiple(async () => - { - Assert.That(ActiveDoAfters.Count(), Is.EqualTo(1)); - Assert.That(sys.IsEntityInContainer(shard), Is.True); - Assert.That(sys.IsEntityInContainer(rods), Is.False); - Assert.That(sys.IsEntityInContainer(wires), Is.False); - Assert.That(rodStack, Has.Count.EqualTo(8)); - Assert.That(wireStack, Has.Count.EqualTo(7)); + Assert.That(ActiveDoAfters.Count(), Is.EqualTo(1)); + Assert.That(sys.IsEntityInContainer(shard), Is.True); + Assert.That(sys.IsEntityInContainer(rods), Is.False); + Assert.That(sys.IsEntityInContainer(wires), Is.False); + Assert.That(rodStack, Has.Count.EqualTo(8)); + Assert.That(wireStack, Has.Count.EqualTo(7)); - await FindEntity(Spear, shouldSucceed: false); - }); + await FindEntity(Spear, shouldSucceed: false); // Cancel the DoAfter. Should drop ingredients to the floor. await CancelDoAfters(); - Assert.Multiple(async () => - { - Assert.That(sys.IsEntityInContainer(rods), Is.False); - Assert.That(sys.IsEntityInContainer(wires), Is.False); - Assert.That(sys.IsEntityInContainer(shard), Is.False); - await FindEntity(Spear, shouldSucceed: false); - await AssertEntityLookup((Rod, 10), (Cable, 10), (ShardGlass, 1)); - }); + Assert.That(sys.IsEntityInContainer(rods), Is.False); + Assert.That(sys.IsEntityInContainer(wires), Is.False); + Assert.That(sys.IsEntityInContainer(shard), Is.False); + await FindEntity(Spear, shouldSucceed: false); + await AssertEntityLookup((Rod, 10), (Cable, 10), (ShardGlass, 1)); // Re-attempt the do-after #pragma warning disable CS4014 // Legacy construction code uses DoAfterAwait. See above. @@ -123,24 +112,17 @@ public async Task CancelCraft() await RunTicks(1); // DoAfter is in progress. Entity not spawned, ingredients are in a container. - Assert.Multiple(async () => - { - Assert.That(ActiveDoAfters.Count(), Is.EqualTo(1)); - Assert.That(sys.IsEntityInContainer(shard), Is.True); - await FindEntity(Spear, shouldSucceed: false); - }); + Assert.That(ActiveDoAfters.Count(), Is.EqualTo(1)); + Assert.That(sys.IsEntityInContainer(shard), Is.True); + await FindEntity(Spear, shouldSucceed: false); // Finish the DoAfter await AwaitDoAfters(); // Spear has been crafted. Rods and wires are no longer contained. Glass has been consumed. - Assert.Multiple(async () => - { - await FindEntity(Spear); - Assert.That(sys.IsEntityInContainer(rods), Is.False); - Assert.That(sys.IsEntityInContainer(wires), Is.False); - Assert.That(SEntMan.Deleted(shard)); - }); + await FindEntity(Spear); + Assert.That(sys.IsEntityInContainer(rods), Is.False); + Assert.That(sys.IsEntityInContainer(wires), Is.False); + Assert.That(SEntMan.Deleted(shard)); } -#endif } diff --git a/Content.IntegrationTests/Tests/Interaction/InteractionTest.Helpers.cs b/Content.IntegrationTests/Tests/Interaction/InteractionTest.Helpers.cs index d34957beeb8..24a4b8e22a8 100644 --- a/Content.IntegrationTests/Tests/Interaction/InteractionTest.Helpers.cs +++ b/Content.IntegrationTests/Tests/Interaction/InteractionTest.Helpers.cs @@ -84,8 +84,9 @@ protected async Task CraftItem(string prototype, bool shouldSucceed = true) /// /// Spawn an entity entity and set it as the target. /// - [MemberNotNull(nameof(Target))] - protected async Task SpawnTarget(string prototype) + [MemberNotNull(nameof(Target), nameof(STarget), nameof(CTarget))] +#pragma warning disable CS8774 // Member must have a non-null value when exiting. + protected async Task SpawnTarget(string prototype) { Target = NetEntity.Invalid; await Server.WaitPost(() => @@ -95,7 +96,9 @@ await Server.WaitPost(() => await RunTicks(5); AssertPrototype(prototype); + return Target!.Value; } +#pragma warning restore CS8774 // Member must have a non-null value when exiting. /// /// Spawn an entity in preparation for deconstruction @@ -1170,14 +1173,17 @@ await Server.WaitPost(() => #region Inputs + + /// /// Make the client press and then release a key. This assumes the key is currently released. + /// This will default to using the entity and coordinates. /// protected async Task PressKey( BoundKeyFunction key, int ticks = 1, NetCoordinates? coordinates = null, - NetEntity cursorEntity = default) + NetEntity? cursorEntity = null) { await SetKey(key, BoundKeyState.Down, coordinates, cursorEntity); await RunTicks(ticks); @@ -1186,15 +1192,17 @@ protected async Task PressKey( } /// - /// Make the client press or release a key + /// Make the client press or release a key. + /// This will default to using the entity and coordinates. /// protected async Task SetKey( BoundKeyFunction key, BoundKeyState state, NetCoordinates? coordinates = null, - NetEntity cursorEntity = default) + NetEntity? cursorEntity = null) { var coords = coordinates ?? TargetCoords; + var target = cursorEntity ?? Target ?? default; ScreenCoordinates screen = default; var funcId = InputManager.NetworkBindMap.KeyFunctionID(key); @@ -1203,7 +1211,7 @@ protected async Task SetKey( State = state, Coordinates = CEntMan.GetCoordinates(coords), ScreenCoordinates = screen, - Uid = CEntMan.GetEntity(cursorEntity), + Uid = CEntMan.GetEntity(target), }; await Client.WaitPost(() => InputSystem.HandleInputCommand(ClientSession, key, message)); diff --git a/Content.IntegrationTests/Tests/Interaction/InteractionTest.cs b/Content.IntegrationTests/Tests/Interaction/InteractionTest.cs index 813ed4117cb..1f3e832ad2c 100644 --- a/Content.IntegrationTests/Tests/Interaction/InteractionTest.cs +++ b/Content.IntegrationTests/Tests/Interaction/InteractionTest.cs @@ -84,6 +84,7 @@ public abstract partial class InteractionTest protected NetEntity? Target; protected EntityUid? STarget => ToServer(Target); + protected EntityUid? CTarget => ToClient(Target); /// @@ -128,7 +129,6 @@ public abstract partial class InteractionTest public float TickPeriod => (float) STiming.TickPeriod.TotalSeconds; - // Simple mob that has one hand and can perform misc interactions. [TestPrototypes] private const string TestPrototypes = @" @@ -141,6 +141,8 @@ public abstract partial class InteractionTest - type: Hands - type: MindContainer - type: Stripping + - type: Puller + - type: Physics - type: Tag tags: - CanPilot @@ -206,8 +208,8 @@ await Server.WaitPost(() => SEntMan.System().WipeMind(ServerSession.ContentData()?.Mind); old = cPlayerMan.LocalEntity; - Player = SEntMan.GetNetEntity(SEntMan.SpawnEntity(PlayerPrototype, SEntMan.GetCoordinates(PlayerCoords))); - SPlayer = SEntMan.GetEntity(Player); + SPlayer = SEntMan.SpawnEntity(PlayerPrototype, SEntMan.GetCoordinates(PlayerCoords)); + Player = SEntMan.GetNetEntity(SPlayer); Server.PlayerMan.SetAttachedEntity(ServerSession, SPlayer); Hands = SEntMan.GetComponent(SPlayer); DoAfters = SEntMan.GetComponent(SPlayer); diff --git a/Content.IntegrationTests/Tests/Movement/BuckleMovementTest.cs b/Content.IntegrationTests/Tests/Movement/BuckleMovementTest.cs new file mode 100644 index 00000000000..8d91855098f --- /dev/null +++ b/Content.IntegrationTests/Tests/Movement/BuckleMovementTest.cs @@ -0,0 +1,63 @@ +using Content.Shared.Alert; +using Content.Shared.Buckle.Components; +using Robust.Shared.Maths; + +namespace Content.IntegrationTests.Tests.Movement; + +public sealed class BuckleMovementTest : MovementTest +{ + // Check that interacting with a chair straps you to it and prevents movement. + [Test] + public async Task ChairTest() + { + await SpawnTarget("Chair"); + + var cAlert = Client.System(); + var sAlert = Server.System(); + var buckle = Comp(Player); + var strap = Comp(Target); + +#pragma warning disable RA0002 + buckle.Delay = TimeSpan.Zero; +#pragma warning restore RA0002 + + // Initially not buckled to the chair, and standing off to the side + Assert.That(Delta(), Is.InRange(0.9f, 1.1f)); + Assert.That(buckle.Buckled, Is.False); + Assert.That(buckle.BuckledTo, Is.Null); + Assert.That(strap.BuckledEntities, Is.Empty); + Assert.That(cAlert.IsShowingAlert(CPlayer, strap.BuckledAlertType), Is.False); + Assert.That(sAlert.IsShowingAlert(SPlayer, strap.BuckledAlertType), Is.False); + + // Interact results in being buckled to the chair + await Interact(); + Assert.That(Delta(), Is.InRange(-0.01f, 0.01f)); + Assert.That(buckle.Buckled, Is.True); + Assert.That(buckle.BuckledTo, Is.EqualTo(STarget)); + Assert.That(strap.BuckledEntities, Is.EquivalentTo(new[]{SPlayer})); + Assert.That(cAlert.IsShowingAlert(CPlayer, strap.BuckledAlertType), Is.True); + Assert.That(sAlert.IsShowingAlert(SPlayer, strap.BuckledAlertType), Is.True); + + // Attempting to walk away does nothing + await Move(DirectionFlag.East, 1); + Assert.That(Delta(), Is.InRange(-0.01f, 0.01f)); + Assert.That(buckle.Buckled, Is.True); + Assert.That(buckle.BuckledTo, Is.EqualTo(STarget)); + Assert.That(strap.BuckledEntities, Is.EquivalentTo(new[]{SPlayer})); + Assert.That(cAlert.IsShowingAlert(CPlayer, strap.BuckledAlertType), Is.True); + Assert.That(sAlert.IsShowingAlert(SPlayer, strap.BuckledAlertType), Is.True); + + // Interacting again will unbuckle the player + await Interact(); + Assert.That(Delta(), Is.InRange(-0.5f, 0.5f)); + Assert.That(buckle.Buckled, Is.False); + Assert.That(buckle.BuckledTo, Is.Null); + Assert.That(strap.BuckledEntities, Is.Empty); + Assert.That(cAlert.IsShowingAlert(CPlayer, strap.BuckledAlertType), Is.False); + Assert.That(sAlert.IsShowingAlert(SPlayer, strap.BuckledAlertType), Is.False); + + // And now they can move away + await Move(DirectionFlag.SouthEast, 1); + Assert.That(Delta(), Is.LessThan(-1)); + } +} diff --git a/Content.IntegrationTests/Tests/Interaction/MovementTest.cs b/Content.IntegrationTests/Tests/Movement/MovementTest.cs similarity index 95% rename from Content.IntegrationTests/Tests/Interaction/MovementTest.cs rename to Content.IntegrationTests/Tests/Movement/MovementTest.cs index dc5aec92cfc..ad7b1d0459f 100644 --- a/Content.IntegrationTests/Tests/Interaction/MovementTest.cs +++ b/Content.IntegrationTests/Tests/Movement/MovementTest.cs @@ -1,8 +1,9 @@ #nullable enable using System.Numerics; +using Content.IntegrationTests.Tests.Interaction; using Robust.Shared.GameObjects; -namespace Content.IntegrationTests.Tests.Interaction; +namespace Content.IntegrationTests.Tests.Movement; /// /// This is a variation of that sets up the player with a normal human entity and a simple diff --git a/Content.IntegrationTests/Tests/Movement/PullingTest.cs b/Content.IntegrationTests/Tests/Movement/PullingTest.cs new file mode 100644 index 00000000000..d96c4ea0e56 --- /dev/null +++ b/Content.IntegrationTests/Tests/Movement/PullingTest.cs @@ -0,0 +1,73 @@ +#nullable enable +using Content.Shared.Alert; +using Content.Shared.Input; +using Content.Shared.Movement.Pulling.Components; +using Robust.Shared.Maths; + +namespace Content.IntegrationTests.Tests.Movement; + +public sealed class PullingTest : MovementTest +{ + protected override int Tiles => 4; + + [Test] + public async Task PullTest() + { + var cAlert = Client.System(); + var sAlert = Server.System(); + await SpawnTarget("MobHuman"); + + var puller = Comp(Player); + var pullable = Comp(Target); + + // Player is initially to the left of the target and not pulling anything + Assert.That(Delta(), Is.InRange(0.9f, 1.1f)); + Assert.That(puller.Pulling, Is.Null); + Assert.That(pullable.Puller, Is.Null); + Assert.That(pullable.BeingPulled, Is.False); + Assert.That(cAlert.IsShowingAlert(CPlayer, puller.PullingAlert), Is.False); + Assert.That(sAlert.IsShowingAlert(SPlayer, puller.PullingAlert), Is.False); + + // Start pulling + await PressKey(ContentKeyFunctions.TryPullObject); + await RunTicks(5); + Assert.That(puller.Pulling, Is.EqualTo(STarget)); + Assert.That(pullable.Puller, Is.EqualTo(SPlayer)); + Assert.That(pullable.BeingPulled, Is.True); + Assert.That(cAlert.IsShowingAlert(CPlayer, puller.PullingAlert), Is.True); + Assert.That(sAlert.IsShowingAlert(SPlayer, puller.PullingAlert), Is.True); + + // Move to the left and check that the target moves with the player and is still being pulled. + await Move(DirectionFlag.West, 1); + Assert.That(Delta(), Is.InRange(0.9f, 1.3f)); + Assert.That(puller.Pulling, Is.EqualTo(STarget)); + Assert.That(pullable.Puller, Is.EqualTo(SPlayer)); + Assert.That(pullable.BeingPulled, Is.True); + Assert.That(cAlert.IsShowingAlert(CPlayer, puller.PullingAlert), Is.True); + Assert.That(sAlert.IsShowingAlert(SPlayer, puller.PullingAlert), Is.True); + + // Move in the other direction + await Move(DirectionFlag.East, 2); + Assert.That(Delta(), Is.InRange(-1.3f, -0.9f)); + Assert.That(puller.Pulling, Is.EqualTo(STarget)); + Assert.That(pullable.Puller, Is.EqualTo(SPlayer)); + Assert.That(pullable.BeingPulled, Is.True); + Assert.That(cAlert.IsShowingAlert(CPlayer, puller.PullingAlert), Is.True); + Assert.That(sAlert.IsShowingAlert(SPlayer, puller.PullingAlert), Is.True); + + // Stop pulling + await PressKey(ContentKeyFunctions.ReleasePulledObject); + await RunTicks(5); + Assert.That(Delta(), Is.InRange(-1.3f, -0.9f)); + Assert.That(puller.Pulling, Is.Null); + Assert.That(pullable.Puller, Is.Null); + Assert.That(pullable.BeingPulled, Is.False); + Assert.That(cAlert.IsShowingAlert(CPlayer, puller.PullingAlert), Is.False); + Assert.That(sAlert.IsShowingAlert(SPlayer, puller.PullingAlert), Is.False); + + // Move back to the left and ensure the target is no longer following us. + await Move(DirectionFlag.West, 2); + Assert.That(Delta(), Is.GreaterThan(2f)); + } +} + diff --git a/Content.IntegrationTests/Tests/Slipping/SlippingTest.cs b/Content.IntegrationTests/Tests/Movement/SlippingTest.cs similarity index 92% rename from Content.IntegrationTests/Tests/Slipping/SlippingTest.cs rename to Content.IntegrationTests/Tests/Movement/SlippingTest.cs index 28da7a94658..9ac84a0a586 100644 --- a/Content.IntegrationTests/Tests/Slipping/SlippingTest.cs +++ b/Content.IntegrationTests/Tests/Movement/SlippingTest.cs @@ -11,7 +11,7 @@ using Robust.Shared.IoC; using Robust.Shared.Maths; -namespace Content.IntegrationTests.Tests.Slipping; +namespace Content.IntegrationTests.Tests.Movement; public sealed class SlippingTest : MovementTest { @@ -41,18 +41,14 @@ public async Task BananaSlipTest() // Assert.That(modifier, Is.EqualTo(1), "Player is not moving at full speed."); // Yeeting this pointless Assert because it's not actually important. // Player is to the left of the banana peel and has not slipped. -#pragma warning disable NUnit2045 Assert.That(Delta(), Is.GreaterThan(0.5f)); Assert.That(sys.Slipped, Does.Not.Contain(SEntMan.GetEntity(Player))); -#pragma warning restore NUnit2045 // Walking over the banana slowly does not trigger a slip. await SetKey(EngineKeyFunctions.Walk, sprintWalks ? BoundKeyState.Up : BoundKeyState.Down); await Move(DirectionFlag.East, 1f); -#pragma warning disable NUnit2045 Assert.That(Delta(), Is.LessThan(0.5f)); Assert.That(sys.Slipped, Does.Not.Contain(SEntMan.GetEntity(Player))); -#pragma warning restore NUnit2045 AssertComp(false, Player); // Moving at normal speeds does trigger a slip. diff --git a/Content.Server/Bed/BedSystem.cs b/Content.Server/Bed/BedSystem.cs index 976ef5139c3..089ce322366 100644 --- a/Content.Server/Bed/BedSystem.cs +++ b/Content.Server/Bed/BedSystem.cs @@ -15,6 +15,7 @@ using Content.Shared.Mobs.Systems; using Robust.Shared.Timing; using Content.Shared.Silicon.Components; // I shouldn't have to modify this. +using Robust.Shared.Utility; namespace Content.Server.Bed { @@ -30,27 +31,31 @@ public sealed class BedSystem : EntitySystem public override void Initialize() { base.Initialize(); - SubscribeLocalEvent(ManageUpdateList); - SubscribeLocalEvent(OnBuckleChange); + SubscribeLocalEvent(OnStrapped); + SubscribeLocalEvent(OnUnstrapped); + SubscribeLocalEvent(OnStasisStrapped); + SubscribeLocalEvent(OnStasisUnstrapped); SubscribeLocalEvent(OnPowerChanged); SubscribeLocalEvent(OnEmagged); SubscribeLocalEvent(OnRefreshParts); SubscribeLocalEvent(OnUpgradeExamine); } - private void ManageUpdateList(EntityUid uid, HealOnBuckleComponent component, ref BuckleChangeEvent args) + private void OnStrapped(Entity bed, ref StrappedEvent args) { - if (args.Buckling) - { - AddComp(uid); - component.NextHealTime = _timing.CurTime + TimeSpan.FromSeconds(component.HealTime); - _actionsSystem.AddAction(args.BuckledEntity, ref component.SleepAction, SleepingSystem.SleepActionId, uid); - return; - } + EnsureComp(bed); + bed.Comp.NextHealTime = _timing.CurTime + TimeSpan.FromSeconds(bed.Comp.HealTime); + _actionsSystem.AddAction(args.Buckle, ref bed.Comp.SleepAction, SleepingSystem.SleepActionId, bed); - _actionsSystem.RemoveAction(args.BuckledEntity, component.SleepAction); - _sleepingSystem.TryWaking(args.BuckledEntity); - RemComp(uid); + // Single action entity, cannot strap multiple entities to the same bed. + DebugTools.AssertEqual(args.Strap.Comp.BuckledEntities.Count, 1); + } + + private void OnUnstrapped(Entity bed, ref UnstrappedEvent args) + { + _actionsSystem.RemoveAction(args.Buckle, bed.Comp.SleepAction); + _sleepingSystem.TryWaking(args.Buckle.Owner); + RemComp(bed); } public override void Update(float frameTime) @@ -89,18 +94,22 @@ private void UpdateAppearance(EntityUid uid, bool isOn) _appearance.SetData(uid, StasisBedVisuals.IsOn, isOn); } - private void OnBuckleChange(EntityUid uid, StasisBedComponent component, ref BuckleChangeEvent args) + private void OnStasisStrapped(Entity bed, ref StrappedEvent args) { - // In testing this also received an unbuckle event when the bed is destroyed - // So don't worry about that - if (!HasComp(args.BuckledEntity)) + if (!HasComp(args.Buckle) || !this.IsPowered(bed, EntityManager)) return; - if (!this.IsPowered(uid, EntityManager)) + var metabolicEvent = new ApplyMetabolicMultiplierEvent(args.Buckle, bed.Comp.Multiplier, true); + RaiseLocalEvent(args.Buckle, ref metabolicEvent); + } + + private void OnStasisUnstrapped(Entity bed, ref UnstrappedEvent args) + { + if (!HasComp(args.Buckle) || !this.IsPowered(bed, EntityManager)) return; - var metabolicEvent = new ApplyMetabolicMultiplierEvent(args.BuckledEntity, component.Multiplier, args.Buckling); - RaiseLocalEvent(args.BuckledEntity, ref metabolicEvent); + var metabolicEvent = new ApplyMetabolicMultiplierEvent(args.Buckle, bed.Comp.Multiplier, false); + RaiseLocalEvent(args.Buckle, ref metabolicEvent); } private void OnPowerChanged(EntityUid uid, StasisBedComponent component, ref PowerChangedEvent args) diff --git a/Content.Server/Bed/Components/HealOnBuckleComponent.cs b/Content.Server/Bed/Components/HealOnBuckleComponent.cs index f29fe30429f..3c6f3a4382b 100644 --- a/Content.Server/Bed/Components/HealOnBuckleComponent.cs +++ b/Content.Server/Bed/Components/HealOnBuckleComponent.cs @@ -5,19 +5,26 @@ namespace Content.Server.Bed.Components [RegisterComponent] public sealed partial class HealOnBuckleComponent : Component { - [DataField("damage", required: true)] - [ViewVariables(VVAccess.ReadWrite)] + /// + /// Damage to apply to entities that are strapped to this entity. + /// + [DataField(required: true)] public DamageSpecifier Damage = default!; - [DataField("healTime", required: false)] - [ViewVariables(VVAccess.ReadWrite)] - public float HealTime = 1f; // How often the bed applies the damage + /// + /// How frequently the damage should be applied, in seconds. + /// + [DataField(required: false)] + public float HealTime = 1f; - [DataField("sleepMultiplier")] + /// + /// Damage multiplier that gets applied if the entity is sleeping. + /// + [DataField] public float SleepMultiplier = 3f; public TimeSpan NextHealTime = TimeSpan.Zero; //Next heal - [DataField("sleepAction")] public EntityUid? SleepAction; + [DataField] public EntityUid? SleepAction; } } diff --git a/Content.Server/Bed/Components/HealOnBuckleHealing.cs b/Content.Server/Bed/Components/HealOnBuckleHealing.cs index a944e67e12d..aaa82c737c5 100644 --- a/Content.Server/Bed/Components/HealOnBuckleHealing.cs +++ b/Content.Server/Bed/Components/HealOnBuckleHealing.cs @@ -1,5 +1,6 @@ namespace Content.Server.Bed.Components { + // TODO rename this component [RegisterComponent] public sealed partial class HealOnBuckleHealingComponent : Component {} diff --git a/Content.Server/Bed/Components/StasisBedComponent.cs b/Content.Server/Bed/Components/StasisBedComponent.cs index bb4096a2a5e..6e0042b2df8 100644 --- a/Content.Server/Bed/Components/StasisBedComponent.cs +++ b/Content.Server/Bed/Components/StasisBedComponent.cs @@ -12,7 +12,8 @@ public sealed partial class StasisBedComponent : Component /// /// What the metabolic update rate will be multiplied by (higher = slower metabolism) /// - [ViewVariables(VVAccess.ReadWrite)] + [ViewVariables(VVAccess.ReadOnly)] // Writing is is not supported. ApplyMetabolicMultiplierEvent needs to be refactored first + [DataField] public float Multiplier = 10f; [DataField(customTypeSerializer: typeof(PrototypeIdSerializer))] diff --git a/Content.Server/Body/Systems/BloodstreamSystem.cs b/Content.Server/Body/Systems/BloodstreamSystem.cs index 2fa376a0c92..095018f9b9a 100644 --- a/Content.Server/Body/Systems/BloodstreamSystem.cs +++ b/Content.Server/Body/Systems/BloodstreamSystem.cs @@ -280,6 +280,9 @@ private void OnApplyMetabolicMultiplier( Entity ent, ref ApplyMetabolicMultiplierEvent args) { + // TODO REFACTOR THIS + // This will slowly drift over time due to floating point errors. + // Instead, raise an event with the base rates and allow modifiers to get applied to it. if (args.Apply) { ent.Comp.UpdateInterval *= args.Multiplier; diff --git a/Content.Server/Body/Systems/MetabolizerSystem.cs b/Content.Server/Body/Systems/MetabolizerSystem.cs index 066bf0a1c5b..a7eec8e3c02 100644 --- a/Content.Server/Body/Systems/MetabolizerSystem.cs +++ b/Content.Server/Body/Systems/MetabolizerSystem.cs @@ -67,6 +67,9 @@ private void OnApplyMetabolicMultiplier( Entity ent, ref ApplyMetabolicMultiplierEvent args) { + // TODO REFACTOR THIS + // This will slowly drift over time due to floating point errors. + // Instead, raise an event with the base rates and allow modifiers to get applied to it. if (args.Apply) { ent.Comp.UpdateInterval *= args.Multiplier; @@ -232,6 +235,9 @@ private void TryMetabolize(Entity ent, ref ApplyMetabolicMultiplierEvent args) { + // TODO REFACTOR THIS + // This will slowly drift over time due to floating point errors. + // Instead, raise an event with the base rates and allow modifiers to get applied to it. if (args.Apply) { ent.Comp.UpdateInterval *= args.Multiplier; diff --git a/Content.Server/NPC/HTN/PrimitiveTasks/Operators/Combat/UnbuckleOperator.cs b/Content.Server/NPC/HTN/PrimitiveTasks/Operators/Combat/UnbuckleOperator.cs index 207665d786f..116e8fe7c7f 100644 --- a/Content.Server/NPC/HTN/PrimitiveTasks/Operators/Combat/UnbuckleOperator.cs +++ b/Content.Server/NPC/HTN/PrimitiveTasks/Operators/Combat/UnbuckleOperator.cs @@ -1,11 +1,9 @@ using Content.Server.Buckle.Systems; -using Content.Shared.Buckle.Components; namespace Content.Server.NPC.HTN.PrimitiveTasks.Operators.Combat; public sealed partial class UnbuckleOperator : HTNOperator { - [Dependency] private readonly IEntityManager _entManager = default!; private BuckleSystem _buckle = default!; [DataField("shutdownState")] @@ -21,10 +19,7 @@ public override void Startup(NPCBlackboard blackboard) { base.Startup(blackboard); var owner = blackboard.GetValue(NPCBlackboard.Owner); - if (!_entManager.TryGetComponent(owner, out var buckle) || !buckle.Buckled) - return; - - _buckle.TryUnbuckle(owner, owner, true, buckle); + _buckle.Unbuckle(owner, null); } public override HTNOperatorStatus Update(NPCBlackboard blackboard, float frameTime) diff --git a/Content.Shared/Buckle/Components/BuckleComponent.cs b/Content.Shared/Buckle/Components/BuckleComponent.cs index cf28b56d51f..ee86e6d4de0 100644 --- a/Content.Shared/Buckle/Components/BuckleComponent.cs +++ b/Content.Shared/Buckle/Components/BuckleComponent.cs @@ -1,10 +1,15 @@ +using System.Diagnostics.CodeAnalysis; using Content.Shared.Interaction; using Robust.Shared.GameStates; using Robust.Shared.Serialization; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom; namespace Content.Shared.Buckle.Components; -[RegisterComponent, NetworkedComponent, AutoGenerateComponentState(true)] +/// +/// This component allows an entity to be buckled to an entity with a . +/// +[RegisterComponent, NetworkedComponent] [Access(typeof(SharedBuckleSystem))] public sealed partial class BuckleComponent : Component { @@ -14,31 +19,23 @@ public sealed partial class BuckleComponent : Component /// across a table two tiles away" problem. /// [DataField] - [ViewVariables(VVAccess.ReadWrite)] public float Range = SharedInteractionSystem.InteractionRange / 1.4f; /// /// True if the entity is buckled, false otherwise. /// - [ViewVariables(VVAccess.ReadWrite)] - [AutoNetworkedField] - public bool Buckled; - - [ViewVariables] - [AutoNetworkedField] - public EntityUid? LastEntityBuckledTo; + [MemberNotNullWhen(true, nameof(BuckledTo))] + public bool Buckled => BuckledTo != null; /// /// Whether or not collisions should be possible with the entity we are strapped to /// - [ViewVariables(VVAccess.ReadWrite)] - [DataField, AutoNetworkedField] + [DataField] public bool DontCollide; /// /// Whether or not we should be allowed to pull the entity we are strapped to /// - [ViewVariables(VVAccess.ReadWrite)] [DataField] public bool PullStrap; @@ -47,20 +44,18 @@ public sealed partial class BuckleComponent : Component /// be able to unbuckle after recently buckling. /// [DataField] - [ViewVariables(VVAccess.ReadWrite)] public TimeSpan Delay = TimeSpan.FromSeconds(0.25f); /// /// The time that this entity buckled at. /// - [ViewVariables] - public TimeSpan BuckleTime; + [DataField(customTypeSerializer: typeof(TimeOffsetSerializer))] + public TimeSpan? BuckleTime; /// /// The strap that this component is buckled to. /// - [ViewVariables] - [AutoNetworkedField] + [DataField] public EntityUid? BuckledTo; /// @@ -68,7 +63,6 @@ public sealed partial class BuckleComponent : Component /// . /// [DataField] - [ViewVariables(VVAccess.ReadWrite)] public int Size = 100; /// @@ -77,11 +71,90 @@ public sealed partial class BuckleComponent : Component [ViewVariables] public int? OriginalDrawDepth; } +[Serializable, NetSerializable] +public sealed class BuckleState(NetEntity? buckledTo, bool dontCollide, TimeSpan? buckleTime) : ComponentState +{ + public readonly NetEntity? BuckledTo = buckledTo; + public readonly bool DontCollide = dontCollide; + public readonly TimeSpan? BuckleTime = buckleTime; +} + + +/// +/// Event raised directed at a strap entity before some entity gets buckled to it. +/// +[ByRefEvent] +public record struct StrapAttemptEvent( + Entity Strap, + Entity Buckle, + EntityUid? User, + bool Popup) +{ + public bool Cancelled; +} + +/// +/// Event raised directed at a buckle entity before it gets buckled to some strap entity. +/// +[ByRefEvent] +public record struct BuckleAttemptEvent( + Entity Strap, + Entity Buckle, + EntityUid? User, + bool Popup) +{ + public bool Cancelled; +} + +/// +/// Event raised directed at a strap entity before some entity gets unbuckled from it. +/// +[ByRefEvent] +public record struct UnstrapAttemptEvent( + Entity Strap, + Entity Buckle, + EntityUid? User, + bool Popup) +{ + public bool Cancelled; +} + +/// +/// Event raised directed at a buckle entity before it gets unbuckled. +/// +[ByRefEvent] +public record struct UnbuckleAttemptEvent( + Entity Strap, + Entity Buckle, + EntityUid? User, + bool Popup) +{ + public bool Cancelled; +} + +/// +/// Event raised directed at a strap entity after something has been buckled to it. +/// +[ByRefEvent] +public readonly record struct StrappedEvent(Entity Strap, Entity Buckle); + +/// +/// Event raised directed at a buckle entity after it has been buckled. +/// +[ByRefEvent] +public readonly record struct BuckledEvent(Entity Strap, Entity Buckle); + +/// +/// Event raised directed at a strap entity after something has been unbuckled from it. +/// [ByRefEvent] -public record struct BuckleAttemptEvent(EntityUid StrapEntity, EntityUid BuckledEntity, EntityUid UserEntity, bool Buckling, bool Cancelled = false); +public readonly record struct UnstrappedEvent(Entity Strap, Entity Buckle); +/// +/// Event raised directed at a buckle entity after it has been unbuckled from some strap entity. +/// [ByRefEvent] -public readonly record struct BuckleChangeEvent(EntityUid StrapEntity, EntityUid BuckledEntity, bool Buckling); +public readonly record struct UnbuckledEvent(Entity Strap, Entity Buckle); [Serializable, NetSerializable] public enum BuckleVisuals diff --git a/Content.Shared/Buckle/Components/StrapComponent.cs b/Content.Shared/Buckle/Components/StrapComponent.cs index 9a19cea0c9a..a16d15f8a2c 100644 --- a/Content.Shared/Buckle/Components/StrapComponent.cs +++ b/Content.Shared/Buckle/Components/StrapComponent.cs @@ -13,117 +13,77 @@ namespace Content.Shared.Buckle.Components; public sealed partial class StrapComponent : Component { /// - /// The entities that are currently buckled + /// The entities that are currently buckled to this strap. /// - [AutoNetworkedField] - [ViewVariables] // TODO serialization + [ViewVariables] public HashSet BuckledEntities = new(); /// /// Entities that this strap accepts and can buckle /// If null it accepts any entity /// - [DataField, ViewVariables(VVAccess.ReadWrite)] + [DataField] public EntityWhitelist? Whitelist; /// /// Entities that this strap does not accept and cannot buckle. /// - [DataField, ViewVariables(VVAccess.ReadWrite)] + [DataField] public EntityWhitelist? Blacklist; /// /// The change in position to the strapped mob /// [DataField, AutoNetworkedField] - [ViewVariables(VVAccess.ReadWrite)] public StrapPosition Position = StrapPosition.None; - /// - /// The distance above which a buckled entity will be automatically unbuckled. - /// Don't change it unless you really have to - /// - /// - /// Dont set this below 0.2 because that causes audio issues with - /// My guess after testing is that the client sets BuckledTo to the strap in *some* ticks for some reason - /// whereas the server doesnt, thus the client tries to unbuckle like 15 times because it passes the strap null check - /// This is why this needs to be above 0.1 to make the InRange check fail in both client and server. - /// - [DataField, AutoNetworkedField] - [ViewVariables(VVAccess.ReadWrite)] - public float MaxBuckleDistance = 0.2f; - - /// - /// Gets and clamps the buckle offset to MaxBuckleDistance - /// - [ViewVariables] - public Vector2 BuckleOffsetClamped => Vector2.Clamp( - BuckleOffset, - Vector2.One * -MaxBuckleDistance, - Vector2.One * MaxBuckleDistance); - /// /// The buckled entity will be offset by this amount from the center of the strap object. - /// If this offset it too big, it will be clamped to /// [DataField, AutoNetworkedField] - [ViewVariables(VVAccess.ReadWrite)] public Vector2 BuckleOffset = Vector2.Zero; /// /// The angle to rotate the player by when they get strapped /// [DataField] - [ViewVariables(VVAccess.ReadWrite)] public Angle Rotation; /// /// The size of the strap which is compared against when buckling entities /// [DataField] - [ViewVariables(VVAccess.ReadWrite)] public int Size = 100; /// /// If disabled, nothing can be buckled on this object, and it will unbuckle anything that's already buckled /// - [ViewVariables] + [DataField, AutoNetworkedField] public bool Enabled = true; /// /// You can specify the offset the entity will have after unbuckling. /// [DataField] - [ViewVariables(VVAccess.ReadWrite)] public Vector2 UnbuckleOffset = Vector2.Zero; /// /// The sound to be played when a mob is buckled /// [DataField] - [ViewVariables(VVAccess.ReadWrite)] public SoundSpecifier BuckleSound = new SoundPathSpecifier("/Audio/Effects/buckle.ogg"); /// /// The sound to be played when a mob is unbuckled /// [DataField] - [ViewVariables(VVAccess.ReadWrite)] public SoundSpecifier UnbuckleSound = new SoundPathSpecifier("/Audio/Effects/unbuckle.ogg"); /// /// ID of the alert to show when buckled /// [DataField] - [ViewVariables(VVAccess.ReadWrite)] public ProtoId BuckledAlertType = "Buckled"; - - /// - /// The sum of the sizes of all the buckled entities in this strap - /// - [AutoNetworkedField] - [ViewVariables] - public int OccupiedSize; } public enum StrapPosition diff --git a/Content.Shared/Buckle/SharedBuckleSystem.Buckle.cs b/Content.Shared/Buckle/SharedBuckleSystem.Buckle.cs index 00040211e36..4e1fd301233 100644 --- a/Content.Shared/Buckle/SharedBuckleSystem.Buckle.cs +++ b/Content.Shared/Buckle/SharedBuckleSystem.Buckle.cs @@ -1,36 +1,47 @@ using System.Diagnostics.CodeAnalysis; using System.Numerics; using Content.Shared.Alert; -using Content.Shared.Bed.Sleep; using Content.Shared.Buckle.Components; using Content.Shared.Database; using Content.Shared.Hands.Components; using Content.Shared.IdentityManagement; -using Content.Shared.Interaction; -using Content.Shared.Mobs.Components; using Content.Shared.Movement.Events; +using Content.Shared.Movement.Pulling.Events; using Content.Shared.Popups; +using Content.Shared.Pulling.Events; using Content.Shared.Standing; using Content.Shared.Storage.Components; using Content.Shared.Stunnable; using Content.Shared.Throwing; using Content.Shared.Verbs; +using Content.Shared.Whitelist; +using Robust.Shared.Containers; +using Robust.Shared.GameStates; +using Robust.Shared.Map; using Robust.Shared.Physics.Components; using Robust.Shared.Physics.Events; +using Robust.Shared.Prototypes; using Robust.Shared.Utility; -using PullableComponent = Content.Shared.Movement.Pulling.Components.PullableComponent; namespace Content.Shared.Buckle; public abstract partial class SharedBuckleSystem { + public static ProtoId BuckledAlertCategory = "Buckled"; + + [Dependency] private readonly EntityWhitelistSystem _whitelistSystem = default!; + private void InitializeBuckle() { - SubscribeLocalEvent(OnBuckleComponentStartup); SubscribeLocalEvent(OnBuckleComponentShutdown); SubscribeLocalEvent(OnBuckleMove); - SubscribeLocalEvent(OnBuckleInteractHand); - SubscribeLocalEvent>(AddUnbuckleVerb); + SubscribeLocalEvent(OnParentChanged); + SubscribeLocalEvent(OnInserted); + + SubscribeLocalEvent(OnPullAttempt); + SubscribeLocalEvent(OnBeingPulledAttempt); + SubscribeLocalEvent(OnPullStarted); + SubscribeLocalEvent(OnBuckleInsertIntoEntityStorageAttempt); SubscribeLocalEvent(OnBucklePreventCollide); @@ -38,69 +49,93 @@ private void InitializeBuckle() SubscribeLocalEvent(OnBuckleStandAttempt); SubscribeLocalEvent(OnBuckleThrowPushbackAttempt); SubscribeLocalEvent(OnBuckleUpdateCanMove); - } - [ValidatePrototypeId] - public const string BuckledAlertCategory = "Buckled"; + SubscribeLocalEvent(OnGetState); + } - private void OnBuckleComponentStartup(EntityUid uid, BuckleComponent component, ComponentStartup args) + private void OnGetState(Entity ent, ref ComponentGetState args) { - UpdateBuckleStatus(uid, component); + args.State = new BuckleState(GetNetEntity(ent.Comp.BuckledTo), ent.Comp.DontCollide, ent.Comp.BuckleTime); } - private void OnBuckleComponentShutdown(EntityUid uid, BuckleComponent component, ComponentShutdown args) + private void OnBuckleComponentShutdown(Entity ent, ref ComponentShutdown args) { - TryUnbuckle(uid, uid, true, component); + Unbuckle(ent!, null); + } + + #region Pulling - component.BuckleTime = default; + private void OnPullAttempt(Entity ent, ref StartPullAttemptEvent args) + { + // Prevent people pulling the chair they're on, etc. + if (ent.Comp.BuckledTo == args.Pulled && !ent.Comp.PullStrap) + args.Cancel(); } - private void OnBuckleMove(EntityUid uid, BuckleComponent component, ref MoveEvent ev) + private void OnBeingPulledAttempt(Entity ent, ref BeingPulledAttemptEvent args) { - if (component.BuckledTo is not { } strapUid) + if (args.Cancelled || !ent.Comp.Buckled) return; - if (!TryComp(strapUid, out var strapComp)) - return; + if (!CanUnbuckle(ent!, args.Puller, false)) + args.Cancel(); + } - var strapPosition = Transform(strapUid).Coordinates; - if (ev.NewPosition.EntityId.IsValid() && ev.NewPosition.InRange(EntityManager, _transform, strapPosition, strapComp.MaxBuckleDistance)) - return; + private void OnPullStarted(Entity ent, ref PullStartedMessage args) + { + Unbuckle(ent!, args.PullerUid); + } + + #endregion + + #region Transform - TryUnbuckle(uid, uid, true, component); + private void OnParentChanged(Entity ent, ref EntParentChangedMessage args) + { + BuckleTransformCheck(ent, args.Transform); } - private void OnBuckleInteractHand(EntityUid uid, BuckleComponent component, InteractHandEvent args) + private void OnInserted(Entity ent, ref EntGotInsertedIntoContainerMessage args) { - if (!component.Buckled) - return; + BuckleTransformCheck(ent, Transform(ent)); + } - if (TryUnbuckle(uid, args.User, buckleComp: component)) - args.Handled = true; + private void OnBuckleMove(Entity ent, ref MoveEvent ev) + { + BuckleTransformCheck(ent, ev.Component); } - private void AddUnbuckleVerb(EntityUid uid, BuckleComponent component, GetVerbsEvent args) + /// + /// Check if the entity should get unbuckled as a result of transform or container changes. + /// + private void BuckleTransformCheck(Entity buckle, TransformComponent xform) { - if (!args.CanAccess || !args.CanInteract || !component.Buckled) + if (_gameTiming.ApplyingState) + return; + + if (buckle.Comp.BuckledTo is not { } strapUid) return; - InteractionVerb verb = new() + if (!TryComp(strapUid, out var strapComp)) { - Act = () => TryUnbuckle(uid, args.User, buckleComp: component), - Text = Loc.GetString("verb-categories-unbuckle"), - Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/VerbIcons/unbuckle.svg.192dpi.png")) - }; + Log.Error($"Encountered buckle entity {ToPrettyString(buckle)} without a valid strap entity {ToPrettyString(strapUid)}"); + SetBuckledTo(buckle, null); + return; + } - if (args.Target == args.User && args.Using == null) + if (xform.ParentUid != strapUid || _container.IsEntityInContainer(buckle)) { - // A user is left clicking themselves with an empty hand, while buckled. - // It is very likely they are trying to unbuckle themselves. - verb.Priority = 1; + Unbuckle(buckle, (strapUid, strapComp), null); + return; } - args.Verbs.Add(verb); + var delta = (xform.LocalPosition - strapComp.BuckleOffset).LengthSquared(); + if (delta > 1e-5) + Unbuckle(buckle, (strapUid, strapComp), null); } + #endregion + private void OnBuckleInsertIntoEntityStorageAttempt(EntityUid uid, BuckleComponent component, ref InsertIntoEntityStorageAttemptEvent args) { if (component.Buckled) @@ -109,10 +144,7 @@ private void OnBuckleInsertIntoEntityStorageAttempt(EntityUid uid, BuckleCompone private void OnBucklePreventCollide(EntityUid uid, BuckleComponent component, ref PreventCollideEvent args) { - if (args.OtherEntity != component.BuckledTo) - return; - - if (component.Buckled || component.DontCollide) + if (args.OtherEntity == component.BuckledTo && component.DontCollide) args.Cancelled = true; } @@ -136,10 +168,7 @@ private void OnBuckleThrowPushbackAttempt(EntityUid uid, BuckleComponent compone private void OnBuckleUpdateCanMove(EntityUid uid, BuckleComponent component, UpdateCanMoveEvent args) { - if (component.LifeStage > ComponentLifeStage.Running) - return; - - if (component.Buckled) // buckle shitcode + if (component.Buckled) args.Cancel(); } @@ -148,162 +177,139 @@ public bool IsBuckled(EntityUid uid, BuckleComponent? component = null) return Resolve(uid, ref component, false) && component.Buckled; } - /// - /// Shows or hides the buckled status effect depending on if the - /// entity is buckled or not. - /// - /// Entity that we want to show the alert - /// buckle component of the entity - /// strap component of the thing we are strapping to - private void UpdateBuckleStatus(EntityUid uid, BuckleComponent buckleComp, StrapComponent? strapComp = null) - { - Appearance.SetData(uid, StrapVisuals.State, buckleComp.Buckled); - if (buckleComp.BuckledTo != null) - { - if (!Resolve(buckleComp.BuckledTo.Value, ref strapComp)) - return; - - var alertType = strapComp.BuckledAlertType; - _alerts.ShowAlert(uid, alertType); - } - else - { - _alerts.ClearAlertCategory(uid, BuckledAlertCategory); - } - } - - /// - /// Sets the field in the component to a value - /// - /// Value tat with be assigned to the field - private void SetBuckledTo(EntityUid buckleUid, EntityUid? strapUid, StrapComponent? strapComp, BuckleComponent buckleComp) + protected void SetBuckledTo(Entity buckle, Entity? strap) { - buckleComp.BuckledTo = strapUid; + if (TryComp(buckle.Comp.BuckledTo, out StrapComponent? old)) + old.BuckledEntities.Remove(buckle); - if (strapUid == null) + if (strap is {} strapEnt && Resolve(strapEnt.Owner, ref strapEnt.Comp)) { - buckleComp.Buckled = false; + strapEnt.Comp.BuckledEntities.Add(buckle); + _alerts.ShowAlert(buckle, strapEnt.Comp.BuckledAlertType); } else { - buckleComp.LastEntityBuckledTo = strapUid; - buckleComp.DontCollide = true; - buckleComp.Buckled = true; - buckleComp.BuckleTime = _gameTiming.CurTime; + _alerts.ClearAlertCategory(buckle, BuckledAlertCategory); } - ActionBlocker.UpdateCanMove(buckleUid); - UpdateBuckleStatus(buckleUid, buckleComp, strapComp); - Dirty(buckleUid, buckleComp); + buckle.Comp.BuckledTo = strap; + buckle.Comp.BuckleTime = _gameTiming.CurTime; + ActionBlocker.UpdateCanMove(buckle); + Appearance.SetData(buckle, StrapVisuals.State, buckle.Comp.Buckled); + Dirty(buckle); } /// /// Checks whether or not buckling is possible /// /// Uid of the owner of BuckleComponent - /// - /// Uid of a third party entity, - /// i.e, the uid of someone else you are dragging to a chair. - /// Can equal buckleUid sometimes + /// + /// Uid of a third party entity, + /// i.e, the uid of someone else you are dragging to a chair. + /// Can equal buckleUid sometimes /// /// Uid of the owner of strap component - private bool CanBuckle( - EntityUid buckleUid, - EntityUid userUid, + /// + /// + private bool CanBuckle(EntityUid buckleUid, + EntityUid? user, EntityUid strapUid, + bool popup, [NotNullWhen(true)] out StrapComponent? strapComp, - BuckleComponent? buckleComp = null) + BuckleComponent buckleComp) { strapComp = null; - - if (userUid == strapUid || - !Resolve(buckleUid, ref buckleComp, false) || - !Resolve(strapUid, ref strapComp, false)) - { + if (!Resolve(strapUid, ref strapComp, false)) return false; - } // Does it pass the Whitelist if (strapComp.Whitelist != null && !strapComp.Whitelist.IsValid(buckleUid, EntityManager) || strapComp.Blacklist?.IsValid(buckleUid, EntityManager) == true) { - if (_netManager.IsServer) - _popup.PopupEntity(Loc.GetString("buckle-component-cannot-fit-message"), userUid, buckleUid, PopupType.Medium); + if (_netManager.IsServer && popup && user != null) + _popup.PopupEntity(Loc.GetString("buckle-component-cannot-fit-message"), user.Value, user.Value, PopupType.Medium); return false; } - // Is it within range - bool Ignored(EntityUid entity) => entity == buckleUid || entity == userUid || entity == strapUid; - - if (!_interaction.InRangeUnobstructed(buckleUid, strapUid, buckleComp.Range, predicate: Ignored, + if (!_interaction.InRangeUnobstructed(buckleUid, + strapUid, + buckleComp.Range, + predicate: entity => entity == buckleUid || entity == user || entity == strapUid, popup: true)) { return false; } - // If in a container - if (_container.TryGetContainingContainer(buckleUid, out var ownerContainer)) - { - // And not in the same container as the strap - if (!_container.TryGetContainingContainer(strapUid, out var strapContainer) || - ownerContainer != strapContainer) - { - return false; - } - } + if (!_container.IsInSameOrNoContainer((buckleUid, null, null), (strapUid, null, null))) + return false; - if (!HasComp(userUid)) + if (user != null && !HasComp(user)) { // PopupPredicted when - if (_netManager.IsServer) - _popup.PopupEntity(Loc.GetString("buckle-component-no-hands-message"), userUid, userUid); + if (_netManager.IsServer && popup) + _popup.PopupEntity(Loc.GetString("buckle-component-no-hands-message"), user.Value, user.Value); return false; } if (buckleComp.Buckled) { - var message = Loc.GetString(buckleUid == userUid + if (_netManager.IsClient || popup || user == null) + return false; + + var message = Loc.GetString(buckleUid == user ? "buckle-component-already-buckled-message" : "buckle-component-other-already-buckled-message", ("owner", Identity.Entity(buckleUid, EntityManager))); - if (_netManager.IsServer) - _popup.PopupEntity(message, userUid, userUid); + _popup.PopupEntity(message, user.Value, user.Value); return false; } + // Check whether someone is attempting to buckle something to their own child var parent = Transform(strapUid).ParentUid; while (parent.IsValid()) { - if (parent == userUid) + if (parent != buckleUid) { - var message = Loc.GetString(buckleUid == userUid - ? "buckle-component-cannot-buckle-message" - : "buckle-component-other-cannot-buckle-message", ("owner", Identity.Entity(buckleUid, EntityManager))); - if (_netManager.IsServer) - _popup.PopupEntity(message, userUid, userUid); + parent = Transform(parent).ParentUid; + continue; + } + if (_netManager.IsClient || popup || user == null) return false; - } - parent = Transform(parent).ParentUid; + var message = Loc.GetString(buckleUid == user + ? "buckle-component-cannot-buckle-message" + : "buckle-component-other-cannot-buckle-message", + ("owner", Identity.Entity(buckleUid, EntityManager))); + + _popup.PopupEntity(message, user.Value, user.Value); + return false; } if (!StrapHasSpace(strapUid, buckleComp, strapComp)) { - var message = Loc.GetString(buckleUid == userUid - ? "buckle-component-cannot-fit-message" - : "buckle-component-other-cannot-fit-message", ("owner", Identity.Entity(buckleUid, EntityManager))); - if (_netManager.IsServer) - _popup.PopupEntity(message, userUid, userUid); + if (_netManager.IsClient || popup || user == null) + return false; + + var message = Loc.GetString(buckleUid == user + ? "buckle-component-cannot-fit-message" + : "buckle-component-other-cannot-fit-message", + ("owner", Identity.Entity(buckleUid, EntityManager))); + + _popup.PopupEntity(message, user.Value, user.Value); return false; } - var attemptEvent = new BuckleAttemptEvent(strapUid, buckleUid, userUid, true); - RaiseLocalEvent(attemptEvent.BuckledEntity, ref attemptEvent); - RaiseLocalEvent(attemptEvent.StrapEntity, ref attemptEvent); - if (attemptEvent.Cancelled) + var buckleAttempt = new BuckleAttemptEvent((strapUid, strapComp), (buckleUid, buckleComp), user, popup); + RaiseLocalEvent(buckleUid, ref buckleAttempt); + if (buckleAttempt.Cancelled) + return false; + + var strapAttempt = new StrapAttemptEvent((strapUid, strapComp), (buckleUid, buckleComp), user, popup); + RaiseLocalEvent(strapUid, ref strapAttempt); + if (strapAttempt.Cancelled) return false; return true; @@ -312,216 +318,194 @@ private bool CanBuckle( /// /// Attempts to buckle an entity to a strap /// - /// Uid of the owner of BuckleComponent - /// + /// Uid of the owner of BuckleComponent + /// /// Uid of a third party entity, /// i.e, the uid of someone else you are dragging to a chair. /// Can equal buckleUid sometimes /// - /// Uid of the owner of strap component - public bool TryBuckle(EntityUid buckleUid, EntityUid userUid, EntityUid strapUid, BuckleComponent? buckleComp = null) + /// Uid of the owner of strap component + public bool TryBuckle(EntityUid buckle, EntityUid? user, EntityUid strap, BuckleComponent? buckleComp = null, bool popup = true) { - if (!Resolve(buckleUid, ref buckleComp, false)) + if (!Resolve(buckle, ref buckleComp, false)) return false; - if (!CanBuckle(buckleUid, userUid, strapUid, out var strapComp, buckleComp)) + if (!CanBuckle(buckle, user, strap, popup, out var strapComp, buckleComp)) return false; - if (!StrapTryAdd(strapUid, buckleUid, buckleComp, false, strapComp)) - { - var message = Loc.GetString(buckleUid == userUid - ? "buckle-component-cannot-buckle-message" - : "buckle-component-other-cannot-buckle-message", ("owner", Identity.Entity(buckleUid, EntityManager))); - if (_netManager.IsServer) - _popup.PopupEntity(message, userUid, userUid); - return false; - } + Buckle((buckle, buckleComp), (strap, strapComp), user); + return true; + } - if (TryComp(buckleUid, out var appearance)) - Appearance.SetData(buckleUid, BuckleVisuals.Buckled, true, appearance); + private void Buckle(Entity buckle, Entity strap, EntityUid? user) + { + if (user == buckle.Owner) + _adminLogger.Add(LogType.Action, LogImpact.Low, $"{ToPrettyString(user):player} buckled themselves to {ToPrettyString(strap)}"); + else if (user != null) + _adminLogger.Add(LogType.Action, LogImpact.Low, $"{ToPrettyString(user):player} buckled {ToPrettyString(buckle)} to {ToPrettyString(strap)}"); - _rotationVisuals.SetHorizontalAngle(buckleUid, strapComp.Rotation); + _audio.PlayPredicted(strap.Comp.BuckleSound, strap, user); - ReAttach(buckleUid, strapUid, buckleComp, strapComp); - SetBuckledTo(buckleUid, strapUid, strapComp, buckleComp); - // TODO user is currently set to null because if it isn't the sound fails to play in some situations, fix that - _audio.PlayPredicted(strapComp.BuckleSound, strapUid, userUid); + SetBuckledTo(buckle, strap!); + Appearance.SetData(strap, StrapVisuals.State, true); + Appearance.SetData(buckle, BuckleVisuals.Buckled, true); - var ev = new BuckleChangeEvent(strapUid, buckleUid, true); - RaiseLocalEvent(ev.BuckledEntity, ref ev); - RaiseLocalEvent(ev.StrapEntity, ref ev); + _rotationVisuals.SetHorizontalAngle(buckle.Owner, strap.Comp.Rotation); - if (TryComp(buckleUid, out var ownerPullable)) - { - if (ownerPullable.Puller != null) - { - _pulling.TryStopPull(buckleUid, ownerPullable); - } - } + var xform = Transform(buckle); + var coords = new EntityCoordinates(strap, strap.Comp.BuckleOffset); + _transform.SetCoordinates(buckle, xform, coords, rotation: Angle.Zero); - if (TryComp(buckleUid, out var physics)) - { - _physics.ResetDynamics(buckleUid, physics); - } + _joints.SetRelay(buckle, strap); - if (!buckleComp.PullStrap && TryComp(strapUid, out var toPullable)) + switch (strap.Comp.Position) { - if (toPullable.Puller == buckleUid) - { - // can't pull it and buckle to it at the same time - _pulling.TryStopPull(strapUid, toPullable); - } + case StrapPosition.Stand: + _standing.Stand(buckle); + break; + case StrapPosition.Down: + _standing.Down(buckle, false, false); + break; } - // Logging - if (userUid != buckleUid) - _adminLogger.Add(LogType.Action, LogImpact.Low, $"{ToPrettyString(userUid):player} buckled {ToPrettyString(buckleUid)} to {ToPrettyString(strapUid)}"); - else - _adminLogger.Add(LogType.Action, LogImpact.Low, $"{ToPrettyString(userUid):player} buckled themselves to {ToPrettyString(strapUid)}"); + var ev = new StrappedEvent(strap, buckle); + RaiseLocalEvent(strap, ref ev); - return true; + var gotEv = new BuckledEvent(strap, buckle); + RaiseLocalEvent(buckle, ref gotEv); + + if (TryComp(buckle, out var physics)) + _physics.ResetDynamics(buckle, physics); + + DebugTools.AssertEqual(xform.ParentUid, strap.Owner); } /// /// Tries to unbuckle the Owner of this component from its current strap. /// /// The entity to unbuckle. - /// The entity doing the unbuckling. - /// - /// Whether to force the unbuckling or not. Does not guarantee true to - /// be returned, but guarantees the owner to be unbuckled afterwards. - /// + /// The entity doing the unbuckling. /// The buckle component of the entity to unbuckle. /// /// true if the owner was unbuckled, otherwise false even if the owner /// was previously already unbuckled. /// - public bool TryUnbuckle(EntityUid buckleUid, EntityUid userUid, bool force = false, BuckleComponent? buckleComp = null) + public bool TryUnbuckle(EntityUid buckleUid, + EntityUid? user, + BuckleComponent? buckleComp = null, + bool popup = true) + { + return TryUnbuckle((buckleUid, buckleComp), user, popup); + } + + public bool TryUnbuckle(Entity buckle, EntityUid? user, bool popup) { - if (!Resolve(buckleUid, ref buckleComp, false) || - buckleComp.BuckledTo is not { } strapUid) + if (!Resolve(buckle.Owner, ref buckle.Comp)) return false; - if (!force) - { - var attemptEvent = new BuckleAttemptEvent(strapUid, buckleUid, userUid, false); - RaiseLocalEvent(attemptEvent.BuckledEntity, ref attemptEvent); - RaiseLocalEvent(attemptEvent.StrapEntity, ref attemptEvent); - if (attemptEvent.Cancelled) - return false; + if (!CanUnbuckle(buckle, user, popup, out var strap)) + return false; - if (_gameTiming.CurTime < buckleComp.BuckleTime + buckleComp.Delay) - return false; + Unbuckle(buckle!, strap, user); + return true; + } - if (!_interaction.InRangeUnobstructed(userUid, strapUid, buckleComp.Range, popup: true)) - return false; + public void Unbuckle(Entity buckle, EntityUid? user) + { + if (!Resolve(buckle.Owner, ref buckle.Comp, false)) + return; - if (HasComp(buckleUid) && buckleUid == userUid) - return false; + if (buckle.Comp.BuckledTo is not { } strap) + return; - // If the person is crit or dead in any kind of strap, return. This prevents people from unbuckling themselves while incapacitated. - if (_mobState.IsIncapacitated(buckleUid) && userUid == buckleUid) - return false; + if (!TryComp(strap, out StrapComponent? strapComp)) + { + Log.Error($"Encountered buckle {ToPrettyString(buckle.Owner)} with invalid strap entity {ToPrettyString(strap)}"); + SetBuckledTo(buckle!, null); + return; } - // Logging - if (userUid != buckleUid) - _adminLogger.Add(LogType.Action, LogImpact.Low, $"{ToPrettyString(userUid):player} unbuckled {ToPrettyString(buckleUid)} from {ToPrettyString(strapUid)}"); - else - _adminLogger.Add(LogType.Action, LogImpact.Low, $"{ToPrettyString(userUid):player} unbuckled themselves from {ToPrettyString(strapUid)}"); + Unbuckle(buckle!, (strap, strapComp), user); + } + + private void Unbuckle(Entity buckle, Entity strap, EntityUid? user) + { + if (user == buckle.Owner) + _adminLogger.Add(LogType.Action, LogImpact.Low, $"{user} unbuckled themselves from {strap}"); + else if (user != null) + _adminLogger.Add(LogType.Action, LogImpact.Low, $"{user} unbuckled {buckle} from {strap}"); - SetBuckledTo(buckleUid, null, null, buckleComp); + _audio.PlayPredicted(strap.Comp.UnbuckleSound, strap, user); - if (!TryComp(strapUid, out var strapComp)) - return false; + SetBuckledTo(buckle, null); - var buckleXform = Transform(buckleUid); - var oldBuckledXform = Transform(strapUid); + var buckleXform = Transform(buckle); + var oldBuckledXform = Transform(strap); - if (buckleXform.ParentUid == strapUid && !Terminating(buckleXform.ParentUid)) + if (buckleXform.ParentUid == strap.Owner && !Terminating(buckleXform.ParentUid)) { - _container.AttachParentToContainerOrGrid((buckleUid, buckleXform)); + _container.AttachParentToContainerOrGrid((buckle, buckleXform)); - var oldBuckledToWorldRot = _transform.GetWorldRotation(strapUid); + var oldBuckledToWorldRot = _transform.GetWorldRotation(strap); _transform.SetWorldRotation(buckleXform, oldBuckledToWorldRot); - if (strapComp.UnbuckleOffset != Vector2.Zero) - buckleXform.Coordinates = oldBuckledXform.Coordinates.Offset(strapComp.UnbuckleOffset); + if (strap.Comp.UnbuckleOffset != Vector2.Zero) + buckleXform.Coordinates = oldBuckledXform.Coordinates.Offset(strap.Comp.UnbuckleOffset); } - if (TryComp(buckleUid, out AppearanceComponent? appearance)) - Appearance.SetData(buckleUid, BuckleVisuals.Buckled, false, appearance); - _rotationVisuals.ResetHorizontalAngle(buckleUid); + _rotationVisuals.ResetHorizontalAngle(buckle.Owner); + Appearance.SetData(strap, StrapVisuals.State, strap.Comp.BuckledEntities.Count != 0); + Appearance.SetData(buckle, BuckleVisuals.Buckled, false); - if (TryComp(buckleUid, out var mobState) - && _mobState.IsIncapacitated(buckleUid, mobState) - || HasComp(buckleUid)) - { - _standing.Down(buckleUid); - } + if (HasComp(buckle) || _mobState.IsIncapacitated(buckle)) + _standing.Down(buckle); else - { - _standing.Stand(buckleUid); - } + _standing.Stand(buckle); - if (_mobState.IsIncapacitated(buckleUid, mobState)) - { - _standing.Down(buckleUid); - } - if (strapComp.BuckledEntities.Remove(buckleUid)) - { - strapComp.OccupiedSize -= buckleComp.Size; - Dirty(strapUid, strapComp); - } - - _joints.RefreshRelay(buckleUid); - Appearance.SetData(strapUid, StrapVisuals.State, strapComp.BuckledEntities.Count != 0); + _joints.RefreshRelay(buckle); - // TODO: Buckle listening to moveevents is sussy anyway. - if (!TerminatingOrDeleted(strapUid)) - _audio.PlayPredicted(strapComp.UnbuckleSound, strapUid, userUid); + var buckleEv = new UnbuckledEvent(strap, buckle); + RaiseLocalEvent(buckle, ref buckleEv); - var ev = new BuckleChangeEvent(strapUid, buckleUid, false); - RaiseLocalEvent(buckleUid, ref ev); - RaiseLocalEvent(strapUid, ref ev); + var strapEv = new UnstrappedEvent(strap, buckle); + RaiseLocalEvent(strap, ref strapEv); + } - return true; + public bool CanUnbuckle(Entity buckle, EntityUid user, bool popup) + { + return CanUnbuckle(buckle, user, popup, out _); } - /// - /// Makes an entity toggle the buckling status of the owner to a - /// specific entity. - /// - /// The entity to buckle/unbuckle from . - /// The entity doing the buckling/unbuckling. - /// - /// The entity to toggle the buckle status of the owner to. - /// - /// - /// Whether to force the unbuckling or not, if it happens. Does not - /// guarantee true to be returned, but guarantees the owner to be - /// unbuckled afterwards. - /// - /// The buckle component of the entity to buckle/unbuckle from . - /// true if the buckling status was changed, false otherwise. - public bool ToggleBuckle( - EntityUid buckleUid, - EntityUid userUid, - EntityUid strapUid, - bool force = false, - BuckleComponent? buckle = null) + private bool CanUnbuckle(Entity buckle, EntityUid? user, bool popup, out Entity strap) { - if (!Resolve(buckleUid, ref buckle, false)) + strap = default; + if (!Resolve(buckle.Owner, ref buckle.Comp)) return false; - if (!buckle.Buckled) - { - return TryBuckle(buckleUid, userUid, strapUid, buckle); - } - else + if (buckle.Comp.BuckledTo is not { } strapUid) + return false; + + if (!TryComp(strapUid, out StrapComponent? strapComp)) { - return TryUnbuckle(buckleUid, userUid, force, buckle); + Log.Error($"Encountered buckle {ToPrettyString(buckle.Owner)} with invalid strap entity {ToPrettyString(strap)}"); + SetBuckledTo(buckle!, null); + return false; } + strap = (strapUid, strapComp); + if (_gameTiming.CurTime < buckle.Comp.BuckleTime + buckle.Comp.Delay) + return false; + + if (user != null && !_interaction.InRangeUnobstructed(user.Value, strap.Owner, buckle.Comp.Range, popup: popup)) + return false; + + var unbuckleAttempt = new UnbuckleAttemptEvent(strap, buckle!, user, popup); + RaiseLocalEvent(buckle, ref unbuckleAttempt); + if (unbuckleAttempt.Cancelled) + return false; + + var unstrapAttempt = new UnstrapAttemptEvent(strap, buckle!, user, popup); + RaiseLocalEvent(strap, ref unstrapAttempt); + return !unstrapAttempt.Cancelled; } } diff --git a/Content.Shared/Buckle/SharedBuckleSystem.Interaction.cs b/Content.Shared/Buckle/SharedBuckleSystem.Interaction.cs new file mode 100644 index 00000000000..8c2d0b8ee18 --- /dev/null +++ b/Content.Shared/Buckle/SharedBuckleSystem.Interaction.cs @@ -0,0 +1,171 @@ +using Content.Shared.Buckle.Components; +using Content.Shared.DragDrop; +using Content.Shared.IdentityManagement; +using Content.Shared.Interaction; +using Content.Shared.Verbs; +using Robust.Shared.Utility; + +namespace Content.Shared.Buckle; + +// Partial class containing interaction & verb event handlers +public abstract partial class SharedBuckleSystem +{ + private void InitializeInteraction() + { + SubscribeLocalEvent>(AddStrapVerbs); + SubscribeLocalEvent(OnStrapInteractHand); + SubscribeLocalEvent(OnStrapDragDropTarget); + SubscribeLocalEvent(OnCanDropTarget); + + SubscribeLocalEvent>(AddUnbuckleVerb); + } + + private void OnCanDropTarget(EntityUid uid, StrapComponent component, ref CanDropTargetEvent args) + { + args.CanDrop = StrapCanDragDropOn(uid, args.User, uid, args.Dragged, component); + args.Handled = true; + } + + private void OnStrapDragDropTarget(EntityUid uid, StrapComponent component, ref DragDropTargetEvent args) + { + if (!StrapCanDragDropOn(uid, args.User, uid, args.Dragged, component)) + return; + + args.Handled = TryBuckle(args.Dragged, args.User, uid, popup: false); + } + + private bool StrapCanDragDropOn( + EntityUid strapUid, + EntityUid userUid, + EntityUid targetUid, + EntityUid buckleUid, + StrapComponent? strapComp = null, + BuckleComponent? buckleComp = null) + { + if (!Resolve(strapUid, ref strapComp, false) || + !Resolve(buckleUid, ref buckleComp, false)) + { + return false; + } + + bool Ignored(EntityUid entity) => entity == userUid || entity == buckleUid || entity == targetUid; + + return _interaction.InRangeUnobstructed(targetUid, buckleUid, buckleComp.Range, predicate: Ignored); + } + + private void OnStrapInteractHand(EntityUid uid, StrapComponent component, InteractHandEvent args) + { + if (args.Handled) + return; + + if (!TryComp(args.User, out BuckleComponent? buckle)) + return; + + if (buckle.BuckledTo == null) + TryBuckle(args.User, args.User, uid, buckle, popup: true); + else if (buckle.BuckledTo == uid) + TryUnbuckle(args.User, args.User, buckle, popup: true); + else + return; + + args.Handled = true; // This generate popups on failure. + } + + private void AddStrapVerbs(EntityUid uid, StrapComponent component, GetVerbsEvent args) + { + if (args.Hands == null || !args.CanAccess || !args.CanInteract || !component.Enabled) + return; + + // Note that for whatever bloody reason, buckle component has its own interaction range. Additionally, this + // range can be set per-component, so we have to check a modified InRangeUnobstructed for every verb. + + // Add unstrap verbs for every strapped entity. + foreach (var entity in component.BuckledEntities) + { + var buckledComp = Comp(entity); + + if (!_interaction.InRangeUnobstructed(args.User, args.Target, range: buckledComp.Range)) + continue; + + var verb = new InteractionVerb() + { + Act = () => TryUnbuckle(entity, args.User, buckleComp: buckledComp), + Category = VerbCategory.Unbuckle, + Text = entity == args.User + ? Loc.GetString("verb-self-target-pronoun") + : Identity.Name(entity, EntityManager) + }; + + // In the event that you have more than once entity with the same name strapped to the same object, + // these two verbs will be identical according to Verb.CompareTo, and only one with actually be added to + // the verb list. However this should rarely ever be a problem. If it ever is, it could be fixed by + // appending an integer to verb.Text to distinguish the verbs. + + args.Verbs.Add(verb); + } + + // Add a verb to buckle the user. + if (TryComp(args.User, out var buckle) && + buckle.BuckledTo != uid && + args.User != uid && + StrapHasSpace(uid, buckle, component) && + _interaction.InRangeUnobstructed(args.User, args.Target, range: buckle.Range)) + { + InteractionVerb verb = new() + { + Act = () => TryBuckle(args.User, args.User, args.Target, buckle), + Category = VerbCategory.Buckle, + Text = Loc.GetString("verb-self-target-pronoun") + }; + args.Verbs.Add(verb); + } + + // If the user is currently holding/pulling an entity that can be buckled, add a verb for that. + if (args.Using is { Valid: true } @using && + TryComp(@using, out var usingBuckle) && + StrapHasSpace(uid, usingBuckle, component) && + _interaction.InRangeUnobstructed(@using, args.Target, range: usingBuckle.Range)) + { + // Check that the entity is unobstructed from the target (ignoring the user). + bool Ignored(EntityUid entity) => entity == args.User || entity == args.Target || entity == @using; + if (!_interaction.InRangeUnobstructed(@using, args.Target, usingBuckle.Range, predicate: Ignored)) + return; + + var isPlayer = _playerManager.TryGetSessionByEntity(@using, out var _); + InteractionVerb verb = new() + { + Act = () => TryBuckle(@using, args.User, args.Target, usingBuckle), + Category = VerbCategory.Buckle, + Text = Identity.Name(@using, EntityManager), + // just a held object, the user is probably just trying to sit down. + // If the used entity is a person being pulled, prioritize this verb. Conversely, if it is + Priority = isPlayer ? 1 : -1 + }; + + args.Verbs.Add(verb); + } + } + + private void AddUnbuckleVerb(EntityUid uid, BuckleComponent component, GetVerbsEvent args) + { + if (!args.CanAccess || !args.CanInteract || !component.Buckled) + return; + + InteractionVerb verb = new() + { + Act = () => TryUnbuckle(uid, args.User, buckleComp: component), + Text = Loc.GetString("verb-categories-unbuckle"), + Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/VerbIcons/unbuckle.svg.192dpi.png")) + }; + + if (args.Target == args.User && args.Using == null) + { + // A user is left clicking themselves with an empty hand, while buckled. + // It is very likely they are trying to unbuckle themselves. + verb.Priority = 1; + } + + args.Verbs.Add(verb); + } + +} diff --git a/Content.Shared/Buckle/SharedBuckleSystem.Strap.cs b/Content.Shared/Buckle/SharedBuckleSystem.Strap.cs index 147af42e728..eb23aa973b4 100644 --- a/Content.Shared/Buckle/SharedBuckleSystem.Strap.cs +++ b/Content.Shared/Buckle/SharedBuckleSystem.Strap.cs @@ -2,39 +2,25 @@ using Content.Shared.Buckle.Components; using Content.Shared.Construction; using Content.Shared.Destructible; -using Content.Shared.DragDrop; using Content.Shared.Foldable; -using Content.Shared.Interaction; -using Content.Shared.Rotation; using Content.Shared.Storage; -using Content.Shared.Verbs; using Robust.Shared.Containers; namespace Content.Shared.Buckle; public abstract partial class SharedBuckleSystem { - [Dependency] private readonly SharedRotationVisualsSystem _rotationVisuals = default!; - private void InitializeStrap() { SubscribeLocalEvent(OnStrapStartup); SubscribeLocalEvent(OnStrapShutdown); SubscribeLocalEvent((e, c, _) => StrapRemoveAll(e, c)); - SubscribeLocalEvent(OnStrapEntModifiedFromContainer); - SubscribeLocalEvent(OnStrapEntModifiedFromContainer); - SubscribeLocalEvent>(AddStrapVerbs); SubscribeLocalEvent(OnStrapContainerGettingInsertedAttempt); - SubscribeLocalEvent(OnStrapInteractHand); SubscribeLocalEvent((e, c, _) => StrapRemoveAll(e, c)); SubscribeLocalEvent((e, c, _) => StrapRemoveAll(e, c)); - SubscribeLocalEvent(OnStrapDragDropTarget); - SubscribeLocalEvent(OnCanDropTarget); SubscribeLocalEvent(OnAttemptFold); - - SubscribeLocalEvent(OnStrapMoveEvent); SubscribeLocalEvent((e, c, _) => StrapRemoveAll(e, c)); } @@ -45,145 +31,17 @@ private void OnStrapStartup(EntityUid uid, StrapComponent component, ComponentSt private void OnStrapShutdown(EntityUid uid, StrapComponent component, ComponentShutdown args) { - if (LifeStage(uid) > EntityLifeStage.MapInitialized) - return; - - StrapRemoveAll(uid, component); - } - - private void OnStrapEntModifiedFromContainer(EntityUid uid, StrapComponent component, ContainerModifiedMessage message) - { - if (_gameTiming.ApplyingState) - return; - - foreach (var buckledEntity in component.BuckledEntities) - { - if (!TryComp(buckledEntity, out var buckleComp)) - { - continue; - } - - ContainerModifiedReAttach(buckledEntity, uid, buckleComp, component); - } - } - - private void ContainerModifiedReAttach(EntityUid buckleUid, EntityUid strapUid, BuckleComponent? buckleComp = null, StrapComponent? strapComp = null) - { - if (!Resolve(buckleUid, ref buckleComp, false) || - !Resolve(strapUid, ref strapComp, false)) - return; - - var contained = _container.TryGetContainingContainer(buckleUid, out var ownContainer); - var strapContained = _container.TryGetContainingContainer(strapUid, out var strapContainer); - - if (contained != strapContained || ownContainer != strapContainer) - { - TryUnbuckle(buckleUid, buckleUid, true, buckleComp); - return; - } - - if (!contained) - { - ReAttach(buckleUid, strapUid, buckleComp, strapComp); - } + if (!TerminatingOrDeleted(uid)) + StrapRemoveAll(uid, component); } private void OnStrapContainerGettingInsertedAttempt(EntityUid uid, StrapComponent component, ContainerGettingInsertedAttemptEvent args) { // If someone is attempting to put this item inside of a backpack, ensure that it has no entities strapped to it. - if (HasComp(args.Container.Owner) && component.BuckledEntities.Count != 0) + if (args.Container.ID == StorageComponent.ContainerId && component.BuckledEntities.Count != 0) args.Cancel(); } - private void OnStrapInteractHand(EntityUid uid, StrapComponent component, InteractHandEvent args) - { - if (args.Handled) - return; - - args.Handled = ToggleBuckle(args.User, args.User, uid); - } - - private void AddStrapVerbs(EntityUid uid, StrapComponent component, GetVerbsEvent args) - { - if (args.Hands == null || !args.CanAccess || !args.CanInteract || !component.Enabled) - return; - - // Note that for whatever bloody reason, buckle component has its own interaction range. Additionally, this - // range can be set per-component, so we have to check a modified InRangeUnobstructed for every verb. - - // Add unstrap verbs for every strapped entity. - foreach (var entity in component.BuckledEntities) - { - var buckledComp = Comp(entity); - - if (!_interaction.InRangeUnobstructed(args.User, args.Target, range: buckledComp.Range)) - continue; - - var verb = new InteractionVerb() - { - Act = () => TryUnbuckle(entity, args.User, buckleComp: buckledComp), - Category = VerbCategory.Unbuckle, - Text = entity == args.User - ? Loc.GetString("verb-self-target-pronoun") - : Comp(entity).EntityName - }; - - // In the event that you have more than once entity with the same name strapped to the same object, - // these two verbs will be identical according to Verb.CompareTo, and only one with actually be added to - // the verb list. However this should rarely ever be a problem. If it ever is, it could be fixed by - // appending an integer to verb.Text to distinguish the verbs. - - args.Verbs.Add(verb); - } - - // Add a verb to buckle the user. - if (TryComp(args.User, out var buckle) && - buckle.BuckledTo != uid && - args.User != uid && - StrapHasSpace(uid, buckle, component) && - _interaction.InRangeUnobstructed(args.User, args.Target, range: buckle.Range)) - { - InteractionVerb verb = new() - { - Act = () => TryBuckle(args.User, args.User, args.Target, buckle), - Category = VerbCategory.Buckle, - Text = Loc.GetString("verb-self-target-pronoun") - }; - args.Verbs.Add(verb); - } - - // If the user is currently holding/pulling an entity that can be buckled, add a verb for that. - if (args.Using is { Valid: true } @using && - TryComp(@using, out var usingBuckle) && - StrapHasSpace(uid, usingBuckle, component) && - _interaction.InRangeUnobstructed(@using, args.Target, range: usingBuckle.Range)) - { - // Check that the entity is unobstructed from the target (ignoring the user). - bool Ignored(EntityUid entity) => entity == args.User || entity == args.Target || entity == @using; - if (!_interaction.InRangeUnobstructed(@using, args.Target, usingBuckle.Range, predicate: Ignored)) - return; - - var isPlayer = _playerManager.TryGetSessionByEntity(@using, out var _); - InteractionVerb verb = new() - { - Act = () => TryBuckle(@using, args.User, args.Target, usingBuckle), - Category = VerbCategory.Buckle, - Text = Comp(@using).EntityName, - // just a held object, the user is probably just trying to sit down. - // If the used entity is a person being pulled, prioritize this verb. Conversely, if it is - Priority = isPlayer ? 1 : -1 - }; - - args.Verbs.Add(verb); - } - } - - private void OnCanDropTarget(EntityUid uid, StrapComponent component, ref CanDropTargetEvent args) - { - args.CanDrop = StrapCanDragDropOn(uid, args.User, uid, args.Dragged, component); - args.Handled = true; - } - private void OnAttemptFold(EntityUid uid, StrapComponent component, ref FoldAttemptEvent args) { if (args.Cancelled) @@ -192,69 +50,6 @@ private void OnAttemptFold(EntityUid uid, StrapComponent component, ref FoldAtte args.Cancelled = component.BuckledEntities.Count != 0; } - private void OnStrapDragDropTarget(EntityUid uid, StrapComponent component, ref DragDropTargetEvent args) - { - if (!StrapCanDragDropOn(uid, args.User, uid, args.Dragged, component)) - return; - - args.Handled = TryBuckle(args.Dragged, args.User, uid); - } - - private void OnStrapMoveEvent(EntityUid uid, StrapComponent component, ref MoveEvent args) - { - // TODO: This looks dirty af. - // On rotation of a strap, reattach all buckled entities. - // This fixes buckle offsets and draw depths. - // This is mega cursed. Please somebody save me from Mr Buckle's wild ride. - // Oh god I'm back here again. Send help. - - // Consider a chair that has a player strapped to it. Then the client receives a new server state, showing - // that the player entity has moved elsewhere, and the chair has rotated. If the client applies the player - // state, then the chairs transform comp state, and then the buckle state. The transform state will - // forcefully teleport the player back to the chair (client-side only). This causes even more issues if the - // chair was teleporting in from nullspace after having left PVS. - // - // One option is to just never trigger re-buckles during state application. - // another is to.. just not do this? Like wtf is this code. But I CBF with buckle atm. - - if (_gameTiming.ApplyingState || args.NewRotation == args.OldRotation) - return; - - foreach (var buckledEntity in component.BuckledEntities) - { - if (!TryComp(buckledEntity, out var buckled)) - continue; - - if (!buckled.Buckled || buckled.LastEntityBuckledTo != uid) - { - Log.Error($"A moving strap entity {ToPrettyString(uid)} attempted to re-parent an entity that does not 'belong' to it {ToPrettyString(buckledEntity)}"); - continue; - } - - ReAttach(buckledEntity, uid, buckled, component); - Dirty(buckledEntity, buckled); - } - } - - private bool StrapCanDragDropOn( - EntityUid strapUid, - EntityUid userUid, - EntityUid targetUid, - EntityUid buckleUid, - StrapComponent? strapComp = null, - BuckleComponent? buckleComp = null) - { - if (!Resolve(strapUid, ref strapComp, false) || - !Resolve(buckleUid, ref buckleComp, false)) - { - return false; - } - - bool Ignored(EntityUid entity) => entity == userUid || entity == buckleUid || entity == targetUid; - - return _interaction.InRangeUnobstructed(targetUid, buckleUid, buckleComp.Range, predicate: Ignored); - } - /// /// Remove everything attached to the strap /// @@ -264,10 +59,6 @@ private void StrapRemoveAll(EntityUid uid, StrapComponent strapComp) { TryUnbuckle(entity, entity, true); } - - strapComp.BuckledEntities.Clear(); - strapComp.OccupiedSize = 0; - Dirty(uid, strapComp); } private bool StrapHasSpace(EntityUid strapUid, BuckleComponent buckleComp, StrapComponent? strapComp = null) @@ -275,30 +66,13 @@ private bool StrapHasSpace(EntityUid strapUid, BuckleComponent buckleComp, Strap if (!Resolve(strapUid, ref strapComp, false)) return false; - return strapComp.OccupiedSize + buckleComp.Size <= strapComp.Size; - } - - /// - /// Try to add an entity to the strap - /// - private bool StrapTryAdd(EntityUid strapUid, EntityUid buckleUid, BuckleComponent buckleComp, bool force = false, StrapComponent? strapComp = null) - { - if (!Resolve(strapUid, ref strapComp, false) || - !strapComp.Enabled) - return false; - - if (!force && !StrapHasSpace(strapUid, buckleComp, strapComp)) - return false; - - if (!strapComp.BuckledEntities.Add(buckleUid)) - return false; - - strapComp.OccupiedSize += buckleComp.Size; - - Appearance.SetData(strapUid, StrapVisuals.State, true); + var avail = strapComp.Size; + foreach (var buckle in strapComp.BuckledEntities) + { + avail -= CompOrNull(buckle)?.Size ?? 0; + } - Dirty(strapUid, strapComp); - return true; + return avail >= buckleComp.Size; } /// @@ -311,6 +85,7 @@ public void StrapSetEnabled(EntityUid strapUid, bool enabled, StrapComponent? st return; strapComp.Enabled = enabled; + Dirty(strapUid, strapComp); if (!enabled) StrapRemoveAll(strapUid, strapComp); diff --git a/Content.Shared/Buckle/SharedBuckleSystem.cs b/Content.Shared/Buckle/SharedBuckleSystem.cs index 67218657e52..770fababded 100644 --- a/Content.Shared/Buckle/SharedBuckleSystem.cs +++ b/Content.Shared/Buckle/SharedBuckleSystem.cs @@ -1,21 +1,17 @@ using Content.Shared.ActionBlocker; using Content.Shared.Administration.Logs; using Content.Shared.Alert; -using Content.Shared.Buckle.Components; using Content.Shared.Interaction; using Content.Shared.Mobs.Systems; using Content.Shared.Popups; -using Content.Shared.Pulling; +using Content.Shared.Rotation; using Content.Shared.Standing; -using Robust.Shared.Audio; using Robust.Shared.Audio.Systems; using Robust.Shared.Containers; -using Robust.Shared.Map; using Robust.Shared.Network; using Robust.Shared.Physics.Systems; using Robust.Shared.Player; using Robust.Shared.Timing; -using PullingSystem = Content.Shared.Movement.Pulling.Systems.PullingSystem; namespace Content.Shared.Buckle; @@ -36,10 +32,10 @@ public abstract partial class SharedBuckleSystem : EntitySystem [Dependency] private readonly SharedInteractionSystem _interaction = default!; [Dependency] private readonly SharedJointSystem _joints = default!; [Dependency] private readonly SharedPopupSystem _popup = default!; - [Dependency] private readonly PullingSystem _pulling = default!; [Dependency] private readonly SharedTransformSystem _transform = default!; [Dependency] private readonly StandingStateSystem _standing = default!; [Dependency] private readonly SharedPhysicsSystem _physics = default!; + [Dependency] private readonly SharedRotationVisualsSystem _rotationVisuals = default!; /// public override void Initialize() @@ -51,45 +47,6 @@ public override void Initialize() InitializeBuckle(); InitializeStrap(); - } - - /// - /// Reattaches this entity to the strap, modifying its position and rotation. - /// - /// The entity to reattach. - /// The entity to reattach the buckleUid entity to. - private void ReAttach( - EntityUid buckleUid, - EntityUid strapUid, - BuckleComponent? buckleComp = null, - StrapComponent? strapComp = null) - { - if (!Resolve(strapUid, ref strapComp, false) - || !Resolve(buckleUid, ref buckleComp, false)) - return; - - _transform.SetCoordinates(buckleUid, new EntityCoordinates(strapUid, strapComp.BuckleOffsetClamped)); - - var buckleTransform = Transform(buckleUid); - - // Buckle subscribes to move for so this might fail. - // TODO: Make buckle not do that. - if (buckleTransform.ParentUid != strapUid) - return; - - _transform.SetLocalRotation(buckleUid, Angle.Zero, buckleTransform); - _joints.SetRelay(buckleUid, strapUid); - - switch (strapComp.Position) - { - case StrapPosition.None: - break; - case StrapPosition.Stand: - _standing.Stand(buckleUid); - break; - case StrapPosition.Down: - _standing.Down(buckleUid, false, false); - break; - } + InitializeInteraction(); } } diff --git a/Content.Shared/Climbing/Systems/ClimbSystem.cs b/Content.Shared/Climbing/Systems/ClimbSystem.cs index c570a821a6f..d2b5a25aee7 100644 --- a/Content.Shared/Climbing/Systems/ClimbSystem.cs +++ b/Content.Shared/Climbing/Systems/ClimbSystem.cs @@ -59,7 +59,7 @@ public override void Initialize() SubscribeLocalEvent(OnParentChange); SubscribeLocalEvent(OnDoAfter); SubscribeLocalEvent(OnClimbEndCollide); - SubscribeLocalEvent(OnBuckleChange); + SubscribeLocalEvent(OnBuckled); SubscribeLocalEvent(OnCanDragDropOn); SubscribeLocalEvent>(AddClimbableVerb); @@ -479,10 +479,8 @@ public void ForciblyStopClimbing(EntityUid uid, ClimbingComponent? climbing = nu StopClimb(uid, climbing, fixtures); } - private void OnBuckleChange(EntityUid uid, ClimbingComponent component, ref BuckleChangeEvent args) + private void OnBuckled(EntityUid uid, ClimbingComponent component, ref BuckledEvent args) { - if (!args.Buckling) - return; StopClimb(uid, component); } diff --git a/Content.Shared/Cuffs/SharedCuffableSystem.cs b/Content.Shared/Cuffs/SharedCuffableSystem.cs index 0d41a0eb36d..d70a1e63083 100644 --- a/Content.Shared/Cuffs/SharedCuffableSystem.cs +++ b/Content.Shared/Cuffs/SharedCuffableSystem.cs @@ -75,6 +75,7 @@ public override void Initialize() SubscribeLocalEvent(OnUnequipAttempt); SubscribeLocalEvent(OnBeingPulledAttempt); SubscribeLocalEvent(OnBuckleAttemptEvent); + SubscribeLocalEvent(OnUnbuckleAttemptEvent); SubscribeLocalEvent>(AddUncuffVerb); SubscribeLocalEvent(OnCuffableDoAfter); SubscribeLocalEvent(OnPull); @@ -200,21 +201,33 @@ private void OnBeingPulledAttempt(EntityUid uid, CuffableComponent component, Be args.Cancel(); } - private void OnBuckleAttemptEvent(EntityUid uid, CuffableComponent component, ref BuckleAttemptEvent args) + private void OnBuckleAttempt(Entity ent, EntityUid? user, ref bool cancelled, bool buckling, bool popup) { - // if someone else is doing it, let it pass. - if (args.UserEntity != uid) + if (cancelled || user != ent.Owner) return; - if (!TryComp(uid, out var hands) || component.CuffedHandCount != hands.Count) + if (!TryComp(ent, out var hands) || ent.Comp.CuffedHandCount != hands.Count) return; - args.Cancelled = true; - var message = args.Buckling + cancelled = true; + if (!popup) + return; + + var message = buckling ? Loc.GetString("handcuff-component-cuff-interrupt-buckled-message") : Loc.GetString("handcuff-component-cuff-interrupt-unbuckled-message"); - _popup.PopupClient(message, uid, args.UserEntity); + _popup.PopupClient(message, ent, user); + } + + private void OnBuckleAttemptEvent(Entity ent, ref BuckleAttemptEvent args) + { + OnBuckleAttempt(ent, args.User, ref args.Cancelled, true, args.Popup); + } + + private void OnUnbuckleAttemptEvent(Entity ent, ref UnbuckleAttemptEvent args) + { + OnBuckleAttempt(ent, args.User, ref args.Cancelled, false, args.Popup); } private void OnPull(EntityUid uid, CuffableComponent component, PullMessage args) diff --git a/Content.Shared/Foldable/FoldableSystem.cs b/Content.Shared/Foldable/FoldableSystem.cs index 10baf8165b5..2a846f4f234 100644 --- a/Content.Shared/Foldable/FoldableSystem.cs +++ b/Content.Shared/Foldable/FoldableSystem.cs @@ -26,7 +26,7 @@ public override void Initialize() SubscribeLocalEvent(OnStoreThisAttempt); SubscribeLocalEvent(OnFoldableOpenAttempt); - SubscribeLocalEvent(OnBuckleAttempt); + SubscribeLocalEvent(OnStrapAttempt); } private void OnHandleState(EntityUid uid, FoldableComponent component, ref AfterAutoHandleStateEvent args) @@ -53,9 +53,9 @@ public void OnStoreThisAttempt(EntityUid uid, FoldableComponent comp, ref StoreM args.Cancelled = true; } - public void OnBuckleAttempt(EntityUid uid, FoldableComponent comp, ref BuckleAttemptEvent args) + public void OnStrapAttempt(EntityUid uid, FoldableComponent comp, ref StrapAttemptEvent args) { - if (args.Buckling && comp.IsFolded) + if (comp.IsFolded) args.Cancelled = true; } diff --git a/Content.Shared/Interaction/RotateToFaceSystem.cs b/Content.Shared/Interaction/RotateToFaceSystem.cs index ed950240af6..fa213011ef1 100644 --- a/Content.Shared/Interaction/RotateToFaceSystem.cs +++ b/Content.Shared/Interaction/RotateToFaceSystem.cs @@ -1,7 +1,6 @@ using System.Numerics; using Content.Shared.ActionBlocker; using Content.Shared.Buckle.Components; -using Content.Shared.Mobs.Systems; using Content.Shared.Rotatable; using JetBrains.Annotations; @@ -83,24 +82,21 @@ public bool TryFaceAngle(EntityUid user, Angle diffAngle, TransformComponent? xf if (!_actionBlockerSystem.CanChangeDirection(user)) return false; - if (EntityManager.TryGetComponent(user, out BuckleComponent? buckle) && buckle.Buckled) + if (TryComp(user, out BuckleComponent? buckle) && buckle.BuckledTo is {} strap) { - var suid = buckle.LastEntityBuckledTo; - if (suid != null) - { - // We're buckled to another object. Is that object rotatable? - if (TryComp(suid.Value, out var rotatable) && rotatable.RotateWhileAnchored) - { - // Note the assumption that even if unanchored, user can only do spinnychair with an "independent wheel". - // (Since the user being buckled to it holds it down with their weight.) - // This is logically equivalent to RotateWhileAnchored. - // Barstools and office chairs have independent wheels, while regular chairs don't. - _transform.SetWorldRotation(Transform(suid.Value), diffAngle); - return true; - } - } - - return false; + // What if a person is strapped to a borg? + // I'm pretty sure this would allow them to be partially ratatouille'd + + // We're buckled to another object. Is that object rotatable? + if (!TryComp(strap, out var rotatable) || !rotatable.RotateWhileAnchored) + return false; + + // Note the assumption that even if unanchored, user can only do spinnychair with an "independent wheel". + // (Since the user being buckled to it holds it down with their weight.) + // This is logically equivalent to RotateWhileAnchored. + // Barstools and office chairs have independent wheels, while regular chairs don't. + _transform.SetWorldRotation(Transform(strap), diffAngle); + return true; } // user is not buckled in; apply to their transform diff --git a/Content.Shared/Mobs/Systems/MobStateSystem.Subscribers.cs b/Content.Shared/Mobs/Systems/MobStateSystem.Subscribers.cs index d9ef671afe2..f24f6f03993 100644 --- a/Content.Shared/Mobs/Systems/MobStateSystem.Subscribers.cs +++ b/Content.Shared/Mobs/Systems/MobStateSystem.Subscribers.cs @@ -1,4 +1,5 @@ using Content.Shared.Bed.Sleep; +using Content.Shared.Buckle.Components; using Content.Shared.CombatMode.Pacification; using Content.Shared.Damage.ForceSay; using Content.Shared.Emoting; @@ -10,15 +11,12 @@ using Content.Shared.Mobs.Components; using Content.Shared.Movement.Events; using Content.Shared.Pointing; -using Content.Shared.Projectiles; using Content.Shared.Pulling.Events; using Content.Shared.Speech; using Content.Shared.Standing; using Content.Shared.Strip.Components; using Content.Shared.Throwing; -using Content.Shared.Weapons.Ranged.Components; using Robust.Shared.Physics.Components; -using Robust.Shared.Physics.Events; namespace Content.Shared.Mobs.Systems; @@ -46,6 +44,16 @@ private void SubscribeEvents() SubscribeLocalEvent(OnSleepAttempt); SubscribeLocalEvent(OnCombatModeShouldHandInteract); SubscribeLocalEvent(OnAttemptPacifiedAttack); + + SubscribeLocalEvent(OnUnbuckleAttempt); + } + + private void OnUnbuckleAttempt(Entity ent, ref UnbuckleAttemptEvent args) + { + // TODO is this necessary? + // Shouldn't the interaction have already been blocked by a general interaction check? + if (args.User == ent.Owner && IsIncapacitated(ent)) + args.Cancelled = true; } private void OnStateExitSubscribers(EntityUid target, MobStateComponent component, MobState state) diff --git a/Content.Shared/Movement/Pulling/Events/PullStartedMessage.cs b/Content.Shared/Movement/Pulling/Events/PullStartedMessage.cs index 29460e1dfc1..c0775b4ce2d 100644 --- a/Content.Shared/Movement/Pulling/Events/PullStartedMessage.cs +++ b/Content.Shared/Movement/Pulling/Events/PullStartedMessage.cs @@ -1,9 +1,6 @@ namespace Content.Shared.Movement.Pulling.Events; -public sealed class PullStartedMessage : PullMessage -{ - public PullStartedMessage(EntityUid pullerUid, EntityUid pullableUid) : - base(pullerUid, pullableUid) - { - } -} +/// +/// Event raised directed BOTH at the puller and pulled entity when a pull starts. +/// +public sealed class PullStartedMessage(EntityUid pullerUid, EntityUid pullableUid) : PullMessage(pullerUid, pullableUid); diff --git a/Content.Shared/Movement/Pulling/Events/PullStoppedMessage.cs b/Content.Shared/Movement/Pulling/Events/PullStoppedMessage.cs index 47aa34562fb..6df4d174839 100644 --- a/Content.Shared/Movement/Pulling/Events/PullStoppedMessage.cs +++ b/Content.Shared/Movement/Pulling/Events/PullStoppedMessage.cs @@ -1,13 +1,6 @@ -using Robust.Shared.Physics.Components; - -namespace Content.Shared.Movement.Pulling.Events; +namespace Content.Shared.Movement.Pulling.Events; /// -/// Raised directed on both puller and pullable. +/// Event raised directed BOTH at the puller and pulled entity when a pull starts. /// -public sealed class PullStoppedMessage : PullMessage -{ - public PullStoppedMessage(EntityUid pullerUid, EntityUid pulledUid) : base(pullerUid, pulledUid) - { - } -} +public sealed class PullStoppedMessage(EntityUid pullerUid, EntityUid pulledUid) : PullMessage(pullerUid, pulledUid); diff --git a/Content.Shared/Movement/Pulling/Systems/PullingSystem.cs b/Content.Shared/Movement/Pulling/Systems/PullingSystem.cs index a76f2472886..11a1d94b29b 100644 --- a/Content.Shared/Movement/Pulling/Systems/PullingSystem.cs +++ b/Content.Shared/Movement/Pulling/Systems/PullingSystem.cs @@ -1,4 +1,3 @@ -using System.Numerics; using Content.Shared.ActionBlocker; using Content.Shared.Administration.Logs; using Content.Shared.Alert; @@ -17,7 +16,6 @@ using Content.Shared.Projectiles; using Content.Shared.Pulling.Events; using Content.Shared.Standing; -using Content.Shared.Throwing; using Content.Shared.Verbs; using Robust.Shared.Containers; using Robust.Shared.Input.Binding; @@ -29,6 +27,8 @@ using Robust.Shared.Physics.Systems; using Robust.Shared.Player; using Robust.Shared.Timing; +using Content.Shared.Throwing; +using System.Numerics; namespace Content.Shared.Movement.Pulling.Systems; @@ -73,11 +73,25 @@ public override void Initialize() SubscribeLocalEvent(OnRefreshMovespeed); SubscribeLocalEvent(OnDropHandItems); + SubscribeLocalEvent(OnBuckled); + SubscribeLocalEvent(OnGotBuckled); + CommandBinds.Builder .Bind(ContentKeyFunctions.MovePulledObject, new PointerInputCmdHandler(OnRequestMovePulledObject)) .Bind(ContentKeyFunctions.ReleasePulledObject, InputCmdHandler.FromDelegate(OnReleasePulledObject, handle: false)) .Register(); } + private void OnBuckled(Entity ent, ref StrappedEvent args) + { + // Prevent people from pulling the entity they are buckled to + if (ent.Comp.Puller == args.Buckle.Owner && !args.Buckle.Comp.PullStrap) + StopPulling(ent, ent); + } + + private void OnGotBuckled(Entity ent, ref BuckledEvent args) + { + StopPulling(ent, ent); + } public override void Shutdown() { @@ -174,7 +188,8 @@ private void OnDropHandItems(EntityUid uid, PullerComponent pullerComp, DropHand private void OnPullerContainerInsert(Entity ent, ref EntGotInsertedIntoContainerMessage args) { - if (ent.Comp.Pulling == null) return; + if (ent.Comp.Pulling == null) + return; if (!TryComp(ent.Comp.Pulling.Value, out PullableComponent? pulling)) return; @@ -307,8 +322,18 @@ private void OnJointRemoved(EntityUid uid, PullableComponent component, JointRem /// private void StopPulling(EntityUid pullableUid, PullableComponent pullableComp) { + if (pullableComp.Puller == null) + return; + if (!_timing.ApplyingState) { + // Joint shutdown + if (pullableComp.PullJointId != null) + { + _joints.RemoveJoint(pullableUid, pullableComp.PullJointId); + pullableComp.PullJointId = null; + } + if (TryComp(pullableUid, out var pullablePhysics)) { _physics.SetFixedRotation(pullableUid, pullableComp.PrevFixedRotation, body: pullablePhysics); @@ -440,15 +465,6 @@ public bool CanPull(EntityUid puller, EntityUid pullableUid, PullerComponent? pu return false; } - if (EntityManager.TryGetComponent(puller, out BuckleComponent? buckle)) - { - // Prevent people pulling the chair they're on, etc. - if (buckle is { PullStrap: false, Buckled: true } && (buckle.LastEntityBuckledTo == pullableUid)) - { - return false; - } - } - var getPulled = new BeingPulledAttemptEvent(puller, pullableUid); RaiseLocalEvent(pullableUid, getPulled, true); var startPull = new StartPullAttemptEvent(puller, pullableUid); @@ -492,11 +508,8 @@ public bool TryStartPull(EntityUid pullerUid, EntityUid pullableUid, if (!CanPull(pullerUid, pullableUid)) return false; - if (!EntityManager.TryGetComponent(pullerUid, out var pullerPhysics) || - !EntityManager.TryGetComponent(pullableUid, out var pullablePhysics)) - { + if (!HasComp(pullerUid) || !TryComp(pullableUid, out PhysicsComponent? pullablePhysics)) return false; - } // Ensure that the puller is not currently pulling anything. if (TryComp(pullerComp.Pulling, out var oldPullable) @@ -540,7 +553,7 @@ public bool TryStartPull(EntityUid pullerUid, EntityUid pullableUid, { // Joint startup var union = _physics.GetHardAABB(pullerUid).Union(_physics.GetHardAABB(pullableUid, body: pullablePhysics)); - var length = Math.Max((float) union.Size.X, (float) union.Size.Y) * 0.75f; + var length = Math.Max(union.Size.X, union.Size.Y) * 0.75f; var joint = _joints.CreateDistanceJoint(pullableUid, pullerUid, id: pullableComp.PullJointId); joint.CollideConnected = false; @@ -584,17 +597,6 @@ public bool TryStopPull(EntityUid pullableUid, PullableComponent pullable, Entit if (msg.Cancelled) return false; - // Stop pulling confirmed! - if (!_timing.ApplyingState) - { - // Joint shutdown - if (pullable.PullJointId != null) - { - _joints.RemoveJoint(pullableUid, pullable.PullJointId); - pullable.PullJointId = null; - } - } - StopPulling(pullableUid, pullable); return true; } diff --git a/Resources/Prototypes/Entities/Structures/Furniture/chairs.yml b/Resources/Prototypes/Entities/Structures/Furniture/chairs.yml index 14b3270ba88..d65d652ff42 100644 --- a/Resources/Prototypes/Entities/Structures/Furniture/chairs.yml +++ b/Resources/Prototypes/Entities/Structures/Furniture/chairs.yml @@ -358,6 +358,8 @@ components: - type: Foldable folded: true + - type: Strap + enabled: False - type: entity name: steel bench diff --git a/Resources/Prototypes/Entities/Structures/Furniture/rollerbeds.yml b/Resources/Prototypes/Entities/Structures/Furniture/rollerbeds.yml index 161ea25bc43..b3cfe6ade3f 100644 --- a/Resources/Prototypes/Entities/Structures/Furniture/rollerbeds.yml +++ b/Resources/Prototypes/Entities/Structures/Furniture/rollerbeds.yml @@ -79,6 +79,8 @@ components: - type: Foldable folded: true + - type: Strap + enabled: False - type: entity id: CheapRollerBed @@ -105,6 +107,8 @@ components: - type: Foldable folded: true + - type: Strap + enabled: False - type: entity id: EmergencyRollerBed @@ -131,3 +135,5 @@ components: - type: Foldable folded: true + - type: Strap + enabled: False