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.Interaction.Events; using Content.Shared.Mobs.Components; using Content.Shared.Movement.Events; using Content.Shared.Popups; using Content.Shared.Pulling.Components; using Content.Shared.Standing; using Content.Shared.Storage.Components; using Content.Shared.Stunnable; using Content.Shared.Throwing; using Content.Shared.Verbs; using Robust.Shared.Physics.Components; using Robust.Shared.Physics.Events; using Robust.Shared.Utility; namespace Content.Shared.Buckle; public abstract partial class SharedBuckleSystem { private void InitializeBuckle() { SubscribeLocalEvent(OnBuckleComponentStartup); SubscribeLocalEvent(OnBuckleComponentShutdown); SubscribeLocalEvent(OnBuckleMove); SubscribeLocalEvent(OnBuckleInteractHand); SubscribeLocalEvent>(AddUnbuckleVerb); SubscribeLocalEvent(OnBuckleInsertIntoEntityStorageAttempt); SubscribeLocalEvent(OnBucklePreventCollide); SubscribeLocalEvent(OnBuckleDownAttempt); SubscribeLocalEvent(OnBuckleStandAttempt); SubscribeLocalEvent(OnBuckleThrowPushbackAttempt); SubscribeLocalEvent(OnBuckleUpdateCanMove); SubscribeLocalEvent(OnBuckleChangeDirectionAttempt); } private void OnBuckleComponentStartup(EntityUid uid, BuckleComponent component, ComponentStartup args) { UpdateBuckleStatus(uid, component); } private void OnBuckleComponentShutdown(EntityUid uid, BuckleComponent component, ComponentShutdown args) { TryUnbuckle(uid, uid, true, component); component.BuckleTime = default; } private void OnBuckleMove(EntityUid uid, BuckleComponent component, ref MoveEvent ev) { if (component.BuckledTo is not {} strapUid) return; if (!TryComp(strapUid, out var strapComp)) return; var strapPosition = Transform(strapUid).Coordinates; if (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 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); } private void OnBuckleInsertIntoEntityStorageAttempt(EntityUid uid, BuckleComponent component, ref InsertIntoEntityStorageAttemptEvent args) { if (component.Buckled) args.Cancelled = true; } private void OnBucklePreventCollide(EntityUid uid, BuckleComponent component, ref PreventCollideEvent args) { if (args.OtherEntity != component.BuckledTo) return; if (component.Buckled || component.DontCollide) args.Cancelled = true; } private void OnBuckleDownAttempt(EntityUid uid, BuckleComponent component, DownAttemptEvent args) { if (component.Buckled) args.Cancel(); } private void OnBuckleStandAttempt(EntityUid uid, BuckleComponent component, StandAttemptEvent args) { if (component.Buckled) args.Cancel(); } private void OnBuckleThrowPushbackAttempt(EntityUid uid, BuckleComponent component, ThrowPushbackAttemptEvent args) { if (component.Buckled) args.Cancel(); } private void OnBuckleUpdateCanMove(EntityUid uid, BuckleComponent component, UpdateCanMoveEvent args) { if (component.LifeStage > ComponentLifeStage.Running) return; if (component.Buckled) // buckle shitcode args.Cancel(); } private void OnBuckleChangeDirectionAttempt(EntityUid uid, BuckleComponent component, ChangeDirectionAttemptEvent args) { if (component.Buckled) args.Cancel(); } public bool IsBuckled(EntityUid uid, BuckleComponent? component = null) { return Resolve(uid, ref component, false) && component.Buckled; } /// /// Shows or hides the buckled status effect depending on if the /// entity is buckled or not. /// /// Entity that we want to show the alert /// buckle component of the entity /// strap component of the thing we are strapping to private void UpdateBuckleStatus(EntityUid uid, BuckleComponent buckleComp, StrapComponent? strapComp = null) { Appearance.SetData(uid, StrapVisuals.State, buckleComp.Buckled); if (buckleComp.BuckledTo != null) { if (!Resolve(buckleComp.BuckledTo.Value, ref strapComp)) return; var alertType = strapComp.BuckledAlertType; _alerts.ShowAlert(uid, alertType); } else { _alerts.ClearAlertCategory(uid, AlertCategory.Buckled); } } /// /// Sets the field in the component to a value /// /// Value tat with be assigned to the field private void SetBuckledTo(EntityUid buckleUid, EntityUid? strapUid, StrapComponent? strapComp, BuckleComponent buckleComp) { buckleComp.BuckledTo = strapUid; if (strapUid == null) { buckleComp.Buckled = false; } else { buckleComp.LastEntityBuckledTo = strapUid; buckleComp.DontCollide = true; buckleComp.Buckled = true; buckleComp.BuckleTime = _gameTiming.CurTime; } ActionBlocker.UpdateCanMove(buckleUid); UpdateBuckleStatus(buckleUid, buckleComp, strapComp); Dirty(buckleComp); } /// /// Checks whether or not buckling is possible /// /// Uid of the owner of BuckleComponent /// /// Uid of a third party entity, /// i.e, the uid of someone else you are dragging to a chair. /// Can equal buckleUid sometimes /// /// Uid of the owner of strap component private bool CanBuckle( EntityUid buckleUid, EntityUid userUid, EntityUid strapUid, [NotNullWhen(true)] out StrapComponent? strapComp, BuckleComponent? buckleComp = null) { strapComp = null; if (userUid == strapUid || !Resolve(buckleUid, ref buckleComp, false) || !Resolve(strapUid, ref strapComp, false)) { return false; } // Does it pass the Whitelist if (strapComp.AllowedEntities != null && !strapComp.AllowedEntities.IsValid(userUid, EntityManager)) { if (_netManager.IsServer) _popup.PopupEntity(Loc.GetString("buckle-component-cannot-fit-message"), userUid, buckleUid, 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, 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 (!HasComp(userUid)) { // PopupPredicted when if (_netManager.IsServer) _popup.PopupEntity(Loc.GetString("buckle-component-no-hands-message"), userUid, userUid); return false; } if (buckleComp.Buckled) { var message = Loc.GetString(buckleUid == userUid ? "buckle-component-already-buckled-message" : "buckle-component-other-already-buckled-message", ("owner", Identity.Entity(buckleUid, EntityManager))); if (_netManager.IsServer) _popup.PopupEntity(message, userUid, userUid); return false; } var parent = Transform(strapUid).ParentUid; while (parent.IsValid()) { if (parent == userUid) { 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; } 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); return false; } var attemptEvent = new BuckleAttemptEvent(strapUid, buckleUid, userUid, true); RaiseLocalEvent(attemptEvent.BuckledEntity, ref attemptEvent); RaiseLocalEvent(attemptEvent.StrapEntity, ref attemptEvent); if (attemptEvent.Cancelled) return false; return true; } /// /// Attempts to buckle an entity to a strap /// /// Uid of the owner of BuckleComponent /// /// Uid of a third party entity, /// i.e, the uid of someone else you are dragging to a chair. /// Can equal buckleUid sometimes /// /// Uid of the owner of strap component public bool TryBuckle(EntityUid buckleUid, EntityUid userUid, EntityUid strapUid, BuckleComponent? buckleComp = null) { if (!Resolve(buckleUid, ref buckleComp, false)) return false; if (!CanBuckle(buckleUid, userUid, strapUid, 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(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(buckleUid, out var ownerPullable)) { if (ownerPullable.Puller != null) { _pulling.TryStopPull(ownerPullable); } } if (TryComp(buckleUid, out var physics)) { _physics.ResetDynamics(physics); } if (!buckleComp.PullStrap && TryComp(strapUid, out var toPullable)) { if (toPullable.Puller == buckleUid) { // can't pull it and buckle to it at the same time _pulling.TryStopPull(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)}"); 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 buckleUid, EntityUid userUid, bool force = false, BuckleComponent? buckleComp = null) { if (!Resolve(buckleUid, ref buckleComp, false) || buckleComp.BuckledTo is not { } strapUid) 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(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(strapUid, out var strapComp)) 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(buckleUid, out var mobState) && _mobState.IsIncapacitated(buckleUid, mobState) || HasComp(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); Dirty(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); 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 buckleUid, EntityUid userUid, EntityUid strapUid, bool force = false, BuckleComponent? buckle = null) { if (!Resolve(buckleUid, ref buckle, false)) return false; if (!buckle.Buckled) { return TryBuckle(buckleUid, userUid, strapUid, buckle); } else { return TryUnbuckle(buckleUid, userUid, force, buckle); } } }