diff --git a/Content.Server/2G2C/Carrying/BeingCarriedComponent.cs b/Content.Server/2G2C/Carrying/BeingCarriedComponent.cs
new file mode 100644
index 000000000..6b9a071d5
--- /dev/null
+++ b/Content.Server/2G2C/Carrying/BeingCarriedComponent.cs
@@ -0,0 +1,11 @@
+namespace Content.Server.Carrying
+{
+ ///
+ /// Used so we can sub to events like VirtualItemDeleted
+ ///
+ [RegisterComponent]
+ public sealed class BeingCarriedComponent : Component
+ {
+ public EntityUid Carrier = default!;
+ }
+}
diff --git a/Content.Server/2G2C/Carrying/CarriableComponent.cs b/Content.Server/2G2C/Carrying/CarriableComponent.cs
new file mode 100644
index 000000000..420c2417d
--- /dev/null
+++ b/Content.Server/2G2C/Carrying/CarriableComponent.cs
@@ -0,0 +1,16 @@
+using System.Threading;
+
+namespace Content.Server.Carrying
+{
+ [RegisterComponent]
+ public sealed class CarriableComponent : Component
+ {
+ public CancellationTokenSource? CancelToken;
+ ///
+ /// Number of free hands required
+ /// to carry the entity
+ ///
+ [DataField("freeHandsRequired")]
+ public int FreeHandsRequired = 2;
+ }
+}
diff --git a/Content.Server/2G2C/Carrying/CarryingComponent.cs b/Content.Server/2G2C/Carrying/CarryingComponent.cs
new file mode 100644
index 000000000..f51717ab1
--- /dev/null
+++ b/Content.Server/2G2C/Carrying/CarryingComponent.cs
@@ -0,0 +1,9 @@
+namespace Content.Server.Carrying
+{
+ ///
+ /// Used so we can sub to events like VirtualItemDeleted
+ ///
+ [RegisterComponent]
+ public sealed class CarryingComponent : Component
+ {}
+}
diff --git a/Content.Server/2G2C/Carrying/CarryingSystem.cs b/Content.Server/2G2C/Carrying/CarryingSystem.cs
new file mode 100644
index 000000000..1dabf5da8
--- /dev/null
+++ b/Content.Server/2G2C/Carrying/CarryingSystem.cs
@@ -0,0 +1,229 @@
+using System.Threading;
+using Content.Server.DoAfter;
+using Content.Server.Hands.Systems;
+using Content.Server.Hands.Components;
+using Content.Shared.MobState.Components;
+using Content.Shared.Hands.Components;
+using Content.Shared.Hands;
+using Content.Shared.Stunnable;
+using Content.Shared.Interaction.Events;
+using Content.Shared.Verbs;
+using Content.Shared.Carrying;
+using Content.Shared.Movement.Events;
+using Content.Shared.Pulling;
+using Content.Shared.Pulling.Components;
+using Content.Shared.Standing;
+using Content.Shared.ActionBlocker;
+using Robust.Shared.Physics;
+
+namespace Content.Server.Carrying
+{
+ public sealed class CarryingSystem : EntitySystem
+ {
+ [Dependency] private readonly HandVirtualItemSystem _virtualItemSystem = default!;
+ [Dependency] private readonly CarryingSlowdownSystem _slowdown = default!;
+ [Dependency] private readonly DoAfterSystem _doAfterSystem = default!;
+ [Dependency] private readonly StandingStateSystem _standingState = default!;
+ [Dependency] private readonly ActionBlockerSystem _actionBlockerSystem = default!;
+ [Dependency] private readonly SharedPullingSystem _pullingSystem = default!;
+ public override void Initialize()
+ {
+ base.Initialize();
+ SubscribeLocalEvent>(AddCarryVerb);
+ SubscribeLocalEvent(OnVirtualItemDeleted);
+ SubscribeLocalEvent(OnMoveAttempt);
+ SubscribeLocalEvent(OnStandAttempt);
+ SubscribeLocalEvent(OnInteractedWith);
+ SubscribeLocalEvent(OnCarrySuccess);
+ SubscribeLocalEvent(OnCarryCancelled);
+ }
+
+
+ private void AddCarryVerb(EntityUid uid, CarriableComponent component, GetVerbsEvent args)
+ {
+ if (!args.CanInteract || !args.CanAccess)
+ return;
+
+ if (!CanCarry(args.User, uid, component))
+ return;
+
+ if (HasComp(args.User)) // yeah not dealing with that
+ return;
+
+ if (!HasComp(uid) && !(TryComp(uid, out var state) && (state.IsCritical() || state.IsDead() || state.IsIncapacitated())))
+ return;
+
+ AlternativeVerb verb = new()
+ {
+ Act = () =>
+ {
+ StartCarryDoAfter(args.User, uid, component);
+ },
+ Text = Loc.GetString("carry-verb"),
+ Priority = 2
+ };
+ args.Verbs.Add(verb);
+ }
+
+ private void OnVirtualItemDeleted(EntityUid uid, CarryingComponent component, VirtualItemDeletedEvent args)
+ {
+ if (!HasComp(args.BlockingEntity))
+ return;
+
+ DropCarried(uid, args.BlockingEntity);
+ }
+
+ private void OnMoveAttempt(EntityUid uid, BeingCarriedComponent component, UpdateCanMoveEvent args)
+ {
+ args.Cancel();
+ }
+
+ private void OnStandAttempt(EntityUid uid, BeingCarriedComponent component, StandAttemptEvent args)
+ {
+ args.Cancel();
+ }
+
+ private void OnInteractedWith(EntityUid uid, BeingCarriedComponent component, GettingInteractedWithAttemptEvent args)
+ {
+ if (args.Uid != component.Carrier)
+ args.Cancel();
+ }
+
+ private void OnCarrySuccess(CarrySuccessfulEvent ev)
+ {
+ if (!CanCarry(ev.Carrier, ev.Carried, ev.Component))
+ return;
+
+ Carry(ev.Carrier, ev.Carried);
+ }
+
+ private void OnCarryCancelled(CarryCancelledEvent ev)
+ {
+ if (ev.Component == null)
+ return;
+
+ ev.Component.CancelToken = null;
+ }
+
+ private void StartCarryDoAfter(EntityUid carrier, EntityUid carried, CarriableComponent component)
+ {
+ if (component.CancelToken != null)
+ {
+ component.CancelToken.Cancel();
+ component.CancelToken = null;
+ }
+
+ component.CancelToken = new CancellationTokenSource();
+ _doAfterSystem.DoAfter(new DoAfterEventArgs(carrier, 3f, component.CancelToken.Token, target: carried)
+ {
+ BroadcastFinishedEvent = new CarrySuccessfulEvent(carrier, carried, component),
+ BroadcastCancelledEvent = new CarryCancelledEvent(carrier, component),
+ BreakOnTargetMove = true,
+ BreakOnUserMove = true,
+ BreakOnStun = true,
+ NeedHand = true
+ });
+ }
+
+ private void Carry(EntityUid carrier, EntityUid carried)
+ {
+ if (TryComp(carried, out var pullable))
+ _pullingSystem.TryStopPull(pullable);
+
+ Transform(carried).Coordinates = Transform(carrier).Coordinates;
+ Transform(carried).ParentUid = carrier;
+ _virtualItemSystem.TrySpawnVirtualItemInHand(carried, carrier);
+ _virtualItemSystem.TrySpawnVirtualItemInHand(carried, carrier);
+ EnsureComp(carrier);
+ ApplyCarrySlowdown(carrier, carried);
+ var carriedComp = EnsureComp(carried);
+ carriedComp.Carrier = carrier;
+ _actionBlockerSystem.UpdateCanMove(carried);
+ }
+
+ public void DropCarried(EntityUid carrier, EntityUid carried)
+ {
+ RemComp(carrier); // get rid of this first so we don't recusrively fire that event
+ RemComp(carrier);
+ RemComp(carried);
+ _actionBlockerSystem.UpdateCanMove(carried);
+ _virtualItemSystem.DeleteInHandsMatching(carrier, carried);
+ Transform(carried).AttachToGridOrMap();
+ _standingState.Stand(carried);
+ }
+
+ private void ApplyCarrySlowdown(EntityUid carrier, EntityUid carried)
+ {
+ if (!TryComp(carrier, out var carrierFixtures))
+ return;
+ if (!TryComp(carried, out var carriedFixtures))
+ return;
+ if (carrierFixtures.Fixtures.Count == 0 || carriedFixtures.Fixtures.Count == 0)
+ return;
+
+ float carrierMass = 0f;
+ float carriedMass = 0f;
+ foreach (var fixture in carrierFixtures.Fixtures.Values)
+ {
+ carrierMass += fixture.Mass;
+ }
+ foreach (var fixture in carriedFixtures.Fixtures.Values)
+ {
+ carriedMass += fixture.Mass;
+ }
+
+ if (carrierMass == 0f)
+ carrierMass = 70f;
+ if (carriedMass == 0f)
+ carriedMass = 70f;
+
+ var massRatioSq = Math.Pow((carriedMass / carrierMass), 2);
+ var modifier = (1 - (massRatioSq * 0.15));
+ var slowdownComp = EnsureComp(carrier);
+ _slowdown.SetModifier(carrier, (float) modifier, (float) modifier, slowdownComp);
+ }
+
+ public bool CanCarry(EntityUid carrier, EntityUid carried, CarriableComponent? carriedComp = null)
+ {
+ if (!Resolve(carried, ref carriedComp))
+ return false;
+
+ if (!TryComp(carrier, out var hands))
+ return false;
+
+ if (hands.CountFreeHands() < carriedComp.FreeHandsRequired)
+ return false;
+
+ return true;
+ }
+
+ private sealed class CarryCancelledEvent : EntityEventArgs
+ {
+ public EntityUid Uid;
+
+ public CarriableComponent Component;
+
+ public CarryCancelledEvent(EntityUid uid, CarriableComponent component)
+ {
+ Uid = uid;
+ Component = component;
+ }
+ }
+
+ private sealed class CarrySuccessfulEvent : EntityEventArgs
+ {
+ public EntityUid Carrier;
+
+ public EntityUid Carried;
+
+ public CarriableComponent Component;
+
+ public CarrySuccessfulEvent(EntityUid carrier, EntityUid carried, CarriableComponent component)
+ {
+ Carrier = carrier;
+ Carried = carried;
+ Component = component;
+ }
+ }
+ }
+}
diff --git a/Content.Shared/2G2C/Carrying/CarryingSlowdownComponent.cs b/Content.Shared/2G2C/Carrying/CarryingSlowdownComponent.cs
new file mode 100644
index 000000000..f9913c36a
--- /dev/null
+++ b/Content.Shared/2G2C/Carrying/CarryingSlowdownComponent.cs
@@ -0,0 +1,28 @@
+using Robust.Shared.GameStates;
+using Robust.Shared.Serialization;
+
+namespace Content.Shared.Carrying
+{
+ [RegisterComponent, NetworkedComponent, Access(typeof(CarryingSlowdownSystem))]
+
+ public sealed class CarryingSlowdownComponent : Component
+ {
+ [DataField("walkModifier", required: true)] [ViewVariables(VVAccess.ReadWrite)]
+ public float WalkModifier = 1.0f;
+
+ [DataField("sprintModifier", required: true)] [ViewVariables(VVAccess.ReadWrite)]
+ public float SprintModifier = 1.0f;
+ }
+
+ [Serializable, NetSerializable]
+ public sealed class CarryingSlowdownComponentState : ComponentState
+ {
+ public float WalkModifier;
+ public float SprintModifier;
+ public CarryingSlowdownComponentState(float walkModifier, float sprintModifier)
+ {
+ WalkModifier = walkModifier;
+ SprintModifier = sprintModifier;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Content.Shared/2G2C/Carrying/CarryingSlowdownSystem.cs b/Content.Shared/2G2C/Carrying/CarryingSlowdownSystem.cs
new file mode 100644
index 000000000..e9f167b68
--- /dev/null
+++ b/Content.Shared/2G2C/Carrying/CarryingSlowdownSystem.cs
@@ -0,0 +1,47 @@
+using Content.Shared.Movement.Systems;
+using Robust.Shared.GameStates;
+
+namespace Content.Shared.Carrying
+{
+ public sealed class CarryingSlowdownSystem : EntitySystem
+ {
+ [Dependency] private readonly MovementSpeedModifierSystem _movementSpeed = default!;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+ SubscribeLocalEvent(OnGetState);
+ SubscribeLocalEvent(OnHandleState);
+ SubscribeLocalEvent(OnRefreshMoveSpeed);
+ }
+
+ public void SetModifier(EntityUid uid, float walkSpeedModifier, float sprintSpeedModifier, CarryingSlowdownComponent? component = null)
+ {
+ if (!Resolve(uid, ref component))
+ return;
+
+ component.WalkModifier = walkSpeedModifier;
+ component.SprintModifier = sprintSpeedModifier;
+ _movementSpeed.RefreshMovementSpeedModifiers(uid);
+ }
+ private void OnGetState(EntityUid uid, CarryingSlowdownComponent component, ref ComponentGetState args)
+ {
+ args.State = new CarryingSlowdownComponentState(component.WalkModifier, component.SprintModifier);
+ }
+
+ private void OnHandleState(EntityUid uid, CarryingSlowdownComponent component, ref ComponentHandleState args)
+ {
+ if (args.Current is CarryingSlowdownComponentState state)
+ {
+ component.WalkModifier = state.WalkModifier;
+ component.SprintModifier = state.SprintModifier;
+
+ _movementSpeed.RefreshMovementSpeedModifiers(uid);
+ }
+ }
+ private void OnRefreshMoveSpeed(EntityUid uid, CarryingSlowdownComponent component, RefreshMovementSpeedModifiersEvent args)
+ {
+ args.ModifySpeed(component.WalkModifier, component.SprintModifier);
+ }
+ }
+}
\ No newline at end of file