Partial buckling refactor (#29031)

* partial buckling refactor

* git mv test

* change test namespace

* git mv test

* Update test namespace

* Add pulling test

* Network BuckleTime

* Add two more tests

* smelly
This commit is contained in:
Leon Friedrich
2024-06-20 03:14:18 +12:00
committed by GitHub
parent e33f0341ad
commit fa3c89a521
38 changed files with 1053 additions and 890 deletions

View File

@@ -1,10 +1,15 @@
using System.Diagnostics.CodeAnalysis;
using Content.Shared.Interaction;
using Robust.Shared.GameStates;
using Robust.Shared.Serialization;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
namespace Content.Shared.Buckle.Components;
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState(true)]
/// <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
{
@@ -14,31 +19,23 @@ 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;
@@ -47,20 +44,18 @@ public sealed partial class BuckleComponent : Component
/// 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>
@@ -68,7 +63,6 @@ public sealed partial class BuckleComponent : Component
/// <see cref="StrapComponent"/>.
/// </summary>
[DataField]
[ViewVariables(VVAccess.ReadWrite)]
public int Size = 100;
/// <summary>
@@ -77,11 +71,90 @@ public sealed partial class BuckleComponent : Component
[ViewVariables] public int? OriginalDrawDepth;
}
[ByRefEvent]
public record struct BuckleAttemptEvent(EntityUid StrapEntity, EntityUid BuckledEntity, EntityUid UserEntity, bool Buckling, bool Cancelled = false);
[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 readonly record struct BuckleChangeEvent(EntityUid StrapEntity, EntityUid BuckledEntity, bool Buckling);
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 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 UnbuckledEvent(Entity<StrapComponent> Strap, Entity<BuckleComponent> Buckle);
[Serializable, NetSerializable]
public enum BuckleVisuals

View File

@@ -13,117 +13,77 @@ namespace Content.Shared.Buckle.Components;
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

View File

@@ -1,39 +1,46 @@
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);
@@ -41,69 +48,93 @@ public abstract partial class SharedBuckleSystem
SubscribeLocalEvent<BuckleComponent, StandAttemptEvent>(OnBuckleStandAttempt);
SubscribeLocalEvent<BuckleComponent, ThrowPushbackAttemptEvent>(OnBuckleThrowPushbackAttempt);
SubscribeLocalEvent<BuckleComponent, UpdateCanMoveEvent>(OnBuckleUpdateCanMove);
SubscribeLocalEvent<BuckleComponent, ComponentGetState>(OnGetState);
}
[ValidatePrototypeId<AlertCategoryPrototype>]
public const string BuckledAlertCategory = "Buckled";
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);
component.BuckleTime = default;
Unbuckle(ent!, null);
}
private void OnBuckleMove(EntityUid uid, BuckleComponent component, ref MoveEvent ev)
#region Pulling
private void OnPullAttempt(Entity<BuckleComponent> ent, ref StartPullAttemptEvent args)
{
if (component.BuckledTo is not { } strapUid)
// Prevent people pulling the chair they're on, etc.
if (ent.Comp.BuckledTo == args.Pulled && !ent.Comp.PullStrap)
args.Cancel();
}
private void OnBeingPulledAttempt(Entity<BuckleComponent> ent, ref BeingPulledAttemptEvent args)
{
if (args.Cancelled || !ent.Comp.Buckled)
return;
if (!CanUnbuckle(ent!, args.Puller, false))
args.Cancel();
}
private void OnPullStarted(Entity<BuckleComponent> ent, ref PullStartedMessage args)
{
Unbuckle(ent!, args.PullerUid);
}
#endregion
#region Transform
private void OnParentChanged(Entity<BuckleComponent> ent, ref EntParentChangedMessage args)
{
BuckleTransformCheck(ent, args.Transform);
}
private void OnInserted(Entity<BuckleComponent> ent, ref EntGotInsertedIntoContainerMessage args)
{
BuckleTransformCheck(ent, Transform(ent));
}
private void OnBuckleMove(Entity<BuckleComponent> ent, ref MoveEvent ev)
{
BuckleTransformCheck(ent, ev.Component);
}
/// <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 (_gameTiming.ApplyingState)
return;
if (buckle.Comp.BuckledTo is not { } strapUid)
return;
if (!TryComp<StrapComponent>(strapUid, out var strapComp))
return;
var strapPosition = Transform(strapUid).Coordinates;
if (ev.NewPosition.EntityId.IsValid() && ev.NewPosition.InRange(EntityManager, _transform, strapPosition, strapComp.MaxBuckleDistance))
return;
TryUnbuckle(uid, uid, true, component);
}
private void OnBuckleInteractHand(EntityUid uid, BuckleComponent component, InteractHandEvent args)
{
if (!component.Buckled)
return;
if (TryUnbuckle(uid, args.User, buckleComp: component))
args.Handled = true;
}
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;
Log.Error($"Encountered buckle entity {ToPrettyString(buckle)} without a valid strap entity {ToPrettyString(strapUid)}");
SetBuckledTo(buckle, null);
return;
}
args.Verbs.Add(verb);
if (xform.ParentUid != strapUid || _container.IsEntityInContainer(buckle))
{
Unbuckle(buckle, (strapUid, strapComp), null);
return;
}
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)
@@ -112,10 +143,7 @@ public abstract partial class SharedBuckleSystem
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;
}
@@ -139,10 +167,7 @@ public abstract partial class SharedBuckleSystem
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();
}
@@ -151,162 +176,139 @@ public abstract partial class SharedBuckleSystem
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);
if (strap is {} strapEnt && Resolve(strapEnt.Owner, ref strapEnt.Comp))
{
strapEnt.Comp.BuckledEntities.Add(buckle);
_alerts.ShowAlert(buckle, strapEnt.Comp.BuckledAlertType);
}
else
{
_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;
}
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);
return false;
parent = Transform(parent).ParentUid;
continue;
}
parent = Transform(parent).ParentUid;
if (_netManager.IsClient || popup || user == null)
return false;
var message = Loc.GetString(buckleUid == user
? "buckle-component-cannot-buckle-message"
: "buckle-component-other-cannot-buckle-message",
("owner", Identity.Entity(buckleUid, EntityManager)));
_popup.PopupEntity(message, user.Value, user.Value);
return false;
}
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;
@@ -315,216 +317,194 @@ public abstract partial class SharedBuckleSystem
/// <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;
}
if (TryComp<AppearanceComponent>(buckleUid, out var appearance))
Appearance.SetData(buckleUid, BuckleVisuals.Buckled, true, appearance);
_rotationVisuals.SetHorizontalAngle(buckleUid, strapComp.Rotation);
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);
var ev = new BuckleChangeEvent(strapUid, buckleUid, true);
RaiseLocalEvent(ev.BuckledEntity, ref ev);
RaiseLocalEvent(ev.StrapEntity, ref ev);
if (TryComp<PullableComponent>(buckleUid, out var ownerPullable))
{
if (ownerPullable.Puller != null)
{
_pulling.TryStopPull(buckleUid, ownerPullable);
}
}
if (TryComp<PhysicsComponent>(buckleUid, out var physics))
{
_physics.ResetDynamics(buckleUid, physics);
}
if (!buckleComp.PullStrap && TryComp<PullableComponent>(strapUid, out var toPullable))
{
if (toPullable.Puller == buckleUid)
{
// can't pull it and buckle to it at the same time
_pulling.TryStopPull(strapUid, toPullable);
}
}
// 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)}");
Buckle((buckle, buckleComp), (strap, strapComp), user);
return true;
}
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)}");
_audio.PlayPredicted(strap.Comp.BuckleSound, strap, user);
SetBuckledTo(buckle, strap!);
Appearance.SetData(strap, StrapVisuals.State, true);
Appearance.SetData(buckle, BuckleVisuals.Buckled, true);
_rotationVisuals.SetHorizontalAngle(buckle.Owner, strap.Comp.Rotation);
var xform = Transform(buckle);
var coords = new EntityCoordinates(strap, strap.Comp.BuckleOffset);
_transform.SetCoordinates(buckle, xform, coords, rotation: Angle.Zero);
_joints.SetRelay(buckle, strap);
switch (strap.Comp.Position)
{
case StrapPosition.Stand:
_standing.Stand(buckle);
break;
case StrapPosition.Down:
_standing.Down(buckle, false, false);
break;
}
var ev = new StrappedEvent(strap, buckle);
RaiseLocalEvent(strap, ref ev);
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)
{
if (!Resolve(buckleUid, ref buckleComp, false) ||
buckleComp.BuckledTo is not { } strapUid)
return TryUnbuckle((buckleUid, buckleComp), user, popup);
}
public bool TryUnbuckle(Entity<BuckleComponent?> buckle, EntityUid? user, bool popup)
{
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 (_gameTiming.CurTime < buckleComp.BuckleTime + buckleComp.Delay)
return false;
if (!_interaction.InRangeUnobstructed(userUid, strapUid, buckleComp.Range, popup: true))
return false;
if (HasComp<SleepingComponent>(buckleUid) && buckleUid == userUid)
return false;
// 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;
}
// 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)}");
SetBuckledTo(buckleUid, null, null, buckleComp);
if (!TryComp<StrapComponent>(strapUid, out var strapComp))
if (!CanUnbuckle(buckle, user, popup, out var strap))
return false;
var buckleXform = Transform(buckleUid);
var oldBuckledXform = Transform(strapUid);
if (buckleXform.ParentUid == strapUid && !Terminating(buckleXform.ParentUid))
{
_container.AttachParentToContainerOrGrid((buckleUid, buckleXform));
var oldBuckledToWorldRot = _transform.GetWorldRotation(strapUid);
_transform.SetWorldRotation(buckleXform, oldBuckledToWorldRot);
if (strapComp.UnbuckleOffset != Vector2.Zero)
buckleXform.Coordinates = oldBuckledXform.Coordinates.Offset(strapComp.UnbuckleOffset);
}
if (TryComp(buckleUid, out AppearanceComponent? appearance))
Appearance.SetData(buckleUid, BuckleVisuals.Buckled, false, appearance);
_rotationVisuals.ResetHorizontalAngle(buckleUid);
if (TryComp<MobStateComponent>(buckleUid, out var mobState)
&& _mobState.IsIncapacitated(buckleUid, mobState)
|| HasComp<KnockedDownComponent>(buckleUid))
{
_standing.Down(buckleUid);
}
else
{
_standing.Stand(buckleUid);
}
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);
// TODO: Buckle listening to moveevents is sussy anyway.
if (!TerminatingOrDeleted(strapUid))
_audio.PlayPredicted(strapComp.UnbuckleSound, strapUid, userUid);
var ev = new BuckleChangeEvent(strapUid, buckleUid, false);
RaiseLocalEvent(buckleUid, ref ev);
RaiseLocalEvent(strapUid, ref ev);
Unbuckle(buckle!, strap, user);
return true;
}
/// <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)
public void Unbuckle(Entity<BuckleComponent?> buckle, EntityUid? user)
{
if (!Resolve(buckleUid, ref buckle, false))
if (!Resolve(buckle.Owner, ref buckle.Comp, false))
return;
if (buckle.Comp.BuckledTo is not { } strap)
return;
if (!TryComp(strap, out StrapComponent? strapComp))
{
Log.Error($"Encountered buckle {ToPrettyString(buckle.Owner)} with invalid strap entity {ToPrettyString(strap)}");
SetBuckledTo(buckle!, null);
return;
}
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}");
_audio.PlayPredicted(strap.Comp.UnbuckleSound, strap, user);
SetBuckledTo(buckle, null);
var buckleXform = Transform(buckle);
var oldBuckledXform = Transform(strap);
if (buckleXform.ParentUid == strap.Owner && !Terminating(buckleXform.ParentUid))
{
_container.AttachParentToContainerOrGrid((buckle, buckleXform));
var oldBuckledToWorldRot = _transform.GetWorldRotation(strap);
_transform.SetWorldRotation(buckleXform, oldBuckledToWorldRot);
if (strap.Comp.UnbuckleOffset != Vector2.Zero)
buckleXform.Coordinates = oldBuckledXform.Coordinates.Offset(strap.Comp.UnbuckleOffset);
}
_rotationVisuals.ResetHorizontalAngle(buckle.Owner);
Appearance.SetData(strap, StrapVisuals.State, strap.Comp.BuckledEntities.Count != 0);
Appearance.SetData(buckle, BuckleVisuals.Buckled, false);
if (HasComp<KnockedDownComponent>(buckle) || _mobState.IsIncapacitated(buckle))
_standing.Down(buckle);
else
_standing.Stand(buckle);
_joints.RefreshRelay(buckle);
var buckleEv = new UnbuckledEvent(strap, buckle);
RaiseLocalEvent(buckle, ref buckleEv);
var strapEv = new UnstrappedEvent(strap, buckle);
RaiseLocalEvent(strap, ref strapEv);
}
public bool CanUnbuckle(Entity<BuckleComponent?> buckle, EntityUid user, bool popup)
{
return CanUnbuckle(buckle, user, popup, out _);
}
private bool CanUnbuckle(Entity<BuckleComponent?> buckle, EntityUid? user, bool popup, out Entity<StrapComponent> strap)
{
strap = default;
if (!Resolve(buckle.Owner, ref buckle.Comp))
return false;
if (!buckle.Buckled)
if (buckle.Comp.BuckledTo is not { } strapUid)
return false;
if (!TryComp(strapUid, out StrapComponent? strapComp))
{
return TryBuckle(buckleUid, userUid, strapUid, buckle);
}
else
{
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;
}
}

