using Content.Shared.ActionBlocker; using Content.Shared.Buckle.Components; using Content.Shared.Climbing.Events; using Content.Shared.DoAfter; using Content.Shared.Hands; using Content.Shared.Hands.Components; using Content.Shared.Interaction.Events; using Content.Shared.Inventory.VirtualItem; using Content.Shared.Item; using Content.Shared.Mobs; using Content.Shared.Movement.Events; using Content.Shared.Movement.Pulling.Components; using Content.Shared.Movement.Pulling.Events; using Content.Shared.Movement.Pulling.Systems; using Content.Shared.Movement.Systems; using Content.Shared.Nyanotrasen.Item.PseudoItem; using Content.Shared.Popups; using Content.Shared.Pulling; using Content.Shared.Resist; using Content.Shared.Standing; using Content.Shared.Storage; using Content.Shared.Stunnable; using Content.Shared.Throwing; using Content.Shared.Verbs; using Robust.Shared.Map.Components; using Robust.Shared.Network; using Robust.Shared.Physics.Components; using System.Numerics; namespace Content.Shared._DV.Carrying; public sealed class CarryingSystem : EntitySystem { [Dependency] private readonly ActionBlockerSystem _actionBlocker = default!; [Dependency] private readonly CarryingSlowdownSystem _slowdown = default!; [Dependency] private readonly INetManager _net = default!; [Dependency] private readonly MovementSpeedModifierSystem _movementSpeed = default!; [Dependency] private readonly PullingSystem _pulling = default!; [Dependency] private readonly SharedDoAfterSystem _doAfter = default!; [Dependency] private readonly SharedPopupSystem _popup = default!; [Dependency] private readonly SharedPseudoItemSystem _pseudoItem = default!; [Dependency] private readonly SharedTransformSystem _transform = default!; [Dependency] private readonly StandingStateSystem _standingState = default!; [Dependency] private readonly SharedVirtualItemSystem _virtualItem = default!; private EntityQuery _physicsQuery; public override void Initialize() { base.Initialize(); _physicsQuery = GetEntityQuery(); SubscribeLocalEvent>(AddCarryVerb); SubscribeLocalEvent>(AddInsertCarriedVerb); SubscribeLocalEvent(OnVirtualItemDeleted); SubscribeLocalEvent(OnThrow); SubscribeLocalEvent(OnParentChanged); SubscribeLocalEvent(OnMobStateChanged); SubscribeLocalEvent(OnDowned); SubscribeLocalEvent(OnInteractionAttempt); SubscribeLocalEvent(OnMoveAttempt); SubscribeLocalEvent(OnStandAttempt); SubscribeLocalEvent(OnInteractedWith); SubscribeLocalEvent(OnPullAttempt); SubscribeLocalEvent(OnDrop); SubscribeLocalEvent(OnDrop); SubscribeLocalEvent(OnDrop); SubscribeLocalEvent(OnDrop); SubscribeLocalEvent(OnDrop); SubscribeLocalEvent(OnDrop); SubscribeLocalEvent(OnRemoved); SubscribeLocalEvent(OnDoAfter); } private void AddCarryVerb(Entity ent, ref GetVerbsEvent args) { var user = args.User; var target = args.Target; if (!args.CanInteract || !args.CanAccess || user == target) return; if (!CanCarry(user, ent)) return; args.Verbs.Add(new AlternativeVerb() { Act = () => StartCarryDoAfter(user, ent), Text = Loc.GetString("carry-verb"), Priority = 2 }); } private void AddInsertCarriedVerb(Entity ent, ref GetVerbsEvent args) { // If the person is carrying someone, and the carried person is a pseudo-item, and the target entity is a storage, // then add an action to insert the carried entity into the target // AKA put carried felenid into a duffelbag if (args.Using is not {} carried || !args.CanAccess || !TryComp(carried, out var pseudoItem)) return; var target = args.Target; if (!TryComp(target, out var storageComp)) return; if (!_pseudoItem.CheckItemFits((carried, pseudoItem), (target, storageComp))) return; args.Verbs.Add(new InnateVerb() { Act = () => { DropCarried(ent, carried); _pseudoItem.TryInsert(target, carried, pseudoItem, storageComp); }, Text = Loc.GetString("action-name-insert-other", ("target", carried)), Priority = 2 }); } /// /// Since the carried entity is stored as 2 virtual items, when deleted we want to drop them. /// private void OnVirtualItemDeleted(Entity ent, ref VirtualItemDeletedEvent args) { if (HasComp(args.BlockingEntity)) DropCarried(ent, args.BlockingEntity); } /// /// Basically using virtual item passthrough to throw the carried person. A new age! /// Maybe other things besides throwing should use virt items like this... /// private void OnThrow(Entity ent, ref BeforeThrowEvent args) { if (!TryComp(args.ItemUid, out var virtItem) || !HasComp(virtItem.BlockingEntity)) return; var carried = virtItem.BlockingEntity; args.ItemUid = carried; args.ThrowSpeed = 5f * MassContest(ent, carried); } private void OnParentChanged(Entity ent, ref EntParentChangedMessage args) { var xform = Transform(ent); if (xform.MapUid != args.OldMapId) return; // Do not drop the carried entity if the new parent is a grid if (xform.ParentUid == xform.GridUid) return; DropCarried(ent, ent.Comp.Carried); } private void OnMobStateChanged(Entity ent, ref MobStateChangedEvent args) { DropCarried(ent, ent.Comp.Carried); } private void OnDowned(Entity ent, ref DownedEvent args) { DropCarried(ent, ent.Comp.Carried); } /// /// Only let the person being carried interact with their carrier and things on their person. /// private void OnInteractionAttempt(Entity ent, ref InteractionAttemptEvent args) { if (args.Target is not {} target) return; var targetParent = Transform(target).ParentUid; var carrier = ent.Comp.Carrier; if (target != carrier && targetParent != carrier && targetParent != ent.Owner) args.Cancelled = true; } private void OnMoveAttempt(Entity ent, ref UpdateCanMoveEvent args) { args.Cancel(); } private void OnStandAttempt(Entity ent, ref StandAttemptEvent args) { args.Cancel(); } private void OnInteractedWith(Entity ent, ref GettingInteractedWithAttemptEvent args) { if (args.Uid != ent.Comp.Carrier) args.Cancelled = true; } private void OnPullAttempt(Entity ent, ref PullAttemptEvent args) { args.Cancelled = true; } private void OnDrop(Entity ent, ref TEvent args) // Augh { DropCarried(ent.Comp.Carrier, ent); } private void OnRemoved(Entity ent, ref ComponentRemove args) { /* This component has been removed for whatever reason, so just make sure that the carrier is cleaned up. */ if (!TryComp(ent.Comp.Carrier, out var carryingComponent)) // This carrier has probably already been cleaned, no reason to try again return; CleanupCarrier(ent.Comp.Carrier, ent); } private void OnDoAfter(Entity ent, ref CarryDoAfterEvent args) { if (args.Handled || args.Cancelled) return; if (!CanCarry(args.Args.User, ent)) return; Carry(args.Args.User, ent); args.Handled = true; } private void StartCarryDoAfter(EntityUid carrier, Entity carried) { TimeSpan length = GetPickupDuration(carrier, carried); if (length.TotalSeconds >= 9f) { _popup.PopupClient(Loc.GetString("carry-too-heavy"), carried, carrier, PopupType.SmallCaution); return; } if (!HasComp(carried)) length *= 2f; var ev = new CarryDoAfterEvent(); var args = new DoAfterArgs(EntityManager, carrier, length, ev, carried, target: carried) { BreakOnMove = true, NeedHand = true }; _doAfter.TryStartDoAfter(args); // Show a popup to the person getting picked up _popup.PopupEntity(Loc.GetString("carry-started", ("carrier", carrier)), carried, carried); } private void Carry(EntityUid carrier, EntityUid carried) { if (TryComp(carried, out var pullable)) _pulling.TryStopPull(carried, pullable); var carrierXform = Transform(carrier); var xform = Transform(carried); _transform.AttachToGridOrMap(carrier, carrierXform); _transform.AttachToGridOrMap(carried, xform); _transform.SetParent(carried, xform, carrier, carrierXform); var carryingComp = EnsureComp(carrier); carryingComp.Carried = carried; Dirty(carrier, carryingComp); var carriedComp = EnsureComp(carried); carriedComp.Carrier = carrier; Dirty(carried, carriedComp); EnsureComp(carried); ApplyCarrySlowdown(carrier, carried); _actionBlocker.UpdateCanMove(carried); if (_net.IsClient) // no spawning prediction return; _virtualItem.TrySpawnVirtualItemInHand(carried, carrier); _virtualItem.TrySpawnVirtualItemInHand(carried, carrier); } public bool TryCarry(EntityUid carrier, Entity toCarry) { if (!Resolve(toCarry, ref toCarry.Comp, false)) return false; if (!CanCarry(carrier, (toCarry, toCarry.Comp))) return false; // The second one means that carrier is a pseudo-item and is inside a bag. if (HasComp(carrier) || HasComp(carrier)) return false; if (GetPickupDuration(carrier, toCarry).TotalSeconds > 9f) return false; Carry(carrier, toCarry); return true; } public void DropCarried(EntityUid carrier, EntityUid carried) { Drop(carried); CleanupCarrier(carrier, carried); } private void CleanupCarrier(EntityUid carrier, EntityUid carried) { RemComp(carrier); // get rid of this first so we don't recursively fire that event RemComp(carrier); _virtualItem.DeleteInHandsMatching(carrier, carried); _movementSpeed.RefreshMovementSpeedModifiers(carrier); } private void Drop(EntityUid carried) { RemComp(carried); RemComp(carried); // TODO SHITMED: make sure this doesnt let you make someone with no legs walk _actionBlocker.UpdateCanMove(carried); Transform(carried).AttachToGridOrMap(); _standingState.Stand(carried); } private void ApplyCarrySlowdown(EntityUid carrier, EntityUid carried) { var massRatio = MassContest(carrier, carried); if (massRatio == 0) massRatio = 1; var massRatioSq = Math.Pow(massRatio, 2); var modifier = (1 - (0.15 / massRatioSq)); modifier = Math.Max(0.1, modifier); _slowdown.SetModifier(carrier, (float) modifier); } public bool CanCarry(EntityUid carrier, Entity carried) { return carrier != carried.Owner && // can't carry multiple people, even if you have 4 hands it will break invariants when removing carryingcomponent for first carried person !HasComp(carrier) && // can't carry someone in a locker, buckled, etc HasComp(Transform(carrier).ParentUid) && // no tower of spacemen or stack overflow !HasComp(carrier) && !HasComp(carried) && // finally check that there are enough free hands TryComp(carrier, out var hands) && hands.CountFreeHands() >= carried.Comp.FreeHandsRequired; } private float MassContest(EntityUid roller, EntityUid target) { if (!_physicsQuery.TryComp(roller, out var rollerPhysics) || !_physicsQuery.TryComp(target, out var targetPhysics)) return 1f; if (targetPhysics.FixturesMass == 0) return 1f; return rollerPhysics.FixturesMass / targetPhysics.FixturesMass; } private TimeSpan GetPickupDuration(EntityUid carrier, EntityUid carried) { var length = TimeSpan.FromSeconds(3); var mod = MassContest(carrier, carried); if (mod != 0) length /= mod; return length; } public override void Update(float frameTime) { base.Update(frameTime); var query = EntityQueryEnumerator(); while (query.MoveNext(out var carried, out var comp, out var xform)) { var carrier = comp.Carrier; if (TerminatingOrDeleted(carrier)) { RemCompDeferred(carried); continue; } // SOMETIMES - when an entity is inserted into disposals, or a cryosleep chamber - it can get re-parented without a proper reparent event // when this happens, it needs to be dropped because it leads to weird behavior if (xform.ParentUid != carrier) { DropCarried(carrier, carried); continue; } // Make sure the carried entity is always centered relative to the carrier, as gravity pulls can offset it otherwise _transform.SetLocalPosition(carried, Vector2.Zero); } } }