using Content.Shared.Buckle.Components;
using Content.Shared.Rotation;
using Robust.Client.GameObjects;
+using Robust.Shared.GameStates;
namespace Content.Client.Buckle;
{
base.Initialize();
- SubscribeLocalEvent<BuckleComponent, AfterAutoHandleStateEvent>(OnBuckleAfterAutoHandleState);
+ SubscribeLocalEvent<BuckleComponent, ComponentHandleState>(OnHandleState);
SubscribeLocalEvent<BuckleComponent, AppearanceChangeEvent>(OnAppearanceChange);
+ SubscribeLocalEvent<StrapComponent, MoveEvent>(OnStrapMoveEvent);
}
- private void OnBuckleAfterAutoHandleState(EntityUid uid, BuckleComponent component, ref AfterAutoHandleStateEvent args)
+ private void OnStrapMoveEvent(EntityUid uid, StrapComponent component, ref MoveEvent args)
{
- ActionBlocker.UpdateCanMove(uid);
+ // I'm moving this to the client-side system, but for the sake of posterity let's keep this comment:
+ // > This is mega cursed. Please somebody save me from Mr Buckle's wild ride
- if (!TryComp<SpriteComponent>(uid, out var ownerSprite))
+ // The nice thing is its still true, this is quite cursed, though maybe not omega cursed anymore.
+ // This code is garbage, it doesn't work with rotated viewports. I need to finally get around to reworking
+ // sprite rendering for entity layers & direction dependent sorting.
+
+ if (args.NewRotation == args.OldRotation)
return;
- // Adjust draw depth when the chair faces north so that the seat back is drawn over the player.
- // Reset the draw depth when rotated in any other direction.
- // TODO when ECSing, make this a visualizer
- // This code was written before rotatable viewports were introduced, so hard-coding Direction.North
- // and comparing it against LocalRotation now breaks this in other rotations. This is a FIXME, but
- // better to get it working for most people before we look at a more permanent solution.
- if (component is { Buckled: true, LastEntityBuckledTo: { } } &&
- Transform(component.LastEntityBuckledTo.Value).LocalRotation.GetCardinalDir() == Direction.North &&
- TryComp<SpriteComponent>(component.LastEntityBuckledTo, out var buckledSprite))
- {
- component.OriginalDrawDepth ??= ownerSprite.DrawDepth;
- ownerSprite.DrawDepth = buckledSprite.DrawDepth - 1;
+ if (!TryComp<SpriteComponent>(uid, out var strapSprite))
return;
- }
- // If here, we're not turning north and should restore the saved draw depth.
- if (component.OriginalDrawDepth.HasValue)
+ var isNorth = Transform(uid).LocalRotation.GetCardinalDir() == Direction.North;
+ foreach (var buckledEntity in component.BuckledEntities)
{
- ownerSprite.DrawDepth = component.OriginalDrawDepth.Value;
- component.OriginalDrawDepth = null;
+ if (!TryComp<BuckleComponent>(buckledEntity, out var buckle))
+ continue;
+
+ if (!TryComp<SpriteComponent>(buckledEntity, out var buckledSprite))
+ continue;
+
+ if (isNorth)
+ {
+ buckle.OriginalDrawDepth ??= buckledSprite.DrawDepth;
+ buckledSprite.DrawDepth = strapSprite.DrawDepth - 1;
+ }
+ else if (buckle.OriginalDrawDepth.HasValue)
+ {
+ buckledSprite.DrawDepth = buckle.OriginalDrawDepth.Value;
+ buckle.OriginalDrawDepth = null;
+ }
}
}
+ private void OnHandleState(Entity<BuckleComponent> 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<BuckleComponent>(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<RotationVisualsComponent>(uid, out var rotVisuals))
--- /dev/null
+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<BuckleComponent>(urist);
+ var strap = Comp<StrapComponent>(Target);
+ var puller = Comp<PullerComponent>(Player);
+ var pullable = Comp<PullableComponent>(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<SharedBuckleSystem>().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);
+ }
+}
{
Assert.That(strap, Is.Not.Null);
Assert.That(strap.BuckledEntities, Is.Empty);
- Assert.That(strap.OccupiedSize, Is.Zero);
});
// Side effects of buckling
// 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.
// Trying to unbuckle too quickly fails
Assert.That(buckleSystem.TryUnbuckle(human, human, buckleComp: buckle), Is.False);
Assert.That(buckle.Buckled);
- Assert.That(buckleSystem.ToggleBuckle(human, human, chair, buckle: buckle), Is.False);
+ Assert.That(buckleSystem.TryUnbuckle(human, human), Is.False);
Assert.That(buckle.Buckled);
#pragma warning restore NUnit2045
});
// Unbuckle, strap
Assert.That(strap.BuckledEntities, Is.Empty);
- Assert.That(strap.OccupiedSize, Is.Zero);
});
#pragma warning disable NUnit2045 // Interdependent asserts.
// On cooldown
Assert.That(buckleSystem.TryUnbuckle(human, human, buckleComp: buckle), Is.False);
Assert.That(buckle.Buckled);
- Assert.That(buckleSystem.ToggleBuckle(human, human, chair, buckle: buckle), Is.False);
+ Assert.That(buckleSystem.TryUnbuckle(human, human), Is.False);
Assert.That(buckle.Buckled);
- Assert.That(buckleSystem.ToggleBuckle(human, human, chair, buckle: buckle), Is.False);
+ Assert.That(buckleSystem.TryUnbuckle(human, human), Is.False);
Assert.That(buckle.Buckled);
#pragma warning restore NUnit2045
});
#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
Assert.That(buckle.Buckled);
Assert.That(buckleSystem.TryUnbuckle(human, human, buckleComp: buckle), Is.False);
Assert.That(buckle.Buckled);
- Assert.That(buckleSystem.ToggleBuckle(human, human, chair, buckle: buckle), Is.False);
- Assert.That(buckle.Buckled);
#pragma warning restore NUnit2045
// Force unbuckle
- Assert.That(buckleSystem.TryUnbuckle(human, human, true, buckleComp: buckle));
+ buckleSystem.Unbuckle(human, human);
Assert.Multiple(() =>
{
Assert.That(buckle.Buckled, Is.False);
// Break our guy's kneecaps
foreach (var leg in legs)
{
- xformSystem.DetachParentToNull(leg.Id, entityManager.GetComponent<TransformComponent>(leg.Id));
+ entityManager.DeleteEntity(leg.Id);
}
});
Assert.That(hand.HeldEntity, Is.Null);
}
- buckleSystem.TryUnbuckle(human, human, true, buckleComp: buckle);
+ buckleSystem.Unbuckle(human, human);
+ Assert.That(buckle.Buckled, Is.False);
});
await pair.CleanReturnAsync();
#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;
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
/// <summary>
/// Cancel crafting a complex recipe.
/// </summary>
await RunTicks(1);
// DoAfter is in progress. Entity not spawned, stacks have been split and someingredients are in a container.
- Assert.Multiple(async () =>
- {
- Assert.That(ActiveDoAfters.Count(), Is.EqualTo(1));
- Assert.That(sys.IsEntityInContainer(shard), Is.True);
- Assert.That(sys.IsEntityInContainer(rods), Is.False);
- Assert.That(sys.IsEntityInContainer(wires), Is.False);
- Assert.That(rodStack, Has.Count.EqualTo(8));
- Assert.That(wireStack, Has.Count.EqualTo(7));
+ Assert.That(ActiveDoAfters.Count(), Is.EqualTo(1));
+ Assert.That(sys.IsEntityInContainer(shard), Is.True);
+ Assert.That(sys.IsEntityInContainer(rods), Is.False);
+ Assert.That(sys.IsEntityInContainer(wires), Is.False);
+ Assert.That(rodStack, Has.Count.EqualTo(8));
+ Assert.That(wireStack, Has.Count.EqualTo(7));
- await FindEntity(Spear, shouldSucceed: false);
- });
+ await FindEntity(Spear, shouldSucceed: false);
// Cancel the DoAfter. Should drop ingredients to the floor.
await CancelDoAfters();
- Assert.Multiple(async () =>
- {
- Assert.That(sys.IsEntityInContainer(rods), Is.False);
- Assert.That(sys.IsEntityInContainer(wires), Is.False);
- Assert.That(sys.IsEntityInContainer(shard), Is.False);
- await FindEntity(Spear, shouldSucceed: false);
- await AssertEntityLookup((Rod, 10), (Cable, 10), (ShardGlass, 1));
- });
+ Assert.That(sys.IsEntityInContainer(rods), Is.False);
+ Assert.That(sys.IsEntityInContainer(wires), Is.False);
+ Assert.That(sys.IsEntityInContainer(shard), Is.False);
+ await FindEntity(Spear, shouldSucceed: false);
+ await AssertEntityLookup((Rod, 10), (Cable, 10), (ShardGlass, 1));
// Re-attempt the do-after
#pragma warning disable CS4014 // Legacy construction code uses DoAfterAwait. See above.
await RunTicks(1);
// DoAfter is in progress. Entity not spawned, ingredients are in a container.
- Assert.Multiple(async () =>
- {
- Assert.That(ActiveDoAfters.Count(), Is.EqualTo(1));
- Assert.That(sys.IsEntityInContainer(shard), Is.True);
- await FindEntity(Spear, shouldSucceed: false);
- });
+ Assert.That(ActiveDoAfters.Count(), Is.EqualTo(1));
+ Assert.That(sys.IsEntityInContainer(shard), Is.True);
+ await FindEntity(Spear, shouldSucceed: false);
// Finish the DoAfter
await AwaitDoAfters();
// Spear has been crafted. Rods and wires are no longer contained. Glass has been consumed.
- Assert.Multiple(async () =>
- {
- await FindEntity(Spear);
- Assert.That(sys.IsEntityInContainer(rods), Is.False);
- Assert.That(sys.IsEntityInContainer(wires), Is.False);
- Assert.That(SEntMan.Deleted(shard));
- });
+ await FindEntity(Spear);
+ Assert.That(sys.IsEntityInContainer(rods), Is.False);
+ Assert.That(sys.IsEntityInContainer(wires), Is.False);
+ Assert.That(SEntMan.Deleted(shard));
}
-#endif
}
/// <summary>
/// Spawn an entity entity and set it as the target.
/// </summary>
- [MemberNotNull(nameof(Target))]
- protected async Task SpawnTarget(string prototype)
+ [MemberNotNull(nameof(Target), nameof(STarget), nameof(CTarget))]
+#pragma warning disable CS8774 // Member must have a non-null value when exiting.
+ protected async Task<NetEntity> SpawnTarget(string prototype)
{
Target = NetEntity.Invalid;
await Server.WaitPost(() =>
await RunTicks(5);
AssertPrototype(prototype);
+ return Target!.Value;
}
+#pragma warning restore CS8774 // Member must have a non-null value when exiting.
/// <summary>
/// Spawn an entity in preparation for deconstruction
#region Inputs
+
+
/// <summary>
/// Make the client press and then release a key. This assumes the key is currently released.
+ /// This will default to using the <see cref="Target"/> entity and <see cref="TargetCoords"/> coordinates.
/// </summary>
protected async Task PressKey(
BoundKeyFunction key,
int ticks = 1,
NetCoordinates? coordinates = null,
- NetEntity cursorEntity = default)
+ NetEntity? cursorEntity = null)
{
await SetKey(key, BoundKeyState.Down, coordinates, cursorEntity);
await RunTicks(ticks);
}
/// <summary>
- /// Make the client press or release a key
+ /// Make the client press or release a key.
+ /// This will default to using the <see cref="Target"/> entity and <see cref="TargetCoords"/> coordinates.
/// </summary>
protected async Task SetKey(
BoundKeyFunction key,
BoundKeyState state,
NetCoordinates? coordinates = null,
- NetEntity cursorEntity = default)
+ NetEntity? cursorEntity = null)
{
var coords = coordinates ?? TargetCoords;
+ var target = cursorEntity ?? Target ?? default;
ScreenCoordinates screen = default;
var funcId = InputManager.NetworkBindMap.KeyFunctionID(key);
State = state,
Coordinates = CEntMan.GetCoordinates(coords),
ScreenCoordinates = screen,
- Uid = CEntMan.GetEntity(cursorEntity),
+ Uid = CEntMan.GetEntity(target),
};
await Client.WaitPost(() => InputSystem.HandleInputCommand(ClientSession, key, message));
protected NetEntity? Target;
protected EntityUid? STarget => ToServer(Target);
+
protected EntityUid? CTarget => ToClient(Target);
/// <summary>
public float TickPeriod => (float) STiming.TickPeriod.TotalSeconds;
-
// Simple mob that has one hand and can perform misc interactions.
[TestPrototypes]
private const string TestPrototypes = @"
- type: ComplexInteraction
- type: MindContainer
- type: Stripping
+ - type: Puller
+ - type: Physics
- type: Tag
tags:
- CanPilot
SEntMan.System<SharedMindSystem>().WipeMind(ServerSession.ContentData()?.Mind);
old = cPlayerMan.LocalEntity;
- Player = SEntMan.GetNetEntity(SEntMan.SpawnEntity(PlayerPrototype, SEntMan.GetCoordinates(PlayerCoords)));
- SPlayer = SEntMan.GetEntity(Player);
+ SPlayer = SEntMan.SpawnEntity(PlayerPrototype, SEntMan.GetCoordinates(PlayerCoords));
+ Player = SEntMan.GetNetEntity(SPlayer);
Server.PlayerMan.SetAttachedEntity(ServerSession, SPlayer);
Hands = SEntMan.GetComponent<HandsComponent>(SPlayer);
DoAfters = SEntMan.GetComponent<DoAfterComponent>(SPlayer);
--- /dev/null
+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<AlertsSystem>();
+ var sAlert = Server.System<AlertsSystem>();
+ var buckle = Comp<BuckleComponent>(Player);
+ var strap = Comp<StrapComponent>(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));
+ }
+}
#nullable enable
using System.Numerics;
+using Content.IntegrationTests.Tests.Interaction;
using Robust.Shared.GameObjects;
-namespace Content.IntegrationTests.Tests.Interaction;
+namespace Content.IntegrationTests.Tests.Movement;
/// <summary>
/// This is a variation of <see cref="InteractionTest"/> that sets up the player with a normal human entity and a simple
--- /dev/null
+#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<AlertsSystem>();
+ var sAlert = Server.System<AlertsSystem>();
+ await SpawnTarget("MobHuman");
+
+ var puller = Comp<PullerComponent>(Player);
+ var pullable = Comp<PullableComponent>(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));
+ }
+}
+
using Robust.Shared.Input;
using Robust.Shared.Maths;
-namespace Content.IntegrationTests.Tests.Slipping;
+namespace Content.IntegrationTests.Tests.Movement;
public sealed class SlippingTest : MovementTest
{
Assert.That(modifier, Is.EqualTo(1), "Player is not moving at full speed.");
// 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, 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<KnockedDownComponent>(false, Player);
// Moving at normal speeds does trigger a slip.
using Content.Shared.Emag.Systems;
using Content.Shared.Mobs.Systems;
using Robust.Shared.Timing;
+using Robust.Shared.Utility;
namespace Content.Server.Bed
{
public override void Initialize()
{
base.Initialize();
- SubscribeLocalEvent<HealOnBuckleComponent, BuckleChangeEvent>(ManageUpdateList);
- SubscribeLocalEvent<StasisBedComponent, BuckleChangeEvent>(OnBuckleChange);
+ SubscribeLocalEvent<HealOnBuckleComponent, StrappedEvent>(OnStrapped);
+ SubscribeLocalEvent<HealOnBuckleComponent, UnstrappedEvent>(OnUnstrapped);
+ SubscribeLocalEvent<StasisBedComponent, StrappedEvent>(OnStasisStrapped);
+ SubscribeLocalEvent<StasisBedComponent, UnstrappedEvent>(OnStasisUnstrapped);
SubscribeLocalEvent<StasisBedComponent, PowerChangedEvent>(OnPowerChanged);
SubscribeLocalEvent<StasisBedComponent, GotEmaggedEvent>(OnEmagged);
}
- private void ManageUpdateList(EntityUid uid, HealOnBuckleComponent component, ref BuckleChangeEvent args)
+ private void OnStrapped(Entity<HealOnBuckleComponent> bed, ref StrappedEvent args)
{
- if (args.Buckling)
- {
- AddComp<HealOnBuckleHealingComponent>(uid);
- component.NextHealTime = _timing.CurTime + TimeSpan.FromSeconds(component.HealTime);
- _actionsSystem.AddAction(args.BuckledEntity, ref component.SleepAction, SleepingSystem.SleepActionId, uid);
- return;
- }
+ EnsureComp<HealOnBuckleHealingComponent>(bed);
+ bed.Comp.NextHealTime = _timing.CurTime + TimeSpan.FromSeconds(bed.Comp.HealTime);
+ _actionsSystem.AddAction(args.Buckle, ref bed.Comp.SleepAction, SleepingSystem.SleepActionId, bed);
- _actionsSystem.RemoveAction(args.BuckledEntity, component.SleepAction);
- _sleepingSystem.TryWaking(args.BuckledEntity);
- RemComp<HealOnBuckleHealingComponent>(uid);
+ // Single action entity, cannot strap multiple entities to the same bed.
+ DebugTools.AssertEqual(args.Strap.Comp.BuckledEntities.Count, 1);
+ }
+
+ private void OnUnstrapped(Entity<HealOnBuckleComponent> bed, ref UnstrappedEvent args)
+ {
+ _actionsSystem.RemoveAction(args.Buckle, bed.Comp.SleepAction);
+ _sleepingSystem.TryWaking(args.Buckle.Owner);
+ RemComp<HealOnBuckleHealingComponent>(bed);
}
public override void Update(float frameTime)
_appearance.SetData(uid, StasisBedVisuals.IsOn, isOn);
}
- private void OnBuckleChange(EntityUid uid, StasisBedComponent component, ref BuckleChangeEvent args)
+ private void OnStasisStrapped(Entity<StasisBedComponent> bed, ref StrappedEvent args)
{
- // In testing this also received an unbuckle event when the bed is destroyed
- // So don't worry about that
- if (!HasComp<BodyComponent>(args.BuckledEntity))
+ if (!HasComp<BodyComponent>(args.Buckle) || !this.IsPowered(bed, EntityManager))
return;
- if (!this.IsPowered(uid, EntityManager))
+ var metabolicEvent = new ApplyMetabolicMultiplierEvent(args.Buckle, bed.Comp.Multiplier, true);
+ RaiseLocalEvent(args.Buckle, ref metabolicEvent);
+ }
+
+ private void OnStasisUnstrapped(Entity<StasisBedComponent> bed, ref UnstrappedEvent args)
+ {
+ if (!HasComp<BodyComponent>(args.Buckle) || !this.IsPowered(bed, EntityManager))
return;
- var metabolicEvent = new ApplyMetabolicMultiplierEvent(args.BuckledEntity, component.Multiplier, args.Buckling);
- RaiseLocalEvent(args.BuckledEntity, ref metabolicEvent);
+ var metabolicEvent = new ApplyMetabolicMultiplierEvent(args.Buckle, bed.Comp.Multiplier, false);
+ RaiseLocalEvent(args.Buckle, ref metabolicEvent);
}
private void OnPowerChanged(EntityUid uid, StasisBedComponent component, ref PowerChangedEvent args)
[RegisterComponent]
public sealed partial class HealOnBuckleComponent : Component
{
- [DataField("damage", required: true)]
- [ViewVariables(VVAccess.ReadWrite)]
+ /// <summary>
+ /// Damage to apply to entities that are strapped to this entity.
+ /// </summary>
+ [DataField(required: true)]
public DamageSpecifier Damage = default!;
- [DataField("healTime", required: false)]
- [ViewVariables(VVAccess.ReadWrite)]
- public float HealTime = 1f; // How often the bed applies the damage
+ /// <summary>
+ /// How frequently the damage should be applied, in seconds.
+ /// </summary>
+ [DataField(required: false)]
+ public float HealTime = 1f;
- [DataField("sleepMultiplier")]
+ /// <summary>
+ /// Damage multiplier that gets applied if the entity is sleeping.
+ /// </summary>
+ [DataField]
public float SleepMultiplier = 3f;
public TimeSpan NextHealTime = TimeSpan.Zero; //Next heal
- [DataField("sleepAction")] public EntityUid? SleepAction;
+ [DataField] public EntityUid? SleepAction;
}
}
namespace Content.Server.Bed.Components
{
+ // TODO rename this component
[RegisterComponent]
public sealed partial class HealOnBuckleHealingComponent : Component
{}
/// <summary>
/// What the metabolic update rate will be multiplied by (higher = slower metabolism)
/// </summary>
- [ViewVariables(VVAccess.ReadWrite)]
+ [ViewVariables(VVAccess.ReadOnly)] // Writing is is not supported. ApplyMetabolicMultiplierEvent needs to be refactored first
+ [DataField]
public float Multiplier = 10f;
}
}
Entity<BloodstreamComponent> 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;
Entity<MetabolizerComponent> 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;
}
}
+ // TODO REFACTOR THIS
+ // This will cause rates to slowly drift over time due to floating point errors.
+ // Instead, the system that raised this should trigger an update and subscribe to get-modifier events.
[ByRefEvent]
public readonly record struct ApplyMetabolicMultiplierEvent(
EntityUid Uid,
Entity<RespiratorComponent> 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;
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")]
{
base.Startup(blackboard);
var owner = blackboard.GetValue<EntityUid>(NPCBlackboard.Owner);
- if (!_entManager.TryGetComponent<BuckleComponent>(owner, out var buckle) || !buckle.Buckled)
- return;
-
- _buckle.TryUnbuckle(owner, owner, true, buckle);
+ _buckle.Unbuckle(owner, null);
}
public override HTNOperatorStatus Update(NPCBlackboard blackboard, float frameTime)
using Content.Shared.Actions;
+using Content.Shared.Buckle.Components;
using Content.Shared.Damage;
using Content.Shared.Damage.ForceSay;
using Content.Shared.Examine;
SubscribeLocalEvent<SleepingComponent, InteractHandEvent>(OnInteractHand);
SubscribeLocalEvent<ForcedSleepingComponent, ComponentInit>(OnInit);
+ SubscribeLocalEvent<SleepingComponent, UnbuckleAttemptEvent>(OnUnbuckleAttempt);
+ }
+
+ private void OnUnbuckleAttempt(Entity<SleepingComponent> ent, ref UnbuckleAttemptEvent args)
+ {
+ // TODO is this necessary?
+ // Shouldn't the interaction have already been blocked by a general interaction check?
+ if (ent.Owner == args.User)
+ args.Cancelled = true;
}
private void OnBedSleepAction(Entity<ActionsContainerComponent> ent, ref SleepActionEvent args)
+using System.Diagnostics.CodeAnalysis;
using Content.Shared.Interaction;
using Robust.Shared.GameStates;
using Robust.Shared.Serialization;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
namespace Content.Shared.Buckle.Components;
-[RegisterComponent, NetworkedComponent, AutoGenerateComponentState(true)]
+/// <summary>
+/// This component allows an entity to be buckled to an entity with a <see cref="StrapComponent"/>.
+/// </summary>
+[RegisterComponent, NetworkedComponent]
[Access(typeof(SharedBuckleSystem))]
public sealed partial class BuckleComponent : Component
{
/// across a table two tiles away" problem.
/// </summary>
[DataField]
- [ViewVariables(VVAccess.ReadWrite)]
public float Range = SharedInteractionSystem.InteractionRange / 1.4f;
/// <summary>
/// True if the entity is buckled, false otherwise.
/// </summary>
- [ViewVariables(VVAccess.ReadWrite)]
- [AutoNetworkedField]
- public bool Buckled;
-
- [ViewVariables]
- [AutoNetworkedField]
- public EntityUid? LastEntityBuckledTo;
+ [MemberNotNullWhen(true, nameof(BuckledTo))]
+ public bool Buckled => BuckledTo != null;
/// <summary>
/// Whether or not collisions should be possible with the entity we are strapped to
/// </summary>
- [ViewVariables(VVAccess.ReadWrite)]
- [DataField, AutoNetworkedField]
+ [DataField]
public bool DontCollide;
/// <summary>
/// Whether or not we should be allowed to pull the entity we are strapped to
/// </summary>
- [ViewVariables(VVAccess.ReadWrite)]
[DataField]
public bool PullStrap;
/// be able to unbuckle after recently buckling.
/// </summary>
[DataField]
- [ViewVariables(VVAccess.ReadWrite)]
public TimeSpan Delay = TimeSpan.FromSeconds(0.25f);
/// <summary>
/// The time that this entity buckled at.
/// </summary>
- [ViewVariables]
- public TimeSpan BuckleTime;
+ [DataField(customTypeSerializer: typeof(TimeOffsetSerializer))]
+ public TimeSpan? BuckleTime;
/// <summary>
/// The strap that this component is buckled to.
/// </summary>
- [ViewVariables]
- [AutoNetworkedField]
+ [DataField]
public EntityUid? BuckledTo;
/// <summary>
/// <see cref="StrapComponent"/>.
/// </summary>
[DataField]
- [ViewVariables(VVAccess.ReadWrite)]
public int Size = 100;
/// <summary>
[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;
+}
+
+
+/// <summary>
+/// Event raised directed at a strap entity before some entity gets buckled to it.
+/// </summary>
+[ByRefEvent]
+public record struct StrapAttemptEvent(
+ Entity<StrapComponent> Strap,
+ Entity<BuckleComponent> Buckle,
+ EntityUid? User,
+ bool Popup)
+{
+ public bool Cancelled;
+}
+
+/// <summary>
+/// Event raised directed at a buckle entity before it gets buckled to some strap entity.
+/// </summary>
+[ByRefEvent]
+public record struct BuckleAttemptEvent(
+ Entity<StrapComponent> Strap,
+ Entity<BuckleComponent> Buckle,
+ EntityUid? User,
+ bool Popup)
+{
+ public bool Cancelled;
+}
+
+/// <summary>
+/// Event raised directed at a strap entity before some entity gets unbuckled from it.
+/// </summary>
+[ByRefEvent]
+public record struct UnstrapAttemptEvent(
+ Entity<StrapComponent> Strap,
+ Entity<BuckleComponent> Buckle,
+ EntityUid? User,
+ bool Popup)
+{
+ public bool Cancelled;
+}
+
+/// <summary>
+/// Event raised directed at a buckle entity before it gets unbuckled.
+/// </summary>
+[ByRefEvent]
+public record struct UnbuckleAttemptEvent(
+ Entity<StrapComponent> Strap,
+ Entity<BuckleComponent> Buckle,
+ EntityUid? User,
+ bool Popup)
+{
+ public bool Cancelled;
+}
+
+/// <summary>
+/// Event raised directed at a strap entity after something has been buckled to it.
+/// </summary>
+[ByRefEvent]
+public readonly record struct StrappedEvent(Entity<StrapComponent> Strap, Entity<BuckleComponent> Buckle);
+
+/// <summary>
+/// Event raised directed at a buckle entity after it has been buckled.
+/// </summary>
+[ByRefEvent]
+public readonly record struct BuckledEvent(Entity<StrapComponent> Strap, Entity<BuckleComponent> Buckle);
+
+/// <summary>
+/// Event raised directed at a strap entity after something has been unbuckled from it.
+/// </summary>
[ByRefEvent]
-public record struct BuckleAttemptEvent(EntityUid StrapEntity, EntityUid BuckledEntity, EntityUid UserEntity, bool Buckling, bool Cancelled = false);
+public readonly record struct UnstrappedEvent(Entity<StrapComponent> Strap, Entity<BuckleComponent> Buckle);
+/// <summary>
+/// Event raised directed at a buckle entity after it has been unbuckled from some strap entity.
+/// </summary>
[ByRefEvent]
-public readonly record struct BuckleChangeEvent(EntityUid StrapEntity, EntityUid BuckledEntity, bool Buckling);
+public readonly record struct UnbuckledEvent(Entity<StrapComponent> Strap, Entity<BuckleComponent> Buckle);
[Serializable, NetSerializable]
public enum BuckleVisuals
public sealed partial class StrapComponent : Component
{
/// <summary>
- /// The entities that are currently buckled
+ /// The entities that are currently buckled to this strap.
/// </summary>
- [AutoNetworkedField]
- [ViewVariables] // TODO serialization
+ [ViewVariables]
public HashSet<EntityUid> BuckledEntities = new();
/// <summary>
/// Entities that this strap accepts and can buckle
/// If null it accepts any entity
/// </summary>
- [DataField, ViewVariables(VVAccess.ReadWrite)]
+ [DataField]
public EntityWhitelist? Whitelist;
/// <summary>
/// Entities that this strap does not accept and cannot buckle.
/// </summary>
- [DataField, ViewVariables(VVAccess.ReadWrite)]
+ [DataField]
public EntityWhitelist? Blacklist;
/// <summary>
/// The change in position to the strapped mob
/// </summary>
[DataField, AutoNetworkedField]
- [ViewVariables(VVAccess.ReadWrite)]
public StrapPosition Position = StrapPosition.None;
- /// <summary>
- /// The distance above which a buckled entity will be automatically unbuckled.
- /// Don't change it unless you really have to
- /// </summary>
- /// <remarks>
- /// Dont set this below 0.2 because that causes audio issues with <see cref="SharedBuckleSystem.OnBuckleMove"/>
- /// 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.
- /// </remarks>
- [DataField, AutoNetworkedField]
- [ViewVariables(VVAccess.ReadWrite)]
- public float MaxBuckleDistance = 0.2f;
-
- /// <summary>
- /// Gets and clamps the buckle offset to MaxBuckleDistance
- /// </summary>
- [ViewVariables]
- public Vector2 BuckleOffsetClamped => Vector2.Clamp(
- BuckleOffset,
- Vector2.One * -MaxBuckleDistance,
- Vector2.One * MaxBuckleDistance);
-
/// <summary>
/// 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 <see cref="MaxBuckleDistance"/>
/// </summary>
[DataField, AutoNetworkedField]
- [ViewVariables(VVAccess.ReadWrite)]
public Vector2 BuckleOffset = Vector2.Zero;
/// <summary>
/// The angle to rotate the player by when they get strapped
/// </summary>
[DataField]
- [ViewVariables(VVAccess.ReadWrite)]
public Angle Rotation;
/// <summary>
/// The size of the strap which is compared against when buckling entities
/// </summary>
[DataField]
- [ViewVariables(VVAccess.ReadWrite)]
public int Size = 100;
/// <summary>
/// If disabled, nothing can be buckled on this object, and it will unbuckle anything that's already buckled
/// </summary>
- [ViewVariables]
+ [DataField, AutoNetworkedField]
public bool Enabled = true;
/// <summary>
/// You can specify the offset the entity will have after unbuckling.
/// </summary>
[DataField]
- [ViewVariables(VVAccess.ReadWrite)]
public Vector2 UnbuckleOffset = Vector2.Zero;
/// <summary>
/// The sound to be played when a mob is buckled
/// </summary>
[DataField]
- [ViewVariables(VVAccess.ReadWrite)]
public SoundSpecifier BuckleSound = new SoundPathSpecifier("/Audio/Effects/buckle.ogg");
/// <summary>
/// The sound to be played when a mob is unbuckled
/// </summary>
[DataField]
- [ViewVariables(VVAccess.ReadWrite)]
public SoundSpecifier UnbuckleSound = new SoundPathSpecifier("/Audio/Effects/unbuckle.ogg");
/// <summary>
/// ID of the alert to show when buckled
/// </summary>
[DataField]
- [ViewVariables(VVAccess.ReadWrite)]
public ProtoId<AlertPrototype> BuckledAlertType = "Buckled";
-
- /// <summary>
- /// The sum of the sizes of all the buckled entities in this strap
- /// </summary>
- [AutoNetworkedField]
- [ViewVariables]
- public int OccupiedSize;
}
public enum StrapPosition
using System.Diagnostics.CodeAnalysis;
using System.Numerics;
using Content.Shared.Alert;
-using Content.Shared.Bed.Sleep;
using Content.Shared.Buckle.Components;
using Content.Shared.Database;
using Content.Shared.Hands.Components;
using Content.Shared.IdentityManagement;
-using Content.Shared.Interaction;
-using Content.Shared.Mobs.Components;
using Content.Shared.Movement.Events;
+using Content.Shared.Movement.Pulling.Events;
using Content.Shared.Popups;
+using Content.Shared.Pulling.Events;
using Content.Shared.Standing;
using Content.Shared.Storage.Components;
using Content.Shared.Stunnable;
using Content.Shared.Throwing;
-using Content.Shared.Verbs;
using Content.Shared.Whitelist;
+using Robust.Shared.Containers;
+using Robust.Shared.GameStates;
+using Robust.Shared.Map;
using Robust.Shared.Physics.Components;
using Robust.Shared.Physics.Events;
+using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
-using PullableComponent = Content.Shared.Movement.Pulling.Components.PullableComponent;
namespace Content.Shared.Buckle;
public abstract partial class SharedBuckleSystem
{
+ public static ProtoId<AlertCategoryPrototype> BuckledAlertCategory = "Buckled";
+
[Dependency] private readonly EntityWhitelistSystem _whitelistSystem = default!;
private void InitializeBuckle()
{
- SubscribeLocalEvent<BuckleComponent, ComponentStartup>(OnBuckleComponentStartup);
SubscribeLocalEvent<BuckleComponent, ComponentShutdown>(OnBuckleComponentShutdown);
SubscribeLocalEvent<BuckleComponent, MoveEvent>(OnBuckleMove);
- SubscribeLocalEvent<BuckleComponent, InteractHandEvent>(OnBuckleInteractHand);
- SubscribeLocalEvent<BuckleComponent, GetVerbsEvent<InteractionVerb>>(AddUnbuckleVerb);
+ SubscribeLocalEvent<BuckleComponent, EntParentChangedMessage>(OnParentChanged);
+ SubscribeLocalEvent<BuckleComponent, EntGotInsertedIntoContainerMessage>(OnInserted);
+
+ SubscribeLocalEvent<BuckleComponent, StartPullAttemptEvent>(OnPullAttempt);
+ SubscribeLocalEvent<BuckleComponent, BeingPulledAttemptEvent>(OnBeingPulledAttempt);
+ SubscribeLocalEvent<BuckleComponent, PullStartedMessage>(OnPullStarted);
+
SubscribeLocalEvent<BuckleComponent, InsertIntoEntityStorageAttemptEvent>(OnBuckleInsertIntoEntityStorageAttempt);
SubscribeLocalEvent<BuckleComponent, PreventCollideEvent>(OnBucklePreventCollide);
SubscribeLocalEvent<BuckleComponent, StandAttemptEvent>(OnBuckleStandAttempt);
SubscribeLocalEvent<BuckleComponent, ThrowPushbackAttemptEvent>(OnBuckleThrowPushbackAttempt);
SubscribeLocalEvent<BuckleComponent, UpdateCanMoveEvent>(OnBuckleUpdateCanMove);
- }
- [ValidatePrototypeId<AlertCategoryPrototype>]
- public const string BuckledAlertCategory = "Buckled";
+ SubscribeLocalEvent<BuckleComponent, ComponentGetState>(OnGetState);
+ }
- private void OnBuckleComponentStartup(EntityUid uid, BuckleComponent component, ComponentStartup args)
+ private void OnGetState(Entity<BuckleComponent> ent, ref ComponentGetState args)
{
- UpdateBuckleStatus(uid, component);
+ args.State = new BuckleState(GetNetEntity(ent.Comp.BuckledTo), ent.Comp.DontCollide, ent.Comp.BuckleTime);
}
- private void OnBuckleComponentShutdown(EntityUid uid, BuckleComponent component, ComponentShutdown args)
+ private void OnBuckleComponentShutdown(Entity<BuckleComponent> ent, ref ComponentShutdown args)
{
- TryUnbuckle(uid, uid, true, component);
+ Unbuckle(ent!, null);
+ }
+
+ #region Pulling
- component.BuckleTime = default;
+ private void OnPullAttempt(Entity<BuckleComponent> ent, ref StartPullAttemptEvent args)
+ {
+ // Prevent people pulling the chair they're on, etc.
+ if (ent.Comp.BuckledTo == args.Pulled && !ent.Comp.PullStrap)
+ args.Cancel();
}
- private void OnBuckleMove(EntityUid uid, BuckleComponent component, ref MoveEvent ev)
+ private void OnBeingPulledAttempt(Entity<BuckleComponent> ent, ref BeingPulledAttemptEvent args)
{
- if (component.BuckledTo is not { } strapUid)
+ if (args.Cancelled || !ent.Comp.Buckled)
return;
- if (!TryComp<StrapComponent>(strapUid, out var strapComp))
- return;
+ if (!CanUnbuckle(ent!, args.Puller, false))
+ args.Cancel();
+ }
- var strapPosition = Transform(strapUid).Coordinates;
- if (ev.NewPosition.EntityId.IsValid() && ev.NewPosition.InRange(EntityManager, _transform, strapPosition, strapComp.MaxBuckleDistance))
- return;
+ private void OnPullStarted(Entity<BuckleComponent> ent, ref PullStartedMessage args)
+ {
+ Unbuckle(ent!, args.PullerUid);
+ }
+
+ #endregion
- TryUnbuckle(uid, uid, true, component);
+ #region Transform
+
+ private void OnParentChanged(Entity<BuckleComponent> ent, ref EntParentChangedMessage args)
+ {
+ BuckleTransformCheck(ent, args.Transform);
}
- private void OnBuckleInteractHand(EntityUid uid, BuckleComponent component, InteractHandEvent args)
+ private void OnInserted(Entity<BuckleComponent> ent, ref EntGotInsertedIntoContainerMessage args)
{
- if (!component.Buckled)
- return;
+ BuckleTransformCheck(ent, Transform(ent));
+ }
- if (TryUnbuckle(uid, args.User, buckleComp: component))
- args.Handled = true;
+ private void OnBuckleMove(Entity<BuckleComponent> ent, ref MoveEvent ev)
+ {
+ BuckleTransformCheck(ent, ev.Component);
}
- private void AddUnbuckleVerb(EntityUid uid, BuckleComponent component, GetVerbsEvent<InteractionVerb> args)
+ /// <summary>
+ /// Check if the entity should get unbuckled as a result of transform or container changes.
+ /// </summary>
+ private void BuckleTransformCheck(Entity<BuckleComponent> buckle, TransformComponent xform)
{
- if (!args.CanAccess || !args.CanInteract || !component.Buckled)
+ if (_gameTiming.ApplyingState)
return;
- InteractionVerb verb = new()
+ if (buckle.Comp.BuckledTo is not { } strapUid)
+ return;
+
+ if (!TryComp<StrapComponent>(strapUid, out var strapComp))
{
- Act = () => TryUnbuckle(uid, args.User, buckleComp: component),
- Text = Loc.GetString("verb-categories-unbuckle"),
- Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/VerbIcons/unbuckle.svg.192dpi.png"))
- };
+ Log.Error($"Encountered buckle entity {ToPrettyString(buckle)} without a valid strap entity {ToPrettyString(strapUid)}");
+ SetBuckledTo(buckle, null);
+ return;
+ }
- if (args.Target == args.User && args.Using == null)
+ if (xform.ParentUid != strapUid || _container.IsEntityInContainer(buckle))
{
- // A user is left clicking themselves with an empty hand, while buckled.
- // It is very likely they are trying to unbuckle themselves.
- verb.Priority = 1;
+ Unbuckle(buckle, (strapUid, strapComp), null);
+ return;
}
- args.Verbs.Add(verb);
+ var delta = (xform.LocalPosition - strapComp.BuckleOffset).LengthSquared();
+ if (delta > 1e-5)
+ Unbuckle(buckle, (strapUid, strapComp), null);
}
+ #endregion
+
private void OnBuckleInsertIntoEntityStorageAttempt(EntityUid uid, BuckleComponent component, ref InsertIntoEntityStorageAttemptEvent args)
{
if (component.Buckled)
private void OnBucklePreventCollide(EntityUid uid, BuckleComponent component, ref PreventCollideEvent args)
{
- if (args.OtherEntity != component.BuckledTo)
- return;
-
- if (component.Buckled || component.DontCollide)
+ if (args.OtherEntity == component.BuckledTo && component.DontCollide)
args.Cancelled = true;
}
private void OnBuckleUpdateCanMove(EntityUid uid, BuckleComponent component, UpdateCanMoveEvent args)
{
- if (component.LifeStage > ComponentLifeStage.Running)
- return;
-
- if (component.Buckled) // buckle shitcode
+ if (component.Buckled)
args.Cancel();
}
return Resolve(uid, ref component, false) && component.Buckled;
}
- /// <summary>
- /// Shows or hides the buckled status effect depending on if the
- /// entity is buckled or not.
- /// </summary>
- /// <param name="uid"> Entity that we want to show the alert </param>
- /// <param name="buckleComp"> buckle component of the entity </param>
- /// <param name="strapComp"> strap component of the thing we are strapping to </param>
- private void UpdateBuckleStatus(EntityUid uid, BuckleComponent buckleComp, StrapComponent? strapComp = null)
+ protected void SetBuckledTo(Entity<BuckleComponent> buckle, Entity<StrapComponent?>? strap)
{
- Appearance.SetData(uid, StrapVisuals.State, buckleComp.Buckled);
- if (buckleComp.BuckledTo != null)
- {
- if (!Resolve(buckleComp.BuckledTo.Value, ref strapComp))
- return;
+ if (TryComp(buckle.Comp.BuckledTo, out StrapComponent? old))
+ old.BuckledEntities.Remove(buckle);
- var alertType = strapComp.BuckledAlertType;
- _alerts.ShowAlert(uid, alertType);
- }
- else
+ if (strap is {} strapEnt && Resolve(strapEnt.Owner, ref strapEnt.Comp))
{
- _alerts.ClearAlertCategory(uid, BuckledAlertCategory);
- }
- }
-
- /// <summary>
- /// Sets the <see cref="BuckleComponent.BuckledTo"/> field in the component to a value
- /// </summary>
- /// <param name="strapUid"> Value tat with be assigned to the field </param>
- private void SetBuckledTo(EntityUid buckleUid, EntityUid? strapUid, StrapComponent? strapComp, BuckleComponent buckleComp)
- {
- buckleComp.BuckledTo = strapUid;
-
- if (strapUid == null)
- {
- buckleComp.Buckled = false;
+ strapEnt.Comp.BuckledEntities.Add(buckle);
+ _alerts.ShowAlert(buckle, strapEnt.Comp.BuckledAlertType);
}
else
{
- buckleComp.LastEntityBuckledTo = strapUid;
- buckleComp.DontCollide = true;
- buckleComp.Buckled = true;
- buckleComp.BuckleTime = _gameTiming.CurTime;
+ _alerts.ClearAlertCategory(buckle, BuckledAlertCategory);
}
- ActionBlocker.UpdateCanMove(buckleUid);
- UpdateBuckleStatus(buckleUid, buckleComp, strapComp);
- Dirty(buckleUid, buckleComp);
+ buckle.Comp.BuckledTo = strap;
+ buckle.Comp.BuckleTime = _gameTiming.CurTime;
+ ActionBlocker.UpdateCanMove(buckle);
+ Appearance.SetData(buckle, StrapVisuals.State, buckle.Comp.Buckled);
+ Dirty(buckle);
}
/// <summary>
/// Checks whether or not buckling is possible
/// </summary>
/// <param name="buckleUid"> Uid of the owner of BuckleComponent </param>
- /// <param name="userUid">
- /// Uid of a third party entity,
- /// i.e, the uid of someone else you are dragging to a chair.
- /// Can equal buckleUid sometimes
+ /// <param name="user">
+ /// Uid of a third party entity,
+ /// i.e, the uid of someone else you are dragging to a chair.
+ /// Can equal buckleUid sometimes
/// </param>
/// <param name="strapUid"> Uid of the owner of strap component </param>
- private bool CanBuckle(
- EntityUid buckleUid,
- EntityUid userUid,
+ /// <param name="strapComp"></param>
+ /// <param name="buckleComp"></param>
+ private bool CanBuckle(EntityUid buckleUid,
+ EntityUid? user,
EntityUid strapUid,
+ bool popup,
[NotNullWhen(true)] out StrapComponent? strapComp,
- BuckleComponent? buckleComp = null)
+ BuckleComponent buckleComp)
{
strapComp = null;
-
- if (userUid == strapUid ||
- !Resolve(buckleUid, ref buckleComp, false) ||
- !Resolve(strapUid, ref strapComp, false))
- {
+ if (!Resolve(strapUid, ref strapComp, false))
return false;
- }
// Does it pass the Whitelist
if (_whitelistSystem.IsWhitelistFail(strapComp.Whitelist, buckleUid) ||
_whitelistSystem.IsBlacklistPass(strapComp.Blacklist, buckleUid))
{
- if (_netManager.IsServer)
- _popup.PopupEntity(Loc.GetString("buckle-component-cannot-fit-message"), userUid, buckleUid, PopupType.Medium);
+ if (_netManager.IsServer && popup && user != null)
+ _popup.PopupEntity(Loc.GetString("buckle-component-cannot-fit-message"), user.Value, user.Value, PopupType.Medium);
return false;
}
- // Is it within range
- bool Ignored(EntityUid entity) => entity == buckleUid || entity == userUid || entity == strapUid;
-
- if (!_interaction.InRangeUnobstructed(buckleUid, strapUid, buckleComp.Range, predicate: Ignored,
+ if (!_interaction.InRangeUnobstructed(buckleUid,
+ strapUid,
+ buckleComp.Range,
+ predicate: entity => entity == buckleUid || entity == user || entity == strapUid,
popup: true))
{
return false;
}
- // If in a container
- if (_container.TryGetContainingContainer(buckleUid, out var ownerContainer))
- {
- // And not in the same container as the strap
- if (!_container.TryGetContainingContainer(strapUid, out var strapContainer) ||
- ownerContainer != strapContainer)
- {
- return false;
- }
- }
+ if (!_container.IsInSameOrNoContainer((buckleUid, null, null), (strapUid, null, null)))
+ return false;
- if (!HasComp<HandsComponent>(userUid))
+ if (user != null && !HasComp<HandsComponent>(user))
{
// PopupPredicted when
- if (_netManager.IsServer)
- _popup.PopupEntity(Loc.GetString("buckle-component-no-hands-message"), userUid, userUid);
+ if (_netManager.IsServer && popup)
+ _popup.PopupEntity(Loc.GetString("buckle-component-no-hands-message"), user.Value, user.Value);
return false;
}
if (buckleComp.Buckled)
{
- var message = Loc.GetString(buckleUid == userUid
+ if (_netManager.IsClient || popup || user == null)
+ return false;
+
+ var message = Loc.GetString(buckleUid == user
? "buckle-component-already-buckled-message"
: "buckle-component-other-already-buckled-message",
("owner", Identity.Entity(buckleUid, EntityManager)));
- if (_netManager.IsServer)
- _popup.PopupEntity(message, userUid, userUid);
+ _popup.PopupEntity(message, user.Value, user.Value);
return false;
}
+ // Check whether someone is attempting to buckle something to their own child
var parent = Transform(strapUid).ParentUid;
while (parent.IsValid())
{
- if (parent == userUid)
+ if (parent != buckleUid)
{
- var message = Loc.GetString(buckleUid == userUid
- ? "buckle-component-cannot-buckle-message"
- : "buckle-component-other-cannot-buckle-message", ("owner", Identity.Entity(buckleUid, EntityManager)));
- if (_netManager.IsServer)
- _popup.PopupEntity(message, userUid, userUid);
+ parent = Transform(parent).ParentUid;
+ continue;
+ }
+ if (_netManager.IsClient || popup || user == null)
return false;
- }
- parent = Transform(parent).ParentUid;
+ var message = Loc.GetString(buckleUid == user
+ ? "buckle-component-cannot-buckle-message"
+ : "buckle-component-other-cannot-buckle-message",
+ ("owner", Identity.Entity(buckleUid, EntityManager)));
+
+ _popup.PopupEntity(message, user.Value, user.Value);
+ return false;
}
if (!StrapHasSpace(strapUid, buckleComp, strapComp))
{
- var message = Loc.GetString(buckleUid == userUid
- ? "buckle-component-cannot-fit-message"
- : "buckle-component-other-cannot-fit-message", ("owner", Identity.Entity(buckleUid, EntityManager)));
- if (_netManager.IsServer)
- _popup.PopupEntity(message, userUid, userUid);
+ if (_netManager.IsClient || popup || user == null)
+ return false;
+
+ var message = Loc.GetString(buckleUid == user
+ ? "buckle-component-cannot-fit-message"
+ : "buckle-component-other-cannot-fit-message",
+ ("owner", Identity.Entity(buckleUid, EntityManager)));
+
+ _popup.PopupEntity(message, user.Value, user.Value);
return false;
}
- var attemptEvent = new BuckleAttemptEvent(strapUid, buckleUid, userUid, true);
- RaiseLocalEvent(attemptEvent.BuckledEntity, ref attemptEvent);
- RaiseLocalEvent(attemptEvent.StrapEntity, ref attemptEvent);
- if (attemptEvent.Cancelled)
+ var buckleAttempt = new BuckleAttemptEvent((strapUid, strapComp), (buckleUid, buckleComp), user, popup);
+ RaiseLocalEvent(buckleUid, ref buckleAttempt);
+ if (buckleAttempt.Cancelled)
+ return false;
+
+ var strapAttempt = new StrapAttemptEvent((strapUid, strapComp), (buckleUid, buckleComp), user, popup);
+ RaiseLocalEvent(strapUid, ref strapAttempt);
+ if (strapAttempt.Cancelled)
return false;
return true;
/// <summary>
/// Attempts to buckle an entity to a strap
/// </summary>
- /// <param name="buckleUid"> Uid of the owner of BuckleComponent </param>
- /// <param name="userUid">
+ /// <param name="buckle"> Uid of the owner of BuckleComponent </param>
+ /// <param name="user">
/// Uid of a third party entity,
/// i.e, the uid of someone else you are dragging to a chair.
/// Can equal buckleUid sometimes
/// </param>
- /// <param name="strapUid"> Uid of the owner of strap component </param>
- public bool TryBuckle(EntityUid buckleUid, EntityUid userUid, EntityUid strapUid, BuckleComponent? buckleComp = null)
+ /// <param name="strap"> Uid of the owner of strap component </param>
+ public bool TryBuckle(EntityUid buckle, EntityUid? user, EntityUid strap, BuckleComponent? buckleComp = null, bool popup = true)
{
- if (!Resolve(buckleUid, ref buckleComp, false))
+ if (!Resolve(buckle, ref buckleComp, false))
return false;
- if (!CanBuckle(buckleUid, userUid, strapUid, out var strapComp, buckleComp))
+ if (!CanBuckle(buckle, user, strap, popup, out var strapComp, buckleComp))
return false;
- if (!StrapTryAdd(strapUid, buckleUid, buckleComp, false, strapComp))
- {
- var message = Loc.GetString(buckleUid == userUid
- ? "buckle-component-cannot-buckle-message"
- : "buckle-component-other-cannot-buckle-message", ("owner", Identity.Entity(buckleUid, EntityManager)));
- if (_netManager.IsServer)
- _popup.PopupEntity(message, userUid, userUid);
- return false;
- }
+ Buckle((buckle, buckleComp), (strap, strapComp), user);
+ return true;
+ }
- if (TryComp<AppearanceComponent>(buckleUid, out var appearance))
- Appearance.SetData(buckleUid, BuckleVisuals.Buckled, true, appearance);
+ private void Buckle(Entity<BuckleComponent> buckle, Entity<StrapComponent> strap, EntityUid? user)
+ {
+ if (user == buckle.Owner)
+ _adminLogger.Add(LogType.Action, LogImpact.Low, $"{ToPrettyString(user):player} buckled themselves to {ToPrettyString(strap)}");
+ else if (user != null)
+ _adminLogger.Add(LogType.Action, LogImpact.Low, $"{ToPrettyString(user):player} buckled {ToPrettyString(buckle)} to {ToPrettyString(strap)}");
- _rotationVisuals.SetHorizontalAngle(buckleUid, strapComp.Rotation);
+ _audio.PlayPredicted(strap.Comp.BuckleSound, strap, user);
- ReAttach(buckleUid, strapUid, buckleComp, strapComp);
- SetBuckledTo(buckleUid, strapUid, strapComp, buckleComp);
- // TODO user is currently set to null because if it isn't the sound fails to play in some situations, fix that
- _audio.PlayPredicted(strapComp.BuckleSound, strapUid, userUid);
+ SetBuckledTo(buckle, strap!);
+ Appearance.SetData(strap, StrapVisuals.State, true);
+ Appearance.SetData(buckle, BuckleVisuals.Buckled, true);
- var ev = new BuckleChangeEvent(strapUid, buckleUid, true);
- RaiseLocalEvent(ev.BuckledEntity, ref ev);
- RaiseLocalEvent(ev.StrapEntity, ref ev);
+ _rotationVisuals.SetHorizontalAngle(buckle.Owner, strap.Comp.Rotation);
- if (TryComp<PullableComponent>(buckleUid, out var ownerPullable))
- {
- if (ownerPullable.Puller != null)
- {
- _pulling.TryStopPull(buckleUid, ownerPullable);
- }
- }
+ var xform = Transform(buckle);
+ var coords = new EntityCoordinates(strap, strap.Comp.BuckleOffset);
+ _transform.SetCoordinates(buckle, xform, coords, rotation: Angle.Zero);
- if (TryComp<PhysicsComponent>(buckleUid, out var physics))
- {
- _physics.ResetDynamics(buckleUid, physics);
- }
+ _joints.SetRelay(buckle, strap);
- if (!buckleComp.PullStrap && TryComp<PullableComponent>(strapUid, out var toPullable))
+ switch (strap.Comp.Position)
{
- if (toPullable.Puller == buckleUid)
- {
- // can't pull it and buckle to it at the same time
- _pulling.TryStopPull(strapUid, toPullable);
- }
+ case StrapPosition.Stand:
+ _standing.Stand(buckle);
+ break;
+ case StrapPosition.Down:
+ _standing.Down(buckle, false, false);
+ break;
}
- // Logging
- if (userUid != buckleUid)
- _adminLogger.Add(LogType.Action, LogImpact.Low, $"{ToPrettyString(userUid):player} buckled {ToPrettyString(buckleUid)} to {ToPrettyString(strapUid)}");
- else
- _adminLogger.Add(LogType.Action, LogImpact.Low, $"{ToPrettyString(userUid):player} buckled themselves to {ToPrettyString(strapUid)}");
+ var ev = new StrappedEvent(strap, buckle);
+ RaiseLocalEvent(strap, ref ev);
- return true;
+ var gotEv = new BuckledEvent(strap, buckle);
+ RaiseLocalEvent(buckle, ref gotEv);
+
+ if (TryComp<PhysicsComponent>(buckle, out var physics))
+ _physics.ResetDynamics(buckle, physics);
+
+ DebugTools.AssertEqual(xform.ParentUid, strap.Owner);
}
/// <summary>
/// Tries to unbuckle the Owner of this component from its current strap.
/// </summary>
/// <param name="buckleUid">The entity to unbuckle.</param>
- /// <param name="userUid">The entity doing the unbuckling.</param>
- /// <param name="force">
- /// Whether to force the unbuckling or not. Does not guarantee true to
- /// be returned, but guarantees the owner to be unbuckled afterwards.
- /// </param>
+ /// <param name="user">The entity doing the unbuckling.</param>
/// <param name="buckleComp">The buckle component of the entity to unbuckle.</param>
/// <returns>
/// true if the owner was unbuckled, otherwise false even if the owner
/// was previously already unbuckled.
/// </returns>
- public bool TryUnbuckle(EntityUid buckleUid, EntityUid userUid, bool force = false, BuckleComponent? buckleComp = null)
+ public bool TryUnbuckle(EntityUid buckleUid,
+ EntityUid? user,
+ BuckleComponent? buckleComp = null,
+ bool popup = true)
+ {
+ return TryUnbuckle((buckleUid, buckleComp), user, popup);
+ }
+
+ public bool TryUnbuckle(Entity<BuckleComponent?> buckle, EntityUid? user, bool popup)
{
- if (!Resolve(buckleUid, ref buckleComp, false) ||
- buckleComp.BuckledTo is not { } strapUid)
+ if (!Resolve(buckle.Owner, ref buckle.Comp))
return false;
- if (!force)
- {
- var attemptEvent = new BuckleAttemptEvent(strapUid, buckleUid, userUid, false);
- RaiseLocalEvent(attemptEvent.BuckledEntity, ref attemptEvent);
- RaiseLocalEvent(attemptEvent.StrapEntity, ref attemptEvent);
- if (attemptEvent.Cancelled)
- return false;
+ if (!CanUnbuckle(buckle, user, popup, out var strap))
+ return false;
- if (_gameTiming.CurTime < buckleComp.BuckleTime + buckleComp.Delay)
- return false;
+ Unbuckle(buckle!, strap, user);
+ return true;
+ }
- if (!_interaction.InRangeUnobstructed(userUid, strapUid, buckleComp.Range, popup: true))
- return false;
+ public void Unbuckle(Entity<BuckleComponent?> buckle, EntityUid? user)
+ {
+ if (!Resolve(buckle.Owner, ref buckle.Comp, false))
+ return;
- if (HasComp<SleepingComponent>(buckleUid) && buckleUid == userUid)
- return false;
+ if (buckle.Comp.BuckledTo is not { } strap)
+ return;
- // If the person is crit or dead in any kind of strap, return. This prevents people from unbuckling themselves while incapacitated.
- if (_mobState.IsIncapacitated(buckleUid) && userUid == buckleUid)
- return false;
+ if (!TryComp(strap, out StrapComponent? strapComp))
+ {
+ Log.Error($"Encountered buckle {ToPrettyString(buckle.Owner)} with invalid strap entity {ToPrettyString(strap)}");
+ SetBuckledTo(buckle!, null);
+ return;
}
- // Logging
- if (userUid != buckleUid)
- _adminLogger.Add(LogType.Action, LogImpact.Low, $"{ToPrettyString(userUid):player} unbuckled {ToPrettyString(buckleUid)} from {ToPrettyString(strapUid)}");
- else
- _adminLogger.Add(LogType.Action, LogImpact.Low, $"{ToPrettyString(userUid):player} unbuckled themselves from {ToPrettyString(strapUid)}");
+ Unbuckle(buckle!, (strap, strapComp), user);
+ }
+
+ private void Unbuckle(Entity<BuckleComponent> buckle, Entity<StrapComponent> strap, EntityUid? user)
+ {
+ if (user == buckle.Owner)
+ _adminLogger.Add(LogType.Action, LogImpact.Low, $"{user} unbuckled themselves from {strap}");
+ else if (user != null)
+ _adminLogger.Add(LogType.Action, LogImpact.Low, $"{user} unbuckled {buckle} from {strap}");
- SetBuckledTo(buckleUid, null, null, buckleComp);
+ _audio.PlayPredicted(strap.Comp.UnbuckleSound, strap, user);
- if (!TryComp<StrapComponent>(strapUid, out var strapComp))
- return false;
+ SetBuckledTo(buckle, null);
- var buckleXform = Transform(buckleUid);
- var oldBuckledXform = Transform(strapUid);
+ var buckleXform = Transform(buckle);
+ var oldBuckledXform = Transform(strap);
- if (buckleXform.ParentUid == strapUid && !Terminating(buckleXform.ParentUid))
+ if (buckleXform.ParentUid == strap.Owner && !Terminating(buckleXform.ParentUid))
{
- _container.AttachParentToContainerOrGrid((buckleUid, buckleXform));
+ _container.AttachParentToContainerOrGrid((buckle, buckleXform));
- var oldBuckledToWorldRot = _transform.GetWorldRotation(strapUid);
+ var oldBuckledToWorldRot = _transform.GetWorldRotation(strap);
_transform.SetWorldRotation(buckleXform, oldBuckledToWorldRot);
- if (strapComp.UnbuckleOffset != Vector2.Zero)
- buckleXform.Coordinates = oldBuckledXform.Coordinates.Offset(strapComp.UnbuckleOffset);
+ if (strap.Comp.UnbuckleOffset != Vector2.Zero)
+ buckleXform.Coordinates = oldBuckledXform.Coordinates.Offset(strap.Comp.UnbuckleOffset);
}
- if (TryComp(buckleUid, out AppearanceComponent? appearance))
- Appearance.SetData(buckleUid, BuckleVisuals.Buckled, false, appearance);
- _rotationVisuals.ResetHorizontalAngle(buckleUid);
+ _rotationVisuals.ResetHorizontalAngle(buckle.Owner);
+ Appearance.SetData(strap, StrapVisuals.State, strap.Comp.BuckledEntities.Count != 0);
+ Appearance.SetData(buckle, BuckleVisuals.Buckled, false);
- if (TryComp<MobStateComponent>(buckleUid, out var mobState)
- && _mobState.IsIncapacitated(buckleUid, mobState)
- || HasComp<KnockedDownComponent>(buckleUid))
- {
- _standing.Down(buckleUid);
- }
+ if (HasComp<KnockedDownComponent>(buckle) || _mobState.IsIncapacitated(buckle))
+ _standing.Down(buckle);
else
- {
- _standing.Stand(buckleUid);
- }
+ _standing.Stand(buckle);
- if (_mobState.IsIncapacitated(buckleUid, mobState))
- {
- _standing.Down(buckleUid);
- }
- if (strapComp.BuckledEntities.Remove(buckleUid))
- {
- strapComp.OccupiedSize -= buckleComp.Size;
- Dirty(strapUid, strapComp);
- }
-
- _joints.RefreshRelay(buckleUid);
- Appearance.SetData(strapUid, StrapVisuals.State, strapComp.BuckledEntities.Count != 0);
+ _joints.RefreshRelay(buckle);
- // TODO: Buckle listening to moveevents is sussy anyway.
- if (!TerminatingOrDeleted(strapUid))
- _audio.PlayPredicted(strapComp.UnbuckleSound, strapUid, userUid);
+ var buckleEv = new UnbuckledEvent(strap, buckle);
+ RaiseLocalEvent(buckle, ref buckleEv);
- var ev = new BuckleChangeEvent(strapUid, buckleUid, false);
- RaiseLocalEvent(buckleUid, ref ev);
- RaiseLocalEvent(strapUid, ref ev);
+ var strapEv = new UnstrappedEvent(strap, buckle);
+ RaiseLocalEvent(strap, ref strapEv);
+ }
- return true;
+ public bool CanUnbuckle(Entity<BuckleComponent?> buckle, EntityUid user, bool popup)
+ {
+ return CanUnbuckle(buckle, user, popup, out _);
}
- /// <summary>
- /// Makes an entity toggle the buckling status of the owner to a
- /// specific entity.
- /// </summary>
- /// <param name="buckleUid">The entity to buckle/unbuckle from <see cref="to"/>.</param>
- /// <param name="userUid">The entity doing the buckling/unbuckling.</param>
- /// <param name="strapUid">
- /// The entity to toggle the buckle status of the owner to.
- /// </param>
- /// <param name="force">
- /// 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.
- /// </param>
- /// <param name="buckle">The buckle component of the entity to buckle/unbuckle from <see cref="to"/>.</param>
- /// <returns>true if the buckling status was changed, false otherwise.</returns>
- public bool ToggleBuckle(
- EntityUid buckleUid,
- EntityUid userUid,
- EntityUid strapUid,
- bool force = false,
- BuckleComponent? buckle = null)
+ private bool CanUnbuckle(Entity<BuckleComponent?> buckle, EntityUid? user, bool popup, out Entity<StrapComponent> strap)
{
- if (!Resolve(buckleUid, ref buckle, false))
+ strap = default;
+ if (!Resolve(buckle.Owner, ref buckle.Comp))
return false;
- if (!buckle.Buckled)
- {
- return TryBuckle(buckleUid, userUid, strapUid, buckle);
- }
- else
+ if (buckle.Comp.BuckledTo is not { } strapUid)
+ return false;
+
+ if (!TryComp(strapUid, out StrapComponent? strapComp))
{
- return TryUnbuckle(buckleUid, userUid, force, buckle);
+ Log.Error($"Encountered buckle {ToPrettyString(buckle.Owner)} with invalid strap entity {ToPrettyString(strap)}");
+ SetBuckledTo(buckle!, null);
+ return false;
}
+ strap = (strapUid, strapComp);
+ if (_gameTiming.CurTime < buckle.Comp.BuckleTime + buckle.Comp.Delay)
+ return false;
+
+ if (user != null && !_interaction.InRangeUnobstructed(user.Value, strap.Owner, buckle.Comp.Range, popup: popup))
+ return false;
+
+ var unbuckleAttempt = new UnbuckleAttemptEvent(strap, buckle!, user, popup);
+ RaiseLocalEvent(buckle, ref unbuckleAttempt);
+ if (unbuckleAttempt.Cancelled)
+ return false;
+
+ var unstrapAttempt = new UnstrapAttemptEvent(strap, buckle!, user, popup);
+ RaiseLocalEvent(strap, ref unstrapAttempt);
+ return !unstrapAttempt.Cancelled;
}
}
--- /dev/null
+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<StrapComponent, GetVerbsEvent<InteractionVerb>>(AddStrapVerbs);
+ SubscribeLocalEvent<StrapComponent, InteractHandEvent>(OnStrapInteractHand);
+ SubscribeLocalEvent<StrapComponent, DragDropTargetEvent>(OnStrapDragDropTarget);
+ SubscribeLocalEvent<StrapComponent, CanDropTargetEvent>(OnCanDropTarget);
+
+ SubscribeLocalEvent<BuckleComponent, GetVerbsEvent<InteractionVerb>>(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<InteractionVerb> 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<BuckleComponent>(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<BuckleComponent>(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<BuckleComponent>(@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<InteractionVerb> 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);
+ }
+
+}
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<StrapComponent, ComponentStartup>(OnStrapStartup);
SubscribeLocalEvent<StrapComponent, ComponentShutdown>(OnStrapShutdown);
SubscribeLocalEvent<StrapComponent, ComponentRemove>((e, c, _) => StrapRemoveAll(e, c));
- SubscribeLocalEvent<StrapComponent, EntInsertedIntoContainerMessage>(OnStrapEntModifiedFromContainer);
- SubscribeLocalEvent<StrapComponent, EntRemovedFromContainerMessage>(OnStrapEntModifiedFromContainer);
- SubscribeLocalEvent<StrapComponent, GetVerbsEvent<InteractionVerb>>(AddStrapVerbs);
SubscribeLocalEvent<StrapComponent, ContainerGettingInsertedAttemptEvent>(OnStrapContainerGettingInsertedAttempt);
- SubscribeLocalEvent<StrapComponent, InteractHandEvent>(OnStrapInteractHand);
SubscribeLocalEvent<StrapComponent, DestructionEventArgs>((e, c, _) => StrapRemoveAll(e, c));
SubscribeLocalEvent<StrapComponent, BreakageEventArgs>((e, c, _) => StrapRemoveAll(e, c));
- SubscribeLocalEvent<StrapComponent, DragDropTargetEvent>(OnStrapDragDropTarget);
- SubscribeLocalEvent<StrapComponent, CanDropTargetEvent>(OnCanDropTarget);
SubscribeLocalEvent<StrapComponent, FoldAttemptEvent>(OnAttemptFold);
-
- SubscribeLocalEvent<StrapComponent, MoveEvent>(OnStrapMoveEvent);
SubscribeLocalEvent<StrapComponent, MachineDeconstructedEvent>((e, c, _) => StrapRemoveAll(e, c));
}
private void OnStrapShutdown(EntityUid uid, StrapComponent component, ComponentShutdown args)
{
- if (LifeStage(uid) > EntityLifeStage.MapInitialized)
- return;
-
- StrapRemoveAll(uid, component);
- }
-
- private void OnStrapEntModifiedFromContainer(EntityUid uid, StrapComponent component, ContainerModifiedMessage message)
- {
- if (_gameTiming.ApplyingState)
- return;
-
- foreach (var buckledEntity in component.BuckledEntities)
- {
- if (!TryComp<BuckleComponent>(buckledEntity, out var buckleComp))
- {
- continue;
- }
-
- ContainerModifiedReAttach(buckledEntity, uid, buckleComp, component);
- }
- }
-
- private void ContainerModifiedReAttach(EntityUid buckleUid, EntityUid strapUid, BuckleComponent? buckleComp = null, StrapComponent? strapComp = null)
- {
- if (!Resolve(buckleUid, ref buckleComp, false) ||
- !Resolve(strapUid, ref strapComp, false))
- return;
-
- var contained = _container.TryGetContainingContainer(buckleUid, out var ownContainer);
- var strapContained = _container.TryGetContainingContainer(strapUid, out var strapContainer);
-
- if (contained != strapContained || ownContainer != strapContainer)
- {
- TryUnbuckle(buckleUid, buckleUid, true, buckleComp);
- return;
- }
-
- if (!contained)
- {
- ReAttach(buckleUid, strapUid, buckleComp, strapComp);
- }
+ if (!TerminatingOrDeleted(uid))
+ StrapRemoveAll(uid, component);
}
private void OnStrapContainerGettingInsertedAttempt(EntityUid uid, StrapComponent component, ContainerGettingInsertedAttemptEvent args)
{
// If someone is attempting to put this item inside of a backpack, ensure that it has no entities strapped to it.
- if (HasComp<StorageComponent>(args.Container.Owner) && component.BuckledEntities.Count != 0)
+ if (args.Container.ID == StorageComponent.ContainerId && component.BuckledEntities.Count != 0)
args.Cancel();
}
- private void OnStrapInteractHand(EntityUid uid, StrapComponent component, InteractHandEvent args)
- {
- if (args.Handled)
- return;
-
- args.Handled = ToggleBuckle(args.User, args.User, uid);
- }
-
- private void AddStrapVerbs(EntityUid uid, StrapComponent component, GetVerbsEvent<InteractionVerb> 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<BuckleComponent>(entity);
-
- if (!_interaction.InRangeUnobstructed(args.User, args.Target, range: buckledComp.Range))
- continue;
-
- var verb = new InteractionVerb()
- {
- Act = () => TryUnbuckle(entity, args.User, buckleComp: buckledComp),
- Category = VerbCategory.Unbuckle,
- Text = entity == args.User
- ? Loc.GetString("verb-self-target-pronoun")
- : Comp<MetaDataComponent>(entity).EntityName
- };
-
- // In the event that you have more than once entity with the same name strapped to the same object,
- // these two verbs will be identical according to Verb.CompareTo, and only one with actually be added to
- // the verb list. However this should rarely ever be a problem. If it ever is, it could be fixed by
- // appending an integer to verb.Text to distinguish the verbs.
-
- args.Verbs.Add(verb);
- }
-
- // Add a verb to buckle the user.
- if (TryComp<BuckleComponent>(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<BuckleComponent>(@using, out var usingBuckle) &&
- StrapHasSpace(uid, usingBuckle, component) &&
- _interaction.InRangeUnobstructed(@using, args.Target, range: usingBuckle.Range))
- {
- // Check that the entity is unobstructed from the target (ignoring the user).
- bool Ignored(EntityUid entity) => entity == args.User || entity == args.Target || entity == @using;
- if (!_interaction.InRangeUnobstructed(@using, args.Target, usingBuckle.Range, predicate: Ignored))
- return;
-
- var isPlayer = _playerManager.TryGetSessionByEntity(@using, out var _);
- InteractionVerb verb = new()
- {
- Act = () => TryBuckle(@using, args.User, args.Target, usingBuckle),
- Category = VerbCategory.Buckle,
- Text = Comp<MetaDataComponent>(@using).EntityName,
- // just a held object, the user is probably just trying to sit down.
- // If the used entity is a person being pulled, prioritize this verb. Conversely, if it is
- Priority = isPlayer ? 1 : -1
- };
-
- args.Verbs.Add(verb);
- }
- }
-
- private void OnCanDropTarget(EntityUid uid, StrapComponent component, ref CanDropTargetEvent args)
- {
- args.CanDrop = StrapCanDragDropOn(uid, args.User, uid, args.Dragged, component);
- args.Handled = true;
- }
-
private void OnAttemptFold(EntityUid uid, StrapComponent component, ref FoldAttemptEvent args)
{
if (args.Cancelled)
args.Cancelled = component.BuckledEntities.Count != 0;
}
- private void OnStrapDragDropTarget(EntityUid uid, StrapComponent component, ref DragDropTargetEvent args)
- {
- if (!StrapCanDragDropOn(uid, args.User, uid, args.Dragged, component))
- return;
-
- args.Handled = TryBuckle(args.Dragged, args.User, uid);
- }
-
- private void OnStrapMoveEvent(EntityUid uid, StrapComponent component, ref MoveEvent args)
- {
- // TODO: This looks dirty af.
- // On rotation of a strap, reattach all buckled entities.
- // This fixes buckle offsets and draw depths.
- // This is mega cursed. Please somebody save me from Mr Buckle's wild ride.
- // Oh god I'm back here again. Send help.
-
- // Consider a chair that has a player strapped to it. Then the client receives a new server state, showing
- // that the player entity has moved elsewhere, and the chair has rotated. If the client applies the player
- // state, then the chairs transform comp state, and then the buckle state. The transform state will
- // forcefully teleport the player back to the chair (client-side only). This causes even more issues if the
- // chair was teleporting in from nullspace after having left PVS.
- //
- // One option is to just never trigger re-buckles during state application.
- // another is to.. just not do this? Like wtf is this code. But I CBF with buckle atm.
-
- if (_gameTiming.ApplyingState || args.NewRotation == args.OldRotation)
- return;
-
- foreach (var buckledEntity in component.BuckledEntities)
- {
- if (!TryComp<BuckleComponent>(buckledEntity, out var buckled))
- continue;
-
- if (!buckled.Buckled || buckled.LastEntityBuckledTo != uid)
- {
- Log.Error($"A moving strap entity {ToPrettyString(uid)} attempted to re-parent an entity that does not 'belong' to it {ToPrettyString(buckledEntity)}");
- continue;
- }
-
- ReAttach(buckledEntity, uid, buckled, component);
- Dirty(buckledEntity, buckled);
- }
- }
-
- private bool StrapCanDragDropOn(
- EntityUid strapUid,
- EntityUid userUid,
- EntityUid targetUid,
- EntityUid buckleUid,
- StrapComponent? strapComp = null,
- BuckleComponent? buckleComp = null)
- {
- if (!Resolve(strapUid, ref strapComp, false) ||
- !Resolve(buckleUid, ref buckleComp, false))
- {
- return false;
- }
-
- bool Ignored(EntityUid entity) => entity == userUid || entity == buckleUid || entity == targetUid;
-
- return _interaction.InRangeUnobstructed(targetUid, buckleUid, buckleComp.Range, predicate: Ignored);
- }
-
/// <summary>
/// Remove everything attached to the strap
/// </summary>
{
TryUnbuckle(entity, entity, true);
}
-
- strapComp.BuckledEntities.Clear();
- strapComp.OccupiedSize = 0;
- Dirty(uid, strapComp);
}
private bool StrapHasSpace(EntityUid strapUid, BuckleComponent buckleComp, StrapComponent? strapComp = null)
if (!Resolve(strapUid, ref strapComp, false))
return false;
- return strapComp.OccupiedSize + buckleComp.Size <= strapComp.Size;
- }
-
- /// <summary>
- /// Try to add an entity to the strap
- /// </summary>
- private bool StrapTryAdd(EntityUid strapUid, EntityUid buckleUid, BuckleComponent buckleComp, bool force = false, StrapComponent? strapComp = null)
- {
- if (!Resolve(strapUid, ref strapComp, false) ||
- !strapComp.Enabled)
- return false;
-
- if (!force && !StrapHasSpace(strapUid, buckleComp, strapComp))
- return false;
-
- if (!strapComp.BuckledEntities.Add(buckleUid))
- return false;
-
- strapComp.OccupiedSize += buckleComp.Size;
-
- Appearance.SetData(strapUid, StrapVisuals.State, true);
+ var avail = strapComp.Size;
+ foreach (var buckle in strapComp.BuckledEntities)
+ {
+ avail -= CompOrNull<BuckleComponent>(buckle)?.Size ?? 0;
+ }
- Dirty(strapUid, strapComp);
- return true;
+ return avail >= buckleComp.Size;
}
/// <summary>
return;
strapComp.Enabled = enabled;
+ Dirty(strapUid, strapComp);
if (!enabled)
StrapRemoveAll(strapUid, strapComp);
using Content.Shared.ActionBlocker;
using Content.Shared.Administration.Logs;
using Content.Shared.Alert;
-using Content.Shared.Buckle.Components;
using Content.Shared.Interaction;
using Content.Shared.Mobs.Systems;
using Content.Shared.Popups;
-using Content.Shared.Pulling;
+using Content.Shared.Rotation;
using Content.Shared.Standing;
-using Robust.Shared.Audio;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Containers;
-using Robust.Shared.Map;
using Robust.Shared.Network;
using Robust.Shared.Physics.Systems;
using Robust.Shared.Player;
using Robust.Shared.Timing;
-using PullingSystem = Content.Shared.Movement.Pulling.Systems.PullingSystem;
namespace Content.Shared.Buckle;
[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!;
/// <inheritdoc/>
public override void Initialize()
InitializeBuckle();
InitializeStrap();
- }
-
- /// <summary>
- /// Reattaches this entity to the strap, modifying its position and rotation.
- /// </summary>
- /// <param name="buckleUid">The entity to reattach.</param>
- /// <param name="strapUid">The entity to reattach the buckleUid entity to.</param>
- 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 <reasons> so this might fail.
- // TODO: Make buckle not do that.
- if (buckleTransform.ParentUid != strapUid)
- return;
-
- _transform.SetLocalRotation(buckleUid, Angle.Zero, buckleTransform);
- _joints.SetRelay(buckleUid, strapUid);
-
- switch (strapComp.Position)
- {
- case StrapPosition.None:
- break;
- case StrapPosition.Stand:
- _standing.Stand(buckleUid);
- break;
- case StrapPosition.Down:
- _standing.Down(buckleUid, false, false);
- break;
- }
+ InitializeInteraction();
}
}
SubscribeLocalEvent<ClimbingComponent, EntParentChangedMessage>(OnParentChange);
SubscribeLocalEvent<ClimbingComponent, ClimbDoAfterEvent>(OnDoAfter);
SubscribeLocalEvent<ClimbingComponent, EndCollideEvent>(OnClimbEndCollide);
- SubscribeLocalEvent<ClimbingComponent, BuckleChangeEvent>(OnBuckleChange);
+ SubscribeLocalEvent<ClimbingComponent, BuckledEvent>(OnBuckled);
SubscribeLocalEvent<ClimbableComponent, CanDropTargetEvent>(OnCanDragDropOn);
SubscribeLocalEvent<ClimbableComponent, GetVerbsEvent<AlternativeVerb>>(AddClimbableVerb);
Climb(uid, uid, climbable, true, component);
}
- private void OnBuckleChange(EntityUid uid, ClimbingComponent component, ref BuckleChangeEvent args)
+ private void OnBuckled(EntityUid uid, ClimbingComponent component, ref BuckledEvent args)
{
- if (!args.Buckling)
- return;
StopClimb(uid, component);
}
SubscribeLocalEvent<CuffableComponent, IsUnequippingAttemptEvent>(OnUnequipAttempt);
SubscribeLocalEvent<CuffableComponent, BeingPulledAttemptEvent>(OnBeingPulledAttempt);
SubscribeLocalEvent<CuffableComponent, BuckleAttemptEvent>(OnBuckleAttemptEvent);
+ SubscribeLocalEvent<CuffableComponent, UnbuckleAttemptEvent>(OnUnbuckleAttemptEvent);
SubscribeLocalEvent<CuffableComponent, GetVerbsEvent<Verb>>(AddUncuffVerb);
SubscribeLocalEvent<CuffableComponent, UnCuffDoAfterEvent>(OnCuffableDoAfter);
SubscribeLocalEvent<CuffableComponent, PullStartedMessage>(OnPull);
args.Cancel();
}
- private void OnBuckleAttemptEvent(EntityUid uid, CuffableComponent component, ref BuckleAttemptEvent args)
+ private void OnBuckleAttempt(Entity<CuffableComponent> ent, EntityUid? user, ref bool cancelled, bool buckling, bool popup)
{
- // if someone else is doing it, let it pass.
- if (args.UserEntity != uid)
+ if (cancelled || user != ent.Owner)
return;
- if (!TryComp<HandsComponent>(uid, out var hands) || component.CuffedHandCount != hands.Count)
+ if (!TryComp<HandsComponent>(ent, out var hands) || ent.Comp.CuffedHandCount != hands.Count)
return;
- args.Cancelled = true;
- var message = args.Buckling
+ cancelled = true;
+ if (!popup)
+ return;
+
+ var message = buckling
? Loc.GetString("handcuff-component-cuff-interrupt-buckled-message")
: Loc.GetString("handcuff-component-cuff-interrupt-unbuckled-message");
- _popup.PopupClient(message, uid, args.UserEntity);
+ _popup.PopupClient(message, ent, user);
+ }
+
+ private void OnBuckleAttemptEvent(Entity<CuffableComponent> ent, ref BuckleAttemptEvent args)
+ {
+ OnBuckleAttempt(ent, args.User, ref args.Cancelled, true, args.Popup);
+ }
+
+ private void OnUnbuckleAttemptEvent(Entity<CuffableComponent> ent, ref UnbuckleAttemptEvent args)
+ {
+ OnBuckleAttempt(ent, args.User, ref args.Cancelled, false, args.Popup);
}
private void OnPull(EntityUid uid, CuffableComponent component, PullMessage args)
SubscribeLocalEvent<FoldableComponent, StoreMobInItemContainerAttemptEvent>(OnStoreThisAttempt);
SubscribeLocalEvent<FoldableComponent, StorageOpenAttemptEvent>(OnFoldableOpenAttempt);
- SubscribeLocalEvent<FoldableComponent, BuckleAttemptEvent>(OnBuckleAttempt);
+ SubscribeLocalEvent<FoldableComponent, StrapAttemptEvent>(OnStrapAttempt);
}
private void OnHandleState(EntityUid uid, FoldableComponent component, ref AfterAutoHandleStateEvent args)
args.Cancelled = true;
}
- public void OnBuckleAttempt(EntityUid uid, FoldableComponent comp, ref BuckleAttemptEvent args)
+ public void OnStrapAttempt(EntityUid uid, FoldableComponent comp, ref StrapAttemptEvent args)
{
- if (args.Buckling && comp.IsFolded)
+ if (comp.IsFolded)
args.Cancelled = true;
}
using System.Numerics;
using Content.Shared.ActionBlocker;
using Content.Shared.Buckle.Components;
-using Content.Shared.Mobs.Systems;
using Content.Shared.Rotatable;
using JetBrains.Annotations;
if (!_actionBlockerSystem.CanChangeDirection(user))
return false;
- if (EntityManager.TryGetComponent(user, out BuckleComponent? buckle) && buckle.Buckled)
+ if (TryComp(user, out BuckleComponent? buckle) && buckle.BuckledTo is {} strap)
{
- var suid = buckle.LastEntityBuckledTo;
- if (suid != null)
- {
- // We're buckled to another object. Is that object rotatable?
- if (TryComp<RotatableComponent>(suid.Value, out var rotatable) && rotatable.RotateWhileAnchored)
- {
- // Note the assumption that even if unanchored, user can only do spinnychair with an "independent wheel".
- // (Since the user being buckled to it holds it down with their weight.)
- // This is logically equivalent to RotateWhileAnchored.
- // Barstools and office chairs have independent wheels, while regular chairs don't.
- _transform.SetWorldRotation(Transform(suid.Value), diffAngle);
- return true;
- }
- }
-
- return false;
+ // What if a person is strapped to a borg?
+ // I'm pretty sure this would allow them to be partially ratatouille'd
+
+ // We're buckled to another object. Is that object rotatable?
+ if (!TryComp<RotatableComponent>(strap, out var rotatable) || !rotatable.RotateWhileAnchored)
+ return false;
+
+ // Note the assumption that even if unanchored, user can only do spinnychair with an "independent wheel".
+ // (Since the user being buckled to it holds it down with their weight.)
+ // This is logically equivalent to RotateWhileAnchored.
+ // Barstools and office chairs have independent wheels, while regular chairs don't.
+ _transform.SetWorldRotation(Transform(strap), diffAngle);
+ return true;
}
// user is not buckled in; apply to their transform
using Content.Shared.Bed.Sleep;
+using Content.Shared.Buckle.Components;
using Content.Shared.CombatMode.Pacification;
using Content.Shared.Damage.ForceSay;
using Content.Shared.Emoting;
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;
SubscribeLocalEvent<MobStateComponent, TryingToSleepEvent>(OnSleepAttempt);
SubscribeLocalEvent<MobStateComponent, CombatModeShouldHandInteractEvent>(OnCombatModeShouldHandInteract);
SubscribeLocalEvent<MobStateComponent, AttemptPacifiedAttackEvent>(OnAttemptPacifiedAttack);
+
+ SubscribeLocalEvent<MobStateComponent, UnbuckleAttemptEvent>(OnUnbuckleAttempt);
+ }
+
+ private void OnUnbuckleAttempt(Entity<MobStateComponent> 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 CheckConcious(Entity<MobStateComponent> ent, ref ConsciousAttemptEvent args)
namespace Content.Shared.Movement.Pulling.Events;
-public sealed class PullStartedMessage : PullMessage
-{
- public PullStartedMessage(EntityUid pullerUid, EntityUid pullableUid) :
- base(pullerUid, pullableUid)
- {
- }
-}
+/// <summary>
+/// Event raised directed BOTH at the puller and pulled entity when a pull starts.
+/// </summary>
+public sealed class PullStartedMessage(EntityUid pullerUid, EntityUid pullableUid) : PullMessage(pullerUid, pullableUid);
-using Robust.Shared.Physics.Components;
-
-namespace Content.Shared.Movement.Pulling.Events;
+namespace Content.Shared.Movement.Pulling.Events;
/// <summary>
-/// Raised directed on both puller and pullable.
+/// Event raised directed BOTH at the puller and pulled entity when a pull starts.
/// </summary>
-public sealed class PullStoppedMessage : PullMessage
-{
- public PullStoppedMessage(EntityUid pullerUid, EntityUid pulledUid) : base(pullerUid, pulledUid)
- {
- }
-}
+public sealed class PullStoppedMessage(EntityUid pullerUid, EntityUid pulledUid) : PullMessage(pullerUid, pulledUid);
-using System.Numerics;
using Content.Shared.ActionBlocker;
using Content.Shared.Administration.Logs;
using Content.Shared.Alert;
using Content.Shared.Popups;
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.Physics;
using Robust.Shared.Physics.Components;
using Robust.Shared.Physics.Events;
SubscribeLocalEvent<PullerComponent, RefreshMovementSpeedModifiersEvent>(OnRefreshMovespeed);
SubscribeLocalEvent<PullerComponent, DropHandItemsEvent>(OnDropHandItems);
+ SubscribeLocalEvent<PullableComponent, StrappedEvent>(OnBuckled);
+ SubscribeLocalEvent<PullableComponent, BuckledEvent>(OnGotBuckled);
+
CommandBinds.Builder
.Bind(ContentKeyFunctions.ReleasePulledObject, InputCmdHandler.FromDelegate(OnReleasePulledObject, handle: false))
.Register<PullingSystem>();
}
+ private void OnBuckled(Entity<PullableComponent> ent, ref StrappedEvent args)
+ {
+ // Prevent people from pulling the entity they are buckled to
+ if (ent.Comp.Puller == args.Buckle.Owner && !args.Buckle.Comp.PullStrap)
+ StopPulling(ent, ent);
+ }
+
+ private void OnGotBuckled(Entity<PullableComponent> ent, ref BuckledEvent args)
+ {
+ StopPulling(ent, ent);
+ }
+
private void OnAfterState(Entity<PullerComponent> ent, ref AfterAutoHandleStateEvent args)
{
if (ent.Comp.Pulling == null)
private void OnPullerContainerInsert(Entity<PullerComponent> 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;
/// </summary>
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<PhysicsComponent>(pullableUid, out var pullablePhysics))
{
_physics.SetFixedRotation(pullableUid, pullableComp.PrevFixedRotation, body: pullablePhysics);
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);
if (!CanPull(pullerUid, pullableUid))
return false;
- if (!EntityManager.TryGetComponent<PhysicsComponent>(pullerUid, out var pullerPhysics) ||
- !EntityManager.TryGetComponent<PhysicsComponent>(pullableUid, out var pullablePhysics))
- {
+ if (!HasComp<PhysicsComponent>(pullerUid) || !TryComp(pullableUid, out PhysicsComponent? pullablePhysics))
return false;
- }
// Ensure that the puller is not currently pulling anything.
if (TryComp<PullableComponent>(pullerComp.Pulling, out var oldPullable)
{
// Joint startup
var union = _physics.GetHardAABB(pullerUid).Union(_physics.GetHardAABB(pullableUid, body: pullablePhysics));
- var length = Math.Max((float) union.Size.X, (float) union.Size.Y) * 0.75f;
+ var length = Math.Max(union.Size.X, union.Size.Y) * 0.75f;
var joint = _joints.CreateDistanceJoint(pullableUid, pullerUid, id: pullableComp.PullJointId);
joint.CollideConnected = false;
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;
}
// Stop moving possibilities
SubscribeLocalEvent((Entity<WaddleAnimationComponent> ent, ref StunnedEvent _) => StopWaddling(ent));
SubscribeLocalEvent((Entity<WaddleAnimationComponent> ent, ref DownedEvent _) => StopWaddling(ent));
- SubscribeLocalEvent((Entity<WaddleAnimationComponent> ent, ref BuckleChangeEvent _) => StopWaddling(ent));
+ SubscribeLocalEvent((Entity<WaddleAnimationComponent> ent, ref BuckledEvent _) => StopWaddling(ent));
SubscribeLocalEvent<WaddleAnimationComponent, GravityChangedEvent>(OnGravityChanged);
}
{
SubscribeLocalEvent<LegsParalyzedComponent, ComponentStartup>(OnStartup);
SubscribeLocalEvent<LegsParalyzedComponent, ComponentShutdown>(OnShutdown);
- SubscribeLocalEvent<LegsParalyzedComponent, BuckleChangeEvent>(OnBuckleChange);
+ SubscribeLocalEvent<LegsParalyzedComponent, BuckledEvent>(OnBuckled);
+ SubscribeLocalEvent<LegsParalyzedComponent, UnbuckledEvent>(OnUnbuckled);
SubscribeLocalEvent<LegsParalyzedComponent, ThrowPushbackAttemptEvent>(OnThrowPushbackAttempt);
SubscribeLocalEvent<LegsParalyzedComponent, UpdateCanMoveEvent>(OnUpdateCanMoveEvent);
}
_bodySystem.UpdateMovementSpeed(uid);
}
- private void OnBuckleChange(EntityUid uid, LegsParalyzedComponent component, ref BuckleChangeEvent args)
+ private void OnBuckled(EntityUid uid, LegsParalyzedComponent component, ref BuckledEvent args)
{
- if (args.Buckling)
- {
- _standingSystem.Stand(args.BuckledEntity);
- }
- else
- {
- _standingSystem.Down(args.BuckledEntity);
- }
+ _standingSystem.Stand(uid);
+ }
+
+ private void OnUnbuckled(EntityUid uid, LegsParalyzedComponent component, ref UnbuckledEvent args)
+ {
+ _standingSystem.Down(uid);
}
private void OnUpdateCanMoveEvent(EntityUid uid, LegsParalyzedComponent component, UpdateCanMoveEvent args)
components:
- type: Foldable
folded: true
+ - type: Strap
+ enabled: False
- type: entity
name: steel bench
components:
- type: Foldable
folded: true
+ - type: Strap
+ enabled: False
- type: entity
id: CheapRollerBed
components:
- type: Foldable
folded: true
+ - type: Strap
+ enabled: False
- type: entity
id: EmergencyRollerBed
components:
- type: Foldable
folded: true
+ - type: Strap
+ enabled: False