View File

@@ -0,0 +1,171 @@
using Content.Shared.Buckle.Components;
using Content.Shared.DragDrop;
using Content.Shared.IdentityManagement;
using Content.Shared.Interaction;
using Content.Shared.Verbs;
using Robust.Shared.Utility;
namespace Content.Shared.Buckle;
// Partial class containing interaction & verb event handlers
public abstract partial class SharedBuckleSystem
{
private void InitializeInteraction()
{
SubscribeLocalEvent<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);
}
}

View File

@@ -2,39 +2,25 @@
using Content.Shared.Buckle.Components;
using Content.Shared.Construction;
using Content.Shared.Destructible;
using Content.Shared.DragDrop;
using Content.Shared.Foldable;
using Content.Shared.Interaction;
using Content.Shared.Rotation;
using Content.Shared.Storage;
using Content.Shared.Verbs;
using Robust.Shared.Containers;
namespace Content.Shared.Buckle;
public abstract partial class SharedBuckleSystem
{
[Dependency] private readonly SharedRotationVisualsSystem _rotationVisuals = default!;
private void InitializeStrap()
{
SubscribeLocalEvent<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));
}
@@ -45,145 +31,17 @@ public abstract partial class SharedBuckleSystem
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)
@@ -192,69 +50,6 @@ public abstract partial class SharedBuckleSystem
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>
@@ -264,10 +59,6 @@ public abstract partial class SharedBuckleSystem
{
TryUnbuckle(entity, entity, true);
}
strapComp.BuckledEntities.Clear();
strapComp.OccupiedSize = 0;
Dirty(uid, strapComp);
}
private bool StrapHasSpace(EntityUid strapUid, BuckleComponent buckleComp, StrapComponent? strapComp = null)
@@ -275,30 +66,13 @@ public abstract partial class SharedBuckleSystem
if (!Resolve(strapUid, ref strapComp, false))
return false;
return strapComp.OccupiedSize + buckleComp.Size <= strapComp.Size;
}
var avail = strapComp.Size;
foreach (var buckle in strapComp.BuckledEntities)
{
avail -= CompOrNull<BuckleComponent>(buckle)?.Size ?? 0;
}
/// <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);
Dirty(strapUid, strapComp);
return true;
return avail >= buckleComp.Size;
}
/// <summary>
@@ -311,6 +85,7 @@ public abstract partial class SharedBuckleSystem
return;
strapComp.Enabled = enabled;
Dirty(strapUid, strapComp);
if (!enabled)
StrapRemoveAll(strapUid, strapComp);

