Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
185 changes: 185 additions & 0 deletions Content.Client/_Scp/Holding/ScpHoldingPredictionSystem.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
using System;
using Content.Shared._Scp.Holding;
using Content.Shared.Hands;
using Content.Shared.Hands.Components;
using Content.Shared.Hands.EntitySystems;
using Content.Shared.Inventory.VirtualItem;
using Robust.Client.Physics;
using Robust.Client.Player;
using Robust.Shared.Timing;

namespace Content.Client._Scp.Holding;

public sealed class ScpHoldingPredictionSystem : EntitySystem
{
[Dependency] private readonly SharedScpHoldingSystem _holding = default!;
[Dependency] private readonly SharedHandsSystem _hands = default!;
[Dependency] private readonly Robust.Client.Physics.PhysicsSystem _physics = default!;
[Dependency] private readonly IPlayerManager _player = default!;
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly SharedVirtualItemSystem _virtualItem = default!;

private static readonly TimeSpan BlockerRespawnSuppressionDuration = TimeSpan.FromSeconds(0.5);

private EntityUid? _suppressedHolder;
private EntityUid? _suppressedTarget;
private TimeSpan _suppressedUntil;
Comment on lines +22 to +26
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Не привязывайте suppression blocker'а к фиксированным 500 мс.

При RTT выше 500 мс _suppressedUntil истечёт раньше, чем приедет authoritative состояние holder'а, и виртуальный blocker снова начнёт респавниться/мигать на клиенте. Здесь безопаснее держать suppression до смены ScpHolderComponent.Target или удаления ScpHolderComponent, а не по wall-clock таймауту.

Also applies to: 48-50, 141-160

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Content.Client/_Scp/Holding/ScpHoldingPredictionSystem.cs` around lines 22 -
26, Replace the fixed 500ms timeout suppression with state-based suppression:
remove BlockerRespawnSuppressionDuration and the use of _suppressedUntil and any
time comparisons, and instead keep suppression until the authoritative
ScpHolderComponent for the suppressed holder either changes its Target or the
component is removed; implement this by storing _suppressedHolder and
_suppressedTarget when you start suppression and, where the code currently
checks _suppressedUntil, instead query the current ScpHolderComponent for that
holder (or its existence) and clear suppression only when
ScpHolderComponent.Target is different from _suppressedTarget or the component
no longer exists. Ensure all locations that reference
BlockerRespawnSuppressionDuration or _suppressedUntil (including the code paths
around the logic that spawns/filters the virtual blocker) are updated to use the
new state-based check.


private EntityQuery<HandsComponent> _handsQuery;
private EntityQuery<ScpHolderComponent> _holderQuery;

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

_handsQuery = GetEntityQuery<HandsComponent>();
_holderQuery = GetEntityQuery<ScpHolderComponent>();

SubscribeLocalEvent<ScpHeldComponent, AfterAutoHandleStateEvent>(OnHeldAfterState);
SubscribeLocalEvent<ScpHoldHandBlockerComponent, GotUnequippedHandEvent>(OnBlockerUnequipped);
SubscribeLocalEvent<ScpHolderComponent, AfterAutoHandleStateEvent>(OnHolderAfterState);
SubscribeLocalEvent<ScpHeldComponent, UpdateIsPredictedEvent>(OnUpdateHeldPredicted);
}

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

if (_timing.CurTime >= _suppressedUntil)
ClearBlockerRespawnSuppression();

var query = EntityQueryEnumerator<ScpHeldComponent>();
while (query.MoveNext(out var uid, out _))
{
_physics.UpdateIsPredicted(uid);
}

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

if (ShouldSuppressBlockerRespawn(local, _suppressedTarget))
DeleteSuppressedBlockers(local, _suppressedTarget!.Value);

if (!_holderQuery.TryComp(local, out var localHolder))
return;

if (ShouldSuppressBlockerRespawn(local, localHolder.Target))
{
DeleteSuppressedBlockers(local, localHolder.Target!.Value);
return;
}

_holding.RefreshHolderState((local, localHolder));
}

private void OnHeldAfterState(Entity<ScpHeldComponent> ent, ref AfterAutoHandleStateEvent args)
{
_holding.RefreshHeldState(ent);
}

private void OnHolderAfterState(Entity<ScpHolderComponent> ent, ref AfterAutoHandleStateEvent args)
{
if (ShouldSuppressBlockerRespawn(ent.Owner, ent.Comp.Target))
{
DeleteSuppressedBlockers(ent.Owner, ent.Comp.Target!.Value);
return;
}

_holding.RefreshHolderState(ent);
}

private void OnBlockerUnequipped(Entity<ScpHoldHandBlockerComponent> ent, ref GotUnequippedHandEvent args)
{
if (_player.LocalEntity != args.User)
return;

if (_holderQuery.TryComp(args.User, out var holder))
{
if (holder.Target == ent.Comp.Target)
return;
}

SuppressBlockerRespawn(args.User, ent.Comp.Target);
}

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

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

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

for (var i = 0; i < ent.Comp.Holders.Count; i++)
{
if (ent.Comp.Holders[i] != local)
continue;

args.IsPredicted = true;
return;
}

if (ent.Comp.Holders.Count > 0)
args.BlockPrediction = true;
}

private void SuppressBlockerRespawn(EntityUid holder, EntityUid target)
{
_suppressedHolder = holder;
_suppressedTarget = target;
_suppressedUntil = _timing.CurTime + BlockerRespawnSuppressionDuration;
}

private void ClearBlockerRespawnSuppression()
{
_suppressedHolder = null;
_suppressedTarget = null;
_suppressedUntil = TimeSpan.Zero;
}

private bool ShouldSuppressBlockerRespawn(EntityUid holder, EntityUid? target)
{
return _suppressedHolder == holder &&
_suppressedTarget != null &&
target == _suppressedTarget &&
_timing.CurTime < _suppressedUntil;
}

private void DeleteSuppressedBlockers(EntityUid holder, EntityUid target)
{
if (!_handsQuery.TryComp(holder, out var hands))
return;

foreach (var heldItem in _hands.EnumerateHeld((holder, hands)))
{
if (!TryComp<VirtualItemComponent>(heldItem, out var virtualItem))
continue;

if (!TryComp<ScpHoldHandBlockerComponent>(heldItem, out var blocker))
continue;

if (virtualItem.BlockingEntity != target)
continue;

if (blocker.Target != target)
continue;

_virtualItem.DeleteVirtualItem((heldItem, virtualItem), holder);
}
}
}
Loading
Loading