Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
83a907e
fix: scp096 stuck
ThereDrD0 Apr 7, 2026
349160b
add: multi pulling v1
ThereDrD0 Apr 7, 2026
692606e
add: tweaks and QoL
ThereDrD0 Apr 7, 2026
e943403
add: update with many features
ThereDrD0 Apr 8, 2026
0a3d1ad
remove: actions
ThereDrD0 Apr 8, 2026
573ff17
Merge branch 'master' into add/multipulling
ThereDrD0 Apr 8, 2026
7427c1f
Merge branch 'master' into add/multipulling
ThereDrD0 Apr 13, 2026
9759c56
add: add whitelist/blaclist
ThereDrD0 Apr 13, 2026
e43a8f6
add: damn update. Added action blocking, code refactor
ThereDrD0 Apr 14, 2026
40052c3
Merge branch 'master' into add/multipulling
ThereDrD0 Apr 16, 2026
2c7b464
refactor: little clean up
ThereDrD0 Apr 16, 2026
3af567b
refactor: oh my god so many refactors
ThereDrD0 Apr 16, 2026
a0c35b8
refactor: some minor refactors + separate systems from components
ThereDrD0 Apr 17, 2026
0946250
refactor: big renaming, move contants to component
ThereDrD0 Apr 17, 2026
86b6d06
refactor: clean up code structure
ThereDrD0 Apr 17, 2026
59b70a9
refactor: virtual blocker
ThereDrD0 Apr 17, 2026
50f0d38
fix: previous refactor
ThereDrD0 Apr 17, 2026
807521b
fix: fixes
ThereDrD0 Apr 18, 2026
2781dc3
Update Content.Server/_Scp/Holding/ScpHoldingSystem.cs
ThereDrD0 Apr 18, 2026
c0eefbd
fix: ai review
ThereDrD0 Apr 18, 2026
7fde976
add: port move by cursor and add HolderHandsRequired
ThereDrD0 Apr 18, 2026
8577e59
refactor: remove feeedback partial, remove ent.Owner
ThereDrD0 Apr 19, 2026
5bff066
refactor: move alerts to new system
ThereDrD0 Apr 19, 2026
c9392e5
remove: sorry codex
ThereDrD0 Apr 19, 2026
173ea19
refactor: make multipulling more multi
ThereDrD0 Apr 19, 2026
33595cd
fix: move to cursor + handcuffs
ThereDrD0 Apr 20, 2026
d4f0233
fix: ai review 2
ThereDrD0 Apr 21, 2026
903a0ed
fix: holding scps
ThereDrD0 Apr 21, 2026
7859510
fix: ai review
ThereDrD0 Apr 21, 2026
549786a
i am ready to merge
ThereDrD0 Apr 21, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
301 changes: 301 additions & 0 deletions Content.Client/_Scp/Holding/ScpHoldingSystem.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,301 @@
using Content.Client.Hands.Systems;
using Content.Client.Inventory;
using Content.Shared._Scp.Holding.Components;
using Content.Shared._Scp.Holding.Systems;
using Content.Shared.Hands;
using Content.Shared.Hands.Components;
using Content.Shared.Input;
using Content.Shared.Inventory.VirtualItem;
using Robust.Client.Physics;
using Robust.Client.Player;
using Robust.Shared.Input.Binding;
using Robust.Shared.Map;
using Robust.Shared.Physics.Components;
using Robust.Shared.Player;
using Robust.Shared.Timing;

namespace Content.Client._Scp.Holding;

