From 5aeb2ac4a02794f4e09f6aa4e9a4892ab76d6c79 Mon Sep 17 00:00:00 2001 From: DrSmugleaf Date: Fri, 18 Nov 2022 22:08:28 +0100 Subject: [PATCH] ECS strap component (#12627) --- Content.Client/Buckle/BuckleComponent.cs | 14 +- Content.Client/Buckle/BuckleSystem.cs | 4 +- Content.Client/Buckle/Strap/StrapComponent.cs | 17 +- .../Buckle/Components/BuckleComponent.cs | 2 + .../Buckle/Components/StrapComponent.cs | 198 ++------ .../Buckle/Systems/BuckleSystem.Buckle.cs | 401 ++++++++++++++++ .../Buckle/Systems/BuckleSystem.Strap.cs | 242 ++++++++++ Content.Server/Buckle/Systems/BuckleSystem.cs | 438 +----------------- Content.Server/Buckle/Systems/StrapSystem.cs | 142 ------ Content.Server/Foldable/FoldableSystem.cs | 5 +- Content.Server/Toilet/ToiletSystem.cs | 3 +- .../Buckle/Components/SharedStrapComponent.cs | 177 ++++--- .../Buckle/SharedBuckleSystem.Buckle.cs | 107 +++++ .../Buckle/SharedBuckleSystem.Strap.cs | 93 ++++ Content.Shared/Buckle/SharedBuckleSystem.cs | 147 +----- 15 files changed, 997 insertions(+), 993 deletions(-) create mode 100644 Content.Server/Buckle/Systems/BuckleSystem.Buckle.cs create mode 100644 Content.Server/Buckle/Systems/BuckleSystem.Strap.cs delete mode 100644 Content.Server/Buckle/Systems/StrapSystem.cs create mode 100644 Content.Shared/Buckle/SharedBuckleSystem.Buckle.cs create mode 100644 Content.Shared/Buckle/SharedBuckleSystem.Strap.cs diff --git a/Content.Client/Buckle/BuckleComponent.cs b/Content.Client/Buckle/BuckleComponent.cs index d1e224312b..69ad94b7d0 100644 --- a/Content.Client/Buckle/BuckleComponent.cs +++ b/Content.Client/Buckle/BuckleComponent.cs @@ -1,11 +1,11 @@ using Content.Shared.Buckle.Components; -namespace Content.Client.Buckle +namespace Content.Client.Buckle; + +[RegisterComponent] +[ComponentReference(typeof(SharedBuckleComponent))] +[Access(typeof(BuckleSystem))] +public sealed class BuckleComponent : SharedBuckleComponent { - [RegisterComponent] - [ComponentReference(typeof(SharedBuckleComponent))] - public sealed class BuckleComponent : SharedBuckleComponent - { - public int? OriginalDrawDepth { get; set; } - } + public int? OriginalDrawDepth { get; set; } } diff --git a/Content.Client/Buckle/BuckleSystem.cs b/Content.Client/Buckle/BuckleSystem.cs index baee2f31ae..7d7f591718 100644 --- a/Content.Client/Buckle/BuckleSystem.cs +++ b/Content.Client/Buckle/BuckleSystem.cs @@ -64,7 +64,9 @@ namespace Content.Client.Buckle private void OnStrapHandleState(EntityUid uid, StrapComponent component, ref ComponentHandleState args) { - if (args.Current is not StrapComponentState state) return; + if (args.Current is not StrapComponentState state) + return; + component.Position = state.Position; component.BuckleOffsetUnclamped = state.BuckleOffsetClamped; component.BuckledEntities.Clear(); diff --git a/Content.Client/Buckle/Strap/StrapComponent.cs b/Content.Client/Buckle/Strap/StrapComponent.cs index efdc42fe8f..1168fff8ea 100644 --- a/Content.Client/Buckle/Strap/StrapComponent.cs +++ b/Content.Client/Buckle/Strap/StrapComponent.cs @@ -1,15 +1,10 @@ using Content.Shared.Buckle.Components; -using Content.Shared.DragDrop; -namespace Content.Client.Buckle.Strap +namespace Content.Client.Buckle.Strap; + +[RegisterComponent] +[ComponentReference(typeof(SharedStrapComponent))] +[Access(typeof(BuckleSystem))] +public sealed class StrapComponent : SharedStrapComponent { - [RegisterComponent] - [ComponentReference(typeof(SharedStrapComponent))] - public sealed class StrapComponent : SharedStrapComponent - { - public override bool DragDropOn(DragDropEvent eventArgs) - { - return false; - } - } } diff --git a/Content.Server/Buckle/Components/BuckleComponent.cs b/Content.Server/Buckle/Components/BuckleComponent.cs index 974a49d171..9f003f219d 100644 --- a/Content.Server/Buckle/Components/BuckleComponent.cs +++ b/Content.Server/Buckle/Components/BuckleComponent.cs @@ -1,3 +1,4 @@ +using Content.Server.Buckle.Systems; using Content.Shared.Buckle.Components; namespace Content.Server.Buckle.Components; @@ -7,6 +8,7 @@ namespace Content.Server.Buckle.Components; /// [RegisterComponent] [ComponentReference(typeof(SharedBuckleComponent))] +[Access(typeof(BuckleSystem))] public sealed class BuckleComponent : SharedBuckleComponent { /// diff --git a/Content.Server/Buckle/Components/StrapComponent.cs b/Content.Server/Buckle/Components/StrapComponent.cs index 12c0157d89..e3b892bab4 100644 --- a/Content.Server/Buckle/Components/StrapComponent.cs +++ b/Content.Server/Buckle/Components/StrapComponent.cs @@ -1,167 +1,57 @@ -using System.Linq; using Content.Server.Buckle.Systems; using Content.Shared.Alert; using Content.Shared.Buckle.Components; -using Content.Shared.DragDrop; using Robust.Shared.Audio; -namespace Content.Server.Buckle.Components +namespace Content.Server.Buckle.Components; + +[RegisterComponent] +[ComponentReference(typeof(SharedStrapComponent))] +[Access(typeof(BuckleSystem))] +public sealed class StrapComponent : SharedStrapComponent { - [RegisterComponent] - [ComponentReference(typeof(SharedStrapComponent))] - public sealed class StrapComponent : SharedStrapComponent - { - [Dependency] private readonly IEntityManager _entityManager = default!; + /// + /// The angle in degrees to rotate the player by when they get strapped + /// + [DataField("rotation")] + public int Rotation { get; set; } - /// - /// The angle in degrees to rotate the player by when they get strapped - /// - [DataField("rotation")] - private int _rotation; + /// + /// The size of the strap which is compared against when buckling entities + /// + [DataField("size")] + public int Size { get; set; } = 100; - /// - /// The size of the strap which is compared against when buckling entities - /// - [DataField("size")] private int _size = 100; - private int _occupiedSize; + /// + /// If disabled, nothing can be buckled on this object, and it will unbuckle anything that's already buckled + /// + public bool Enabled { get; set; } = true; - private bool _enabled = true; + /// + /// You can specify the offset the entity will have after unbuckling. + /// + [DataField("unbuckleOffset", required: false)] + public Vector2 UnbuckleOffset = Vector2.Zero; + /// + /// The sound to be played when a mob is buckled + /// + [DataField("buckleSound")] + public SoundSpecifier BuckleSound { get; } = new SoundPathSpecifier("/Audio/Effects/buckle.ogg"); - /// - /// If disabled, nothing can be buckled on this object, and it will unbuckle anything that's already buckled - /// - public bool Enabled - { - get => _enabled; - set - { - _enabled = value; - if (_enabled == value) return; - RemoveAll(); - } - } + /// + /// The sound to be played when a mob is unbuckled + /// + [DataField("unbuckleSound")] + public SoundSpecifier UnbuckleSound { get; } = new SoundPathSpecifier("/Audio/Effects/unbuckle.ogg"); - /// - /// You can specify the offset the entity will have after unbuckling. - /// - [DataField("unbuckleOffset", required: false)] - public Vector2 UnbuckleOffset = Vector2.Zero; - /// - /// The sound to be played when a mob is buckled - /// - [DataField("buckleSound")] - public SoundSpecifier BuckleSound { get; } = new SoundPathSpecifier("/Audio/Effects/buckle.ogg"); + /// + /// ID of the alert to show when buckled + /// + [DataField("buckledAlertType")] + public AlertType BuckledAlertType { get; } = AlertType.Buckled; - /// - /// The sound to be played when a mob is unbuckled - /// - [DataField("unbuckleSound")] - public SoundSpecifier UnbuckleSound { get; } = new SoundPathSpecifier("/Audio/Effects/unbuckle.ogg"); - - /// - /// ID of the alert to show when buckled - /// - [DataField("buckledAlertType")] - public AlertType BuckledAlertType { get; } = AlertType.Buckled; - - /// - /// The sum of the sizes of all the buckled entities in this strap - /// - [ViewVariables] - public int OccupiedSize => _occupiedSize; - - /// - /// Checks if this strap has enough space for a new occupant. - /// - /// The new occupant - /// true if there is enough space, false otherwise - public bool HasSpace(BuckleComponent buckle) - { - return OccupiedSize + buckle.Size <= _size; - } - - /// - /// DO NOT CALL THIS DIRECTLY. - /// Adds a buckled entity. Called from - /// - /// The component to add - /// - /// Whether or not to check if the strap has enough space - /// - /// True if added, false otherwise - public bool TryAdd(BuckleComponent buckle, bool force = false) - { - if (!Enabled) return false; - - if (!force && !HasSpace(buckle)) - { - return false; - } - - if (!BuckledEntities.Add(buckle.Owner)) - { - return false; - } - - _occupiedSize += buckle.Size; - - if(_entityManager.TryGetComponent(buckle.Owner, out var appearanceComponent)) - appearanceComponent.SetData(StrapVisuals.RotationAngle, _rotation); - - // Update the visuals of the strap object - if (IoCManager.Resolve().TryGetComponent(Owner, out var appearance)) - { - appearance.SetData(StrapVisuals.State, true); - } - - Dirty(); - return true; - } - - /// - /// Removes a buckled entity. - /// Called from - /// - /// The component to remove - public void Remove(BuckleComponent buckle) - { - if (BuckledEntities.Remove(buckle.Owner)) - { - if (IoCManager.Resolve().TryGetComponent(Owner, out var appearance)) - { - appearance.SetData(StrapVisuals.State, false); - } - - _occupiedSize -= buckle.Size; - Dirty(); - } - } - - protected override void OnRemove() - { - base.OnRemove(); - - RemoveAll(); - } - - public void RemoveAll() - { - var buckleSystem = IoCManager.Resolve().System(); - - foreach (var entity in BuckledEntities.ToArray()) - { - buckleSystem.TryUnbuckle(entity, entity, true); - } - - BuckledEntities.Clear(); - _occupiedSize = 0; - Dirty(); - } - - public override bool DragDropOn(DragDropEvent eventArgs) - { - var buckleSystem = IoCManager.Resolve().System(); - return buckleSystem.TryBuckle(eventArgs.Dragged, eventArgs.User, Owner); - } - } + /// + /// The sum of the sizes of all the buckled entities in this strap + /// + public int OccupiedSize { get; set; } } diff --git a/Content.Server/Buckle/Systems/BuckleSystem.Buckle.cs b/Content.Server/Buckle/Systems/BuckleSystem.Buckle.cs new file mode 100644 index 0000000000..fa24a30562 --- /dev/null +++ b/Content.Server/Buckle/Systems/BuckleSystem.Buckle.cs @@ -0,0 +1,401 @@ +using System.Diagnostics.CodeAnalysis; +using Content.Server.Buckle.Components; +using Content.Server.Storage.Components; +using Content.Shared.Alert; +using Content.Shared.Bed.Sleep; +using Content.Shared.Buckle.Components; +using Content.Shared.DragDrop; +using Content.Shared.Hands.Components; +using Content.Shared.IdentityManagement; +using Content.Shared.Interaction; +using Content.Shared.MobState.Components; +using Content.Shared.Pulling.Components; +using Content.Shared.Stunnable; +using Content.Shared.Vehicle.Components; +using Content.Shared.Verbs; +using Robust.Shared.GameStates; +using Robust.Shared.Player; + +namespace Content.Server.Buckle.Systems; + +public sealed partial class BuckleSystem +{ + private void InitializeBuckle() + { + SubscribeLocalEvent(OnBuckleStartup); + SubscribeLocalEvent(OnBuckleShutdown); + SubscribeLocalEvent(OnBuckleGetState); + SubscribeLocalEvent(MoveEvent); + SubscribeLocalEvent(HandleInteractHand); + SubscribeLocalEvent>(AddUnbuckleVerb); + SubscribeLocalEvent(OnEntityStorageInsertAttempt); + SubscribeLocalEvent(OnBuckleCanDrop); + SubscribeLocalEvent(OnBuckleDragDrop); + } + + private void AddUnbuckleVerb(EntityUid uid, BuckleComponent component, GetVerbsEvent args) + { + if (!args.CanAccess || !args.CanInteract || !component.Buckled) + return; + + InteractionVerb verb = new() + { + Act = () => TryUnbuckle(uid, args.User, buckle: component), + Text = Loc.GetString("verb-categories-unbuckle"), + IconTexture = "/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); + } + + private void OnBuckleStartup(EntityUid uid, BuckleComponent component, ComponentStartup args) + { + UpdateBuckleStatus(uid, component); + } + + private void OnBuckleShutdown(EntityUid uid, BuckleComponent component, ComponentShutdown args) + { + TryUnbuckle(uid, uid, true, component); + + component.BuckleTime = default; + } + + private void OnBuckleGetState(EntityUid uid, BuckleComponent component, ref ComponentGetState args) + { + args.State = new BuckleComponentState(component.Buckled, component.LastEntityBuckledTo, component.DontCollide); + } + + private void HandleInteractHand(EntityUid uid, BuckleComponent component, InteractHandEvent args) + { + args.Handled = TryUnbuckle(uid, args.User, buckle: component); + } + + private void MoveEvent(EntityUid uid, BuckleComponent buckle, ref MoveEvent ev) + { + var strap = buckle.BuckledTo; + + if (strap == null) + { + return; + } + + var strapPosition = Transform(strap.Owner).Coordinates; + + if (ev.NewPosition.InRange(EntityManager, strapPosition, strap.MaxBuckleDistance)) + { + return; + } + + TryUnbuckle(uid, buckle.Owner, true, buckle); + } + + private void OnEntityStorageInsertAttempt(EntityUid uid, BuckleComponent comp, InsertIntoEntityStorageAttemptEvent args) + { + if (comp.Buckled) + args.Cancel(); + } + + private void OnBuckleCanDrop(EntityUid uid, BuckleComponent component, CanDropEvent args) + { + args.Handled = HasComp(args.Target); + } + + private void OnBuckleDragDrop(EntityUid uid, BuckleComponent component, DragDropEvent args) + { + args.Handled = TryBuckle(uid, args.User, args.Target, component); + } + + /// + /// Shows or hides the buckled status effect depending on if the + /// entity is buckled or not. + /// + private void UpdateBuckleStatus(EntityUid uid, BuckleComponent component) + { + if (component.Buckled) + { + var alertType = component.BuckledTo?.BuckledAlertType ?? AlertType.Buckled; + _alerts.ShowAlert(uid, alertType); + } + else + { + _alerts.ClearAlertCategory(uid, AlertCategory.Buckled); + } + } + + private void SetBuckledTo(BuckleComponent buckle, StrapComponent? strap) + { + buckle.BuckledTo = strap; + buckle.LastEntityBuckledTo = strap?.Owner; + + if (strap == null) + { + buckle.Buckled = false; + } + else + { + buckle.DontCollide = true; + buckle.Buckled = true; + buckle.BuckleTime = _gameTiming.CurTime; + } + + _actionBlocker.UpdateCanMove(buckle.Owner); + UpdateBuckleStatus(buckle.Owner, buckle); + Dirty(buckle); + } + + private bool CanBuckle( + EntityUid buckleId, + EntityUid user, + EntityUid to, + [NotNullWhen(true)] out StrapComponent? strap, + BuckleComponent? buckle = null) + { + strap = null; + + if (user == to || + !Resolve(buckleId, ref buckle, false) || + !Resolve(to, ref strap, false)) + { + return false; + } + + var strapUid = strap.Owner; + bool Ignored(EntityUid entity) => entity == buckleId || entity == user || entity == strapUid; + + if (!_interactions.InRangeUnobstructed(buckleId, strapUid, buckle.Range, predicate: Ignored, popup: true)) + { + return false; + } + + // If in a container + if (_containers.TryGetContainingContainer(buckleId, out var ownerContainer)) + { + // And not in the same container as the strap + if (!_containers.TryGetContainingContainer(strap.Owner, out var strapContainer) || + ownerContainer != strapContainer) + { + return false; + } + } + + if (!HasComp(user)) + { + _popups.PopupEntity(Loc.GetString("buckle-component-no-hands-message"), user, Filter.Entities(user)); + return false; + } + + if (buckle.Buckled) + { + var message = Loc.GetString(buckleId == user + ? "buckle-component-already-buckled-message" + : "buckle-component-other-already-buckled-message", + ("owner", Identity.Entity(buckleId, EntityManager))); + _popups.PopupEntity(message, user, Filter.Entities(user)); + + return false; + } + + var parent = Transform(to).ParentUid; + while (parent.IsValid()) + { + if (parent == user) + { + var message = Loc.GetString(buckleId == user + ? "buckle-component-cannot-buckle-message" + : "buckle-component-other-cannot-buckle-message", ("owner", Identity.Entity(buckleId, EntityManager))); + _popups.PopupEntity(message, user, Filter.Entities(user)); + + return false; + } + + parent = Transform(parent).ParentUid; + } + + if (!StrapHasSpace(to, buckle, strap)) + { + var message = Loc.GetString(buckleId == user + ? "buckle-component-cannot-fit-message" + : "buckle-component-other-cannot-fit-message", ("owner", Identity.Entity(buckleId, EntityManager))); + _popups.PopupEntity(message, user, Filter.Entities(user)); + + return false; + } + + return true; + } + + public bool TryBuckle(EntityUid buckleId, EntityUid user, EntityUid to, BuckleComponent? buckle = null) + { + if (!Resolve(buckleId, ref buckle, false)) + return false; + + if (!CanBuckle(buckleId, user, to, out var strap, buckle)) + return false; + + _audio.Play(strap.BuckleSound, Filter.Pvs(buckleId), buckleId); + + if (!StrapTryAdd(to, buckle, strap: strap)) + { + var message = Loc.GetString(buckleId == user + ? "buckle-component-cannot-buckle-message" + : "buckle-component-other-cannot-buckle-message", ("owner", Identity.Entity(buckleId, EntityManager))); + _popups.PopupEntity(message, user, Filter.Entities(user)); + return false; + } + + if (TryComp(buckleId, out var appearance)) + _appearance.SetData(buckleId, BuckleVisuals.Buckled, true, appearance); + + ReAttach(buckleId, strap, buckle); + SetBuckledTo(buckle, strap); + + var ev = new BuckleChangeEvent { Buckling = true, Strap = strap.Owner, BuckledEntity = buckleId }; + RaiseLocalEvent(ev.BuckledEntity, ev); + RaiseLocalEvent(ev.Strap, ev); + + if (TryComp(buckleId, out SharedPullableComponent? ownerPullable)) + { + if (ownerPullable.Puller != null) + { + _pulling.TryStopPull(ownerPullable); + } + } + + if (TryComp(to, out SharedPullableComponent? toPullable)) + { + if (toPullable.Puller == buckleId) + { + // can't pull it and buckle to it at the same time + _pulling.TryStopPull(toPullable); + } + } + + return true; + } + + /// + /// Tries to unbuckle the Owner of this component from its current strap. + /// + /// The entity to unbuckle. + /// The entity doing the unbuckling. + /// + /// Whether to force the unbuckling or not. Does not guarantee true to + /// be returned, but guarantees the owner to be unbuckled afterwards. + /// + /// The buckle component of the entity to unbuckle. + /// + /// true if the owner was unbuckled, otherwise false even if the owner + /// was previously already unbuckled. + /// + public bool TryUnbuckle(EntityUid buckleId, EntityUid user, bool force = false, BuckleComponent? buckle = null) + { + if (!Resolve(buckleId, ref buckle, false) || + buckle.BuckledTo is not { } oldBuckledTo) + { + return false; + } + + if (!force) + { + if (_gameTiming.CurTime < buckle.BuckleTime + buckle.UnbuckleDelay) + return false; + + if (!_interactions.InRangeUnobstructed(user, oldBuckledTo.Owner, buckle.Range, popup: true)) + return false; + + if (HasComp(buckleId) && buckleId == user) + return false; + + // If the strap is a vehicle and the rider is not the person unbuckling, return. + if (TryComp(oldBuckledTo.Owner, out VehicleComponent? vehicle) && + vehicle.Rider != user) + return false; + } + + SetBuckledTo(buckle, null); + + var xform = Transform(buckleId); + var oldBuckledXform = Transform(oldBuckledTo.Owner); + + if (xform.ParentUid == oldBuckledXform.Owner) + { + _containers.AttachParentToContainerOrGrid(xform); + xform.WorldRotation = oldBuckledXform.WorldRotation; + + if (oldBuckledTo.UnbuckleOffset != Vector2.Zero) + xform.Coordinates = oldBuckledXform.Coordinates.Offset(oldBuckledTo.UnbuckleOffset); + } + + if (TryComp(buckleId, out AppearanceComponent? appearance)) + _appearance.SetData(buckleId, BuckleVisuals.Buckled, false, appearance); + + if ((TryComp(buckleId, out var mobState) && _mobState.IsIncapacitated(buckleId, mobState)) || + HasComp(buckleId)) + { + _standing.Down(buckleId); + } + else + { + _standing.Stand(buckleId); + } + + _mobState.EnterState(mobState, mobState?.CurrentState); + + // Sync StrapComponent data + _appearance.SetData(oldBuckledTo.Owner, StrapVisuals.State, false); + if (oldBuckledTo.BuckledEntities.Remove(buckleId)) + { + oldBuckledTo.OccupiedSize -= buckle.Size; + Dirty(oldBuckledTo); + } + + _audio.Play(oldBuckledTo.UnbuckleSound, Filter.Pvs(buckleId), buckleId); + + var ev = new BuckleChangeEvent { Buckling = false, Strap = oldBuckledTo.Owner, BuckledEntity = buckleId }; + RaiseLocalEvent(buckleId, ev); + RaiseLocalEvent(oldBuckledTo.Owner, ev); + + return true; + } + + /// + /// Makes an entity toggle the buckling status of the owner to a + /// specific entity. + /// + /// The entity to buckle/unbuckle from . + /// The entity doing the buckling/unbuckling. + /// + /// The entity to toggle the buckle status of the owner to. + /// + /// + /// Whether to force the unbuckling or not, if it happens. Does not + /// guarantee true to be returned, but guarantees the owner to be + /// unbuckled afterwards. + /// + /// The buckle component of the entity to buckle/unbuckle from . + /// true if the buckling status was changed, false otherwise. + public bool ToggleBuckle( + EntityUid buckleId, + EntityUid user, + EntityUid to, + bool force = false, + BuckleComponent? buckle = null) + { + if (!Resolve(buckleId, ref buckle, false)) + return false; + + if (buckle.BuckledTo?.Owner == to) + { + return TryUnbuckle(buckleId, user, force, buckle); + } + + return TryBuckle(buckleId, user, to, buckle); + } +} diff --git a/Content.Server/Buckle/Systems/BuckleSystem.Strap.cs b/Content.Server/Buckle/Systems/BuckleSystem.Strap.cs new file mode 100644 index 0000000000..e2e55d5ba7 --- /dev/null +++ b/Content.Server/Buckle/Systems/BuckleSystem.Strap.cs @@ -0,0 +1,242 @@ +using System.Linq; +using Content.Server.Buckle.Components; +using Content.Server.Construction.Completions; +using Content.Shared.Buckle.Components; +using Content.Shared.Destructible; +using Content.Shared.DragDrop; +using Content.Shared.Interaction; +using Content.Shared.Storage; +using Content.Shared.Verbs; +using Robust.Server.GameObjects; +using Robust.Shared.Containers; +using Robust.Shared.GameStates; + +namespace Content.Server.Buckle.Systems; + +public sealed partial class BuckleSystem +{ + private void InitializeStrap() + { + SubscribeLocalEvent(OnStrapShutdown); + SubscribeLocalEvent((_, c, _) => StrapRemoveAll(c)); + SubscribeLocalEvent(OnStrapGetState); + SubscribeLocalEvent(ContainerModifiedStrap); + SubscribeLocalEvent(ContainerModifiedStrap); + SubscribeLocalEvent>(AddStrapVerbs); + SubscribeLocalEvent(OnStrapInsertAttempt); + SubscribeLocalEvent(OnStrapInteractHand); + SubscribeLocalEvent((_,c,_) => StrapRemoveAll(c)); + SubscribeLocalEvent((_, c, _) => StrapRemoveAll(c)); + SubscribeLocalEvent((_, c, _) => StrapRemoveAll(c)); + SubscribeLocalEvent(OnStrapDragDrop); + } + + private void OnStrapGetState(EntityUid uid, StrapComponent component, ref ComponentGetState args) + { + args.State = new StrapComponentState(component.Position, component.BuckleOffset, component.BuckledEntities, component.MaxBuckleDistance); + } + + private void ContainerModifiedStrap(EntityUid uid, StrapComponent strap, ContainerModifiedMessage message) + { + if (GameTiming.ApplyingState) + return; + + foreach (var buckledEntity in strap.BuckledEntities) + { + if (!TryComp(buckledEntity, out BuckleComponent? buckled)) + { + continue; + } + + ContainerModifiedReAttach(buckledEntity, strap.Owner, buckled, strap); + } + } + + private void ContainerModifiedReAttach(EntityUid buckleId, EntityUid strapId, BuckleComponent? buckle = null, StrapComponent? strap = null) + { + if (!Resolve(buckleId, ref buckle, false) || + !Resolve(strapId, ref strap, false)) + { + return; + } + + var contained = _containers.TryGetContainingContainer(buckleId, out var ownContainer); + var strapContained = _containers.TryGetContainingContainer(strapId, out var strapContainer); + + if (contained != strapContained || ownContainer != strapContainer) + { + TryUnbuckle(buckleId, buckle.Owner, true, buckle); + return; + } + + if (!contained) + { + ReAttach(buckleId, strap, buckle); + } + } + + private void OnStrapShutdown(EntityUid uid, StrapComponent component, ComponentShutdown args) + { + if (LifeStage(uid) > EntityLifeStage.MapInitialized) + return; + + StrapRemoveAll(component); + } + + private void OnStrapInsertAttempt(EntityUid uid, StrapComponent component, ContainerGettingInsertedAttemptEvent args) + { + // If someone is attempting to put this item inside of a backpack, ensure that it has no entities strapped to it. + if (HasComp(args.Container.Owner) && component.BuckledEntities.Count != 0) + args.Cancel(); + } + + private void OnStrapInteractHand(EntityUid uid, StrapComponent component, InteractHandEvent args) + { + if (args.Handled) + return; + + ToggleBuckle(args.User, args.User, uid); + } + + private void AddStrapVerbs(EntityUid uid, StrapComponent strap, GetVerbsEvent args) + { + if (args.Hands == null || !args.CanAccess || !args.CanInteract || !strap.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 strap.BuckledEntities) + { + var buckledComp = Comp(entity); + + if (!_interactions.InRangeUnobstructed(args.User, args.Target, range: buckledComp.Range)) + continue; + + InteractionVerb verb = new() + { + Act = () => TryUnbuckle(entity, args.User, buckle: buckledComp), + Category = VerbCategory.Unbuckle + }; + + if (entity == args.User) + verb.Text = Loc.GetString("verb-self-target-pronoun"); + else + verb.Text = Comp(entity).EntityName; + + // In the event that you have more than once entity with the same name strapped to the same object, + // these two verbs will be identical according to Verb.CompareTo, and only one with actually be added to + // the verb list. However this should rarely ever be a problem. If it ever is, it could be fixed by + // appending an integer to verb.Text to distinguish the verbs. + + args.Verbs.Add(verb); + } + + // Add a verb to buckle the user. + if (TryComp(args.User, out BuckleComponent? buckle) && + buckle.BuckledTo != strap && + args.User != strap.Owner && + StrapHasSpace(uid, buckle, strap) && + _interactions.InRangeUnobstructed(args.User, args.Target, range: buckle.Range)) + { + InteractionVerb verb = new() + { + Act = () => TryBuckle(args.User, args.User, args.Target, buckle), + Category = VerbCategory.Buckle, + Text = Loc.GetString("verb-self-target-pronoun") + }; + args.Verbs.Add(verb); + } + + // If the user is currently holding/pulling an entity that can be buckled, add a verb for that. + if (args.Using is {Valid: true} @using && + TryComp(@using, out BuckleComponent? usingBuckle) && + StrapHasSpace(uid, usingBuckle, strap) && + _interactions.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 (!_interactions.InRangeUnobstructed(@using, args.Target, usingBuckle.Range, predicate: Ignored)) + return; + + InteractionVerb verb = new() + { + Act = () => TryBuckle(@using, args.User, args.Target, usingBuckle), + Category = VerbCategory.Buckle, + Text = Comp(@using).EntityName, + // just a held object, the user is probably just trying to sit down. + // If the used entity is a person being pulled, prioritize this verb. Conversely, if it is + Priority = HasComp(@using) ? 1 : -1 + }; + + args.Verbs.Add(verb); + } + } + + private void StrapRemoveAll(StrapComponent strap) + { + foreach (var entity in strap.BuckledEntities.ToArray()) + { + TryUnbuckle(entity, entity, true); + } + + strap.BuckledEntities.Clear(); + strap.OccupiedSize = 0; + Dirty(strap); + } + + private void OnStrapDragDrop(EntityUid uid, StrapComponent component, DragDropEvent args) + { + if (!StrapCanDragDropOn(uid, args.User, args.Target, args.Dragged, component)) + return; + + args.Handled = TryBuckle(args.Dragged, args.User, uid); + } + + private bool StrapHasSpace(EntityUid strapId, BuckleComponent buckle, StrapComponent? strap = null) + { + if (!Resolve(strapId, ref strap, false)) + return false; + + return strap.OccupiedSize + buckle.Size <= strap.Size; + } + + private bool StrapTryAdd(EntityUid strapId, BuckleComponent buckle, bool force = false, StrapComponent? strap = null) + { + if (!Resolve(strapId, ref strap, false) || + !strap.Enabled) + { + return false; + } + + if (!force && !StrapHasSpace(strapId, buckle, strap)) + return false; + + if (!strap.BuckledEntities.Add(buckle.Owner)) + return false; + + strap.OccupiedSize += buckle.Size; + + _appearance.SetData(buckle.Owner, StrapVisuals.RotationAngle, strap.Rotation); + + _appearance.SetData(strap.Owner, StrapVisuals.State, true); + + Dirty(strap); + return true; + } + + public void StrapSetEnabled(EntityUid strapId, bool enabled, StrapComponent? strap = null) + { + if (!Resolve(strapId, ref strap, false) || + strap.Enabled == enabled) + { + return; + } + + strap.Enabled = enabled; + + if (!enabled) + StrapRemoveAll(strap); + } +} diff --git a/Content.Server/Buckle/Systems/BuckleSystem.cs b/Content.Server/Buckle/Systems/BuckleSystem.cs index fee3ac2df0..e7b8ac5699 100644 --- a/Content.Server/Buckle/Systems/BuckleSystem.cs +++ b/Content.Server/Buckle/Systems/BuckleSystem.cs @@ -1,36 +1,19 @@ -using System.Diagnostics.CodeAnalysis; -using Content.Server.Buckle.Components; using Content.Server.Interaction; using Content.Server.Popups; using Content.Server.Pulling; -using Content.Server.Storage.Components; using Content.Shared.ActionBlocker; using Content.Shared.Alert; -using Content.Shared.Bed.Sleep; using Content.Shared.Buckle; -using Content.Shared.Buckle.Components; -using Content.Shared.DragDrop; -using Content.Shared.Hands.Components; -using Content.Shared.IdentityManagement; -using Content.Shared.Interaction; -using Content.Shared.MobState.Components; using Content.Shared.MobState.EntitySystems; -using Content.Shared.Pulling.Components; -using Content.Shared.Stunnable; -using Content.Shared.Vehicle.Components; -using Content.Shared.Verbs; using JetBrains.Annotations; using Robust.Server.Containers; using Robust.Server.GameObjects; -using Robust.Shared.Containers; -using Robust.Shared.GameStates; -using Robust.Shared.Player; using Robust.Shared.Timing; namespace Content.Server.Buckle.Systems; [UsedImplicitly] -public sealed class BuckleSystem : SharedBuckleSystem +public sealed partial class BuckleSystem : SharedBuckleSystem { [Dependency] private readonly IGameTiming _gameTiming = default!; @@ -52,422 +35,7 @@ public sealed class BuckleSystem : SharedBuckleSystem UpdatesAfter.Add(typeof(InteractionSystem)); UpdatesAfter.Add(typeof(InputSystem)); - SubscribeLocalEvent(OnStrapGetState); - SubscribeLocalEvent(ContainerModifiedStrap); - SubscribeLocalEvent(ContainerModifiedStrap); - - SubscribeLocalEvent(OnBuckleStartup); - SubscribeLocalEvent(OnBuckleShutdown); - SubscribeLocalEvent(OnBuckleGetState); - SubscribeLocalEvent(MoveEvent); - SubscribeLocalEvent(HandleInteractHand); - SubscribeLocalEvent>(AddUnbuckleVerb); - SubscribeLocalEvent(OnEntityStorageInsertAttempt); - SubscribeLocalEvent(OnBuckleCanDrop); - SubscribeLocalEvent(OnBuckleDragDrop); - } - - private void OnStrapGetState(EntityUid uid, StrapComponent component, ref ComponentGetState args) - { - args.State = new StrapComponentState(component.Position, component.BuckleOffset, component.BuckledEntities, component.MaxBuckleDistance); - } - - private void AddUnbuckleVerb(EntityUid uid, BuckleComponent component, GetVerbsEvent args) - { - if (!args.CanAccess || !args.CanInteract || !component.Buckled) - return; - - InteractionVerb verb = new() - { - Act = () => TryUnbuckle(uid, args.User, buckle: component), - Text = Loc.GetString("verb-categories-unbuckle"), - IconTexture = "/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); - } - - private void OnBuckleStartup(EntityUid uid, BuckleComponent component, ComponentStartup args) - { - UpdateBuckleStatus(uid, component); - } - - private void OnBuckleShutdown(EntityUid uid, BuckleComponent component, ComponentShutdown args) - { - component.BuckledTo?.Remove(component); - TryUnbuckle(uid, uid, true, component); - - component.BuckleTime = default; - } - - private void OnBuckleGetState(EntityUid uid, BuckleComponent component, ref ComponentGetState args) - { - args.State = new BuckleComponentState(component.Buckled, component.LastEntityBuckledTo, component.DontCollide); - } - - private void HandleInteractHand(EntityUid uid, BuckleComponent component, InteractHandEvent args) - { - args.Handled = TryUnbuckle(uid, args.User, buckle: component); - } - - private void MoveEvent(EntityUid uid, BuckleComponent buckle, ref MoveEvent ev) - { - var strap = buckle.BuckledTo; - - if (strap == null) - { - return; - } - - var strapPosition = Transform(strap.Owner).Coordinates; - - if (ev.NewPosition.InRange(EntityManager, strapPosition, strap.MaxBuckleDistance)) - { - return; - } - - TryUnbuckle(uid, buckle.Owner, true, buckle); - } - - private void ContainerModifiedStrap(EntityUid uid, StrapComponent strap, ContainerModifiedMessage message) - { - if (GameTiming.ApplyingState) - return; - - foreach (var buckledEntity in strap.BuckledEntities) - { - if (!TryComp(buckledEntity, out BuckleComponent? buckled)) - { - continue; - } - - ContainerModifiedReAttach(buckledEntity, strap.Owner, buckled, strap); - } - } - - private void ContainerModifiedReAttach(EntityUid buckleId, EntityUid strapId, BuckleComponent? buckle = null, StrapComponent? strap = null) - { - if (!Resolve(buckleId, ref buckle, false) || - !Resolve(strapId, ref strap, false)) - { - return; - } - - var contained = _containers.TryGetContainingContainer(buckleId, out var ownContainer); - var strapContained = _containers.TryGetContainingContainer(strapId, out var strapContainer); - - if (contained != strapContained || ownContainer != strapContainer) - { - TryUnbuckle(buckleId, buckle.Owner, true, buckle); - return; - } - - if (!contained) - { - ReAttach(buckleId, strap, buckle); - } - } - - public void OnEntityStorageInsertAttempt(EntityUid uid, BuckleComponent comp, InsertIntoEntityStorageAttemptEvent args) - { - if (comp.Buckled) - args.Cancel(); - } - - private void OnBuckleCanDrop(EntityUid uid, BuckleComponent component, CanDropEvent args) - { - args.Handled = HasComp(args.Target); - } - - private void OnBuckleDragDrop(EntityUid uid, BuckleComponent component, DragDropEvent args) - { - args.Handled = TryBuckle(uid, args.User, args.Target, component); - } - - /// - /// Shows or hides the buckled status effect depending on if the - /// entity is buckled or not. - /// - private void UpdateBuckleStatus(EntityUid uid, BuckleComponent component) - { - if (component.Buckled) - { - var alertType = component.BuckledTo?.BuckledAlertType ?? AlertType.Buckled; - _alerts.ShowAlert(uid, alertType); - } - else - { - _alerts.ClearAlertCategory(uid, AlertCategory.Buckled); - } - } - - private void SetBuckledTo(BuckleComponent buckle, StrapComponent? strap) - { - buckle.BuckledTo = strap; - buckle.LastEntityBuckledTo = strap?.Owner; - - if (strap == null) - { - buckle.Buckled = false; - } - else - { - buckle.DontCollide = true; - buckle.Buckled = true; - buckle.BuckleTime = _gameTiming.CurTime; - } - - _actionBlocker.UpdateCanMove(buckle.Owner); - UpdateBuckleStatus(buckle.Owner, buckle); - Dirty(buckle); - } - - public bool CanBuckle( - EntityUid buckleId, - EntityUid user, - EntityUid to, - [NotNullWhen(true)] out StrapComponent? strap, - BuckleComponent? buckle = null) - { - strap = null; - - if (user == to || - !Resolve(buckleId, ref buckle, false) || - !Resolve(to, ref strap, false)) - { - return false; - } - - var strapUid = strap.Owner; - bool Ignored(EntityUid entity) => entity == buckleId || entity == user || entity == strapUid; - - if (!_interactions.InRangeUnobstructed(buckleId, strapUid, buckle.Range, predicate: Ignored, popup: true)) - { - return false; - } - - // If in a container - if (_containers.TryGetContainingContainer(buckleId, out var ownerContainer)) - { - // And not in the same container as the strap - if (!_containers.TryGetContainingContainer(strap.Owner, out var strapContainer) || - ownerContainer != strapContainer) - { - return false; - } - } - - if (!HasComp(user)) - { - _popups.PopupEntity(Loc.GetString("buckle-component-no-hands-message"), user, Filter.Entities(user)); - return false; - } - - if (buckle.Buckled) - { - var message = Loc.GetString(buckleId == user - ? "buckle-component-already-buckled-message" - : "buckle-component-other-already-buckled-message", - ("owner", Identity.Entity(buckleId, EntityManager))); - _popups.PopupEntity(message, user, Filter.Entities(user)); - - return false; - } - - var parent = Transform(to).ParentUid; - while (parent.IsValid()) - { - if (parent == user) - { - var message = Loc.GetString(buckleId == user - ? "buckle-component-cannot-buckle-message" - : "buckle-component-other-cannot-buckle-message", ("owner", Identity.Entity(buckleId, EntityManager))); - _popups.PopupEntity(message, user, Filter.Entities(user)); - - return false; - } - - parent = Transform(parent).ParentUid; - } - - if (!strap.HasSpace(buckle)) - { - var message = Loc.GetString(buckleId == user - ? "buckle-component-cannot-fit-message" - : "buckle-component-other-cannot-fit-message", ("owner", Identity.Entity(buckleId, EntityManager))); - _popups.PopupEntity(message, user, Filter.Entities(user)); - - return false; - } - - return true; - } - - public bool TryBuckle(EntityUid buckleId, EntityUid user, EntityUid to, BuckleComponent? buckle = null) - { - if (!Resolve(buckleId, ref buckle, false)) - return false; - - if (!CanBuckle(buckleId, user, to, out var strap, buckle)) - return false; - - _audio.Play(strap.BuckleSound, Filter.Pvs(buckleId), buckleId); - - if (!strap.TryAdd(buckle)) - { - var message = Loc.GetString(buckleId == user - ? "buckle-component-cannot-buckle-message" - : "buckle-component-other-cannot-buckle-message", ("owner", Identity.Entity(buckleId, EntityManager))); - _popups.PopupEntity(message, user, Filter.Entities(user)); - return false; - } - - if (TryComp(buckleId, out var appearance)) - _appearance.SetData(buckleId, BuckleVisuals.Buckled, true, appearance); - - ReAttach(buckleId, strap, buckle); - SetBuckledTo(buckle, strap); - - var ev = new BuckleChangeEvent { Buckling = true, Strap = strap.Owner, BuckledEntity = buckleId }; - RaiseLocalEvent(ev.BuckledEntity, ev); - RaiseLocalEvent(ev.Strap, ev); - - if (TryComp(buckleId, out SharedPullableComponent? ownerPullable)) - { - if (ownerPullable.Puller != null) - { - _pulling.TryStopPull(ownerPullable); - } - } - - if (TryComp(to, out SharedPullableComponent? toPullable)) - { - if (toPullable.Puller == buckleId) - { - // can't pull it and buckle to it at the same time - _pulling.TryStopPull(toPullable); - } - } - - return true; - } - - /// - /// Tries to unbuckle the Owner of this component from its current strap. - /// - /// The entity to unbuckle. - /// The entity doing the unbuckling. - /// - /// Whether to force the unbuckling or not. Does not guarantee true to - /// be returned, but guarantees the owner to be unbuckled afterwards. - /// - /// The buckle component of the entity to unbuckle. - /// - /// true if the owner was unbuckled, otherwise false even if the owner - /// was previously already unbuckled. - /// - public bool TryUnbuckle(EntityUid buckleId, EntityUid user, bool force = false, BuckleComponent? buckle = null) - { - if (!Resolve(buckleId, ref buckle, false) || - buckle.BuckledTo is not { } oldBuckledTo) - { - return false; - } - - if (!force) - { - if (_gameTiming.CurTime < buckle.BuckleTime + buckle.UnbuckleDelay) - return false; - - if (!_interactions.InRangeUnobstructed(user, oldBuckledTo.Owner, buckle.Range, popup: true)) - return false; - - if (HasComp(buckleId) && buckleId == user) - return false; - - // If the strap is a vehicle and the rider is not the person unbuckling, return. - if (TryComp(oldBuckledTo.Owner, out VehicleComponent? vehicle) && - vehicle.Rider != user) - return false; - } - - SetBuckledTo(buckle, null); - - var xform = Transform(buckleId); - var oldBuckledXform = Transform(oldBuckledTo.Owner); - - if (xform.ParentUid == oldBuckledXform.Owner) - { - _containers.AttachParentToContainerOrGrid(xform); - xform.WorldRotation = oldBuckledXform.WorldRotation; - - if (oldBuckledTo.UnbuckleOffset != Vector2.Zero) - xform.Coordinates = oldBuckledXform.Coordinates.Offset(oldBuckledTo.UnbuckleOffset); - } - - if (TryComp(buckleId, out AppearanceComponent? appearance)) - _appearance.SetData(buckleId, BuckleVisuals.Buckled, false, appearance); - - if (HasComp(buckleId) - | (TryComp(buckleId, out var mobState) && _mobState.IsIncapacitated(buckleId, mobState))) - { - _standing.Down(buckleId); - } - else - { - _standing.Stand(buckleId); - } - - _mobState.EnterState(mobState, mobState?.CurrentState); - - oldBuckledTo.Remove(buckle); - _audio.Play(oldBuckledTo.UnbuckleSound, Filter.Pvs(buckleId), buckleId); - - var ev = new BuckleChangeEvent { Buckling = false, Strap = oldBuckledTo.Owner, BuckledEntity = buckleId }; - RaiseLocalEvent(buckleId, ev); - RaiseLocalEvent(oldBuckledTo.Owner, ev); - - return true; - } - - /// - /// Makes an entity toggle the buckling status of the owner to a - /// specific entity. - /// - /// The entity to buckle/unbuckle from . - /// The entity doing the buckling/unbuckling. - /// - /// The entity to toggle the buckle status of the owner to. - /// - /// - /// Whether to force the unbuckling or not, if it happens. Does not - /// guarantee true to be returned, but guarantees the owner to be - /// unbuckled afterwards. - /// - /// The buckle component of the entity to buckle/unbuckle from . - /// true if the buckling status was changed, false otherwise. - public bool ToggleBuckle( - EntityUid buckleId, - EntityUid user, - EntityUid to, - bool force = false, - BuckleComponent? buckle = null) - { - if (!Resolve(buckleId, ref buckle, false)) - return false; - - if (buckle.BuckledTo?.Owner == to) - { - return TryUnbuckle(buckleId, user, force, buckle); - } - - return TryBuckle(buckleId, user, to, buckle); + InitializeBuckle(); + InitializeStrap(); } } diff --git a/Content.Server/Buckle/Systems/StrapSystem.cs b/Content.Server/Buckle/Systems/StrapSystem.cs deleted file mode 100644 index 6850941ff0..0000000000 --- a/Content.Server/Buckle/Systems/StrapSystem.cs +++ /dev/null @@ -1,142 +0,0 @@ -using Content.Server.Buckle.Components; -using Content.Server.Construction.Completions; -using Content.Server.Interaction; -using Content.Shared.Destructible; -using Content.Shared.Interaction; -using Content.Shared.Storage; -using Content.Shared.Verbs; -using JetBrains.Annotations; -using Robust.Server.GameObjects; -using Robust.Shared.Containers; - -namespace Content.Server.Buckle.Systems -{ - [UsedImplicitly] - internal sealed class StrapSystem : EntitySystem - { - [Dependency] private readonly BuckleSystem _buckle = default!; - [Dependency] private readonly InteractionSystem _interactionSystem = default!; - - public override void Initialize() - { - base.Initialize(); - - SubscribeLocalEvent>(AddStrapVerbs); - SubscribeLocalEvent(OnInsertAttempt); - SubscribeLocalEvent(OnInteractHand); - SubscribeLocalEvent((_,c,_) => RemoveAll(c)); - SubscribeLocalEvent((_, c, _) => RemoveAll(c)); - SubscribeLocalEvent((_, c, _) => RemoveAll(c)); - SubscribeLocalEvent(OnShutdown); - } - - private void OnShutdown(EntityUid uid, StrapComponent component, ComponentShutdown args) - { - if (LifeStage(uid) > EntityLifeStage.MapInitialized) - return; - - // Component is being removed, but entity is not shutting down. - component.RemoveAll(); - } - - private void OnInsertAttempt(EntityUid uid, StrapComponent component, ContainerGettingInsertedAttemptEvent args) - { - // If someone is attempting to put this item inside of a backpack, ensure that it has no entities strapped to it. - if (HasComp(args.Container.Owner) && component.BuckledEntities.Count != 0) - args.Cancel(); - } - - private void OnInteractHand(EntityUid uid, StrapComponent component, InteractHandEvent args) - { - if (args.Handled) - return; - - _buckle.ToggleBuckle(args.User, args.User, uid); - } - - // TODO ECS BUCKLE/STRAP These 'Strap' verbs are an incestuous mess of buckle component and strap component - // functions. Whenever these are fully ECSed, maybe do it in a way that allows for these verbs to be handled in - // a sensible manner in a single system? - - private void AddStrapVerbs(EntityUid uid, StrapComponent component, GetVerbsEvent args) - { - if (args.Hands == null || !args.CanAccess || !args.CanInteract || !component.Enabled) - return; - - // Note that for whatever bloody reason, buckle component has its own interaction range. Additionally, this - // range can be set per-component, so we have to check a modified InRangeUnobstructed for every verb. - - // Add unstrap verbs for every strapped entity. - foreach (var entity in component.BuckledEntities) - { - var buckledComp = EntityManager.GetComponent(entity); - - if (!_interactionSystem.InRangeUnobstructed(args.User, args.Target, range: buckledComp.Range)) - continue; - - InteractionVerb verb = new() - { - Act = () => _buckle.TryUnbuckle(entity, args.User, buckle: buckledComp), - Category = VerbCategory.Unbuckle - }; - - if (entity == args.User) - verb.Text = Loc.GetString("verb-self-target-pronoun"); - else - verb.Text = EntityManager.GetComponent(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 (EntityManager.TryGetComponent(args.User, out var buckle) && - buckle.BuckledTo != component && - args.User != component.Owner && - component.HasSpace(buckle) && - _interactionSystem.InRangeUnobstructed(args.User, args.Target, range: buckle.Range)) - { - InteractionVerb verb = new() - { - Act = () => _buckle.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 && - EntityManager.TryGetComponent(@using, out var usingBuckle) && - component.HasSpace(usingBuckle) && - _interactionSystem.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 (!_interactionSystem.InRangeUnobstructed(@using, args.Target, usingBuckle.Range, predicate: Ignored)) - return; - - InteractionVerb verb = new() - { - Act = () => _buckle.TryBuckle(@using, args.User, args.Target, usingBuckle), - Category = VerbCategory.Buckle, - Text = EntityManager.GetComponent(@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 = EntityManager.HasComponent(@using) ? 1 : -1 - }; - - args.Verbs.Add(verb); - } - } - - public void RemoveAll(StrapComponent component) - { - component.RemoveAll(); - } - } -} diff --git a/Content.Server/Foldable/FoldableSystem.cs b/Content.Server/Foldable/FoldableSystem.cs index d24ffb7f6a..7b784bcf93 100644 --- a/Content.Server/Foldable/FoldableSystem.cs +++ b/Content.Server/Foldable/FoldableSystem.cs @@ -1,5 +1,6 @@ using System.Linq; using Content.Server.Buckle.Components; +using Content.Server.Buckle.Systems; using Content.Server.Storage.Components; using Content.Shared.Foldable; using Content.Shared.Verbs; @@ -11,6 +12,7 @@ namespace Content.Server.Foldable [UsedImplicitly] public sealed class FoldableSystem : SharedFoldableSystem { + [Dependency] private BuckleSystem _buckle = default!; [Dependency] private SharedContainerSystem _container = default!; public override void Initialize() @@ -84,8 +86,7 @@ namespace Content.Server.Foldable base.SetFolded(component, folded); // You can't buckle an entity to a folded object - if (TryComp(component.Owner, out StrapComponent? strap)) - strap.Enabled = !component.IsFolded; + _buckle.StrapSetEnabled(component.Owner, !component.IsFolded); } public void OnStoreThisAttempt(EntityUid uid, FoldableComponent comp, StoreMobInItemContainerAttemptEvent args) diff --git a/Content.Server/Toilet/ToiletSystem.cs b/Content.Server/Toilet/ToiletSystem.cs index ec6291d883..12456c14df 100644 --- a/Content.Server/Toilet/ToiletSystem.cs +++ b/Content.Server/Toilet/ToiletSystem.cs @@ -5,7 +5,6 @@ using Content.Server.Popups; using Content.Server.Storage.Components; using Content.Server.Storage.EntitySystems; using Content.Server.Tools; -using Content.Shared.Audio; using Content.Shared.Body.Components; using Content.Shared.Body.Part; using Content.Shared.Examine; @@ -37,7 +36,7 @@ namespace Content.Server.Toilet SubscribeLocalEvent(OnInit); SubscribeLocalEvent(OnMapInit); SubscribeLocalEvent(OnInteractUsing); - SubscribeLocalEvent(OnInteractHand, new []{typeof(StrapSystem)}); + SubscribeLocalEvent(OnInteractHand, new []{typeof(BuckleSystem)}); SubscribeLocalEvent(OnExamine); SubscribeLocalEvent(OnSuicide); SubscribeLocalEvent(OnToiletPried); diff --git a/Content.Shared/Buckle/Components/SharedStrapComponent.cs b/Content.Shared/Buckle/Components/SharedStrapComponent.cs index 5f9c5574be..b769929027 100644 --- a/Content.Shared/Buckle/Components/SharedStrapComponent.cs +++ b/Content.Shared/Buckle/Components/SharedStrapComponent.cs @@ -1,102 +1,89 @@ -using Content.Shared.DragDrop; -using Content.Shared.Interaction; using Robust.Shared.GameStates; using Robust.Shared.Serialization; -namespace Content.Shared.Buckle.Components +namespace Content.Shared.Buckle.Components; + +public enum StrapPosition { - public enum StrapPosition + /// + /// (Default) Makes no change to the buckled mob + /// + None = 0, + + /// + /// Makes the mob stand up + /// + Stand, + + /// + /// Makes the mob lie down + /// + Down +} + +[NetworkedComponent] +[Access(typeof(SharedBuckleSystem))] +public abstract class SharedStrapComponent : Component +{ + /// + /// The change in position to the strapped mob + /// + [DataField("position")] + public StrapPosition Position { get; set; } = StrapPosition.None; + + /// + /// The entity that is currently buckled here + /// + public readonly HashSet BuckledEntities = new(); + + /// + /// The distance above which a buckled entity will be automatically unbuckled. + /// Don't change it unless you really have to + /// + [DataField("maxBuckleDistance", required: false)] + public float MaxBuckleDistance = 0.1f; + + /// + /// Gets and clamps the buckle offset to MaxBuckleDistance + /// + public Vector2 BuckleOffset => Vector2.Clamp( + BuckleOffsetUnclamped, + Vector2.One * -MaxBuckleDistance, + Vector2.One * MaxBuckleDistance); + + /// + /// The buckled entity will be offset by this amount from the center of the strap object. + /// If this offset it too big, it will be clamped to + /// + [DataField("buckleOffset", required: false)] + [Access(Other = AccessPermissions.ReadWrite)] + public Vector2 BuckleOffsetUnclamped = Vector2.Zero; +} + +[Serializable, NetSerializable] +public sealed class StrapComponentState : ComponentState +{ + /// + /// The change in position that this strap makes to the strapped mob + /// + public StrapPosition Position; + + public float MaxBuckleDistance; + public Vector2 BuckleOffsetClamped; + public HashSet BuckledEntities; + + public StrapComponentState(StrapPosition position, Vector2 offset, HashSet buckled, float maxBuckleDistance) { - /// - /// (Default) Makes no change to the buckled mob - /// - None = 0, - - /// - /// Makes the mob stand up - /// - Stand, - - /// - /// Makes the mob lie down - /// - Down - } - - [NetworkedComponent()] - public abstract class SharedStrapComponent : Component, IDragDropOn - { - /// - /// The change in position to the strapped mob - /// - [DataField("position")] - public StrapPosition Position { get; set; } = StrapPosition.None; - - - /// - /// The entity that is currently buckled here, synced from - /// - public readonly HashSet BuckledEntities = new(); - - /// - /// The distance above which a buckled entity will be automatically unbuckled. - /// Don't change it unless you really have to - /// - [DataField("maxBuckleDistance", required: false)] - public float MaxBuckleDistance = 0.1f; - - /// - /// Gets and clamps the buckle offset to MaxBuckleDistance - /// - public Vector2 BuckleOffset => Vector2.Clamp( - BuckleOffsetUnclamped, - Vector2.One * -MaxBuckleDistance, - Vector2.One * MaxBuckleDistance); - - /// - /// The buckled entity will be offset by this amount from the center of the strap object. - /// If this offset it too big, it will be clamped to - /// - [DataField("buckleOffset", required: false)] - public Vector2 BuckleOffsetUnclamped = Vector2.Zero; - - bool IDragDropOn.CanDragDropOn(DragDropEvent eventArgs) - { - if (!IoCManager.Resolve().TryGetComponent(eventArgs.Dragged, out SharedBuckleComponent? buckleComponent)) return false; - bool Ignored(EntityUid entity) => entity == eventArgs.User || entity == eventArgs.Dragged || entity == eventArgs.Target; - - return EntitySystem.Get().InRangeUnobstructed(eventArgs.Target, eventArgs.Dragged, buckleComponent.Range, predicate: Ignored); - } - - public abstract bool DragDropOn(DragDropEvent eventArgs); - } - - [Serializable, NetSerializable] - public sealed class StrapComponentState : ComponentState - { - /// - /// The change in position that this strap makes to the strapped mob - /// - public StrapPosition Position; - - public float MaxBuckleDistance; - public Vector2 BuckleOffsetClamped; - public HashSet BuckledEntities; - - public StrapComponentState(StrapPosition position, Vector2 offset, HashSet buckled, float maxBuckleDistance) - { - Position = position; - BuckleOffsetClamped = offset; - BuckledEntities = buckled; - MaxBuckleDistance = maxBuckleDistance; - } - } - - [Serializable, NetSerializable] - public enum StrapVisuals : byte - { - RotationAngle, - BuckledState, - State + Position = position; + BuckleOffsetClamped = offset; + BuckledEntities = buckled; + MaxBuckleDistance = maxBuckleDistance; } } + +[Serializable, NetSerializable] +public enum StrapVisuals : byte +{ + RotationAngle, + State +} diff --git a/Content.Shared/Buckle/SharedBuckleSystem.Buckle.cs b/Content.Shared/Buckle/SharedBuckleSystem.Buckle.cs new file mode 100644 index 0000000000..b136d7892b --- /dev/null +++ b/Content.Shared/Buckle/SharedBuckleSystem.Buckle.cs @@ -0,0 +1,107 @@ +using Content.Shared.Buckle.Components; +using Content.Shared.Interaction.Events; +using Content.Shared.Movement.Events; +using Content.Shared.Standing; +using Content.Shared.Throwing; +using Content.Shared.Vehicle.Components; +using Robust.Shared.Map; +using Robust.Shared.Physics.Events; + +namespace Content.Shared.Buckle; + +public abstract partial class SharedBuckleSystem +{ + private void InitializeBuckle() + { + SubscribeLocalEvent(PreventCollision); + SubscribeLocalEvent(HandleDown); + SubscribeLocalEvent(HandleStand); + SubscribeLocalEvent(HandleThrowPushback); + SubscribeLocalEvent(HandleMove); + SubscribeLocalEvent(OnBuckleChangeDirectionAttempt); + } + + private void PreventCollision(EntityUid uid, SharedBuckleComponent component, ref PreventCollideEvent args) + { + if (args.BodyB.Owner != component.LastEntityBuckledTo) + return; + + if (component.Buckled || component.DontCollide) + args.Cancelled = true; + } + + private void HandleDown(EntityUid uid, SharedBuckleComponent component, DownAttemptEvent args) + { + if (component.Buckled) + args.Cancel(); + } + + private void HandleStand(EntityUid uid, SharedBuckleComponent component, StandAttemptEvent args) + { + if (component.Buckled) + args.Cancel(); + } + + private void HandleThrowPushback(EntityUid uid, SharedBuckleComponent component, ThrowPushbackAttemptEvent args) + { + if (component.Buckled) + args.Cancel(); + } + + private void HandleMove(EntityUid uid, SharedBuckleComponent component, UpdateCanMoveEvent args) + { + if (component.LifeStage > ComponentLifeStage.Running) + return; + + if (component.Buckled && + !HasComp(Transform(uid).ParentUid)) // buckle+vehicle shitcode + args.Cancel(); + } + + private void OnBuckleChangeDirectionAttempt(EntityUid uid, SharedBuckleComponent component, ChangeDirectionAttemptEvent args) + { + if (component.Buckled) + args.Cancel(); + } + + public bool IsBuckled(EntityUid uid, SharedBuckleComponent? component = null) + { + return Resolve(uid, ref component, false) && component.Buckled; + } + + /// + /// Reattaches this entity to the strap, modifying its position and rotation. + /// + /// The entity to reattach. + /// The strap to reattach to. + /// The buckle component of the entity to reattach. + public void ReAttach(EntityUid buckleId, SharedStrapComponent strap, SharedBuckleComponent? buckle = null) + { + if (!Resolve(buckleId, ref buckle, false)) + return; + + var ownTransform = Transform(buckleId); + var strapTransform = Transform(strap.Owner); + + ownTransform.Coordinates = new EntityCoordinates(strapTransform.Owner, strap.BuckleOffset); + + // Buckle subscribes to move for so this might fail. + // TODO: Make buckle not do that. + if (ownTransform.ParentUid != strapTransform.Owner) + return; + + ownTransform.LocalRotation = Angle.Zero; + + switch (strap.Position) + { + case StrapPosition.None: + break; + case StrapPosition.Stand: + _standing.Stand(buckleId); + break; + case StrapPosition.Down: + _standing.Down(buckleId, false, false); + break; + } + } +} diff --git a/Content.Shared/Buckle/SharedBuckleSystem.Strap.cs b/Content.Shared/Buckle/SharedBuckleSystem.Strap.cs new file mode 100644 index 0000000000..58edb3f2d7 --- /dev/null +++ b/Content.Shared/Buckle/SharedBuckleSystem.Strap.cs @@ -0,0 +1,93 @@ +using Content.Shared.Buckle.Components; +using Content.Shared.DragDrop; +using Content.Shared.Interaction; +using Robust.Shared.GameStates; + +namespace Content.Shared.Buckle; + +public abstract partial class SharedBuckleSystem +{ + [Dependency] private readonly SharedInteractionSystem _interactions = default!; + + private void InitializeStrap() + { + SubscribeLocalEvent(OnStrapRotate); + SubscribeLocalEvent(OnStrapHandleState); + SubscribeLocalEvent(OnStrapCanDragDropOn); + } + + private void OnStrapHandleState(EntityUid uid, SharedStrapComponent component, ref ComponentHandleState args) + { + if (args.Current is not StrapComponentState state) + return; + + component.Position = state.Position; + component.BuckleOffsetUnclamped = state.BuckleOffsetClamped; + component.BuckledEntities.Clear(); + component.BuckledEntities.UnionWith(state.BuckledEntities); + component.MaxBuckleDistance = state.MaxBuckleDistance; + } + + private void OnStrapRotate(EntityUid uid, SharedStrapComponent 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 (!EntityManager.TryGetComponent(buckledEntity, out SharedBuckleComponent? buckled)) + { + continue; + } + + if (!buckled.Buckled || buckled.LastEntityBuckledTo != uid) + { + Logger.Error($"A moving strap entity {ToPrettyString(uid)} attempted to re-parent an entity that does not 'belong' to it {ToPrettyString(buckledEntity)}"); + continue; + } + + ReAttach(buckledEntity, component, buckle: buckled); + Dirty(buckled); + } + } + + protected bool StrapCanDragDropOn( + EntityUid strapId, + EntityUid user, + EntityUid target, + EntityUid buckleId, + SharedStrapComponent? strap = null, + SharedBuckleComponent? buckle = null) + { + if (!Resolve(strapId, ref strap, false) || + !Resolve(buckleId, ref buckle, false)) + { + return false; + } + + bool Ignored(EntityUid entity) => entity == user || entity == buckleId || entity == target; + + return _interactions.InRangeUnobstructed(target, buckleId, buckle.Range, predicate: Ignored); + } + + private void OnStrapCanDragDropOn(EntityUid uid, SharedStrapComponent strap, CanDragDropOnEvent args) + { + args.CanDrop = StrapCanDragDropOn(args.Target, args.User, args.Target, args.Dragged, strap); + args.Handled = true; + } +} diff --git a/Content.Shared/Buckle/SharedBuckleSystem.cs b/Content.Shared/Buckle/SharedBuckleSystem.cs index 9e4a00a6c9..9ad83acbfa 100644 --- a/Content.Shared/Buckle/SharedBuckleSystem.cs +++ b/Content.Shared/Buckle/SharedBuckleSystem.cs @@ -1,16 +1,9 @@ -using Content.Shared.Buckle.Components; -using Content.Shared.Interaction.Events; -using Content.Shared.Movement.Events; using Content.Shared.Standing; -using Content.Shared.Throwing; -using Content.Shared.Vehicle.Components; -using Robust.Shared.Map; -using Robust.Shared.Physics.Events; using Robust.Shared.Timing; namespace Content.Shared.Buckle; -public abstract class SharedBuckleSystem : EntitySystem +public abstract partial class SharedBuckleSystem : EntitySystem { [Dependency] protected readonly IGameTiming GameTiming = default!; @@ -20,141 +13,7 @@ public abstract class SharedBuckleSystem : EntitySystem { base.Initialize(); - SubscribeLocalEvent(OnStrapRotate); - - SubscribeLocalEvent(PreventCollision); - SubscribeLocalEvent(HandleDown); - SubscribeLocalEvent(HandleStand); - SubscribeLocalEvent(HandleThrowPushback); - SubscribeLocalEvent(HandleMove); - SubscribeLocalEvent(OnBuckleChangeDirectionAttempt); - } - - private void OnStrapRotate(EntityUid uid, SharedStrapComponent 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 (!EntityManager.TryGetComponent(buckledEntity, out SharedBuckleComponent? buckled)) - { - continue; - } - - if (!buckled.Buckled || buckled.LastEntityBuckledTo != uid) - { - Logger.Error($"A moving strap entity {ToPrettyString(uid)} attempted to re-parent an entity that does not 'belong' to it {ToPrettyString(buckledEntity)}"); - continue; - } - - ReAttach(buckledEntity, component, buckle: buckled); - Dirty(buckled); - } - } - - public bool IsBuckled(EntityUid uid, SharedBuckleComponent? component = null) - { - return Resolve(uid, ref component, false) && component.Buckled; - } - - private void OnBuckleChangeDirectionAttempt(EntityUid uid, SharedBuckleComponent component, ChangeDirectionAttemptEvent args) - { - if (component.Buckled) - args.Cancel(); - } - - private void HandleMove(EntityUid uid, SharedBuckleComponent component, UpdateCanMoveEvent args) - { - if (component.LifeStage > ComponentLifeStage.Running) - return; - - if (component.Buckled && - !HasComp(Transform(uid).ParentUid)) // buckle+vehicle shitcode - args.Cancel(); - } - - private void HandleStand(EntityUid uid, SharedBuckleComponent component, StandAttemptEvent args) - { - if (component.Buckled) - { - args.Cancel(); - } - } - - private void HandleDown(EntityUid uid, SharedBuckleComponent component, DownAttemptEvent args) - { - if (component.Buckled) - { - args.Cancel(); - } - } - - private void HandleThrowPushback(EntityUid uid, SharedBuckleComponent component, ThrowPushbackAttemptEvent args) - { - if (!component.Buckled) return; - args.Cancel(); - } - - private void PreventCollision(EntityUid uid, SharedBuckleComponent component, ref PreventCollideEvent args) - { - if (args.BodyB.Owner != component.LastEntityBuckledTo) - return; - - if (component.Buckled || component.DontCollide) - { - args.Cancelled = true; - } - } - - /// - /// Reattaches this entity to the strap, modifying its position and rotation. - /// - /// The entity to reattach. - /// The strap to reattach to. - /// The buckle component of the entity to reattach. - public void ReAttach(EntityUid buckleId, SharedStrapComponent strap, SharedBuckleComponent? buckle = null) - { - if (!Resolve(buckleId, ref buckle, false)) - return; - - var ownTransform = Transform(buckleId); - var strapTransform = Transform(strap.Owner); - - ownTransform.Coordinates = new EntityCoordinates(strapTransform.Owner, strap.BuckleOffset); - - // Buckle subscribes to move for so this might fail. - // TODO: Make buckle not do that. - if (ownTransform.ParentUid != strapTransform.Owner) - return; - - ownTransform.LocalRotation = Angle.Zero; - - switch (strap.Position) - { - case StrapPosition.None: - break; - case StrapPosition.Stand: - _standing.Stand(buckleId); - break; - case StrapPosition.Down: - _standing.Down(buckleId, false, false); - break; - } + InitializeBuckle(); + InitializeStrap(); } }