From c89af12574fbbe4354b3125098561825c5139b6a Mon Sep 17 00:00:00 2001 From: sleepyyapril Date: Thu, 14 Nov 2024 14:55:09 -0400 Subject: [PATCH] Revert "Partial buckling refactor (#29031)" This reverts commit 1741356fc1e6b4055a9787c55df7c8082b10788b. --- 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 | 14 +- .../{Movement => Interaction}/MovementTest.cs | 3 +- .../Tests/Movement/BuckleMovementTest.cs | 63 -- .../Tests/Movement/PullingTest.cs | 73 --- .../{Movement => Slipping}/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 | 52 +- .../Buckle/SharedBuckleSystem.Buckle.cs | 557 +++++++++--------- .../Buckle/SharedBuckleSystem.Interaction.cs | 171 ------ .../Buckle/SharedBuckleSystem.Strap.cs | 66 ++- Content.Shared/Buckle/SharedBuckleSystem.cs | 49 +- .../Climbing/Systems/ClimbSystem.cs | 11 +- 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 | 131 +++- .../Entities/Structures/Furniture/chairs.yml | 2 - .../Structures/Furniture/rollerbeds.yml | 6 - 35 files changed, 716 insertions(+), 1016 deletions(-) delete mode 100644 Content.IntegrationTests/Tests/Buckle/BuckleDragTest.cs rename Content.IntegrationTests/Tests/{Movement => Interaction}/MovementTest.cs (95%) delete mode 100644 Content.IntegrationTests/Tests/Movement/BuckleMovementTest.cs delete mode 100644 Content.IntegrationTests/Tests/Movement/PullingTest.cs rename Content.IntegrationTests/Tests/{Movement => Slipping}/SlippingTest.cs (92%) delete mode 100644 Content.Shared/Buckle/SharedBuckleSystem.Interaction.cs diff --git a/Content.Client/Buckle/BuckleSystem.cs b/Content.Client/Buckle/BuckleSystem.cs index 4429996aca3..d4614210d9f 100644 --- a/Content.Client/Buckle/BuckleSystem.cs +++ b/Content.Client/Buckle/BuckleSystem.cs @@ -3,7 +3,6 @@ using Content.Shared.Buckle.Components; using Content.Shared.Rotation; using Robust.Client.GameObjects; -using Robust.Shared.GameStates; namespace Content.Client.Buckle; @@ -15,63 +14,40 @@ public override void Initialize() { base.Initialize(); - SubscribeLocalEvent(OnHandleState); + SubscribeLocalEvent(OnBuckleAfterAutoHandleState); SubscribeLocalEvent(OnAppearanceChange); - SubscribeLocalEvent(OnStrapMoveEvent); } - private void OnStrapMoveEvent(EntityUid uid, StrapComponent component, ref MoveEvent args) + private void OnBuckleAfterAutoHandleState(EntityUid uid, BuckleComponent component, ref AfterAutoHandleStateEvent args) { - // 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 + ActionBlocker.UpdateCanMove(uid); - // 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) + if (!TryComp(uid, out var ownerSprite)) return; - if (!TryComp(uid, out var strapSprite)) + // 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; return; + } - var isNorth = Transform(uid).LocalRotation.GetCardinalDir() == Direction.North; - foreach (var buckledEntity in component.BuckledEntities) + // If here, we're not turning north and should restore the saved draw depth. + if (component.OriginalDrawDepth.HasValue) { - 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; - } + ownerSprite.DrawDepth = component.OriginalDrawDepth.Value; + component.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 deleted file mode 100644 index 8df151d5a0e..00000000000 --- a/Content.IntegrationTests/Tests/Buckle/BuckleDragTest.cs +++ /dev/null @@ -1,56 +0,0 @@ -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 5681013d7b0..7c700d9fb8a 100644 --- a/Content.IntegrationTests/Tests/Buckle/BuckleTest.cs +++ b/Content.IntegrationTests/Tests/Buckle/BuckleTest.cs @@ -90,6 +90,7 @@ 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 @@ -109,6 +110,8 @@ 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. @@ -118,7 +121,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.TryUnbuckle(human, human), Is.False); + Assert.That(buckleSystem.ToggleBuckle(human, human, chair, buckle: buckle), Is.False); Assert.That(buckle.Buckled); #pragma warning restore NUnit2045 }); @@ -145,6 +148,7 @@ await server.WaitAssertion(() => // Unbuckle, strap Assert.That(strap.BuckledEntities, Is.Empty); + Assert.That(strap.OccupiedSize, Is.Zero); }); #pragma warning disable NUnit2045 // Interdependent asserts. @@ -155,9 +159,9 @@ await server.WaitAssertion(() => // On cooldown Assert.That(buckleSystem.TryUnbuckle(human, human, buckleComp: buckle), Is.False); Assert.That(buckle.Buckled); - Assert.That(buckleSystem.TryUnbuckle(human, human), Is.False); + Assert.That(buckleSystem.ToggleBuckle(human, human, chair, buckle: buckle), Is.False); Assert.That(buckle.Buckled); - Assert.That(buckleSystem.TryUnbuckle(human, human), Is.False); + Assert.That(buckleSystem.ToggleBuckle(human, human, chair, buckle: buckle), Is.False); Assert.That(buckle.Buckled); #pragma warning restore NUnit2045 }); @@ -184,6 +188,7 @@ 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 @@ -196,10 +201,12 @@ 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 - buckleSystem.Unbuckle(human, human); + Assert.That(buckleSystem.TryUnbuckle(human, human, true, buckleComp: buckle)); Assert.Multiple(() => { Assert.That(buckle.Buckled, Is.False); @@ -303,7 +310,7 @@ await server.WaitAssertion(() => // Break our guy's kneecaps foreach (var leg in legs) { - entityManager.DeleteEntity(leg.Id); + xformSystem.DetachParentToNull(leg.Id, entityManager.GetComponent(leg.Id)); } }); @@ -320,8 +327,7 @@ await server.WaitAssertion(() => Assert.That(hand.HeldEntity, Is.Null); } - buckleSystem.Unbuckle(human, human); - Assert.That(buckle.Buckled, Is.False); + buckleSystem.TryUnbuckle(human, human, true, buckleComp: buckle); }); await pair.CleanReturnAsync(); diff --git a/Content.IntegrationTests/Tests/Climbing/ClimbingTest.cs b/Content.IntegrationTests/Tests/Climbing/ClimbingTest.cs index 2db0a9acd3d..d8d3086520e 100644 --- a/Content.IntegrationTests/Tests/Climbing/ClimbingTest.cs +++ b/Content.IntegrationTests/Tests/Climbing/ClimbingTest.cs @@ -1,6 +1,5 @@ #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 74d0e924217..76911eba5f7 100644 --- a/Content.IntegrationTests/Tests/Construction/Interaction/CraftingTests.cs +++ b/Content.IntegrationTests/Tests/Construction/Interaction/CraftingTests.cs @@ -59,6 +59,11 @@ 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. /// @@ -88,22 +93,28 @@ 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.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.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)); - await FindEntity(Spear, shouldSucceed: false); + await FindEntity(Spear, shouldSucceed: false); + }); // Cancel the DoAfter. Should drop ingredients to the floor. await CancelDoAfters(); - 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.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)); + }); // Re-attempt the do-after #pragma warning disable CS4014 // Legacy construction code uses DoAfterAwait. See above. @@ -112,17 +123,24 @@ public async Task CancelCraft() await RunTicks(1); // DoAfter is in progress. Entity not spawned, ingredients are in a container. - Assert.That(ActiveDoAfters.Count(), Is.EqualTo(1)); - Assert.That(sys.IsEntityInContainer(shard), Is.True); - await FindEntity(Spear, shouldSucceed: false); + Assert.Multiple(async () => + { + 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. - await FindEntity(Spear); - Assert.That(sys.IsEntityInContainer(rods), Is.False); - Assert.That(sys.IsEntityInContainer(wires), Is.False); - Assert.That(SEntMan.Deleted(shard)); + 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)); + }); } +#endif } diff --git a/Content.IntegrationTests/Tests/Interaction/InteractionTest.Helpers.cs b/Content.IntegrationTests/Tests/Interaction/InteractionTest.Helpers.cs index 05ccf6e408a..39f3cdf251b 100644 --- a/Content.IntegrationTests/Tests/Interaction/InteractionTest.Helpers.cs +++ b/Content.IntegrationTests/Tests/Interaction/InteractionTest.Helpers.cs @@ -82,9 +82,8 @@ protected async Task CraftItem(string prototype, bool shouldSucceed = true) /// /// Spawn an entity entity and set it as the target. /// - [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) + [MemberNotNull(nameof(Target))] + protected async Task SpawnTarget(string prototype) { Target = NetEntity.Invalid; await Server.WaitPost(() => @@ -94,9 +93,7 @@ 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 @@ -1012,17 +1009,14 @@ 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 = null) + NetEntity cursorEntity = default) { await SetKey(key, BoundKeyState.Down, coordinates, cursorEntity); await RunTicks(ticks); @@ -1031,17 +1025,15 @@ protected async Task PressKey( } /// - /// Make the client press or release a key. - /// This will default to using the entity and coordinates. + /// Make the client press or release a key /// protected async Task SetKey( BoundKeyFunction key, BoundKeyState state, NetCoordinates? coordinates = null, - NetEntity? cursorEntity = null) + NetEntity cursorEntity = default) { var coords = coordinates ?? TargetCoords; - var target = cursorEntity ?? Target ?? default; ScreenCoordinates screen = default; var funcId = InputManager.NetworkBindMap.KeyFunctionID(key); @@ -1050,7 +1042,7 @@ protected async Task SetKey( State = state, Coordinates = CEntMan.GetCoordinates(coords), ScreenCoordinates = screen, - Uid = CEntMan.GetEntity(target), + Uid = CEntMan.GetEntity(cursorEntity), }; 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 5b7c9071115..42f64b344cd 100644 --- a/Content.IntegrationTests/Tests/Interaction/InteractionTest.cs +++ b/Content.IntegrationTests/Tests/Interaction/InteractionTest.cs @@ -84,7 +84,6 @@ public abstract partial class InteractionTest protected NetEntity? Target; protected EntityUid? STarget => ToServer(Target); - protected EntityUid? CTarget => ToClient(Target); /// @@ -127,6 +126,7 @@ 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 = @" @@ -139,8 +139,6 @@ public abstract partial class InteractionTest - type: Hands - type: MindContainer - type: Stripping - - type: Puller - - type: Physics - type: Tag tags: - CanPilot @@ -204,11 +202,11 @@ await Server.WaitPost(() => SEntMan.System().WipeMind(ServerSession.ContentData()?.Mind); old = cPlayerMan.LocalEntity; - SPlayer = SEntMan.SpawnEntity(PlayerPrototype, SEntMan.GetCoordinates(PlayerCoords)); - Player = SEntMan.GetNetEntity(SPlayer); - Server.PlayerMan.SetAttachedEntity(ServerSession, SPlayer); - Hands = SEntMan.GetComponent(SPlayer); - DoAfters = SEntMan.GetComponent(SPlayer); + Player = SEntMan.GetNetEntity(SEntMan.SpawnEntity(PlayerPrototype, SEntMan.GetCoordinates(PlayerCoords))); + var serverPlayerEnt = SEntMan.GetEntity(Player); + Server.PlayerMan.SetAttachedEntity(ServerSession, serverPlayerEnt); + Hands = SEntMan.GetComponent(serverPlayerEnt); + DoAfters = SEntMan.GetComponent(serverPlayerEnt); }); // Check player got attached. diff --git a/Content.IntegrationTests/Tests/Movement/MovementTest.cs b/Content.IntegrationTests/Tests/Interaction/MovementTest.cs similarity index 95% rename from Content.IntegrationTests/Tests/Movement/MovementTest.cs rename to Content.IntegrationTests/Tests/Interaction/MovementTest.cs index ad7b1d0459f..dc5aec92cfc 100644 --- a/Content.IntegrationTests/Tests/Movement/MovementTest.cs +++ b/Content.IntegrationTests/Tests/Interaction/MovementTest.cs @@ -1,9 +1,8 @@ #nullable enable using System.Numerics; -using Content.IntegrationTests.Tests.Interaction; using Robust.Shared.GameObjects; -namespace Content.IntegrationTests.Tests.Movement; +namespace Content.IntegrationTests.Tests.Interaction; /// /// 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/BuckleMovementTest.cs b/Content.IntegrationTests/Tests/Movement/BuckleMovementTest.cs deleted file mode 100644 index 8d91855098f..00000000000 --- a/Content.IntegrationTests/Tests/Movement/BuckleMovementTest.cs +++ /dev/null @@ -1,63 +0,0 @@ -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/Movement/PullingTest.cs b/Content.IntegrationTests/Tests/Movement/PullingTest.cs deleted file mode 100644 index d96c4ea0e56..00000000000 --- a/Content.IntegrationTests/Tests/Movement/PullingTest.cs +++ /dev/null @@ -1,73 +0,0 @@ -#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/Movement/SlippingTest.cs b/Content.IntegrationTests/Tests/Slipping/SlippingTest.cs similarity index 92% rename from Content.IntegrationTests/Tests/Movement/SlippingTest.cs rename to Content.IntegrationTests/Tests/Slipping/SlippingTest.cs index 9ac84a0a586..28da7a94658 100644 --- a/Content.IntegrationTests/Tests/Movement/SlippingTest.cs +++ b/Content.IntegrationTests/Tests/Slipping/SlippingTest.cs @@ -11,7 +11,7 @@ using Robust.Shared.IoC; using Robust.Shared.Maths; -namespace Content.IntegrationTests.Tests.Movement; +namespace Content.IntegrationTests.Tests.Slipping; public sealed class SlippingTest : MovementTest { @@ -41,14 +41,18 @@ 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 089ce322366..976ef5139c3 100644 --- a/Content.Server/Bed/BedSystem.cs +++ b/Content.Server/Bed/BedSystem.cs @@ -15,7 +15,6 @@ 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 { @@ -31,31 +30,27 @@ public sealed class BedSystem : EntitySystem public override void Initialize() { base.Initialize(); - SubscribeLocalEvent(OnStrapped); - SubscribeLocalEvent(OnUnstrapped); - SubscribeLocalEvent(OnStasisStrapped); - SubscribeLocalEvent(OnStasisUnstrapped); + SubscribeLocalEvent(ManageUpdateList); + SubscribeLocalEvent(OnBuckleChange); SubscribeLocalEvent(OnPowerChanged); SubscribeLocalEvent(OnEmagged); SubscribeLocalEvent(OnRefreshParts); SubscribeLocalEvent(OnUpgradeExamine); } - private void OnStrapped(Entity bed, ref StrappedEvent args) + private void ManageUpdateList(EntityUid uid, HealOnBuckleComponent component, ref BuckleChangeEvent args) { - EnsureComp(bed); - bed.Comp.NextHealTime = _timing.CurTime + TimeSpan.FromSeconds(bed.Comp.HealTime); - _actionsSystem.AddAction(args.Buckle, ref bed.Comp.SleepAction, SleepingSystem.SleepActionId, bed); + if (args.Buckling) + { + AddComp(uid); + component.NextHealTime = _timing.CurTime + TimeSpan.FromSeconds(component.HealTime); + _actionsSystem.AddAction(args.BuckledEntity, ref component.SleepAction, SleepingSystem.SleepActionId, uid); + return; + } - // 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); + _actionsSystem.RemoveAction(args.BuckledEntity, component.SleepAction); + _sleepingSystem.TryWaking(args.BuckledEntity); + RemComp(uid); } public override void Update(float frameTime) @@ -94,22 +89,18 @@ private void UpdateAppearance(EntityUid uid, bool isOn) _appearance.SetData(uid, StasisBedVisuals.IsOn, isOn); } - private void OnStasisStrapped(Entity bed, ref StrappedEvent args) + private void OnBuckleChange(EntityUid uid, StasisBedComponent component, ref BuckleChangeEvent args) { - if (!HasComp(args.Buckle) || !this.IsPowered(bed, EntityManager)) + // In testing this also received an unbuckle event when the bed is destroyed + // So don't worry about that + if (!HasComp(args.BuckledEntity)) return; - 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)) + if (!this.IsPowered(uid, EntityManager)) return; - var metabolicEvent = new ApplyMetabolicMultiplierEvent(args.Buckle, bed.Comp.Multiplier, false); - RaiseLocalEvent(args.Buckle, ref metabolicEvent); + var metabolicEvent = new ApplyMetabolicMultiplierEvent(args.BuckledEntity, component.Multiplier, args.Buckling); + RaiseLocalEvent(args.BuckledEntity, 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 3c6f3a4382b..f29fe30429f 100644 --- a/Content.Server/Bed/Components/HealOnBuckleComponent.cs +++ b/Content.Server/Bed/Components/HealOnBuckleComponent.cs @@ -5,26 +5,19 @@ namespace Content.Server.Bed.Components [RegisterComponent] public sealed partial class HealOnBuckleComponent : Component { - /// - /// Damage to apply to entities that are strapped to this entity. - /// - [DataField(required: true)] + [DataField("damage", required: true)] + [ViewVariables(VVAccess.ReadWrite)] public DamageSpecifier Damage = default!; - /// - /// How frequently the damage should be applied, in seconds. - /// - [DataField(required: false)] - public float HealTime = 1f; + [DataField("healTime", required: false)] + [ViewVariables(VVAccess.ReadWrite)] + public float HealTime = 1f; // How often the bed applies the damage - /// - /// Damage multiplier that gets applied if the entity is sleeping. - /// - [DataField] + [DataField("sleepMultiplier")] public float SleepMultiplier = 3f; public TimeSpan NextHealTime = TimeSpan.Zero; //Next heal - [DataField] public EntityUid? SleepAction; + [DataField("sleepAction")] public EntityUid? SleepAction; } } diff --git a/Content.Server/Bed/Components/HealOnBuckleHealing.cs b/Content.Server/Bed/Components/HealOnBuckleHealing.cs index aaa82c737c5..a944e67e12d 100644 --- a/Content.Server/Bed/Components/HealOnBuckleHealing.cs +++ b/Content.Server/Bed/Components/HealOnBuckleHealing.cs @@ -1,6 +1,5 @@ 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 6e0042b2df8..bb4096a2a5e 100644 --- a/Content.Server/Bed/Components/StasisBedComponent.cs +++ b/Content.Server/Bed/Components/StasisBedComponent.cs @@ -12,8 +12,7 @@ public sealed partial class StasisBedComponent : Component /// /// What the metabolic update rate will be multiplied by (higher = slower metabolism) /// - [ViewVariables(VVAccess.ReadOnly)] // Writing is is not supported. ApplyMetabolicMultiplierEvent needs to be refactored first - [DataField] + [ViewVariables(VVAccess.ReadWrite)] 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 7804b2e87fa..d1fad6541ba 100644 --- a/Content.Server/Body/Systems/BloodstreamSystem.cs +++ b/Content.Server/Body/Systems/BloodstreamSystem.cs @@ -280,9 +280,6 @@ 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 a7eec8e3c02..066bf0a1c5b 100644 --- a/Content.Server/Body/Systems/MetabolizerSystem.cs +++ b/Content.Server/Body/Systems/MetabolizerSystem.cs @@ -67,9 +67,6 @@ 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; @@ -235,9 +232,6 @@ 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 116e8fe7c7f..207665d786f 100644 --- a/Content.Server/NPC/HTN/PrimitiveTasks/Operators/Combat/UnbuckleOperator.cs +++ b/Content.Server/NPC/HTN/PrimitiveTasks/Operators/Combat/UnbuckleOperator.cs @@ -1,9 +1,11 @@ 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")] @@ -19,7 +21,10 @@ public override void Startup(NPCBlackboard blackboard) { base.Startup(blackboard); var owner = blackboard.GetValue(NPCBlackboard.Owner); - _buckle.Unbuckle(owner, null); + if (!_entManager.TryGetComponent(owner, out var buckle) || !buckle.Buckled) + return; + + _buckle.TryUnbuckle(owner, owner, true, buckle); } 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 ee86e6d4de0..cf28b56d51f 100644 --- a/Content.Shared/Buckle/Components/BuckleComponent.cs +++ b/Content.Shared/Buckle/Components/BuckleComponent.cs @@ -1,15 +1,10 @@ -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; -/// -/// This component allows an entity to be buckled to an entity with a . -/// -[RegisterComponent, NetworkedComponent] +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState(true)] [Access(typeof(SharedBuckleSystem))] public sealed partial class BuckleComponent : Component { @@ -19,23 +14,31 @@ 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. /// - [MemberNotNullWhen(true, nameof(BuckledTo))] - public bool Buckled => BuckledTo != null; + [ViewVariables(VVAccess.ReadWrite)] + [AutoNetworkedField] + public bool Buckled; + + [ViewVariables] + [AutoNetworkedField] + public EntityUid? LastEntityBuckledTo; /// /// Whether or not collisions should be possible with the entity we are strapped to /// - [DataField] + [ViewVariables(VVAccess.ReadWrite)] + [DataField, AutoNetworkedField] 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; @@ -44,18 +47,20 @@ 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. /// - [DataField(customTypeSerializer: typeof(TimeOffsetSerializer))] - public TimeSpan? BuckleTime; + [ViewVariables] + public TimeSpan BuckleTime; /// /// The strap that this component is buckled to. /// - [DataField] + [ViewVariables] + [AutoNetworkedField] public EntityUid? BuckledTo; /// @@ -63,6 +68,7 @@ public sealed partial class BuckleComponent : Component /// . /// [DataField] + [ViewVariables(VVAccess.ReadWrite)] public int Size = 100; /// @@ -71,90 +77,11 @@ 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 readonly record struct UnstrappedEvent(Entity Strap, Entity Buckle); +public record struct BuckleAttemptEvent(EntityUid StrapEntity, EntityUid BuckledEntity, EntityUid UserEntity, bool Buckling, bool Cancelled = false); -/// -/// Event raised directed at a buckle entity after it has been unbuckled from some strap entity. -/// [ByRefEvent] -public readonly record struct UnbuckledEvent(Entity Strap, Entity Buckle); +public readonly record struct BuckleChangeEvent(EntityUid StrapEntity, EntityUid BuckledEntity, bool Buckling); [Serializable, NetSerializable] public enum BuckleVisuals diff --git a/Content.Shared/Buckle/Components/StrapComponent.cs b/Content.Shared/Buckle/Components/StrapComponent.cs index 7849fbfb9f4..72c92ebf84b 100644 --- a/Content.Shared/Buckle/Components/StrapComponent.cs +++ b/Content.Shared/Buckle/Components/StrapComponent.cs @@ -12,77 +12,117 @@ namespace Content.Shared.Buckle.Components; public sealed partial class StrapComponent : Component { /// - /// The entities that are currently buckled to this strap. + /// The entities that are currently buckled /// - [ViewVariables] + [AutoNetworkedField] + [ViewVariables] // TODO serialization public HashSet BuckledEntities = new(); /// /// Entities that this strap accepts and can buckle /// If null it accepts any entity /// - [DataField] + [DataField, ViewVariables(VVAccess.ReadWrite)] public EntityWhitelist? Whitelist; /// /// Entities that this strap does not accept and cannot buckle. /// - [DataField] + [DataField, ViewVariables(VVAccess.ReadWrite)] 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 /// - [DataField, AutoNetworkedField] + [ViewVariables] 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] - public ProtoId BuckledAlertType = "Buckled"; + [ViewVariables(VVAccess.ReadWrite)] + public AlertType BuckledAlertType = AlertType.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 be9cdf9c623..c07d90b3a2b 100644 --- a/Content.Shared/Buckle/SharedBuckleSystem.Buckle.cs +++ b/Content.Shared/Buckle/SharedBuckleSystem.Buckle.cs @@ -1,46 +1,36 @@ 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.Whitelist; -using Robust.Shared.Containers; -using Robust.Shared.GameStates; -using Robust.Shared.Map; +using Content.Shared.Verbs; 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(OnParentChanged); - SubscribeLocalEvent(OnInserted); - - SubscribeLocalEvent(OnPullAttempt); - SubscribeLocalEvent(OnBeingPulledAttempt); - SubscribeLocalEvent(OnPullStarted); - + SubscribeLocalEvent(OnBuckleInteractHand); + SubscribeLocalEvent>(AddUnbuckleVerb); SubscribeLocalEvent(OnBuckleInsertIntoEntityStorageAttempt); SubscribeLocalEvent(OnBucklePreventCollide); @@ -48,93 +38,66 @@ private void InitializeBuckle() SubscribeLocalEvent(OnBuckleStandAttempt); SubscribeLocalEvent(OnBuckleThrowPushbackAttempt); SubscribeLocalEvent(OnBuckleUpdateCanMove); - - SubscribeLocalEvent(OnGetState); } - private void OnGetState(Entity ent, ref ComponentGetState args) + private void OnBuckleComponentStartup(EntityUid uid, BuckleComponent component, ComponentStartup args) { - args.State = new BuckleState(GetNetEntity(ent.Comp.BuckledTo), ent.Comp.DontCollide, ent.Comp.BuckleTime); + UpdateBuckleStatus(uid, component); } - private void OnBuckleComponentShutdown(Entity ent, ref ComponentShutdown args) + private void OnBuckleComponentShutdown(EntityUid uid, BuckleComponent component, ComponentShutdown args) { - Unbuckle(ent!, null); - } - - #region Pulling + TryUnbuckle(uid, uid, true, component); - 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(); + component.BuckleTime = default; } - private void OnBeingPulledAttempt(Entity ent, ref BeingPulledAttemptEvent args) + private void OnBuckleMove(EntityUid uid, BuckleComponent component, ref MoveEvent ev) { - if (args.Cancelled || !ent.Comp.Buckled) + if (component.BuckledTo is not {} strapUid) return; - if (!CanUnbuckle(ent!, args.Puller, false)) - args.Cancel(); - } - - private void OnPullStarted(Entity ent, ref PullStartedMessage args) - { - Unbuckle(ent!, args.PullerUid); - } - - #endregion + if (!TryComp(strapUid, out var strapComp)) + return; - #region Transform + var strapPosition = Transform(strapUid).Coordinates; + if (ev.NewPosition.EntityId.IsValid() && ev.NewPosition.InRange(EntityManager, _transform, strapPosition, strapComp.MaxBuckleDistance)) + return; - private void OnParentChanged(Entity ent, ref EntParentChangedMessage args) - { - BuckleTransformCheck(ent, args.Transform); + TryUnbuckle(uid, uid, true, component); } - private void OnInserted(Entity ent, ref EntGotInsertedIntoContainerMessage args) + private void OnBuckleInteractHand(EntityUid uid, BuckleComponent component, InteractHandEvent args) { - BuckleTransformCheck(ent, Transform(ent)); - } + if (!component.Buckled) + return; - private void OnBuckleMove(Entity ent, ref MoveEvent ev) - { - BuckleTransformCheck(ent, ev.Component); + if (TryUnbuckle(uid, args.User, buckleComp: component)) + args.Handled = true; } - /// - /// Check if the entity should get unbuckled as a result of transform or container changes. - /// - private void BuckleTransformCheck(Entity buckle, TransformComponent xform) + private void AddUnbuckleVerb(EntityUid uid, BuckleComponent component, GetVerbsEvent args) { - if (_gameTiming.ApplyingState) + if (!args.CanAccess || !args.CanInteract || !component.Buckled) return; - if (buckle.Comp.BuckledTo is not { } strapUid) - return; - - if (!TryComp(strapUid, out var strapComp)) + InteractionVerb verb = new() { - Log.Error($"Encountered buckle entity {ToPrettyString(buckle)} without a valid strap entity {ToPrettyString(strapUid)}"); - SetBuckledTo(buckle, null); - return; - } + 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 (xform.ParentUid != strapUid || _container.IsEntityInContainer(buckle)) + if (args.Target == args.User && args.Using == null) { - Unbuckle(buckle, (strapUid, strapComp), null); - return; + // 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; } - var delta = (xform.LocalPosition - strapComp.BuckleOffset).LengthSquared(); - if (delta > 1e-5) - Unbuckle(buckle, (strapUid, strapComp), null); + args.Verbs.Add(verb); } - #endregion - private void OnBuckleInsertIntoEntityStorageAttempt(EntityUid uid, BuckleComponent component, ref InsertIntoEntityStorageAttemptEvent args) { if (component.Buckled) @@ -143,7 +106,10 @@ private void OnBuckleInsertIntoEntityStorageAttempt(EntityUid uid, BuckleCompone private void OnBucklePreventCollide(EntityUid uid, BuckleComponent component, ref PreventCollideEvent args) { - if (args.OtherEntity == component.BuckledTo && component.DontCollide) + if (args.OtherEntity != component.BuckledTo) + return; + + if (component.Buckled || component.DontCollide) args.Cancelled = true; } @@ -167,7 +133,10 @@ private void OnBuckleThrowPushbackAttempt(EntityUid uid, BuckleComponent compone private void OnBuckleUpdateCanMove(EntityUid uid, BuckleComponent component, UpdateCanMoveEvent args) { - if (component.Buckled) + if (component.LifeStage > ComponentLifeStage.Running) + return; + + if (component.Buckled) // buckle shitcode args.Cancel(); } @@ -176,139 +145,162 @@ public bool IsBuckled(EntityUid uid, BuckleComponent? component = null) return Resolve(uid, ref component, false) && component.Buckled; } - protected void SetBuckledTo(Entity buckle, Entity? strap) + /// + /// 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) { - if (TryComp(buckle.Comp.BuckledTo, out StrapComponent? old)) - old.BuckledEntities.Remove(buckle); + Appearance.SetData(uid, StrapVisuals.State, buckleComp.Buckled); + if (buckleComp.BuckledTo != null) + { + if (!Resolve(buckleComp.BuckledTo.Value, ref strapComp)) + return; - if (strap is {} strapEnt && Resolve(strapEnt.Owner, ref strapEnt.Comp)) + var alertType = strapComp.BuckledAlertType; + _alerts.ShowAlert(uid, alertType); + } + else { - strapEnt.Comp.BuckledEntities.Add(buckle); - _alerts.ShowAlert(buckle, strapEnt.Comp.BuckledAlertType); + _alerts.ClearAlertCategory(uid, AlertCategory.Buckled); + } + } + + /// + /// 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) + { + buckleComp.BuckledTo = strapUid; + + if (strapUid == null) + { + buckleComp.Buckled = false; } else { - _alerts.ClearAlertCategory(buckle, BuckledAlertCategory); + buckleComp.LastEntityBuckledTo = strapUid; + buckleComp.DontCollide = true; + buckleComp.Buckled = true; + buckleComp.BuckleTime = _gameTiming.CurTime; } - buckle.Comp.BuckledTo = strap; - buckle.Comp.BuckleTime = _gameTiming.CurTime; - ActionBlocker.UpdateCanMove(buckle); - Appearance.SetData(buckle, StrapVisuals.State, buckle.Comp.Buckled); - Dirty(buckle); + ActionBlocker.UpdateCanMove(buckleUid); + UpdateBuckleStatus(buckleUid, buckleComp, strapComp); + Dirty(buckleComp); } /// /// 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? user, + private bool CanBuckle( + EntityUid buckleUid, + EntityUid userUid, EntityUid strapUid, - bool popup, [NotNullWhen(true)] out StrapComponent? strapComp, - BuckleComponent buckleComp) + BuckleComponent? buckleComp = null) { strapComp = null; - if (!Resolve(strapUid, ref strapComp, false)) + + if (userUid == strapUid || + !Resolve(buckleUid, ref buckleComp, false) || + !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 && user != null) - _popup.PopupEntity(Loc.GetString("buckle-component-cannot-fit-message"), user.Value, user.Value, PopupType.Medium); + if (_netManager.IsServer) + _popup.PopupEntity(Loc.GetString("buckle-component-cannot-fit-message"), userUid, buckleUid, PopupType.Medium); return false; } - if (!_interaction.InRangeUnobstructed(buckleUid, - strapUid, - buckleComp.Range, - predicate: entity => entity == buckleUid || entity == user || entity == strapUid, + // Is it within range + bool Ignored(EntityUid entity) => entity == buckleUid || entity == userUid || entity == strapUid; + + if (!_interaction.InRangeUnobstructed(buckleUid, strapUid, buckleComp.Range, predicate: Ignored, popup: true)) { return false; } - if (!_container.IsInSameOrNoContainer((buckleUid, null, null), (strapUid, null, null))) - 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 (user != null && !HasComp(user)) + if (!HasComp(userUid)) { // PopupPredicted when - if (_netManager.IsServer && popup) - _popup.PopupEntity(Loc.GetString("buckle-component-no-hands-message"), user.Value, user.Value); + if (_netManager.IsServer) + _popup.PopupEntity(Loc.GetString("buckle-component-no-hands-message"), userUid, userUid); return false; } if (buckleComp.Buckled) { - if (_netManager.IsClient || popup || user == null) - return false; - - var message = Loc.GetString(buckleUid == user + var message = Loc.GetString(buckleUid == userUid ? "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 != buckleUid) + if (parent == userUid) { - parent = Transform(parent).ParentUid; - continue; - } + 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); - if (_netManager.IsClient || popup || user == null) return false; + } - 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; + parent = Transform(parent).ParentUid; } if (!StrapHasSpace(strapUid, buckleComp, strapComp)) { - 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); + 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); return false; } - 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) + var attemptEvent = new BuckleAttemptEvent(strapUid, buckleUid, userUid, true); + RaiseLocalEvent(attemptEvent.BuckledEntity, ref attemptEvent); + RaiseLocalEvent(attemptEvent.StrapEntity, ref attemptEvent); + if (attemptEvent.Cancelled) return false; return true; @@ -317,194 +309,217 @@ private bool CanBuckle(EntityUid buckleUid, /// /// 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 buckle, EntityUid? user, EntityUid strap, BuckleComponent? buckleComp = null, bool popup = true) + /// Uid of the owner of strap component + public bool TryBuckle(EntityUid buckleUid, EntityUid userUid, EntityUid strapUid, BuckleComponent? buckleComp = null) { - if (!Resolve(buckle, ref buckleComp, false)) + if (!Resolve(buckleUid, ref buckleComp, false)) return false; - if (!CanBuckle(buckle, user, strap, popup, out var strapComp, buckleComp)) + if (!CanBuckle(buckleUid, userUid, strapUid, out var strapComp, buckleComp)) return false; - Buckle((buckle, buckleComp), (strap, strapComp), user); - return true; - } - - 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)}"); - - _audio.PlayPredicted(strap.Comp.BuckleSound, strap, user); + 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; + } - SetBuckledTo(buckle, strap!); - Appearance.SetData(strap, StrapVisuals.State, true); - Appearance.SetData(buckle, BuckleVisuals.Buckled, true); + if (TryComp(buckleUid, out var appearance)) + Appearance.SetData(buckleUid, BuckleVisuals.Buckled, true, appearance); - _rotationVisuals.SetHorizontalAngle(buckle.Owner, strap.Comp.Rotation); + _rotationVisuals.SetHorizontalAngle(buckleUid, strapComp.Rotation); - var xform = Transform(buckle); - var coords = new EntityCoordinates(strap, strap.Comp.BuckleOffset); - _transform.SetCoordinates(buckle, xform, coords, rotation: Angle.Zero); + 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); - _joints.SetRelay(buckle, strap); + var ev = new BuckleChangeEvent(strapUid, buckleUid, true); + RaiseLocalEvent(ev.BuckledEntity, ref ev); + RaiseLocalEvent(ev.StrapEntity, ref ev); - switch (strap.Comp.Position) + if (TryComp(buckleUid, out var ownerPullable)) { - case StrapPosition.Stand: - _standing.Stand(buckle); - break; - case StrapPosition.Down: - _standing.Down(buckle, false, false); - break; + if (ownerPullable.Puller != null) + { + _pulling.TryStopPull(buckleUid, ownerPullable); + } } - var ev = new StrappedEvent(strap, buckle); - RaiseLocalEvent(strap, ref ev); + if (TryComp(buckleUid, out var physics)) + { + _physics.ResetDynamics(buckleUid, physics); + } - var gotEv = new BuckledEvent(strap, buckle); - RaiseLocalEvent(buckle, ref gotEv); + if (!buckleComp.PullStrap && TryComp(strapUid, out var toPullable)) + { + if (toPullable.Puller == buckleUid) + { + // can't pull it and buckle to it at the same time + _pulling.TryStopPull(strapUid, toPullable); + } + } - if (TryComp(buckle, out var physics)) - _physics.ResetDynamics(buckle, physics); + // 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)}"); - DebugTools.AssertEqual(xform.ParentUid, strap.Owner); + return true; } /// /// Tries to unbuckle the Owner of this component from its current strap. /// /// The entity to unbuckle. - /// The entity doing the unbuckling. + /// 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 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? user, - BuckleComponent? buckleComp = null, - bool popup = true) - { - return TryUnbuckle((buckleUid, buckleComp), user, popup); - } - - public bool TryUnbuckle(Entity buckle, EntityUid? user, bool popup) + public bool TryUnbuckle(EntityUid buckleUid, EntityUid userUid, bool force = false, BuckleComponent? buckleComp = null) { - if (!Resolve(buckle.Owner, ref buckle.Comp)) + if (!Resolve(buckleUid, ref buckleComp, false) || + buckleComp.BuckledTo is not { } strapUid) return false; - if (!CanUnbuckle(buckle, user, popup, out var strap)) - 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; - Unbuckle(buckle!, strap, user); - return true; - } + if (_gameTiming.CurTime < buckleComp.BuckleTime + buckleComp.Delay) + return false; - public void Unbuckle(Entity buckle, EntityUid? user) - { - if (!Resolve(buckle.Owner, ref buckle.Comp, false)) - return; + if (!_interaction.InRangeUnobstructed(userUid, strapUid, buckleComp.Range, popup: true)) + return false; - if (buckle.Comp.BuckledTo is not { } strap) - return; + if (HasComp(buckleUid) && buckleUid == userUid) + 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; + // 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; } - 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}"); + // 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)}"); - _audio.PlayPredicted(strap.Comp.UnbuckleSound, strap, user); + SetBuckledTo(buckleUid, null, null, buckleComp); - SetBuckledTo(buckle, null); + if (!TryComp(strapUid, out var strapComp)) + return false; - var buckleXform = Transform(buckle); - var oldBuckledXform = Transform(strap); + var buckleXform = Transform(buckleUid); + var oldBuckledXform = Transform(strapUid); - if (buckleXform.ParentUid == strap.Owner && !Terminating(buckleXform.ParentUid)) + if (buckleXform.ParentUid == strapUid && !Terminating(buckleXform.ParentUid)) { - _container.AttachParentToContainerOrGrid((buckle, buckleXform)); + _container.AttachParentToContainerOrGrid((buckleUid, buckleXform)); - var oldBuckledToWorldRot = _transform.GetWorldRotation(strap); + var oldBuckledToWorldRot = _transform.GetWorldRotation(strapUid); _transform.SetWorldRotation(buckleXform, oldBuckledToWorldRot); - if (strap.Comp.UnbuckleOffset != Vector2.Zero) - buckleXform.Coordinates = oldBuckledXform.Coordinates.Offset(strap.Comp.UnbuckleOffset); + if (strapComp.UnbuckleOffset != Vector2.Zero) + buckleXform.Coordinates = oldBuckledXform.Coordinates.Offset(strapComp.UnbuckleOffset); } - _rotationVisuals.ResetHorizontalAngle(buckle.Owner); - Appearance.SetData(strap, StrapVisuals.State, strap.Comp.BuckledEntities.Count != 0); - Appearance.SetData(buckle, BuckleVisuals.Buckled, false); + if (TryComp(buckleUid, out AppearanceComponent? appearance)) + Appearance.SetData(buckleUid, BuckleVisuals.Buckled, false, appearance); + _rotationVisuals.ResetHorizontalAngle(buckleUid); - if (HasComp(buckle) || _mobState.IsIncapacitated(buckle)) - _standing.Down(buckle); + if (TryComp(buckleUid, out var mobState) + && _mobState.IsIncapacitated(buckleUid, mobState) + || HasComp(buckleUid)) + { + _standing.Down(buckleUid); + } else - _standing.Stand(buckle); + { + _standing.Stand(buckleUid); + } - _joints.RefreshRelay(buckle); + if (_mobState.IsIncapacitated(buckleUid, mobState)) + { + _standing.Down(buckleUid); + } + if (strapComp.BuckledEntities.Remove(buckleUid)) + { + strapComp.OccupiedSize -= buckleComp.Size; + //Dirty(strapUid); + Dirty(strapComp); + } - var buckleEv = new UnbuckledEvent(strap, buckle); - RaiseLocalEvent(buckle, ref buckleEv); + _joints.RefreshRelay(buckleUid); + Appearance.SetData(strapUid, StrapVisuals.State, strapComp.BuckledEntities.Count != 0); - var strapEv = new UnstrappedEvent(strap, buckle); - RaiseLocalEvent(strap, ref strapEv); - } + // TODO: Buckle listening to moveevents is sussy anyway. + if (!TerminatingOrDeleted(strapUid)) + _audio.PlayPredicted(strapComp.UnbuckleSound, strapUid, userUid); - public bool CanUnbuckle(Entity buckle, EntityUid user, bool popup) - { - return CanUnbuckle(buckle, user, popup, out _); + var ev = new BuckleChangeEvent(strapUid, buckleUid, false); + RaiseLocalEvent(buckleUid, ref ev); + RaiseLocalEvent(strapUid, ref ev); + + return true; } - private bool CanUnbuckle(Entity buckle, EntityUid? user, bool popup, out Entity strap) + /// + /// 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) { - strap = default; - if (!Resolve(buckle.Owner, ref buckle.Comp)) - return false; - - if (buckle.Comp.BuckledTo is not { } strapUid) + if (!Resolve(buckleUid, ref buckle, false)) return false; - if (!TryComp(strapUid, out StrapComponent? strapComp)) + if (!buckle.Buckled) { - Log.Error($"Encountered buckle {ToPrettyString(buckle.Owner)} with invalid strap entity {ToPrettyString(strap)}"); - SetBuckledTo(buckle!, null); - return false; + return TryBuckle(buckleUid, userUid, strapUid, buckle); + } + else + { + return TryUnbuckle(buckleUid, userUid, force, buckle); } - 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 deleted file mode 100644 index 8c2d0b8ee18..00000000000 --- a/Content.Shared/Buckle/SharedBuckleSystem.Interaction.cs +++ /dev/null @@ -1,171 +0,0 @@ -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 f51354435b5..7be54360741 100644 --- a/Content.Shared/Buckle/SharedBuckleSystem.Strap.cs +++ b/Content.Shared/Buckle/SharedBuckleSystem.Strap.cs @@ -2,38 +2,40 @@ 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((_, c, _) => StrapRemoveAll(c)); + SubscribeLocalEvent(OnStrapEntModifiedFromContainer); + SubscribeLocalEvent(OnStrapEntModifiedFromContainer); + SubscribeLocalEvent>(AddStrapVerbs); SubscribeLocalEvent(OnStrapContainerGettingInsertedAttempt); -<<<<<<< HEAD SubscribeLocalEvent(OnStrapInteractHand); SubscribeLocalEvent((_,c,_) => StrapRemoveAll(c)); SubscribeLocalEvent((_, c, _) => StrapRemoveAll(c)); -======= - SubscribeLocalEvent((e, c, _) => StrapRemoveAll(e, c)); - SubscribeLocalEvent((e, c, _) => StrapRemoveAll(e, c)); ->>>>>>> fa3c89a521 (Partial buckling refactor (#29031)) + SubscribeLocalEvent(OnStrapDragDropTarget); + SubscribeLocalEvent(OnCanDropTarget); SubscribeLocalEvent(OnAttemptFold); -<<<<<<< HEAD SubscribeLocalEvent(OnStrapMoveEvent); SubscribeLocalEvent((_, c, _) => StrapRemoveAll(c)); -======= - SubscribeLocalEvent((e, c, _) => StrapRemoveAll(e, c)); ->>>>>>> fa3c89a521 (Partial buckling refactor (#29031)) } private void OnStrapStartup(EntityUid uid, StrapComponent component, ComponentStartup args) @@ -43,7 +45,6 @@ private void OnStrapStartup(EntityUid uid, StrapComponent component, ComponentSt private void OnStrapShutdown(EntityUid uid, StrapComponent component, ComponentShutdown args) { -<<<<<<< HEAD if (LifeStage(uid) > EntityLifeStage.MapInitialized) return; @@ -85,20 +86,15 @@ private void ContainerModifiedReAttach(EntityUid buckleUid, EntityUid strapUid, { ReAttach(buckleUid, strapUid, buckleComp, strapComp); } -======= - if (!TerminatingOrDeleted(uid)) - StrapRemoveAll(uid, component); ->>>>>>> fa3c89a521 (Partial buckling refactor (#29031)) } 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 (args.Container.ID == StorageComponent.ContainerId && component.BuckledEntities.Count != 0) + if (HasComp(args.Container.Owner) && component.BuckledEntities.Count != 0) args.Cancel(); } -<<<<<<< HEAD private void OnStrapInteractHand(EntityUid uid, StrapComponent component, InteractHandEvent args) { if (args.Handled) @@ -188,8 +184,6 @@ private void OnCanDropTarget(EntityUid uid, StrapComponent component, ref CanDro args.Handled = true; } -======= ->>>>>>> fa3c89a521 (Partial buckling refactor (#29031)) private void OnAttemptFold(EntityUid uid, StrapComponent component, ref FoldAttemptEvent args) { if (args.Cancelled) @@ -198,7 +192,6 @@ private void OnAttemptFold(EntityUid uid, StrapComponent component, ref FoldAtte args.Cancelled = component.BuckledEntities.Count != 0; } -<<<<<<< HEAD private void OnStrapDragDropTarget(EntityUid uid, StrapComponent component, ref DragDropTargetEvent args) { if (!StrapCanDragDropOn(uid, args.User, uid, args.Dragged, component)) @@ -262,8 +255,6 @@ private bool StrapCanDragDropOn( return _interaction.InRangeUnobstructed(targetUid, buckleUid, buckleComp.Range, predicate: Ignored); } -======= ->>>>>>> fa3c89a521 (Partial buckling refactor (#29031)) /// /// Remove everything attached to the strap /// @@ -273,13 +264,10 @@ private void StrapRemoveAll(StrapComponent strapComp) { TryUnbuckle(entity, entity, true); } -<<<<<<< HEAD strapComp.BuckledEntities.Clear(); strapComp.OccupiedSize = 0; Dirty(strapComp); -======= ->>>>>>> fa3c89a521 (Partial buckling refactor (#29031)) } private bool StrapHasSpace(EntityUid strapUid, BuckleComponent buckleComp, StrapComponent? strapComp = null) @@ -287,13 +275,30 @@ private bool StrapHasSpace(EntityUid strapUid, BuckleComponent buckleComp, Strap if (!Resolve(strapUid, ref strapComp, false)) return false; - var avail = strapComp.Size; - foreach (var buckle in strapComp.BuckledEntities) - { - avail -= CompOrNull(buckle)?.Size ?? 0; - } + 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; - return avail >= buckleComp.Size; + strapComp.OccupiedSize += buckleComp.Size; + + Appearance.SetData(strapUid, StrapVisuals.State, true); + + Dirty(strapUid, strapComp); + return true; } /// @@ -306,7 +311,6 @@ public void StrapSetEnabled(EntityUid strapUid, bool enabled, StrapComponent? st return; strapComp.Enabled = enabled; - Dirty(strapUid, strapComp); if (!enabled) StrapRemoveAll(strapComp); diff --git a/Content.Shared/Buckle/SharedBuckleSystem.cs b/Content.Shared/Buckle/SharedBuckleSystem.cs index 770fababded..67218657e52 100644 --- a/Content.Shared/Buckle/SharedBuckleSystem.cs +++ b/Content.Shared/Buckle/SharedBuckleSystem.cs @@ -1,17 +1,21 @@ 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.Rotation; +using Content.Shared.Pulling; 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; @@ -32,10 +36,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() @@ -47,6 +51,45 @@ public override void Initialize() InitializeBuckle(); InitializeStrap(); - InitializeInteraction(); + } + + /// + /// 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; + } } } diff --git a/Content.Shared/Climbing/Systems/ClimbSystem.cs b/Content.Shared/Climbing/Systems/ClimbSystem.cs index e40a295042a..c570a821a6f 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(OnBuckled); + SubscribeLocalEvent(OnBuckleChange); SubscribeLocalEvent(OnCanDragDropOn); SubscribeLocalEvent>(AddClimbableVerb); @@ -474,8 +474,15 @@ public void ForciblySetClimbing(EntityUid uid, EntityUid climbable, ClimbingComp Climb(uid, uid, climbable, true, component); } - private void OnBuckled(EntityUid uid, ClimbingComponent component, ref BuckledEvent args) + public void ForciblyStopClimbing(EntityUid uid, ClimbingComponent? climbing = null, FixturesComponent? fixtures = null) { + StopClimb(uid, climbing, fixtures); + } + + private void OnBuckleChange(EntityUid uid, ClimbingComponent component, ref BuckleChangeEvent args) + { + if (!args.Buckling) + return; StopClimb(uid, component); } diff --git a/Content.Shared/Cuffs/SharedCuffableSystem.cs b/Content.Shared/Cuffs/SharedCuffableSystem.cs index fc0c331d59c..9777b239884 100644 --- a/Content.Shared/Cuffs/SharedCuffableSystem.cs +++ b/Content.Shared/Cuffs/SharedCuffableSystem.cs @@ -75,7 +75,6 @@ public override void Initialize() SubscribeLocalEvent(OnUnequipAttempt); SubscribeLocalEvent(OnBeingPulledAttempt); SubscribeLocalEvent(OnBuckleAttemptEvent); - SubscribeLocalEvent(OnUnbuckleAttemptEvent); SubscribeLocalEvent>(AddUncuffVerb); SubscribeLocalEvent(OnCuffableDoAfter); SubscribeLocalEvent(OnPull); @@ -200,33 +199,21 @@ private void OnBeingPulledAttempt(EntityUid uid, CuffableComponent component, Be args.Cancel(); } - private void OnBuckleAttempt(Entity ent, EntityUid? user, ref bool cancelled, bool buckling, bool popup) + private void OnBuckleAttemptEvent(EntityUid uid, CuffableComponent component, ref BuckleAttemptEvent args) { - if (cancelled || user != ent.Owner) + // if someone else is doing it, let it pass. + if (args.UserEntity != uid) return; - if (!TryComp(ent, out var hands) || ent.Comp.CuffedHandCount != hands.Count) + if (!TryComp(uid, out var hands) || component.CuffedHandCount != hands.Count) return; - cancelled = true; - if (!popup) - return; - - var message = buckling + args.Cancelled = true; + var message = args.Buckling ? Loc.GetString("handcuff-component-cuff-interrupt-buckled-message") : Loc.GetString("handcuff-component-cuff-interrupt-unbuckled-message"); - _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); + _popup.PopupClient(message, uid, args.UserEntity); } private void OnPull(EntityUid uid, CuffableComponent component, PullMessage args) diff --git a/Content.Shared/Foldable/FoldableSystem.cs b/Content.Shared/Foldable/FoldableSystem.cs index 2a846f4f234..10baf8165b5 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(OnStrapAttempt); + SubscribeLocalEvent(OnBuckleAttempt); } 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 OnStrapAttempt(EntityUid uid, FoldableComponent comp, ref StrapAttemptEvent args) + public void OnBuckleAttempt(EntityUid uid, FoldableComponent comp, ref BuckleAttemptEvent args) { - if (comp.IsFolded) + if (args.Buckling && comp.IsFolded) args.Cancelled = true; } diff --git a/Content.Shared/Interaction/RotateToFaceSystem.cs b/Content.Shared/Interaction/RotateToFaceSystem.cs index fa213011ef1..ed950240af6 100644 --- a/Content.Shared/Interaction/RotateToFaceSystem.cs +++ b/Content.Shared/Interaction/RotateToFaceSystem.cs @@ -1,6 +1,7 @@ using System.Numerics; using Content.Shared.ActionBlocker; using Content.Shared.Buckle.Components; +using Content.Shared.Mobs.Systems; using Content.Shared.Rotatable; using JetBrains.Annotations; @@ -82,21 +83,24 @@ public bool TryFaceAngle(EntityUid user, Angle diffAngle, TransformComponent? xf if (!_actionBlockerSystem.CanChangeDirection(user)) return false; - if (TryComp(user, out BuckleComponent? buckle) && buckle.BuckledTo is {} strap) + if (EntityManager.TryGetComponent(user, out BuckleComponent? buckle) && buckle.Buckled) { - // 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; + 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; } // 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 f24f6f03993..d9ef671afe2 100644 --- a/Content.Shared/Mobs/Systems/MobStateSystem.Subscribers.cs +++ b/Content.Shared/Mobs/Systems/MobStateSystem.Subscribers.cs @@ -1,5 +1,4 @@ using Content.Shared.Bed.Sleep; -using Content.Shared.Buckle.Components; using Content.Shared.CombatMode.Pacification; using Content.Shared.Damage.ForceSay; using Content.Shared.Emoting; @@ -11,12 +10,15 @@ 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; @@ -44,16 +46,6 @@ 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 c0775b4ce2d..29460e1dfc1 100644 --- a/Content.Shared/Movement/Pulling/Events/PullStartedMessage.cs +++ b/Content.Shared/Movement/Pulling/Events/PullStartedMessage.cs @@ -1,6 +1,9 @@ namespace Content.Shared.Movement.Pulling.Events; -/// -/// 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); +public sealed class PullStartedMessage : PullMessage +{ + public PullStartedMessage(EntityUid pullerUid, EntityUid pullableUid) : + base(pullerUid, pullableUid) + { + } +} diff --git a/Content.Shared/Movement/Pulling/Events/PullStoppedMessage.cs b/Content.Shared/Movement/Pulling/Events/PullStoppedMessage.cs index 6df4d174839..47aa34562fb 100644 --- a/Content.Shared/Movement/Pulling/Events/PullStoppedMessage.cs +++ b/Content.Shared/Movement/Pulling/Events/PullStoppedMessage.cs @@ -1,6 +1,13 @@ -namespace Content.Shared.Movement.Pulling.Events; +using Robust.Shared.Physics.Components; + +namespace Content.Shared.Movement.Pulling.Events; /// -/// Event raised directed BOTH at the puller and pulled entity when a pull starts. +/// Raised directed on both puller and pullable. /// -public sealed class PullStoppedMessage(EntityUid pullerUid, EntityUid pulledUid) : PullMessage(pullerUid, pulledUid); +public sealed class PullStoppedMessage : PullMessage +{ + public PullStoppedMessage(EntityUid pullerUid, EntityUid pulledUid) : base(pullerUid, pulledUid) + { + } +} diff --git a/Content.Shared/Movement/Pulling/Systems/PullingSystem.cs b/Content.Shared/Movement/Pulling/Systems/PullingSystem.cs index 78bbb9e5032..4bf53c8dbdd 100644 --- a/Content.Shared/Movement/Pulling/Systems/PullingSystem.cs +++ b/Content.Shared/Movement/Pulling/Systems/PullingSystem.cs @@ -1,3 +1,4 @@ +using System.Numerics; using Content.Shared.ActionBlocker; using Content.Shared.Administration.Logs; using Content.Shared.Alert; @@ -16,9 +17,12 @@ 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; +using Robust.Shared.Map; +using Robust.Shared.Network; using Robust.Shared.Physics; using Robust.Shared.Physics.Components; using Robust.Shared.Physics.Events; @@ -69,33 +73,92 @@ 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) + public override void Shutdown() { - // Prevent people from pulling the entity they are buckled to - if (ent.Comp.Puller == args.Buckle.Owner && !args.Buckle.Comp.PullStrap) - StopPulling(ent, ent); + base.Shutdown(); + CommandBinds.Unregister(); } - private void OnGotBuckled(Entity ent, ref BuckledEvent args) + public override void Update(float frameTime) { - StopPulling(ent, ent); + if (_net.IsClient) // Client cannot predict this + return; + + var query = EntityQueryEnumerator(); + while (query.MoveNext(out var puller, out var pullerComp, out var pullerPhysics, out var pullerXForm)) + { + // If not pulling, reset the pushing cooldowns and exit + if (pullerComp.Pulling is not { } pulled || !TryComp(pulled, out var pulledComp)) + { + pullerComp.PushingTowards = null; + pullerComp.NextPushTargetChange = TimeSpan.Zero; + continue; + } + + pulledComp.BeingActivelyPushed = false; // Temporarily set to false; if the checks below pass, it will be set to true again + + // If pulling but the pullee is invalid or is on a different map, stop pulling + var pulledXForm = Transform(pulled); + if (!TryComp(pulled, out var pulledPhysics) + || pulledPhysics.BodyType == BodyType.Static + || pulledXForm.MapUid != pullerXForm.MapUid) + { + StopPulling(pulled, pulledComp); + continue; + } + + if (pullerComp.PushingTowards is null) + continue; + + // If pushing but the target position is invalid, or the push action has expired or finished, stop pushing + if (pullerComp.NextPushStop < _timing.CurTime + || !(pullerComp.PushingTowards.Value.ToMap(EntityManager, _xformSys) is var pushCoordinates) + || pushCoordinates.MapId != pulledXForm.MapID) + { + pullerComp.PushingTowards = null; + pullerComp.NextPushTargetChange = TimeSpan.Zero; + continue; + } + + // Actual force calculation. All the Vector2's below are in map coordinates. + var desiredDeltaPos = pushCoordinates.Position - Transform(pulled).Coordinates.ToMapPos(EntityManager, _xformSys); + if (desiredDeltaPos.LengthSquared() < 0.1f) + { + pullerComp.PushingTowards = null; + continue; + } + + var velocityAndDirectionAngle = new Angle(pulledPhysics.LinearVelocity) - new Angle(desiredDeltaPos); + var currentRelativeSpeed = pulledPhysics.LinearVelocity.Length() * (float) Math.Cos(velocityAndDirectionAngle.Theta); + var desiredAcceleration = MathF.Max(0f, pullerComp.MaxPushSpeed - currentRelativeSpeed); + + var desiredImpulse = pulledPhysics.Mass * desiredDeltaPos; + var maxSourceImpulse = MathF.Min(pullerComp.PushAcceleration, desiredAcceleration) * pullerPhysics.Mass; + var actualImpulse = desiredImpulse.LengthSquared() > maxSourceImpulse * maxSourceImpulse ? desiredDeltaPos.Normalized() * maxSourceImpulse : desiredImpulse; + + // Ideally we'd want to apply forces instead of impulses, however... + // We cannot use ApplyForce here because it will be cleared on the next physics substep which will render it ultimately useless + // The alternative is to run this function on every physics substep, but that is way too expensive for such a minor system + _physics.ApplyLinearImpulse(pulled, actualImpulse); + if (_gravity.IsWeightless(puller, pullerPhysics, pullerXForm)) + _physics.ApplyLinearImpulse(puller, -actualImpulse); + + pulledComp.BeingActivelyPushed = true; + } + query.Dispose(); } - private void OnAfterState(Entity ent, ref AfterAutoHandleStateEvent args) + private void OnPullerMoveInput(EntityUid uid, PullerComponent component, ref MoveInputEvent args) { - if (ent.Comp.Pulling == null) - RemComp(ent.Owner); - else - EnsureComp(ent.Owner); + // Stop pushing + component.PushingTowards = null; + component.NextPushStop = TimeSpan.Zero; } private void OnDropHandItems(EntityUid uid, PullerComponent pullerComp, DropHandItemsEvent args) @@ -111,8 +174,7 @@ 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; @@ -245,18 +307,8 @@ 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); @@ -388,6 +440,15 @@ 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); @@ -431,8 +492,11 @@ public bool TryStartPull(EntityUid pullerUid, EntityUid pullableUid, if (!CanPull(pullerUid, pullableUid)) return false; - if (!HasComp(pullerUid) || !TryComp(pullableUid, out PhysicsComponent? pullablePhysics)) + if (!EntityManager.TryGetComponent(pullerUid, out var pullerPhysics) || + !EntityManager.TryGetComponent(pullableUid, out var pullablePhysics)) + { return false; + } // Ensure that the puller is not currently pulling anything. if (TryComp(pullerComp.Pulling, out var oldPullable) @@ -476,7 +540,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(union.Size.X, union.Size.Y) * 0.75f; + var length = Math.Max((float) union.Size.X, (float) union.Size.Y) * 0.75f; var joint = _joints.CreateDistanceJoint(pullableUid, pullerUid, id: pullableComp.PullJointId); joint.CollideConnected = false; @@ -520,6 +584,17 @@ 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 d65d652ff42..14b3270ba88 100644 --- a/Resources/Prototypes/Entities/Structures/Furniture/chairs.yml +++ b/Resources/Prototypes/Entities/Structures/Furniture/chairs.yml @@ -358,8 +358,6 @@ 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 b3cfe6ade3f..161ea25bc43 100644 --- a/Resources/Prototypes/Entities/Structures/Furniture/rollerbeds.yml +++ b/Resources/Prototypes/Entities/Structures/Furniture/rollerbeds.yml @@ -79,8 +79,6 @@ components: - type: Foldable folded: true - - type: Strap - enabled: False - type: entity id: CheapRollerBed @@ -107,8 +105,6 @@ components: - type: Foldable folded: true - - type: Strap - enabled: False - type: entity id: EmergencyRollerBed @@ -135,5 +131,3 @@ components: - type: Foldable folded: true - - type: Strap - enabled: False