using System.Diagnostics.CodeAnalysis; using Content.Server.Hands.Components; using Content.Server.Pulling; using Content.Shared.ActionBlocker; using Content.Shared.Vehicle.Components; using Content.Shared.Alert; using Content.Shared.Buckle.Components; using Content.Shared.Interaction; using Content.Shared.MobState.Components; using Content.Shared.MobState.EntitySystems; using Content.Shared.Popups; using Content.Shared.Pulling.Components; using Content.Shared.Standing; using Content.Shared.Stunnable; using Robust.Server.GameObjects; using Robust.Shared.Audio; using Robust.Shared.Containers; using Robust.Shared.Player; using Robust.Shared.Timing; using Content.Shared.IdentityManagement; namespace Content.Server.Buckle.Components { /// /// Component that handles sitting entities into s. /// [RegisterComponent] [ComponentReference(typeof(SharedBuckleComponent))] public sealed class BuckleComponent : SharedBuckleComponent { [Dependency] private readonly IEntityManager _entMan = default!; [Dependency] private readonly IEntitySystemManager _sysMan = default!; [Dependency] private readonly IGameTiming _gameTiming = default!; [DataField("size")] private int _size = 100; /// /// The amount of time that must pass for this entity to /// be able to unbuckle after recently buckling. /// [DataField("delay")] [ViewVariables] private TimeSpan _unbuckleDelay = TimeSpan.FromSeconds(0.25f); /// /// The time that this entity buckled at. /// [ViewVariables] private TimeSpan _buckleTime; /// /// The position offset that is being applied to this entity if buckled. /// public Vector2 BuckleOffset { get; private set; } private StrapComponent? _buckledTo; /// /// The strap that this component is buckled to. /// [ViewVariables] public StrapComponent? BuckledTo { get => _buckledTo; private set { _buckledTo = value; _buckleTime = _gameTiming.CurTime; _sysMan.GetEntitySystem().UpdateCanMove(Owner); Dirty(_entMan); } } [ViewVariables] public override bool Buckled => BuckledTo != null; /// /// The amount of space that this entity occupies in a /// . /// [ViewVariables] public int Size => _size; /// /// Shows or hides the buckled status effect depending on if the /// entity is buckled or not. /// private void UpdateBuckleStatus() { if (Buckled) { AlertType alertType = BuckledTo?.BuckledAlertType ?? AlertType.Buckled; EntitySystem.Get().ShowAlert(Owner, alertType); } else { EntitySystem.Get().ClearAlertCategory(Owner, AlertCategory.Buckled); } } /// /// Reattaches this entity to the strap, modifying its position and rotation. /// /// The strap to reattach to. public void ReAttach(StrapComponent strap) { var ownTransform = _entMan.GetComponent(Owner); var strapTransform = _entMan.GetComponent(strap.Owner); ownTransform.AttachParent(strapTransform); ownTransform.LocalRotation = Angle.Zero; switch (strap.Position) { case StrapPosition.None: break; case StrapPosition.Stand: EntitySystem.Get().Stand(Owner); break; case StrapPosition.Down: EntitySystem.Get().Down(Owner, false, false); break; } ownTransform.LocalPosition = strap.BuckleOffset; } public bool CanBuckle(EntityUid user, EntityUid to, [NotNullWhen(true)] out StrapComponent? strap) { var popupSystem = EntitySystem.Get(); strap = null; if (user == to) { return false; } if (!_entMan.TryGetComponent(to, out strap)) { return false; } var strapUid = strap.Owner; bool Ignored(EntityUid entity) => entity == Owner || entity == user || entity == strapUid; if (!EntitySystem.Get().InRangeUnobstructed(Owner, strapUid, Range, predicate: Ignored, popup: true)) { return false; } // If in a container if (Owner.TryGetContainer(out var ownerContainer)) { // And not in the same container as the strap if (!strap.Owner.TryGetContainer(out var strapContainer) || ownerContainer != strapContainer) { return false; } } if (!_entMan.HasComponent(user)) { popupSystem.PopupEntity(Loc.GetString("buckle-component-no-hands-message"), user, Filter.Entities(user)); return false; } if (Buckled) { var message = Loc.GetString(Owner == user ? "buckle-component-already-buckled-message" : "buckle-component-other-already-buckled-message", ("owner", Identity.Entity(Owner, _entMan))); popupSystem.PopupEntity(message, user, Filter.Entities(user)); return false; } var parent = _entMan.GetComponent(to).Parent; while (parent != null) { if (parent == _entMan.GetComponent(user)) { var message = Loc.GetString(Owner == user ? "buckle-component-cannot-buckle-message" : "buckle-component-other-cannot-buckle-message", ("owner", Identity.Entity(Owner, _entMan))); popupSystem.PopupEntity(message, user, Filter.Entities(user)); return false; } parent = parent.Parent; } if (!strap.HasSpace(this)) { var message = Loc.GetString(Owner == user ? "buckle-component-cannot-fit-message" : "buckle-component-other-cannot-fit-message", ("owner", Identity.Entity(Owner, _entMan))); popupSystem.PopupEntity(message, user, Filter.Entities(user)); return false; } return true; } public override bool TryBuckle(EntityUid user, EntityUid to) { var popupSystem = EntitySystem.Get(); if (!CanBuckle(user, to, out var strap)) { return false; } SoundSystem.Play(strap.BuckleSound.GetSound(), Filter.Pvs(Owner), Owner); if (!strap.TryAdd(this)) { var message = Loc.GetString(Owner == user ? "buckle-component-cannot-buckle-message" : "buckle-component-other-cannot-buckle-message", ("owner", Identity.Entity(Owner, _entMan))); popupSystem.PopupEntity(message, user, Filter.Entities(user)); return false; } if(_entMan.TryGetComponent(Owner, out var appearance)) appearance.SetData(BuckleVisuals.Buckled, true); ReAttach(strap); BuckledTo = strap; LastEntityBuckledTo = BuckledTo.Owner; DontCollide = true; UpdateBuckleStatus(); var ev = new BuckleChangeEvent() { Buckling = true, Strap = BuckledTo.Owner, BuckledEntity = Owner }; _entMan.EventBus.RaiseLocalEvent(ev.BuckledEntity, ev, false); _entMan.EventBus.RaiseLocalEvent(ev.Strap, ev, false); if (_entMan.TryGetComponent(Owner, out SharedPullableComponent? ownerPullable)) { if (ownerPullable.Puller != null) { EntitySystem.Get().TryStopPull(ownerPullable); } } if (_entMan.TryGetComponent(to, out SharedPullableComponent? toPullable)) { if (toPullable.Puller == Owner) { // can't pull it and buckle to it at the same time EntitySystem.Get().TryStopPull(toPullable); } } return true; } /// /// Tries to unbuckle the Owner of this component from its current strap. /// /// 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. /// /// /// true if the owner was unbuckled, otherwise false even if the owner /// was previously already unbuckled. /// public bool TryUnbuckle(EntityUid user, bool force = false) { if (BuckledTo == null) { return false; } var oldBuckledTo = BuckledTo; if (!force) { if (_gameTiming.CurTime < _buckleTime + _unbuckleDelay) { return false; } if (!EntitySystem.Get().InRangeUnobstructed(user, oldBuckledTo.Owner, Range, popup: true)) { return false; } // If the strap is a vehicle and the rider is not the person unbuckling, return. if (_entMan.TryGetComponent(oldBuckledTo.Owner, out var vehicle) && vehicle.Rider != user) return false; } BuckledTo = null; var entManager = IoCManager.Resolve(); var xform = entManager.GetComponent(Owner); var oldBuckledXform = entManager.GetComponent(oldBuckledTo.Owner); if (xform.ParentUid == oldBuckledXform.Owner) { xform.AttachParentToContainerOrGrid(); xform.WorldRotation = oldBuckledXform.WorldRotation; if (oldBuckledTo.UnbuckleOffset != Vector2.Zero) xform.Coordinates = oldBuckledXform.Coordinates.Offset(oldBuckledTo.UnbuckleOffset); } if(_entMan.TryGetComponent(Owner, out var appearance)) appearance.SetData(BuckleVisuals.Buckled, false); if (_entMan.HasComponent(Owner) | (_entMan.TryGetComponent(Owner, out var mobState) && mobState.IsIncapacitated())) { EntitySystem.Get().Down(Owner); } else { EntitySystem.Get().Stand(Owner); } IoCManager.Resolve().GetEntitySystem() .EnterState(mobState, mobState?.CurrentState); UpdateBuckleStatus(); oldBuckledTo.Remove(this); SoundSystem.Play(oldBuckledTo.UnbuckleSound.GetSound(), Filter.Pvs(Owner), Owner); var ev = new BuckleChangeEvent() { Buckling = false, Strap = oldBuckledTo.Owner, BuckledEntity = Owner }; _entMan.EventBus.RaiseLocalEvent(Owner, ev, false); _entMan.EventBus.RaiseLocalEvent(oldBuckledTo.Owner, ev, false); return true; } /// /// Makes an entity toggle the buckling status of the owner to a /// specific entity. /// /// 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. /// /// true if the buckling status was changed, false otherwise. public bool ToggleBuckle(EntityUid user, EntityUid to, bool force = false) { if (BuckledTo?.Owner == to) { return TryUnbuckle(user, force); } return TryBuckle(user, to); } protected override void Startup() { base.Startup(); UpdateBuckleStatus(); } protected override void Shutdown() { BuckledTo?.Remove(this); TryUnbuckle(Owner, true); _buckleTime = default; UpdateBuckleStatus(); base.Shutdown(); } public override ComponentState GetComponentState() { int? drawDepth = null; if (BuckledTo != null && _entMan.GetComponent(BuckledTo.Owner).LocalRotation.GetCardinalDir() == Direction.North && _entMan.TryGetComponent(BuckledTo.Owner, out var spriteComponent)) { drawDepth = spriteComponent.DrawDepth - 1; } return new BuckleComponentState(Buckled, drawDepth, LastEntityBuckledTo, DontCollide); } } }