View File

@@ -1,21 +1,17 @@
using Content.Shared.ActionBlocker;
using Content.Shared.Administration.Logs;
using Content.Shared.Alert;
using Content.Shared.Buckle.Components;
using Content.Shared.Interaction;
using Content.Shared.Mobs.Systems;
using Content.Shared.Popups;
using Content.Shared.Pulling;
using Content.Shared.Rotation;
using Content.Shared.Standing;
using Robust.Shared.Audio;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Containers;
using Robust.Shared.Map;
using Robust.Shared.Network;
using Robust.Shared.Physics.Systems;
using Robust.Shared.Player;
using Robust.Shared.Timing;
using PullingSystem = Content.Shared.Movement.Pulling.Systems.PullingSystem;
namespace Content.Shared.Buckle;
@@ -36,10 +32,10 @@ public abstract partial class SharedBuckleSystem : EntitySystem
[Dependency] private readonly SharedInteractionSystem _interaction = default!;
[Dependency] private readonly SharedJointSystem _joints = default!;
[Dependency] private readonly SharedPopupSystem _popup = default!;
[Dependency] private readonly PullingSystem _pulling = default!;
[Dependency] private readonly SharedTransformSystem _transform = default!;
[Dependency] private readonly StandingStateSystem _standing = default!;
[Dependency] private readonly SharedPhysicsSystem _physics = default!;
[Dependency] private readonly SharedRotationVisualsSystem _rotationVisuals = default!;
/// <inheritdoc/>
public override void Initialize()
@@ -51,45 +47,6 @@ public abstract partial class SharedBuckleSystem : EntitySystem
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();
}
}