public sealed partial class ScpHoldingSystem : SharedScpHoldingSystem
{
[Dependency] private readonly HandsSystem _hands = default!;
[Dependency] private readonly Robust.Client.Physics.PhysicsSystem _physics = default!;
[Dependency] private readonly VirtualItemSystem _virtualItem = default!;
[Dependency] private readonly IPlayerManager _player = default!;
[Dependency] private readonly IGameTiming _timing = default!;

private EntityUid? _trackedHolderTarget;
private readonly List<EntityUid> _authoritativeBlockers = [];
private readonly List<EntityUid> _predictedBlockers = [];

private EntityQuery<HandsComponent> _handsQuery;
private EntityQuery<ScpHoldableComponent> _holdableQuery;
private EntityQuery<ScpHoldHandBlockerComponent> _blockerQuery;
private EntityQuery<ActiveScpHolderComponent> _activeHolderQuery;
private EntityQuery<VirtualItemComponent> _virtualItemQuery;

public override void Initialize()
{
base.Initialize();

CommandBinds.Builder
.Bind(ContentKeyFunctions.MovePulledObject, new PointerInputCmdHandler(OnMoveHeldToCursor))
.Register<ScpHoldingSystem>();

_handsQuery = GetEntityQuery<HandsComponent>();
_holdableQuery = GetEntityQuery<ScpHoldableComponent>();
_blockerQuery = GetEntityQuery<ScpHoldHandBlockerComponent>();
_activeHolderQuery = GetEntityQuery<ActiveScpHolderComponent>();
_virtualItemQuery = GetEntityQuery<VirtualItemComponent>();

SubscribeLocalEvent<ActiveScpHoldableComponent, AfterAutoHandleStateEvent>(OnHeldAfterState);
SubscribeLocalEvent<ActiveScpHolderComponent, AfterAutoHandleStateEvent>(OnHolderAfterState);
SubscribeLocalEvent<ScpHoldHandBlockerComponent, ComponentStartup>(OnBlockerStartup);
SubscribeLocalEvent<ScpHoldHandBlockerComponent, GotEquippedHandEvent>(OnBlockerEquipped);
SubscribeLocalEvent<ActiveScpHoldableComponent, UpdateIsPredictedEvent>(OnUpdateHeldPredicted);
}

public override void Shutdown()
{
base.Shutdown();
CommandBinds.Unregister<ScpHoldingSystem>();
}

public override void Update(float frameTime)
{
base.Update(frameTime);

if (_player.LocalEntity is not { Valid: true } local)
{
UpdateTrackedLocalHeldTarget(null);
return;
}

if (!_activeHolderQuery.TryComp(local, out var localHolder))
{
UpdateTrackedLocalHeldTarget(null);
return;
}

ReconcileLocalHolderState((local, localHolder));
}

private void OnHeldAfterState(Entity<ActiveScpHoldableComponent> ent, ref AfterAutoHandleStateEvent args)
{
ReconcileHeldAfterState(ent);
}

private void OnHolderAfterState(Entity<ActiveScpHolderComponent> ent, ref AfterAutoHandleStateEvent args)
{
if (_player.LocalEntity != ent)
return;

ReconcileLocalHolderState(ent);
}

private void OnBlockerStartup(Entity<ScpHoldHandBlockerComponent> ent, ref ComponentStartup args)
{
if (!_timing.ApplyingState)
return;

ReconcileLocalHolderBlocker(ent);
}

private void OnBlockerEquipped(Entity<ScpHoldHandBlockerComponent> ent, ref GotEquippedHandEvent args)
{
if (!_timing.ApplyingState)
return;

ReconcileLocalHolderBlocker(ent, args.User);
}

private void OnUpdateHeldPredicted(Entity<ActiveScpHoldableComponent> ent, ref UpdateIsPredictedEvent args)
{
if (_player.LocalEntity is not { Valid: true } local)
return;

if (ent.Owner == local)
{
args.IsPredicted = true;
return;
}

if (_activeHolderQuery.TryComp(local, out var localHolder))
{
if (localHolder.Target == ent)
{
args.IsPredicted = true;
return;
}
}

foreach (var holder in ent.Comp.Holders)
{
if (holder != local)
continue;

args.IsPredicted = true;
return;
}

if (ent.Comp.Holders.Count > 0)
args.BlockPrediction = true;
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

private bool OnMoveHeldToCursor(ICommonSession? session, EntityCoordinates coords, EntityUid uid)
{
if (_player.LocalEntity is not { Valid: true } local)
return false;

TryMoveHeldToCursor(local, coords);
return false;
}

private void ReconcileHeldAfterState(Entity<ActiveScpHoldableComponent> held)
{
_physics.UpdateIsPredicted(held);

if (HasComp<ActiveStateScpHoldableFullHoldComponent>(held))
SyncPlaceholderHands(held);
}

protected override void UpdateHeldStates()
{
var query = EntityQueryEnumerator<ActiveScpHoldableComponent, PhysicsComponent>();
while (query.MoveNext(out var uid, out var held, out var physics))
{
if (!physics.Predict)
continue;

_physics.UpdateIsPredicted(uid);
UpdateHeld((uid, held));
}
}

protected override void OnHeldStateShutdown(Entity<ActiveScpHoldableComponent> held)
{
_physics.UpdateIsPredicted(held);
}

private void ReconcileLocalHolderBlocker(EntityUid blocker, EntityUid? holderUid = null)
{
holderUid ??= _player.LocalEntity;

if (holderUid is not { Valid: true } holder)
return;

if (!_activeHolderQuery.TryComp(holder, out var activeHolder))
return;

if (activeHolder.Target == null)
return;

if (!_virtualItemQuery.TryComp(blocker, out var virtualItem))
return;

if (virtualItem.BlockingEntity != activeHolder.Target.Value)
return;

if (!_handsQuery.TryComp(holder, out var hands))
return;

if (!_hands.IsHolding((holder, hands), blocker))
return;

ReconcileLocalHolderState((holder, activeHolder));
}

private void ReconcileLocalHolderState(Entity<ActiveScpHolderComponent> holder)
{
UpdateTrackedLocalHeldTarget(holder, holder.Comp.Target);
ReconcileLocalHolderBlockerSteadyState(holder);
}

private void ReconcileLocalHolderBlockerSteadyState(Entity<ActiveScpHolderComponent> holder)
{
if (holder.Comp.Target == null)
return;

if (!_handsQuery.TryComp(holder, out var hands))
return;

var target = holder.Comp.Target.Value;
if (!_holdableQuery.TryComp(target, out var holdable))
return;

var requiredHolderHandCount = GetRequiredHolderHandCount(holdable);
_authoritativeBlockers.Clear();
_predictedBlockers.Clear();

Entity<HandsComponent?> holderHands = (holder, hands);

foreach (var heldItem in _hands.EnumerateHeld(holderHands))
{
if (!_virtualItemQuery.TryComp(heldItem, out var virtualItem))
continue;

if (virtualItem.BlockingEntity != target)
continue;

if (!IsClientSide(heldItem))
{
_authoritativeBlockers.Add(heldItem);
continue;
}

if (!_blockerQuery.HasComp(heldItem))
continue;

_predictedBlockers.Add(heldItem);
}

var requiredPredictedBlockerCount = Math.Max(requiredHolderHandCount - _authoritativeBlockers.Count, 0);
for (var i = requiredPredictedBlockerCount; i < _predictedBlockers.Count; i++)
{
QueueDel(_predictedBlockers[i]);
}

if (_timing.ApplyingState)
{
return;
}

var currentPredictedBlockerCount = Math.Min(_predictedBlockers.Count, requiredPredictedBlockerCount);
while (currentPredictedBlockerCount < requiredPredictedBlockerCount)
{
if (!_hands.TryGetEmptyHand(holderHands, out var emptyHand))
break;

if (!_virtualItem.TrySpawnVirtualItem(target, holder, out var spawnedVirtualItem))
break;

EnsureComp<ScpHoldHandBlockerComponent>(spawnedVirtualItem.Value);
_hands.DoPickup(holder, emptyHand, spawnedVirtualItem.Value, hands);
currentPredictedBlockerCount++;
}
}

private void UpdateTrackedLocalHeldTarget(EntityUid? currentTarget, EntityUid? previousTarget = null)
{
if (_trackedHolderTarget == currentTarget)
return;

previousTarget ??= _trackedHolderTarget;

if (previousTarget != null)
_physics.UpdateIsPredicted(previousTarget.Value);

_trackedHolderTarget = currentTarget;

if (_trackedHolderTarget != null)
_physics.UpdateIsPredicted(_trackedHolderTarget.Value);
}

private void UpdateTrackedLocalHeldTarget(EntityUid holderUid, EntityUid? currentTarget, EntityUid? previousTarget = null)
{
if (_player.LocalEntity != holderUid)
return;

UpdateTrackedLocalHeldTarget(currentTarget, previousTarget);
}
}

This file was deleted.

9 changes: 9 additions & 0 deletions Content.Server/Movement/Systems/PullController.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
using System.Numerics;
using Content.Server._Scp.Holding;
using Content.Shared._Scp.Holding.Components;
using Content.Shared._Scp.Holding.Systems;
Comment thread
ThereDrD0 marked this conversation as resolved.
using Content.Server.Movement.Components;
using Content.Server.Physics.Controllers;
using Content.Shared.ActionBlocker;
Expand Down Expand Up @@ -59,6 +62,7 @@ public sealed class PullController : VirtualController
[Dependency] private readonly ActionBlockerSystem _actionBlockerSystem = default!;
[Dependency] private readonly SharedContainerSystem _container = default!;
[Dependency] private readonly SharedGravitySystem _gravity = default!;
[Dependency] private readonly ScpHoldingSystem _scpHolding = default!;
[Dependency] private readonly SharedTransformSystem _transformSystem = default!;

/// <summary>
Expand Down Expand Up @@ -118,6 +122,11 @@ private bool OnRequestMovePulledObject(ICommonSession? session, EntityCoordinate
return false;
}

// Fire edit start - route cursor-move through ScpHolding before vanilla pulling handles the same input.
if (_scpHolding.TryMoveHeldToCursor(player, coords))
return false;
// Fire edit end
Comment thread
ThereDrD0 marked this conversation as resolved.

if (!_pullerQuery.TryComp(player, out var pullerComp))
return false;

Expand Down
Loading
Loading