From 83a907e712ed8f383b15720b58ab8999f570924c Mon Sep 17 00:00:00 2001 From: drdth Date: Tue, 7 Apr 2026 18:18:42 +0300 Subject: [PATCH 01/27] fix: scp096 stuck --- .../Interaction/SharedInteractionSystem.Blocking.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Content.Shared/Interaction/SharedInteractionSystem.Blocking.cs b/Content.Shared/Interaction/SharedInteractionSystem.Blocking.cs index ada0542c4f7..fa432ffd254 100644 --- a/Content.Shared/Interaction/SharedInteractionSystem.Blocking.cs +++ b/Content.Shared/Interaction/SharedInteractionSystem.Blocking.cs @@ -41,11 +41,16 @@ private void CancelUseEvent(Entity ent, ref UseAttemptEv private void OnMoveAttempt(EntityUid uid, BlockMovementComponent component, UpdateCanMoveEvent args) { + // Fire edit start - do not let a blocker cancel its own shutdown refresh + if (component.LifeStage > ComponentLifeStage.Running) + return; + // If we're relaying then don't cancel. if (HasComp(uid)) return; args.Cancel(); // no more scurrying around + // Fire edit end } private void CancellableInteractEvent(EntityUid uid, BlockMovementComponent component, CancellableEntityEventArgs args) From 349160b0d79422d8229bee6b7e6489dee6cca13c Mon Sep 17 00:00:00 2001 From: drdth Date: Tue, 7 Apr 2026 22:05:06 +0300 Subject: [PATCH 02/27] add: multi pulling v1 --- .../Holding/ScpHoldingPredictionSystem.cs | 84 + .../Tests/_Scp/ScpHoldingTest.cs | 1407 +++++++++++++++++ .../_Scp/Holding/ScpHeldComponent.cs | 111 ++ .../_Scp/Holding/ScpHoldComponent.cs | 67 + .../Holding/ScpHoldHandBlockerComponent.cs | 17 + .../_Scp/Holding/ScpHoldImmuneComponent.cs | 18 + .../_Scp/Holding/ScpHolderComponent.cs | 35 + .../_Scp/Holding/ScpHoldingEvents.cs | 15 + .../_Scp/Holding/SharedScpHoldingSystem.cs | 1004 ++++++++++++ .../_prototypes/_scp/actions/holding.ftl | 4 + .../en-US/_strings/_scp/holding/holding.ftl | 11 + .../_prototypes/_scp/actions/holding.ftl | 4 + .../ru-RU/_strings/_scp/holding/holding.ftl | 11 + Resources/Prototypes/_Scp/Actions/holding.yml | 32 + Resources/Prototypes/_Scp/Alerts/holding.yml | 8 + .../_Scp/Entities/StatusEffects/holding.yml | 5 + 16 files changed, 2833 insertions(+) create mode 100644 Content.Client/_Scp/Holding/ScpHoldingPredictionSystem.cs create mode 100644 Content.IntegrationTests/Tests/_Scp/ScpHoldingTest.cs create mode 100644 Content.Shared/_Scp/Holding/ScpHeldComponent.cs create mode 100644 Content.Shared/_Scp/Holding/ScpHoldComponent.cs create mode 100644 Content.Shared/_Scp/Holding/ScpHoldHandBlockerComponent.cs create mode 100644 Content.Shared/_Scp/Holding/ScpHoldImmuneComponent.cs create mode 100644 Content.Shared/_Scp/Holding/ScpHolderComponent.cs create mode 100644 Content.Shared/_Scp/Holding/ScpHoldingEvents.cs create mode 100644 Content.Shared/_Scp/Holding/SharedScpHoldingSystem.cs create mode 100644 Resources/Locale/en-US/_prototypes/_scp/actions/holding.ftl create mode 100644 Resources/Locale/en-US/_strings/_scp/holding/holding.ftl create mode 100644 Resources/Locale/ru-RU/_prototypes/_scp/actions/holding.ftl create mode 100644 Resources/Locale/ru-RU/_strings/_scp/holding/holding.ftl create mode 100644 Resources/Prototypes/_Scp/Actions/holding.yml create mode 100644 Resources/Prototypes/_Scp/Alerts/holding.yml create mode 100644 Resources/Prototypes/_Scp/Entities/StatusEffects/holding.yml diff --git a/Content.Client/_Scp/Holding/ScpHoldingPredictionSystem.cs b/Content.Client/_Scp/Holding/ScpHoldingPredictionSystem.cs new file mode 100644 index 00000000000..16dd4527fed --- /dev/null +++ b/Content.Client/_Scp/Holding/ScpHoldingPredictionSystem.cs @@ -0,0 +1,84 @@ +using Content.Shared._Scp.Holding; +using Robust.Client.Physics; +using Robust.Client.Player; + +namespace Content.Client._Scp.Holding; + +public sealed class ScpHoldingPredictionSystem : EntitySystem +{ + [Dependency] private readonly SharedScpHoldingSystem _holding = default!; + [Dependency] private readonly Robust.Client.Physics.PhysicsSystem _physics = default!; + [Dependency] private readonly IPlayerManager _player = default!; + + private EntityQuery _holderQuery; + + public override void Initialize() + { + base.Initialize(); + + _holderQuery = GetEntityQuery(); + + SubscribeLocalEvent(OnHeldAfterState); + SubscribeLocalEvent(OnHolderAfterState); + SubscribeLocalEvent(OnUpdateHeldPredicted); + } + + public override void Update(float frameTime) + { + base.Update(frameTime); + + var query = EntityQueryEnumerator(); + while (query.MoveNext(out var uid, out _)) + { + _physics.UpdateIsPredicted(uid); + } + + if (_player.LocalEntity is not { Valid: true } local || + !_holderQuery.TryComp(local, out var localHolder)) + { + return; + } + + _holding.RefreshHolderState((local, localHolder)); + } + + private void OnHeldAfterState(Entity ent, ref AfterAutoHandleStateEvent args) + { + _holding.RefreshHeldState(ent); + } + + private void OnHolderAfterState(Entity ent, ref AfterAutoHandleStateEvent args) + { + _holding.RefreshHolderState(ent); + } + + private void OnUpdateHeldPredicted(Entity 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) && 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; + } +} diff --git a/Content.IntegrationTests/Tests/_Scp/ScpHoldingTest.cs b/Content.IntegrationTests/Tests/_Scp/ScpHoldingTest.cs new file mode 100644 index 00000000000..159aee5b402 --- /dev/null +++ b/Content.IntegrationTests/Tests/_Scp/ScpHoldingTest.cs @@ -0,0 +1,1407 @@ +#nullable enable +using System; +using System.Linq; +using System.Numerics; +using System.Reflection; +using Content.Shared.Alert; +using Content.Server.Body.Systems; +using Content.Shared._Scp.Holding; +using Content.Shared.Actions; +using Content.Shared.Actions.Components; +using Content.Shared.Body.Components; +using Content.Shared.Body.Part; +using Content.Shared.Hands.Components; +using Content.Shared.Hands.EntitySystems; +using Content.Shared.Interaction.Components; +using Content.Shared.Inventory.VirtualItem; +using Content.Shared.Movement.Components; +using Content.Shared.Movement.Events; +using Content.Shared.Movement.Pulling.Components; +using Content.Shared.Movement.Systems; +using Content.Shared.StatusEffectNew; +using Robust.Server.Console; +using Robust.Client.Physics; +using Robust.Shared.GameObjects; +using Robust.Shared.Map; +using Robust.Shared.Maths; +using Robust.Shared.Physics.Components; +using Robust.Shared.Physics.Systems; +using Robust.Shared.Prototypes; +using Robust.Shared.Timing; + +namespace Content.IntegrationTests.Tests._Scp; + +[TestFixture] +public sealed class ScpHoldingTest +{ + private const string HolderPrototype = "ScpHoldingTestHolder"; + private static readonly FieldInfo SoftEscapeAvailableAtField = + typeof(ScpHeldComponent).GetField(nameof(ScpHeldComponent.SoftEscapeAvailableAt))!; + + [TestPrototypes] + private const string Prototypes = """ +- type: entity + id: ScpHoldingTestHolder + parent: MobHuman + components: + - type: ScpHold +"""; + + [Test] + public async Task SoftHoldBreakoutByMovementAndActionRespectsCooldown() + { + await using var pair = await PoolManager.GetServerClient(new PoolSettings { Fresh = true }); + var server = pair.Server; + var entMan = server.EntMan; + var alerts = server.System(); + var timing = server.ResolveDependency(); + var statusEffects = server.System(); + var proto = server.ResolveDependency(); + var actions = server.System(); + var holding = server.System(); + var map = await pair.CreateTestMap(); + + EntityUid holder = default; + EntityUid target = default; + + await server.WaitPost(() => + { + holder = entMan.SpawnEntity(HolderPrototype, map.GridCoords); + target = entMan.SpawnEntity("MobHuman", map.GridCoords); + StartHold(entMan, holding, holder, target); + }); + + await server.WaitAssertion(() => + { + var held = entMan.GetComponent(target); + Assert.Multiple(() => + { + Assert.That(held.FullHold, Is.False); + Assert.That(held.Holders, Has.Count.EqualTo(1)); + Assert.That(held.BreakoutActionEntity, Is.Not.Null); + Assert.That(statusEffects.HasStatusEffect(target, "StatusEffectScpHeld"), Is.True); + Assert.That(alerts.IsShowingAlert(target, "ScpHoldGrabbed"), Is.True); + }); + }); + + await server.WaitPost(() => RaiseMoveInput(entMan, target)); + await server.WaitRunTicks(2); + + await server.WaitAssertion(() => + { + Assert.That(entMan.HasComponent(target), Is.False); + }); + + await server.WaitPost(() => + { + StartHold(entMan, holding, holder, target); + var held = entMan.GetComponent(target); + SetSoftEscapeAvailableAt(held, timing.CurTime + TimeSpan.FromSeconds(1)); + }); + + await server.WaitPost(() => RaiseMoveInput(entMan, target)); + await server.WaitRunTicks(2); + + await server.WaitAssertion(() => + { + Assert.That(entMan.HasComponent(target), Is.True); + }); + + await server.WaitPost(() => + { + var alert = proto.Index("ScpHoldGrabbed"); + Assert.That(alerts.ActivateAlert(target, alert), Is.True); + }); + await server.WaitRunTicks(2); + + await server.WaitAssertion(() => + { + Assert.That(entMan.HasComponent(target), Is.True); + }); + + await server.WaitPost(() => + { + var held = entMan.GetComponent(target); + var action = actions.GetAction(held.BreakoutActionEntity); + var targetActions = entMan.GetComponent(target); + + Assert.That(action, Is.Not.Null); + actions.PerformAction((target, targetActions), action!.Value); + }); + await server.WaitRunTicks(2); + + await server.WaitAssertion(() => + { + Assert.That(entMan.HasComponent(target), Is.True); + }); + + await server.WaitPost(() => + { + var held = entMan.GetComponent(target); + SetSoftEscapeAvailableAt(held, timing.CurTime); + var alert = proto.Index("ScpHoldGrabbed"); + Assert.That(alerts.ActivateAlert(target, alert), Is.True); + }); + await server.WaitRunTicks(2); + + await server.WaitAssertion(() => + { + Assert.That(entMan.HasComponent(target), Is.False); + }); + + await pair.CleanReturnAsync(); + } + + [Test] + public async Task SoftHoldUsesCustomDragAndLeavesVanillaPullIdle() + { + await using var pair = await PoolManager.GetServerClient(new PoolSettings + { + Fresh = true, + Connected = true, + DummyTicker = false, + }); + + var server = pair.Server; + var client = pair.Client; + var sEntMan = server.EntMan; + var cEntMan = client.EntMan; + var sPhysics = server.System(); + var cPhysics = client.System(); + var sTransform = server.System(); + var cTransform = client.System(); + var sAlerts = server.System(); + var cAlerts = client.System(); + var sStatusEffects = server.System(); + var cStatusEffects = client.System(); + var sHandsSystem = server.System(); + var cHandsSystem = client.System(); + var holding = server.System(); + var map = await pair.CreateTestMap(); + + EntityUid holder = default; + EntityUid target = default; + + await server.WaitPost(() => + { + holder = sEntMan.SpawnEntity(HolderPrototype, map.GridCoords); + target = sEntMan.SpawnEntity("MobHuman", map.GridCoords.Offset(new Vector2(0.1f, 0f))); + StartHold(sEntMan, holding, holder, target); + }); + + await pair.RunTicksSync(10); + await pair.SyncTicks(targetDelta: 1); + + await server.WaitAssertion(() => + { + var held = sEntMan.GetComponent(target); + var holderState = sEntMan.GetComponent(holder); + var holderSpeed = sEntMan.GetComponent(holder); + var holderHands = sEntMan.GetComponent(holder); + var puller = sEntMan.GetComponent(holder); + var pullable = sEntMan.GetComponent(target); + var move = new UpdateCanMoveEvent(target); + var distance = GetDistance(sTransform, holder, target); + var contacts = sPhysics.GetContactingEntities(holder); + + sEntMan.EventBus.RaiseLocalEvent(target, move); + + Assert.Multiple(() => + { + Assert.That(held.FullHold, Is.False); + Assert.That(held.PrimaryHolder, Is.EqualTo(holder)); + Assert.That(held.Holders, Has.Count.EqualTo(1)); + Assert.That(move.Cancelled, Is.False); + Assert.That(distance, Is.GreaterThan(0.18f)); + Assert.That(distance, Is.LessThan(0.7f)); + Assert.That(contacts, Does.Not.Contain(target)); + Assert.That(holderState.SlowdownEnabled, Is.True); + Assert.That(holderSpeed.WalkSpeedModifier, Is.LessThan(0.6f)); + Assert.That(holderSpeed.SprintSpeedModifier, Is.LessThan(0.6f)); + Assert.That(puller.Pulling, Is.Null); + Assert.That(pullable.Puller, Is.Null); + Assert.That(CountHolderHandBlockers(sEntMan, sHandsSystem, holder, target, holderHands), Is.EqualTo(1)); + Assert.That(CountHolderTargetVirtualItems(sEntMan, sHandsSystem, holder, target, holderHands), Is.EqualTo(1)); + Assert.That(sStatusEffects.HasStatusEffect(target, "StatusEffectScpHeld"), Is.True); + Assert.That(sAlerts.IsShowingAlert(target, "ScpHoldGrabbed"), Is.True); + }); + }); + + EntityUid clientHolder = default; + EntityUid clientTarget = default; + await client.WaitAssertion(() => + { + clientHolder = ToClientEntity(sEntMan, cEntMan, holder); + clientTarget = ToClientEntity(sEntMan, cEntMan, target); + + var held = cEntMan.GetComponent(clientTarget); + var holderState = cEntMan.GetComponent(clientHolder); + var holderSpeed = cEntMan.GetComponent(clientHolder); + var holderHands = cEntMan.GetComponent(clientHolder); + var puller = cEntMan.GetComponent(clientHolder); + var pullable = cEntMan.GetComponent(clientTarget); + var distance = GetDistance(cTransform, clientHolder, clientTarget); + var contacts = cPhysics.GetContactingEntities(clientHolder); + + Assert.Multiple(() => + { + Assert.That(held.FullHold, Is.False); + Assert.That(held.PrimaryHolder, Is.EqualTo(clientHolder)); + Assert.That(held.Holders, Has.Count.EqualTo(1)); + Assert.That(distance, Is.GreaterThan(0.18f)); + Assert.That(distance, Is.LessThan(0.55f)); + Assert.That(contacts, Does.Not.Contain(clientTarget)); + Assert.That(holderState.SlowdownEnabled, Is.True); + Assert.That(holderSpeed.WalkSpeedModifier, Is.LessThan(0.6f)); + Assert.That(holderSpeed.SprintSpeedModifier, Is.LessThan(0.6f)); + Assert.That(puller.Pulling, Is.Null); + Assert.That(pullable.Puller, Is.Null); + Assert.That(CountHolderHandBlockers(cEntMan, cHandsSystem, clientHolder, clientTarget, holderHands), Is.EqualTo(1)); + Assert.That(CountHolderTargetVirtualItems(cEntMan, cHandsSystem, clientHolder, clientTarget, holderHands), Is.EqualTo(1)); + Assert.That(cStatusEffects.HasStatusEffect(clientTarget, "StatusEffectScpHeld"), Is.True); + }); + }); + + var serverDistanceSamples = new float[24]; + var clientDistanceSamples = new float[24]; + await server.WaitPost(() => + { + var holderPhysics = sEntMan.GetComponent(holder); + sPhysics.SetLinearVelocity(holder, new Vector2(4f, 0f), body: holderPhysics); + }); + + for (var i = 0; i < 16; i++) + { + var sampleIndex = i; + await pair.RunTicksSync(1); + await pair.SyncTicks(targetDelta: 1); + await server.WaitPost(() => serverDistanceSamples[sampleIndex] = GetDistance(sTransform, holder, target)); + await client.WaitPost(() => clientDistanceSamples[sampleIndex] = GetDistance(cTransform, clientHolder, clientTarget)); + } + + await server.WaitPost(() => + { + var holderPhysics = sEntMan.GetComponent(holder); + sPhysics.SetLinearVelocity(holder, Vector2.Zero, body: holderPhysics); + }); + + for (var i = 16; i < serverDistanceSamples.Length; i++) + { + var sampleIndex = i; + await pair.RunTicksSync(1); + await pair.SyncTicks(targetDelta: 1); + await server.WaitPost(() => serverDistanceSamples[sampleIndex] = GetDistance(sTransform, holder, target)); + await client.WaitPost(() => clientDistanceSamples[sampleIndex] = GetDistance(cTransform, clientHolder, clientTarget)); + } + + Assert.Multiple(() => + { + Assert.That(serverDistanceSamples.Max(), Is.LessThan(0.6f)); + Assert.That(serverDistanceSamples.Min(), Is.GreaterThan(0.16f)); + Assert.That(GetLargestDistanceStep(serverDistanceSamples), Is.LessThan(0.2f)); + Assert.That(serverDistanceSamples[^1], Is.GreaterThan(0.18f)); + Assert.That(serverDistanceSamples[^1], Is.LessThan(0.4f)); + + Assert.That(clientDistanceSamples.Max(), Is.LessThan(0.6f)); + Assert.That(clientDistanceSamples.Min(), Is.GreaterThan(0.16f)); + Assert.That(GetLargestDistanceStep(clientDistanceSamples), Is.LessThan(0.2f)); + Assert.That(clientDistanceSamples[^1], Is.GreaterThan(0.18f)); + Assert.That(clientDistanceSamples[^1], Is.LessThan(0.4f)); + }); + + await server.WaitAssertion(() => + { + Assert.That(sEntMan.HasComponent(target), Is.True); + }); + + await client.WaitAssertion(() => + { + Assert.That(cEntMan.HasComponent(clientTarget), Is.True); + }); + + await server.WaitPost(() => + { + var targetCoords = sEntMan.GetComponent(target).Coordinates; + sTransform.SetCoordinates(holder, targetCoords); + }); + + await pair.RunTicksSync(10); + await pair.SyncTicks(targetDelta: 1); + + await server.WaitAssertion(() => + { + var distance = GetDistance(sTransform, holder, target); + var contacts = sPhysics.GetContactingEntities(holder); + + Assert.Multiple(() => + { + Assert.That(distance, Is.GreaterThan(0.16f)); + Assert.That(distance, Is.LessThan(0.55f)); + Assert.That(contacts, Does.Not.Contain(target)); + }); + }); + + await client.WaitAssertion(() => + { + var distance = GetDistance(cTransform, clientHolder, clientTarget); + var contacts = cPhysics.GetContactingEntities(clientHolder); + + Assert.Multiple(() => + { + Assert.That(distance, Is.GreaterThan(0.16f)); + Assert.That(distance, Is.LessThan(0.55f)); + Assert.That(contacts, Does.Not.Contain(clientTarget)); + }); + }); + + await pair.CleanReturnAsync(); + } + + [Test] + public async Task SecondHolderEntersFullHoldAndFillsHands() + { + await using var pair = await PoolManager.GetServerClient(new PoolSettings { Fresh = true }); + var server = pair.Server; + var entMan = server.EntMan; + var holding = server.System(); + var handsSystem = server.System(); + var map = await pair.CreateTestMap(); + + EntityUid holderOne = default; + EntityUid holderTwo = default; + EntityUid target = default; + + await server.WaitPost(() => + { + holderOne = entMan.SpawnEntity(HolderPrototype, map.GridCoords); + holderTwo = entMan.SpawnEntity(HolderPrototype, map.GridCoords); + target = entMan.SpawnEntity("MobHuman", map.GridCoords); + + StartHold(entMan, holding, holderOne, target); + StartHold(entMan, holding, holderTwo, target); + }); + + await server.WaitAssertion(() => + { + var held = entMan.GetComponent(target); + var hands = entMan.GetComponent(target); + var holderOnePuller = entMan.GetComponent(holderOne); + var holderTwoPuller = entMan.GetComponent(holderTwo); + var pullable = entMan.GetComponent(target); + var move = new UpdateCanMoveEvent(target); + entMan.EventBus.RaiseLocalEvent(target, move); + + Assert.Multiple(() => + { + Assert.That(held.FullHold, Is.True); + Assert.That(held.RequiredHolderCount, Is.EqualTo(2)); + Assert.That(held.Holders, Has.Count.EqualTo(2)); + Assert.That(move.Cancelled, Is.True); + Assert.That(CountBlockingVirtualHands(entMan, handsSystem, target, hands), Is.EqualTo(hands.SortedHands.Count)); + Assert.That(holderOnePuller.Pulling, Is.Null); + Assert.That(holderTwoPuller.Pulling, Is.Null); + Assert.That(pullable.Puller, Is.Null); + }); + }); + + await pair.CleanReturnAsync(); + } + + [Test] + public async Task MultiHandTargetNeedsMatchingHolderCountAndResyncsOnHandLoss() + { + await using var pair = await PoolManager.GetServerClient(new PoolSettings { Fresh = true }); + var server = pair.Server; + var entMan = server.EntMan; + var host = server.ResolveDependency(); + var holding = server.System(); + var handsSystem = server.System(); + var transform = server.System(); + var bodySystem = server.System(); + var map = await pair.CreateTestMap(); + + EntityUid holderOne = default; + EntityUid holderTwo = default; + EntityUid holderThree = default; + EntityUid target = default; + + await server.WaitPost(() => + { + holderOne = entMan.SpawnEntity(HolderPrototype, map.GridCoords); + holderTwo = entMan.SpawnEntity(HolderPrototype, map.GridCoords); + holderThree = entMan.SpawnEntity(HolderPrototype, map.GridCoords); + target = entMan.SpawnEntity("MobHuman", map.GridCoords); + + host.ExecuteCommand(null, $"addhand {entMan.GetNetEntity(target)}"); + }); + await server.WaitRunTicks(2); + + await server.WaitPost(() => + { + StartHold(entMan, holding, holderOne, target); + StartHold(entMan, holding, holderTwo, target); + }); + + await server.WaitAssertion(() => + { + var held = entMan.GetComponent(target); + Assert.Multiple(() => + { + Assert.That(held.RequiredHolderCount, Is.EqualTo(3)); + Assert.That(held.FullHold, Is.False); + Assert.That(held.Holders, Has.Count.EqualTo(2)); + }); + }); + + await server.WaitPost(() => + { + StartHold(entMan, holding, holderThree, target); + }); + + await server.WaitAssertion(() => + { + var held = entMan.GetComponent(target); + var hands = entMan.GetComponent(target); + + Assert.Multiple(() => + { + Assert.That(held.FullHold, Is.True); + Assert.That(held.RequiredHolderCount, Is.EqualTo(3)); + Assert.That(hands.SortedHands.Count, Is.EqualTo(3)); + Assert.That(CountBlockingVirtualHands(entMan, handsSystem, target, hands), Is.EqualTo(3)); + }); + }); + + await server.WaitPost(() => + { + var body = entMan.GetComponent(target); + var removedHand = bodySystem.GetBodyChildrenOfType(target, BodyPartType.Hand, body).First().Id; + transform.AttachToGridOrMap(removedHand); + }); + await server.WaitRunTicks(2); + + await server.WaitAssertion(() => + { + var held = entMan.GetComponent(target); + var hands = entMan.GetComponent(target); + + Assert.Multiple(() => + { + Assert.That(held.RequiredHolderCount, Is.EqualTo(2)); + Assert.That(held.FullHold, Is.True); + Assert.That(hands.SortedHands.Count, Is.EqualTo(2)); + Assert.That(CountBlockingVirtualHands(entMan, handsSystem, target, hands), Is.EqualTo(2)); + }); + }); + + await pair.CleanReturnAsync(); + } + + [Test] + public async Task FullBreakoutByMovementAppliesImmunity() + { + await using var pair = await PoolManager.GetServerClient(new PoolSettings { Fresh = true }); + var server = pair.Server; + var entMan = server.EntMan; + var timing = server.ResolveDependency(); + var holding = server.System(); + var map = await pair.CreateTestMap(); + + EntityUid holderOne = default; + EntityUid holderTwo = default; + EntityUid target = default; + + await server.WaitPost(() => + { + holderOne = entMan.SpawnEntity(HolderPrototype, map.GridCoords); + holderTwo = entMan.SpawnEntity(HolderPrototype, map.GridCoords); + target = entMan.SpawnEntity("MobHuman", map.GridCoords); + + StartHold(entMan, holding, holderOne, target); + StartHold(entMan, holding, holderTwo, target); + }); + + await server.WaitPost(() => RaiseMoveInput(entMan, target)); + await server.WaitRunTicks(2); + + await server.WaitAssertion(() => + { + var held = entMan.GetComponent(target); + Assert.Multiple(() => + { + Assert.That(held.FullHold, Is.True); + Assert.That(held.BreakoutDoAfterId, Is.Null); + }); + }); + + await server.WaitRunTicks(GetTickCount(timing, TimeSpan.FromSeconds(10))); + + await server.WaitPost(() => RaiseMoveInput(entMan, target)); + await server.WaitRunTicks(2); + + await server.WaitAssertion(() => + { + var held = entMan.GetComponent(target); + Assert.That(held.BreakoutDoAfterId, Is.Not.Null); + }); + + await server.WaitRunTicks(GetTickCount(timing, TimeSpan.FromSeconds(5))); + + await server.WaitAssertion(() => + { + Assert.Multiple(() => + { + Assert.That(entMan.HasComponent(target), Is.False); + Assert.That(entMan.HasComponent(target), Is.True); + }); + }); + + await server.WaitPost(() => + { + var holdComp = entMan.GetComponent(holderOne); + Assert.That(holding.TryToggleHold((holderOne, holdComp), target), Is.False); + }); + + await server.WaitAssertion(() => + { + Assert.That(entMan.HasComponent(target), Is.False); + }); + + await server.WaitRunTicks(GetTickCount(timing, TimeSpan.FromSeconds(5))); + + await server.WaitPost(() => + { + var holdComp = entMan.GetComponent(holderOne); + Assert.That(holding.TryToggleHold((holderOne, holdComp), target), Is.True); + }); + + await server.WaitAssertion(() => + { + Assert.That(entMan.HasComponent(target), Is.True); + }); + + await pair.CleanReturnAsync(); + } + + [Test] + public async Task FullBreakoutByActionStartsAndCompletes() + { + await using var pair = await PoolManager.GetServerClient(new PoolSettings { Fresh = true }); + var server = pair.Server; + var entMan = server.EntMan; + var timing = server.ResolveDependency(); + var actions = server.System(); + var holding = server.System(); + var map = await pair.CreateTestMap(); + + EntityUid holderOne = default; + EntityUid holderTwo = default; + EntityUid target = default; + + await server.WaitPost(() => + { + holderOne = entMan.SpawnEntity(HolderPrototype, map.GridCoords); + holderTwo = entMan.SpawnEntity(HolderPrototype, map.GridCoords); + target = entMan.SpawnEntity("MobHuman", map.GridCoords); + + StartHold(entMan, holding, holderOne, target); + StartHold(entMan, holding, holderTwo, target); + }); + + await server.WaitRunTicks(GetTickCount(timing, TimeSpan.FromSeconds(10))); + + await server.WaitPost(() => + { + var held = entMan.GetComponent(target); + var action = actions.GetAction(held.BreakoutActionEntity); + var targetActions = entMan.GetComponent(target); + + Assert.That(action, Is.Not.Null); + actions.PerformAction((target, targetActions), action!.Value); + }); + await server.WaitRunTicks(2); + + await server.WaitAssertion(() => + { + var held = entMan.GetComponent(target); + Assert.That(held.BreakoutDoAfterId, Is.Not.Null); + }); + + await server.WaitRunTicks(GetTickCount(timing, TimeSpan.FromSeconds(5))); + + await server.WaitAssertion(() => + { + Assert.That(entMan.HasComponent(target), Is.False); + }); + + await pair.CleanReturnAsync(); + } + + [Test] + public async Task ClientHoldActionPredictsSoftHoldBeforeServerAck() + { + await using var pair = await PoolManager.GetServerClient(new PoolSettings + { + Fresh = true, + Connected = true, + DummyTicker = false, + }); + + var server = pair.Server; + var client = pair.Client; + var sEntMan = server.EntMan; + var cEntMan = client.EntMan; + var sPhysics = server.System(); + var cPhysics = client.System(); + var sTransform = server.System(); + var cTransform = client.System(); + var sAlerts = server.System(); + var cAlerts = client.System(); + var sStatusEffects = server.System(); + var cStatusEffects = client.System(); + var sHandsSystem = server.System(); + var cHandsSystem = client.System(); + var map = await pair.CreateTestMap(); + + var serverPlayer = pair.Player!.AttachedEntity!.Value; + EntityUid target = default; + + await server.WaitPost(() => + { + sTransform.SetCoordinates(serverPlayer, map.GridCoords); + sEntMan.AddComponent(serverPlayer); + target = sEntMan.SpawnEntity("MobHuman", map.GridCoords.Offset(new Vector2(0.1f, 0f))); + }); + + await pair.RunTicksSync(10); + await pair.SyncTicks(targetDelta: 1); + + EntityUid holdAction = default; + await server.WaitAssertion(() => + { + var hold = sEntMan.GetComponent(serverPlayer); + Assert.That(hold.ActionEntity, Is.Not.Null); + holdAction = hold.ActionEntity!.Value; + }); + + var clientPlayer = EntityUid.Invalid; + var clientTarget = EntityUid.Invalid; + await client.WaitAssertion(() => + { + clientPlayer = client.AttachedEntity!.Value; + clientTarget = ToClientEntity(sEntMan, cEntMan, target); + + Assert.Multiple(() => + { + Assert.That(cEntMan.HasComponent(clientPlayer), Is.True); + Assert.That(cEntMan.EntityExists(clientTarget), Is.True); + }); + }); + + var holdActionNet = sEntMan.GetNetEntity(holdAction); + var targetNet = sEntMan.GetNetEntity(target); + + await client.WaitPost(() => + { + cEntMan.RaisePredictiveEvent(new RequestPerformActionEvent(holdActionNet, targetNet)); + + var held = cEntMan.GetComponent(clientTarget); + var holderHands = cEntMan.GetComponent(clientPlayer); + Assert.Multiple(() => + { + Assert.That(held.FullHold, Is.False); + Assert.That(held.Holders, Has.Count.EqualTo(1)); + Assert.That(cEntMan.HasComponent(clientPlayer), Is.True); + Assert.That(CountHolderHandBlockers(cEntMan, cHandsSystem, clientPlayer, clientTarget, holderHands), Is.EqualTo(1)); + Assert.That(CountHolderTargetVirtualItems(cEntMan, cHandsSystem, clientPlayer, clientTarget, holderHands), Is.EqualTo(1)); + Assert.That(cStatusEffects.HasStatusEffect(clientTarget, "StatusEffectScpHeld"), Is.True); + }); + }); + + await server.WaitAssertion(() => + { + Assert.That(sEntMan.HasComponent(target), Is.False); + }); + + var maxPredictedClientBlockers = 1; + for (var i = 0; i < 6; i++) + { + await pair.RunTicksSync(1); + await pair.SyncTicks(targetDelta: 1); + await client.WaitPost(() => + { + if (!cEntMan.TryGetComponent(clientPlayer, out var holderHands)) + return; + + maxPredictedClientBlockers = Math.Max(maxPredictedClientBlockers, + CountHolderTargetVirtualItems(cEntMan, cHandsSystem, clientPlayer, clientTarget, holderHands)); + }); + } + + Assert.That(maxPredictedClientBlockers, Is.EqualTo(1)); + + await pair.RunTicksSync(10); + await pair.SyncTicks(targetDelta: 1); + + await server.WaitAssertion(() => + { + var held = sEntMan.GetComponent(target); + var holderHands = sEntMan.GetComponent(serverPlayer); + var puller = sEntMan.GetComponent(serverPlayer); + var pullable = sEntMan.GetComponent(target); + var distance = GetDistance(sTransform, serverPlayer, target); + Assert.Multiple(() => + { + Assert.That(held.FullHold, Is.False); + Assert.That(held.Holders, Has.Count.EqualTo(1)); + Assert.That(held.PrimaryHolder, Is.EqualTo(serverPlayer)); + Assert.That(distance, Is.GreaterThan(0.18f)); + Assert.That(distance, Is.LessThan(0.55f)); + Assert.That(puller.Pulling, Is.Null); + Assert.That(pullable.Puller, Is.Null); + Assert.That(CountHolderHandBlockers(sEntMan, sHandsSystem, serverPlayer, target, holderHands), Is.EqualTo(1)); + Assert.That(CountHolderTargetVirtualItems(sEntMan, sHandsSystem, serverPlayer, target, holderHands), Is.EqualTo(1)); + Assert.That(sStatusEffects.HasStatusEffect(target, "StatusEffectScpHeld"), Is.True); + Assert.That(sAlerts.IsShowingAlert(target, "ScpHoldGrabbed"), Is.True); + }); + }); + + await client.WaitAssertion(() => + { + var held = cEntMan.GetComponent(clientTarget); + var holderHands = cEntMan.GetComponent(clientPlayer); + var puller = cEntMan.GetComponent(clientPlayer); + var pullable = cEntMan.GetComponent(clientTarget); + var distance = GetDistance(cTransform, clientPlayer, clientTarget); + Assert.Multiple(() => + { + Assert.That(held.FullHold, Is.False); + Assert.That(held.Holders, Has.Count.EqualTo(1)); + Assert.That(held.PrimaryHolder, Is.EqualTo(clientPlayer)); + Assert.That(distance, Is.GreaterThan(0.18f)); + Assert.That(distance, Is.LessThan(0.55f)); + Assert.That(puller.Pulling, Is.Null); + Assert.That(pullable.Puller, Is.Null); + Assert.That(CountHolderHandBlockers(cEntMan, cHandsSystem, clientPlayer, clientTarget, holderHands), Is.EqualTo(1)); + Assert.That(CountHolderTargetVirtualItems(cEntMan, cHandsSystem, clientPlayer, clientTarget, holderHands), Is.EqualTo(1)); + Assert.That(cStatusEffects.HasStatusEffect(clientTarget, "StatusEffectScpHeld"), Is.True); + }); + }); + + await pair.CleanReturnAsync(); + } + + [Test] + public async Task ClientSecondHoldActionPredictsFullHoldBeforeServerAck() + { + await using var pair = await PoolManager.GetServerClient(new PoolSettings + { + Fresh = true, + Connected = true, + DummyTicker = false, + }); + + var server = pair.Server; + var client = pair.Client; + var sEntMan = server.EntMan; + var cEntMan = client.EntMan; + var sTransform = server.System(); + var cHandsSystem = client.System(); + var holding = server.System(); + var map = await pair.CreateTestMap(); + + var serverPlayer = pair.Player!.AttachedEntity!.Value; + EntityUid holderOne = default; + EntityUid target = default; + + await server.WaitPost(() => + { + sTransform.SetCoordinates(serverPlayer, map.GridCoords); + sEntMan.AddComponent(serverPlayer); + + holderOne = sEntMan.SpawnEntity(HolderPrototype, map.GridCoords); + target = sEntMan.SpawnEntity("MobHuman", map.GridCoords.Offset(new Vector2(0.5f, 0f))); + StartHold(sEntMan, holding, holderOne, target); + }); + + await pair.RunTicksSync(10); + await pair.SyncTicks(targetDelta: 1); + + EntityUid holdAction = default; + await server.WaitAssertion(() => + { + var hold = sEntMan.GetComponent(serverPlayer); + Assert.That(hold.ActionEntity, Is.Not.Null); + holdAction = hold.ActionEntity!.Value; + + var held = sEntMan.GetComponent(target); + Assert.Multiple(() => + { + Assert.That(held.FullHold, Is.False); + Assert.That(held.Holders, Has.Count.EqualTo(1)); + }); + }); + + var clientPlayer = EntityUid.Invalid; + var clientTarget = EntityUid.Invalid; + await client.WaitAssertion(() => + { + clientPlayer = client.AttachedEntity!.Value; + clientTarget = ToClientEntity(sEntMan, cEntMan, target); + + var held = cEntMan.GetComponent(clientTarget); + Assert.Multiple(() => + { + Assert.That(held.FullHold, Is.False); + Assert.That(held.Holders, Has.Count.EqualTo(1)); + }); + }); + + var holdActionNet = sEntMan.GetNetEntity(holdAction); + var targetNet = sEntMan.GetNetEntity(target); + + await client.WaitPost(() => + { + cEntMan.RaisePredictiveEvent(new RequestPerformActionEvent(holdActionNet, targetNet)); + + var held = cEntMan.GetComponent(clientTarget); + var hands = cEntMan.GetComponent(clientTarget); + + Assert.Multiple(() => + { + Assert.That(held.FullHold, Is.True); + Assert.That(held.Holders, Has.Count.EqualTo(2)); + Assert.That(CountBlockingVirtualHands(cEntMan, cHandsSystem, clientTarget, hands), Is.EqualTo(hands.SortedHands.Count)); + }); + }); + + await server.WaitAssertion(() => + { + var held = sEntMan.GetComponent(target); + Assert.Multiple(() => + { + Assert.That(held.FullHold, Is.False); + Assert.That(held.Holders, Has.Count.EqualTo(1)); + }); + }); + + await pair.RunTicksSync(10); + await pair.SyncTicks(targetDelta: 1); + + await server.WaitAssertion(() => + { + var held = sEntMan.GetComponent(target); + Assert.Multiple(() => + { + Assert.That(held.FullHold, Is.True); + Assert.That(held.Holders, Has.Count.EqualTo(2)); + }); + }); + + await client.WaitAssertion(() => + { + var held = cEntMan.GetComponent(clientTarget); + Assert.Multiple(() => + { + Assert.That(held.FullHold, Is.True); + Assert.That(held.Holders, Has.Count.EqualTo(2)); + }); + }); + + await pair.CleanReturnAsync(); + } + + [Test] + public async Task ClientPrimaryReassignmentKeepsCustomDragAndReconcilesCleanly() + { + await using var pair = await PoolManager.GetServerClient(new PoolSettings + { + Fresh = true, + Connected = true, + DummyTicker = false, + }); + + var server = pair.Server; + var client = pair.Client; + var sEntMan = server.EntMan; + var cEntMan = client.EntMan; + var sPhysics = server.System(); + var cPhysics = client.System(); + var host = server.ResolveDependency(); + var sTransform = server.System(); + var cTransform = client.System(); + var holding = server.System(); + var map = await pair.CreateTestMap(); + + var serverPlayer = pair.Player!.AttachedEntity!.Value; + EntityUid holderTwo = default; + EntityUid target = default; + + await server.WaitPost(() => + { + sTransform.SetCoordinates(serverPlayer, map.GridCoords); + sEntMan.AddComponent(serverPlayer); + + target = sEntMan.SpawnEntity("MobHuman", map.GridCoords.Offset(new Vector2(0.95f, 0f))); + host.ExecuteCommand(null, $"addhand {sEntMan.GetNetEntity(target)}"); + + holderTwo = sEntMan.SpawnEntity(HolderPrototype, map.GridCoords.Offset(new Vector2(-0.5f, 0f))); + + StartHold(sEntMan, holding, serverPlayer, target); + StartHold(sEntMan, holding, holderTwo, target); + }); + + await pair.RunTicksSync(10); + await pair.SyncTicks(targetDelta: 1); + + await server.WaitAssertion(() => + { + var held = sEntMan.GetComponent(target); + var serverPlayerPuller = sEntMan.GetComponent(serverPlayer); + var holderTwoPuller = sEntMan.GetComponent(holderTwo); + var pullable = sEntMan.GetComponent(target); + var distance = GetDistance(sTransform, serverPlayer, target); + var contacts = sPhysics.GetContactingEntities(serverPlayer); + + Assert.Multiple(() => + { + Assert.That(held.FullHold, Is.False); + Assert.That(held.Holders, Has.Count.EqualTo(2)); + Assert.That(held.PrimaryHolder, Is.EqualTo(serverPlayer)); + Assert.That(distance, Is.GreaterThan(0.18f)); + Assert.That(distance, Is.LessThan(0.5f)); + Assert.That(contacts, Does.Not.Contain(target)); + Assert.That(serverPlayerPuller.Pulling, Is.Null); + Assert.That(holderTwoPuller.Pulling, Is.Null); + Assert.That(pullable.Puller, Is.Null); + }); + }); + + var clientPlayer = EntityUid.Invalid; + var clientTarget = EntityUid.Invalid; + var clientHolderTwo = EntityUid.Invalid; + await client.WaitAssertion(() => + { + clientPlayer = client.AttachedEntity!.Value; + clientTarget = ToClientEntity(sEntMan, cEntMan, target); + clientHolderTwo = ToClientEntity(sEntMan, cEntMan, holderTwo); + + var held = cEntMan.GetComponent(clientTarget); + var playerPuller = cEntMan.GetComponent(clientPlayer); + var holderTwoPuller = cEntMan.GetComponent(clientHolderTwo); + var pullable = cEntMan.GetComponent(clientTarget); + var distance = GetDistance(cTransform, clientPlayer, clientTarget); + var contacts = cPhysics.GetContactingEntities(clientPlayer); + + Assert.Multiple(() => + { + Assert.That(held.FullHold, Is.False); + Assert.That(held.Holders, Has.Count.EqualTo(2)); + Assert.That(held.PrimaryHolder, Is.EqualTo(clientPlayer)); + Assert.That(distance, Is.GreaterThan(0.18f)); + Assert.That(distance, Is.LessThan(0.5f)); + Assert.That(contacts, Does.Not.Contain(clientTarget)); + Assert.That(playerPuller.Pulling, Is.Null); + Assert.That(holderTwoPuller.Pulling, Is.Null); + Assert.That(pullable.Puller, Is.Null); + }); + }); + + await server.WaitPost(() => + { + var holdComp = sEntMan.GetComponent(serverPlayer); + Assert.That(holding.TryToggleHold((serverPlayer, holdComp), target), Is.True); + }); + + await pair.RunTicksSync(5); + await pair.SyncTicks(targetDelta: 1); + + await server.WaitAssertion(() => + { + var held = sEntMan.GetComponent(target); + var pullable = sEntMan.GetComponent(target); + var holderTwoPuller = sEntMan.GetComponent(holderTwo); + var distance = GetDistance(sTransform, holderTwo, target); + var contacts = sPhysics.GetContactingEntities(holderTwo); + + Assert.Multiple(() => + { + Assert.That(held.Holders, Has.Count.EqualTo(1)); + Assert.That(held.PrimaryHolder, Is.EqualTo(holderTwo)); + Assert.That(distance, Is.GreaterThan(0.18f)); + Assert.That(distance, Is.LessThan(0.55f)); + Assert.That(contacts, Does.Not.Contain(target)); + Assert.That(holderTwoPuller.Pulling, Is.Null); + Assert.That(pullable.Puller, Is.Null); + }); + }); + + await client.WaitAssertion(() => + { + var held = cEntMan.GetComponent(clientTarget); + var pullable = cEntMan.GetComponent(clientTarget); + var holderTwoPuller = cEntMan.GetComponent(clientHolderTwo); + var distance = GetDistance(cTransform, clientHolderTwo, clientTarget); + var contacts = cPhysics.GetContactingEntities(clientHolderTwo); + + Assert.Multiple(() => + { + Assert.That(held.FullHold, Is.False); + Assert.That(held.Holders, Has.Count.EqualTo(1)); + Assert.That(held.PrimaryHolder, Is.EqualTo(clientHolderTwo)); + Assert.That(distance, Is.GreaterThan(0.18f)); + Assert.That(distance, Is.LessThan(0.7f)); + Assert.That(contacts, Does.Not.Contain(clientTarget)); + Assert.That(holderTwoPuller.Pulling, Is.Null); + Assert.That(pullable.Puller, Is.Null); + }); + }); + + await pair.CleanReturnAsync(); + } + + [Test] + public async Task ClientFullBreakoutActionPredictsDoAfterAndReconciles() + { + await using var pair = await PoolManager.GetServerClient(new PoolSettings + { + Fresh = true, + Connected = true, + DummyTicker = false, + }); + + var server = pair.Server; + var client = pair.Client; + var sEntMan = server.EntMan; + var cEntMan = client.EntMan; + var timing = server.ResolveDependency(); + var sTransform = server.System(); + var holding = server.System(); + var map = await pair.CreateTestMap(); + + var serverPlayer = pair.Player!.AttachedEntity!.Value; + EntityUid holderOne = default; + EntityUid holderTwo = default; + + await server.WaitPost(() => + { + sTransform.SetCoordinates(serverPlayer, map.GridCoords); + holderOne = sEntMan.SpawnEntity(HolderPrototype, map.GridCoords.Offset(new Vector2(0.5f, 0f))); + holderTwo = sEntMan.SpawnEntity(HolderPrototype, map.GridCoords.Offset(new Vector2(-0.5f, 0f))); + StartHold(sEntMan, holding, holderOne, serverPlayer); + StartHold(sEntMan, holding, holderTwo, serverPlayer); + }); + + await pair.RunTicksSync(10); + await pair.SyncTicks(targetDelta: 1); + await pair.RunTicksSync(GetTickCount(timing, TimeSpan.FromSeconds(10))); + await pair.SyncTicks(targetDelta: 1); + + EntityUid breakoutAction = default; + await server.WaitAssertion(() => + { + var held = sEntMan.GetComponent(serverPlayer); + Assert.Multiple(() => + { + Assert.That(held.FullHold, Is.True); + Assert.That(held.BreakoutActionEntity, Is.Not.Null); + }); + + breakoutAction = held.BreakoutActionEntity!.Value; + }); + + var clientPlayer = EntityUid.Invalid; + await client.WaitAssertion(() => + { + clientPlayer = client.AttachedEntity!.Value; + var held = cEntMan.GetComponent(clientPlayer); + Assert.That(held.FullHold, Is.True); + }); + + var breakoutActionNet = sEntMan.GetNetEntity(breakoutAction); + + await client.WaitPost(() => + { + cEntMan.RaisePredictiveEvent(new RequestPerformActionEvent(breakoutActionNet)); + + var held = cEntMan.GetComponent(clientPlayer); + Assert.Multiple(() => + { + Assert.That(held.BreakoutDoAfterId, Is.Not.Null); + Assert.That(cEntMan.HasComponent(clientPlayer), Is.False); + }); + }); + + await server.WaitAssertion(() => + { + var held = sEntMan.GetComponent(serverPlayer); + Assert.Multiple(() => + { + Assert.That(held.FullHold, Is.True); + Assert.That(held.BreakoutDoAfterId, Is.Null); + }); + }); + + await pair.RunTicksSync(GetTickCount(timing, TimeSpan.FromSeconds(5)) + 5); + await pair.SyncTicks(targetDelta: 1); + + await server.WaitAssertion(() => + { + Assert.Multiple(() => + { + Assert.That(sEntMan.HasComponent(serverPlayer), Is.False); + Assert.That(sEntMan.HasComponent(serverPlayer), Is.True); + }); + }); + + await client.WaitAssertion(() => + { + Assert.Multiple(() => + { + Assert.That(cEntMan.HasComponent(clientPlayer), Is.False); + Assert.That(cEntMan.HasComponent(clientPlayer), Is.True); + }); + }); + + await pair.CleanReturnAsync(); + } + + [Test] + public async Task ClientGrabbedAlertPredictsSoftBreakout() + { + await using var pair = await PoolManager.GetServerClient(new PoolSettings + { + Fresh = true, + Connected = true, + DummyTicker = false, + }); + + var server = pair.Server; + var client = pair.Client; + var sEntMan = server.EntMan; + var cEntMan = client.EntMan; + var sTransform = server.System(); + var holding = server.System(); + var sAlerts = server.System(); + var cAlerts = client.System(); + var map = await pair.CreateTestMap(); + + var serverPlayer = pair.Player!.AttachedEntity!.Value; + EntityUid holder = default; + + await server.WaitPost(() => + { + sTransform.SetCoordinates(serverPlayer, map.GridCoords); + holder = sEntMan.SpawnEntity(HolderPrototype, map.GridCoords.Offset(new Vector2(0.1f, 0f))); + StartHold(sEntMan, holding, holder, serverPlayer); + }); + + await pair.RunTicksSync(10); + await pair.SyncTicks(targetDelta: 1); + + var clientPlayer = EntityUid.Invalid; + await client.WaitAssertion(() => + { + clientPlayer = client.AttachedEntity!.Value; + + Assert.Multiple(() => + { + Assert.That(cEntMan.HasComponent(clientPlayer), Is.True); + Assert.That(cAlerts.IsShowingAlert(clientPlayer, "ScpHoldGrabbed"), Is.True); + }); + }); + + await client.WaitPost(() => + { + cEntMan.RaisePredictiveEvent(new ClickAlertEvent("ScpHoldGrabbed")); + + Assert.Multiple(() => + { + Assert.That(cEntMan.HasComponent(clientPlayer), Is.False); + Assert.That(cAlerts.IsShowingAlert(clientPlayer, "ScpHoldGrabbed"), Is.False); + }); + }); + + await server.WaitAssertion(() => + { + Assert.That(sEntMan.HasComponent(serverPlayer), Is.True); + Assert.That(sAlerts.IsShowingAlert(serverPlayer, "ScpHoldGrabbed"), Is.True); + }); + + await pair.RunTicksSync(10); + await pair.SyncTicks(targetDelta: 1); + + await server.WaitAssertion(() => + { + Assert.That(sEntMan.HasComponent(serverPlayer), Is.False); + Assert.That(sAlerts.IsShowingAlert(serverPlayer, "ScpHoldGrabbed"), Is.False); + }); + + await client.WaitAssertion(() => + { + Assert.That(cEntMan.HasComponent(clientPlayer), Is.False); + Assert.That(cAlerts.IsShowingAlert(clientPlayer, "ScpHoldGrabbed"), Is.False); + }); + + await pair.CleanReturnAsync(); + } + + [Test] + public async Task ReleaseAndRangeLossReassignOrClearHold() + { + await using var pair = await PoolManager.GetServerClient(new PoolSettings { Fresh = true }); + var server = pair.Server; + var entMan = server.EntMan; + var host = server.ResolveDependency(); + var holding = server.System(); + var transform = server.System(); + var map = await pair.CreateTestMap(); + + EntityUid holderOne = default; + EntityUid holderTwo = default; + EntityUid target = default; + + await server.WaitPost(() => + { + holderOne = entMan.SpawnEntity(HolderPrototype, map.GridCoords); + holderTwo = entMan.SpawnEntity(HolderPrototype, map.GridCoords); + target = entMan.SpawnEntity("MobHuman", map.GridCoords); + + host.ExecuteCommand(null, $"addhand {entMan.GetNetEntity(target)}"); + }); + await server.WaitRunTicks(2); + + await server.WaitPost(() => + { + StartHold(entMan, holding, holderOne, target); + StartHold(entMan, holding, holderTwo, target); + }); + + await server.WaitAssertion(() => + { + var held = entMan.GetComponent(target); + var holderOnePuller = entMan.GetComponent(holderOne); + var holderTwoPuller = entMan.GetComponent(holderTwo); + var pullable = entMan.GetComponent(target); + + Assert.Multiple(() => + { + Assert.That(held.FullHold, Is.False); + Assert.That(held.PrimaryHolder, Is.EqualTo(holderOne)); + Assert.That(holderOnePuller.Pulling, Is.Null); + Assert.That(holderTwoPuller.Pulling, Is.Null); + Assert.That(pullable.Puller, Is.Null); + }); + }); + + await server.WaitPost(() => + { + var holderComp = entMan.GetComponent(holderOne); + Assert.That(holding.TryToggleHold((holderOne, holderComp), target), Is.True); + }); + await server.WaitRunTicks(4); + + await server.WaitAssertion(() => + { + var held = entMan.GetComponent(target); + var holderOnePuller = entMan.GetComponent(holderOne); + var holderTwoPuller = entMan.GetComponent(holderTwo); + var pullable = entMan.GetComponent(target); + + Assert.Multiple(() => + { + Assert.That(held.FullHold, Is.False); + Assert.That(held.Holders, Has.Count.EqualTo(1)); + Assert.That(held.PrimaryHolder, Is.EqualTo(holderTwo)); + Assert.That(holderOnePuller.Pulling, Is.Null); + Assert.That(holderTwoPuller.Pulling, Is.Null); + Assert.That(pullable.Puller, Is.Null); + }); + }); + + await server.WaitPost(() => + { + transform.SetCoordinates(holderTwo, map.GridCoords.Offset(new Vector2(10f, 0f))); + }); + await server.WaitRunTicks(2); + + await server.WaitAssertion(() => + { + Assert.That(entMan.HasComponent(target), Is.False); + }); + + await pair.CleanReturnAsync(); + } + + private static int CountBlockingVirtualHands(IEntityManager entMan, SharedHandsSystem handsSystem, EntityUid uid, HandsComponent hands) + { + return handsSystem.EnumerateHeld((uid, hands)).Count(item => + entMan.TryGetComponent(item, out VirtualItemComponent? virtualItem) && + virtualItem.BlockingEntity == uid && + entMan.HasComponent(item)); + } + + private static int CountHolderHandBlockers(IEntityManager entMan, SharedHandsSystem handsSystem, EntityUid holder, EntityUid target, HandsComponent hands) + { + return handsSystem.EnumerateHeld((holder, hands)).Count(item => + entMan.TryGetComponent(item, out VirtualItemComponent? virtualItem) && + entMan.TryGetComponent(item, out ScpHoldHandBlockerComponent? blocker) && + blocker.Target == target && + virtualItem.BlockingEntity == target && + entMan.HasComponent(item)); + } + + private static int CountHolderTargetVirtualItems(IEntityManager entMan, SharedHandsSystem handsSystem, EntityUid holder, EntityUid target, HandsComponent hands) + { + return handsSystem.EnumerateHeld((holder, hands)).Count(item => + entMan.TryGetComponent(item, out VirtualItemComponent? virtualItem) && + virtualItem.BlockingEntity == target); + } + + private static float GetDistance(SharedTransformSystem transform, EntityUid first, EntityUid second) + { + return Vector2.Distance( + transform.GetMapCoordinates(first).Position, + transform.GetMapCoordinates(second).Position); + } + + private static float GetLargestDistanceStep(float[] samples) + { + var largest = 0f; + + for (var i = 1; i < samples.Length; i++) + { + largest = Math.Max(largest, MathF.Abs(samples[i] - samples[i - 1])); + } + + return largest; + } + + private static int GetTickCount(IGameTiming timing, TimeSpan duration) + { + return Math.Max(1, (int) Math.Ceiling(duration.TotalSeconds / timing.TickPeriod.TotalSeconds) + 1); + } + + private static void SetSoftEscapeAvailableAt(ScpHeldComponent held, TimeSpan value) + { + SoftEscapeAvailableAtField.SetValue(held, value); + } + + private static void RaiseMoveInput(IEntityManager entMan, EntityUid uid) + { + var mover = entMan.GetComponent(uid); + var move = new MoveInputEvent((uid, mover), MoveButtons.None, Direction.East, true); + entMan.EventBus.RaiseLocalEvent(uid, ref move); + } + + private static void StartHold(IEntityManager entMan, SharedScpHoldingSystem holding, EntityUid holder, EntityUid target) + { + var holdComp = entMan.GetComponent(holder); + Assert.That(holding.TryToggleHold((holder, holdComp), target), Is.True); + } + + private static EntityUid ToClientEntity(IEntityManager serverEntMan, IEntityManager clientEntMan, EntityUid serverEntity) + { + return clientEntMan.GetEntity(serverEntMan.GetNetEntity(serverEntity)); + } +} diff --git a/Content.Shared/_Scp/Holding/ScpHeldComponent.cs b/Content.Shared/_Scp/Holding/ScpHeldComponent.cs new file mode 100644 index 00000000000..43be8df7e68 --- /dev/null +++ b/Content.Shared/_Scp/Holding/ScpHeldComponent.cs @@ -0,0 +1,111 @@ +using Content.Shared.Actions; +using Content.Shared.DoAfter; +using Robust.Shared.GameStates; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom; + +namespace Content.Shared._Scp.Holding; + +/// +/// Runtime state stored on a target while at least one holder is contributing. +/// +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState(true), AutoGenerateComponentPause] +[Access(typeof(SharedScpHoldingSystem))] +public sealed partial class ScpHeldComponent : Component +{ + /// + /// Temporary breakout action prototype granted to the target. + /// + [DataField] + public EntProtoId BreakoutAction = "ActionScpHoldBreakout"; + + /// + /// Runtime breakout action entity. + /// + [AutoNetworkedField] + public EntityUid? BreakoutActionEntity; + + /// + /// Whether the target is currently in the immobile full hold stage. + /// + [AutoNetworkedField] + public bool FullHold; + + /// + /// Next timestamp when a soft breakout attempt may succeed. + /// + [DataField(customTypeSerializer: typeof(TimeOffsetSerializer)), AutoNetworkedField, AutoPausedField] + public TimeSpan SoftEscapeAvailableAt; + + /// + /// Timestamp when the current uninterrupted full hold started. + /// + [DataField(customTypeSerializer: typeof(TimeOffsetSerializer)), AutoNetworkedField, AutoPausedField] + public TimeSpan? FullHoldStartedAt; + + /// + /// Ordered holder list used for reassignment and contribution counting. + /// + [AutoNetworkedField] + public List Holders = new(); + + /// + /// Current primary holder used as the soft hold drag anchor. + /// + [AutoNetworkedField] + public EntityUid? PrimaryHolder; + + /// + /// Required contributor count for entering full hold. + /// + [AutoNetworkedField] + public int RequiredHolderCount = 2; + + /// + /// Copied soft breakout cooldown configuration from the initial holder. + /// + [AutoNetworkedField] + public TimeSpan SoftEscapeCooldown = TimeSpan.FromSeconds(1); + + /// + /// Copied full hold delay configuration from the initial holder. + /// + [AutoNetworkedField] + public TimeSpan FullHoldDelay = TimeSpan.FromSeconds(10); + + /// + /// Copied full breakout duration configuration from the initial holder. + /// + [AutoNetworkedField] + public TimeSpan FullBreakoutDuration = TimeSpan.FromSeconds(5); + + /// + /// Copied post-breakout immunity duration from the initial holder. + /// + [AutoNetworkedField] + public TimeSpan PostBreakoutImmunity = TimeSpan.FromSeconds(5); + + /// + /// Copied maximum hold range from the initial holder. + /// + [AutoNetworkedField] + public float HoldRange = 1f; + + /// + /// Copied walk slowdown applied through . + /// + [AutoNetworkedField] + public float WalkModifier = 0.5f; + + /// + /// Copied sprint slowdown applied through . + /// + [AutoNetworkedField] + public float SprintModifier = 0.5f; + + /// + /// Active breakout do-after id for a full hold, if one exists. + /// + [AutoNetworkedField] + public ushort? BreakoutDoAfterId; +} diff --git a/Content.Shared/_Scp/Holding/ScpHoldComponent.cs b/Content.Shared/_Scp/Holding/ScpHoldComponent.cs new file mode 100644 index 00000000000..ae4a2872ca0 --- /dev/null +++ b/Content.Shared/_Scp/Holding/ScpHoldComponent.cs @@ -0,0 +1,67 @@ +using Content.Shared.Actions; +using Robust.Shared.GameStates; +using Robust.Shared.Prototypes; + +namespace Content.Shared._Scp.Holding; + +/// +/// Grants the owner the ability to contribute to SCP holding. +/// +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] +[Access(typeof(SharedScpHoldingSystem))] +public sealed partial class ScpHoldComponent : Component +{ + /// + /// Action prototype used to start or release a hold. + /// + [DataField] + public EntProtoId Action = "ActionScpHoldTarget"; + + /// + /// Runtime action entity granted to the holder. + /// + [AutoNetworkedField] + public EntityUid? ActionEntity; + + /// + /// Minimum delay between soft breakout attempts while the hold is active. + /// + [DataField] + public TimeSpan SoftEscapeCooldown = TimeSpan.FromSeconds(1); + + /// + /// Minimum uninterrupted full hold duration before a breakout do-after may start. + /// + [DataField] + public TimeSpan FullHoldDelay = TimeSpan.FromSeconds(10); + + /// + /// Duration of the visible breakout do-after for a full hold. + /// + [DataField] + public TimeSpan FullBreakoutDuration = TimeSpan.FromSeconds(5); + + /// + /// Duration of immunity after a successful full breakout. + /// + [DataField] + public TimeSpan PostBreakoutImmunity = TimeSpan.FromSeconds(5); + + /// + /// Maximum unobstructed range allowed between holder and target. + /// + [DataField] + public float HoldRange = 1f; + + /// + /// Walk speed modifier applied to holders when this system supplies slowdown. + /// + [DataField] + public float WalkModifier = 0.5f; + + /// + /// Sprint speed modifier applied to holders when this system supplies slowdown. + /// + [DataField] + public float SprintModifier = 0.5f; +} diff --git a/Content.Shared/_Scp/Holding/ScpHoldHandBlockerComponent.cs b/Content.Shared/_Scp/Holding/ScpHoldHandBlockerComponent.cs new file mode 100644 index 00000000000..606551c7168 --- /dev/null +++ b/Content.Shared/_Scp/Holding/ScpHoldHandBlockerComponent.cs @@ -0,0 +1,17 @@ +using Robust.Shared.GameStates; + +namespace Content.Shared._Scp.Holding; + +/// +/// Marks a virtual item that reserves one holder hand for an active SCP hold. +/// +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState(true)] +[Access(typeof(SharedScpHoldingSystem))] +public sealed partial class ScpHoldHandBlockerComponent : Component +{ + /// + /// The held target represented by this virtual item. + /// + [AutoNetworkedField] + public EntityUid Target; +} diff --git a/Content.Shared/_Scp/Holding/ScpHoldImmuneComponent.cs b/Content.Shared/_Scp/Holding/ScpHoldImmuneComponent.cs new file mode 100644 index 00000000000..fc603688b31 --- /dev/null +++ b/Content.Shared/_Scp/Holding/ScpHoldImmuneComponent.cs @@ -0,0 +1,18 @@ +using Robust.Shared.GameStates; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom; + +namespace Content.Shared._Scp.Holding; + +/// +/// Prevents the target from being held again for a short period after a successful full breakout. +/// +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState, AutoGenerateComponentPause] +[Access(typeof(SharedScpHoldingSystem))] +public sealed partial class ScpHoldImmuneComponent : Component +{ + /// + /// Timestamp when the immunity expires. + /// + [DataField(customTypeSerializer: typeof(TimeOffsetSerializer)), AutoNetworkedField, AutoPausedField] + public TimeSpan ExpiresAt; +} diff --git a/Content.Shared/_Scp/Holding/ScpHolderComponent.cs b/Content.Shared/_Scp/Holding/ScpHolderComponent.cs new file mode 100644 index 00000000000..6989c3dad0a --- /dev/null +++ b/Content.Shared/_Scp/Holding/ScpHolderComponent.cs @@ -0,0 +1,35 @@ +using Robust.Shared.GameStates; + +namespace Content.Shared._Scp.Holding; + +/// +/// Runtime contribution state stored on each active holder. +/// +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState(true)] +[Access(typeof(SharedScpHoldingSystem))] +public sealed partial class ScpHolderComponent : Component +{ + /// + /// Target currently being contributed to. + /// + [AutoNetworkedField] + public EntityUid? Target; + + /// + /// Whether this holder should currently receive the custom slowdown. + /// + [AutoNetworkedField] + public bool SlowdownEnabled; + + /// + /// Walk speed modifier used when is true. + /// + [AutoNetworkedField] + public float WalkModifier = 1f; + + /// + /// Sprint speed modifier used when is true. + /// + [AutoNetworkedField] + public float SprintModifier = 1f; +} diff --git a/Content.Shared/_Scp/Holding/ScpHoldingEvents.cs b/Content.Shared/_Scp/Holding/ScpHoldingEvents.cs new file mode 100644 index 00000000000..3ba74d4d417 --- /dev/null +++ b/Content.Shared/_Scp/Holding/ScpHoldingEvents.cs @@ -0,0 +1,15 @@ +using Content.Shared.Actions; +using Content.Shared.Alert; +using Content.Shared.DoAfter; +using Robust.Shared.Serialization; + +namespace Content.Shared._Scp.Holding; + +public sealed partial class ScpHoldActionEvent : EntityTargetActionEvent; + +public sealed partial class ScpHoldBreakoutActionEvent : InstantActionEvent; + +public sealed partial class ScpHoldBreakoutAlertEvent : BaseAlertEvent; + +[Serializable, NetSerializable] +public sealed partial class ScpHoldBreakoutDoAfterEvent : SimpleDoAfterEvent; diff --git a/Content.Shared/_Scp/Holding/SharedScpHoldingSystem.cs b/Content.Shared/_Scp/Holding/SharedScpHoldingSystem.cs new file mode 100644 index 00000000000..928d8aeb92a --- /dev/null +++ b/Content.Shared/_Scp/Holding/SharedScpHoldingSystem.cs @@ -0,0 +1,1004 @@ +using System.Numerics; +using Content.Shared.ActionBlocker; +using Content.Shared.Alert; +using Content.Shared.Actions; +using Content.Shared.Actions.Components; +using Content.Shared.Body.Components; +using Content.Shared.Body.Part; +using Content.Shared.Body.Systems; +using Content.Shared.DoAfter; +using Content.Shared.Hands; +using Content.Shared.Hands.Components; +using Content.Shared.Hands.EntitySystems; +using Content.Shared.Interaction; +using Content.Shared.Interaction.Components; +using Content.Shared.Movement.Components; +using Content.Shared.Inventory.VirtualItem; +using Content.Shared.Movement.Events; +using Content.Shared.Movement.Systems; +using Content.Shared.Popups; +using Content.Shared.StatusEffectNew; +using Content.Shared.StatusEffectNew.Components; +using Robust.Shared.GameStates; +using Robust.Shared.Containers; +using Robust.Shared.Maths; +using Robust.Shared.Network; +using Robust.Shared.Physics; +using Robust.Shared.Physics.Components; +using Robust.Shared.Physics.Events; +using Robust.Shared.Physics.Systems; +using Robust.Shared.Timing; + +namespace Content.Shared._Scp.Holding; + +public sealed class SharedScpHoldingSystem : EntitySystem +{ + [Dependency] private readonly ActionBlockerSystem _actionBlocker = default!; + [Dependency] private readonly AlertsSystem _alerts = default!; + [Dependency] private readonly SharedActionsSystem _actions = default!; + [Dependency] private readonly SharedBodySystem _body = default!; + [Dependency] private readonly SharedContainerSystem _container = default!; + [Dependency] private readonly SharedDoAfterSystem _doAfter = default!; + [Dependency] private readonly SharedHandsSystem _hands = default!; + [Dependency] private readonly SharedInteractionSystem _interaction = default!; + [Dependency] private readonly MovementSpeedModifierSystem _movement = default!; + [Dependency] private readonly INetManager _net = default!; + [Dependency] private readonly SharedPhysicsSystem _physics = default!; + [Dependency] private readonly SharedPopupSystem _popup = default!; + [Dependency] private readonly StatusEffectsSystem _statusEffects = default!; + [Dependency] private readonly IGameTiming _timing = default!; + [Dependency] private readonly SharedTransformSystem _transform = default!; + [Dependency] private readonly SharedVirtualItemSystem _virtualItem = default!; + + private const string GrabbedStatusEffect = "StatusEffectScpHeld"; + private const float SoftDragDistanceFactor = 0.3f; + private const float SoftDragMinimumDistance = 0.18f; + private const float SoftDragMaximumDistance = 0.3f; + private const float SoftDragSnapTolerance = 0.03f; + private const float SoftDragSettleTolerance = 0.08f; + private const float SoftDragVelocityDirectionThreshold = 0.05f; + private const float SoftDragCatchUpTime = 0.05f; + private const float SoftDragMaximumCorrectionSpeed = 6f; + private const float SoftDragAwayVelocityStrength = 0.6f; + private const float SoftDragVelocityTolerance = 0.05f; + + private readonly List _holdersToRemove = new(); + private readonly List> _virtualBlockersToDelete = new(); + + private EntityQuery _bodyQuery; + private EntityQuery _handsQuery; + private EntityQuery _moverQuery; + private EntityQuery _physicsQuery; + private EntityQuery _heldQuery; + private EntityQuery _holdQuery; + private EntityQuery _holderQuery; + + public override void Initialize() + { + base.Initialize(); + + _bodyQuery = GetEntityQuery(); + _handsQuery = GetEntityQuery(); + _moverQuery = GetEntityQuery(); + _physicsQuery = GetEntityQuery(); + _heldQuery = GetEntityQuery(); + _holdQuery = GetEntityQuery(); + _holderQuery = GetEntityQuery(); + + SubscribeLocalEvent(OnHoldStartup); + SubscribeLocalEvent(OnHoldShutdown); + SubscribeLocalEvent(OnHoldAction); + + SubscribeLocalEvent(OnHeldStartup); + SubscribeLocalEvent(OnHeldShutdown); + SubscribeLocalEvent(OnBreakoutAction); + SubscribeLocalEvent(OnBreakoutAlert); + SubscribeLocalEvent(OnBreakoutDoAfter); + SubscribeLocalEvent(OnHeldMoveInput); + SubscribeLocalEvent(OnHandCountChanged); + SubscribeLocalEvent(OnHeldUpdateCanMove); + SubscribeLocalEvent(OnHeldPreventCollide); + + SubscribeLocalEvent(OnHolderStartup); + SubscribeLocalEvent(OnHolderShutdown); + SubscribeLocalEvent(OnHolderRefreshMoveSpeed); + SubscribeLocalEvent(OnHolderHandsModified); + SubscribeLocalEvent(OnHolderPreventCollide); + } + + public override void Update(float frameTime) + { + base.Update(frameTime); + + var immuneQuery = EntityQueryEnumerator(); + while (immuneQuery.MoveNext(out var uid, out var immune)) + { + if (_timing.CurTime >= immune.ExpiresAt) + RemComp(uid); + } + + var heldQuery = EntityQueryEnumerator(); + while (heldQuery.MoveNext(out var uid, out var held)) + { + if (_net.IsClient && + (!_physicsQuery.TryComp(uid, out var physics) || !physics.Predict)) + { + continue; + } + + UpdateHeld((uid, held)); + } + } + + private void OnHoldStartup(Entity ent, ref ComponentStartup args) + { + _actions.AddAction(ent.Owner, ref ent.Comp.ActionEntity, ent.Comp.Action); + } + + private void OnHoldShutdown(Entity ent, ref ComponentShutdown args) + { + _actions.RemoveAction(ent.Comp.ActionEntity); + + if (_net.IsClient || + TerminatingOrDeleted(ent.Owner) || + !_holderQuery.TryComp(ent.Owner, out var holder) || + holder.Target == null || + TerminatingOrDeleted(holder.Target.Value)) + { + return; + } + + ReleaseHolderContribution(ent.Owner, holder.Target.Value, clearIfEmpty: true); + } + + private void OnHoldAction(Entity ent, ref ScpHoldActionEvent args) + { + if (args.Handled) + return; + + args.Handled = TryToggleHold(ent, args.Target); + } + + private void OnHeldStartup(Entity ent, ref ComponentStartup args) + { + _actions.AddAction(ent.Owner, ref ent.Comp.BreakoutActionEntity, ent.Comp.BreakoutAction); + RefreshHeldState(ent); + } + + private void OnHeldShutdown(Entity ent, ref ComponentShutdown args) + { + _actions.RemoveAction(ent.Comp.BreakoutActionEntity); + _alerts.ClearAlert(ent.Owner, "ScpHoldGrabbed"); + _statusEffects.TryRemoveStatusEffect(ent.Owner, GrabbedStatusEffect); + _virtualItem.DeleteInHandsMatching(ent.Owner, ent.Owner); + + if (!_timing.ApplyingState) + CancelBreakoutDoAfter(ent); + + if (!_net.IsClient) + { + for (var i = 0; i < ent.Comp.Holders.Count; i++) + { + var holderUid = ent.Comp.Holders[i]; + if (!TerminatingOrDeleted(holderUid) && _holderQuery.HasComp(holderUid)) + RemComp(holderUid); + } + } + + _actionBlocker.UpdateCanMove(ent.Owner); + + if (_net.IsClient) + _physics.UpdateIsPredicted(ent.Owner); + } + + private void OnBreakoutAction(Entity ent, ref ScpHoldBreakoutActionEvent args) + { + if (args.Handled) + return; + + args.Handled = TryBreakOut(ent, viaMovement: false); + } + + private void OnBreakoutAlert(Entity ent, ref ScpHoldBreakoutAlertEvent args) + { + if (args.Handled) + return; + + args.Handled = true; + TryBreakOut(ent, viaMovement: false); + } + + private void OnBreakoutDoAfter(Entity ent, ref ScpHoldBreakoutDoAfterEvent args) + { + SetBreakoutDoAfterId(ent, null); + + if (args.Handled) + return; + + args.Handled = true; + + if (args.Cancelled) + { + PopupTarget(ent.Owner, "scp-hold-breakout-interrupted"); + return; + } + + ClearHoldState(ent, applyImmunity: true); + } + + private void OnHeldMoveInput(Entity ent, ref MoveInputEvent args) + { + if (!args.State) + return; + + TryBreakOut(ent, viaMovement: true); + } + + private void OnHandCountChanged(Entity ent, ref HandCountChangedEvent args) + { + if (_net.IsClient) + return; + + SyncHeldState(ent); + } + + private void OnHeldUpdateCanMove(Entity ent, ref UpdateCanMoveEvent args) + { + if (ent.Comp.LifeStage > ComponentLifeStage.Running) + return; + + if (ent.Comp.FullHold) + args.Cancel(); + } + + private void OnHeldPreventCollide(Entity ent, ref PreventCollideEvent args) + { + if (args.Cancelled) + return; + + if (_holderQuery.TryComp(args.OtherEntity, out var holder) && + holder.Target == ent.Owner) + { + args.Cancelled = true; + } + } + + private void OnHolderStartup(Entity ent, ref ComponentStartup args) + { + RefreshHolderState(ent); + } + + private void OnHolderShutdown(Entity ent, ref ComponentShutdown args) + { + ent.Comp.Target = null; + ent.Comp.SlowdownEnabled = false; + DeleteHolderHandBlockers(ent.Owner); + _movement.RefreshMovementSpeedModifiers(ent.Owner); + } + + private void OnHolderRefreshMoveSpeed(Entity ent, ref RefreshMovementSpeedModifiersEvent args) + { + if (!ent.Comp.SlowdownEnabled) + return; + + args.ModifySpeed(ent.Comp.WalkModifier, ent.Comp.SprintModifier); + } + + private void OnHolderHandsModified(Entity ent, ref DidEquipHandEvent args) + { + if (ent.Comp.LifeStage > ComponentLifeStage.Running || + TerminatingOrDeleted(ent.Owner) || + ent.Comp.Target == null || + TerminatingOrDeleted(ent.Comp.Target.Value)) + { + return; + } + + RefreshHolderState(ent); + } + + private void OnHolderPreventCollide(Entity ent, ref PreventCollideEvent args) + { + if (args.Cancelled || + ent.Comp.Target == null || + ent.Comp.Target != args.OtherEntity) + { + return; + } + + args.Cancelled = true; + } + + public bool TryToggleHold(Entity holder, EntityUid target) + { + if (_holderQuery.TryComp(holder.Owner, out var activeHolder) && activeHolder.Target != null) + { + if (activeHolder.Target.Value == target) + { + ReleaseHolderContribution(holder.Owner, target, clearIfEmpty: true); + return true; + } + + PopupHolder(holder.Owner, "scp-hold-already-holding-other"); + return false; + } + + if (!CanToggleHold(holder, target)) + return false; + + var held = EnsureHeldState(target, holder.Comp); + AddHolderContribution(holder.Owner, held); + SyncHeldState(held); + return true; + } + + public bool CanToggleHold(Entity holder, EntityUid target, bool quiet = false) + { + if (!Exists(target) || holder.Owner == target) + return false; + + if (!_moverQuery.HasComp(holder.Owner) || + !_moverQuery.HasComp(target) || + !_physicsQuery.TryComp(target, out var targetPhysics) || + targetPhysics.BodyType == BodyType.Static) + { + if (!quiet) + PopupHolder(holder.Owner, "scp-hold-target-invalid", ("target", target)); + return false; + } + + if (!_container.IsInSameOrNoContainer(holder.Owner, target)) + { + if (!quiet) + PopupHolder(holder.Owner, "scp-hold-target-invalid", ("target", target)); + return false; + } + + if (TryComp(target, out _)) + { + if (!quiet) + PopupHolder(holder.Owner, "scp-hold-target-immune", ("target", target)); + return false; + } + + if (!HasAvailableHolderHand(holder.Owner)) + { + if (!quiet) + PopupHolder(holder.Owner, "scp-hold-holder-no-free-hand", ("target", target)); + return false; + } + + var range = holder.Comp.HoldRange; + if (_heldQuery.TryComp(target, out var held)) + { + range = held.HoldRange; + + if (held.FullHold && held.Holders.Count >= held.RequiredHolderCount) + { + if (!quiet) + PopupHolder(holder.Owner, "scp-hold-target-fully-held", ("target", target)); + return false; + } + } + + if (!_interaction.InRangeUnobstructed(holder.Owner, target, range)) + { + if (!quiet) + PopupHolder(holder.Owner, "scp-hold-target-too-far", ("target", target)); + return false; + } + + return true; + } + + public bool TryBreakOut(Entity held, bool viaMovement) + { + return held.Comp.FullHold + ? TryStartFullBreakout(held, viaMovement) + : TrySoftBreakOut(held, viaMovement); + } + + public void RefreshHeldState(Entity held) + { + _alerts.ShowAlert(held.Owner, "ScpHoldGrabbed"); + SyncHeldStatusEffect(held.Owner); + SyncPlaceholderHands(held); + _actionBlocker.UpdateCanMove(held.Owner); + + if (_net.IsClient) + _physics.UpdateIsPredicted(held.Owner); + } + + public void RefreshHolderState(Entity holder) + { + SyncHolderHandBlocker(holder); + _movement.RefreshMovementSpeedModifiers(holder.Owner); + } + + private bool TrySoftBreakOut(Entity held, bool viaMovement) + { + if (_timing.CurTime < held.Comp.SoftEscapeAvailableAt) + return false; + + if (!viaMovement) + PopupTarget(held.Owner, "scp-hold-breakout-start"); + + ClearHoldState(held, applyImmunity: false); + return true; + } + + private bool TryStartFullBreakout(Entity held, bool viaMovement) + { + if (held.Comp.FullHoldStartedAt == null || + _timing.CurTime < held.Comp.FullHoldStartedAt.Value + held.Comp.FullHoldDelay) + { + if (!viaMovement) + PopupTarget(held.Owner, "scp-hold-breakout-too-early"); + return false; + } + + if (held.Comp.BreakoutDoAfterId != null) + return true; + + var doAfter = new DoAfterArgs(EntityManager, held.Owner, held.Comp.FullBreakoutDuration, + new ScpHoldBreakoutDoAfterEvent(), held.Owner, target: held.Owner) + { + BreakOnMove = true, + BreakOnDamage = true, + NeedHand = false, + Hidden = false, + }; + + if (!_doAfter.TryStartDoAfter(doAfter, out var id)) + return false; + + SetBreakoutDoAfterId(held, id.Value.Index); + PopupTarget(held.Owner, "scp-hold-breakout-start"); + return true; + } + + private void UpdateHeld(Entity held) + { + if (!EnsurePrimaryHolder(held)) + { + ClearHoldState(held, applyImmunity: false); + return; + } + + var desiredSoftDragDistance = GetDesiredSoftDragDistance(held); + var maintenanceRange = GetHoldMaintenanceRange(held.Comp.HoldRange, desiredSoftDragDistance); + + if (!held.Comp.FullHold) + UpdateSoftDrag(held, maintenanceRange, desiredSoftDragDistance); + else + ZeroHeldVelocity(held.Owner); + + _holdersToRemove.Clear(); + + for (var i = 0; i < held.Comp.Holders.Count; i++) + { + var holderUid = held.Comp.Holders[i]; + + if (!Exists(holderUid) || + !_holdQuery.HasComp(holderUid) || + !_holderQuery.TryComp(holderUid, out var holder) || + holder.Target != held.Owner || + !_container.IsInSameOrNoContainer(holderUid, held.Owner) || + !_interaction.InRangeUnobstructed(holderUid, held.Owner, maintenanceRange)) + { + _holdersToRemove.Add(holderUid); + } + } + + for (var i = 0; i < _holdersToRemove.Count; i++) + { + ReleaseHolderContribution(_holdersToRemove[i], held.Owner, clearIfEmpty: false); + + if (!_heldQuery.TryComp(held.Owner, out _)) + return; + } + + if (_heldQuery.TryComp(held.Owner, out var refreshed)) + SyncHeldState((held.Owner, refreshed)); + } + + private Entity EnsureHeldState(EntityUid target, ScpHoldComponent config) + { + var created = !_heldQuery.TryComp(target, out var held); + held ??= EnsureComp(target); + + if (created) + CopyConfig(config, held); + + held.RequiredHolderCount = GetRequiredHolderCount(target); + return (target, held); + } + + private void AddHolderContribution(EntityUid holderUid, Entity held) + { + if (!held.Comp.Holders.Contains(holderUid)) + held.Comp.Holders.Add(holderUid); + + var holder = EnsureComp(holderUid); + holder.Target = held.Owner; + holder.SlowdownEnabled = false; + holder.WalkModifier = held.Comp.WalkModifier; + holder.SprintModifier = held.Comp.SprintModifier; + Dirty(holderUid, holder); + RefreshHolderState((holderUid, holder)); + } + + private void ReleaseHolderContribution(EntityUid holderUid, EntityUid targetUid, bool clearIfEmpty) + { + if (!_heldQuery.TryComp(targetUid, out var held)) + return; + + for (var i = held.Holders.Count - 1; i >= 0; i--) + { + if (held.Holders[i] == holderUid) + held.Holders.RemoveAt(i); + } + + if (_holderQuery.HasComp(holderUid)) + RemComp(holderUid); + + if (held.PrimaryHolder == holderUid) + held.PrimaryHolder = null; + + if (held.Holders.Count == 0) + { + if (clearIfEmpty) + ClearHoldState((targetUid, held), applyImmunity: false); + return; + } + + SyncHeldState((targetUid, held)); + } + + private void SyncHeldState(Entity held) + { + if (!_heldQuery.TryComp(held.Owner, out var heldComp)) + return; + + held.Comp = heldComp; + held.Comp.RequiredHolderCount = GetRequiredHolderCount(held.Owner); + + if (held.Comp.Holders.Count == 0) + { + ClearHoldState(held, applyImmunity: false); + return; + } + + if (!EnsurePrimaryHolder(held)) + { + ClearHoldState(held, applyImmunity: false); + return; + } + + if (held.Comp.Holders.Count >= held.Comp.RequiredHolderCount) + { + EnterFullHold(held); + return; + } + + ExitFullHold(held); + var desiredSoftDragDistance = GetDesiredSoftDragDistance(held); + var maintenanceRange = GetHoldMaintenanceRange(held.Comp.HoldRange, desiredSoftDragDistance); + UpdateSoftDrag(held, maintenanceRange, desiredSoftDragDistance); + UpdateHolderSlowdowns(held); + SyncPlaceholderHands(held); + Dirty(held); + } + + private void EnterFullHold(Entity held) + { + if (!held.Comp.FullHold) + { + held.Comp.FullHold = true; + held.Comp.FullHoldStartedAt = _timing.CurTime; + } + + UpdateHolderSlowdowns(held); + SyncPlaceholderHands(held); + ZeroHeldVelocity(held.Owner); + _actionBlocker.UpdateCanMove(held.Owner); + Dirty(held); + } + + private void ExitFullHold(Entity held) + { + CancelBreakoutDoAfter(held); + + if (!held.Comp.FullHold && held.Comp.FullHoldStartedAt == null) + return; + + held.Comp.FullHold = false; + held.Comp.FullHoldStartedAt = null; + SyncPlaceholderHands(held); + _actionBlocker.UpdateCanMove(held.Owner); + Dirty(held); + } + + private bool EnsurePrimaryHolder(Entity held) + { + if (held.Comp.PrimaryHolder != null && + _holderQuery.TryComp(held.Comp.PrimaryHolder.Value, out var activeHolder) && + activeHolder.Target == held.Owner && + held.Comp.Holders.Contains(held.Comp.PrimaryHolder.Value)) + { + return true; + } + + held.Comp.PrimaryHolder = null; + + for (var i = 0; i < held.Comp.Holders.Count; i++) + { + var holderUid = held.Comp.Holders[i]; + + if (!_holderQuery.TryComp(holderUid, out var holder) || + holder.Target != held.Owner) + { + continue; + } + + held.Comp.PrimaryHolder = holderUid; + return true; + } + + return false; + } + + private void UpdateSoftDrag(Entity held, float maintenanceRange, float desiredDistance) + { + if (held.Comp.PrimaryHolder == null) + return; + + var primaryHolder = held.Comp.PrimaryHolder.Value; + if (!_holderQuery.TryComp(primaryHolder, out var holder) || + holder.Target != held.Owner || + !_container.IsInSameOrNoContainer(primaryHolder, held.Owner) || + !_interaction.InRangeUnobstructed(primaryHolder, held.Owner, maintenanceRange) || + !_physicsQuery.TryComp(held.Owner, out var heldPhysics)) + { + return; + } + + var holderCoords = _transform.GetMapCoordinates(primaryHolder); + var heldCoords = _transform.GetMapCoordinates(held.Owner); + + if (holderCoords.MapId != heldCoords.MapId) + return; + + var offset = heldCoords.Position - holderCoords.Position; + var distance = offset.Length(); + var holderVelocity = _physicsQuery.TryComp(primaryHolder, out var holderPhysics) + ? holderPhysics.LinearVelocity + : Vector2.Zero; + var holderSpeed = holderVelocity.Length(); + + var direction = GetSoftDragDirection(primaryHolder, holderVelocity, offset, distance); + var desiredPosition = holderCoords.Position + direction * desiredDistance; + var correction = desiredPosition - heldCoords.Position; + var correctionDistance = correction.Length(); + + Vector2 desiredVelocity; + if (correctionDistance <= SoftDragSettleTolerance) + { + desiredVelocity = holderVelocity.LengthSquared() > SoftDragVelocityDirectionThreshold * SoftDragVelocityDirectionThreshold + ? holderVelocity + : Vector2.Zero; + } + else + { + var correctionDirection = correction / correctionDistance; + var correctionSpeed = Math.Min(correctionDistance / GetSoftDragCatchUpTime(), SoftDragMaximumCorrectionSpeed); + desiredVelocity = holderVelocity + correctionDirection * correctionSpeed; + + var relativeVelocity = heldPhysics.LinearVelocity - holderVelocity; + var awaySpeed = MathF.Max(0f, -Vector2.Dot(relativeVelocity, correctionDirection)); + if (awaySpeed > 0f) + desiredVelocity += correctionDirection * awaySpeed * SoftDragAwayVelocityStrength; + } + + ApplyHeldVelocity(held.Owner, desiredVelocity, heldPhysics); + } + + private void ClearHoldState(Entity held, bool applyImmunity) + { + if (_heldQuery.TryComp(held.Owner, out var refreshed)) + held = (held.Owner, refreshed); + + CancelBreakoutDoAfter(held); + _virtualItem.DeleteInHandsMatching(held.Owner, held.Owner); + _actionBlocker.UpdateCanMove(held.Owner); + + for (var i = 0; i < held.Comp.Holders.Count; i++) + { + var holderUid = held.Comp.Holders[i]; + if (_holderQuery.HasComp(holderUid)) + RemComp(holderUid); + } + + held.Comp.Holders.Clear(); + held.Comp.PrimaryHolder = null; + + if (applyImmunity) + { + var immune = EnsureComp(held.Owner); + immune.ExpiresAt = _timing.CurTime + held.Comp.PostBreakoutImmunity; + Dirty(held.Owner, immune); + } + + RemComp(held.Owner); + } + + private void UpdateHolderSlowdowns(Entity held) + { + for (var i = 0; i < held.Comp.Holders.Count; i++) + { + var holderUid = held.Comp.Holders[i]; + if (!_holderQuery.TryComp(holderUid, out var holder)) + continue; + + SetHolderSlowdown((holderUid, holder), true, held.Comp.WalkModifier, held.Comp.SprintModifier); + } + } + + private void SetHolderSlowdown(Entity holder, bool enabled, float walkModifier, float sprintModifier) + { + if (holder.Comp.SlowdownEnabled == enabled && + MathHelper.CloseTo(holder.Comp.WalkModifier, walkModifier) && + MathHelper.CloseTo(holder.Comp.SprintModifier, sprintModifier)) + { + return; + } + + holder.Comp.SlowdownEnabled = enabled; + holder.Comp.WalkModifier = walkModifier; + holder.Comp.SprintModifier = sprintModifier; + Dirty(holder); + _movement.RefreshMovementSpeedModifiers(holder.Owner); + } + + private void SyncPlaceholderHands(Entity held) + { + _virtualItem.DeleteInHandsMatching(held.Owner, held.Owner); + + if (!held.Comp.FullHold || !_handsQuery.TryComp(held.Owner, out var hands)) + return; + + foreach (var hand in _hands.EnumerateHands((held.Owner, hands))) + { + if (!_hands.TryGetHeldItem((held.Owner, hands), hand, out var heldItem)) + continue; + + if (HasComp(heldItem.Value)) + continue; + + _hands.DoDrop((held.Owner, hands), hand, doDropInteraction: true); + } + + while (_virtualItem.TrySpawnVirtualItemInHand(held.Owner, held.Owner, out var virtualItem, silent: true)) + { + EnsureComp(virtualItem.Value); + } + } + + private void SyncHeldStatusEffect(EntityUid target) + { + if (_statusEffects.HasStatusEffect(target, GrabbedStatusEffect) || + !_statusEffects.CanAddStatusEffect(target, GrabbedStatusEffect)) + { + return; + } + + EnsureComp(target); + PredictedTrySpawnInContainer(GrabbedStatusEffect, target, StatusEffectContainerComponent.ContainerId, out _); + } + + private void SyncHolderHandBlocker(Entity holder) + { + _virtualBlockersToDelete.Clear(); + EntityUid? validBlocker = null; + var target = holder.Comp.Target; + + foreach (var heldItem in _hands.EnumerateHeld(holder.Owner)) + { + if (!TryComp(heldItem, out var virtualItem)) + { + continue; + } + + var matchesCurrentTarget = holder.Comp.LifeStage <= ComponentLifeStage.Running && + target != null && + virtualItem.BlockingEntity == target.Value; + + if (matchesCurrentTarget) + { + if (validBlocker == null) + { + validBlocker = heldItem; + EnsureComp(heldItem); + var blocker = EnsureComp(heldItem); + var currentTarget = target!.Value; + if (blocker.Target != currentTarget) + { + blocker.Target = currentTarget; + Dirty(heldItem, blocker); + } + continue; + } + } + + if (TryComp(heldItem, out _) || matchesCurrentTarget) + _virtualBlockersToDelete.Add((heldItem, virtualItem)); + } + + for (var i = 0; i < _virtualBlockersToDelete.Count; i++) + { + _virtualItem.DeleteVirtualItem(_virtualBlockersToDelete[i], holder.Owner); + } + + if (holder.Comp.LifeStage > ComponentLifeStage.Running || + holder.Comp.Target == null || + validBlocker != null) + { + return; + } + + if (!_handsQuery.TryComp(holder.Owner, out var hands) || + !_hands.TryGetEmptyHand((holder.Owner, hands), out _)) + { + return; + } + + if (!_virtualItem.TrySpawnVirtualItemInHand(holder.Comp.Target.Value, holder.Owner, out var spawnedVirtualItem, silent: true)) + return; + + EnsureComp(spawnedVirtualItem.Value); + var blockerComp = EnsureComp(spawnedVirtualItem.Value); + blockerComp.Target = holder.Comp.Target.Value; + Dirty(spawnedVirtualItem.Value, blockerComp); + } + + private bool HasAvailableHolderHand(EntityUid holderUid) + { + return _handsQuery.TryComp(holderUid, out var hands) && + _hands.TryGetEmptyHand((holderUid, hands), out _); + } + + private void DeleteHolderHandBlockers(EntityUid holderUid) + { + _virtualBlockersToDelete.Clear(); + + foreach (var heldItem in _hands.EnumerateHeld(holderUid)) + { + if (TryComp(heldItem, out _) && + TryComp(heldItem, out var virtualItem)) + { + _virtualBlockersToDelete.Add((heldItem, virtualItem)); + } + } + + for (var i = 0; i < _virtualBlockersToDelete.Count; i++) + { + _virtualItem.DeleteVirtualItem(_virtualBlockersToDelete[i], holderUid); + } + } + + private int GetRequiredHolderCount(EntityUid target) + { + if (_bodyQuery.TryComp(target, out var body)) + { + var handCount = 0; + foreach (var _ in _body.GetBodyChildrenOfType(target, BodyPartType.Hand, body)) + { + handCount++; + } + + if (handCount > 0) + return handCount; + } + + return 2; + } + + private void CopyConfig(ScpHoldComponent source, ScpHeldComponent target) + { + target.SoftEscapeCooldown = source.SoftEscapeCooldown; + target.FullHoldDelay = source.FullHoldDelay; + target.FullBreakoutDuration = source.FullBreakoutDuration; + target.PostBreakoutImmunity = source.PostBreakoutImmunity; + target.HoldRange = source.HoldRange; + target.WalkModifier = source.WalkModifier; + target.SprintModifier = source.SprintModifier; + target.SoftEscapeAvailableAt = _timing.CurTime; + target.FullHoldStartedAt = null; + } + + private float GetDesiredSoftDragDistance(Entity held) + { + return GetBaseSoftDragDistance(held.Comp.HoldRange); + } + + private static float GetHoldMaintenanceRange(float configuredRange, float desiredSoftDragDistance) + { + return MathF.Max(MathF.Max(configuredRange, SharedInteractionSystem.InteractionRange), desiredSoftDragDistance + SoftDragSnapTolerance); + } + + private static float GetBaseSoftDragDistance(float holdRange) + { + return Math.Clamp(holdRange * SoftDragDistanceFactor, SoftDragMinimumDistance, SoftDragMaximumDistance); + } + + private float GetSoftDragCatchUpTime() + { + return MathF.Max((float) _timing.TickPeriod.TotalSeconds, SoftDragCatchUpTime); + } + + private Vector2 GetSoftDragDirection(EntityUid holderUid, Vector2 holderVelocity, Vector2 offset, float distance) + { + if (distance > SoftDragSnapTolerance) + return offset / distance; + + if (holderVelocity.LengthSquared() > SoftDragVelocityDirectionThreshold * SoftDragVelocityDirectionThreshold) + return -Vector2.Normalize(holderVelocity); + + return Transform(holderUid).LocalRotation.ToWorldVec(); + } + + private void ApplyHeldVelocity(EntityUid uid, Vector2 desiredVelocity, PhysicsComponent physics) + { + if (Vector2.DistanceSquared(physics.LinearVelocity, desiredVelocity) > SoftDragVelocityTolerance * SoftDragVelocityTolerance) + _physics.SetLinearVelocity(uid, desiredVelocity, body: physics); + + if (!MathHelper.CloseTo(physics.AngularVelocity, 0f)) + _physics.SetAngularVelocity(uid, 0f, body: physics); + } + + private void ZeroHeldVelocity(EntityUid uid) + { + if (!_physicsQuery.TryComp(uid, out var physics)) + return; + + if (physics.LinearVelocity == Vector2.Zero && MathHelper.CloseTo(physics.AngularVelocity, 0f)) + return; + + _physics.SetLinearVelocity(uid, Vector2.Zero, body: physics); + _physics.SetAngularVelocity(uid, 0f, body: physics); + } + + private void CancelBreakoutDoAfter(Entity held) + { + if (held.Comp.BreakoutDoAfterId == null) + return; + + _doAfter.Cancel(held.Owner, held.Comp.BreakoutDoAfterId.Value); + SetBreakoutDoAfterId(held, null); + } + + private void SetBreakoutDoAfterId(Entity held, ushort? breakoutDoAfterId) + { + if (held.Comp.BreakoutDoAfterId == breakoutDoAfterId) + return; + + held.Comp.BreakoutDoAfterId = breakoutDoAfterId; + Dirty(held); + } + + private void PopupHolder(EntityUid holder, string key, params (string, object)[] args) + { + if (_net.IsClient) + return; + + _popup.PopupEntity(Loc.GetString(key, args), holder, holder); + } + + private void PopupTarget(EntityUid target, string key, params (string, object)[] args) + { + if (_net.IsClient) + return; + + _popup.PopupEntity(Loc.GetString(key, args), target, target); + } +} diff --git a/Resources/Locale/en-US/_prototypes/_scp/actions/holding.ftl b/Resources/Locale/en-US/_prototypes/_scp/actions/holding.ftl new file mode 100644 index 00000000000..0d2ff8ed9f9 --- /dev/null +++ b/Resources/Locale/en-US/_prototypes/_scp/actions/holding.ftl @@ -0,0 +1,4 @@ +ent-ActionScpHoldTarget = forceful grip + .desc = Grab a nearby target and keep them restrained. One of your hands stays occupied while the grip lasts. +ent-ActionScpHoldBreakout = struggle free + .desc = Attempt to break out of a forceful grip. The grabbed status effect can trigger the same breakout attempt. diff --git a/Resources/Locale/en-US/_strings/_scp/holding/holding.ftl b/Resources/Locale/en-US/_strings/_scp/holding/holding.ftl new file mode 100644 index 00000000000..64199813c21 --- /dev/null +++ b/Resources/Locale/en-US/_strings/_scp/holding/holding.ftl @@ -0,0 +1,11 @@ +scp-hold-already-holding-other = You are already holding someone else. +scp-hold-target-invalid = {CAPITALIZE(THE($target))} cannot be held. +scp-hold-target-immune = {CAPITALIZE(THE($target))} cannot be held yet. +scp-hold-target-fully-held = {CAPITALIZE(THE($target))} is already fully held. +scp-hold-target-too-far = You are too far away to hold {THE($target)}. +scp-hold-holder-no-free-hand = You need a free hand to grab {THE($target)}. +scp-hold-breakout-too-early = You need to endure the hold a little longer before breaking free. +scp-hold-breakout-start = You start trying to break free. +scp-hold-breakout-interrupted = Your breakout attempt was interrupted. +alerts-scp-held-name = Forcefully restrained +alerts-scp-held-desc = Someone is physically gripping you. Move or click this alert to try to break free. In a hard restraint you must endure the hold before the breakout starts. diff --git a/Resources/Locale/ru-RU/_prototypes/_scp/actions/holding.ftl b/Resources/Locale/ru-RU/_prototypes/_scp/actions/holding.ftl new file mode 100644 index 00000000000..46f4a51c6f9 --- /dev/null +++ b/Resources/Locale/ru-RU/_prototypes/_scp/actions/holding.ftl @@ -0,0 +1,4 @@ +ent-ActionScpHoldTarget = силовой захват + .desc = Схватить ближайшую цель и удерживать её силой. Пока захват активен, одна ваша рука занята. +ent-ActionScpHoldBreakout = попытка вырваться + .desc = Попытаться освободиться из силового удержания. Ту же попытку можно начать через статус-эффект удержания. diff --git a/Resources/Locale/ru-RU/_strings/_scp/holding/holding.ftl b/Resources/Locale/ru-RU/_strings/_scp/holding/holding.ftl new file mode 100644 index 00000000000..d68c70c219b --- /dev/null +++ b/Resources/Locale/ru-RU/_strings/_scp/holding/holding.ftl @@ -0,0 +1,11 @@ +scp-hold-already-holding-other = Вы уже удерживаете кого-то другого. +scp-hold-target-invalid = {CAPITALIZE(THE($target))} нельзя удержать. +scp-hold-target-immune = {CAPITALIZE(THE($target))} пока нельзя удержать. +scp-hold-target-fully-held = {CAPITALIZE(THE($target))} уже полностью удерживается. +scp-hold-target-too-far = Вы слишком далеко, чтобы удерживать {THE($target)}. +scp-hold-holder-no-free-hand = Чтобы схватить {THE($target)}, нужна свободная рука. +scp-hold-breakout-too-early = Нужно продержаться в удержании ещё немного, прежде чем вырываться. +scp-hold-breakout-start = Вы начинаете вырываться. +scp-hold-breakout-interrupted = Попытка вырваться была прервана. +alerts-scp-held-name = Вас удерживают силой +alerts-scp-held-desc = Кто-то физически держит вас. Двигайтесь или нажмите на этот статус-эффект, чтобы попытаться вырваться. В полном удержании сначала нужно выдержать захват. diff --git a/Resources/Prototypes/_Scp/Actions/holding.yml b/Resources/Prototypes/_Scp/Actions/holding.yml new file mode 100644 index 00000000000..523a4977d5e --- /dev/null +++ b/Resources/Prototypes/_Scp/Actions/holding.yml @@ -0,0 +1,32 @@ +- type: entity + id: ActionScpHoldTarget + categories: [ HideSpawnMenu ] + components: + - type: Action + icon: + sprite: Objects/Misc/handcuffs.rsi + state: handcuff + itemIconStyle: NoItem + priority: 12 + - type: TargetAction + interactOnMiss: false + checkCanAccess: false + range: 0 + ignoreContainer: true + - type: EntityTargetAction + event: !type:ScpHoldActionEvent + canTargetSelf: false + +- type: entity + id: ActionScpHoldBreakout + categories: [ HideSpawnMenu ] + components: + - type: Action + checkCanInteract: false + icon: + sprite: Objects/Misc/handcuffs.rsi + state: handcuff + itemIconStyle: NoItem + priority: 12 + - type: InstantAction + event: !type:ScpHoldBreakoutActionEvent diff --git a/Resources/Prototypes/_Scp/Alerts/holding.yml b/Resources/Prototypes/_Scp/Alerts/holding.yml new file mode 100644 index 00000000000..895d0dbe0f8 --- /dev/null +++ b/Resources/Prototypes/_Scp/Alerts/holding.yml @@ -0,0 +1,8 @@ +- type: alert + id: ScpHoldGrabbed + icons: + - sprite: /Textures/Objects/Misc/handcuffs.rsi + state: handcuff + name: alerts-scp-held-name + description: alerts-scp-held-desc + clickEvent: !type:ScpHoldBreakoutAlertEvent diff --git a/Resources/Prototypes/_Scp/Entities/StatusEffects/holding.yml b/Resources/Prototypes/_Scp/Entities/StatusEffects/holding.yml new file mode 100644 index 00000000000..91dc1b60c62 --- /dev/null +++ b/Resources/Prototypes/_Scp/Entities/StatusEffects/holding.yml @@ -0,0 +1,5 @@ +- type: entity + parent: MobStatusEffectDebuff + id: StatusEffectScpHeld + categories: [ HideSpawnMenu ] + name: forcefully restrained From 692606e25e9b3fc8be2bdc0fc24674d99e440ab3 Mon Sep 17 00:00:00 2001 From: drdth Date: Wed, 8 Apr 2026 01:55:26 +0300 Subject: [PATCH 03/27] add: tweaks and QoL --- .../Holding/ScpHoldingPredictionSystem.cs | 100 +++- .../Tests/_Scp/ScpHoldingTest.cs | 549 +++++++++++++++++- .../Movement/Pulling/Systems/PullingSystem.cs | 13 +- .../_Scp/Holding/PullingSystem.ScpHolding.cs | 63 ++ .../Holding/ScpHeldHandBlockerComponent.cs | 23 + .../_Scp/Holding/ScpHoldComponent.cs | 6 + .../_Scp/Holding/ScpHoldableComponent.cs | 7 + .../_Scp/Holding/SharedScpHoldingSystem.cs | 272 +++++++-- .../en-US/_strings/_scp/holding/holding.ftl | 4 +- .../ru-RU/_strings/_scp/holding/holding.ftl | 4 +- .../Prototypes/Entities/Mobs/Species/base.yml | 2 + 11 files changed, 987 insertions(+), 56 deletions(-) create mode 100644 Content.Shared/_Scp/Holding/PullingSystem.ScpHolding.cs create mode 100644 Content.Shared/_Scp/Holding/ScpHeldHandBlockerComponent.cs create mode 100644 Content.Shared/_Scp/Holding/ScpHoldableComponent.cs diff --git a/Content.Client/_Scp/Holding/ScpHoldingPredictionSystem.cs b/Content.Client/_Scp/Holding/ScpHoldingPredictionSystem.cs index 16dd4527fed..198d3d4c8a9 100644 --- a/Content.Client/_Scp/Holding/ScpHoldingPredictionSystem.cs +++ b/Content.Client/_Scp/Holding/ScpHoldingPredictionSystem.cs @@ -1,24 +1,42 @@ +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; + + private EntityQuery _handsQuery; private EntityQuery _holderQuery; public override void Initialize() { base.Initialize(); + _handsQuery = GetEntityQuery(); _holderQuery = GetEntityQuery(); SubscribeLocalEvent(OnHeldAfterState); + SubscribeLocalEvent(OnBlockerUnequipped); SubscribeLocalEvent(OnHolderAfterState); SubscribeLocalEvent(OnUpdateHeldPredicted); } @@ -27,15 +45,30 @@ public override void Update(float frameTime) { base.Update(frameTime); + if (_timing.CurTime >= _suppressedUntil) + ClearBlockerRespawnSuppression(); + var query = EntityQueryEnumerator(); while (query.MoveNext(out var uid, out _)) { _physics.UpdateIsPredicted(uid); } - if (_player.LocalEntity is not { Valid: true } local || - !_holderQuery.TryComp(local, out var localHolder)) + 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; } @@ -49,9 +82,31 @@ private void OnHeldAfterState(Entity ent, ref AfterAutoHandleS private void OnHolderAfterState(Entity 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 ent, ref GotUnequippedHandEvent args) + { + if (_player.LocalEntity != args.User) + { + return; + } + + if (_holderQuery.TryComp(args.User, out var holder) && + holder.Target == ent.Comp.Target) + { + return; + } + + SuppressBlockerRespawn(args.User, ent.Comp.Target); + } + private void OnUpdateHeldPredicted(Entity ent, ref UpdateIsPredictedEvent args) { if (_player.LocalEntity is not { Valid: true } local) @@ -81,4 +136,45 @@ private void OnUpdateHeldPredicted(Entity ent, ref UpdateIsPre 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(heldItem, out var virtualItem) || + !TryComp(heldItem, out var blocker) || + virtualItem.BlockingEntity != target || + blocker.Target != target) + { + continue; + } + + _virtualItem.DeleteVirtualItem((heldItem, virtualItem), holder); + } + } } diff --git a/Content.IntegrationTests/Tests/_Scp/ScpHoldingTest.cs b/Content.IntegrationTests/Tests/_Scp/ScpHoldingTest.cs index 159aee5b402..025cf932551 100644 --- a/Content.IntegrationTests/Tests/_Scp/ScpHoldingTest.cs +++ b/Content.IntegrationTests/Tests/_Scp/ScpHoldingTest.cs @@ -19,6 +19,7 @@ using Content.Shared.Movement.Pulling.Components; using Content.Shared.Movement.Systems; using Content.Shared.StatusEffectNew; +using Content.Shared.Throwing; using Robust.Server.Console; using Robust.Client.Physics; using Robust.Shared.GameObjects; @@ -201,10 +202,14 @@ await server.WaitAssertion(() => var puller = sEntMan.GetComponent(holder); var pullable = sEntMan.GetComponent(target); var move = new UpdateCanMoveEvent(target); + var collide = new AttemptMobCollideEvent(); + var targetCollide = new AttemptMobTargetCollideEvent(); var distance = GetDistance(sTransform, holder, target); var contacts = sPhysics.GetContactingEntities(holder); sEntMan.EventBus.RaiseLocalEvent(target, move); + sEntMan.EventBus.RaiseLocalEvent(target, ref collide); + sEntMan.EventBus.RaiseLocalEvent(target, ref targetCollide); Assert.Multiple(() => { @@ -212,6 +217,8 @@ await server.WaitAssertion(() => Assert.That(held.PrimaryHolder, Is.EqualTo(holder)); Assert.That(held.Holders, Has.Count.EqualTo(1)); Assert.That(move.Cancelled, Is.False); + Assert.That(collide.Cancelled, Is.True); + Assert.That(targetCollide.Cancelled, Is.True); Assert.That(distance, Is.GreaterThan(0.18f)); Assert.That(distance, Is.LessThan(0.7f)); Assert.That(contacts, Does.Not.Contain(target)); @@ -240,9 +247,14 @@ await client.WaitAssertion(() => var holderHands = cEntMan.GetComponent(clientHolder); var puller = cEntMan.GetComponent(clientHolder); var pullable = cEntMan.GetComponent(clientTarget); + var collide = new AttemptMobCollideEvent(); + var targetCollide = new AttemptMobTargetCollideEvent(); var distance = GetDistance(cTransform, clientHolder, clientTarget); var contacts = cPhysics.GetContactingEntities(clientHolder); + cEntMan.EventBus.RaiseLocalEvent(clientTarget, ref collide); + cEntMan.EventBus.RaiseLocalEvent(clientTarget, ref targetCollide); + Assert.Multiple(() => { Assert.That(held.FullHold, Is.False); @@ -250,6 +262,8 @@ await client.WaitAssertion(() => Assert.That(held.Holders, Has.Count.EqualTo(1)); Assert.That(distance, Is.GreaterThan(0.18f)); Assert.That(distance, Is.LessThan(0.55f)); + Assert.That(collide.Cancelled, Is.True); + Assert.That(targetCollide.Cancelled, Is.True); Assert.That(contacts, Does.Not.Contain(clientTarget)); Assert.That(holderState.SlowdownEnabled, Is.True); Assert.That(holderSpeed.WalkSpeedModifier, Is.LessThan(0.6f)); @@ -357,6 +371,70 @@ await client.WaitAssertion(() => await pair.CleanReturnAsync(); } + [Test] + public async Task PullAttemptOnHoldableTargetRedirectsToHoldAndReplacesVanillaPull() + { + await using var pair = await PoolManager.GetServerClient(new PoolSettings { Fresh = true }); + var server = pair.Server; + var entMan = server.EntMan; + var pulling = server.System(); + var map = await pair.CreateTestMap(); + + EntityUid holder = default; + EntityUid plainTarget = default; + EntityUid holdTarget = default; + + await server.WaitPost(() => + { + holder = entMan.SpawnEntity(HolderPrototype, map.GridCoords); + plainTarget = entMan.SpawnEntity("MobHuman", map.GridCoords.Offset(new Vector2(0.1f, 0f))); + holdTarget = entMan.SpawnEntity("MobHuman", map.GridCoords.Offset(new Vector2(-0.1f, 0f))); + + entMan.RemoveComponent(plainTarget); + + Assert.That(pulling.TryStartPull(holder, plainTarget), Is.True); + }); + await server.WaitRunTicks(2); + + await server.WaitAssertion(() => + { + var holderPuller = entMan.GetComponent(holder); + var plainPullable = entMan.GetComponent(plainTarget); + + Assert.Multiple(() => + { + Assert.That(holderPuller.Pulling, Is.EqualTo(plainTarget)); + Assert.That(plainPullable.Puller, Is.EqualTo(holder)); + Assert.That(entMan.HasComponent(holdTarget), Is.False); + }); + }); + + await server.WaitPost(() => + { + Assert.That(pulling.TryStartPull(holder, holdTarget), Is.True); + }); + await server.WaitRunTicks(2); + + await server.WaitAssertion(() => + { + var holderPuller = entMan.GetComponent(holder); + var holderState = entMan.GetComponent(holder); + var plainPullable = entMan.GetComponent(plainTarget); + var holdPullable = entMan.GetComponent(holdTarget); + + Assert.Multiple(() => + { + Assert.That(holderPuller.Pulling, Is.Null); + Assert.That(holderState.Target, Is.EqualTo(holdTarget)); + Assert.That(plainPullable.Puller, Is.Null); + Assert.That(holdPullable.Puller, Is.Null); + Assert.That(entMan.HasComponent(holdTarget), Is.True); + }); + }); + + await pair.CleanReturnAsync(); + } + [Test] public async Task SecondHolderEntersFullHoldAndFillsHands() { @@ -398,6 +476,7 @@ await server.WaitAssertion(() => Assert.That(held.Holders, Has.Count.EqualTo(2)); Assert.That(move.Cancelled, Is.True); Assert.That(CountBlockingVirtualHands(entMan, handsSystem, target, hands), Is.EqualTo(hands.SortedHands.Count)); + Assert.That(VictimHandsUseHolderIcons(entMan, handsSystem, target, hands, holderOne, holderTwo), Is.True); Assert.That(holderOnePuller.Pulling, Is.Null); Assert.That(holderTwoPuller.Pulling, Is.Null); Assert.That(pullable.Puller, Is.Null); @@ -407,6 +486,40 @@ await server.WaitAssertion(() => await pair.CleanReturnAsync(); } + [Test] + public async Task TargetWithoutScpHoldableCannotBeHeld() + { + await using var pair = await PoolManager.GetServerClient(new PoolSettings { Fresh = true }); + var server = pair.Server; + var entMan = server.EntMan; + var holding = server.System(); + var map = await pair.CreateTestMap(); + + EntityUid holder = default; + EntityUid target = default; + + await server.WaitPost(() => + { + holder = entMan.SpawnEntity(HolderPrototype, map.GridCoords); + target = entMan.SpawnEntity("MobHuman", map.GridCoords); + entMan.RemoveComponent(target); + }); + + await server.WaitAssertion(() => + { + var holdComp = entMan.GetComponent(holder); + + Assert.Multiple(() => + { + Assert.That(holding.CanToggleHold((holder, holdComp), target, quiet: true), Is.False); + Assert.That(holding.TryToggleHold((holder, holdComp), target), Is.False); + Assert.That(entMan.HasComponent(target), Is.False); + }); + }); + + await pair.CleanReturnAsync(); + } + [Test] public async Task MultiHandTargetNeedsMatchingHolderCountAndResyncsOnHandLoss() { @@ -469,6 +582,7 @@ await server.WaitAssertion(() => Assert.That(held.RequiredHolderCount, Is.EqualTo(3)); Assert.That(hands.SortedHands.Count, Is.EqualTo(3)); Assert.That(CountBlockingVirtualHands(entMan, handsSystem, target, hands), Is.EqualTo(3)); + Assert.That(VictimHandsUseHolderIcons(entMan, handsSystem, target, hands, holderOne, holderTwo, holderThree), Is.True); }); }); @@ -491,6 +605,7 @@ await server.WaitAssertion(() => Assert.That(held.FullHold, Is.True); Assert.That(hands.SortedHands.Count, Is.EqualTo(2)); Assert.That(CountBlockingVirtualHands(entMan, handsSystem, target, hands), Is.EqualTo(2)); + Assert.That(VictimHandsUseHolderIcons(entMan, handsSystem, target, hands, holderOne, holderTwo, holderThree), Is.True); }); }); @@ -542,7 +657,12 @@ await server.WaitAssertion(() => await server.WaitAssertion(() => { var held = entMan.GetComponent(target); - Assert.That(held.BreakoutDoAfterId, Is.Not.Null); + Assert.Multiple(() => + { + Assert.That(held.BreakoutDoAfterId, Is.Not.Null); + Assert.That(CountAttachedPrototype(entMan, holderOne, "WhistleExclamation"), Is.EqualTo(1)); + Assert.That(CountAttachedPrototype(entMan, holderTwo, "WhistleExclamation"), Is.EqualTo(1)); + }); }); await server.WaitRunTicks(GetTickCount(timing, TimeSpan.FromSeconds(5))); @@ -624,7 +744,12 @@ await server.WaitPost(() => await server.WaitAssertion(() => { var held = entMan.GetComponent(target); - Assert.That(held.BreakoutDoAfterId, Is.Not.Null); + Assert.Multiple(() => + { + Assert.That(held.BreakoutDoAfterId, Is.Not.Null); + Assert.That(CountAttachedPrototype(entMan, holderOne, "WhistleExclamation"), Is.EqualTo(1)); + Assert.That(CountAttachedPrototype(entMan, holderTwo, "WhistleExclamation"), Is.EqualTo(1)); + }); }); await server.WaitRunTicks(GetTickCount(timing, TimeSpan.FromSeconds(5))); @@ -637,6 +762,152 @@ await server.WaitAssertion(() => await pair.CleanReturnAsync(); } + [Test] + public async Task DroppingOrThrowingHolderBlockerReleasesHold() + { + await using var pair = await PoolManager.GetServerClient(new PoolSettings { Fresh = true }); + var server = pair.Server; + var entMan = server.EntMan; + var handsSystem = server.System(); + var holding = server.System(); + var map = await pair.CreateTestMap(); + + EntityUid holder = default; + EntityUid target = default; + + await server.WaitPost(() => + { + holder = entMan.SpawnEntity(HolderPrototype, map.GridCoords); + target = entMan.SpawnEntity("MobHuman", map.GridCoords.Offset(new Vector2(0.1f, 0f))); + StartHold(entMan, holding, holder, target); + }); + await server.WaitRunTicks(2); + + await server.WaitPost(() => + { + var hands = entMan.GetComponent(holder); + var blocker = FindHolderHandBlocker(entMan, handsSystem, holder, target, hands); + + Assert.That(blocker, Is.Not.EqualTo(EntityUid.Invalid)); + Assert.That(handsSystem.TryDrop((holder, hands), blocker), Is.True); + }); + await server.WaitRunTicks(2); + + await server.WaitAssertion(() => + { + Assert.That(entMan.HasComponent(target), Is.False); + Assert.That(entMan.HasComponent(holder), Is.False); + }); + + await server.WaitPost(() => StartHold(entMan, holding, holder, target)); + await server.WaitRunTicks(2); + + await server.WaitPost(() => + { + var hands = entMan.GetComponent(holder); + var blocker = FindHolderHandBlocker(entMan, handsSystem, holder, target, hands); + var throwEvent = new BeforeThrowEvent(blocker, Vector2.UnitX, 5f, holder); + + Assert.That(blocker, Is.Not.EqualTo(EntityUid.Invalid)); + entMan.EventBus.RaiseLocalEvent(holder, ref throwEvent); + Assert.That(throwEvent.Cancelled, Is.True); + }); + await server.WaitRunTicks(2); + + await server.WaitAssertion(() => + { + Assert.That(entMan.HasComponent(target), Is.False); + Assert.That(entMan.HasComponent(holder), Is.False); + }); + + await pair.CleanReturnAsync(); + } + + [Test] + public async Task ClientDroppingHolderBlockerReleasesWithoutRespawnFlicker() + { + await using var pair = await PoolManager.GetServerClient(new PoolSettings + { + Fresh = true, + Connected = true, + DummyTicker = false, + }); + + var server = pair.Server; + var client = pair.Client; + var sEntMan = server.EntMan; + var cEntMan = client.EntMan; + var sTransform = server.System(); + var sHandsSystem = server.System(); + var cHandsSystem = client.System(); + var holding = server.System(); + var map = await pair.CreateTestMap(); + + var serverPlayer = pair.Player!.AttachedEntity!.Value; + EntityUid target = default; + + await server.WaitPost(() => + { + sTransform.SetCoordinates(serverPlayer, map.GridCoords); + sEntMan.EnsureComponent(serverPlayer); + target = sEntMan.SpawnEntity("MobHuman", map.GridCoords.Offset(new Vector2(0.1f, 0f))); + StartHold(sEntMan, holding, serverPlayer, target); + }); + + await pair.RunTicksSync(10); + await pair.SyncTicks(targetDelta: 1); + + var clientPlayer = EntityUid.Invalid; + var clientTarget = EntityUid.Invalid; + await client.WaitAssertion(() => + { + clientPlayer = client.AttachedEntity!.Value; + clientTarget = ToClientEntity(sEntMan, cEntMan, target); + var hands = cEntMan.GetComponent(clientPlayer); + + Assert.Multiple(() => + { + Assert.That(cEntMan.HasComponent(clientPlayer), Is.True); + Assert.That(cEntMan.HasComponent(clientTarget), Is.True); + Assert.That(CountHolderTargetVirtualItems(cEntMan, cHandsSystem, clientPlayer, clientTarget, hands), Is.EqualTo(1)); + }); + }); + + await client.WaitPost(() => + { + var hands = cEntMan.GetComponent(clientPlayer); + var blocker = FindHolderHandBlocker(cEntMan, cHandsSystem, clientPlayer, clientTarget, hands); + + Assert.That(blocker, Is.Not.EqualTo(EntityUid.Invalid)); + Assert.That(cHandsSystem.TryDrop((clientPlayer, hands), blocker), Is.True); + Assert.Multiple(() => + { + Assert.That(cEntMan.HasComponent(clientTarget), Is.False); + Assert.That(cEntMan.HasComponent(clientPlayer), Is.False); + Assert.That(CountHolderTargetVirtualItems(cEntMan, cHandsSystem, clientPlayer, clientTarget, hands), Is.EqualTo(0)); + }); + }); + + var maxClientBlockers = 0; + for (var i = 0; i < 12; i++) + { + await pair.RunTicksSync(1); + await pair.SyncTicks(targetDelta: 1); + await client.WaitPost(() => + { + if (!cEntMan.TryGetComponent(clientPlayer, out var hands)) + return; + + maxClientBlockers = Math.Max(maxClientBlockers, + CountHolderTargetVirtualItems(cEntMan, cHandsSystem, clientPlayer, clientTarget, hands)); + }); + } + + Assert.That(maxClientBlockers, Is.EqualTo(0)); + + await pair.CleanReturnAsync(); + } + [Test] public async Task ClientHoldActionPredictsSoftHoldBeforeServerAck() { @@ -669,7 +940,7 @@ public async Task ClientHoldActionPredictsSoftHoldBeforeServerAck() await server.WaitPost(() => { sTransform.SetCoordinates(serverPlayer, map.GridCoords); - sEntMan.AddComponent(serverPlayer); + sEntMan.EnsureComponent(serverPlayer); target = sEntMan.SpawnEntity("MobHuman", map.GridCoords.Offset(new Vector2(0.1f, 0f))); }); @@ -791,6 +1062,188 @@ await client.WaitAssertion(() => await pair.CleanReturnAsync(); } + [Test] + public async Task ClientHoldActionCooldownAndFullBreakoutPenaltyReplicate() + { + await using var pair = await PoolManager.GetServerClient(new PoolSettings + { + Fresh = true, + Connected = true, + DummyTicker = false, + }); + + var server = pair.Server; + var client = pair.Client; + var sEntMan = server.EntMan; + var cEntMan = client.EntMan; + var sTiming = server.ResolveDependency(); + var cTiming = client.ResolveDependency(); + var sTransform = server.System(); + var holding = server.System(); + var map = await pair.CreateTestMap(); + + var serverPlayer = pair.Player!.AttachedEntity!.Value; + EntityUid firstTarget = default; + EntityUid breakoutTarget = default; + EntityUid holderTwo = default; + + await server.WaitPost(() => + { + sTransform.SetCoordinates(serverPlayer, map.GridCoords); + sEntMan.EnsureComponent(serverPlayer); + firstTarget = sEntMan.SpawnEntity("MobHuman", map.GridCoords.Offset(new Vector2(0.4f, 0f))); + breakoutTarget = sEntMan.SpawnEntity("MobHuman", map.GridCoords.Offset(new Vector2(0.4f, 0.6f))); + holderTwo = sEntMan.SpawnEntity(HolderPrototype, map.GridCoords.Offset(new Vector2(-0.4f, 0.6f))); + }); + + await pair.RunTicksSync(10); + await pair.SyncTicks(targetDelta: 1); + + EntityUid serverHoldAction = default; + EntityUid clientPlayer = default; + EntityUid clientFirstTarget = default; + EntityUid clientBreakoutTarget = default; + EntityUid clientHoldAction = default; + + await server.WaitAssertion(() => + { + var hold = sEntMan.GetComponent(serverPlayer); + Assert.That(hold.ActionEntity, Is.Not.Null); + serverHoldAction = hold.ActionEntity!.Value; + }); + + await client.WaitAssertion(() => + { + clientPlayer = client.AttachedEntity!.Value; + clientFirstTarget = ToClientEntity(sEntMan, cEntMan, firstTarget); + clientBreakoutTarget = ToClientEntity(sEntMan, cEntMan, breakoutTarget); + clientHoldAction = ToClientEntity(sEntMan, cEntMan, serverHoldAction); + }); + + var holdActionNet = sEntMan.GetNetEntity(serverHoldAction); + var firstTargetNet = sEntMan.GetNetEntity(firstTarget); + var breakoutTargetNet = sEntMan.GetNetEntity(breakoutTarget); + + await client.WaitPost(() => + { + cEntMan.RaisePredictiveEvent(new RequestPerformActionEvent(holdActionNet, firstTargetNet)); + cEntMan.RaisePredictiveEvent(new RequestPerformActionEvent(holdActionNet, breakoutTargetNet)); + + Assert.Multiple(() => + { + Assert.That(cEntMan.HasComponent(clientFirstTarget), Is.True); + Assert.That(cEntMan.HasComponent(clientBreakoutTarget), Is.False); + Assert.That(GetActionCooldownRemaining(cEntMan, clientHoldAction, cTiming), Is.GreaterThan(TimeSpan.Zero)); + }); + }); + + await pair.RunTicksSync(10); + await pair.SyncTicks(targetDelta: 1); + + await server.WaitAssertion(() => + { + Assert.Multiple(() => + { + Assert.That(sEntMan.HasComponent(firstTarget), Is.True); + Assert.That(sEntMan.HasComponent(breakoutTarget), Is.False); + Assert.That(GetActionCooldownRemaining(sEntMan, serverHoldAction, sTiming), Is.GreaterThan(TimeSpan.Zero)); + }); + }); + + await client.WaitAssertion(() => + { + Assert.That(GetActionCooldownRemaining(cEntMan, clientHoldAction, cTiming), Is.GreaterThan(TimeSpan.Zero)); + }); + + await pair.RunTicksSync(GetTickCount(sTiming, TimeSpan.FromSeconds(1)) + 1); + await pair.SyncTicks(targetDelta: 1); + + await client.WaitPost(() => + { + cEntMan.RaisePredictiveEvent(new RequestPerformActionEvent(holdActionNet, firstTargetNet)); + Assert.That(cEntMan.HasComponent(clientFirstTarget), Is.False); + }); + + await pair.RunTicksSync(10); + await pair.SyncTicks(targetDelta: 1); + + await server.WaitAssertion(() => + { + Assert.That(sEntMan.HasComponent(firstTarget), Is.False); + }); + + await pair.RunTicksSync(GetTickCount(sTiming, TimeSpan.FromSeconds(1)) + 1); + await pair.SyncTicks(targetDelta: 1); + + await client.WaitPost(() => + { + cEntMan.RaisePredictiveEvent(new RequestPerformActionEvent(holdActionNet, breakoutTargetNet)); + Assert.That(cEntMan.HasComponent(clientBreakoutTarget), Is.True); + }); + + await pair.RunTicksSync(10); + await pair.SyncTicks(targetDelta: 1); + + await server.WaitAssertion(() => + { + Assert.That(sEntMan.HasComponent(breakoutTarget), Is.True); + }); + + await server.WaitPost(() => + { + StartHold(sEntMan, holding, holderTwo, breakoutTarget); + }); + + await pair.RunTicksSync(10); + await pair.SyncTicks(targetDelta: 1); + await pair.RunTicksSync(GetTickCount(sTiming, TimeSpan.FromSeconds(10))); + await pair.SyncTicks(targetDelta: 1); + + await server.WaitPost(() => RaiseMoveInput(sEntMan, breakoutTarget)); + await pair.RunTicksSync(2); + await pair.SyncTicks(targetDelta: 1); + await pair.RunTicksSync(GetTickCount(sTiming, TimeSpan.FromSeconds(5)) + 5); + await pair.SyncTicks(targetDelta: 1); + + await server.WaitAssertion(() => + { + var holderTwoHold = sEntMan.GetComponent(holderTwo); + + Assert.Multiple(() => + { + Assert.That(sEntMan.HasComponent(breakoutTarget), Is.False); + Assert.That(GetActionCooldownRemaining(sEntMan, serverHoldAction, sTiming), Is.GreaterThanOrEqualTo(TimeSpan.FromSeconds(1.5))); + Assert.That(holderTwoHold.ActionEntity, Is.Not.Null); + Assert.That(GetActionCooldownRemaining(sEntMan, holderTwoHold.ActionEntity!.Value, sTiming), Is.GreaterThanOrEqualTo(TimeSpan.FromSeconds(1.5))); + }); + }); + + await client.WaitAssertion(() => + { + Assert.Multiple(() => + { + Assert.That(cEntMan.HasComponent(clientBreakoutTarget), Is.False); + Assert.That(GetActionCooldownRemaining(cEntMan, clientHoldAction, cTiming), Is.GreaterThanOrEqualTo(TimeSpan.FromSeconds(1.5))); + }); + }); + + await client.WaitPost(() => + { + cEntMan.RaisePredictiveEvent(new RequestPerformActionEvent(holdActionNet, firstTargetNet)); + Assert.That(cEntMan.HasComponent(clientFirstTarget), Is.False); + }); + + await pair.RunTicksSync(5); + await pair.SyncTicks(targetDelta: 1); + + await server.WaitAssertion(() => + { + Assert.That(sEntMan.HasComponent(firstTarget), Is.False); + }); + + await pair.CleanReturnAsync(); + } + [Test] public async Task ClientSecondHoldActionPredictsFullHoldBeforeServerAck() { @@ -806,6 +1259,7 @@ public async Task ClientSecondHoldActionPredictsFullHoldBeforeServerAck() var sEntMan = server.EntMan; var cEntMan = client.EntMan; var sTransform = server.System(); + var sHandsSystem = server.System(); var cHandsSystem = client.System(); var holding = server.System(); var map = await pair.CreateTestMap(); @@ -817,7 +1271,7 @@ public async Task ClientSecondHoldActionPredictsFullHoldBeforeServerAck() await server.WaitPost(() => { sTransform.SetCoordinates(serverPlayer, map.GridCoords); - sEntMan.AddComponent(serverPlayer); + sEntMan.EnsureComponent(serverPlayer); holderOne = sEntMan.SpawnEntity(HolderPrototype, map.GridCoords); target = sEntMan.SpawnEntity("MobHuman", map.GridCoords.Offset(new Vector2(0.5f, 0f))); @@ -844,10 +1298,12 @@ await server.WaitAssertion(() => var clientPlayer = EntityUid.Invalid; var clientTarget = EntityUid.Invalid; + var clientHolderOne = EntityUid.Invalid; await client.WaitAssertion(() => { clientPlayer = client.AttachedEntity!.Value; clientTarget = ToClientEntity(sEntMan, cEntMan, target); + clientHolderOne = ToClientEntity(sEntMan, cEntMan, holderOne); var held = cEntMan.GetComponent(clientTarget); Assert.Multiple(() => @@ -872,6 +1328,7 @@ await client.WaitPost(() => Assert.That(held.FullHold, Is.True); Assert.That(held.Holders, Has.Count.EqualTo(2)); Assert.That(CountBlockingVirtualHands(cEntMan, cHandsSystem, clientTarget, hands), Is.EqualTo(hands.SortedHands.Count)); + Assert.That(VictimHandsUseHolderIcons(cEntMan, cHandsSystem, clientTarget, hands, clientHolderOne, clientPlayer), Is.True); }); }); @@ -940,7 +1397,7 @@ public async Task ClientPrimaryReassignmentKeepsCustomDragAndReconcilesCleanly() await server.WaitPost(() => { sTransform.SetCoordinates(serverPlayer, map.GridCoords); - sEntMan.AddComponent(serverPlayer); + sEntMan.EnsureComponent(serverPlayer); target = sEntMan.SpawnEntity("MobHuman", map.GridCoords.Offset(new Vector2(0.95f, 0f))); host.ExecuteCommand(null, $"addhand {sEntMan.GetNetEntity(target)}"); @@ -1111,9 +1568,13 @@ await server.WaitAssertion(() => }); var clientPlayer = EntityUid.Invalid; + var clientHolderOne = EntityUid.Invalid; + var clientHolderTwo = EntityUid.Invalid; await client.WaitAssertion(() => { clientPlayer = client.AttachedEntity!.Value; + clientHolderOne = ToClientEntity(sEntMan, cEntMan, holderOne); + clientHolderTwo = ToClientEntity(sEntMan, cEntMan, holderTwo); var held = cEntMan.GetComponent(clientPlayer); Assert.That(held.FullHold, Is.True); }); @@ -1129,6 +1590,8 @@ await client.WaitPost(() => { Assert.That(held.BreakoutDoAfterId, Is.Not.Null); Assert.That(cEntMan.HasComponent(clientPlayer), Is.False); + Assert.That(CountAttachedPrototype(cEntMan, clientHolderOne, "WhistleExclamation"), Is.EqualTo(1)); + Assert.That(CountAttachedPrototype(cEntMan, clientHolderTwo, "WhistleExclamation"), Is.EqualTo(1)); }); }); @@ -1337,18 +1800,60 @@ private static int CountBlockingVirtualHands(IEntityManager entMan, SharedHandsS { return handsSystem.EnumerateHeld((uid, hands)).Count(item => entMan.TryGetComponent(item, out VirtualItemComponent? virtualItem) && - virtualItem.BlockingEntity == uid && + entMan.TryGetComponent(item, out ScpHeldHandBlockerComponent? blocker) && + blocker.Target == uid && + blocker.Holder == virtualItem.BlockingEntity && entMan.HasComponent(item)); } + private static bool VictimHandsUseHolderIcons(IEntityManager entMan, SharedHandsSystem handsSystem, EntityUid uid, HandsComponent hands, params EntityUid[] holders) + { + var expected = holders.ToHashSet(); + var count = 0; + + foreach (var item in handsSystem.EnumerateHeld((uid, hands))) + { + if (!entMan.TryGetComponent(item, out VirtualItemComponent? virtualItem) || + !entMan.TryGetComponent(item, out ScpHeldHandBlockerComponent? blocker) || + blocker.Target != uid || + blocker.Holder != virtualItem.BlockingEntity || + !entMan.HasComponent(item) || + !expected.Contains(blocker.Holder)) + { + return false; + } + + count++; + } + + return count == hands.SortedHands.Count; + } + + private static int CountAttachedPrototype(IEntityManager entMan, EntityUid parent, string prototypeId) + { + var count = 0; + var enumerator = entMan.GetComponent(parent).ChildEnumerator; + + while (enumerator.MoveNext(out var child)) + { + if (entMan.TryGetComponent(child, out MetaDataComponent? metadata) && + !metadata.Deleted && + metadata.EntityPrototype?.ID == prototypeId) + { + count++; + } + } + + return count; + } + private static int CountHolderHandBlockers(IEntityManager entMan, SharedHandsSystem handsSystem, EntityUid holder, EntityUid target, HandsComponent hands) { return handsSystem.EnumerateHeld((holder, hands)).Count(item => entMan.TryGetComponent(item, out VirtualItemComponent? virtualItem) && entMan.TryGetComponent(item, out ScpHoldHandBlockerComponent? blocker) && blocker.Target == target && - virtualItem.BlockingEntity == target && - entMan.HasComponent(item)); + virtualItem.BlockingEntity == target); } private static int CountHolderTargetVirtualItems(IEntityManager entMan, SharedHandsSystem handsSystem, EntityUid holder, EntityUid target, HandsComponent hands) @@ -1358,6 +1863,22 @@ private static int CountHolderTargetVirtualItems(IEntityManager entMan, SharedHa virtualItem.BlockingEntity == target); } + private static EntityUid FindHolderHandBlocker(IEntityManager entMan, SharedHandsSystem handsSystem, EntityUid holder, EntityUid target, HandsComponent hands) + { + foreach (var item in handsSystem.EnumerateHeld((holder, hands))) + { + if (entMan.TryGetComponent(item, out VirtualItemComponent? virtualItem) && + entMan.TryGetComponent(item, out ScpHoldHandBlockerComponent? blocker) && + blocker.Target == target && + virtualItem.BlockingEntity == target) + { + return item; + } + } + + return EntityUid.Invalid; + } + private static float GetDistance(SharedTransformSystem transform, EntityUid first, EntityUid second) { return Vector2.Distance( @@ -1382,6 +1903,18 @@ private static int GetTickCount(IGameTiming timing, TimeSpan duration) return Math.Max(1, (int) Math.Ceiling(duration.TotalSeconds / timing.TickPeriod.TotalSeconds) + 1); } + private static TimeSpan GetActionCooldownRemaining(IEntityManager entMan, EntityUid action, IGameTiming timing) + { + if (!entMan.TryGetComponent(action, out ActionComponent? actionComp) || + actionComp.Cooldown is not { } cooldown || + cooldown.End <= timing.CurTime) + { + return TimeSpan.Zero; + } + + return cooldown.End - timing.CurTime; + } + private static void SetSoftEscapeAvailableAt(ScpHeldComponent held, TimeSpan value) { SoftEscapeAvailableAtField.SetValue(held, value); diff --git a/Content.Shared/Movement/Pulling/Systems/PullingSystem.cs b/Content.Shared/Movement/Pulling/Systems/PullingSystem.cs index 9a1222cf2b9..f7c3a5fcd44 100644 --- a/Content.Shared/Movement/Pulling/Systems/PullingSystem.cs +++ b/Content.Shared/Movement/Pulling/Systems/PullingSystem.cs @@ -39,7 +39,9 @@ namespace Content.Shared.Movement.Pulling.Systems; /// /// Allows one entity to pull another behind them via a physics distance joint. /// -public sealed class PullingSystem : EntitySystem +// Fire edit start - enable _Scp partial hook +public sealed partial class PullingSystem : EntitySystem +// Fire edit end { [Dependency] private readonly IGameTiming _timing = default!; [Dependency] private readonly MobStateSystem _mobState = default!; // Sunrise-edit @@ -60,6 +62,10 @@ public override void Initialize() { base.Initialize(); + // Fire added start - initialize _Scp hold redirect caches + InitializeScpHolding(); + // Fire added end + UpdatesAfter.Add(typeof(SharedPhysicsSystem)); UpdatesOutsidePrediction = true; @@ -523,6 +529,11 @@ public bool TryStartPull(EntityUid pullerUid, EntityUid pullableUid, if (pullerComp.Pulling == pullableUid) return true; + // Fire added start - redirect scp-hold-capable pull attempts into the hold flow + if (TryRedirectPullToScpHold(pullerUid, pullableUid, pullerComp, pullableComp, out var holdResult)) + return holdResult; + // Fire added end + if (!CanPull(pullerUid, pullableUid)) return false; diff --git a/Content.Shared/_Scp/Holding/PullingSystem.ScpHolding.cs b/Content.Shared/_Scp/Holding/PullingSystem.ScpHolding.cs new file mode 100644 index 00000000000..fa2b38eb8f9 --- /dev/null +++ b/Content.Shared/_Scp/Holding/PullingSystem.ScpHolding.cs @@ -0,0 +1,63 @@ +using Content.Shared._Scp.Holding; +using Content.Shared.Movement.Pulling.Components; + +#pragma warning disable IDE0130 +namespace Content.Shared.Movement.Pulling.Systems; + +public sealed partial class PullingSystem +{ + [Dependency] private readonly SharedScpHoldingSystem _scpHolding = default!; + + private EntityQuery _pullableQuery; + private EntityQuery _scpHoldQuery; + private EntityQuery _scpHoldableQuery; + private EntityQuery _scpHolderQuery; + + private void InitializeScpHolding() + { + _pullableQuery = GetEntityQuery(); + _scpHoldQuery = GetEntityQuery(); + _scpHoldableQuery = GetEntityQuery(); + _scpHolderQuery = GetEntityQuery(); + } + + private bool TryRedirectPullToScpHold(EntityUid pullerUid, EntityUid pullableUid, + PullerComponent pullerComp, PullableComponent pullableComp, out bool result) + { + result = false; + + if (!_scpHoldQuery.TryComp(pullerUid, out var holdComp) || + !_scpHoldableQuery.HasComp(pullableUid)) + { + return false; + } + + var holder = (pullerUid, holdComp); + + if (_scpHolderQuery.TryComp(pullerUid, out var activeHolder) && + activeHolder.Target != null) + { + result = _scpHolding.TryToggleHold(holder, pullableUid); + return true; + } + + if (!_scpHolding.CanToggleHold(holder, pullableUid, ignoreHandAvailability: pullerComp.Pulling != null)) + return true; + + if (pullerComp.Pulling is { } currentPullUid && + _pullableQuery.TryComp(currentPullUid, out var currentPull) && + !TryStopPull(currentPullUid, currentPull, pullerUid)) + { + return true; + } + + if (pullableComp.Puller != null && + !TryStopPull(pullableUid, pullableComp, pullableComp.Puller)) + { + return true; + } + + result = _scpHolding.TryToggleHold(holder, pullableUid); + return true; + } +} diff --git a/Content.Shared/_Scp/Holding/ScpHeldHandBlockerComponent.cs b/Content.Shared/_Scp/Holding/ScpHeldHandBlockerComponent.cs new file mode 100644 index 00000000000..2fe590610bb --- /dev/null +++ b/Content.Shared/_Scp/Holding/ScpHeldHandBlockerComponent.cs @@ -0,0 +1,23 @@ +using Robust.Shared.GameStates; + +namespace Content.Shared._Scp.Holding; + +/// +/// Marks a victim hand placeholder virtual item created by SCP holding. +/// +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] +[Access(typeof(SharedScpHoldingSystem))] +public sealed partial class ScpHeldHandBlockerComponent : Component +{ + /// + /// Held target whose hand is occupied by this placeholder. + /// + [AutoNetworkedField] + public EntityUid Target; + + /// + /// Holder whose sprite is shown in this placeholder. + /// + [AutoNetworkedField] + public EntityUid Holder; +} diff --git a/Content.Shared/_Scp/Holding/ScpHoldComponent.cs b/Content.Shared/_Scp/Holding/ScpHoldComponent.cs index ae4a2872ca0..5c4899ff14c 100644 --- a/Content.Shared/_Scp/Holding/ScpHoldComponent.cs +++ b/Content.Shared/_Scp/Holding/ScpHoldComponent.cs @@ -23,6 +23,12 @@ public sealed partial class ScpHoldComponent : Component [AutoNetworkedField] public EntityUid? ActionEntity; + /// + /// Cooldown applied to the hold action after each successful use. + /// + [DataField] + public TimeSpan HoldActionCooldown = TimeSpan.FromSeconds(1); + /// /// Minimum delay between soft breakout attempts while the hold is active. /// diff --git a/Content.Shared/_Scp/Holding/ScpHoldableComponent.cs b/Content.Shared/_Scp/Holding/ScpHoldableComponent.cs new file mode 100644 index 00000000000..bfd82fc0c67 --- /dev/null +++ b/Content.Shared/_Scp/Holding/ScpHoldableComponent.cs @@ -0,0 +1,7 @@ +namespace Content.Shared._Scp.Holding; + +/// +/// Marks an entity as a valid target for the SCP holding mechanic. +/// +[RegisterComponent] +public sealed partial class ScpHoldableComponent : Component; diff --git a/Content.Shared/_Scp/Holding/SharedScpHoldingSystem.cs b/Content.Shared/_Scp/Holding/SharedScpHoldingSystem.cs index 928d8aeb92a..e274e123214 100644 --- a/Content.Shared/_Scp/Holding/SharedScpHoldingSystem.cs +++ b/Content.Shared/_Scp/Holding/SharedScpHoldingSystem.cs @@ -2,10 +2,10 @@ using Content.Shared.ActionBlocker; using Content.Shared.Alert; using Content.Shared.Actions; -using Content.Shared.Actions.Components; using Content.Shared.Body.Components; using Content.Shared.Body.Part; using Content.Shared.Body.Systems; +using Content.Shared.Coordinates; using Content.Shared.DoAfter; using Content.Shared.Hands; using Content.Shared.Hands.Components; @@ -19,9 +19,10 @@ using Content.Shared.Popups; using Content.Shared.StatusEffectNew; using Content.Shared.StatusEffectNew.Components; -using Robust.Shared.GameStates; +using Content.Shared.Throwing; +using Robust.Shared.Audio; +using Robust.Shared.Audio.Systems; using Robust.Shared.Containers; -using Robust.Shared.Maths; using Robust.Shared.Network; using Robust.Shared.Physics; using Robust.Shared.Physics.Components; @@ -35,6 +36,7 @@ public sealed class SharedScpHoldingSystem : EntitySystem { [Dependency] private readonly ActionBlockerSystem _actionBlocker = default!; [Dependency] private readonly AlertsSystem _alerts = default!; + [Dependency] private readonly SharedAudioSystem _audio = default!; [Dependency] private readonly SharedActionsSystem _actions = default!; [Dependency] private readonly SharedBodySystem _body = default!; [Dependency] private readonly SharedContainerSystem _container = default!; @@ -51,9 +53,10 @@ public sealed class SharedScpHoldingSystem : EntitySystem [Dependency] private readonly SharedVirtualItemSystem _virtualItem = default!; private const string GrabbedStatusEffect = "StatusEffectScpHeld"; + private const string BreakoutAttemptEffect = "WhistleExclamation"; private const float SoftDragDistanceFactor = 0.3f; - private const float SoftDragMinimumDistance = 0.18f; - private const float SoftDragMaximumDistance = 0.3f; + private const float SoftDragMinimumDistance = 0.4f; + private const float SoftDragMaximumDistance = 0.6f; private const float SoftDragSnapTolerance = 0.03f; private const float SoftDragSettleTolerance = 0.08f; private const float SoftDragVelocityDirectionThreshold = 0.05f; @@ -61,9 +64,14 @@ public sealed class SharedScpHoldingSystem : EntitySystem private const float SoftDragMaximumCorrectionSpeed = 6f; private const float SoftDragAwayVelocityStrength = 0.6f; private const float SoftDragVelocityTolerance = 0.05f; + private static readonly SoundSpecifier BreakoutAttemptSound = + new SoundCollectionSpecifier("storageRustle", + AudioParams.Default.WithVolume(-8f).WithMaxDistance(4f).WithVariation(0.15f)); - private readonly List _holdersToRemove = new(); - private readonly List> _virtualBlockersToDelete = new(); + private readonly List _holdersToRemove = []; + private readonly List _holderCooldownsToApply = []; + private readonly List _placeholderIcons = []; + private readonly List> _virtualBlockersToDelete = []; private EntityQuery _bodyQuery; private EntityQuery _handsQuery; @@ -71,6 +79,7 @@ public sealed class SharedScpHoldingSystem : EntitySystem private EntityQuery _physicsQuery; private EntityQuery _heldQuery; private EntityQuery _holdQuery; + private EntityQuery _holdableQuery; private EntityQuery _holderQuery; public override void Initialize() @@ -83,6 +92,7 @@ public override void Initialize() _physicsQuery = GetEntityQuery(); _heldQuery = GetEntityQuery(); _holdQuery = GetEntityQuery(); + _holdableQuery = GetEntityQuery(); _holderQuery = GetEntityQuery(); SubscribeLocalEvent(OnHoldStartup); @@ -97,13 +107,17 @@ public override void Initialize() SubscribeLocalEvent(OnHeldMoveInput); SubscribeLocalEvent(OnHandCountChanged); SubscribeLocalEvent(OnHeldUpdateCanMove); + SubscribeLocalEvent(OnHeldAttemptMobCollide); + SubscribeLocalEvent(OnHeldAttemptMobTargetCollide); SubscribeLocalEvent(OnHeldPreventCollide); SubscribeLocalEvent(OnHolderStartup); SubscribeLocalEvent(OnHolderShutdown); SubscribeLocalEvent(OnHolderRefreshMoveSpeed); + SubscribeLocalEvent(OnHolderBeforeThrow); SubscribeLocalEvent(OnHolderHandsModified); SubscribeLocalEvent(OnHolderPreventCollide); + SubscribeLocalEvent(OnHolderBlockerDropped); } public override void Update(float frameTime) @@ -133,6 +147,9 @@ public override void Update(float frameTime) private void OnHoldStartup(Entity ent, ref ComponentStartup args) { _actions.AddAction(ent.Owner, ref ent.Comp.ActionEntity, ent.Comp.Action); + + if (ent.Comp.ActionEntity != null) + _actions.SetUseDelay(ent.Comp.ActionEntity.Value, ent.Comp.HoldActionCooldown); } private void OnHoldShutdown(Entity ent, ref ComponentShutdown args) @@ -170,16 +187,15 @@ private void OnHeldShutdown(Entity ent, ref ComponentShutdown _actions.RemoveAction(ent.Comp.BreakoutActionEntity); _alerts.ClearAlert(ent.Owner, "ScpHoldGrabbed"); _statusEffects.TryRemoveStatusEffect(ent.Owner, GrabbedStatusEffect); - _virtualItem.DeleteInHandsMatching(ent.Owner, ent.Owner); + DeleteHeldHandBlockers(ent.Owner); if (!_timing.ApplyingState) CancelBreakoutDoAfter(ent); if (!_net.IsClient) { - for (var i = 0; i < ent.Comp.Holders.Count; i++) + foreach (var holderUid in ent.Comp.Holders) { - var holderUid = ent.Comp.Holders[i]; if (!TerminatingOrDeleted(holderUid) && _holderQuery.HasComp(holderUid)) RemComp(holderUid); } @@ -251,6 +267,16 @@ private void OnHeldUpdateCanMove(Entity ent, ref UpdateCanMove args.Cancel(); } + private static void OnHeldAttemptMobCollide(Entity ent, ref AttemptMobCollideEvent args) + { + args.Cancelled = true; + } + + private static void OnHeldAttemptMobTargetCollide(Entity ent, ref AttemptMobTargetCollideEvent args) + { + args.Cancelled = true; + } + private void OnHeldPreventCollide(Entity ent, ref PreventCollideEvent args) { if (args.Cancelled) @@ -284,6 +310,19 @@ private void OnHolderRefreshMoveSpeed(Entity ent, ref Refres args.ModifySpeed(ent.Comp.WalkModifier, ent.Comp.SprintModifier); } + private void OnHolderBeforeThrow(Entity ent, ref BeforeThrowEvent args) + { + if (ent.Comp.Target == null || + !TryComp(args.ItemUid, out var blocker) || + blocker.Target != ent.Comp.Target.Value) + { + return; + } + + ReleaseHolderContribution(ent.Owner, ent.Comp.Target.Value, clearIfEmpty: true); + args.Cancelled = true; + } + private void OnHolderHandsModified(Entity ent, ref DidEquipHandEvent args) { if (ent.Comp.LifeStage > ComponentLifeStage.Running || @@ -309,8 +348,23 @@ private void OnHolderPreventCollide(Entity ent, ref PreventC args.Cancelled = true; } + private void OnHolderBlockerDropped(Entity ent, ref GettingDroppedAttemptEvent args) + { + if (!_holderQuery.TryComp(args.User, out var holder) || + holder.Target == null || + holder.Target != ent.Comp.Target) + { + return; + } + + ReleaseHolderContribution(args.User, ent.Comp.Target, clearIfEmpty: true); + } + public bool TryToggleHold(Entity holder, EntityUid target) { + if (!CanUseHoldAction(holder)) + return false; + if (_holderQuery.TryComp(holder.Owner, out var activeHolder) && activeHolder.Target != null) { if (activeHolder.Target.Value == target) @@ -332,11 +386,25 @@ public bool TryToggleHold(Entity holder, EntityUid target) return true; } - public bool CanToggleHold(Entity holder, EntityUid target, bool quiet = false) + public bool CanToggleHold( + Entity holder, + EntityUid target, + bool quiet = false, + bool ignoreHandAvailability = false) { if (!Exists(target) || holder.Owner == target) return false; + if (!CanUseHoldAction(holder, quiet)) + return false; + + if (!_holdableQuery.HasComp(target)) + { + if (!quiet) + PopupHolder(holder.Owner, "scp-hold-target-not-holdable", ("target", target)); + return false; + } + if (!_moverQuery.HasComp(holder.Owner) || !_moverQuery.HasComp(target) || !_physicsQuery.TryComp(target, out var targetPhysics) || @@ -361,7 +429,7 @@ public bool CanToggleHold(Entity holder, EntityUid target, boo return false; } - if (!HasAvailableHolderHand(holder.Owner)) + if (!ignoreHandAvailability && !HasAvailableHolderHand(holder.Owner)) { if (!quiet) PopupHolder(holder.Owner, "scp-hold-holder-no-free-hand", ("target", target)); @@ -394,7 +462,7 @@ public bool CanToggleHold(Entity holder, EntityUid target, boo public bool TryBreakOut(Entity held, bool viaMovement) { return held.Comp.FullHold - ? TryStartFullBreakout(held, viaMovement) + ? TryStartFullBreakout(held) : TrySoftBreakOut(held, viaMovement); } @@ -427,21 +495,33 @@ private bool TrySoftBreakOut(Entity held, bool viaMovement) return true; } - private bool TryStartFullBreakout(Entity held, bool viaMovement) + private bool TryStartFullBreakout(Entity held) { - if (held.Comp.FullHoldStartedAt == null || - _timing.CurTime < held.Comp.FullHoldStartedAt.Value + held.Comp.FullHoldDelay) + if (held.Comp.FullHoldStartedAt == null) { - if (!viaMovement) - PopupTarget(held.Owner, "scp-hold-breakout-too-early"); + PopupTarget(held.Owner, "scp-hold-breakout-too-early", ("seconds", 1)); + return false; + } + + var breakoutAvailableAt = held.Comp.FullHoldStartedAt.Value + held.Comp.FullHoldDelay; + if (_timing.CurTime < breakoutAvailableAt) + { + var remaining = breakoutAvailableAt - _timing.CurTime; + var remainingSeconds = Math.Max(1, (int) Math.Ceiling(remaining.TotalSeconds)); + PopupTarget(held.Owner, "scp-hold-breakout-too-early", ("seconds", remainingSeconds)); return false; } if (held.Comp.BreakoutDoAfterId != null) return true; - var doAfter = new DoAfterArgs(EntityManager, held.Owner, held.Comp.FullBreakoutDuration, - new ScpHoldBreakoutDoAfterEvent(), held.Owner, target: held.Owner) + var doAfter = new DoAfterArgs( + EntityManager, + held.Owner, + held.Comp.FullBreakoutDuration, + new ScpHoldBreakoutDoAfterEvent(), + held.Owner, + target: held.Owner) { BreakOnMove = true, BreakOnDamage = true, @@ -453,6 +533,8 @@ private bool TryStartFullBreakout(Entity held, bool viaMovemen return false; SetBreakoutDoAfterId(held, id.Value.Index); + ShowBreakoutAttemptFeedback(held); + PopupTarget(held.Owner, "scp-hold-breakout-start"); return true; } @@ -475,10 +557,8 @@ private void UpdateHeld(Entity held) _holdersToRemove.Clear(); - for (var i = 0; i < held.Comp.Holders.Count; i++) + foreach (var holderUid in held.Comp.Holders) { - var holderUid = held.Comp.Holders[i]; - if (!Exists(holderUid) || !_holdQuery.HasComp(holderUid) || !_holderQuery.TryComp(holderUid, out var holder) || @@ -490,9 +570,9 @@ private void UpdateHeld(Entity held) } } - for (var i = 0; i < _holdersToRemove.Count; i++) + foreach (var holderUid in _holdersToRemove) { - ReleaseHolderContribution(_holdersToRemove[i], held.Owner, clearIfEmpty: false); + ReleaseHolderContribution(holderUid, held.Owner, clearIfEmpty: false); if (!_heldQuery.TryComp(held.Owner, out _)) return; @@ -631,10 +711,8 @@ private bool EnsurePrimaryHolder(Entity held) held.Comp.PrimaryHolder = null; - for (var i = 0; i < held.Comp.Holders.Count; i++) + foreach (var holderUid in held.Comp.Holders) { - var holderUid = held.Comp.Holders[i]; - if (!_holderQuery.TryComp(holderUid, out var holder) || holder.Target != held.Owner) { @@ -674,8 +752,6 @@ private void UpdateSoftDrag(Entity held, float maintenanceRang var holderVelocity = _physicsQuery.TryComp(primaryHolder, out var holderPhysics) ? holderPhysics.LinearVelocity : Vector2.Zero; - var holderSpeed = holderVelocity.Length(); - var direction = GetSoftDragDirection(primaryHolder, holderVelocity, offset, distance); var desiredPosition = holderCoords.Position + direction * desiredDistance; var correction = desiredPosition - heldCoords.Position; @@ -709,12 +785,15 @@ private void ClearHoldState(Entity held, bool applyImmunity) held = (held.Owner, refreshed); CancelBreakoutDoAfter(held); - _virtualItem.DeleteInHandsMatching(held.Owner, held.Owner); + DeleteHeldHandBlockers(held.Owner); _actionBlocker.UpdateCanMove(held.Owner); + _holderCooldownsToApply.Clear(); - for (var i = 0; i < held.Comp.Holders.Count; i++) + foreach (var holderUid in held.Comp.Holders) { - var holderUid = held.Comp.Holders[i]; + if (applyImmunity) + _holderCooldownsToApply.Add(holderUid); + if (_holderQuery.HasComp(holderUid)) RemComp(holderUid); } @@ -729,14 +808,18 @@ private void ClearHoldState(Entity held, bool applyImmunity) Dirty(held.Owner, immune); } + foreach (var holderUid in _holderCooldownsToApply) + { + ApplyFullBreakoutHolderCooldown(holderUid); + } + RemComp(held.Owner); } private void UpdateHolderSlowdowns(Entity held) { - for (var i = 0; i < held.Comp.Holders.Count; i++) + foreach (var holderUid in held.Comp.Holders) { - var holderUid = held.Comp.Holders[i]; if (!_holderQuery.TryComp(holderUid, out var holder)) continue; @@ -762,7 +845,7 @@ private void SetHolderSlowdown(Entity holder, bool enabled, private void SyncPlaceholderHands(Entity held) { - _virtualItem.DeleteInHandsMatching(held.Owner, held.Owner); + DeleteHeldHandBlockers(held.Owner); if (!held.Comp.FullHold || !_handsQuery.TryComp(held.Owner, out var hands)) return; @@ -778,9 +861,34 @@ private void SyncPlaceholderHands(Entity held) _hands.DoDrop((held.Owner, hands), hand, doDropInteraction: true); } - while (_virtualItem.TrySpawnVirtualItemInHand(held.Owner, held.Owner, out var virtualItem, silent: true)) + _placeholderIcons.Clear(); + foreach (var holderUid in held.Comp.Holders) { + if (_holderQuery.TryComp(holderUid, out var holder) && holder.Target == held.Owner) + _placeholderIcons.Add(holderUid); + } + + if (_placeholderIcons.Count == 0) + return; + + var iconIndex = 0; + while (_hands.TryGetEmptyHand((held.Owner, hands), out var emptyHand)) + { + var holderUid = _placeholderIcons[iconIndex % _placeholderIcons.Count]; + if (!_virtualItem.TrySpawnVirtualItemInHand(holderUid, held.Owner, out var virtualItem, empty: emptyHand, silent: true)) + break; + EnsureComp(virtualItem.Value); + var blocker = EnsureComp(virtualItem.Value); + + if (blocker.Target != held.Owner || blocker.Holder != holderUid) + { + blocker.Target = held.Owner; + blocker.Holder = holderUid; + Dirty(virtualItem.Value, blocker); + } + + iconIndex++; } } @@ -818,7 +926,7 @@ private void SyncHolderHandBlocker(Entity holder) if (validBlocker == null) { validBlocker = heldItem; - EnsureComp(heldItem); + RemComp(heldItem); var blocker = EnsureComp(heldItem); var currentTarget = target!.Value; if (blocker.Target != currentTarget) @@ -834,9 +942,9 @@ private void SyncHolderHandBlocker(Entity holder) _virtualBlockersToDelete.Add((heldItem, virtualItem)); } - for (var i = 0; i < _virtualBlockersToDelete.Count; i++) + foreach (var virtualItem in _virtualBlockersToDelete) { - _virtualItem.DeleteVirtualItem(_virtualBlockersToDelete[i], holder.Owner); + _virtualItem.DeleteVirtualItem(virtualItem, holder.Owner); } if (holder.Comp.LifeStage > ComponentLifeStage.Running || @@ -855,7 +963,6 @@ private void SyncHolderHandBlocker(Entity holder) if (!_virtualItem.TrySpawnVirtualItemInHand(holder.Comp.Target.Value, holder.Owner, out var spawnedVirtualItem, silent: true)) return; - EnsureComp(spawnedVirtualItem.Value); var blockerComp = EnsureComp(spawnedVirtualItem.Value); blockerComp.Target = holder.Comp.Target.Value; Dirty(spawnedVirtualItem.Value, blockerComp); @@ -880,9 +987,28 @@ private void DeleteHolderHandBlockers(EntityUid holderUid) } } - for (var i = 0; i < _virtualBlockersToDelete.Count; i++) + foreach (var virtualItem in _virtualBlockersToDelete) + { + _virtualItem.DeleteVirtualItem(virtualItem, holderUid); + } + } + + private void DeleteHeldHandBlockers(EntityUid heldUid) + { + _virtualBlockersToDelete.Clear(); + + foreach (var heldItem in _hands.EnumerateHeld(heldUid)) + { + if (TryComp(heldItem, out _) && + TryComp(heldItem, out var virtualItem)) + { + _virtualBlockersToDelete.Add((heldItem, virtualItem)); + } + } + + foreach (var virtualItem in _virtualBlockersToDelete) { - _virtualItem.DeleteVirtualItem(_virtualBlockersToDelete[i], holderUid); + _virtualItem.DeleteVirtualItem(virtualItem, heldUid); } } @@ -916,6 +1042,44 @@ private void CopyConfig(ScpHoldComponent source, ScpHeldComponent target) target.FullHoldStartedAt = null; } + private bool CanUseHoldAction(Entity holder, bool quiet = false) + { + if (!IsHoldActionCoolingDown(holder, out var remaining)) + return true; + + if (!quiet) + { + var remainingSeconds = Math.Max(1, (int) Math.Ceiling(remaining.TotalSeconds)); + PopupHolder(holder.Owner, "scp-hold-holder-action-on-cooldown", ("seconds", remainingSeconds)); + } + + return false; + } + + private bool IsHoldActionCoolingDown(Entity holder, out TimeSpan remaining) + { + remaining = TimeSpan.Zero; + + if (holder.Comp.ActionEntity is not { } actionUid) + return false; + + var action = _actions.GetAction(actionUid); + if (action?.Comp.Cooldown is not { } cooldown || cooldown.End <= _timing.CurTime) + return false; + + remaining = cooldown.End - _timing.CurTime; + return true; + } + + private void ApplyFullBreakoutHolderCooldown(EntityUid holderUid) + { + if (!_holdQuery.TryComp(holderUid, out var hold) || hold.ActionEntity == null) + return; + + var cooldown = TimeSpan.FromTicks(hold.HoldActionCooldown.Ticks * 2); + _actions.SetIfBiggerCooldown(hold.ActionEntity.Value, cooldown); + } + private float GetDesiredSoftDragDistance(Entity held) { return GetBaseSoftDragDistance(held.Comp.HoldRange); @@ -986,6 +1150,28 @@ private void SetBreakoutDoAfterId(Entity held, ushort? breakou Dirty(held); } + private void ShowBreakoutAttemptFeedback(Entity held) + { + if (_net.IsClient && !_timing.IsFirstTimePredicted) + return; + + foreach (var holderUid in held.Comp.Holders) + { + if (TerminatingOrDeleted(holderUid) || !_holderQuery.TryComp(holderUid, out var holder) || holder.Target != held.Owner) + continue; + + if (_net.IsClient) + PredictedSpawnAttachedTo(BreakoutAttemptEffect, holderUid.ToCoordinates()); + else + SpawnAttachedTo(BreakoutAttemptEffect, holderUid.ToCoordinates()); + } + + if (_net.IsClient) + _audio.PlayPredicted(BreakoutAttemptSound, held.Owner, held.Owner); + else + _audio.PlayPvs(BreakoutAttemptSound, held.Owner); + } + private void PopupHolder(EntityUid holder, string key, params (string, object)[] args) { if (_net.IsClient) diff --git a/Resources/Locale/en-US/_strings/_scp/holding/holding.ftl b/Resources/Locale/en-US/_strings/_scp/holding/holding.ftl index 64199813c21..137966b2281 100644 --- a/Resources/Locale/en-US/_strings/_scp/holding/holding.ftl +++ b/Resources/Locale/en-US/_strings/_scp/holding/holding.ftl @@ -1,10 +1,12 @@ scp-hold-already-holding-other = You are already holding someone else. scp-hold-target-invalid = {CAPITALIZE(THE($target))} cannot be held. +scp-hold-target-not-holdable = {CAPITALIZE(THE($target))} cannot be grabbed with this hold. scp-hold-target-immune = {CAPITALIZE(THE($target))} cannot be held yet. scp-hold-target-fully-held = {CAPITALIZE(THE($target))} is already fully held. scp-hold-target-too-far = You are too far away to hold {THE($target)}. scp-hold-holder-no-free-hand = You need a free hand to grab {THE($target)}. -scp-hold-breakout-too-early = You need to endure the hold a little longer before breaking free. +scp-hold-holder-action-on-cooldown = You can grab again in {$seconds} s. +scp-hold-breakout-too-early = You can try to break free in {$seconds} s. scp-hold-breakout-start = You start trying to break free. scp-hold-breakout-interrupted = Your breakout attempt was interrupted. alerts-scp-held-name = Forcefully restrained diff --git a/Resources/Locale/ru-RU/_strings/_scp/holding/holding.ftl b/Resources/Locale/ru-RU/_strings/_scp/holding/holding.ftl index d68c70c219b..b1970366ec7 100644 --- a/Resources/Locale/ru-RU/_strings/_scp/holding/holding.ftl +++ b/Resources/Locale/ru-RU/_strings/_scp/holding/holding.ftl @@ -1,10 +1,12 @@ scp-hold-already-holding-other = Вы уже удерживаете кого-то другого. scp-hold-target-invalid = {CAPITALIZE(THE($target))} нельзя удержать. +scp-hold-target-not-holdable = {CAPITALIZE(THE($target))} нельзя схватить этим удержанием. scp-hold-target-immune = {CAPITALIZE(THE($target))} пока нельзя удержать. scp-hold-target-fully-held = {CAPITALIZE(THE($target))} уже полностью удерживается. scp-hold-target-too-far = Вы слишком далеко, чтобы удерживать {THE($target)}. scp-hold-holder-no-free-hand = Чтобы схватить {THE($target)}, нужна свободная рука. -scp-hold-breakout-too-early = Нужно продержаться в удержании ещё немного, прежде чем вырываться. +scp-hold-holder-action-on-cooldown = Схватить снова можно через {$seconds} с. +scp-hold-breakout-too-early = Попытаться вырваться можно через {$seconds} с. scp-hold-breakout-start = Вы начинаете вырываться. scp-hold-breakout-interrupted = Попытка вырваться была прервана. alerts-scp-held-name = Вас удерживают силой diff --git a/Resources/Prototypes/Entities/Mobs/Species/base.yml b/Resources/Prototypes/Entities/Mobs/Species/base.yml index 6085f9ff1e1..797aa999bd1 100644 --- a/Resources/Prototypes/Entities/Mobs/Species/base.yml +++ b/Resources/Prototypes/Entities/Mobs/Species/base.yml @@ -278,6 +278,8 @@ - type: CritHeartbeat # Sunrise-End # Fire start + - type: ScpHold + - type: ScpHoldable - type: FieldOfView - type: Blinkable - type: SpeakOnEyeStateChange From e943403cf86aaeba7547d8e311459485060d1fcf Mon Sep 17 00:00:00 2001 From: drdth Date: Wed, 8 Apr 2026 05:15:00 +0300 Subject: [PATCH 04/27] add: update with many features --- .../Tests/_Scp/ScpHoldingTest.cs | 236 +++- .../EntitySystems/SharedHandsSystem.Drop.cs | 12 +- .../_Scp/Holding/PullingSystem.ScpHolding.cs | 7 +- .../_Scp/Holding/ScpHeldComponent.cs | 10 +- .../_Scp/Holding/ScpHoldComponent.cs | 44 +- .../_Scp/Holding/ScpHoldableComponent.cs | 64 +- .../_Scp/Holding/ScpHoldingEvents.cs | 28 +- .../Holding/SharedScpHoldingSystem.Actions.cs | 266 ++++ .../Holding/SharedScpHoldingSystem.Drag.cs | 129 ++ .../Holding/SharedScpHoldingSystem.Events.cs | 259 ++++ .../SharedScpHoldingSystem.Feedback.cs | 76 ++ .../Holding/SharedScpHoldingSystem.Hands.cs | 195 +++ .../Holding/SharedScpHoldingSystem.State.cs | 304 +++++ .../_Scp/Holding/SharedScpHoldingSystem.cs | 1132 +---------------- 14 files changed, 1584 insertions(+), 1178 deletions(-) create mode 100644 Content.Shared/_Scp/Holding/SharedScpHoldingSystem.Actions.cs create mode 100644 Content.Shared/_Scp/Holding/SharedScpHoldingSystem.Drag.cs create mode 100644 Content.Shared/_Scp/Holding/SharedScpHoldingSystem.Events.cs create mode 100644 Content.Shared/_Scp/Holding/SharedScpHoldingSystem.Feedback.cs create mode 100644 Content.Shared/_Scp/Holding/SharedScpHoldingSystem.Hands.cs create mode 100644 Content.Shared/_Scp/Holding/SharedScpHoldingSystem.State.cs diff --git a/Content.IntegrationTests/Tests/_Scp/ScpHoldingTest.cs b/Content.IntegrationTests/Tests/_Scp/ScpHoldingTest.cs index 025cf932551..b84dba69862 100644 --- a/Content.IntegrationTests/Tests/_Scp/ScpHoldingTest.cs +++ b/Content.IntegrationTests/Tests/_Scp/ScpHoldingTest.cs @@ -3,6 +3,7 @@ using System.Linq; using System.Numerics; using System.Reflection; +using Content.IntegrationTests.Tests.Helpers; using Content.Shared.Alert; using Content.Server.Body.Systems; using Content.Shared._Scp.Holding; @@ -17,6 +18,7 @@ using Content.Shared.Movement.Components; using Content.Shared.Movement.Events; using Content.Shared.Movement.Pulling.Components; +using Content.Shared.Movement.Pulling.Systems; using Content.Shared.Movement.Systems; using Content.Shared.StatusEffectNew; using Content.Shared.Throwing; @@ -29,6 +31,7 @@ using Robust.Shared.Physics.Systems; using Robust.Shared.Prototypes; using Robust.Shared.Timing; +using Content.Shared.Whitelist; namespace Content.IntegrationTests.Tests._Scp; @@ -36,9 +39,21 @@ namespace Content.IntegrationTests.Tests._Scp; public sealed class ScpHoldingTest { private const string HolderPrototype = "ScpHoldingTestHolder"; + private const string HoldableWhitelistedHolderPrototype = "ScpHoldingTestHolderHoldableWhitelisted"; + private const string HoldableBlacklistedHolderPrototype = "ScpHoldingTestHolderHoldableBlacklisted"; + private const string TestListenerComponentName = "TestListener"; + private static readonly ProtoId GrabbedAlertId = "ScpHoldGrabbed"; private static readonly FieldInfo SoftEscapeAvailableAtField = typeof(ScpHeldComponent).GetField(nameof(ScpHeldComponent.SoftEscapeAvailableAt))!; + private static EntityWhitelist CreateComponentWhitelist(params string[] components) + { + return new EntityWhitelist + { + Components = components, + }; + } + [TestPrototypes] private const string Prototypes = """ - type: entity @@ -46,6 +61,22 @@ public sealed class ScpHoldingTest parent: MobHuman components: - type: ScpHold +- type: entity + id: ScpHoldingTestHolderHoldableWhitelisted + parent: ScpHoldingTestHolder + components: + - type: ScpHold + holdableWhitelist: + components: + - TestListener +- type: entity + id: ScpHoldingTestHolderHoldableBlacklisted + parent: ScpHoldingTestHolder + components: + - type: ScpHold + holdableBlacklist: + components: + - TestListener """; [Test] @@ -110,7 +141,7 @@ await server.WaitAssertion(() => await server.WaitPost(() => { - var alert = proto.Index("ScpHoldGrabbed"); + var alert = proto.Index(GrabbedAlertId); Assert.That(alerts.ActivateAlert(target, alert), Is.True); }); await server.WaitRunTicks(2); @@ -140,7 +171,7 @@ await server.WaitPost(() => { var held = entMan.GetComponent(target); SetSoftEscapeAvailableAt(held, timing.CurTime); - var alert = proto.Index("ScpHoldGrabbed"); + var alert = proto.Index(GrabbedAlertId); Assert.That(alerts.ActivateAlert(target, alert), Is.True); }); await server.WaitRunTicks(2); @@ -520,6 +551,157 @@ await server.WaitAssertion(() => await pair.CleanReturnAsync(); } + [Test] + public async Task HolderAndHoldableFiltersUseCheckBoth() + { + await using var pair = await PoolManager.GetServerClient(new PoolSettings { Fresh = true }); + var server = pair.Server; + var entMan = server.EntMan; + var holding = server.System(); + var map = await pair.CreateTestMap(); + + EntityUid successHolder = default; + EntityUid blockedHolder = default; + EntityUid blacklistHolder = default; + EntityUid successTarget = default; + EntityUid blockedTarget = default; + EntityUid blacklistTarget = default; + EntityUid holderBlacklistTarget = default; + + await server.WaitPost(() => + { + successHolder = entMan.SpawnEntity(HoldableWhitelistedHolderPrototype, map.GridCoords); + blockedHolder = entMan.SpawnEntity(HolderPrototype, map.GridCoords); + blacklistHolder = entMan.SpawnEntity(HoldableBlacklistedHolderPrototype, map.GridCoords); + successTarget = entMan.SpawnEntity("MobHuman", map.GridCoords); + blockedTarget = entMan.SpawnEntity("MobHuman", map.GridCoords); + blacklistTarget = entMan.SpawnEntity("MobHuman", map.GridCoords); + holderBlacklistTarget = entMan.SpawnEntity("MobHuman", map.GridCoords); + + entMan.AddComponent(successHolder); + entMan.AddComponent(blacklistHolder); + entMan.AddComponent(successTarget); + entMan.AddComponent(blacklistTarget); + entMan.AddComponent(holderBlacklistTarget); + + var successTargetHoldable = entMan.GetComponent(successTarget); + successTargetHoldable.HolderWhitelist = CreateComponentWhitelist(TestListenerComponentName); + + var holderBlacklistHoldable = entMan.GetComponent(holderBlacklistTarget); + holderBlacklistHoldable.HolderBlacklist = CreateComponentWhitelist(TestListenerComponentName); + }); + + await server.WaitAssertion(() => + { + var successHold = entMan.GetComponent(successHolder); + var blockedHold = entMan.GetComponent(blockedHolder); + var blacklistHold = entMan.GetComponent(blacklistHolder); + + Assert.Multiple(() => + { + Assert.That(holding.CanToggleHold((successHolder, successHold), blockedTarget, quiet: true), Is.False); + Assert.That(holding.TryToggleHold((successHolder, successHold), blockedTarget), Is.False); + + Assert.That(holding.CanToggleHold((blockedHolder, blockedHold), successTarget, quiet: true), Is.False); + Assert.That(holding.TryToggleHold((blockedHolder, blockedHold), successTarget), Is.False); + + Assert.That(holding.CanToggleHold((blacklistHolder, blacklistHold), blacklistTarget, quiet: true), Is.False); + Assert.That(holding.TryToggleHold((blacklistHolder, blacklistHold), blacklistTarget), Is.False); + + Assert.That(holding.CanToggleHold((successHolder, successHold), holderBlacklistTarget, quiet: true), Is.False); + Assert.That(holding.TryToggleHold((successHolder, successHold), holderBlacklistTarget), Is.False); + + Assert.That(holding.CanToggleHold((successHolder, successHold), successTarget, quiet: true), Is.True); + Assert.That(holding.TryToggleHold((successHolder, successHold), successTarget), Is.True); + + Assert.That(entMan.HasComponent(blockedTarget), Is.False); + Assert.That(entMan.HasComponent(successTarget), Is.True); + }); + }); + + await pair.CleanReturnAsync(); + } + + [Test] + public async Task HoldAttemptEventCanCancelGrab() + { + await using var pair = await PoolManager.GetServerClient(new PoolSettings { Fresh = true }); + var server = pair.Server; + var entMan = server.EntMan; + var holding = server.System(); + var attempts = server.System(); + _ = server.System(); + var map = await pair.CreateTestMap(); + + EntityUid holder = default; + EntityUid target = default; + + await server.WaitPost(() => + { + holder = entMan.SpawnEntity(HolderPrototype, map.GridCoords); + target = entMan.SpawnEntity("MobHuman", map.GridCoords); + entMan.AddComponent(holder); + entMan.AddComponent(target); + entMan.AddComponent(target); + }); + + await server.WaitAssertion(() => + { + var holdComp = entMan.GetComponent(holder); + + Assert.Multiple(() => + { + Assert.That(holding.TryToggleHold((holder, holdComp), target), Is.False); + Assert.That(entMan.HasComponent(target), Is.False); + Assert.That(attempts.Count(target), Is.EqualTo(1)); + Assert.That(attempts.Count(holder), Is.EqualTo(1)); + }); + }); + + await pair.CleanReturnAsync(); + } + + [Test] + public async Task BreakoutEventRaisedWhenTargetEscapes() + { + await using var pair = await PoolManager.GetServerClient(new PoolSettings { Fresh = true }); + var server = pair.Server; + var entMan = server.EntMan; + var holding = server.System(); + var breakouts = server.System(); + var map = await pair.CreateTestMap(); + + EntityUid holder = default; + EntityUid target = default; + + await server.WaitPost(() => + { + holder = entMan.SpawnEntity(HolderPrototype, map.GridCoords); + target = entMan.SpawnEntity("MobHuman", map.GridCoords); + entMan.AddComponent(target); + StartHold(entMan, holding, holder, target); + }); + + await server.WaitPost(() => RaiseMoveInput(entMan, target)); + await server.WaitRunTicks(2); + + await server.WaitAssertion(() => + { + var breakout = breakouts.GetEvents(target).Single(); + + Assert.Multiple(() => + { + Assert.That(entMan.HasComponent(target), Is.False); + Assert.That(breakouts.Count(target), Is.EqualTo(1)); + Assert.That(breakout.ViaMovement, Is.True); + Assert.That(breakout.WasFullHold, Is.False); + Assert.That(breakout.AppliedImmunity, Is.False); + }); + }); + + await pair.CleanReturnAsync(); + } + [Test] public async Task MultiHandTargetNeedsMatchingHolderCountAndResyncsOnHandLoss() { @@ -870,6 +1052,7 @@ await client.WaitAssertion(() => Assert.That(cEntMan.HasComponent(clientPlayer), Is.True); Assert.That(cEntMan.HasComponent(clientTarget), Is.True); Assert.That(CountHolderTargetVirtualItems(cEntMan, cHandsSystem, clientPlayer, clientTarget, hands), Is.EqualTo(1)); + Assert.That(CountPrototypeEntities(cEntMan, "clientsideclone"), Is.EqualTo(0)); }); }); @@ -877,14 +1060,16 @@ await client.WaitPost(() => { var hands = cEntMan.GetComponent(clientPlayer); var blocker = FindHolderHandBlocker(cEntMan, cHandsSystem, clientPlayer, clientTarget, hands); + var dropLocation = new EntityCoordinates(clientPlayer, new Vector2(0.5f, 0f)); Assert.That(blocker, Is.Not.EqualTo(EntityUid.Invalid)); - Assert.That(cHandsSystem.TryDrop((clientPlayer, hands), blocker), Is.True); + Assert.That(cHandsSystem.TryDrop((clientPlayer, hands), blocker, dropLocation), Is.True); Assert.Multiple(() => { Assert.That(cEntMan.HasComponent(clientTarget), Is.False); Assert.That(cEntMan.HasComponent(clientPlayer), Is.False); Assert.That(CountHolderTargetVirtualItems(cEntMan, cHandsSystem, clientPlayer, clientTarget, hands), Is.EqualTo(0)); + Assert.That(CountPrototypeEntities(cEntMan, "clientsideclone"), Is.EqualTo(0)); }); }); @@ -903,7 +1088,11 @@ await client.WaitPost(() => }); } - Assert.That(maxClientBlockers, Is.EqualTo(0)); + Assert.Multiple(() => + { + Assert.That(maxClientBlockers, Is.EqualTo(0)); + Assert.That(CountPrototypeEntities(cEntMan, "clientsideclone"), Is.EqualTo(0)); + }); await pair.CleanReturnAsync(); } @@ -1847,6 +2036,23 @@ private static int CountAttachedPrototype(IEntityManager entMan, EntityUid paren return count; } + private static int CountPrototypeEntities(IEntityManager entMan, string prototypeId) + { + var count = 0; + var query = entMan.AllEntityQueryEnumerator(); + + while (query.MoveNext(out _, out var metadata)) + { + if (!metadata.Deleted && + metadata.EntityPrototype?.ID == prototypeId) + { + count++; + } + } + + return count; + } + private static int CountHolderHandBlockers(IEntityManager entMan, SharedHandsSystem handsSystem, EntityUid holder, EntityUid target, HandsComponent hands) { return handsSystem.EnumerateHeld((holder, hands)).Count(item => @@ -1900,7 +2106,7 @@ private static float GetLargestDistanceStep(float[] samples) private static int GetTickCount(IGameTiming timing, TimeSpan duration) { - return Math.Max(1, (int) Math.Ceiling(duration.TotalSeconds / timing.TickPeriod.TotalSeconds) + 1); + return Math.Max(1, (int)Math.Ceiling(duration.TotalSeconds / timing.TickPeriod.TotalSeconds) + 1); } private static TimeSpan GetActionCooldownRemaining(IEntityManager entMan, EntityUid action, IGameTiming timing) @@ -1938,3 +2144,23 @@ private static EntityUid ToClientEntity(IEntityManager serverEntMan, IEntityMana return clientEntMan.GetEntity(serverEntMan.GetNetEntity(serverEntity)); } } + +[RegisterComponent] +public sealed partial class ScpHoldAttemptCancelTestComponent : Component; + +public sealed class ScpHoldAttemptListenerSystem : TestListenerSystem; + +public sealed class ScpHoldBreakoutListenerSystem : TestListenerSystem; + +public sealed class ScpHoldAttemptCancelSystem : EntitySystem +{ + public override void Initialize() + { + SubscribeLocalEvent(OnAttempt); + } + + private static void OnAttempt(Entity ent, ref ScpHoldAttemptEvent args) + { + args.Cancel(); + } +} diff --git a/Content.Shared/Hands/EntitySystems/SharedHandsSystem.Drop.cs b/Content.Shared/Hands/EntitySystems/SharedHandsSystem.Drop.cs index 94ab593f905..2102cb71832 100644 --- a/Content.Shared/Hands/EntitySystems/SharedHandsSystem.Drop.cs +++ b/Content.Shared/Hands/EntitySystems/SharedHandsSystem.Drop.cs @@ -77,7 +77,7 @@ public bool CanDropHeld(EntityUid uid, string handId, bool checkActionBlocker = if (!ContainerSystem.TryGetContainer(uid, handId, out var container)) return false; - if (container.ContainedEntities.FirstOrNull() is not {} held) + if (container.ContainedEntities.FirstOrNull() is not { } held) return false; if (!ContainerSystem.CanRemove(held, container)) @@ -139,9 +139,13 @@ public bool TryDrop(Entity ent, string handId, EntityCoordinate return false; // Fire added end - // if item is a fake item (like with pulling), just delete it rather than bothering with trying to drop it into the world - if (TryComp(entity, out VirtualItemComponent? @virtual)) - _virtualSystem.DeleteVirtualItem((entity.Value, @virtual), ent); + // Fire edit start - virtual items should leave the hand through the normal removal path, not through world drop handling + if (TryComp(entity, out VirtualItemComponent? _)) + { + DoDrop(ent, handId, doDropInteraction: false); + return true; + } + // Fire edit end if (TerminatingOrDeleted(entity)) return true; diff --git a/Content.Shared/_Scp/Holding/PullingSystem.ScpHolding.cs b/Content.Shared/_Scp/Holding/PullingSystem.ScpHolding.cs index fa2b38eb8f9..ee78115692b 100644 --- a/Content.Shared/_Scp/Holding/PullingSystem.ScpHolding.cs +++ b/Content.Shared/_Scp/Holding/PullingSystem.ScpHolding.cs @@ -41,7 +41,10 @@ private bool TryRedirectPullToScpHold(EntityUid pullerUid, EntityUid pullableUid return true; } - if (!_scpHolding.CanToggleHold(holder, pullableUid, ignoreHandAvailability: pullerComp.Pulling != null)) + if (!_scpHolding.CanToggleHold(holder, + pullableUid, + ignoreHandAvailability: pullerComp.Pulling != null, + checkAttempt: true)) return true; if (pullerComp.Pulling is { } currentPullUid && @@ -57,7 +60,7 @@ private bool TryRedirectPullToScpHold(EntityUid pullerUid, EntityUid pullableUid return true; } - result = _scpHolding.TryToggleHold(holder, pullableUid); + result = _scpHolding.TryToggleHold(holder, pullableUid, attemptChecked: true); return true; } } diff --git a/Content.Shared/_Scp/Holding/ScpHeldComponent.cs b/Content.Shared/_Scp/Holding/ScpHeldComponent.cs index 43be8df7e68..7bc00a8a5d4 100644 --- a/Content.Shared/_Scp/Holding/ScpHeldComponent.cs +++ b/Content.Shared/_Scp/Holding/ScpHeldComponent.cs @@ -62,31 +62,31 @@ public sealed partial class ScpHeldComponent : Component public int RequiredHolderCount = 2; /// - /// Copied soft breakout cooldown configuration from the initial holder. + /// Copied soft breakout cooldown configuration from the initial holdable target. /// [AutoNetworkedField] public TimeSpan SoftEscapeCooldown = TimeSpan.FromSeconds(1); /// - /// Copied full hold delay configuration from the initial holder. + /// Copied full hold delay configuration from the initial holdable target. /// [AutoNetworkedField] public TimeSpan FullHoldDelay = TimeSpan.FromSeconds(10); /// - /// Copied full breakout duration configuration from the initial holder. + /// Copied full breakout duration configuration from the initial holdable target. /// [AutoNetworkedField] public TimeSpan FullBreakoutDuration = TimeSpan.FromSeconds(5); /// - /// Copied post-breakout immunity duration from the initial holder. + /// Copied post-breakout immunity duration from the initial holdable target. /// [AutoNetworkedField] public TimeSpan PostBreakoutImmunity = TimeSpan.FromSeconds(5); /// - /// Copied maximum hold range from the initial holder. + /// Copied maximum hold range from the initial holdable target. /// [AutoNetworkedField] public float HoldRange = 1f; diff --git a/Content.Shared/_Scp/Holding/ScpHoldComponent.cs b/Content.Shared/_Scp/Holding/ScpHoldComponent.cs index 5c4899ff14c..1f62a07c393 100644 --- a/Content.Shared/_Scp/Holding/ScpHoldComponent.cs +++ b/Content.Shared/_Scp/Holding/ScpHoldComponent.cs @@ -1,4 +1,4 @@ -using Content.Shared.Actions; +using Content.Shared.Whitelist; using Robust.Shared.GameStates; using Robust.Shared.Prototypes; @@ -24,50 +24,20 @@ public sealed partial class ScpHoldComponent : Component public EntityUid? ActionEntity; /// - /// Cooldown applied to the hold action after each successful use. - /// - [DataField] - public TimeSpan HoldActionCooldown = TimeSpan.FromSeconds(1); - - /// - /// Minimum delay between soft breakout attempts while the hold is active. - /// - [DataField] - public TimeSpan SoftEscapeCooldown = TimeSpan.FromSeconds(1); - - /// - /// Minimum uninterrupted full hold duration before a breakout do-after may start. - /// - [DataField] - public TimeSpan FullHoldDelay = TimeSpan.FromSeconds(10); - - /// - /// Duration of the visible breakout do-after for a full hold. - /// - [DataField] - public TimeSpan FullBreakoutDuration = TimeSpan.FromSeconds(5); - - /// - /// Duration of immunity after a successful full breakout. + /// Optional whitelist of entities this holder may grab. /// [DataField] - public TimeSpan PostBreakoutImmunity = TimeSpan.FromSeconds(5); + public EntityWhitelist? HoldableWhitelist; /// - /// Maximum unobstructed range allowed between holder and target. + /// Optional blacklist of entities this holder may not grab. /// [DataField] - public float HoldRange = 1f; + public EntityWhitelist? HoldableBlacklist; /// - /// Walk speed modifier applied to holders when this system supplies slowdown. - /// - [DataField] - public float WalkModifier = 0.5f; - - /// - /// Sprint speed modifier applied to holders when this system supplies slowdown. + /// Cooldown applied to the hold action after each successful use. /// [DataField] - public float SprintModifier = 0.5f; + public TimeSpan HoldActionCooldown = TimeSpan.FromSeconds(1); } diff --git a/Content.Shared/_Scp/Holding/ScpHoldableComponent.cs b/Content.Shared/_Scp/Holding/ScpHoldableComponent.cs index bfd82fc0c67..0a09ba5551d 100644 --- a/Content.Shared/_Scp/Holding/ScpHoldableComponent.cs +++ b/Content.Shared/_Scp/Holding/ScpHoldableComponent.cs @@ -1,7 +1,67 @@ +using System; +using Content.Shared.Whitelist; + namespace Content.Shared._Scp.Holding; /// -/// Marks an entity as a valid target for the SCP holding mechanic. +/// Marks an entity as a valid target for the SCP holding mechanic and stores per-target hold tuning. /// [RegisterComponent] -public sealed partial class ScpHoldableComponent : Component; +public sealed partial class ScpHoldableComponent : Component +{ + /// + /// Optional whitelist of entities that may hold this target. + /// + [DataField] + public EntityWhitelist? HolderWhitelist; + + /// + /// Optional blacklist of entities that may not hold this target. + /// + [DataField] + public EntityWhitelist? HolderBlacklist; + + /// + /// Minimum delay between successful soft breakout attempts while the hold is active. + /// + [DataField] + public TimeSpan SoftEscapeCooldown = TimeSpan.FromSeconds(1); + + /// + /// Minimum uninterrupted full hold duration before a breakout do-after may start. + /// + [DataField] + public TimeSpan FullHoldDelay = TimeSpan.FromSeconds(10); + + /// + /// Duration of the visible breakout do-after for a full hold. + /// + [DataField] + public TimeSpan FullBreakoutDuration = TimeSpan.FromSeconds(5); + + /// + /// Duration of immunity after a successful full breakout. + /// + [DataField] + public TimeSpan PostBreakoutImmunity = TimeSpan.FromSeconds(5); + + /// + /// Maximum unobstructed range allowed between holder and target. + /// + [DataField] + public float HoldRange = 1f; + + /// + /// Walk speed modifier applied to holders while they move this target. + /// Lower values make the target heavier to move. + /// + [DataField] + public float HolderWalkModifier = 0.5f; + + /// + /// Sprint speed modifier applied to holders while they move this target. + /// Lower values make the target heavier to move. + /// + [DataField] + public float HolderSprintModifier = 0.5f; +} diff --git a/Content.Shared/_Scp/Holding/ScpHoldingEvents.cs b/Content.Shared/_Scp/Holding/ScpHoldingEvents.cs index 3ba74d4d417..091af12660b 100644 --- a/Content.Shared/_Scp/Holding/ScpHoldingEvents.cs +++ b/Content.Shared/_Scp/Holding/ScpHoldingEvents.cs @@ -1,6 +1,7 @@ using Content.Shared.Actions; using Content.Shared.Alert; using Content.Shared.DoAfter; +using Robust.Shared.GameObjects; using Robust.Shared.Serialization; namespace Content.Shared._Scp.Holding; @@ -11,5 +12,30 @@ public sealed partial class ScpHoldBreakoutActionEvent : InstantActionEvent; public sealed partial class ScpHoldBreakoutAlertEvent : BaseAlertEvent; +public sealed partial class ScpHoldAttemptEvent(EntityUid holder, EntityUid target) : CancellableEntityEventArgs +{ + public EntityUid Holder { get; } = holder; + public EntityUid Target { get; } = target; +} + +public sealed partial class ScpHoldBreakoutEvent(bool viaMovement, bool wasFullHold, bool appliedImmunity) : EntityEventArgs +{ + public bool ViaMovement { get; } = viaMovement; + public bool WasFullHold { get; } = wasFullHold; + public bool AppliedImmunity { get; } = appliedImmunity; +} + [Serializable, NetSerializable] -public sealed partial class ScpHoldBreakoutDoAfterEvent : SimpleDoAfterEvent; +public sealed partial class ScpHoldBreakoutDoAfterEvent : SimpleDoAfterEvent +{ + public bool ViaMovement; + + public ScpHoldBreakoutDoAfterEvent() + { + } + + public ScpHoldBreakoutDoAfterEvent(bool viaMovement) + { + ViaMovement = viaMovement; + } +} diff --git a/Content.Shared/_Scp/Holding/SharedScpHoldingSystem.Actions.cs b/Content.Shared/_Scp/Holding/SharedScpHoldingSystem.Actions.cs new file mode 100644 index 00000000000..6d4f7f42fe5 --- /dev/null +++ b/Content.Shared/_Scp/Holding/SharedScpHoldingSystem.Actions.cs @@ -0,0 +1,266 @@ +using System; +using Content.Shared.DoAfter; +using Content.Shared.Movement.Components; +using Robust.Shared.Physics; + +namespace Content.Shared._Scp.Holding; + +public sealed partial class SharedScpHoldingSystem +{ + /* + * Action-local query caches, hold toggling API, breakout flow, and cooldown helpers. + */ + private EntityQuery _moverQuery; + private EntityQuery _holdableQuery; + + private void InitializeActionQueries() + { + _moverQuery = GetEntityQuery(); + _holdableQuery = GetEntityQuery(); + } + + public bool TryToggleHold(Entity holder, EntityUid target, bool attemptChecked = false) + { + if (!CanUseHoldAction(holder)) + return false; + + if (_holderQuery.TryComp(holder.Owner, out var activeHolder) && activeHolder.Target != null) + { + if (activeHolder.Target.Value == target) + { + ReleaseHolderContribution(holder.Owner, target, clearIfEmpty: true); + return true; + } + + PopupHolder(holder.Owner, "scp-hold-already-holding-other"); + return false; + } + + if (!CanToggleHold(holder, target, checkAttempt: !attemptChecked)) + return false; + + var holdable = _holdableQuery.Comp(target); + var held = EnsureHeldState(target, holdable); + AddHolderContribution(holder.Owner, held); + SyncHeldState(held); + return true; + } + + public bool CanToggleHold( + Entity holder, + EntityUid target, + bool quiet = false, + bool ignoreHandAvailability = false, + bool checkAttempt = false) + { + if (!Exists(target) || holder.Owner == target) + return false; + + if (!CanUseHoldAction(holder, quiet)) + return false; + + if (!_holdableQuery.TryComp(target, out var holdable)) + { + if (!quiet) + PopupHolder(holder.Owner, "scp-hold-target-not-holdable", ("target", target)); + return false; + } + + if (!_whitelist.CheckBoth(target, holder.Comp.HoldableBlacklist, holder.Comp.HoldableWhitelist) || + !_whitelist.CheckBoth(holder.Owner, holdable.HolderBlacklist, holdable.HolderWhitelist)) + { + if (!quiet) + PopupHolder(holder.Owner, "scp-hold-target-invalid", ("target", target)); + return false; + } + + if (!_moverQuery.HasComp(holder.Owner) || + !_moverQuery.HasComp(target) || + !_physicsQuery.TryComp(target, out var targetPhysics) || + targetPhysics.BodyType == BodyType.Static) + { + if (!quiet) + PopupHolder(holder.Owner, "scp-hold-target-invalid", ("target", target)); + return false; + } + + if (!_container.IsInSameOrNoContainer(holder.Owner, target)) + { + if (!quiet) + PopupHolder(holder.Owner, "scp-hold-target-invalid", ("target", target)); + return false; + } + + if (TryComp(target, out _)) + { + if (!quiet) + PopupHolder(holder.Owner, "scp-hold-target-immune", ("target", target)); + return false; + } + + if (!ignoreHandAvailability && !HasAvailableHolderHand(holder.Owner)) + { + if (!quiet) + PopupHolder(holder.Owner, "scp-hold-holder-no-free-hand", ("target", target)); + return false; + } + + var range = holdable.HoldRange; + if (_heldQuery.TryComp(target, out var held)) + { + range = held.HoldRange; + + if (held.FullHold && held.Holders.Count >= held.RequiredHolderCount) + { + if (!quiet) + PopupHolder(holder.Owner, "scp-hold-target-fully-held", ("target", target)); + return false; + } + } + + if (!_interaction.InRangeUnobstructed(holder.Owner, target, range)) + { + if (!quiet) + PopupHolder(holder.Owner, "scp-hold-target-too-far", ("target", target)); + return false; + } + + if (checkAttempt && !CanPassHoldAttempt(holder.Owner, target)) + return false; + + return true; + } + + public bool TryBreakOut(Entity held, bool viaMovement) + { + return held.Comp.FullHold + ? TryStartFullBreakout(held, viaMovement) + : TrySoftBreakOut(held, viaMovement); + } + + public void RefreshHeldState(Entity held) + { + _alerts.ShowAlert(held.Owner, "ScpHoldGrabbed"); + SyncHeldStatusEffect(held.Owner); + SyncPlaceholderHands(held); + _actionBlocker.UpdateCanMove(held.Owner); + + if (_net.IsClient) + _physics.UpdateIsPredicted(held.Owner); + } + + public void RefreshHolderState(Entity holder) + { + SyncHolderHandBlocker(holder); + _movement.RefreshMovementSpeedModifiers(holder.Owner); + } + + private bool TrySoftBreakOut(Entity held, bool viaMovement) + { + if (_timing.CurTime < held.Comp.SoftEscapeAvailableAt) + return false; + + if (!viaMovement) + PopupTarget(held.Owner, "scp-hold-breakout-start"); + + RaiseBreakoutEvent(held, viaMovement, applyImmunity: false); + ClearHoldState(held, applyImmunity: false); + return true; + } + + private bool TryStartFullBreakout(Entity held, bool viaMovement) + { + if (held.Comp.FullHoldStartedAt == null) + { + PopupTarget(held.Owner, "scp-hold-breakout-too-early", ("seconds", 1)); + return false; + } + + var breakoutAvailableAt = held.Comp.FullHoldStartedAt.Value + held.Comp.FullHoldDelay; + if (_timing.CurTime < breakoutAvailableAt) + { + var remaining = breakoutAvailableAt - _timing.CurTime; + var remainingSeconds = Math.Max(1, (int)Math.Ceiling(remaining.TotalSeconds)); + PopupTarget(held.Owner, "scp-hold-breakout-too-early", ("seconds", remainingSeconds)); + return false; + } + + if (held.Comp.BreakoutDoAfterId != null) + return true; + + var doAfter = new DoAfterArgs( + EntityManager, + held.Owner, + held.Comp.FullBreakoutDuration, + new ScpHoldBreakoutDoAfterEvent(viaMovement), + held.Owner, + target: held.Owner) + { + BreakOnMove = true, + BreakOnDamage = true, + NeedHand = false, + Hidden = false, + }; + + if (!_doAfter.TryStartDoAfter(doAfter, out var id)) + return false; + + SetBreakoutDoAfterId(held, id.Value.Index); + ShowBreakoutAttemptFeedback(held); + + PopupTarget(held.Owner, "scp-hold-breakout-start"); + return true; + } + + private bool CanUseHoldAction(Entity holder, bool quiet = false) + { + if (!IsHoldActionCoolingDown(holder, out var remaining)) + return true; + + if (!quiet) + { + var remainingSeconds = Math.Max(1, (int)Math.Ceiling(remaining.TotalSeconds)); + PopupHolder(holder.Owner, "scp-hold-holder-action-on-cooldown", ("seconds", remainingSeconds)); + } + + return false; + } + + private bool IsHoldActionCoolingDown(Entity holder, out TimeSpan remaining) + { + remaining = TimeSpan.Zero; + + if (holder.Comp.ActionEntity is not { } actionUid) + return false; + + var action = _actions.GetAction(actionUid); + if (action?.Comp.Cooldown is not { } cooldown || cooldown.End <= _timing.CurTime) + return false; + + remaining = cooldown.End - _timing.CurTime; + return true; + } + + private void ApplyFullBreakoutHolderCooldown(EntityUid holderUid) + { + if (!_holdQuery.TryComp(holderUid, out var hold) || hold.ActionEntity == null) + return; + + var cooldown = TimeSpan.FromTicks(hold.HoldActionCooldown.Ticks * 2); + _actions.SetIfBiggerCooldown(hold.ActionEntity.Value, cooldown); + } + + private bool CanPassHoldAttempt(EntityUid holderUid, EntityUid targetUid) + { + var attempt = new ScpHoldAttemptEvent(holderUid, targetUid); + RaiseLocalEvent(targetUid, attempt); + RaiseLocalEvent(holderUid, attempt); + return !attempt.Cancelled; + } + + private void RaiseBreakoutEvent(Entity held, bool viaMovement, bool applyImmunity) + { + var ev = new ScpHoldBreakoutEvent(viaMovement, held.Comp.FullHold, applyImmunity); + RaiseLocalEvent(held.Owner, ev); + } +} diff --git a/Content.Shared/_Scp/Holding/SharedScpHoldingSystem.Drag.cs b/Content.Shared/_Scp/Holding/SharedScpHoldingSystem.Drag.cs new file mode 100644 index 00000000000..411078acfd1 --- /dev/null +++ b/Content.Shared/_Scp/Holding/SharedScpHoldingSystem.Drag.cs @@ -0,0 +1,129 @@ +using System.Numerics; +using Content.Shared.Interaction; +using Robust.Shared.Physics.Components; + +namespace Content.Shared._Scp.Holding; + +public sealed partial class SharedScpHoldingSystem +{ + /* + * Drag-local dependencies, soft-drag movement, and helper calculations. + */ + [Dependency] private readonly SharedTransformSystem _transform = default!; + + private const float SoftDragDistanceFactor = 0.3f; + private const float SoftDragMinimumDistance = 0.4f; + private const float SoftDragMaximumDistance = 0.6f; + private const float SoftDragSnapTolerance = 0.03f; + private const float SoftDragSettleTolerance = 0.08f; + private const float SoftDragVelocityDirectionThreshold = 0.05f; + private const float SoftDragCatchUpTime = 0.05f; + private const float SoftDragMaximumCorrectionSpeed = 6f; + private const float SoftDragAwayVelocityStrength = 0.6f; + private const float SoftDragVelocityTolerance = 0.05f; + + private void UpdateSoftDrag(Entity held, float maintenanceRange, float desiredDistance) + { + if (held.Comp.PrimaryHolder == null) + return; + + var primaryHolder = held.Comp.PrimaryHolder.Value; + if (!_holderQuery.TryComp(primaryHolder, out var holder) || + holder.Target != held.Owner || + !_container.IsInSameOrNoContainer(primaryHolder, held.Owner) || + !_interaction.InRangeUnobstructed(primaryHolder, held.Owner, maintenanceRange) || + !_physicsQuery.TryComp(held.Owner, out var heldPhysics)) + { + return; + } + + var holderCoords = _transform.GetMapCoordinates(primaryHolder); + var heldCoords = _transform.GetMapCoordinates(held.Owner); + + if (holderCoords.MapId != heldCoords.MapId) + return; + + var offset = heldCoords.Position - holderCoords.Position; + var distance = offset.Length(); + var holderVelocity = _physicsQuery.TryComp(primaryHolder, out var holderPhysics) + ? holderPhysics.LinearVelocity + : Vector2.Zero; + var direction = GetSoftDragDirection(primaryHolder, holderVelocity, offset, distance); + var desiredPosition = holderCoords.Position + direction * desiredDistance; + var correction = desiredPosition - heldCoords.Position; + var correctionDistance = correction.Length(); + + Vector2 desiredVelocity; + if (correctionDistance <= SoftDragSettleTolerance) + { + desiredVelocity = holderVelocity.LengthSquared() > SoftDragVelocityDirectionThreshold * SoftDragVelocityDirectionThreshold + ? holderVelocity + : Vector2.Zero; + } + else + { + var correctionDirection = correction / correctionDistance; + var correctionSpeed = Math.Min(correctionDistance / GetSoftDragCatchUpTime(), SoftDragMaximumCorrectionSpeed); + desiredVelocity = holderVelocity + correctionDirection * correctionSpeed; + + var relativeVelocity = heldPhysics.LinearVelocity - holderVelocity; + var awaySpeed = MathF.Max(0f, -Vector2.Dot(relativeVelocity, correctionDirection)); + if (awaySpeed > 0f) + desiredVelocity += correctionDirection * awaySpeed * SoftDragAwayVelocityStrength; + } + + ApplyHeldVelocity(held.Owner, desiredVelocity, heldPhysics); + } + + private float GetDesiredSoftDragDistance(Entity held) + { + return GetBaseSoftDragDistance(held.Comp.HoldRange); + } + + private static float GetHoldMaintenanceRange(float configuredRange, float desiredSoftDragDistance) + { + return MathF.Max(MathF.Max(configuredRange, SharedInteractionSystem.InteractionRange), desiredSoftDragDistance + SoftDragSnapTolerance); + } + + private static float GetBaseSoftDragDistance(float holdRange) + { + return Math.Clamp(holdRange * SoftDragDistanceFactor, SoftDragMinimumDistance, SoftDragMaximumDistance); + } + + private float GetSoftDragCatchUpTime() + { + return MathF.Max((float)_timing.TickPeriod.TotalSeconds, SoftDragCatchUpTime); + } + + private Vector2 GetSoftDragDirection(EntityUid holderUid, Vector2 holderVelocity, Vector2 offset, float distance) + { + if (distance > SoftDragSnapTolerance) + return offset / distance; + + if (holderVelocity.LengthSquared() > SoftDragVelocityDirectionThreshold * SoftDragVelocityDirectionThreshold) + return -Vector2.Normalize(holderVelocity); + + return Transform(holderUid).LocalRotation.ToWorldVec(); + } + + private void ApplyHeldVelocity(EntityUid uid, Vector2 desiredVelocity, PhysicsComponent physics) + { + if (Vector2.DistanceSquared(physics.LinearVelocity, desiredVelocity) > SoftDragVelocityTolerance * SoftDragVelocityTolerance) + _physics.SetLinearVelocity(uid, desiredVelocity, body: physics); + + if (!MathHelper.CloseTo(physics.AngularVelocity, 0f)) + _physics.SetAngularVelocity(uid, 0f, body: physics); + } + + private void ZeroHeldVelocity(EntityUid uid) + { + if (!_physicsQuery.TryComp(uid, out var physics)) + return; + + if (physics.LinearVelocity == Vector2.Zero && MathHelper.CloseTo(physics.AngularVelocity, 0f)) + return; + + _physics.SetLinearVelocity(uid, Vector2.Zero, body: physics); + _physics.SetAngularVelocity(uid, 0f, body: physics); + } +} diff --git a/Content.Shared/_Scp/Holding/SharedScpHoldingSystem.Events.cs b/Content.Shared/_Scp/Holding/SharedScpHoldingSystem.Events.cs new file mode 100644 index 00000000000..ba7b527acf7 --- /dev/null +++ b/Content.Shared/_Scp/Holding/SharedScpHoldingSystem.Events.cs @@ -0,0 +1,259 @@ +using Content.Shared.Hands; +using Content.Shared.Hands.EntitySystems; +using Content.Shared.Interaction.Components; +using Content.Shared.Movement.Events; +using Content.Shared.Movement.Systems; +using Content.Shared.Throwing; +using Robust.Shared.Physics.Events; + +namespace Content.Shared._Scp.Holding; + +public sealed partial class SharedScpHoldingSystem +{ + /* + * Event subscription wiring plus routing/lifecycle reactions for held and holder entities. + */ + private void SubscribeHoldingEvents() + { + SubscribeLocalEvent(OnHoldStartup); + SubscribeLocalEvent(OnHoldShutdown); + SubscribeLocalEvent(OnHoldAction); + + SubscribeLocalEvent(OnHeldStartup); + SubscribeLocalEvent(OnHeldShutdown); + SubscribeLocalEvent(OnBreakoutAction); + SubscribeLocalEvent(OnBreakoutAlert); + SubscribeLocalEvent(OnBreakoutDoAfter); + SubscribeLocalEvent(OnHeldMoveInput); + SubscribeLocalEvent(OnHandCountChanged); + SubscribeLocalEvent(OnHeldUpdateCanMove); + SubscribeLocalEvent(OnHeldAttemptMobCollide); + SubscribeLocalEvent(OnHeldAttemptMobTargetCollide); + SubscribeLocalEvent(OnHeldPreventCollide); + + SubscribeLocalEvent(OnHolderStartup); + SubscribeLocalEvent(OnHolderShutdown); + SubscribeLocalEvent(OnHolderRefreshMoveSpeed); + SubscribeLocalEvent(OnHolderBeforeThrow); + SubscribeLocalEvent(OnHolderHandsModified); + SubscribeLocalEvent(OnHolderPreventCollide); + SubscribeLocalEvent(OnHolderBlockerDropped); + } + + private void OnHoldStartup(Entity ent, ref ComponentStartup args) + { + _actions.AddAction(ent.Owner, ref ent.Comp.ActionEntity, ent.Comp.Action); + + if (ent.Comp.ActionEntity != null) + _actions.SetUseDelay(ent.Comp.ActionEntity.Value, ent.Comp.HoldActionCooldown); + } + + private void OnHoldShutdown(Entity ent, ref ComponentShutdown args) + { + _actions.RemoveAction(ent.Comp.ActionEntity); + + if (_net.IsClient || + TerminatingOrDeleted(ent.Owner) || + !_holderQuery.TryComp(ent.Owner, out var holder) || + holder.Target == null || + TerminatingOrDeleted(holder.Target.Value)) + { + return; + } + + ReleaseHolderContribution(ent.Owner, holder.Target.Value, clearIfEmpty: true); + } + + private void OnHoldAction(Entity ent, ref ScpHoldActionEvent args) + { + if (args.Handled) + return; + + args.Handled = TryToggleHold(ent, args.Target); + } + + private void OnHeldStartup(Entity ent, ref ComponentStartup args) + { + _actions.AddAction(ent.Owner, ref ent.Comp.BreakoutActionEntity, ent.Comp.BreakoutAction); + RefreshHeldState(ent); + } + + private void OnHeldShutdown(Entity ent, ref ComponentShutdown args) + { + _actions.RemoveAction(ent.Comp.BreakoutActionEntity); + _alerts.ClearAlert(ent.Owner, "ScpHoldGrabbed"); + _statusEffects.TryRemoveStatusEffect(ent.Owner, GrabbedStatusEffect); + DeleteHeldHandBlockers(ent.Owner); + + if (!_timing.ApplyingState) + CancelBreakoutDoAfter(ent); + + if (!_net.IsClient) + { + foreach (var holderUid in ent.Comp.Holders) + { + if (!TerminatingOrDeleted(holderUid) && _holderQuery.HasComp(holderUid)) + RemComp(holderUid); + } + } + + _actionBlocker.UpdateCanMove(ent.Owner); + + if (_net.IsClient) + _physics.UpdateIsPredicted(ent.Owner); + } + + private void OnBreakoutAction(Entity ent, ref ScpHoldBreakoutActionEvent args) + { + if (args.Handled) + return; + + args.Handled = TryBreakOut(ent, viaMovement: false); + } + + private void OnBreakoutAlert(Entity ent, ref ScpHoldBreakoutAlertEvent args) + { + if (args.Handled) + return; + + args.Handled = true; + TryBreakOut(ent, viaMovement: false); + } + + private void OnBreakoutDoAfter(Entity ent, ref ScpHoldBreakoutDoAfterEvent args) + { + SetBreakoutDoAfterId(ent, null); + + if (args.Handled) + return; + + args.Handled = true; + + if (args.Cancelled) + { + PopupTarget(ent.Owner, "scp-hold-breakout-interrupted"); + return; + } + + RaiseBreakoutEvent(ent, args.ViaMovement, applyImmunity: true); + ClearHoldState(ent, applyImmunity: true); + } + + private void OnHeldMoveInput(Entity ent, ref MoveInputEvent args) + { + if (!args.State) + return; + + TryBreakOut(ent, viaMovement: true); + } + + private void OnHandCountChanged(Entity ent, ref HandCountChangedEvent args) + { + if (_net.IsClient) + return; + + SyncHeldState(ent); + } + + private void OnHeldUpdateCanMove(Entity ent, ref UpdateCanMoveEvent args) + { + if (ent.Comp.LifeStage > ComponentLifeStage.Running) + return; + + if (ent.Comp.FullHold) + args.Cancel(); + } + + private static void OnHeldAttemptMobCollide(Entity ent, ref AttemptMobCollideEvent args) + { + args.Cancelled = true; + } + + private static void OnHeldAttemptMobTargetCollide(Entity ent, ref AttemptMobTargetCollideEvent args) + { + args.Cancelled = true; + } + + private void OnHeldPreventCollide(Entity ent, ref PreventCollideEvent args) + { + if (args.Cancelled) + return; + + if (_holderQuery.TryComp(args.OtherEntity, out var holder) && + holder.Target == ent.Owner) + { + args.Cancelled = true; + } + } + + private void OnHolderStartup(Entity ent, ref ComponentStartup args) + { + RefreshHolderState(ent); + } + + private void OnHolderShutdown(Entity ent, ref ComponentShutdown args) + { + ent.Comp.Target = null; + ent.Comp.SlowdownEnabled = false; + DeleteHolderHandBlockers(ent.Owner); + _movement.RefreshMovementSpeedModifiers(ent.Owner); + } + + private void OnHolderRefreshMoveSpeed(Entity ent, ref RefreshMovementSpeedModifiersEvent args) + { + if (!ent.Comp.SlowdownEnabled) + return; + + args.ModifySpeed(ent.Comp.WalkModifier, ent.Comp.SprintModifier); + } + + private void OnHolderBeforeThrow(Entity ent, ref BeforeThrowEvent args) + { + if (ent.Comp.Target == null || + !TryComp(args.ItemUid, out var blocker) || + blocker.Target != ent.Comp.Target.Value) + { + return; + } + + ReleaseHolderContribution(ent.Owner, ent.Comp.Target.Value, clearIfEmpty: true); + args.Cancelled = true; + } + + private void OnHolderHandsModified(Entity ent, ref DidEquipHandEvent args) + { + if (ent.Comp.LifeStage > ComponentLifeStage.Running || + TerminatingOrDeleted(ent.Owner) || + ent.Comp.Target == null || + TerminatingOrDeleted(ent.Comp.Target.Value)) + { + return; + } + + RefreshHolderState(ent); + } + + private void OnHolderPreventCollide(Entity ent, ref PreventCollideEvent args) + { + if (args.Cancelled || + ent.Comp.Target == null || + ent.Comp.Target != args.OtherEntity) + { + return; + } + + args.Cancelled = true; + } + + private void OnHolderBlockerDropped(Entity ent, ref GettingDroppedAttemptEvent args) + { + if (!_holderQuery.TryComp(args.User, out var holder) || + holder.Target == null || + holder.Target != ent.Comp.Target) + { + return; + } + + ReleaseHolderContribution(args.User, ent.Comp.Target, clearIfEmpty: true); + } +} diff --git a/Content.Shared/_Scp/Holding/SharedScpHoldingSystem.Feedback.cs b/Content.Shared/_Scp/Holding/SharedScpHoldingSystem.Feedback.cs new file mode 100644 index 00000000000..72a0edb0e6e --- /dev/null +++ b/Content.Shared/_Scp/Holding/SharedScpHoldingSystem.Feedback.cs @@ -0,0 +1,76 @@ +using Content.Shared.Coordinates; +using Content.Shared.Popups; +using Robust.Shared.Audio; +using Robust.Shared.Audio.Systems; + +namespace Content.Shared._Scp.Holding; + +public sealed partial class SharedScpHoldingSystem +{ + /* + * Feedback-local dependencies, breakout do-after tracking, and popup/audio helpers. + */ + [Dependency] private readonly SharedAudioSystem _audio = default!; + [Dependency] private readonly SharedPopupSystem _popup = default!; + + private const string BreakoutAttemptEffect = "WhistleExclamation"; + private static readonly SoundSpecifier BreakoutAttemptSound = + new SoundCollectionSpecifier("storageRustle", + AudioParams.Default.WithVolume(-8f).WithMaxDistance(4f).WithVariation(0.15f)); + + private void CancelBreakoutDoAfter(Entity held) + { + if (held.Comp.BreakoutDoAfterId == null) + return; + + _doAfter.Cancel(held.Owner, held.Comp.BreakoutDoAfterId.Value); + SetBreakoutDoAfterId(held, null); + } + + private void SetBreakoutDoAfterId(Entity held, ushort? breakoutDoAfterId) + { + if (held.Comp.BreakoutDoAfterId == breakoutDoAfterId) + return; + + held.Comp.BreakoutDoAfterId = breakoutDoAfterId; + Dirty(held); + } + + private void ShowBreakoutAttemptFeedback(Entity held) + { + if (_net.IsClient && !_timing.IsFirstTimePredicted) + return; + + foreach (var holderUid in held.Comp.Holders) + { + if (TerminatingOrDeleted(holderUid) || !_holderQuery.TryComp(holderUid, out var holder) || holder.Target != held.Owner) + continue; + + if (_net.IsClient) + PredictedSpawnAttachedTo(BreakoutAttemptEffect, holderUid.ToCoordinates()); + else + SpawnAttachedTo(BreakoutAttemptEffect, holderUid.ToCoordinates()); + } + + if (_net.IsClient) + _audio.PlayPredicted(BreakoutAttemptSound, held.Owner, held.Owner); + else + _audio.PlayPvs(BreakoutAttemptSound, held.Owner); + } + + private void PopupHolder(EntityUid holder, string key, params (string, object)[] args) + { + if (_net.IsClient) + return; + + _popup.PopupEntity(Loc.GetString(key, args), holder, holder); + } + + private void PopupTarget(EntityUid target, string key, params (string, object)[] args) + { + if (_net.IsClient) + return; + + _popup.PopupEntity(Loc.GetString(key, args), target, target); + } +} diff --git a/Content.Shared/_Scp/Holding/SharedScpHoldingSystem.Hands.cs b/Content.Shared/_Scp/Holding/SharedScpHoldingSystem.Hands.cs new file mode 100644 index 00000000000..7dcf3ee0972 --- /dev/null +++ b/Content.Shared/_Scp/Holding/SharedScpHoldingSystem.Hands.cs @@ -0,0 +1,195 @@ +using Content.Shared.Hands.Components; +using Content.Shared.Hands.EntitySystems; +using Content.Shared.Interaction.Components; +using Content.Shared.Inventory.VirtualItem; +using Content.Shared.StatusEffectNew.Components; + +namespace Content.Shared._Scp.Holding; + +public sealed partial class SharedScpHoldingSystem +{ + /* + * Hand-local dependencies, caches, placeholders, virtual blockers, and held-status visuals. + */ + [Dependency] private readonly SharedHandsSystem _hands = default!; + [Dependency] private readonly SharedVirtualItemSystem _virtualItem = default!; + + private readonly List _placeholderIcons = []; + private readonly List> _virtualBlockersToDelete = []; + + private EntityQuery _handsQuery; + + private void InitializeHandQueries() + { + _handsQuery = GetEntityQuery(); + } + + private void SyncPlaceholderHands(Entity held) + { + DeleteHeldHandBlockers(held.Owner); + + if (!held.Comp.FullHold || !_handsQuery.TryComp(held.Owner, out var hands)) + return; + + foreach (var hand in _hands.EnumerateHands((held.Owner, hands))) + { + if (!_hands.TryGetHeldItem((held.Owner, hands), hand, out var heldItem)) + continue; + + if (HasComp(heldItem.Value)) + continue; + + _hands.DoDrop((held.Owner, hands), hand, doDropInteraction: true); + } + + _placeholderIcons.Clear(); + foreach (var holderUid in held.Comp.Holders) + { + if (_holderQuery.TryComp(holderUid, out var holder) && holder.Target == held.Owner) + _placeholderIcons.Add(holderUid); + } + + if (_placeholderIcons.Count == 0) + return; + + var iconIndex = 0; + while (_hands.TryGetEmptyHand((held.Owner, hands), out var emptyHand)) + { + var holderUid = _placeholderIcons[iconIndex % _placeholderIcons.Count]; + if (!_virtualItem.TrySpawnVirtualItemInHand(holderUid, held.Owner, out var virtualItem, empty: emptyHand, silent: true)) + break; + + EnsureComp(virtualItem.Value); + var blocker = EnsureComp(virtualItem.Value); + + if (blocker.Target != held.Owner || blocker.Holder != holderUid) + { + blocker.Target = held.Owner; + blocker.Holder = holderUid; + Dirty(virtualItem.Value, blocker); + } + + iconIndex++; + } + } + + private void SyncHeldStatusEffect(EntityUid target) + { + if (_statusEffects.HasStatusEffect(target, GrabbedStatusEffect) || + !_statusEffects.CanAddStatusEffect(target, GrabbedStatusEffect)) + { + return; + } + + EnsureComp(target); + PredictedTrySpawnInContainer(GrabbedStatusEffect, target, StatusEffectContainerComponent.ContainerId, out _); + } + + private void SyncHolderHandBlocker(Entity holder) + { + _virtualBlockersToDelete.Clear(); + EntityUid? validBlocker = null; + var target = holder.Comp.Target; + + foreach (var heldItem in _hands.EnumerateHeld(holder.Owner)) + { + if (!TryComp(heldItem, out var virtualItem)) + { + continue; + } + + var matchesCurrentTarget = holder.Comp.LifeStage <= ComponentLifeStage.Running && + target != null && + virtualItem.BlockingEntity == target.Value; + + if (matchesCurrentTarget) + { + if (validBlocker == null) + { + validBlocker = heldItem; + RemComp(heldItem); + var blocker = EnsureComp(heldItem); + var currentTarget = target!.Value; + if (blocker.Target != currentTarget) + { + blocker.Target = currentTarget; + Dirty(heldItem, blocker); + } + continue; + } + } + + if (TryComp(heldItem, out _) || matchesCurrentTarget) + _virtualBlockersToDelete.Add((heldItem, virtualItem)); + } + + foreach (var virtualItem in _virtualBlockersToDelete) + { + _virtualItem.DeleteVirtualItem(virtualItem, holder.Owner); + } + + if (holder.Comp.LifeStage > ComponentLifeStage.Running || + holder.Comp.Target == null || + validBlocker != null) + { + return; + } + + if (!_handsQuery.TryComp(holder.Owner, out var hands) || + !_hands.TryGetEmptyHand((holder.Owner, hands), out _)) + { + return; + } + + if (!_virtualItem.TrySpawnVirtualItemInHand(holder.Comp.Target.Value, holder.Owner, out var spawnedVirtualItem, silent: true)) + return; + + var blockerComp = EnsureComp(spawnedVirtualItem.Value); + blockerComp.Target = holder.Comp.Target.Value; + Dirty(spawnedVirtualItem.Value, blockerComp); + } + + private bool HasAvailableHolderHand(EntityUid holderUid) + { + return _handsQuery.TryComp(holderUid, out var hands) && + _hands.TryGetEmptyHand((holderUid, hands), out _); + } + + private void DeleteHolderHandBlockers(EntityUid holderUid) + { + _virtualBlockersToDelete.Clear(); + + foreach (var heldItem in _hands.EnumerateHeld(holderUid)) + { + if (TryComp(heldItem, out _) && + TryComp(heldItem, out var virtualItem)) + { + _virtualBlockersToDelete.Add((heldItem, virtualItem)); + } + } + + foreach (var virtualItem in _virtualBlockersToDelete) + { + _virtualItem.DeleteVirtualItem(virtualItem, holderUid); + } + } + + private void DeleteHeldHandBlockers(EntityUid heldUid) + { + _virtualBlockersToDelete.Clear(); + + foreach (var heldItem in _hands.EnumerateHeld(heldUid)) + { + if (TryComp(heldItem, out _) && + TryComp(heldItem, out var virtualItem)) + { + _virtualBlockersToDelete.Add((heldItem, virtualItem)); + } + } + + foreach (var virtualItem in _virtualBlockersToDelete) + { + _virtualItem.DeleteVirtualItem(virtualItem, heldUid); + } + } +} diff --git a/Content.Shared/_Scp/Holding/SharedScpHoldingSystem.State.cs b/Content.Shared/_Scp/Holding/SharedScpHoldingSystem.State.cs new file mode 100644 index 00000000000..9b52d4248b1 --- /dev/null +++ b/Content.Shared/_Scp/Holding/SharedScpHoldingSystem.State.cs @@ -0,0 +1,304 @@ +using Content.Shared.Body.Components; +using Content.Shared.Body.Part; +using Content.Shared.Body.Systems; + +namespace Content.Shared._Scp.Holding; + +public sealed partial class SharedScpHoldingSystem +{ + /* + * State-local dependencies, caches, held lifecycle, holder membership, and full-hold transitions. + */ + [Dependency] private readonly SharedBodySystem _body = default!; + + private readonly List _holdersToRemove = []; + private readonly List _holderCooldownsToApply = []; + + private EntityQuery _bodyQuery; + + private void InitializeStateQueries() + { + _bodyQuery = GetEntityQuery(); + } + + private void UpdateHeld(Entity held) + { + if (!EnsurePrimaryHolder(held)) + { + ClearHoldState(held, applyImmunity: false); + return; + } + + var desiredSoftDragDistance = GetDesiredSoftDragDistance(held); + var maintenanceRange = GetHoldMaintenanceRange(held.Comp.HoldRange, desiredSoftDragDistance); + + if (!held.Comp.FullHold) + UpdateSoftDrag(held, maintenanceRange, desiredSoftDragDistance); + else + ZeroHeldVelocity(held.Owner); + + _holdersToRemove.Clear(); + + foreach (var holderUid in held.Comp.Holders) + { + if (!Exists(holderUid) || + !_holdQuery.HasComp(holderUid) || + !_holderQuery.TryComp(holderUid, out var holder) || + holder.Target != held.Owner || + !_container.IsInSameOrNoContainer(holderUid, held.Owner) || + !_interaction.InRangeUnobstructed(holderUid, held.Owner, maintenanceRange)) + { + _holdersToRemove.Add(holderUid); + } + } + + foreach (var holderUid in _holdersToRemove) + { + ReleaseHolderContribution(holderUid, held.Owner, clearIfEmpty: false); + + if (!_heldQuery.TryComp(held.Owner, out _)) + return; + } + + if (_heldQuery.TryComp(held.Owner, out var refreshed)) + SyncHeldState((held.Owner, refreshed)); + } + + private Entity EnsureHeldState(EntityUid target, ScpHoldableComponent config) + { + var created = !_heldQuery.TryComp(target, out var held); + held ??= EnsureComp(target); + + if (created) + CopyConfig(config, held); + + held.RequiredHolderCount = GetRequiredHolderCount(target); + return (target, held); + } + + private void AddHolderContribution(EntityUid holderUid, Entity held) + { + if (!held.Comp.Holders.Contains(holderUid)) + held.Comp.Holders.Add(holderUid); + + var holder = EnsureComp(holderUid); + holder.Target = held.Owner; + holder.SlowdownEnabled = false; + holder.WalkModifier = held.Comp.WalkModifier; + holder.SprintModifier = held.Comp.SprintModifier; + Dirty(holderUid, holder); + RefreshHolderState((holderUid, holder)); + } + + private void ReleaseHolderContribution(EntityUid holderUid, EntityUid targetUid, bool clearIfEmpty) + { + if (!_heldQuery.TryComp(targetUid, out var held)) + return; + + for (var i = held.Holders.Count - 1; i >= 0; i--) + { + if (held.Holders[i] == holderUid) + held.Holders.RemoveAt(i); + } + + if (_holderQuery.HasComp(holderUid)) + RemComp(holderUid); + + if (held.PrimaryHolder == holderUid) + held.PrimaryHolder = null; + + if (held.Holders.Count == 0) + { + if (clearIfEmpty) + ClearHoldState((targetUid, held), applyImmunity: false); + return; + } + + SyncHeldState((targetUid, held)); + } + + private void SyncHeldState(Entity held) + { + if (!_heldQuery.TryComp(held.Owner, out var heldComp)) + return; + + held.Comp = heldComp; + held.Comp.RequiredHolderCount = GetRequiredHolderCount(held.Owner); + + if (held.Comp.Holders.Count == 0) + { + ClearHoldState(held, applyImmunity: false); + return; + } + + if (!EnsurePrimaryHolder(held)) + { + ClearHoldState(held, applyImmunity: false); + return; + } + + if (held.Comp.Holders.Count >= held.Comp.RequiredHolderCount) + { + EnterFullHold(held); + return; + } + + ExitFullHold(held); + var desiredSoftDragDistance = GetDesiredSoftDragDistance(held); + var maintenanceRange = GetHoldMaintenanceRange(held.Comp.HoldRange, desiredSoftDragDistance); + UpdateSoftDrag(held, maintenanceRange, desiredSoftDragDistance); + UpdateHolderSlowdowns(held); + SyncPlaceholderHands(held); + Dirty(held); + } + + private void EnterFullHold(Entity held) + { + if (!held.Comp.FullHold) + { + held.Comp.FullHold = true; + held.Comp.FullHoldStartedAt = _timing.CurTime; + } + + UpdateHolderSlowdowns(held); + SyncPlaceholderHands(held); + ZeroHeldVelocity(held.Owner); + _actionBlocker.UpdateCanMove(held.Owner); + Dirty(held); + } + + private void ExitFullHold(Entity held) + { + CancelBreakoutDoAfter(held); + + if (!held.Comp.FullHold && held.Comp.FullHoldStartedAt == null) + return; + + held.Comp.FullHold = false; + held.Comp.FullHoldStartedAt = null; + SyncPlaceholderHands(held); + _actionBlocker.UpdateCanMove(held.Owner); + Dirty(held); + } + + private bool EnsurePrimaryHolder(Entity held) + { + if (held.Comp.PrimaryHolder != null && + _holderQuery.TryComp(held.Comp.PrimaryHolder.Value, out var activeHolder) && + activeHolder.Target == held.Owner && + held.Comp.Holders.Contains(held.Comp.PrimaryHolder.Value)) + { + return true; + } + + held.Comp.PrimaryHolder = null; + + foreach (var holderUid in held.Comp.Holders) + { + if (!_holderQuery.TryComp(holderUid, out var holder) || + holder.Target != held.Owner) + { + continue; + } + + held.Comp.PrimaryHolder = holderUid; + return true; + } + + return false; + } + + private void ClearHoldState(Entity held, bool applyImmunity) + { + if (_heldQuery.TryComp(held.Owner, out var refreshed)) + held = (held.Owner, refreshed); + + CancelBreakoutDoAfter(held); + DeleteHeldHandBlockers(held.Owner); + _actionBlocker.UpdateCanMove(held.Owner); + _holderCooldownsToApply.Clear(); + + foreach (var holderUid in held.Comp.Holders) + { + if (applyImmunity) + _holderCooldownsToApply.Add(holderUid); + + if (_holderQuery.HasComp(holderUid)) + RemComp(holderUid); + } + + held.Comp.Holders.Clear(); + held.Comp.PrimaryHolder = null; + + if (applyImmunity) + { + var immune = EnsureComp(held.Owner); + immune.ExpiresAt = _timing.CurTime + held.Comp.PostBreakoutImmunity; + Dirty(held.Owner, immune); + } + + foreach (var holderUid in _holderCooldownsToApply) + { + ApplyFullBreakoutHolderCooldown(holderUid); + } + + RemComp(held.Owner); + } + + private void UpdateHolderSlowdowns(Entity held) + { + foreach (var holderUid in held.Comp.Holders) + { + if (!_holderQuery.TryComp(holderUid, out var holder)) + continue; + + SetHolderSlowdown((holderUid, holder), true, held.Comp.WalkModifier, held.Comp.SprintModifier); + } + } + + private void SetHolderSlowdown(Entity holder, bool enabled, float walkModifier, float sprintModifier) + { + if (holder.Comp.SlowdownEnabled == enabled && + MathHelper.CloseTo(holder.Comp.WalkModifier, walkModifier) && + MathHelper.CloseTo(holder.Comp.SprintModifier, sprintModifier)) + { + return; + } + + holder.Comp.SlowdownEnabled = enabled; + holder.Comp.WalkModifier = walkModifier; + holder.Comp.SprintModifier = sprintModifier; + Dirty(holder); + _movement.RefreshMovementSpeedModifiers(holder.Owner); + } + + private int GetRequiredHolderCount(EntityUid target) + { + if (_bodyQuery.TryComp(target, out var body)) + { + var handCount = 0; + foreach (var _ in _body.GetBodyChildrenOfType(target, BodyPartType.Hand, body)) + { + handCount++; + } + + if (handCount > 0) + return handCount; + } + + return 2; + } + + private void CopyConfig(ScpHoldableComponent source, ScpHeldComponent target) + { + target.SoftEscapeCooldown = source.SoftEscapeCooldown; + target.FullHoldDelay = source.FullHoldDelay; + target.FullBreakoutDuration = source.FullBreakoutDuration; + target.PostBreakoutImmunity = source.PostBreakoutImmunity; + target.HoldRange = source.HoldRange; + target.WalkModifier = source.HolderWalkModifier; + target.SprintModifier = source.HolderSprintModifier; + target.SoftEscapeAvailableAt = _timing.CurTime; + target.FullHoldStartedAt = null; + } +} diff --git a/Content.Shared/_Scp/Holding/SharedScpHoldingSystem.cs b/Content.Shared/_Scp/Holding/SharedScpHoldingSystem.cs index e274e123214..88dd745f3c3 100644 --- a/Content.Shared/_Scp/Holding/SharedScpHoldingSystem.cs +++ b/Content.Shared/_Scp/Holding/SharedScpHoldingSystem.cs @@ -1,123 +1,55 @@ -using System.Numerics; using Content.Shared.ActionBlocker; using Content.Shared.Alert; using Content.Shared.Actions; -using Content.Shared.Body.Components; -using Content.Shared.Body.Part; -using Content.Shared.Body.Systems; -using Content.Shared.Coordinates; using Content.Shared.DoAfter; -using Content.Shared.Hands; -using Content.Shared.Hands.Components; -using Content.Shared.Hands.EntitySystems; using Content.Shared.Interaction; -using Content.Shared.Interaction.Components; -using Content.Shared.Movement.Components; -using Content.Shared.Inventory.VirtualItem; -using Content.Shared.Movement.Events; using Content.Shared.Movement.Systems; -using Content.Shared.Popups; using Content.Shared.StatusEffectNew; -using Content.Shared.StatusEffectNew.Components; -using Content.Shared.Throwing; -using Robust.Shared.Audio; -using Robust.Shared.Audio.Systems; +using Content.Shared.Whitelist; using Robust.Shared.Containers; using Robust.Shared.Network; -using Robust.Shared.Physics; using Robust.Shared.Physics.Components; -using Robust.Shared.Physics.Events; using Robust.Shared.Physics.Systems; using Robust.Shared.Timing; namespace Content.Shared._Scp.Holding; -public sealed class SharedScpHoldingSystem : EntitySystem +public sealed partial class SharedScpHoldingSystem : EntitySystem { + /* + * Core lifecycle, dependencies, constants, and runtime caches. + */ [Dependency] private readonly ActionBlockerSystem _actionBlocker = default!; [Dependency] private readonly AlertsSystem _alerts = default!; - [Dependency] private readonly SharedAudioSystem _audio = default!; [Dependency] private readonly SharedActionsSystem _actions = default!; - [Dependency] private readonly SharedBodySystem _body = default!; [Dependency] private readonly SharedContainerSystem _container = default!; [Dependency] private readonly SharedDoAfterSystem _doAfter = default!; - [Dependency] private readonly SharedHandsSystem _hands = default!; [Dependency] private readonly SharedInteractionSystem _interaction = default!; [Dependency] private readonly MovementSpeedModifierSystem _movement = default!; [Dependency] private readonly INetManager _net = default!; [Dependency] private readonly SharedPhysicsSystem _physics = default!; - [Dependency] private readonly SharedPopupSystem _popup = default!; [Dependency] private readonly StatusEffectsSystem _statusEffects = default!; [Dependency] private readonly IGameTiming _timing = default!; - [Dependency] private readonly SharedTransformSystem _transform = default!; - [Dependency] private readonly SharedVirtualItemSystem _virtualItem = default!; + [Dependency] private readonly EntityWhitelistSystem _whitelist = default!; private const string GrabbedStatusEffect = "StatusEffectScpHeld"; - private const string BreakoutAttemptEffect = "WhistleExclamation"; - private const float SoftDragDistanceFactor = 0.3f; - private const float SoftDragMinimumDistance = 0.4f; - private const float SoftDragMaximumDistance = 0.6f; - private const float SoftDragSnapTolerance = 0.03f; - private const float SoftDragSettleTolerance = 0.08f; - private const float SoftDragVelocityDirectionThreshold = 0.05f; - private const float SoftDragCatchUpTime = 0.05f; - private const float SoftDragMaximumCorrectionSpeed = 6f; - private const float SoftDragAwayVelocityStrength = 0.6f; - private const float SoftDragVelocityTolerance = 0.05f; - private static readonly SoundSpecifier BreakoutAttemptSound = - new SoundCollectionSpecifier("storageRustle", - AudioParams.Default.WithVolume(-8f).WithMaxDistance(4f).WithVariation(0.15f)); - - private readonly List _holdersToRemove = []; - private readonly List _holderCooldownsToApply = []; - private readonly List _placeholderIcons = []; - private readonly List> _virtualBlockersToDelete = []; - - private EntityQuery _bodyQuery; - private EntityQuery _handsQuery; - private EntityQuery _moverQuery; private EntityQuery _physicsQuery; private EntityQuery _heldQuery; private EntityQuery _holdQuery; - private EntityQuery _holdableQuery; private EntityQuery _holderQuery; public override void Initialize() { base.Initialize(); - _bodyQuery = GetEntityQuery(); - _handsQuery = GetEntityQuery(); - _moverQuery = GetEntityQuery(); _physicsQuery = GetEntityQuery(); _heldQuery = GetEntityQuery(); _holdQuery = GetEntityQuery(); - _holdableQuery = GetEntityQuery(); _holderQuery = GetEntityQuery(); - - SubscribeLocalEvent(OnHoldStartup); - SubscribeLocalEvent(OnHoldShutdown); - SubscribeLocalEvent(OnHoldAction); - - SubscribeLocalEvent(OnHeldStartup); - SubscribeLocalEvent(OnHeldShutdown); - SubscribeLocalEvent(OnBreakoutAction); - SubscribeLocalEvent(OnBreakoutAlert); - SubscribeLocalEvent(OnBreakoutDoAfter); - SubscribeLocalEvent(OnHeldMoveInput); - SubscribeLocalEvent(OnHandCountChanged); - SubscribeLocalEvent(OnHeldUpdateCanMove); - SubscribeLocalEvent(OnHeldAttemptMobCollide); - SubscribeLocalEvent(OnHeldAttemptMobTargetCollide); - SubscribeLocalEvent(OnHeldPreventCollide); - - SubscribeLocalEvent(OnHolderStartup); - SubscribeLocalEvent(OnHolderShutdown); - SubscribeLocalEvent(OnHolderRefreshMoveSpeed); - SubscribeLocalEvent(OnHolderBeforeThrow); - SubscribeLocalEvent(OnHolderHandsModified); - SubscribeLocalEvent(OnHolderPreventCollide); - SubscribeLocalEvent(OnHolderBlockerDropped); + InitializeActionQueries(); + InitializeHandQueries(); + InitializeStateQueries(); + SubscribeHoldingEvents(); } public override void Update(float frameTime) @@ -143,1048 +75,4 @@ public override void Update(float frameTime) UpdateHeld((uid, held)); } } - - private void OnHoldStartup(Entity ent, ref ComponentStartup args) - { - _actions.AddAction(ent.Owner, ref ent.Comp.ActionEntity, ent.Comp.Action); - - if (ent.Comp.ActionEntity != null) - _actions.SetUseDelay(ent.Comp.ActionEntity.Value, ent.Comp.HoldActionCooldown); - } - - private void OnHoldShutdown(Entity ent, ref ComponentShutdown args) - { - _actions.RemoveAction(ent.Comp.ActionEntity); - - if (_net.IsClient || - TerminatingOrDeleted(ent.Owner) || - !_holderQuery.TryComp(ent.Owner, out var holder) || - holder.Target == null || - TerminatingOrDeleted(holder.Target.Value)) - { - return; - } - - ReleaseHolderContribution(ent.Owner, holder.Target.Value, clearIfEmpty: true); - } - - private void OnHoldAction(Entity ent, ref ScpHoldActionEvent args) - { - if (args.Handled) - return; - - args.Handled = TryToggleHold(ent, args.Target); - } - - private void OnHeldStartup(Entity ent, ref ComponentStartup args) - { - _actions.AddAction(ent.Owner, ref ent.Comp.BreakoutActionEntity, ent.Comp.BreakoutAction); - RefreshHeldState(ent); - } - - private void OnHeldShutdown(Entity ent, ref ComponentShutdown args) - { - _actions.RemoveAction(ent.Comp.BreakoutActionEntity); - _alerts.ClearAlert(ent.Owner, "ScpHoldGrabbed"); - _statusEffects.TryRemoveStatusEffect(ent.Owner, GrabbedStatusEffect); - DeleteHeldHandBlockers(ent.Owner); - - if (!_timing.ApplyingState) - CancelBreakoutDoAfter(ent); - - if (!_net.IsClient) - { - foreach (var holderUid in ent.Comp.Holders) - { - if (!TerminatingOrDeleted(holderUid) && _holderQuery.HasComp(holderUid)) - RemComp(holderUid); - } - } - - _actionBlocker.UpdateCanMove(ent.Owner); - - if (_net.IsClient) - _physics.UpdateIsPredicted(ent.Owner); - } - - private void OnBreakoutAction(Entity ent, ref ScpHoldBreakoutActionEvent args) - { - if (args.Handled) - return; - - args.Handled = TryBreakOut(ent, viaMovement: false); - } - - private void OnBreakoutAlert(Entity ent, ref ScpHoldBreakoutAlertEvent args) - { - if (args.Handled) - return; - - args.Handled = true; - TryBreakOut(ent, viaMovement: false); - } - - private void OnBreakoutDoAfter(Entity ent, ref ScpHoldBreakoutDoAfterEvent args) - { - SetBreakoutDoAfterId(ent, null); - - if (args.Handled) - return; - - args.Handled = true; - - if (args.Cancelled) - { - PopupTarget(ent.Owner, "scp-hold-breakout-interrupted"); - return; - } - - ClearHoldState(ent, applyImmunity: true); - } - - private void OnHeldMoveInput(Entity ent, ref MoveInputEvent args) - { - if (!args.State) - return; - - TryBreakOut(ent, viaMovement: true); - } - - private void OnHandCountChanged(Entity ent, ref HandCountChangedEvent args) - { - if (_net.IsClient) - return; - - SyncHeldState(ent); - } - - private void OnHeldUpdateCanMove(Entity ent, ref UpdateCanMoveEvent args) - { - if (ent.Comp.LifeStage > ComponentLifeStage.Running) - return; - - if (ent.Comp.FullHold) - args.Cancel(); - } - - private static void OnHeldAttemptMobCollide(Entity ent, ref AttemptMobCollideEvent args) - { - args.Cancelled = true; - } - - private static void OnHeldAttemptMobTargetCollide(Entity ent, ref AttemptMobTargetCollideEvent args) - { - args.Cancelled = true; - } - - private void OnHeldPreventCollide(Entity ent, ref PreventCollideEvent args) - { - if (args.Cancelled) - return; - - if (_holderQuery.TryComp(args.OtherEntity, out var holder) && - holder.Target == ent.Owner) - { - args.Cancelled = true; - } - } - - private void OnHolderStartup(Entity ent, ref ComponentStartup args) - { - RefreshHolderState(ent); - } - - private void OnHolderShutdown(Entity ent, ref ComponentShutdown args) - { - ent.Comp.Target = null; - ent.Comp.SlowdownEnabled = false; - DeleteHolderHandBlockers(ent.Owner); - _movement.RefreshMovementSpeedModifiers(ent.Owner); - } - - private void OnHolderRefreshMoveSpeed(Entity ent, ref RefreshMovementSpeedModifiersEvent args) - { - if (!ent.Comp.SlowdownEnabled) - return; - - args.ModifySpeed(ent.Comp.WalkModifier, ent.Comp.SprintModifier); - } - - private void OnHolderBeforeThrow(Entity ent, ref BeforeThrowEvent args) - { - if (ent.Comp.Target == null || - !TryComp(args.ItemUid, out var blocker) || - blocker.Target != ent.Comp.Target.Value) - { - return; - } - - ReleaseHolderContribution(ent.Owner, ent.Comp.Target.Value, clearIfEmpty: true); - args.Cancelled = true; - } - - private void OnHolderHandsModified(Entity ent, ref DidEquipHandEvent args) - { - if (ent.Comp.LifeStage > ComponentLifeStage.Running || - TerminatingOrDeleted(ent.Owner) || - ent.Comp.Target == null || - TerminatingOrDeleted(ent.Comp.Target.Value)) - { - return; - } - - RefreshHolderState(ent); - } - - private void OnHolderPreventCollide(Entity ent, ref PreventCollideEvent args) - { - if (args.Cancelled || - ent.Comp.Target == null || - ent.Comp.Target != args.OtherEntity) - { - return; - } - - args.Cancelled = true; - } - - private void OnHolderBlockerDropped(Entity ent, ref GettingDroppedAttemptEvent args) - { - if (!_holderQuery.TryComp(args.User, out var holder) || - holder.Target == null || - holder.Target != ent.Comp.Target) - { - return; - } - - ReleaseHolderContribution(args.User, ent.Comp.Target, clearIfEmpty: true); - } - - public bool TryToggleHold(Entity holder, EntityUid target) - { - if (!CanUseHoldAction(holder)) - return false; - - if (_holderQuery.TryComp(holder.Owner, out var activeHolder) && activeHolder.Target != null) - { - if (activeHolder.Target.Value == target) - { - ReleaseHolderContribution(holder.Owner, target, clearIfEmpty: true); - return true; - } - - PopupHolder(holder.Owner, "scp-hold-already-holding-other"); - return false; - } - - if (!CanToggleHold(holder, target)) - return false; - - var held = EnsureHeldState(target, holder.Comp); - AddHolderContribution(holder.Owner, held); - SyncHeldState(held); - return true; - } - - public bool CanToggleHold( - Entity holder, - EntityUid target, - bool quiet = false, - bool ignoreHandAvailability = false) - { - if (!Exists(target) || holder.Owner == target) - return false; - - if (!CanUseHoldAction(holder, quiet)) - return false; - - if (!_holdableQuery.HasComp(target)) - { - if (!quiet) - PopupHolder(holder.Owner, "scp-hold-target-not-holdable", ("target", target)); - return false; - } - - if (!_moverQuery.HasComp(holder.Owner) || - !_moverQuery.HasComp(target) || - !_physicsQuery.TryComp(target, out var targetPhysics) || - targetPhysics.BodyType == BodyType.Static) - { - if (!quiet) - PopupHolder(holder.Owner, "scp-hold-target-invalid", ("target", target)); - return false; - } - - if (!_container.IsInSameOrNoContainer(holder.Owner, target)) - { - if (!quiet) - PopupHolder(holder.Owner, "scp-hold-target-invalid", ("target", target)); - return false; - } - - if (TryComp(target, out _)) - { - if (!quiet) - PopupHolder(holder.Owner, "scp-hold-target-immune", ("target", target)); - return false; - } - - if (!ignoreHandAvailability && !HasAvailableHolderHand(holder.Owner)) - { - if (!quiet) - PopupHolder(holder.Owner, "scp-hold-holder-no-free-hand", ("target", target)); - return false; - } - - var range = holder.Comp.HoldRange; - if (_heldQuery.TryComp(target, out var held)) - { - range = held.HoldRange; - - if (held.FullHold && held.Holders.Count >= held.RequiredHolderCount) - { - if (!quiet) - PopupHolder(holder.Owner, "scp-hold-target-fully-held", ("target", target)); - return false; - } - } - - if (!_interaction.InRangeUnobstructed(holder.Owner, target, range)) - { - if (!quiet) - PopupHolder(holder.Owner, "scp-hold-target-too-far", ("target", target)); - return false; - } - - return true; - } - - public bool TryBreakOut(Entity held, bool viaMovement) - { - return held.Comp.FullHold - ? TryStartFullBreakout(held) - : TrySoftBreakOut(held, viaMovement); - } - - public void RefreshHeldState(Entity held) - { - _alerts.ShowAlert(held.Owner, "ScpHoldGrabbed"); - SyncHeldStatusEffect(held.Owner); - SyncPlaceholderHands(held); - _actionBlocker.UpdateCanMove(held.Owner); - - if (_net.IsClient) - _physics.UpdateIsPredicted(held.Owner); - } - - public void RefreshHolderState(Entity holder) - { - SyncHolderHandBlocker(holder); - _movement.RefreshMovementSpeedModifiers(holder.Owner); - } - - private bool TrySoftBreakOut(Entity held, bool viaMovement) - { - if (_timing.CurTime < held.Comp.SoftEscapeAvailableAt) - return false; - - if (!viaMovement) - PopupTarget(held.Owner, "scp-hold-breakout-start"); - - ClearHoldState(held, applyImmunity: false); - return true; - } - - private bool TryStartFullBreakout(Entity held) - { - if (held.Comp.FullHoldStartedAt == null) - { - PopupTarget(held.Owner, "scp-hold-breakout-too-early", ("seconds", 1)); - return false; - } - - var breakoutAvailableAt = held.Comp.FullHoldStartedAt.Value + held.Comp.FullHoldDelay; - if (_timing.CurTime < breakoutAvailableAt) - { - var remaining = breakoutAvailableAt - _timing.CurTime; - var remainingSeconds = Math.Max(1, (int) Math.Ceiling(remaining.TotalSeconds)); - PopupTarget(held.Owner, "scp-hold-breakout-too-early", ("seconds", remainingSeconds)); - return false; - } - - if (held.Comp.BreakoutDoAfterId != null) - return true; - - var doAfter = new DoAfterArgs( - EntityManager, - held.Owner, - held.Comp.FullBreakoutDuration, - new ScpHoldBreakoutDoAfterEvent(), - held.Owner, - target: held.Owner) - { - BreakOnMove = true, - BreakOnDamage = true, - NeedHand = false, - Hidden = false, - }; - - if (!_doAfter.TryStartDoAfter(doAfter, out var id)) - return false; - - SetBreakoutDoAfterId(held, id.Value.Index); - ShowBreakoutAttemptFeedback(held); - - PopupTarget(held.Owner, "scp-hold-breakout-start"); - return true; - } - - private void UpdateHeld(Entity held) - { - if (!EnsurePrimaryHolder(held)) - { - ClearHoldState(held, applyImmunity: false); - return; - } - - var desiredSoftDragDistance = GetDesiredSoftDragDistance(held); - var maintenanceRange = GetHoldMaintenanceRange(held.Comp.HoldRange, desiredSoftDragDistance); - - if (!held.Comp.FullHold) - UpdateSoftDrag(held, maintenanceRange, desiredSoftDragDistance); - else - ZeroHeldVelocity(held.Owner); - - _holdersToRemove.Clear(); - - foreach (var holderUid in held.Comp.Holders) - { - if (!Exists(holderUid) || - !_holdQuery.HasComp(holderUid) || - !_holderQuery.TryComp(holderUid, out var holder) || - holder.Target != held.Owner || - !_container.IsInSameOrNoContainer(holderUid, held.Owner) || - !_interaction.InRangeUnobstructed(holderUid, held.Owner, maintenanceRange)) - { - _holdersToRemove.Add(holderUid); - } - } - - foreach (var holderUid in _holdersToRemove) - { - ReleaseHolderContribution(holderUid, held.Owner, clearIfEmpty: false); - - if (!_heldQuery.TryComp(held.Owner, out _)) - return; - } - - if (_heldQuery.TryComp(held.Owner, out var refreshed)) - SyncHeldState((held.Owner, refreshed)); - } - - private Entity EnsureHeldState(EntityUid target, ScpHoldComponent config) - { - var created = !_heldQuery.TryComp(target, out var held); - held ??= EnsureComp(target); - - if (created) - CopyConfig(config, held); - - held.RequiredHolderCount = GetRequiredHolderCount(target); - return (target, held); - } - - private void AddHolderContribution(EntityUid holderUid, Entity held) - { - if (!held.Comp.Holders.Contains(holderUid)) - held.Comp.Holders.Add(holderUid); - - var holder = EnsureComp(holderUid); - holder.Target = held.Owner; - holder.SlowdownEnabled = false; - holder.WalkModifier = held.Comp.WalkModifier; - holder.SprintModifier = held.Comp.SprintModifier; - Dirty(holderUid, holder); - RefreshHolderState((holderUid, holder)); - } - - private void ReleaseHolderContribution(EntityUid holderUid, EntityUid targetUid, bool clearIfEmpty) - { - if (!_heldQuery.TryComp(targetUid, out var held)) - return; - - for (var i = held.Holders.Count - 1; i >= 0; i--) - { - if (held.Holders[i] == holderUid) - held.Holders.RemoveAt(i); - } - - if (_holderQuery.HasComp(holderUid)) - RemComp(holderUid); - - if (held.PrimaryHolder == holderUid) - held.PrimaryHolder = null; - - if (held.Holders.Count == 0) - { - if (clearIfEmpty) - ClearHoldState((targetUid, held), applyImmunity: false); - return; - } - - SyncHeldState((targetUid, held)); - } - - private void SyncHeldState(Entity held) - { - if (!_heldQuery.TryComp(held.Owner, out var heldComp)) - return; - - held.Comp = heldComp; - held.Comp.RequiredHolderCount = GetRequiredHolderCount(held.Owner); - - if (held.Comp.Holders.Count == 0) - { - ClearHoldState(held, applyImmunity: false); - return; - } - - if (!EnsurePrimaryHolder(held)) - { - ClearHoldState(held, applyImmunity: false); - return; - } - - if (held.Comp.Holders.Count >= held.Comp.RequiredHolderCount) - { - EnterFullHold(held); - return; - } - - ExitFullHold(held); - var desiredSoftDragDistance = GetDesiredSoftDragDistance(held); - var maintenanceRange = GetHoldMaintenanceRange(held.Comp.HoldRange, desiredSoftDragDistance); - UpdateSoftDrag(held, maintenanceRange, desiredSoftDragDistance); - UpdateHolderSlowdowns(held); - SyncPlaceholderHands(held); - Dirty(held); - } - - private void EnterFullHold(Entity held) - { - if (!held.Comp.FullHold) - { - held.Comp.FullHold = true; - held.Comp.FullHoldStartedAt = _timing.CurTime; - } - - UpdateHolderSlowdowns(held); - SyncPlaceholderHands(held); - ZeroHeldVelocity(held.Owner); - _actionBlocker.UpdateCanMove(held.Owner); - Dirty(held); - } - - private void ExitFullHold(Entity held) - { - CancelBreakoutDoAfter(held); - - if (!held.Comp.FullHold && held.Comp.FullHoldStartedAt == null) - return; - - held.Comp.FullHold = false; - held.Comp.FullHoldStartedAt = null; - SyncPlaceholderHands(held); - _actionBlocker.UpdateCanMove(held.Owner); - Dirty(held); - } - - private bool EnsurePrimaryHolder(Entity held) - { - if (held.Comp.PrimaryHolder != null && - _holderQuery.TryComp(held.Comp.PrimaryHolder.Value, out var activeHolder) && - activeHolder.Target == held.Owner && - held.Comp.Holders.Contains(held.Comp.PrimaryHolder.Value)) - { - return true; - } - - held.Comp.PrimaryHolder = null; - - foreach (var holderUid in held.Comp.Holders) - { - if (!_holderQuery.TryComp(holderUid, out var holder) || - holder.Target != held.Owner) - { - continue; - } - - held.Comp.PrimaryHolder = holderUid; - return true; - } - - return false; - } - - private void UpdateSoftDrag(Entity held, float maintenanceRange, float desiredDistance) - { - if (held.Comp.PrimaryHolder == null) - return; - - var primaryHolder = held.Comp.PrimaryHolder.Value; - if (!_holderQuery.TryComp(primaryHolder, out var holder) || - holder.Target != held.Owner || - !_container.IsInSameOrNoContainer(primaryHolder, held.Owner) || - !_interaction.InRangeUnobstructed(primaryHolder, held.Owner, maintenanceRange) || - !_physicsQuery.TryComp(held.Owner, out var heldPhysics)) - { - return; - } - - var holderCoords = _transform.GetMapCoordinates(primaryHolder); - var heldCoords = _transform.GetMapCoordinates(held.Owner); - - if (holderCoords.MapId != heldCoords.MapId) - return; - - var offset = heldCoords.Position - holderCoords.Position; - var distance = offset.Length(); - var holderVelocity = _physicsQuery.TryComp(primaryHolder, out var holderPhysics) - ? holderPhysics.LinearVelocity - : Vector2.Zero; - var direction = GetSoftDragDirection(primaryHolder, holderVelocity, offset, distance); - var desiredPosition = holderCoords.Position + direction * desiredDistance; - var correction = desiredPosition - heldCoords.Position; - var correctionDistance = correction.Length(); - - Vector2 desiredVelocity; - if (correctionDistance <= SoftDragSettleTolerance) - { - desiredVelocity = holderVelocity.LengthSquared() > SoftDragVelocityDirectionThreshold * SoftDragVelocityDirectionThreshold - ? holderVelocity - : Vector2.Zero; - } - else - { - var correctionDirection = correction / correctionDistance; - var correctionSpeed = Math.Min(correctionDistance / GetSoftDragCatchUpTime(), SoftDragMaximumCorrectionSpeed); - desiredVelocity = holderVelocity + correctionDirection * correctionSpeed; - - var relativeVelocity = heldPhysics.LinearVelocity - holderVelocity; - var awaySpeed = MathF.Max(0f, -Vector2.Dot(relativeVelocity, correctionDirection)); - if (awaySpeed > 0f) - desiredVelocity += correctionDirection * awaySpeed * SoftDragAwayVelocityStrength; - } - - ApplyHeldVelocity(held.Owner, desiredVelocity, heldPhysics); - } - - private void ClearHoldState(Entity held, bool applyImmunity) - { - if (_heldQuery.TryComp(held.Owner, out var refreshed)) - held = (held.Owner, refreshed); - - CancelBreakoutDoAfter(held); - DeleteHeldHandBlockers(held.Owner); - _actionBlocker.UpdateCanMove(held.Owner); - _holderCooldownsToApply.Clear(); - - foreach (var holderUid in held.Comp.Holders) - { - if (applyImmunity) - _holderCooldownsToApply.Add(holderUid); - - if (_holderQuery.HasComp(holderUid)) - RemComp(holderUid); - } - - held.Comp.Holders.Clear(); - held.Comp.PrimaryHolder = null; - - if (applyImmunity) - { - var immune = EnsureComp(held.Owner); - immune.ExpiresAt = _timing.CurTime + held.Comp.PostBreakoutImmunity; - Dirty(held.Owner, immune); - } - - foreach (var holderUid in _holderCooldownsToApply) - { - ApplyFullBreakoutHolderCooldown(holderUid); - } - - RemComp(held.Owner); - } - - private void UpdateHolderSlowdowns(Entity held) - { - foreach (var holderUid in held.Comp.Holders) - { - if (!_holderQuery.TryComp(holderUid, out var holder)) - continue; - - SetHolderSlowdown((holderUid, holder), true, held.Comp.WalkModifier, held.Comp.SprintModifier); - } - } - - private void SetHolderSlowdown(Entity holder, bool enabled, float walkModifier, float sprintModifier) - { - if (holder.Comp.SlowdownEnabled == enabled && - MathHelper.CloseTo(holder.Comp.WalkModifier, walkModifier) && - MathHelper.CloseTo(holder.Comp.SprintModifier, sprintModifier)) - { - return; - } - - holder.Comp.SlowdownEnabled = enabled; - holder.Comp.WalkModifier = walkModifier; - holder.Comp.SprintModifier = sprintModifier; - Dirty(holder); - _movement.RefreshMovementSpeedModifiers(holder.Owner); - } - - private void SyncPlaceholderHands(Entity held) - { - DeleteHeldHandBlockers(held.Owner); - - if (!held.Comp.FullHold || !_handsQuery.TryComp(held.Owner, out var hands)) - return; - - foreach (var hand in _hands.EnumerateHands((held.Owner, hands))) - { - if (!_hands.TryGetHeldItem((held.Owner, hands), hand, out var heldItem)) - continue; - - if (HasComp(heldItem.Value)) - continue; - - _hands.DoDrop((held.Owner, hands), hand, doDropInteraction: true); - } - - _placeholderIcons.Clear(); - foreach (var holderUid in held.Comp.Holders) - { - if (_holderQuery.TryComp(holderUid, out var holder) && holder.Target == held.Owner) - _placeholderIcons.Add(holderUid); - } - - if (_placeholderIcons.Count == 0) - return; - - var iconIndex = 0; - while (_hands.TryGetEmptyHand((held.Owner, hands), out var emptyHand)) - { - var holderUid = _placeholderIcons[iconIndex % _placeholderIcons.Count]; - if (!_virtualItem.TrySpawnVirtualItemInHand(holderUid, held.Owner, out var virtualItem, empty: emptyHand, silent: true)) - break; - - EnsureComp(virtualItem.Value); - var blocker = EnsureComp(virtualItem.Value); - - if (blocker.Target != held.Owner || blocker.Holder != holderUid) - { - blocker.Target = held.Owner; - blocker.Holder = holderUid; - Dirty(virtualItem.Value, blocker); - } - - iconIndex++; - } - } - - private void SyncHeldStatusEffect(EntityUid target) - { - if (_statusEffects.HasStatusEffect(target, GrabbedStatusEffect) || - !_statusEffects.CanAddStatusEffect(target, GrabbedStatusEffect)) - { - return; - } - - EnsureComp(target); - PredictedTrySpawnInContainer(GrabbedStatusEffect, target, StatusEffectContainerComponent.ContainerId, out _); - } - - private void SyncHolderHandBlocker(Entity holder) - { - _virtualBlockersToDelete.Clear(); - EntityUid? validBlocker = null; - var target = holder.Comp.Target; - - foreach (var heldItem in _hands.EnumerateHeld(holder.Owner)) - { - if (!TryComp(heldItem, out var virtualItem)) - { - continue; - } - - var matchesCurrentTarget = holder.Comp.LifeStage <= ComponentLifeStage.Running && - target != null && - virtualItem.BlockingEntity == target.Value; - - if (matchesCurrentTarget) - { - if (validBlocker == null) - { - validBlocker = heldItem; - RemComp(heldItem); - var blocker = EnsureComp(heldItem); - var currentTarget = target!.Value; - if (blocker.Target != currentTarget) - { - blocker.Target = currentTarget; - Dirty(heldItem, blocker); - } - continue; - } - } - - if (TryComp(heldItem, out _) || matchesCurrentTarget) - _virtualBlockersToDelete.Add((heldItem, virtualItem)); - } - - foreach (var virtualItem in _virtualBlockersToDelete) - { - _virtualItem.DeleteVirtualItem(virtualItem, holder.Owner); - } - - if (holder.Comp.LifeStage > ComponentLifeStage.Running || - holder.Comp.Target == null || - validBlocker != null) - { - return; - } - - if (!_handsQuery.TryComp(holder.Owner, out var hands) || - !_hands.TryGetEmptyHand((holder.Owner, hands), out _)) - { - return; - } - - if (!_virtualItem.TrySpawnVirtualItemInHand(holder.Comp.Target.Value, holder.Owner, out var spawnedVirtualItem, silent: true)) - return; - - var blockerComp = EnsureComp(spawnedVirtualItem.Value); - blockerComp.Target = holder.Comp.Target.Value; - Dirty(spawnedVirtualItem.Value, blockerComp); - } - - private bool HasAvailableHolderHand(EntityUid holderUid) - { - return _handsQuery.TryComp(holderUid, out var hands) && - _hands.TryGetEmptyHand((holderUid, hands), out _); - } - - private void DeleteHolderHandBlockers(EntityUid holderUid) - { - _virtualBlockersToDelete.Clear(); - - foreach (var heldItem in _hands.EnumerateHeld(holderUid)) - { - if (TryComp(heldItem, out _) && - TryComp(heldItem, out var virtualItem)) - { - _virtualBlockersToDelete.Add((heldItem, virtualItem)); - } - } - - foreach (var virtualItem in _virtualBlockersToDelete) - { - _virtualItem.DeleteVirtualItem(virtualItem, holderUid); - } - } - - private void DeleteHeldHandBlockers(EntityUid heldUid) - { - _virtualBlockersToDelete.Clear(); - - foreach (var heldItem in _hands.EnumerateHeld(heldUid)) - { - if (TryComp(heldItem, out _) && - TryComp(heldItem, out var virtualItem)) - { - _virtualBlockersToDelete.Add((heldItem, virtualItem)); - } - } - - foreach (var virtualItem in _virtualBlockersToDelete) - { - _virtualItem.DeleteVirtualItem(virtualItem, heldUid); - } - } - - private int GetRequiredHolderCount(EntityUid target) - { - if (_bodyQuery.TryComp(target, out var body)) - { - var handCount = 0; - foreach (var _ in _body.GetBodyChildrenOfType(target, BodyPartType.Hand, body)) - { - handCount++; - } - - if (handCount > 0) - return handCount; - } - - return 2; - } - - private void CopyConfig(ScpHoldComponent source, ScpHeldComponent target) - { - target.SoftEscapeCooldown = source.SoftEscapeCooldown; - target.FullHoldDelay = source.FullHoldDelay; - target.FullBreakoutDuration = source.FullBreakoutDuration; - target.PostBreakoutImmunity = source.PostBreakoutImmunity; - target.HoldRange = source.HoldRange; - target.WalkModifier = source.WalkModifier; - target.SprintModifier = source.SprintModifier; - target.SoftEscapeAvailableAt = _timing.CurTime; - target.FullHoldStartedAt = null; - } - - private bool CanUseHoldAction(Entity holder, bool quiet = false) - { - if (!IsHoldActionCoolingDown(holder, out var remaining)) - return true; - - if (!quiet) - { - var remainingSeconds = Math.Max(1, (int) Math.Ceiling(remaining.TotalSeconds)); - PopupHolder(holder.Owner, "scp-hold-holder-action-on-cooldown", ("seconds", remainingSeconds)); - } - - return false; - } - - private bool IsHoldActionCoolingDown(Entity holder, out TimeSpan remaining) - { - remaining = TimeSpan.Zero; - - if (holder.Comp.ActionEntity is not { } actionUid) - return false; - - var action = _actions.GetAction(actionUid); - if (action?.Comp.Cooldown is not { } cooldown || cooldown.End <= _timing.CurTime) - return false; - - remaining = cooldown.End - _timing.CurTime; - return true; - } - - private void ApplyFullBreakoutHolderCooldown(EntityUid holderUid) - { - if (!_holdQuery.TryComp(holderUid, out var hold) || hold.ActionEntity == null) - return; - - var cooldown = TimeSpan.FromTicks(hold.HoldActionCooldown.Ticks * 2); - _actions.SetIfBiggerCooldown(hold.ActionEntity.Value, cooldown); - } - - private float GetDesiredSoftDragDistance(Entity held) - { - return GetBaseSoftDragDistance(held.Comp.HoldRange); - } - - private static float GetHoldMaintenanceRange(float configuredRange, float desiredSoftDragDistance) - { - return MathF.Max(MathF.Max(configuredRange, SharedInteractionSystem.InteractionRange), desiredSoftDragDistance + SoftDragSnapTolerance); - } - - private static float GetBaseSoftDragDistance(float holdRange) - { - return Math.Clamp(holdRange * SoftDragDistanceFactor, SoftDragMinimumDistance, SoftDragMaximumDistance); - } - - private float GetSoftDragCatchUpTime() - { - return MathF.Max((float) _timing.TickPeriod.TotalSeconds, SoftDragCatchUpTime); - } - - private Vector2 GetSoftDragDirection(EntityUid holderUid, Vector2 holderVelocity, Vector2 offset, float distance) - { - if (distance > SoftDragSnapTolerance) - return offset / distance; - - if (holderVelocity.LengthSquared() > SoftDragVelocityDirectionThreshold * SoftDragVelocityDirectionThreshold) - return -Vector2.Normalize(holderVelocity); - - return Transform(holderUid).LocalRotation.ToWorldVec(); - } - - private void ApplyHeldVelocity(EntityUid uid, Vector2 desiredVelocity, PhysicsComponent physics) - { - if (Vector2.DistanceSquared(physics.LinearVelocity, desiredVelocity) > SoftDragVelocityTolerance * SoftDragVelocityTolerance) - _physics.SetLinearVelocity(uid, desiredVelocity, body: physics); - - if (!MathHelper.CloseTo(physics.AngularVelocity, 0f)) - _physics.SetAngularVelocity(uid, 0f, body: physics); - } - - private void ZeroHeldVelocity(EntityUid uid) - { - if (!_physicsQuery.TryComp(uid, out var physics)) - return; - - if (physics.LinearVelocity == Vector2.Zero && MathHelper.CloseTo(physics.AngularVelocity, 0f)) - return; - - _physics.SetLinearVelocity(uid, Vector2.Zero, body: physics); - _physics.SetAngularVelocity(uid, 0f, body: physics); - } - - private void CancelBreakoutDoAfter(Entity held) - { - if (held.Comp.BreakoutDoAfterId == null) - return; - - _doAfter.Cancel(held.Owner, held.Comp.BreakoutDoAfterId.Value); - SetBreakoutDoAfterId(held, null); - } - - private void SetBreakoutDoAfterId(Entity held, ushort? breakoutDoAfterId) - { - if (held.Comp.BreakoutDoAfterId == breakoutDoAfterId) - return; - - held.Comp.BreakoutDoAfterId = breakoutDoAfterId; - Dirty(held); - } - - private void ShowBreakoutAttemptFeedback(Entity held) - { - if (_net.IsClient && !_timing.IsFirstTimePredicted) - return; - - foreach (var holderUid in held.Comp.Holders) - { - if (TerminatingOrDeleted(holderUid) || !_holderQuery.TryComp(holderUid, out var holder) || holder.Target != held.Owner) - continue; - - if (_net.IsClient) - PredictedSpawnAttachedTo(BreakoutAttemptEffect, holderUid.ToCoordinates()); - else - SpawnAttachedTo(BreakoutAttemptEffect, holderUid.ToCoordinates()); - } - - if (_net.IsClient) - _audio.PlayPredicted(BreakoutAttemptSound, held.Owner, held.Owner); - else - _audio.PlayPvs(BreakoutAttemptSound, held.Owner); - } - - private void PopupHolder(EntityUid holder, string key, params (string, object)[] args) - { - if (_net.IsClient) - return; - - _popup.PopupEntity(Loc.GetString(key, args), holder, holder); - } - - private void PopupTarget(EntityUid target, string key, params (string, object)[] args) - { - if (_net.IsClient) - return; - - _popup.PopupEntity(Loc.GetString(key, args), target, target); - } } From 0a3d1ad26fe37aa2871e5f5f696d9276cfe94ec7 Mon Sep 17 00:00:00 2001 From: drdth Date: Wed, 8 Apr 2026 08:51:45 +0300 Subject: [PATCH 05/27] remove: actions --- .../Tests/_Scp/ScpHoldingTest.cs | 186 +++++++----------- .../_Scp/Holding/ScpHeldComponent.cs | 14 -- .../_Scp/Holding/ScpHoldComponent.cs | 17 +- .../_Scp/Holding/ScpHoldingEvents.cs | 5 - .../Holding/SharedScpHoldingSystem.Actions.cs | 43 ++-- .../Holding/SharedScpHoldingSystem.Events.cs | 31 --- .../_Scp/Holding/SharedScpHoldingSystem.cs | 4 +- .../_prototypes/_scp/actions/holding.ftl | 4 - .../_prototypes/_scp/actions/holding.ftl | 4 - Resources/Prototypes/_Scp/Actions/holding.yml | 32 --- 10 files changed, 106 insertions(+), 234 deletions(-) delete mode 100644 Resources/Locale/en-US/_prototypes/_scp/actions/holding.ftl delete mode 100644 Resources/Locale/ru-RU/_prototypes/_scp/actions/holding.ftl delete mode 100644 Resources/Prototypes/_Scp/Actions/holding.yml diff --git a/Content.IntegrationTests/Tests/_Scp/ScpHoldingTest.cs b/Content.IntegrationTests/Tests/_Scp/ScpHoldingTest.cs index b84dba69862..d452922f096 100644 --- a/Content.IntegrationTests/Tests/_Scp/ScpHoldingTest.cs +++ b/Content.IntegrationTests/Tests/_Scp/ScpHoldingTest.cs @@ -7,13 +7,12 @@ using Content.Shared.Alert; using Content.Server.Body.Systems; using Content.Shared._Scp.Holding; -using Content.Shared.Actions; -using Content.Shared.Actions.Components; using Content.Shared.Body.Components; using Content.Shared.Body.Part; using Content.Shared.Hands.Components; using Content.Shared.Hands.EntitySystems; using Content.Shared.Interaction.Components; +using Content.Shared.Input; using Content.Shared.Inventory.VirtualItem; using Content.Shared.Movement.Components; using Content.Shared.Movement.Events; @@ -22,15 +21,18 @@ using Content.Shared.Movement.Systems; using Content.Shared.StatusEffectNew; using Content.Shared.Throwing; +using Robust.Client.Input; using Robust.Server.Console; using Robust.Client.Physics; using Robust.Shared.GameObjects; +using Robust.Shared.Input; using Robust.Shared.Map; using Robust.Shared.Maths; using Robust.Shared.Physics.Components; using Robust.Shared.Physics.Systems; using Robust.Shared.Prototypes; using Robust.Shared.Timing; +using Robust.UnitTesting; using Content.Shared.Whitelist; namespace Content.IntegrationTests.Tests._Scp; @@ -80,7 +82,7 @@ private static EntityWhitelist CreateComponentWhitelist(params string[] componen """; [Test] - public async Task SoftHoldBreakoutByMovementAndActionRespectsCooldown() + public async Task SoftHoldBreakoutByMovementAndAlertRespectsCooldown() { await using var pair = await PoolManager.GetServerClient(new PoolSettings { Fresh = true }); var server = pair.Server; @@ -89,7 +91,6 @@ public async Task SoftHoldBreakoutByMovementAndActionRespectsCooldown() var timing = server.ResolveDependency(); var statusEffects = server.System(); var proto = server.ResolveDependency(); - var actions = server.System(); var holding = server.System(); var map = await pair.CreateTestMap(); @@ -110,7 +111,6 @@ await server.WaitAssertion(() => { Assert.That(held.FullHold, Is.False); Assert.That(held.Holders, Has.Count.EqualTo(1)); - Assert.That(held.BreakoutActionEntity, Is.Not.Null); Assert.That(statusEffects.HasStatusEffect(target, "StatusEffectScpHeld"), Is.True); Assert.That(alerts.IsShowingAlert(target, "ScpHoldGrabbed"), Is.True); }); @@ -151,22 +151,6 @@ await server.WaitAssertion(() => Assert.That(entMan.HasComponent(target), Is.True); }); - await server.WaitPost(() => - { - var held = entMan.GetComponent(target); - var action = actions.GetAction(held.BreakoutActionEntity); - var targetActions = entMan.GetComponent(target); - - Assert.That(action, Is.Not.Null); - actions.PerformAction((target, targetActions), action!.Value); - }); - await server.WaitRunTicks(2); - - await server.WaitAssertion(() => - { - Assert.That(entMan.HasComponent(target), Is.True); - }); - await server.WaitPost(() => { var held = entMan.GetComponent(target); @@ -886,13 +870,14 @@ await server.WaitAssertion(() => } [Test] - public async Task FullBreakoutByActionStartsAndCompletes() + public async Task FullBreakoutByAlertStartsAndCompletes() { await using var pair = await PoolManager.GetServerClient(new PoolSettings { Fresh = true }); var server = pair.Server; var entMan = server.EntMan; + var alerts = server.System(); var timing = server.ResolveDependency(); - var actions = server.System(); + var proto = server.ResolveDependency(); var holding = server.System(); var map = await pair.CreateTestMap(); @@ -914,12 +899,8 @@ await server.WaitPost(() => await server.WaitPost(() => { - var held = entMan.GetComponent(target); - var action = actions.GetAction(held.BreakoutActionEntity); - var targetActions = entMan.GetComponent(target); - - Assert.That(action, Is.Not.Null); - actions.PerformAction((target, targetActions), action!.Value); + var alert = proto.Index(GrabbedAlertId); + Assert.That(alerts.ActivateAlert(target, alert), Is.True); }); await server.WaitRunTicks(2); @@ -1098,7 +1079,7 @@ await client.WaitPost(() => } [Test] - public async Task ClientHoldActionPredictsSoftHoldBeforeServerAck() + public async Task ClientPullAttemptPredictsSoftHoldBeforeServerAck() { await using var pair = await PoolManager.GetServerClient(new PoolSettings { @@ -1111,6 +1092,7 @@ public async Task ClientHoldActionPredictsSoftHoldBeforeServerAck() var client = pair.Client; var sEntMan = server.EntMan; var cEntMan = client.EntMan; + var cTiming = client.ResolveDependency(); var sPhysics = server.System(); var cPhysics = client.System(); var sTransform = server.System(); @@ -1136,14 +1118,6 @@ await server.WaitPost(() => await pair.RunTicksSync(10); await pair.SyncTicks(targetDelta: 1); - EntityUid holdAction = default; - await server.WaitAssertion(() => - { - var hold = sEntMan.GetComponent(serverPlayer); - Assert.That(hold.ActionEntity, Is.Not.Null); - holdAction = hold.ActionEntity!.Value; - }); - var clientPlayer = EntityUid.Invalid; var clientTarget = EntityUid.Invalid; await client.WaitAssertion(() => @@ -1158,13 +1132,10 @@ await client.WaitAssertion(() => }); }); - var holdActionNet = sEntMan.GetNetEntity(holdAction); - var targetNet = sEntMan.GetNetEntity(target); + await PressClientPullKey(client, cEntMan, cTiming, clientTarget); - await client.WaitPost(() => + await client.WaitAssertion(() => { - cEntMan.RaisePredictiveEvent(new RequestPerformActionEvent(holdActionNet, targetNet)); - var held = cEntMan.GetComponent(clientTarget); var holderHands = cEntMan.GetComponent(clientPlayer); Assert.Multiple(() => @@ -1252,7 +1223,7 @@ await client.WaitAssertion(() => } [Test] - public async Task ClientHoldActionCooldownAndFullBreakoutPenaltyReplicate() + public async Task ClientPullCooldownAndFullBreakoutPenaltyReplicate() { await using var pair = await PoolManager.GetServerClient(new PoolSettings { @@ -1288,41 +1259,27 @@ await server.WaitPost(() => await pair.RunTicksSync(10); await pair.SyncTicks(targetDelta: 1); - EntityUid serverHoldAction = default; EntityUid clientPlayer = default; EntityUid clientFirstTarget = default; EntityUid clientBreakoutTarget = default; - EntityUid clientHoldAction = default; - - await server.WaitAssertion(() => - { - var hold = sEntMan.GetComponent(serverPlayer); - Assert.That(hold.ActionEntity, Is.Not.Null); - serverHoldAction = hold.ActionEntity!.Value; - }); await client.WaitAssertion(() => { clientPlayer = client.AttachedEntity!.Value; clientFirstTarget = ToClientEntity(sEntMan, cEntMan, firstTarget); clientBreakoutTarget = ToClientEntity(sEntMan, cEntMan, breakoutTarget); - clientHoldAction = ToClientEntity(sEntMan, cEntMan, serverHoldAction); }); - var holdActionNet = sEntMan.GetNetEntity(serverHoldAction); - var firstTargetNet = sEntMan.GetNetEntity(firstTarget); - var breakoutTargetNet = sEntMan.GetNetEntity(breakoutTarget); + await PressClientPullKey(client, cEntMan, cTiming, clientFirstTarget); + await PressClientPullKey(client, cEntMan, cTiming, clientBreakoutTarget); - await client.WaitPost(() => + await client.WaitAssertion(() => { - cEntMan.RaisePredictiveEvent(new RequestPerformActionEvent(holdActionNet, firstTargetNet)); - cEntMan.RaisePredictiveEvent(new RequestPerformActionEvent(holdActionNet, breakoutTargetNet)); - Assert.Multiple(() => { Assert.That(cEntMan.HasComponent(clientFirstTarget), Is.True); Assert.That(cEntMan.HasComponent(clientBreakoutTarget), Is.False); - Assert.That(GetActionCooldownRemaining(cEntMan, clientHoldAction, cTiming), Is.GreaterThan(TimeSpan.Zero)); + Assert.That(GetHoldCooldownRemaining(cEntMan, clientPlayer, cTiming), Is.GreaterThan(TimeSpan.Zero)); }); }); @@ -1335,21 +1292,22 @@ await server.WaitAssertion(() => { Assert.That(sEntMan.HasComponent(firstTarget), Is.True); Assert.That(sEntMan.HasComponent(breakoutTarget), Is.False); - Assert.That(GetActionCooldownRemaining(sEntMan, serverHoldAction, sTiming), Is.GreaterThan(TimeSpan.Zero)); + Assert.That(GetHoldCooldownRemaining(sEntMan, serverPlayer, sTiming), Is.GreaterThan(TimeSpan.Zero)); }); }); await client.WaitAssertion(() => { - Assert.That(GetActionCooldownRemaining(cEntMan, clientHoldAction, cTiming), Is.GreaterThan(TimeSpan.Zero)); + Assert.That(GetHoldCooldownRemaining(cEntMan, clientPlayer, cTiming), Is.GreaterThan(TimeSpan.Zero)); }); await pair.RunTicksSync(GetTickCount(sTiming, TimeSpan.FromSeconds(1)) + 1); await pair.SyncTicks(targetDelta: 1); - await client.WaitPost(() => + await PressClientPullKey(client, cEntMan, cTiming, clientFirstTarget); + + await client.WaitAssertion(() => { - cEntMan.RaisePredictiveEvent(new RequestPerformActionEvent(holdActionNet, firstTargetNet)); Assert.That(cEntMan.HasComponent(clientFirstTarget), Is.False); }); @@ -1364,9 +1322,10 @@ await server.WaitAssertion(() => await pair.RunTicksSync(GetTickCount(sTiming, TimeSpan.FromSeconds(1)) + 1); await pair.SyncTicks(targetDelta: 1); - await client.WaitPost(() => + await PressClientPullKey(client, cEntMan, cTiming, clientBreakoutTarget); + + await client.WaitAssertion(() => { - cEntMan.RaisePredictiveEvent(new RequestPerformActionEvent(holdActionNet, breakoutTargetNet)); Assert.That(cEntMan.HasComponent(clientBreakoutTarget), Is.True); }); @@ -1396,14 +1355,11 @@ await server.WaitPost(() => await server.WaitAssertion(() => { - var holderTwoHold = sEntMan.GetComponent(holderTwo); - Assert.Multiple(() => { Assert.That(sEntMan.HasComponent(breakoutTarget), Is.False); - Assert.That(GetActionCooldownRemaining(sEntMan, serverHoldAction, sTiming), Is.GreaterThanOrEqualTo(TimeSpan.FromSeconds(1.5))); - Assert.That(holderTwoHold.ActionEntity, Is.Not.Null); - Assert.That(GetActionCooldownRemaining(sEntMan, holderTwoHold.ActionEntity!.Value, sTiming), Is.GreaterThanOrEqualTo(TimeSpan.FromSeconds(1.5))); + Assert.That(GetHoldCooldownRemaining(sEntMan, serverPlayer, sTiming), Is.GreaterThanOrEqualTo(TimeSpan.FromSeconds(1.5))); + Assert.That(GetHoldCooldownRemaining(sEntMan, holderTwo, sTiming), Is.GreaterThanOrEqualTo(TimeSpan.FromSeconds(1.5))); }); }); @@ -1412,13 +1368,14 @@ await client.WaitAssertion(() => Assert.Multiple(() => { Assert.That(cEntMan.HasComponent(clientBreakoutTarget), Is.False); - Assert.That(GetActionCooldownRemaining(cEntMan, clientHoldAction, cTiming), Is.GreaterThanOrEqualTo(TimeSpan.FromSeconds(1.5))); + Assert.That(GetHoldCooldownRemaining(cEntMan, clientPlayer, cTiming), Is.GreaterThanOrEqualTo(TimeSpan.FromSeconds(1.5))); }); }); - await client.WaitPost(() => + await PressClientPullKey(client, cEntMan, cTiming, clientFirstTarget); + + await client.WaitAssertion(() => { - cEntMan.RaisePredictiveEvent(new RequestPerformActionEvent(holdActionNet, firstTargetNet)); Assert.That(cEntMan.HasComponent(clientFirstTarget), Is.False); }); @@ -1434,7 +1391,7 @@ await server.WaitAssertion(() => } [Test] - public async Task ClientSecondHoldActionPredictsFullHoldBeforeServerAck() + public async Task ClientSecondPullPredictsFullHoldBeforeServerAck() { await using var pair = await PoolManager.GetServerClient(new PoolSettings { @@ -1447,6 +1404,7 @@ public async Task ClientSecondHoldActionPredictsFullHoldBeforeServerAck() var client = pair.Client; var sEntMan = server.EntMan; var cEntMan = client.EntMan; + var cTiming = client.ResolveDependency(); var sTransform = server.System(); var sHandsSystem = server.System(); var cHandsSystem = client.System(); @@ -1470,21 +1428,6 @@ await server.WaitPost(() => await pair.RunTicksSync(10); await pair.SyncTicks(targetDelta: 1); - EntityUid holdAction = default; - await server.WaitAssertion(() => - { - var hold = sEntMan.GetComponent(serverPlayer); - Assert.That(hold.ActionEntity, Is.Not.Null); - holdAction = hold.ActionEntity!.Value; - - var held = sEntMan.GetComponent(target); - Assert.Multiple(() => - { - Assert.That(held.FullHold, Is.False); - Assert.That(held.Holders, Has.Count.EqualTo(1)); - }); - }); - var clientPlayer = EntityUid.Invalid; var clientTarget = EntityUid.Invalid; var clientHolderOne = EntityUid.Invalid; @@ -1502,13 +1445,10 @@ await client.WaitAssertion(() => }); }); - var holdActionNet = sEntMan.GetNetEntity(holdAction); - var targetNet = sEntMan.GetNetEntity(target); + await PressClientPullKey(client, cEntMan, cTiming, clientTarget); - await client.WaitPost(() => + await client.WaitAssertion(() => { - cEntMan.RaisePredictiveEvent(new RequestPerformActionEvent(holdActionNet, targetNet)); - var held = cEntMan.GetComponent(clientTarget); var hands = cEntMan.GetComponent(clientTarget); @@ -1707,7 +1647,7 @@ await client.WaitAssertion(() => } [Test] - public async Task ClientFullBreakoutActionPredictsDoAfterAndReconciles() + public async Task ClientFullBreakoutAlertPredictsDoAfterAndReconciles() { await using var pair = await PoolManager.GetServerClient(new PoolSettings { @@ -1742,18 +1682,13 @@ await server.WaitPost(() => await pair.SyncTicks(targetDelta: 1); await pair.RunTicksSync(GetTickCount(timing, TimeSpan.FromSeconds(10))); await pair.SyncTicks(targetDelta: 1); - - EntityUid breakoutAction = default; await server.WaitAssertion(() => { var held = sEntMan.GetComponent(serverPlayer); Assert.Multiple(() => { Assert.That(held.FullHold, Is.True); - Assert.That(held.BreakoutActionEntity, Is.Not.Null); }); - - breakoutAction = held.BreakoutActionEntity!.Value; }); var clientPlayer = EntityUid.Invalid; @@ -1768,11 +1703,9 @@ await client.WaitAssertion(() => Assert.That(held.FullHold, Is.True); }); - var breakoutActionNet = sEntMan.GetNetEntity(breakoutAction); - await client.WaitPost(() => { - cEntMan.RaisePredictiveEvent(new RequestPerformActionEvent(breakoutActionNet)); + cEntMan.RaisePredictiveEvent(new ClickAlertEvent("ScpHoldGrabbed")); var held = cEntMan.GetComponent(clientPlayer); Assert.Multiple(() => @@ -2109,16 +2042,47 @@ private static int GetTickCount(IGameTiming timing, TimeSpan duration) return Math.Max(1, (int)Math.Ceiling(duration.TotalSeconds / timing.TickPeriod.TotalSeconds) + 1); } - private static TimeSpan GetActionCooldownRemaining(IEntityManager entMan, EntityUid action, IGameTiming timing) + private static TimeSpan GetHoldCooldownRemaining(IEntityManager entMan, EntityUid holder, IGameTiming timing) { - if (!entMan.TryGetComponent(action, out ActionComponent? actionComp) || - actionComp.Cooldown is not { } cooldown || - cooldown.End <= timing.CurTime) + if (!entMan.TryGetComponent(holder, out ScpHoldComponent? holdComp) || + holdComp.HoldAvailableAt is not { } cooldownEnd || + cooldownEnd <= timing.CurTime) { return TimeSpan.Zero; } - return cooldown.End - timing.CurTime; + return cooldownEnd - timing.CurTime; + } + + private static async Task PressClientPullKey( + RobustIntegrationTest.ClientIntegrationInstance client, + IEntityManager entMan, + IGameTiming timing, + EntityUid cursorEntity) + { + await SendClientPullInput(client, entMan, timing, cursorEntity, BoundKeyState.Down); + await SendClientPullInput(client, entMan, timing, cursorEntity, BoundKeyState.Up); + } + + private static async Task SendClientPullInput( + RobustIntegrationTest.ClientIntegrationInstance client, + IEntityManager entMan, + IGameTiming timing, + EntityUid cursorEntity, + BoundKeyState state) + { + var inputManager = client.ResolveDependency(); + var funcId = inputManager.NetworkBindMap.KeyFunctionID(ContentKeyFunctions.TryPullObject); + var transform = entMan.GetComponent(cursorEntity); + var inputSystem = client.System(); + var message = new ClientFullInputCmdMessage(timing.CurTick, timing.TickFraction, funcId) + { + State = state, + Coordinates = transform.Coordinates, + Uid = cursorEntity, + }; + + await client.WaitPost(() => inputSystem.HandleInputCommand(client.Session!, ContentKeyFunctions.TryPullObject, message)); } private static void SetSoftEscapeAvailableAt(ScpHeldComponent held, TimeSpan value) diff --git a/Content.Shared/_Scp/Holding/ScpHeldComponent.cs b/Content.Shared/_Scp/Holding/ScpHeldComponent.cs index 7bc00a8a5d4..33b4684d6a3 100644 --- a/Content.Shared/_Scp/Holding/ScpHeldComponent.cs +++ b/Content.Shared/_Scp/Holding/ScpHeldComponent.cs @@ -1,7 +1,5 @@ -using Content.Shared.Actions; using Content.Shared.DoAfter; using Robust.Shared.GameStates; -using Robust.Shared.Prototypes; using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom; namespace Content.Shared._Scp.Holding; @@ -13,18 +11,6 @@ namespace Content.Shared._Scp.Holding; [Access(typeof(SharedScpHoldingSystem))] public sealed partial class ScpHeldComponent : Component { - /// - /// Temporary breakout action prototype granted to the target. - /// - [DataField] - public EntProtoId BreakoutAction = "ActionScpHoldBreakout"; - - /// - /// Runtime breakout action entity. - /// - [AutoNetworkedField] - public EntityUid? BreakoutActionEntity; - /// /// Whether the target is currently in the immobile full hold stage. /// diff --git a/Content.Shared/_Scp/Holding/ScpHoldComponent.cs b/Content.Shared/_Scp/Holding/ScpHoldComponent.cs index 1f62a07c393..d68b5608636 100644 --- a/Content.Shared/_Scp/Holding/ScpHoldComponent.cs +++ b/Content.Shared/_Scp/Holding/ScpHoldComponent.cs @@ -1,27 +1,20 @@ using Content.Shared.Whitelist; using Robust.Shared.GameStates; -using Robust.Shared.Prototypes; namespace Content.Shared._Scp.Holding; /// /// Grants the owner the ability to contribute to SCP holding. /// -[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState, AutoGenerateComponentPause] [Access(typeof(SharedScpHoldingSystem))] public sealed partial class ScpHoldComponent : Component { /// - /// Action prototype used to start or release a hold. + /// Next timestamp when this entity may start a new hold contribution. /// - [DataField] - public EntProtoId Action = "ActionScpHoldTarget"; - - /// - /// Runtime action entity granted to the holder. - /// - [AutoNetworkedField] - public EntityUid? ActionEntity; + [AutoNetworkedField, AutoPausedField] + public TimeSpan? HoldAvailableAt; /// /// Optional whitelist of entities this holder may grab. @@ -36,7 +29,7 @@ public sealed partial class ScpHoldComponent : Component public EntityWhitelist? HoldableBlacklist; /// - /// Cooldown applied to the hold action after each successful use. + /// Cooldown applied after each successful hold contribution start. /// [DataField] public TimeSpan HoldActionCooldown = TimeSpan.FromSeconds(1); diff --git a/Content.Shared/_Scp/Holding/ScpHoldingEvents.cs b/Content.Shared/_Scp/Holding/ScpHoldingEvents.cs index 091af12660b..41d4e815d98 100644 --- a/Content.Shared/_Scp/Holding/ScpHoldingEvents.cs +++ b/Content.Shared/_Scp/Holding/ScpHoldingEvents.cs @@ -1,4 +1,3 @@ -using Content.Shared.Actions; using Content.Shared.Alert; using Content.Shared.DoAfter; using Robust.Shared.GameObjects; @@ -6,10 +5,6 @@ namespace Content.Shared._Scp.Holding; -public sealed partial class ScpHoldActionEvent : EntityTargetActionEvent; - -public sealed partial class ScpHoldBreakoutActionEvent : InstantActionEvent; - public sealed partial class ScpHoldBreakoutAlertEvent : BaseAlertEvent; public sealed partial class ScpHoldAttemptEvent(EntityUid holder, EntityUid target) : CancellableEntityEventArgs diff --git a/Content.Shared/_Scp/Holding/SharedScpHoldingSystem.Actions.cs b/Content.Shared/_Scp/Holding/SharedScpHoldingSystem.Actions.cs index 6d4f7f42fe5..c4df06c7025 100644 --- a/Content.Shared/_Scp/Holding/SharedScpHoldingSystem.Actions.cs +++ b/Content.Shared/_Scp/Holding/SharedScpHoldingSystem.Actions.cs @@ -8,12 +8,12 @@ namespace Content.Shared._Scp.Holding; public sealed partial class SharedScpHoldingSystem { /* - * Action-local query caches, hold toggling API, breakout flow, and cooldown helpers. + * Hold-local query caches, hold toggling API, breakout flow, and cooldown helpers. */ private EntityQuery _moverQuery; private EntityQuery _holdableQuery; - private void InitializeActionQueries() + private void InitializeHoldQueries() { _moverQuery = GetEntityQuery(); _holdableQuery = GetEntityQuery(); @@ -21,9 +21,6 @@ private void InitializeActionQueries() public bool TryToggleHold(Entity holder, EntityUid target, bool attemptChecked = false) { - if (!CanUseHoldAction(holder)) - return false; - if (_holderQuery.TryComp(holder.Owner, out var activeHolder) && activeHolder.Target != null) { if (activeHolder.Target.Value == target) @@ -36,6 +33,9 @@ public bool TryToggleHold(Entity holder, EntityUid target, boo return false; } + if (!CanStartHold(holder)) + return false; + if (!CanToggleHold(holder, target, checkAttempt: !attemptChecked)) return false; @@ -43,6 +43,7 @@ public bool TryToggleHold(Entity holder, EntityUid target, boo var held = EnsureHeldState(target, holdable); AddHolderContribution(holder.Owner, held); SyncHeldState(held); + StartHoldCooldown(holder); return true; } @@ -56,7 +57,7 @@ public bool CanToggleHold( if (!Exists(target) || holder.Owner == target) return false; - if (!CanUseHoldAction(holder, quiet)) + if (!CanStartHold(holder, quiet)) return false; if (!_holdableQuery.TryComp(target, out var holdable)) @@ -212,9 +213,9 @@ private bool TryStartFullBreakout(Entity held, bool viaMovemen return true; } - private bool CanUseHoldAction(Entity holder, bool quiet = false) + private bool CanStartHold(Entity holder, bool quiet = false) { - if (!IsHoldActionCoolingDown(holder, out var remaining)) + if (!IsHoldCoolingDown(holder, out var remaining)) return true; if (!quiet) @@ -226,28 +227,34 @@ private bool CanUseHoldAction(Entity holder, bool quiet = fals return false; } - private bool IsHoldActionCoolingDown(Entity holder, out TimeSpan remaining) + private bool IsHoldCoolingDown(Entity holder, out TimeSpan remaining) { remaining = TimeSpan.Zero; - if (holder.Comp.ActionEntity is not { } actionUid) - return false; - - var action = _actions.GetAction(actionUid); - if (action?.Comp.Cooldown is not { } cooldown || cooldown.End <= _timing.CurTime) + if (holder.Comp.HoldAvailableAt is not { } availableAt || availableAt <= _timing.CurTime) return false; - remaining = cooldown.End - _timing.CurTime; + remaining = availableAt - _timing.CurTime; return true; } + private void StartHoldCooldown(Entity holder) + { + holder.Comp.HoldAvailableAt = _timing.CurTime + holder.Comp.HoldActionCooldown; + Dirty(holder); + } + private void ApplyFullBreakoutHolderCooldown(EntityUid holderUid) { - if (!_holdQuery.TryComp(holderUid, out var hold) || hold.ActionEntity == null) + if (!_holdQuery.TryComp(holderUid, out var hold)) + return; + + var cooldownEnd = _timing.CurTime + TimeSpan.FromTicks(hold.HoldActionCooldown.Ticks * 2); + if (hold.HoldAvailableAt != null && hold.HoldAvailableAt.Value >= cooldownEnd) return; - var cooldown = TimeSpan.FromTicks(hold.HoldActionCooldown.Ticks * 2); - _actions.SetIfBiggerCooldown(hold.ActionEntity.Value, cooldown); + hold.HoldAvailableAt = cooldownEnd; + Dirty(holderUid, hold); } private bool CanPassHoldAttempt(EntityUid holderUid, EntityUid targetUid) diff --git a/Content.Shared/_Scp/Holding/SharedScpHoldingSystem.Events.cs b/Content.Shared/_Scp/Holding/SharedScpHoldingSystem.Events.cs index ba7b527acf7..955ca721bc3 100644 --- a/Content.Shared/_Scp/Holding/SharedScpHoldingSystem.Events.cs +++ b/Content.Shared/_Scp/Holding/SharedScpHoldingSystem.Events.cs @@ -15,13 +15,10 @@ public sealed partial class SharedScpHoldingSystem */ private void SubscribeHoldingEvents() { - SubscribeLocalEvent(OnHoldStartup); SubscribeLocalEvent(OnHoldShutdown); - SubscribeLocalEvent(OnHoldAction); SubscribeLocalEvent(OnHeldStartup); SubscribeLocalEvent(OnHeldShutdown); - SubscribeLocalEvent(OnBreakoutAction); SubscribeLocalEvent(OnBreakoutAlert); SubscribeLocalEvent(OnBreakoutDoAfter); SubscribeLocalEvent(OnHeldMoveInput); @@ -40,18 +37,8 @@ private void SubscribeHoldingEvents() SubscribeLocalEvent(OnHolderBlockerDropped); } - private void OnHoldStartup(Entity ent, ref ComponentStartup args) - { - _actions.AddAction(ent.Owner, ref ent.Comp.ActionEntity, ent.Comp.Action); - - if (ent.Comp.ActionEntity != null) - _actions.SetUseDelay(ent.Comp.ActionEntity.Value, ent.Comp.HoldActionCooldown); - } - private void OnHoldShutdown(Entity ent, ref ComponentShutdown args) { - _actions.RemoveAction(ent.Comp.ActionEntity); - if (_net.IsClient || TerminatingOrDeleted(ent.Owner) || !_holderQuery.TryComp(ent.Owner, out var holder) || @@ -64,23 +51,13 @@ private void OnHoldShutdown(Entity ent, ref ComponentShutdown ReleaseHolderContribution(ent.Owner, holder.Target.Value, clearIfEmpty: true); } - private void OnHoldAction(Entity ent, ref ScpHoldActionEvent args) - { - if (args.Handled) - return; - - args.Handled = TryToggleHold(ent, args.Target); - } - private void OnHeldStartup(Entity ent, ref ComponentStartup args) { - _actions.AddAction(ent.Owner, ref ent.Comp.BreakoutActionEntity, ent.Comp.BreakoutAction); RefreshHeldState(ent); } private void OnHeldShutdown(Entity ent, ref ComponentShutdown args) { - _actions.RemoveAction(ent.Comp.BreakoutActionEntity); _alerts.ClearAlert(ent.Owner, "ScpHoldGrabbed"); _statusEffects.TryRemoveStatusEffect(ent.Owner, GrabbedStatusEffect); DeleteHeldHandBlockers(ent.Owner); @@ -103,14 +80,6 @@ private void OnHeldShutdown(Entity ent, ref ComponentShutdown _physics.UpdateIsPredicted(ent.Owner); } - private void OnBreakoutAction(Entity ent, ref ScpHoldBreakoutActionEvent args) - { - if (args.Handled) - return; - - args.Handled = TryBreakOut(ent, viaMovement: false); - } - private void OnBreakoutAlert(Entity ent, ref ScpHoldBreakoutAlertEvent args) { if (args.Handled) diff --git a/Content.Shared/_Scp/Holding/SharedScpHoldingSystem.cs b/Content.Shared/_Scp/Holding/SharedScpHoldingSystem.cs index 88dd745f3c3..7ad43a37948 100644 --- a/Content.Shared/_Scp/Holding/SharedScpHoldingSystem.cs +++ b/Content.Shared/_Scp/Holding/SharedScpHoldingSystem.cs @@ -1,6 +1,5 @@ using Content.Shared.ActionBlocker; using Content.Shared.Alert; -using Content.Shared.Actions; using Content.Shared.DoAfter; using Content.Shared.Interaction; using Content.Shared.Movement.Systems; @@ -21,7 +20,6 @@ public sealed partial class SharedScpHoldingSystem : EntitySystem */ [Dependency] private readonly ActionBlockerSystem _actionBlocker = default!; [Dependency] private readonly AlertsSystem _alerts = default!; - [Dependency] private readonly SharedActionsSystem _actions = default!; [Dependency] private readonly SharedContainerSystem _container = default!; [Dependency] private readonly SharedDoAfterSystem _doAfter = default!; [Dependency] private readonly SharedInteractionSystem _interaction = default!; @@ -46,7 +44,7 @@ public override void Initialize() _heldQuery = GetEntityQuery(); _holdQuery = GetEntityQuery(); _holderQuery = GetEntityQuery(); - InitializeActionQueries(); + InitializeHoldQueries(); InitializeHandQueries(); InitializeStateQueries(); SubscribeHoldingEvents(); diff --git a/Resources/Locale/en-US/_prototypes/_scp/actions/holding.ftl b/Resources/Locale/en-US/_prototypes/_scp/actions/holding.ftl deleted file mode 100644 index 0d2ff8ed9f9..00000000000 --- a/Resources/Locale/en-US/_prototypes/_scp/actions/holding.ftl +++ /dev/null @@ -1,4 +0,0 @@ -ent-ActionScpHoldTarget = forceful grip - .desc = Grab a nearby target and keep them restrained. One of your hands stays occupied while the grip lasts. -ent-ActionScpHoldBreakout = struggle free - .desc = Attempt to break out of a forceful grip. The grabbed status effect can trigger the same breakout attempt. diff --git a/Resources/Locale/ru-RU/_prototypes/_scp/actions/holding.ftl b/Resources/Locale/ru-RU/_prototypes/_scp/actions/holding.ftl deleted file mode 100644 index 46f4a51c6f9..00000000000 --- a/Resources/Locale/ru-RU/_prototypes/_scp/actions/holding.ftl +++ /dev/null @@ -1,4 +0,0 @@ -ent-ActionScpHoldTarget = силовой захват - .desc = Схватить ближайшую цель и удерживать её силой. Пока захват активен, одна ваша рука занята. -ent-ActionScpHoldBreakout = попытка вырваться - .desc = Попытаться освободиться из силового удержания. Ту же попытку можно начать через статус-эффект удержания. diff --git a/Resources/Prototypes/_Scp/Actions/holding.yml b/Resources/Prototypes/_Scp/Actions/holding.yml deleted file mode 100644 index 523a4977d5e..00000000000 --- a/Resources/Prototypes/_Scp/Actions/holding.yml +++ /dev/null @@ -1,32 +0,0 @@ -- type: entity - id: ActionScpHoldTarget - categories: [ HideSpawnMenu ] - components: - - type: Action - icon: - sprite: Objects/Misc/handcuffs.rsi - state: handcuff - itemIconStyle: NoItem - priority: 12 - - type: TargetAction - interactOnMiss: false - checkCanAccess: false - range: 0 - ignoreContainer: true - - type: EntityTargetAction - event: !type:ScpHoldActionEvent - canTargetSelf: false - -- type: entity - id: ActionScpHoldBreakout - categories: [ HideSpawnMenu ] - components: - - type: Action - checkCanInteract: false - icon: - sprite: Objects/Misc/handcuffs.rsi - state: handcuff - itemIconStyle: NoItem - priority: 12 - - type: InstantAction - event: !type:ScpHoldBreakoutActionEvent From 9759c56ebfd1affb9046f3b639db5f79bc30b329 Mon Sep 17 00:00:00 2001 From: drdth Date: Mon, 13 Apr 2026 05:27:37 +0300 Subject: [PATCH 06/27] add: add whitelist/blaclist --- Resources/Prototypes/Entities/Mobs/Species/base.yml | 2 +- .../_Scp/Entities/Mobs/Player/Scp/Main/scp096.yml | 1 + .../external_administrative_zone_commandant.yml | 7 +++++++ .../external_administrative_zone_officer.yml | 7 +++++++ .../Jobs/CommandantSquad/field_medical_specialist.yml | 7 +++++++ .../junior_external_administrative_zone_officer.yml | 7 +++++++ .../senior_external_administrative_zone_officer.yml | 7 +++++++ .../_Scp/Roles/Jobs/LowAccessPersonnel/class_d.yml | 9 +++++++++ .../Roles/Jobs/LowAccessPersonnel/class_d_botanist.yml | 9 +++++++++ .../_Scp/Roles/Jobs/LowAccessPersonnel/class_d_cook.yml | 9 +++++++++ .../Roles/Jobs/LowAccessPersonnel/class_d_janitor.yml | 9 +++++++++ .../heavy_containment_zone_commandant.yml | 9 +++++++++ .../heavy_containment_zone_officer.yml | 9 +++++++++ .../junior_heavy_containment_zone_officer.yml | 9 +++++++++ .../senior_heavy_containment_zone_officer.yml | 9 +++++++++ 15 files changed, 109 insertions(+), 1 deletion(-) diff --git a/Resources/Prototypes/Entities/Mobs/Species/base.yml b/Resources/Prototypes/Entities/Mobs/Species/base.yml index 797aa999bd1..2ed83ae1f95 100644 --- a/Resources/Prototypes/Entities/Mobs/Species/base.yml +++ b/Resources/Prototypes/Entities/Mobs/Species/base.yml @@ -278,7 +278,7 @@ - type: CritHeartbeat # Sunrise-End # Fire start - - type: ScpHold + - type: ScpHold # TODO: Убрать перед мержем - type: ScpHoldable - type: FieldOfView - type: Blinkable diff --git a/Resources/Prototypes/_Scp/Entities/Mobs/Player/Scp/Main/scp096.yml b/Resources/Prototypes/_Scp/Entities/Mobs/Player/Scp/Main/scp096.yml index edbdfe09adb..3be48c69350 100644 --- a/Resources/Prototypes/_Scp/Entities/Mobs/Player/Scp/Main/scp096.yml +++ b/Resources/Prototypes/_Scp/Entities/Mobs/Player/Scp/Main/scp096.yml @@ -115,6 +115,7 @@ - type: Hands showInHands: false disableExplosionRecursion: true + - type: ScpHoldable - type: GhostPanelAntagonistMarker name: ghost-panel-antagonist-scp-name description: ghost-panel-antagonist-scp-description diff --git a/Resources/Prototypes/_Scp/Roles/Jobs/CommandantSquad/external_administrative_zone_commandant.yml b/Resources/Prototypes/_Scp/Roles/Jobs/CommandantSquad/external_administrative_zone_commandant.yml index 753d503f2d6..ada744bf5d5 100644 --- a/Resources/Prototypes/_Scp/Roles/Jobs/CommandantSquad/external_administrative_zone_commandant.yml +++ b/Resources/Prototypes/_Scp/Roles/Jobs/CommandantSquad/external_administrative_zone_commandant.yml @@ -28,6 +28,13 @@ class: C - type: AccessLevel level: Three + - type: ScpHold + holdableWhitelist: + components: + - HumanoidAppearance + holdableBlacklist: + components: + - Scp - type: ScpAnnounceOnSpawn text: squad-commander-announce-on-spawn channels: diff --git a/Resources/Prototypes/_Scp/Roles/Jobs/CommandantSquad/external_administrative_zone_officer.yml b/Resources/Prototypes/_Scp/Roles/Jobs/CommandantSquad/external_administrative_zone_officer.yml index 10b8e31d5a5..43e61dd14f7 100644 --- a/Resources/Prototypes/_Scp/Roles/Jobs/CommandantSquad/external_administrative_zone_officer.yml +++ b/Resources/Prototypes/_Scp/Roles/Jobs/CommandantSquad/external_administrative_zone_officer.yml @@ -28,6 +28,13 @@ class: C - type: AccessLevel level: Three + - type: ScpHold + holdableWhitelist: + components: + - HumanoidAppearance + holdableBlacklist: + components: + - Scp - type: SpeakOnEyeStateChange # переопределение моргания с другими параметрами для охраны и МОГа # Часть фраз уходит, так как более разговорная. А эти парни натренированы. phraseOnClose: diff --git a/Resources/Prototypes/_Scp/Roles/Jobs/CommandantSquad/field_medical_specialist.yml b/Resources/Prototypes/_Scp/Roles/Jobs/CommandantSquad/field_medical_specialist.yml index ad1027479c3..3a06e87df35 100644 --- a/Resources/Prototypes/_Scp/Roles/Jobs/CommandantSquad/field_medical_specialist.yml +++ b/Resources/Prototypes/_Scp/Roles/Jobs/CommandantSquad/field_medical_specialist.yml @@ -31,6 +31,13 @@ class: C - type: AccessLevel level: Three + - type: ScpHold + holdableWhitelist: + components: + - HumanoidAppearance + holdableBlacklist: + components: + - Scp - type: Fear phobias: - Exoremophobia diff --git a/Resources/Prototypes/_Scp/Roles/Jobs/CommandantSquad/junior_external_administrative_zone_officer.yml b/Resources/Prototypes/_Scp/Roles/Jobs/CommandantSquad/junior_external_administrative_zone_officer.yml index bcd4dc7d15c..f83018f592f 100644 --- a/Resources/Prototypes/_Scp/Roles/Jobs/CommandantSquad/junior_external_administrative_zone_officer.yml +++ b/Resources/Prototypes/_Scp/Roles/Jobs/CommandantSquad/junior_external_administrative_zone_officer.yml @@ -27,6 +27,13 @@ class: C - type: AccessLevel level: Three + - type: ScpHold + holdableWhitelist: + components: + - HumanoidAppearance + holdableBlacklist: + components: + - Scp - type: SpeakOnEyeStateChange # переопределение моргания с другими параметрами для охраны и МОГа # Часть фраз уходит, так как более разговорная. А эти парни натренированы. phraseOnClose: diff --git a/Resources/Prototypes/_Scp/Roles/Jobs/CommandantSquad/senior_external_administrative_zone_officer.yml b/Resources/Prototypes/_Scp/Roles/Jobs/CommandantSquad/senior_external_administrative_zone_officer.yml index e4353f351a1..d3709e6686d 100644 --- a/Resources/Prototypes/_Scp/Roles/Jobs/CommandantSquad/senior_external_administrative_zone_officer.yml +++ b/Resources/Prototypes/_Scp/Roles/Jobs/CommandantSquad/senior_external_administrative_zone_officer.yml @@ -28,6 +28,13 @@ class: C - type: AccessLevel level: Three + - type: ScpHold + holdableWhitelist: + components: + - HumanoidAppearance + holdableBlacklist: + components: + - Scp - type: SpeakOnEyeStateChange # переопределение моргания с другими параметрами для охраны и МОГа # Часть фраз уходит, так как более разговорная. А эти парни натренированы. phraseOnClose: diff --git a/Resources/Prototypes/_Scp/Roles/Jobs/LowAccessPersonnel/class_d.yml b/Resources/Prototypes/_Scp/Roles/Jobs/LowAccessPersonnel/class_d.yml index 00efb76ff5b..abf63f08253 100644 --- a/Resources/Prototypes/_Scp/Roles/Jobs/LowAccessPersonnel/class_d.yml +++ b/Resources/Prototypes/_Scp/Roles/Jobs/LowAccessPersonnel/class_d.yml @@ -18,6 +18,15 @@ - ExternalAdministrativeZoneSecurityService includeJobName: true requiredGameRunLevel: InRound + - type: ScpHold + holdableWhitelist: + components: + - Scp096 + holdableBlacklist: + components: + - ActiveScp096Rage + - ActiveScp096HeatingUp + - ActiveScp096WithoutFace - type: startingGear id: ClassDGear diff --git a/Resources/Prototypes/_Scp/Roles/Jobs/LowAccessPersonnel/class_d_botanist.yml b/Resources/Prototypes/_Scp/Roles/Jobs/LowAccessPersonnel/class_d_botanist.yml index ca349700658..67eeb5fca00 100644 --- a/Resources/Prototypes/_Scp/Roles/Jobs/LowAccessPersonnel/class_d_botanist.yml +++ b/Resources/Prototypes/_Scp/Roles/Jobs/LowAccessPersonnel/class_d_botanist.yml @@ -23,6 +23,15 @@ - ExternalAdministrativeZoneSecurityService includeJobName: true requiredGameRunLevel: InRound + - type: ScpHold + holdableWhitelist: + components: + - Scp096 + holdableBlacklist: + components: + - ActiveScp096Rage + - ActiveScp096HeatingUp + - ActiveScp096WithoutFace - type: startingGear id: ClassDBotanistGear diff --git a/Resources/Prototypes/_Scp/Roles/Jobs/LowAccessPersonnel/class_d_cook.yml b/Resources/Prototypes/_Scp/Roles/Jobs/LowAccessPersonnel/class_d_cook.yml index af37b4abf9e..fa4d0e42134 100644 --- a/Resources/Prototypes/_Scp/Roles/Jobs/LowAccessPersonnel/class_d_cook.yml +++ b/Resources/Prototypes/_Scp/Roles/Jobs/LowAccessPersonnel/class_d_cook.yml @@ -23,6 +23,15 @@ - ExternalAdministrativeZoneSecurityService includeJobName: true requiredGameRunLevel: InRound + - type: ScpHold + holdableWhitelist: + components: + - Scp096 + holdableBlacklist: + components: + - ActiveScp096Rage + - ActiveScp096HeatingUp + - ActiveScp096WithoutFace - type: startingGear id: ClassDCookGear diff --git a/Resources/Prototypes/_Scp/Roles/Jobs/LowAccessPersonnel/class_d_janitor.yml b/Resources/Prototypes/_Scp/Roles/Jobs/LowAccessPersonnel/class_d_janitor.yml index 2eb3e3f2278..b316f0af7c1 100644 --- a/Resources/Prototypes/_Scp/Roles/Jobs/LowAccessPersonnel/class_d_janitor.yml +++ b/Resources/Prototypes/_Scp/Roles/Jobs/LowAccessPersonnel/class_d_janitor.yml @@ -20,6 +20,15 @@ - ExternalAdministrativeZoneSecurityService includeJobName: true requiredGameRunLevel: InRound + - type: ScpHold + holdableWhitelist: + components: + - Scp096 + holdableBlacklist: + components: + - ActiveScp096Rage + - ActiveScp096HeatingUp + - ActiveScp096WithoutFace - type: startingGear id: ClassDJanitorGear diff --git a/Resources/Prototypes/_Scp/Roles/Jobs/SpecialPurposeSquad/heavy_containment_zone_commandant.yml b/Resources/Prototypes/_Scp/Roles/Jobs/SpecialPurposeSquad/heavy_containment_zone_commandant.yml index ab5fe8465a1..8daa40d8746 100644 --- a/Resources/Prototypes/_Scp/Roles/Jobs/SpecialPurposeSquad/heavy_containment_zone_commandant.yml +++ b/Resources/Prototypes/_Scp/Roles/Jobs/SpecialPurposeSquad/heavy_containment_zone_commandant.yml @@ -28,6 +28,15 @@ class: C - type: AccessLevel level: Three + - type: ScpHold + holdableWhitelist: + components: + - Scp096 + holdableBlacklist: + components: + - ActiveScp096Rage + - ActiveScp096HeatingUp + - ActiveScp096WithoutFace - type: ScpAnnounceOnSpawn text: squad-commander-announce-on-spawn channels: diff --git a/Resources/Prototypes/_Scp/Roles/Jobs/SpecialPurposeSquad/heavy_containment_zone_officer.yml b/Resources/Prototypes/_Scp/Roles/Jobs/SpecialPurposeSquad/heavy_containment_zone_officer.yml index c6ab3659cbd..712b92f6b23 100644 --- a/Resources/Prototypes/_Scp/Roles/Jobs/SpecialPurposeSquad/heavy_containment_zone_officer.yml +++ b/Resources/Prototypes/_Scp/Roles/Jobs/SpecialPurposeSquad/heavy_containment_zone_officer.yml @@ -28,6 +28,15 @@ class: C - type: AccessLevel level: Three + - type: ScpHold + holdableWhitelist: + components: + - Scp096 + holdableBlacklist: + components: + - ActiveScp096Rage + - ActiveScp096HeatingUp + - ActiveScp096WithoutFace - type: SpeakOnEyeStateChange # переопределение моргания с другими параметрами для охраны и МОГа # Часть фраз уходит, так как более разговорная. А эти парни натренированы. phraseOnClose: diff --git a/Resources/Prototypes/_Scp/Roles/Jobs/SpecialPurposeSquad/junior_heavy_containment_zone_officer.yml b/Resources/Prototypes/_Scp/Roles/Jobs/SpecialPurposeSquad/junior_heavy_containment_zone_officer.yml index 99ad7d3e958..ebc833989d0 100644 --- a/Resources/Prototypes/_Scp/Roles/Jobs/SpecialPurposeSquad/junior_heavy_containment_zone_officer.yml +++ b/Resources/Prototypes/_Scp/Roles/Jobs/SpecialPurposeSquad/junior_heavy_containment_zone_officer.yml @@ -27,6 +27,15 @@ class: C - type: AccessLevel level: Three + - type: ScpHold + holdableWhitelist: + components: + - Scp096 + holdableBlacklist: + components: + - ActiveScp096Rage + - ActiveScp096HeatingUp + - ActiveScp096WithoutFace - type: SpeakOnEyeStateChange # переопределение моргания с другими параметрами для охраны и МОГа # Часть фраз уходит, так как более разговорная. А эти парни натренированы. phraseOnClose: diff --git a/Resources/Prototypes/_Scp/Roles/Jobs/SpecialPurposeSquad/senior_heavy_containment_zone_officer.yml b/Resources/Prototypes/_Scp/Roles/Jobs/SpecialPurposeSquad/senior_heavy_containment_zone_officer.yml index b5a6901a778..4b90ab6e995 100644 --- a/Resources/Prototypes/_Scp/Roles/Jobs/SpecialPurposeSquad/senior_heavy_containment_zone_officer.yml +++ b/Resources/Prototypes/_Scp/Roles/Jobs/SpecialPurposeSquad/senior_heavy_containment_zone_officer.yml @@ -28,6 +28,15 @@ class: C - type: AccessLevel level: Three + - type: ScpHold + holdableWhitelist: + components: + - Scp096 + holdableBlacklist: + components: + - ActiveScp096Rage + - ActiveScp096HeatingUp + - ActiveScp096WithoutFace - type: SpeakOnEyeStateChange # переопределение моргания с другими параметрами для охраны и МОГа # Часть фраз уходит, так как более разговорная. А эти парни натренированы. phraseOnClose: From e43a8f6a123536da4b3e98b57570439e4cd8cee3 Mon Sep 17 00:00:00 2001 From: drdth Date: Tue, 14 Apr 2026 03:27:58 +0300 Subject: [PATCH 07/27] add: damn update. Added action blocking, code refactor --- .../Holding/ScpHoldingPredictionSystem.cs | 33 ++-- .../Tests/_Scp/ScpHoldingTest.cs | 121 +++++++++++++++ .../_Scp/Holding/ScpHeldComponent.cs | 6 - .../Holding/ScpHeldHandBlockerComponent.cs | 4 +- .../_Scp/Holding/ScpHoldComponent.cs | 2 +- .../Holding/ScpHoldRestrictedComponent.cs | 10 ++ Content.Shared/_Scp/Holding/ScpHoldStage.cs | 7 + .../Holding/SharedScpHoldingSystem.Actions.cs | 92 ++++++++--- .../Holding/SharedScpHoldingSystem.Drag.cs | 20 ++- .../Holding/SharedScpHoldingSystem.Events.cs | 84 +++++----- .../SharedScpHoldingSystem.Feedback.cs | 39 +++-- .../Holding/SharedScpHoldingSystem.Hands.cs | 26 ++-- .../SharedScpHoldingSystem.Restrictions.cs | 53 +++++++ .../Holding/SharedScpHoldingSystem.State.cs | 144 +++++++++++++----- .../_Scp/Holding/SharedScpHoldingSystem.cs | 51 ++++++- .../Scp096/Main/Components/Scp096Component.cs | 28 ++++ .../Systems/SharedScp096System.Holding.cs | 68 +++++++++ .../Main/Systems/SharedScp096System.Rage.cs | 4 + .../Systems/SharedScp096System.WithoutFace.cs | 2 + .../Scp096/Main/Systems/SharedScp096System.cs | 1 + .../en-US/_strings/_scp/holding/holding.ftl | 1 + .../ru-RU/_strings/_scp/holding/holding.ftl | 1 + Resources/Prototypes/Actions/types.yml | 2 + Resources/Prototypes/_Scp/Actions/scp096.yml | 6 + .../Administration/security_commander.yml | 4 + .../heavy_containment_zone_commandant.yml | 1 + .../heavy_containment_zone_officer.yml | 1 + .../junior_heavy_containment_zone_officer.yml | 1 + .../senior_heavy_containment_zone_officer.yml | 1 + 29 files changed, 662 insertions(+), 151 deletions(-) create mode 100644 Content.Shared/_Scp/Holding/ScpHoldRestrictedComponent.cs create mode 100644 Content.Shared/_Scp/Holding/ScpHoldStage.cs create mode 100644 Content.Shared/_Scp/Holding/SharedScpHoldingSystem.Restrictions.cs create mode 100644 Content.Shared/_Scp/Scp096/Main/Systems/SharedScp096System.Holding.cs diff --git a/Content.Client/_Scp/Holding/ScpHoldingPredictionSystem.cs b/Content.Client/_Scp/Holding/ScpHoldingPredictionSystem.cs index 198d3d4c8a9..a5fc75f719d 100644 --- a/Content.Client/_Scp/Holding/ScpHoldingPredictionSystem.cs +++ b/Content.Client/_Scp/Holding/ScpHoldingPredictionSystem.cs @@ -94,14 +94,12 @@ private void OnHolderAfterState(Entity ent, ref AfterAutoHan private void OnBlockerUnequipped(Entity ent, ref GotUnequippedHandEvent args) { if (_player.LocalEntity != args.User) - { return; - } - if (_holderQuery.TryComp(args.User, out var holder) && - holder.Target == ent.Comp.Target) + if (_holderQuery.TryComp(args.User, out var holder)) { - return; + if (holder.Target == ent.Comp.Target) + return; } SuppressBlockerRespawn(args.User, ent.Comp.Target); @@ -118,10 +116,13 @@ private void OnUpdateHeldPredicted(Entity ent, ref UpdateIsPre return; } - if (_holderQuery.TryComp(local, out var localHolder) && localHolder.Target == ent.Owner) + if (_holderQuery.TryComp(local, out var localHolder)) { - args.IsPredicted = true; - return; + if (localHolder.Target == ent.Owner) + { + args.IsPredicted = true; + return; + } } for (var i = 0; i < ent.Comp.Holders.Count; i++) @@ -166,13 +167,17 @@ private void DeleteSuppressedBlockers(EntityUid holder, EntityUid target) foreach (var heldItem in _hands.EnumerateHeld((holder, hands))) { - if (!TryComp(heldItem, out var virtualItem) || - !TryComp(heldItem, out var blocker) || - virtualItem.BlockingEntity != target || - blocker.Target != target) - { + if (!TryComp(heldItem, out var virtualItem)) + continue; + + if (!TryComp(heldItem, out var blocker)) + continue; + + if (virtualItem.BlockingEntity != target) + continue; + + if (blocker.Target != target) continue; - } _virtualItem.DeleteVirtualItem((heldItem, virtualItem), holder); } diff --git a/Content.IntegrationTests/Tests/_Scp/ScpHoldingTest.cs b/Content.IntegrationTests/Tests/_Scp/ScpHoldingTest.cs index d452922f096..26235e7791d 100644 --- a/Content.IntegrationTests/Tests/_Scp/ScpHoldingTest.cs +++ b/Content.IntegrationTests/Tests/_Scp/ScpHoldingTest.cs @@ -1918,6 +1918,127 @@ await server.WaitAssertion(() => await pair.CleanReturnAsync(); } + [Test] + public async Task SoftHoldTargetTeleportClearsStateOnServerAndClient() + { + await using var pair = await PoolManager.GetServerClient(new PoolSettings + { + Fresh = true, + Connected = true, + DummyTicker = false, + }); + + var server = pair.Server; + var client = pair.Client; + var sEntMan = server.EntMan; + var cEntMan = client.EntMan; + var holding = server.System(); + var sTransform = server.System(); + var sAlerts = server.System(); + var cAlerts = client.System(); + var sStatusEffects = server.System(); + var cStatusEffects = client.System(); + var sHandsSystem = server.System(); + var cHandsSystem = client.System(); + var map = await pair.CreateTestMap(); + + var serverPlayer = pair.Player!.AttachedEntity!.Value; + EntityUid holder = default; + + await server.WaitPost(() => + { + sTransform.SetCoordinates(serverPlayer, map.GridCoords); + holder = sEntMan.SpawnEntity(HolderPrototype, map.GridCoords.Offset(new Vector2(0.1f, 0f))); + StartHold(sEntMan, holding, holder, serverPlayer); + }); + + await pair.RunTicksSync(10); + await pair.SyncTicks(targetDelta: 1); + + await server.WaitAssertion(() => + { + var held = sEntMan.GetComponent(serverPlayer); + var holderState = sEntMan.GetComponent(holder); + var holderHands = sEntMan.GetComponent(holder); + + Assert.Multiple(() => + { + Assert.That(held.FullHold, Is.False); + Assert.That(held.Holders, Has.Count.EqualTo(1)); + Assert.That(holderState.Target, Is.EqualTo(serverPlayer)); + Assert.That(CountHolderHandBlockers(sEntMan, sHandsSystem, holder, serverPlayer, holderHands), Is.EqualTo(1)); + Assert.That(CountHolderTargetVirtualItems(sEntMan, sHandsSystem, holder, serverPlayer, holderHands), Is.EqualTo(1)); + Assert.That(sStatusEffects.HasStatusEffect(serverPlayer, "StatusEffectScpHeld"), Is.True); + Assert.That(sAlerts.IsShowingAlert(serverPlayer, "ScpHoldGrabbed"), Is.True); + }); + }); + + var clientPlayer = EntityUid.Invalid; + var clientHolder = EntityUid.Invalid; + await client.WaitAssertion(() => + { + clientPlayer = client.AttachedEntity!.Value; + clientHolder = ToClientEntity(sEntMan, cEntMan, holder); + + var held = cEntMan.GetComponent(clientPlayer); + var holderState = cEntMan.GetComponent(clientHolder); + var holderHands = cEntMan.GetComponent(clientHolder); + + Assert.Multiple(() => + { + Assert.That(held.FullHold, Is.False); + Assert.That(held.Holders, Has.Count.EqualTo(1)); + Assert.That(holderState.Target, Is.EqualTo(clientPlayer)); + Assert.That(CountHolderHandBlockers(cEntMan, cHandsSystem, clientHolder, clientPlayer, holderHands), Is.EqualTo(1)); + Assert.That(CountHolderTargetVirtualItems(cEntMan, cHandsSystem, clientHolder, clientPlayer, holderHands), Is.EqualTo(1)); + Assert.That(cStatusEffects.HasStatusEffect(clientPlayer, "StatusEffectScpHeld"), Is.True); + Assert.That(cAlerts.IsShowingAlert(clientPlayer, "ScpHoldGrabbed"), Is.True); + }); + }); + + await server.WaitPost(() => + { + sTransform.SetCoordinates(serverPlayer, map.GridCoords.Offset(new Vector2(10f, 0f))); + }); + + await pair.RunTicksSync(10); + await pair.SyncTicks(targetDelta: 1); + + await server.WaitAssertion(() => + { + var holderHands = sEntMan.GetComponent(holder); + + Assert.Multiple(() => + { + Assert.That(sEntMan.HasComponent(serverPlayer), Is.False); + Assert.That(sEntMan.HasComponent(serverPlayer), Is.False); + Assert.That(sEntMan.HasComponent(holder), Is.False); + Assert.That(CountHolderHandBlockers(sEntMan, sHandsSystem, holder, serverPlayer, holderHands), Is.EqualTo(0)); + Assert.That(CountHolderTargetVirtualItems(sEntMan, sHandsSystem, holder, serverPlayer, holderHands), Is.EqualTo(0)); + Assert.That(sStatusEffects.HasStatusEffect(serverPlayer, "StatusEffectScpHeld"), Is.False); + Assert.That(sAlerts.IsShowingAlert(serverPlayer, "ScpHoldGrabbed"), Is.False); + }); + }); + + await client.WaitAssertion(() => + { + var holderHands = cEntMan.GetComponent(clientHolder); + + Assert.Multiple(() => + { + Assert.That(cEntMan.HasComponent(clientPlayer), Is.False); + Assert.That(cEntMan.HasComponent(clientPlayer), Is.False); + Assert.That(cEntMan.HasComponent(clientHolder), Is.False); + Assert.That(CountHolderHandBlockers(cEntMan, cHandsSystem, clientHolder, clientPlayer, holderHands), Is.EqualTo(0)); + Assert.That(CountHolderTargetVirtualItems(cEntMan, cHandsSystem, clientHolder, clientPlayer, holderHands), Is.EqualTo(0)); + Assert.That(cStatusEffects.HasStatusEffect(clientPlayer, "StatusEffectScpHeld"), Is.False); + Assert.That(cAlerts.IsShowingAlert(clientPlayer, "ScpHoldGrabbed"), Is.False); + }); + }); + + await pair.CleanReturnAsync(); + } + private static int CountBlockingVirtualHands(IEntityManager entMan, SharedHandsSystem handsSystem, EntityUid uid, HandsComponent hands) { return handsSystem.EnumerateHeld((uid, hands)).Count(item => diff --git a/Content.Shared/_Scp/Holding/ScpHeldComponent.cs b/Content.Shared/_Scp/Holding/ScpHeldComponent.cs index 33b4684d6a3..5663627e850 100644 --- a/Content.Shared/_Scp/Holding/ScpHeldComponent.cs +++ b/Content.Shared/_Scp/Holding/ScpHeldComponent.cs @@ -1,4 +1,3 @@ -using Content.Shared.DoAfter; using Robust.Shared.GameStates; using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom; @@ -44,13 +43,11 @@ public sealed partial class ScpHeldComponent : Component /// /// Required contributor count for entering full hold. /// - [AutoNetworkedField] public int RequiredHolderCount = 2; /// /// Copied soft breakout cooldown configuration from the initial holdable target. /// - [AutoNetworkedField] public TimeSpan SoftEscapeCooldown = TimeSpan.FromSeconds(1); /// @@ -68,7 +65,6 @@ public sealed partial class ScpHeldComponent : Component /// /// Copied post-breakout immunity duration from the initial holdable target. /// - [AutoNetworkedField] public TimeSpan PostBreakoutImmunity = TimeSpan.FromSeconds(5); /// @@ -80,13 +76,11 @@ public sealed partial class ScpHeldComponent : Component /// /// Copied walk slowdown applied through . /// - [AutoNetworkedField] public float WalkModifier = 0.5f; /// /// Copied sprint slowdown applied through . /// - [AutoNetworkedField] public float SprintModifier = 0.5f; /// diff --git a/Content.Shared/_Scp/Holding/ScpHeldHandBlockerComponent.cs b/Content.Shared/_Scp/Holding/ScpHeldHandBlockerComponent.cs index 2fe590610bb..88b1250a0ca 100644 --- a/Content.Shared/_Scp/Holding/ScpHeldHandBlockerComponent.cs +++ b/Content.Shared/_Scp/Holding/ScpHeldHandBlockerComponent.cs @@ -5,19 +5,17 @@ namespace Content.Shared._Scp.Holding; /// /// Marks a victim hand placeholder virtual item created by SCP holding. /// -[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] +[RegisterComponent, NetworkedComponent] [Access(typeof(SharedScpHoldingSystem))] public sealed partial class ScpHeldHandBlockerComponent : Component { /// /// Held target whose hand is occupied by this placeholder. /// - [AutoNetworkedField] public EntityUid Target; /// /// Holder whose sprite is shown in this placeholder. /// - [AutoNetworkedField] public EntityUid Holder; } diff --git a/Content.Shared/_Scp/Holding/ScpHoldComponent.cs b/Content.Shared/_Scp/Holding/ScpHoldComponent.cs index d68b5608636..1b5e56bf91e 100644 --- a/Content.Shared/_Scp/Holding/ScpHoldComponent.cs +++ b/Content.Shared/_Scp/Holding/ScpHoldComponent.cs @@ -6,7 +6,7 @@ namespace Content.Shared._Scp.Holding; /// /// Grants the owner the ability to contribute to SCP holding. /// -[RegisterComponent, NetworkedComponent, AutoGenerateComponentState, AutoGenerateComponentPause] +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState(fieldDeltas: true), AutoGenerateComponentPause] [Access(typeof(SharedScpHoldingSystem))] public sealed partial class ScpHoldComponent : Component { diff --git a/Content.Shared/_Scp/Holding/ScpHoldRestrictedComponent.cs b/Content.Shared/_Scp/Holding/ScpHoldRestrictedComponent.cs new file mode 100644 index 00000000000..bbd0c66cbad --- /dev/null +++ b/Content.Shared/_Scp/Holding/ScpHoldRestrictedComponent.cs @@ -0,0 +1,10 @@ +using Robust.Shared.GameStates; + +namespace Content.Shared._Scp.Holding; + +[RegisterComponent, NetworkedComponent] +public sealed partial class ScpHoldRestrictedComponent : Component +{ + [DataField] + public ScpHoldStage Stage = ScpHoldStage.Full; +} diff --git a/Content.Shared/_Scp/Holding/ScpHoldStage.cs b/Content.Shared/_Scp/Holding/ScpHoldStage.cs new file mode 100644 index 00000000000..24c37d4be3b --- /dev/null +++ b/Content.Shared/_Scp/Holding/ScpHoldStage.cs @@ -0,0 +1,7 @@ +namespace Content.Shared._Scp.Holding; + +public enum ScpHoldStage +{ + Soft, + Full, +} diff --git a/Content.Shared/_Scp/Holding/SharedScpHoldingSystem.Actions.cs b/Content.Shared/_Scp/Holding/SharedScpHoldingSystem.Actions.cs index c4df06c7025..d54b7d0986d 100644 --- a/Content.Shared/_Scp/Holding/SharedScpHoldingSystem.Actions.cs +++ b/Content.Shared/_Scp/Holding/SharedScpHoldingSystem.Actions.cs @@ -40,9 +40,13 @@ public bool TryToggleHold(Entity holder, EntityUid target, boo return false; var holdable = _holdableQuery.Comp(target); - var held = EnsureHeldState(target, holdable); + var held = EnsureHeldState(target, holdable, out var heldCreated); AddHolderContribution(holder.Owner, held); SyncHeldState(held); + + if (heldCreated) + Dirty(held); + StartHoldCooldown(holder); return true; } @@ -54,7 +58,7 @@ public bool CanToggleHold( bool ignoreHandAvailability = false, bool checkAttempt = false) { - if (!Exists(target) || holder.Owner == target) + if (holder.Owner == target) return false; if (!CanStartHold(holder, quiet)) @@ -67,18 +71,42 @@ public bool CanToggleHold( return false; } - if (!_whitelist.CheckBoth(target, holder.Comp.HoldableBlacklist, holder.Comp.HoldableWhitelist) || - !_whitelist.CheckBoth(holder.Owner, holdable.HolderBlacklist, holdable.HolderWhitelist)) + if (!_whitelist.CheckBoth(target, holder.Comp.HoldableBlacklist, holder.Comp.HoldableWhitelist)) + { + if (!quiet) + PopupHolder(holder.Owner, "scp-hold-target-invalid", ("target", target)); + return false; + } + + if (!_whitelist.CheckBoth(holder.Owner, holdable.HolderBlacklist, holdable.HolderWhitelist)) + { + if (!quiet) + PopupHolder(holder.Owner, "scp-hold-target-invalid", ("target", target)); + return false; + } + + if (!_moverQuery.HasComp(holder.Owner)) + { + if (!quiet) + PopupHolder(holder.Owner, "scp-hold-target-invalid", ("target", target)); + return false; + } + + if (!_moverQuery.HasComp(target)) + { + if (!quiet) + PopupHolder(holder.Owner, "scp-hold-target-invalid", ("target", target)); + return false; + } + + if (!_physicsQuery.TryComp(target, out var targetPhysics)) { if (!quiet) PopupHolder(holder.Owner, "scp-hold-target-invalid", ("target", target)); return false; } - if (!_moverQuery.HasComp(holder.Owner) || - !_moverQuery.HasComp(target) || - !_physicsQuery.TryComp(target, out var targetPhysics) || - targetPhysics.BodyType == BodyType.Static) + if (targetPhysics.BodyType == BodyType.Static) { if (!quiet) PopupHolder(holder.Owner, "scp-hold-target-invalid", ("target", target)); @@ -111,7 +139,7 @@ public bool CanToggleHold( { range = held.HoldRange; - if (held.FullHold && held.Holders.Count >= held.RequiredHolderCount) + if (held.FullHold) { if (!quiet) PopupHolder(holder.Owner, "scp-hold-target-fully-held", ("target", target)); @@ -139,15 +167,23 @@ public bool TryBreakOut(Entity held, bool viaMovement) : TrySoftBreakOut(held, viaMovement); } + public bool TryForceBreakOut(Entity held, bool viaMovement = false, bool applyImmunity = false) + { + if (!Resolve(held, ref held.Comp, false)) + return false; + + BreakOut((held.Owner, held.Comp), viaMovement, applyImmunity); + return true; + } + public void RefreshHeldState(Entity held) { _alerts.ShowAlert(held.Owner, "ScpHoldGrabbed"); SyncHeldStatusEffect(held.Owner); SyncPlaceholderHands(held); _actionBlocker.UpdateCanMove(held.Owner); - - if (_net.IsClient) - _physics.UpdateIsPredicted(held.Owner); + EnsureCombatModeDisabled(held.Owner); + _physics.UpdateIsPredicted(held.Owner); } public void RefreshHolderState(Entity holder) @@ -164,8 +200,7 @@ private bool TrySoftBreakOut(Entity held, bool viaMovement) if (!viaMovement) PopupTarget(held.Owner, "scp-hold-breakout-start"); - RaiseBreakoutEvent(held, viaMovement, applyImmunity: false); - ClearHoldState(held, applyImmunity: false); + BreakOut(held, viaMovement, applyImmunity: false); return true; } @@ -240,8 +275,7 @@ private bool IsHoldCoolingDown(Entity holder, out TimeSpan rem private void StartHoldCooldown(Entity holder) { - holder.Comp.HoldAvailableAt = _timing.CurTime + holder.Comp.HoldActionCooldown; - Dirty(holder); + SetHoldAvailableAt(holder, _timing.CurTime + holder.Comp.HoldActionCooldown); } private void ApplyFullBreakoutHolderCooldown(EntityUid holderUid) @@ -253,8 +287,24 @@ private void ApplyFullBreakoutHolderCooldown(EntityUid holderUid) if (hold.HoldAvailableAt != null && hold.HoldAvailableAt.Value >= cooldownEnd) return; - hold.HoldAvailableAt = cooldownEnd; - Dirty(holderUid, hold); + SetHoldAvailableAt((holderUid, hold), cooldownEnd); + } + + private void SetHoldAvailableAt(Entity holder, TimeSpan? holdAvailableAt) + { + if (holder.Comp.HoldAvailableAt == holdAvailableAt) + return; + + var previousHoldAvailableAt = holder.Comp.HoldAvailableAt; + holder.Comp.HoldAvailableAt = holdAvailableAt; + + if (previousHoldAvailableAt == null || holdAvailableAt == null) + { + Dirty(holder); + return; + } + + DirtyHoldField(holder, nameof(ScpHoldComponent.HoldAvailableAt)); } private bool CanPassHoldAttempt(EntityUid holderUid, EntityUid targetUid) @@ -270,4 +320,10 @@ private void RaiseBreakoutEvent(Entity held, bool viaMovement, var ev = new ScpHoldBreakoutEvent(viaMovement, held.Comp.FullHold, applyImmunity); RaiseLocalEvent(held.Owner, ev); } + + private void BreakOut(Entity held, bool viaMovement, bool applyImmunity) + { + RaiseBreakoutEvent(held, viaMovement, applyImmunity); + ClearHoldState(held, applyImmunity); + } } diff --git a/Content.Shared/_Scp/Holding/SharedScpHoldingSystem.Drag.cs b/Content.Shared/_Scp/Holding/SharedScpHoldingSystem.Drag.cs index 411078acfd1..8bd3c0ac376 100644 --- a/Content.Shared/_Scp/Holding/SharedScpHoldingSystem.Drag.cs +++ b/Content.Shared/_Scp/Holding/SharedScpHoldingSystem.Drag.cs @@ -28,14 +28,20 @@ private void UpdateSoftDrag(Entity held, float maintenanceRang return; var primaryHolder = held.Comp.PrimaryHolder.Value; - if (!_holderQuery.TryComp(primaryHolder, out var holder) || - holder.Target != held.Owner || - !_container.IsInSameOrNoContainer(primaryHolder, held.Owner) || - !_interaction.InRangeUnobstructed(primaryHolder, held.Owner, maintenanceRange) || - !_physicsQuery.TryComp(held.Owner, out var heldPhysics)) - { + if (!_holderQuery.TryComp(primaryHolder, out var holder)) + return; + + if (holder.Target != held.Owner) + return; + + if (!_container.IsInSameOrNoContainer(primaryHolder, held.Owner)) + return; + + if (!_interaction.InRangeUnobstructed(primaryHolder, held.Owner, maintenanceRange)) + return; + + if (!_physicsQuery.TryComp(held.Owner, out var heldPhysics)) return; - } var holderCoords = _transform.GetMapCoordinates(primaryHolder); var heldCoords = _transform.GetMapCoordinates(held.Owner); diff --git a/Content.Shared/_Scp/Holding/SharedScpHoldingSystem.Events.cs b/Content.Shared/_Scp/Holding/SharedScpHoldingSystem.Events.cs index 955ca721bc3..943f16a0f66 100644 --- a/Content.Shared/_Scp/Holding/SharedScpHoldingSystem.Events.cs +++ b/Content.Shared/_Scp/Holding/SharedScpHoldingSystem.Events.cs @@ -1,6 +1,7 @@ +using Content.Shared.Actions.Events; +using Content.Shared.CombatMode; using Content.Shared.Hands; using Content.Shared.Hands.EntitySystems; -using Content.Shared.Interaction.Components; using Content.Shared.Movement.Events; using Content.Shared.Movement.Systems; using Content.Shared.Throwing; @@ -26,7 +27,9 @@ private void SubscribeHoldingEvents() SubscribeLocalEvent(OnHeldUpdateCanMove); SubscribeLocalEvent(OnHeldAttemptMobCollide); SubscribeLocalEvent(OnHeldAttemptMobTargetCollide); + SubscribeLocalEvent(OnHeldCombatModeChanged); SubscribeLocalEvent(OnHeldPreventCollide); + SubscribeLocalEvent(OnHoldRestrictedActionAttempt); SubscribeLocalEvent(OnHolderStartup); SubscribeLocalEvent(OnHolderShutdown); @@ -39,14 +42,14 @@ private void SubscribeHoldingEvents() private void OnHoldShutdown(Entity ent, ref ComponentShutdown args) { - if (_net.IsClient || - TerminatingOrDeleted(ent.Owner) || - !_holderQuery.TryComp(ent.Owner, out var holder) || - holder.Target == null || - TerminatingOrDeleted(holder.Target.Value)) - { + if (_net.IsClient) + return; + + if (!_holderQuery.TryComp(ent.Owner, out var holder)) + return; + + if (holder.Target == null) return; - } ReleaseHolderContribution(ent.Owner, holder.Target.Value, clearIfEmpty: true); } @@ -59,8 +62,8 @@ private void OnHeldStartup(Entity ent, ref ComponentStartup ar private void OnHeldShutdown(Entity ent, ref ComponentShutdown args) { _alerts.ClearAlert(ent.Owner, "ScpHoldGrabbed"); - _statusEffects.TryRemoveStatusEffect(ent.Owner, GrabbedStatusEffect); - DeleteHeldHandBlockers(ent.Owner); + _statusEffects.TryRemoveStatusEffect(ent, GrabbedStatusEffect); + DeleteHeldHandBlockers(ent); if (!_timing.ApplyingState) CancelBreakoutDoAfter(ent); @@ -69,15 +72,13 @@ private void OnHeldShutdown(Entity ent, ref ComponentShutdown { foreach (var holderUid in ent.Comp.Holders) { - if (!TerminatingOrDeleted(holderUid) && _holderQuery.HasComp(holderUid)) + if (_holderQuery.HasComp(holderUid)) RemComp(holderUid); } } _actionBlocker.UpdateCanMove(ent.Owner); - - if (_net.IsClient) - _physics.UpdateIsPredicted(ent.Owner); + _physics.UpdateIsPredicted(ent.Owner); } private void OnBreakoutAlert(Entity ent, ref ScpHoldBreakoutAlertEvent args) @@ -96,16 +97,14 @@ private void OnBreakoutDoAfter(Entity ent, ref ScpHoldBreakout if (args.Handled) return; - args.Handled = true; - if (args.Cancelled) { PopupTarget(ent.Owner, "scp-hold-breakout-interrupted"); return; } - RaiseBreakoutEvent(ent, args.ViaMovement, applyImmunity: true); - ClearHoldState(ent, applyImmunity: true); + BreakOut(ent, args.ViaMovement, applyImmunity: true); + args.Handled = true; } private void OnHeldMoveInput(Entity ent, ref MoveInputEvent args) @@ -178,12 +177,14 @@ private void OnHolderRefreshMoveSpeed(Entity ent, ref Refres private void OnHolderBeforeThrow(Entity ent, ref BeforeThrowEvent args) { - if (ent.Comp.Target == null || - !TryComp(args.ItemUid, out var blocker) || - blocker.Target != ent.Comp.Target.Value) - { + if (ent.Comp.Target == null) + return; + + if (!TryComp(args.ItemUid, out var blocker)) + return; + + if (blocker.Target != ent.Comp.Target.Value) return; - } ReleaseHolderContribution(ent.Owner, ent.Comp.Target.Value, clearIfEmpty: true); args.Cancelled = true; @@ -191,37 +192,42 @@ private void OnHolderBeforeThrow(Entity ent, ref BeforeThrow private void OnHolderHandsModified(Entity ent, ref DidEquipHandEvent args) { - if (ent.Comp.LifeStage > ComponentLifeStage.Running || - TerminatingOrDeleted(ent.Owner) || - ent.Comp.Target == null || - TerminatingOrDeleted(ent.Comp.Target.Value)) - { + if (ent.Comp.LifeStage > ComponentLifeStage.Running) + return; + + if (ent.Comp.Target == null) + return; + + if (!_heldQuery.HasComp(ent.Comp.Target.Value)) return; - } RefreshHolderState(ent); } private void OnHolderPreventCollide(Entity ent, ref PreventCollideEvent args) { - if (args.Cancelled || - ent.Comp.Target == null || - ent.Comp.Target != args.OtherEntity) - { + if (args.Cancelled) + return; + + if (ent.Comp.Target == null) + return; + + if (ent.Comp.Target != args.OtherEntity) return; - } args.Cancelled = true; } private void OnHolderBlockerDropped(Entity ent, ref GettingDroppedAttemptEvent args) { - if (!_holderQuery.TryComp(args.User, out var holder) || - holder.Target == null || - holder.Target != ent.Comp.Target) - { + if (!_holderQuery.TryComp(args.User, out var holder)) + return; + + if (holder.Target == null) + return; + + if (holder.Target != ent.Comp.Target) return; - } ReleaseHolderContribution(args.User, ent.Comp.Target, clearIfEmpty: true); } diff --git a/Content.Shared/_Scp/Holding/SharedScpHoldingSystem.Feedback.cs b/Content.Shared/_Scp/Holding/SharedScpHoldingSystem.Feedback.cs index 72a0edb0e6e..c8b825992c8 100644 --- a/Content.Shared/_Scp/Holding/SharedScpHoldingSystem.Feedback.cs +++ b/Content.Shared/_Scp/Holding/SharedScpHoldingSystem.Feedback.cs @@ -33,7 +33,7 @@ private void SetBreakoutDoAfterId(Entity held, ushort? breakou return; held.Comp.BreakoutDoAfterId = breakoutDoAfterId; - Dirty(held); + DirtyHeldField(held, nameof(ScpHeldComponent.BreakoutDoAfterId)); } private void ShowBreakoutAttemptFeedback(Entity held) @@ -43,19 +43,16 @@ private void ShowBreakoutAttemptFeedback(Entity held) foreach (var holderUid in held.Comp.Holders) { - if (TerminatingOrDeleted(holderUid) || !_holderQuery.TryComp(holderUid, out var holder) || holder.Target != held.Owner) + if (!_holderQuery.TryComp(holderUid, out var holder)) continue; - if (_net.IsClient) - PredictedSpawnAttachedTo(BreakoutAttemptEffect, holderUid.ToCoordinates()); - else - SpawnAttachedTo(BreakoutAttemptEffect, holderUid.ToCoordinates()); + if (holder.Target != held.Owner) + continue; + + SpawnBreakoutAttemptEffect(holderUid); } - if (_net.IsClient) - _audio.PlayPredicted(BreakoutAttemptSound, held.Owner, held.Owner); - else - _audio.PlayPvs(BreakoutAttemptSound, held.Owner); + PlayBreakoutAttemptSound(held.Owner); } private void PopupHolder(EntityUid holder, string key, params (string, object)[] args) @@ -73,4 +70,26 @@ private void PopupTarget(EntityUid target, string key, params (string, object)[] _popup.PopupEntity(Loc.GetString(key, args), target, target); } + + private void SpawnBreakoutAttemptEffect(EntityUid holderUid) + { + if (_net.IsClient) + { + PredictedSpawnAttachedTo(BreakoutAttemptEffect, holderUid.ToCoordinates()); + return; + } + + SpawnAttachedTo(BreakoutAttemptEffect, holderUid.ToCoordinates()); + } + + private void PlayBreakoutAttemptSound(EntityUid targetUid) + { + if (_net.IsClient) + { + _audio.PlayPredicted(BreakoutAttemptSound, targetUid, targetUid); + return; + } + + _audio.PlayPvs(BreakoutAttemptSound, targetUid); + } } diff --git a/Content.Shared/_Scp/Holding/SharedScpHoldingSystem.Hands.cs b/Content.Shared/_Scp/Holding/SharedScpHoldingSystem.Hands.cs index 7dcf3ee0972..ffa6c6a9fd7 100644 --- a/Content.Shared/_Scp/Holding/SharedScpHoldingSystem.Hands.cs +++ b/Content.Shared/_Scp/Holding/SharedScpHoldingSystem.Hands.cs @@ -61,13 +61,8 @@ private void SyncPlaceholderHands(Entity held) EnsureComp(virtualItem.Value); var blocker = EnsureComp(virtualItem.Value); - - if (blocker.Target != held.Owner || blocker.Holder != holderUid) - { - blocker.Target = held.Owner; - blocker.Holder = holderUid; - Dirty(virtualItem.Value, blocker); - } + blocker.Target = held.Owner; + blocker.Holder = holderUid; iconIndex++; } @@ -108,13 +103,18 @@ private void SyncHolderHandBlocker(Entity holder) { validBlocker = heldItem; RemComp(heldItem); - var blocker = EnsureComp(heldItem); + var existingBlockerCreated = !TryComp(heldItem, out var blocker); + blocker ??= EnsureComp(heldItem); var currentTarget = target!.Value; if (blocker.Target != currentTarget) { blocker.Target = currentTarget; - Dirty(heldItem, blocker); + DirtyHandBlockerField((heldItem, blocker), nameof(ScpHoldHandBlockerComponent.Target)); } + + if (existingBlockerCreated) + Dirty(heldItem, blocker); + continue; } } @@ -144,9 +144,13 @@ private void SyncHolderHandBlocker(Entity holder) if (!_virtualItem.TrySpawnVirtualItemInHand(holder.Comp.Target.Value, holder.Owner, out var spawnedVirtualItem, silent: true)) return; - var blockerComp = EnsureComp(spawnedVirtualItem.Value); + var blockerCreated = !TryComp(spawnedVirtualItem.Value, out var blockerComp); + blockerComp ??= EnsureComp(spawnedVirtualItem.Value); blockerComp.Target = holder.Comp.Target.Value; - Dirty(spawnedVirtualItem.Value, blockerComp); + DirtyHandBlockerField((spawnedVirtualItem.Value, blockerComp), nameof(ScpHoldHandBlockerComponent.Target)); + + if (blockerCreated) + Dirty(spawnedVirtualItem.Value, blockerComp); } private bool HasAvailableHolderHand(EntityUid holderUid) diff --git a/Content.Shared/_Scp/Holding/SharedScpHoldingSystem.Restrictions.cs b/Content.Shared/_Scp/Holding/SharedScpHoldingSystem.Restrictions.cs new file mode 100644 index 00000000000..1cf7448f519 --- /dev/null +++ b/Content.Shared/_Scp/Holding/SharedScpHoldingSystem.Restrictions.cs @@ -0,0 +1,53 @@ +using Content.Shared.Actions.Events; +using Content.Shared.CombatMode; + +namespace Content.Shared._Scp.Holding; + +public sealed partial class SharedScpHoldingSystem +{ + [Dependency] private readonly SharedCombatModeSystem _combatMode = default!; + + private void OnHoldRestrictedActionAttempt(Entity ent, ref ActionAttemptEvent args) + { + if (args.Cancelled || !IsHeldAtStage(args.User, ent.Comp.Stage)) + return; + + args.Cancelled = true; + _popup.PopupClient(Loc.GetString("scp-hold-action-restricted"), args.User, args.User); + } + + private void OnHeldCombatModeChanged(Entity ent, ref CombatModeChangedEvent args) + { + if (!args.IsInCombatMode) + return; + + EnsureCombatModeDisabled(ent.Owner); + } + + public bool IsHeldAtStage(EntityUid uid, ScpHoldStage stage) + { + return _heldQuery.TryComp(uid, out var held) && IsHeldAtStage((uid, held), stage); + } + + private static bool IsHeldAtStage(Entity held, ScpHoldStage stage) + { + return stage switch + { + ScpHoldStage.Soft => true, + ScpHoldStage.Full => held.Comp.FullHold, + _ => false, + }; + } + + private void EnsureCombatModeDisabled(EntityUid uid) + { + if (!IsHeldAtStage(uid, ScpHoldStage.Soft) || + !TryComp(uid, out var combatMode) || + !combatMode.IsInCombatMode) + { + return; + } + + _combatMode.SetInCombatMode(uid, false, combatMode); + } +} diff --git a/Content.Shared/_Scp/Holding/SharedScpHoldingSystem.State.cs b/Content.Shared/_Scp/Holding/SharedScpHoldingSystem.State.cs index 9b52d4248b1..2739efd338f 100644 --- a/Content.Shared/_Scp/Holding/SharedScpHoldingSystem.State.cs +++ b/Content.Shared/_Scp/Holding/SharedScpHoldingSystem.State.cs @@ -41,15 +41,8 @@ private void UpdateHeld(Entity held) foreach (var holderUid in held.Comp.Holders) { - if (!Exists(holderUid) || - !_holdQuery.HasComp(holderUid) || - !_holderQuery.TryComp(holderUid, out var holder) || - holder.Target != held.Owner || - !_container.IsInSameOrNoContainer(holderUid, held.Owner) || - !_interaction.InRangeUnobstructed(holderUid, held.Owner, maintenanceRange)) - { + if (ShouldReleaseHolder(holderUid, held.Owner, maintenanceRange)) _holdersToRemove.Add(holderUid); - } } foreach (var holderUid in _holdersToRemove) @@ -64,9 +57,9 @@ private void UpdateHeld(Entity held) SyncHeldState((held.Owner, refreshed)); } - private Entity EnsureHeldState(EntityUid target, ScpHoldableComponent config) + private Entity EnsureHeldState(EntityUid target, ScpHoldableComponent config, out bool created) { - var created = !_heldQuery.TryComp(target, out var held); + created = !_heldQuery.TryComp(target, out var held); held ??= EnsureComp(target); if (created) @@ -79,15 +72,19 @@ private Entity EnsureHeldState(EntityUid target, ScpHoldableCo private void AddHolderContribution(EntityUid holderUid, Entity held) { if (!held.Comp.Holders.Contains(holderUid)) + { held.Comp.Holders.Add(holderUid); + DirtyHeldField(held, nameof(ScpHeldComponent.Holders)); + } - var holder = EnsureComp(holderUid); - holder.Target = held.Owner; - holder.SlowdownEnabled = false; - holder.WalkModifier = held.Comp.WalkModifier; - holder.SprintModifier = held.Comp.SprintModifier; - Dirty(holderUid, holder); + var holderCreated = !_holderQuery.TryComp(holderUid, out var holder); + holder ??= EnsureComp(holderUid); + SetHolderTarget((holderUid, holder), held.Owner); + SetHolderSlowdown((holderUid, holder), false, held.Comp.WalkModifier, held.Comp.SprintModifier); RefreshHolderState((holderUid, holder)); + + if (holderCreated) + Dirty(holderUid, holder); } private void ReleaseHolderContribution(EntityUid holderUid, EntityUid targetUid, bool clearIfEmpty) @@ -95,17 +92,24 @@ private void ReleaseHolderContribution(EntityUid holderUid, EntityUid targetUid, if (!_heldQuery.TryComp(targetUid, out var held)) return; + var removed = false; for (var i = held.Holders.Count - 1; i >= 0; i--) { if (held.Holders[i] == holderUid) + { held.Holders.RemoveAt(i); + removed = true; + } } + if (removed) + DirtyHeldField(targetUid, held, nameof(ScpHeldComponent.Holders)); + if (_holderQuery.HasComp(holderUid)) RemComp(holderUid); if (held.PrimaryHolder == holderUid) - held.PrimaryHolder = null; + SetHeldPrimaryHolder((targetUid, held), null); if (held.Holders.Count == 0) { @@ -149,7 +153,6 @@ private void SyncHeldState(Entity held) UpdateSoftDrag(held, maintenanceRange, desiredSoftDragDistance); UpdateHolderSlowdowns(held); SyncPlaceholderHands(held); - Dirty(held); } private void EnterFullHold(Entity held) @@ -158,13 +161,16 @@ private void EnterFullHold(Entity held) { held.Comp.FullHold = true; held.Comp.FullHoldStartedAt = _timing.CurTime; + DirtyHeldFields( + held, + nameof(ScpHeldComponent.FullHold), + nameof(ScpHeldComponent.FullHoldStartedAt)); } UpdateHolderSlowdowns(held); SyncPlaceholderHands(held); ZeroHeldVelocity(held.Owner); _actionBlocker.UpdateCanMove(held.Owner); - Dirty(held); } private void ExitFullHold(Entity held) @@ -176,32 +182,30 @@ private void ExitFullHold(Entity held) held.Comp.FullHold = false; held.Comp.FullHoldStartedAt = null; + DirtyHeldFields( + held, + nameof(ScpHeldComponent.FullHold), + nameof(ScpHeldComponent.FullHoldStartedAt)); SyncPlaceholderHands(held); _actionBlocker.UpdateCanMove(held.Owner); - Dirty(held); } private bool EnsurePrimaryHolder(Entity held) { - if (held.Comp.PrimaryHolder != null && - _holderQuery.TryComp(held.Comp.PrimaryHolder.Value, out var activeHolder) && - activeHolder.Target == held.Owner && - held.Comp.Holders.Contains(held.Comp.PrimaryHolder.Value)) - { + if (held.Comp.PrimaryHolder != null && IsValidPrimaryHolder(held, held.Comp.PrimaryHolder.Value)) return true; - } - held.Comp.PrimaryHolder = null; + SetHeldPrimaryHolder(held, null); foreach (var holderUid in held.Comp.Holders) { - if (!_holderQuery.TryComp(holderUid, out var holder) || - holder.Target != held.Owner) - { + if (!_holderQuery.TryComp(holderUid, out var holder)) + continue; + + if (holder.Target != held.Owner) continue; - } - held.Comp.PrimaryHolder = holderUid; + SetHeldPrimaryHolder(held, holderUid); return true; } @@ -232,9 +236,13 @@ private void ClearHoldState(Entity held, bool applyImmunity) if (applyImmunity) { - var immune = EnsureComp(held.Owner); + var immuneCreated = !TryComp(held.Owner, out var immune); + immune ??= EnsureComp(held.Owner); immune.ExpiresAt = _timing.CurTime + held.Comp.PostBreakoutImmunity; - Dirty(held.Owner, immune); + DirtyImmuneField((held.Owner, immune), nameof(ScpHoldImmuneComponent.ExpiresAt)); + + if (immuneCreated) + Dirty(held.Owner, immune); } foreach (var holderUid in _holderCooldownsToApply) @@ -265,10 +273,24 @@ private void SetHolderSlowdown(Entity holder, bool enabled, return; } - holder.Comp.SlowdownEnabled = enabled; - holder.Comp.WalkModifier = walkModifier; - holder.Comp.SprintModifier = sprintModifier; - Dirty(holder); + if (holder.Comp.SlowdownEnabled != enabled) + { + holder.Comp.SlowdownEnabled = enabled; + DirtyHolderField(holder, nameof(ScpHolderComponent.SlowdownEnabled)); + } + + if (!MathHelper.CloseTo(holder.Comp.WalkModifier, walkModifier)) + { + holder.Comp.WalkModifier = walkModifier; + DirtyHolderField(holder, nameof(ScpHolderComponent.WalkModifier)); + } + + if (!MathHelper.CloseTo(holder.Comp.SprintModifier, sprintModifier)) + { + holder.Comp.SprintModifier = sprintModifier; + DirtyHolderField(holder, nameof(ScpHolderComponent.SprintModifier)); + } + _movement.RefreshMovementSpeedModifiers(holder.Owner); } @@ -301,4 +323,50 @@ private void CopyConfig(ScpHoldableComponent source, ScpHeldComponent target) target.SoftEscapeAvailableAt = _timing.CurTime; target.FullHoldStartedAt = null; } + + private bool ShouldReleaseHolder(EntityUid holderUid, EntityUid heldUid, float maintenanceRange) + { + if (!_holdQuery.HasComp(holderUid)) + return true; + + if (!_holderQuery.TryComp(holderUid, out var holder)) + return true; + + if (holder.Target != heldUid) + return true; + + if (!_container.IsInSameOrNoContainer(holderUid, heldUid)) + return true; + + return !_interaction.InRangeUnobstructed(holderUid, heldUid, maintenanceRange); + } + + private bool IsValidPrimaryHolder(Entity held, EntityUid primaryHolderUid) + { + if (!_holderQuery.TryComp(primaryHolderUid, out var holder)) + return false; + + if (holder.Target != held.Owner) + return false; + + return held.Comp.Holders.Contains(primaryHolderUid); + } + + private void SetHolderTarget(Entity holder, EntityUid? target) + { + if (holder.Comp.Target == target) + return; + + holder.Comp.Target = target; + DirtyHolderField(holder, nameof(ScpHolderComponent.Target)); + } + + private void SetHeldPrimaryHolder(Entity held, EntityUid? primaryHolder) + { + if (held.Comp.PrimaryHolder == primaryHolder) + return; + + held.Comp.PrimaryHolder = primaryHolder; + DirtyHeldField(held, nameof(ScpHeldComponent.PrimaryHolder)); + } } diff --git a/Content.Shared/_Scp/Holding/SharedScpHoldingSystem.cs b/Content.Shared/_Scp/Holding/SharedScpHoldingSystem.cs index 7ad43a37948..5473f688efc 100644 --- a/Content.Shared/_Scp/Holding/SharedScpHoldingSystem.cs +++ b/Content.Shared/_Scp/Holding/SharedScpHoldingSystem.cs @@ -64,13 +64,56 @@ public override void Update(float frameTime) var heldQuery = EntityQueryEnumerator(); while (heldQuery.MoveNext(out var uid, out var held)) { - if (_net.IsClient && - (!_physicsQuery.TryComp(uid, out var physics) || !physics.Predict)) - { + if (ShouldSkipHeldUpdate(uid)) continue; - } UpdateHeld((uid, held)); } } + + private bool ShouldSkipHeldUpdate(EntityUid uid) + { + if (!_net.IsClient) + return false; + + if (!_physicsQuery.TryComp(uid, out var physics)) + return true; + + return !physics.Predict; + } + + private void DirtyHoldField(Entity holder, string fieldName) + { + DirtyField(holder.AsNullable(), fieldName); + } + + private void DirtyHeldField(Entity held, string fieldName) + { + Dirty(held); + } + + private void DirtyHeldField(EntityUid uid, ScpHeldComponent held, string fieldName) + { + Dirty(uid, held); + } + + private void DirtyHeldFields(Entity held, params string[] fieldNames) + { + Dirty(held); + } + + private void DirtyHolderField(Entity holder, string fieldName) + { + Dirty(holder); + } + + private void DirtyImmuneField(Entity immune, string fieldName) + { + Dirty(immune); + } + + private void DirtyHandBlockerField(Entity blocker, string fieldName) + { + Dirty(blocker); + } } diff --git a/Content.Shared/_Scp/Scp096/Main/Components/Scp096Component.cs b/Content.Shared/_Scp/Scp096/Main/Components/Scp096Component.cs index 4c4d65bd397..c188d0df355 100644 --- a/Content.Shared/_Scp/Scp096/Main/Components/Scp096Component.cs +++ b/Content.Shared/_Scp/Scp096/Main/Components/Scp096Component.cs @@ -138,6 +138,34 @@ public sealed partial class Scp096Component : Component #endregion + #region Hold breakout + + /// + /// Урон, который получает каждый удерживающий скромника при успешном вырывании или смене состояния. + /// + [DataField] + public DamageSpecifier HoldBreakoutDamage = new() + { + DamageDict = new() + { + { "Blunt", 20 }, + }, + }; + + /// + /// Время паралича удерживающих после вырывания. + /// + [DataField] + public TimeSpan HoldBreakoutParalyzeTime = TimeSpan.FromSeconds(5f); + + /// + /// Сила отталкивания удерживающих после вырывания. + /// + [DataField] + public float HoldBreakoutImpulse = 10f; + + #endregion + #region Face skin rip /// diff --git a/Content.Shared/_Scp/Scp096/Main/Systems/SharedScp096System.Holding.cs b/Content.Shared/_Scp/Scp096/Main/Systems/SharedScp096System.Holding.cs new file mode 100644 index 00000000000..334ece1a628 --- /dev/null +++ b/Content.Shared/_Scp/Scp096/Main/Systems/SharedScp096System.Holding.cs @@ -0,0 +1,68 @@ +using System.Numerics; +using Content.Shared._Scp.Holding; +using Content.Shared._Scp.Scp096.Main.Components; +using Robust.Shared.Physics.Systems; + +namespace Content.Shared._Scp.Scp096.Main.Systems; + +public abstract partial class SharedScp096System +{ + [Dependency] private readonly SharedScpHoldingSystem _holding = default!; + [Dependency] private readonly SharedPhysicsSystem _physics = default!; + [Dependency] private readonly SharedTransformSystem _transform = default!; + + private void InitializeHolding() + { + SubscribeLocalEvent(OnHoldAttempt); + SubscribeLocalEvent(OnHoldBreakout); + } + + private void OnHoldAttempt(Entity ent, ref ScpHoldAttemptEvent args) + { + if (IsInHoldRestrictedState(ent.Owner)) + args.Cancel(); + } + + private void OnHoldBreakout(Entity ent, ref ScpHoldBreakoutEvent args) + { + if (_timing.ApplyingState) + return; + + if (!args.WasFullHold && !IsInHoldRestrictedState(ent.Owner)) + return; + + if (!TryComp(ent.Owner, out var held)) + return; + + var scpPosition = _transform.GetWorldPosition(ent.Owner); + foreach (var holderUid in held.Holders) + { + ApplyHoldBreakoutEffects(ent, holderUid, scpPosition); + } + } + + protected bool IsInHoldRestrictedState(EntityUid uid) + { + return HasComp(uid) + || HasComp(uid) + || HasComp(uid); + } + + protected void TryBreakOutOfHold(EntityUid uid) + { + _holding.TryForceBreakOut((uid, (ScpHeldComponent?) null)); + } + + private void ApplyHoldBreakoutEffects(Entity ent, EntityUid holderUid, Vector2 scpPosition) + { + _damageable.TryChangeDamage(holderUid, ent.Comp.HoldBreakoutDamage, origin: ent.Owner); + _stun.TryUpdateParalyzeDuration(holderUid, ent.Comp.HoldBreakoutParalyzeTime); + + var direction = _transform.GetWorldPosition(holderUid) - scpPosition; + direction = direction.LengthSquared() < 0.001f + ? Vector2.UnitY + : Vector2.Normalize(direction); + + _physics.ApplyLinearImpulse(holderUid, direction * ent.Comp.HoldBreakoutImpulse); + } +} diff --git a/Content.Shared/_Scp/Scp096/Main/Systems/SharedScp096System.Rage.cs b/Content.Shared/_Scp/Scp096/Main/Systems/SharedScp096System.Rage.cs index 259cdc3714c..92a186ab269 100644 --- a/Content.Shared/_Scp/Scp096/Main/Systems/SharedScp096System.Rage.cs +++ b/Content.Shared/_Scp/Scp096/Main/Systems/SharedScp096System.Rage.cs @@ -37,6 +37,8 @@ private void InitializeRage() protected virtual void OnHeatingUpStart(Entity ent, ref ComponentStartup args) { + TryBreakOutOfHold(ent.Owner); + // Устанавливаем время окончания пред-агр состояния ent.Comp.RageHeatUpEnd = _timing.CurTime + ent.Comp.RageHeatUp; @@ -79,6 +81,8 @@ protected virtual void OnHeatingUpShutdown(Entity ent, ref ComponentStartup args) { + TryBreakOutOfHold(ent.Owner); + ent.Comp.RageStartTime = _timing.CurTime; Dirty(ent); diff --git a/Content.Shared/_Scp/Scp096/Main/Systems/SharedScp096System.WithoutFace.cs b/Content.Shared/_Scp/Scp096/Main/Systems/SharedScp096System.WithoutFace.cs index 8a983f05ca5..834fb616a2f 100644 --- a/Content.Shared/_Scp/Scp096/Main/Systems/SharedScp096System.WithoutFace.cs +++ b/Content.Shared/_Scp/Scp096/Main/Systems/SharedScp096System.WithoutFace.cs @@ -29,6 +29,8 @@ private void InitializeWithoutFace() /// private void OnWithoutFaceStartup(Entity ent, ref ComponentStartup args) { + TryBreakOutOfHold(ent.Owner); + var message = Loc.GetString("scp096-face-skin-rip-full", ("name", Identity.Name(ent, EntityManager))); _popup.PopupPredicted(message, ent, ent); diff --git a/Content.Shared/_Scp/Scp096/Main/Systems/SharedScp096System.cs b/Content.Shared/_Scp/Scp096/Main/Systems/SharedScp096System.cs index 25535ee229e..4bbf164092a 100644 --- a/Content.Shared/_Scp/Scp096/Main/Systems/SharedScp096System.cs +++ b/Content.Shared/_Scp/Scp096/Main/Systems/SharedScp096System.cs @@ -99,6 +99,7 @@ public override void Initialize() InitializeTargets(); InitializeHands(); InitializeActions(); + InitializeHolding(); InitializeWithoutFace(); InitializeAppearance(); InitializeFace(); diff --git a/Resources/Locale/en-US/_strings/_scp/holding/holding.ftl b/Resources/Locale/en-US/_strings/_scp/holding/holding.ftl index 137966b2281..eb92da2b002 100644 --- a/Resources/Locale/en-US/_strings/_scp/holding/holding.ftl +++ b/Resources/Locale/en-US/_strings/_scp/holding/holding.ftl @@ -9,5 +9,6 @@ scp-hold-holder-action-on-cooldown = You can grab again in {$seconds} s. scp-hold-breakout-too-early = You can try to break free in {$seconds} s. scp-hold-breakout-start = You start trying to break free. scp-hold-breakout-interrupted = Your breakout attempt was interrupted. +scp-hold-action-restricted = You cannot do that while being held. alerts-scp-held-name = Forcefully restrained alerts-scp-held-desc = Someone is physically gripping you. Move or click this alert to try to break free. In a hard restraint you must endure the hold before the breakout starts. diff --git a/Resources/Locale/ru-RU/_strings/_scp/holding/holding.ftl b/Resources/Locale/ru-RU/_strings/_scp/holding/holding.ftl index b1970366ec7..419c6565090 100644 --- a/Resources/Locale/ru-RU/_strings/_scp/holding/holding.ftl +++ b/Resources/Locale/ru-RU/_strings/_scp/holding/holding.ftl @@ -9,5 +9,6 @@ scp-hold-holder-action-on-cooldown = Схватить снова можно че scp-hold-breakout-too-early = Попытаться вырваться можно через {$seconds} с. scp-hold-breakout-start = Вы начинаете вырываться. scp-hold-breakout-interrupted = Попытка вырваться была прервана. +scp-hold-action-restricted = Вы не можете сделать это, пока вас удерживают. alerts-scp-held-name = Вас удерживают силой alerts-scp-held-desc = Кто-то физически держит вас. Двигайтесь или нажмите на этот статус-эффект, чтобы попытаться вырваться. В полном удержании сначала нужно выдержать захват. diff --git a/Resources/Prototypes/Actions/types.yml b/Resources/Prototypes/Actions/types.yml index fabea2350e3..89241e15629 100644 --- a/Resources/Prototypes/Actions/types.yml +++ b/Resources/Prototypes/Actions/types.yml @@ -261,6 +261,8 @@ priority: -100 - type: InstantAction event: !type:ToggleCombatActionEvent + - type: ScpHoldRestricted # Fire edit - block combat mode while held + stage: Soft - type: entity parent: ActionCombatModeToggle diff --git a/Resources/Prototypes/_Scp/Actions/scp096.yml b/Resources/Prototypes/_Scp/Actions/scp096.yml index ffd94dcacbf..5d7a61cd7ab 100644 --- a/Resources/Prototypes/_Scp/Actions/scp096.yml +++ b/Resources/Prototypes/_Scp/Actions/scp096.yml @@ -11,6 +11,8 @@ state: scream - type: InstantAction event: !type:Scp096CryOutEvent + - type: ScpHoldRestricted + stage: Full - type: SafeTimeRestricted - type: entity @@ -26,6 +28,8 @@ state: rip - type: InstantAction event: !type:Scp096FaceSkinRipEvent + - type: ScpHoldRestricted + stage: Full - type: SafeTimeRestricted - type: entity @@ -42,3 +46,5 @@ state: sit - type: InstantAction event: !type:Scp096SitDownEvent + - type: ScpHoldRestricted + stage: Full diff --git a/Resources/Prototypes/_Scp/Roles/Jobs/Administration/security_commander.yml b/Resources/Prototypes/_Scp/Roles/Jobs/Administration/security_commander.yml index bcea05af290..9e80eb00110 100644 --- a/Resources/Prototypes/_Scp/Roles/Jobs/Administration/security_commander.yml +++ b/Resources/Prototypes/_Scp/Roles/Jobs/Administration/security_commander.yml @@ -33,6 +33,10 @@ class: B - type: AccessLevel level: Four + - type: ScpHold + holdableWhitelist: + components: + - ClassDAppearance - type: ScpAnnounceOnSpawn text: head-announce-on-spawn channels: diff --git a/Resources/Prototypes/_Scp/Roles/Jobs/SpecialPurposeSquad/heavy_containment_zone_commandant.yml b/Resources/Prototypes/_Scp/Roles/Jobs/SpecialPurposeSquad/heavy_containment_zone_commandant.yml index 8daa40d8746..676697729c5 100644 --- a/Resources/Prototypes/_Scp/Roles/Jobs/SpecialPurposeSquad/heavy_containment_zone_commandant.yml +++ b/Resources/Prototypes/_Scp/Roles/Jobs/SpecialPurposeSquad/heavy_containment_zone_commandant.yml @@ -32,6 +32,7 @@ holdableWhitelist: components: - Scp096 + - ClassDAppearance holdableBlacklist: components: - ActiveScp096Rage diff --git a/Resources/Prototypes/_Scp/Roles/Jobs/SpecialPurposeSquad/heavy_containment_zone_officer.yml b/Resources/Prototypes/_Scp/Roles/Jobs/SpecialPurposeSquad/heavy_containment_zone_officer.yml index 712b92f6b23..5c988ff75ce 100644 --- a/Resources/Prototypes/_Scp/Roles/Jobs/SpecialPurposeSquad/heavy_containment_zone_officer.yml +++ b/Resources/Prototypes/_Scp/Roles/Jobs/SpecialPurposeSquad/heavy_containment_zone_officer.yml @@ -32,6 +32,7 @@ holdableWhitelist: components: - Scp096 + - ClassDAppearance holdableBlacklist: components: - ActiveScp096Rage diff --git a/Resources/Prototypes/_Scp/Roles/Jobs/SpecialPurposeSquad/junior_heavy_containment_zone_officer.yml b/Resources/Prototypes/_Scp/Roles/Jobs/SpecialPurposeSquad/junior_heavy_containment_zone_officer.yml index ebc833989d0..cf3914e9b3d 100644 --- a/Resources/Prototypes/_Scp/Roles/Jobs/SpecialPurposeSquad/junior_heavy_containment_zone_officer.yml +++ b/Resources/Prototypes/_Scp/Roles/Jobs/SpecialPurposeSquad/junior_heavy_containment_zone_officer.yml @@ -31,6 +31,7 @@ holdableWhitelist: components: - Scp096 + - ClassDAppearance holdableBlacklist: components: - ActiveScp096Rage diff --git a/Resources/Prototypes/_Scp/Roles/Jobs/SpecialPurposeSquad/senior_heavy_containment_zone_officer.yml b/Resources/Prototypes/_Scp/Roles/Jobs/SpecialPurposeSquad/senior_heavy_containment_zone_officer.yml index 4b90ab6e995..82e0a99ca57 100644 --- a/Resources/Prototypes/_Scp/Roles/Jobs/SpecialPurposeSquad/senior_heavy_containment_zone_officer.yml +++ b/Resources/Prototypes/_Scp/Roles/Jobs/SpecialPurposeSquad/senior_heavy_containment_zone_officer.yml @@ -32,6 +32,7 @@ holdableWhitelist: components: - Scp096 + - ClassDAppearance holdableBlacklist: components: - ActiveScp096Rage From 2c7b464c3a19f2d7029736f9d14cb9057fdcaf0d Mon Sep 17 00:00:00 2001 From: drdth Date: Thu, 16 Apr 2026 20:37:00 +0300 Subject: [PATCH 08/27] refactor: little clean up --- .../Holding/ScpHoldingPredictionSystem.cs | 23 +++++++++---------- .../_Scp/Holding/ScpHoldingSystem.cs | 8 +++++++ .../Holding/SharedScpHoldingSystem.Actions.cs | 4 ++-- .../Holding/SharedScpHoldingSystem.Drag.cs | 3 ++- .../Holding/SharedScpHoldingSystem.Events.cs | 3 ++- .../SharedScpHoldingSystem.Feedback.cs | 3 ++- .../Holding/SharedScpHoldingSystem.Hands.cs | 3 ++- .../SharedScpHoldingSystem.Restrictions.cs | 2 +- .../Holding/SharedScpHoldingSystem.State.cs | 3 ++- .../_Scp/Holding/SharedScpHoldingSystem.cs | 13 +++++++---- 10 files changed, 40 insertions(+), 25 deletions(-) create mode 100644 Content.Server/_Scp/Holding/ScpHoldingSystem.cs diff --git a/Content.Client/_Scp/Holding/ScpHoldingPredictionSystem.cs b/Content.Client/_Scp/Holding/ScpHoldingPredictionSystem.cs index a5fc75f719d..8855ff4fe25 100644 --- a/Content.Client/_Scp/Holding/ScpHoldingPredictionSystem.cs +++ b/Content.Client/_Scp/Holding/ScpHoldingPredictionSystem.cs @@ -1,8 +1,8 @@ -using System; +using Content.Client.Hands.Systems; +using Content.Client.Inventory; 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; @@ -10,16 +10,15 @@ namespace Content.Client._Scp.Holding; -public sealed class ScpHoldingPredictionSystem : EntitySystem +public sealed class ScpHoldingSystem : SharedScpHoldingSystem { - [Dependency] private readonly SharedScpHoldingSystem _holding = default!; - [Dependency] private readonly SharedHandsSystem _hands = default!; + [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!; - [Dependency] private readonly SharedVirtualItemSystem _virtualItem = default!; - private static readonly TimeSpan BlockerRespawnSuppressionDuration = TimeSpan.FromSeconds(0.5); + private static readonly TimeSpan BlockerRespawnSuppressionDuration = TimeSpan.FromSeconds(0.5f); private EntityUid? _suppressedHolder; private EntityUid? _suppressedTarget; @@ -72,12 +71,12 @@ public override void Update(float frameTime) return; } - _holding.RefreshHolderState((local, localHolder)); + RefreshHolderState((local, localHolder)); } private void OnHeldAfterState(Entity ent, ref AfterAutoHandleStateEvent args) { - _holding.RefreshHeldState(ent); + RefreshHeldState(ent); } private void OnHolderAfterState(Entity ent, ref AfterAutoHandleStateEvent args) @@ -88,7 +87,7 @@ private void OnHolderAfterState(Entity ent, ref AfterAutoHan return; } - _holding.RefreshHolderState(ent); + RefreshHolderState(ent); } private void OnBlockerUnequipped(Entity ent, ref GotUnequippedHandEvent args) @@ -125,9 +124,9 @@ private void OnUpdateHeldPredicted(Entity ent, ref UpdateIsPre } } - for (var i = 0; i < ent.Comp.Holders.Count; i++) + foreach (var holder in ent.Comp.Holders) { - if (ent.Comp.Holders[i] != local) + if (holder != local) continue; args.IsPredicted = true; diff --git a/Content.Server/_Scp/Holding/ScpHoldingSystem.cs b/Content.Server/_Scp/Holding/ScpHoldingSystem.cs new file mode 100644 index 00000000000..fe044ae038c --- /dev/null +++ b/Content.Server/_Scp/Holding/ScpHoldingSystem.cs @@ -0,0 +1,8 @@ +using Content.Shared._Scp.Holding; + +namespace Content.Server._Scp.Holding; + +public abstract class ScpHoldingSystem : SharedScpHoldingSystem +{ + +} diff --git a/Content.Shared/_Scp/Holding/SharedScpHoldingSystem.Actions.cs b/Content.Shared/_Scp/Holding/SharedScpHoldingSystem.Actions.cs index d54b7d0986d..b9797c43225 100644 --- a/Content.Shared/_Scp/Holding/SharedScpHoldingSystem.Actions.cs +++ b/Content.Shared/_Scp/Holding/SharedScpHoldingSystem.Actions.cs @@ -1,15 +1,15 @@ -using System; using Content.Shared.DoAfter; using Content.Shared.Movement.Components; using Robust.Shared.Physics; namespace Content.Shared._Scp.Holding; -public sealed partial class SharedScpHoldingSystem +public abstract partial class SharedScpHoldingSystem { /* * Hold-local query caches, hold toggling API, breakout flow, and cooldown helpers. */ + private EntityQuery _moverQuery; private EntityQuery _holdableQuery; diff --git a/Content.Shared/_Scp/Holding/SharedScpHoldingSystem.Drag.cs b/Content.Shared/_Scp/Holding/SharedScpHoldingSystem.Drag.cs index 8bd3c0ac376..958e894dc1f 100644 --- a/Content.Shared/_Scp/Holding/SharedScpHoldingSystem.Drag.cs +++ b/Content.Shared/_Scp/Holding/SharedScpHoldingSystem.Drag.cs @@ -4,11 +4,12 @@ namespace Content.Shared._Scp.Holding; -public sealed partial class SharedScpHoldingSystem +public abstract partial class SharedScpHoldingSystem { /* * Drag-local dependencies, soft-drag movement, and helper calculations. */ + [Dependency] private readonly SharedTransformSystem _transform = default!; private const float SoftDragDistanceFactor = 0.3f; diff --git a/Content.Shared/_Scp/Holding/SharedScpHoldingSystem.Events.cs b/Content.Shared/_Scp/Holding/SharedScpHoldingSystem.Events.cs index 943f16a0f66..e7e1978ca6c 100644 --- a/Content.Shared/_Scp/Holding/SharedScpHoldingSystem.Events.cs +++ b/Content.Shared/_Scp/Holding/SharedScpHoldingSystem.Events.cs @@ -9,11 +9,12 @@ namespace Content.Shared._Scp.Holding; -public sealed partial class SharedScpHoldingSystem +public abstract partial class SharedScpHoldingSystem { /* * Event subscription wiring plus routing/lifecycle reactions for held and holder entities. */ + private void SubscribeHoldingEvents() { SubscribeLocalEvent(OnHoldShutdown); diff --git a/Content.Shared/_Scp/Holding/SharedScpHoldingSystem.Feedback.cs b/Content.Shared/_Scp/Holding/SharedScpHoldingSystem.Feedback.cs index c8b825992c8..ce0d91c147f 100644 --- a/Content.Shared/_Scp/Holding/SharedScpHoldingSystem.Feedback.cs +++ b/Content.Shared/_Scp/Holding/SharedScpHoldingSystem.Feedback.cs @@ -5,11 +5,12 @@ namespace Content.Shared._Scp.Holding; -public sealed partial class SharedScpHoldingSystem +public abstract partial class SharedScpHoldingSystem { /* * Feedback-local dependencies, breakout do-after tracking, and popup/audio helpers. */ + [Dependency] private readonly SharedAudioSystem _audio = default!; [Dependency] private readonly SharedPopupSystem _popup = default!; diff --git a/Content.Shared/_Scp/Holding/SharedScpHoldingSystem.Hands.cs b/Content.Shared/_Scp/Holding/SharedScpHoldingSystem.Hands.cs index ffa6c6a9fd7..04dc2cc447b 100644 --- a/Content.Shared/_Scp/Holding/SharedScpHoldingSystem.Hands.cs +++ b/Content.Shared/_Scp/Holding/SharedScpHoldingSystem.Hands.cs @@ -6,11 +6,12 @@ namespace Content.Shared._Scp.Holding; -public sealed partial class SharedScpHoldingSystem +public abstract partial class SharedScpHoldingSystem { /* * Hand-local dependencies, caches, placeholders, virtual blockers, and held-status visuals. */ + [Dependency] private readonly SharedHandsSystem _hands = default!; [Dependency] private readonly SharedVirtualItemSystem _virtualItem = default!; diff --git a/Content.Shared/_Scp/Holding/SharedScpHoldingSystem.Restrictions.cs b/Content.Shared/_Scp/Holding/SharedScpHoldingSystem.Restrictions.cs index 1cf7448f519..c792229d10c 100644 --- a/Content.Shared/_Scp/Holding/SharedScpHoldingSystem.Restrictions.cs +++ b/Content.Shared/_Scp/Holding/SharedScpHoldingSystem.Restrictions.cs @@ -3,7 +3,7 @@ namespace Content.Shared._Scp.Holding; -public sealed partial class SharedScpHoldingSystem +public abstract partial class SharedScpHoldingSystem { [Dependency] private readonly SharedCombatModeSystem _combatMode = default!; diff --git a/Content.Shared/_Scp/Holding/SharedScpHoldingSystem.State.cs b/Content.Shared/_Scp/Holding/SharedScpHoldingSystem.State.cs index 2739efd338f..c72f9438e40 100644 --- a/Content.Shared/_Scp/Holding/SharedScpHoldingSystem.State.cs +++ b/Content.Shared/_Scp/Holding/SharedScpHoldingSystem.State.cs @@ -4,11 +4,12 @@ namespace Content.Shared._Scp.Holding; -public sealed partial class SharedScpHoldingSystem +public abstract partial class SharedScpHoldingSystem { /* * State-local dependencies, caches, held lifecycle, holder membership, and full-hold transitions. */ + [Dependency] private readonly SharedBodySystem _body = default!; private readonly List _holdersToRemove = []; diff --git a/Content.Shared/_Scp/Holding/SharedScpHoldingSystem.cs b/Content.Shared/_Scp/Holding/SharedScpHoldingSystem.cs index 5473f688efc..68a69f5b9b7 100644 --- a/Content.Shared/_Scp/Holding/SharedScpHoldingSystem.cs +++ b/Content.Shared/_Scp/Holding/SharedScpHoldingSystem.cs @@ -9,28 +9,30 @@ using Robust.Shared.Network; using Robust.Shared.Physics.Components; using Robust.Shared.Physics.Systems; +using Robust.Shared.Prototypes; using Robust.Shared.Timing; namespace Content.Shared._Scp.Holding; -public sealed partial class SharedScpHoldingSystem : EntitySystem +public abstract partial class SharedScpHoldingSystem : EntitySystem { /* * Core lifecycle, dependencies, constants, and runtime caches. */ + [Dependency] private readonly ActionBlockerSystem _actionBlocker = default!; [Dependency] private readonly AlertsSystem _alerts = default!; [Dependency] private readonly SharedContainerSystem _container = default!; [Dependency] private readonly SharedDoAfterSystem _doAfter = default!; [Dependency] private readonly SharedInteractionSystem _interaction = default!; [Dependency] private readonly MovementSpeedModifierSystem _movement = default!; - [Dependency] private readonly INetManager _net = default!; [Dependency] private readonly SharedPhysicsSystem _physics = default!; [Dependency] private readonly StatusEffectsSystem _statusEffects = default!; - [Dependency] private readonly IGameTiming _timing = default!; [Dependency] private readonly EntityWhitelistSystem _whitelist = default!; + [Dependency] private readonly IGameTiming _timing = default!; + [Dependency] private readonly INetManager _net = default!; - private const string GrabbedStatusEffect = "StatusEffectScpHeld"; + private static readonly EntProtoId GrabbedStatusEffect = "StatusEffectScpHeld"; private EntityQuery _physicsQuery; private EntityQuery _heldQuery; private EntityQuery _holdQuery; @@ -44,6 +46,7 @@ public override void Initialize() _heldQuery = GetEntityQuery(); _holdQuery = GetEntityQuery(); _holderQuery = GetEntityQuery(); + InitializeHoldQueries(); InitializeHandQueries(); InitializeStateQueries(); @@ -58,7 +61,7 @@ public override void Update(float frameTime) while (immuneQuery.MoveNext(out var uid, out var immune)) { if (_timing.CurTime >= immune.ExpiresAt) - RemComp(uid); + RemCompDeferred(uid); } var heldQuery = EntityQueryEnumerator(); From 3af567bd5860049352f24754244f7a404647d342 Mon Sep 17 00:00:00 2001 From: drdth Date: Fri, 17 Apr 2026 02:31:29 +0300 Subject: [PATCH 09/27] refactor: oh my god so many refactors --- ...redictionSystem.cs => ScpHoldingSystem.cs} | 95 +++- .../Tests/_Scp/ScpHoldingTest.cs | 419 ++++++++++++++++-- .../_Scp/Holding/ScpHoldingSystem.cs | 38 +- .../Holding/ScpBreakoutAttemptComponent.cs | 10 + .../_Scp/Holding/ScpFullHeldComponent.cs | 18 + .../_Scp/Holding/ScpHeldComponent.cs | 35 +- .../Holding/ScpHeldHandBlockerComponent.cs | 4 +- .../Holding/ScpHoldHandBlockerComponent.cs | 4 +- .../_Scp/Holding/ScpHoldableComponent.cs | 19 +- .../_Scp/Holding/ScpHolderComponent.cs | 22 +- .../Holding/ScpHolderSlowdownComponent.cs | 23 + .../Holding/SharedScpHoldingSystem.Actions.cs | 41 +- .../SharedScpHoldingSystem.BreakoutAttempt.cs | 36 ++ .../Holding/SharedScpHoldingSystem.Events.cs | 134 +++--- .../SharedScpHoldingSystem.Feedback.cs | 67 ++- .../Holding/SharedScpHoldingSystem.Hands.cs | 110 ++++- .../SharedScpHoldingSystem.Restrictions.cs | 4 +- .../Holding/SharedScpHoldingSystem.State.cs | 116 ++--- .../_Scp/Holding/SharedScpHoldingSystem.cs | 55 +-- 19 files changed, 914 insertions(+), 336 deletions(-) rename Content.Client/_Scp/Holding/{ScpHoldingPredictionSystem.cs => ScpHoldingSystem.cs} (65%) create mode 100644 Content.Shared/_Scp/Holding/ScpBreakoutAttemptComponent.cs create mode 100644 Content.Shared/_Scp/Holding/ScpFullHeldComponent.cs create mode 100644 Content.Shared/_Scp/Holding/ScpHolderSlowdownComponent.cs create mode 100644 Content.Shared/_Scp/Holding/SharedScpHoldingSystem.BreakoutAttempt.cs diff --git a/Content.Client/_Scp/Holding/ScpHoldingPredictionSystem.cs b/Content.Client/_Scp/Holding/ScpHoldingSystem.cs similarity index 65% rename from Content.Client/_Scp/Holding/ScpHoldingPredictionSystem.cs rename to Content.Client/_Scp/Holding/ScpHoldingSystem.cs index 8855ff4fe25..ce1a418e203 100644 --- a/Content.Client/_Scp/Holding/ScpHoldingPredictionSystem.cs +++ b/Content.Client/_Scp/Holding/ScpHoldingSystem.cs @@ -6,6 +6,7 @@ using Content.Shared.Inventory.VirtualItem; using Robust.Client.Physics; using Robust.Client.Player; +using Robust.Shared.Physics.Components; using Robust.Shared.Timing; namespace Content.Client._Scp.Holding; @@ -22,21 +23,27 @@ public sealed class ScpHoldingSystem : SharedScpHoldingSystem private EntityUid? _suppressedHolder; private EntityUid? _suppressedTarget; + private EntityUid? _trackedHolderTarget; private TimeSpan _suppressedUntil; private EntityQuery _handsQuery; + private EntityQuery _physicsQuery; + private EntityQuery _blockerQuery; private EntityQuery _holderQuery; + private EntityQuery _virtualItemQuery; public override void Initialize() { base.Initialize(); _handsQuery = GetEntityQuery(); + _physicsQuery = GetEntityQuery(); + _blockerQuery = GetEntityQuery(); _holderQuery = GetEntityQuery(); + _virtualItemQuery = GetEntityQuery(); SubscribeLocalEvent(OnHeldAfterState); SubscribeLocalEvent(OnBlockerUnequipped); - SubscribeLocalEvent(OnHolderAfterState); SubscribeLocalEvent(OnUpdateHeldPredicted); } @@ -47,14 +54,9 @@ public override void Update(float frameTime) if (_timing.CurTime >= _suppressedUntil) ClearBlockerRespawnSuppression(); - var query = EntityQueryEnumerator(); - while (query.MoveNext(out var uid, out _)) - { - _physics.UpdateIsPredicted(uid); - } - if (_player.LocalEntity is not { Valid: true } local) { + UpdateTrackedLocalHeldTarget(null); ClearBlockerRespawnSuppression(); return; } @@ -63,31 +65,24 @@ public override void Update(float frameTime) DeleteSuppressedBlockers(local, _suppressedTarget!.Value); if (!_holderQuery.TryComp(local, out var localHolder)) + { + UpdateTrackedLocalHeldTarget(null); return; + } if (ShouldSuppressBlockerRespawn(local, localHolder.Target)) { DeleteSuppressedBlockers(local, localHolder.Target!.Value); + UpdateTrackedLocalHeldTarget(localHolder.Target); return; } - RefreshHolderState((local, localHolder)); + SyncHolderState((local, localHolder)); } private void OnHeldAfterState(Entity ent, ref AfterAutoHandleStateEvent args) { - RefreshHeldState(ent); - } - - private void OnHolderAfterState(Entity ent, ref AfterAutoHandleStateEvent args) - { - if (ShouldSuppressBlockerRespawn(ent.Owner, ent.Comp.Target)) - { - DeleteSuppressedBlockers(ent.Owner, ent.Comp.Target!.Value); - return; - } - - RefreshHolderState(ent); + ReconcileHeldAfterState(ent); } private void OnBlockerUnequipped(Entity ent, ref GotUnequippedHandEvent args) @@ -137,6 +132,38 @@ private void OnUpdateHeldPredicted(Entity ent, ref UpdateIsPre args.BlockPrediction = true; } + protected override bool ShouldUsePredictedBreakoutFeedback => true; + + protected override bool ShouldUpdateHeld(EntityUid uid, ScpHeldComponent held) + { + return _physicsQuery.TryComp(uid, out var physics) && physics.Predict; + } + + protected override bool CanShowBreakoutAttemptFeedback() + { + return _timing.IsFirstTimePredicted; + } + + protected override void OnHeldStateRefreshed(Entity held) + { + _physics.UpdateIsPredicted(held.Owner); + } + + protected override void OnHeldStateShutdown(Entity held) + { + _physics.UpdateIsPredicted(held.Owner); + } + + protected override void OnHolderStateRefreshed(Entity holder) + { + UpdateTrackedLocalHeldTarget(holder.Owner, holder.Comp.Target); + } + + protected override void OnHolderStateShutdown(EntityUid holderUid, EntityUid? target) + { + UpdateTrackedLocalHeldTarget(holderUid, null, target); + } + private void SuppressBlockerRespawn(EntityUid holder, EntityUid target) { _suppressedHolder = holder; @@ -159,6 +186,30 @@ private bool ShouldSuppressBlockerRespawn(EntityUid holder, EntityUid? target) _timing.CurTime < _suppressedUntil; } + 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); + } + private void DeleteSuppressedBlockers(EntityUid holder, EntityUid target) { if (!_handsQuery.TryComp(holder, out var hands)) @@ -166,10 +217,10 @@ private void DeleteSuppressedBlockers(EntityUid holder, EntityUid target) foreach (var heldItem in _hands.EnumerateHeld((holder, hands))) { - if (!TryComp(heldItem, out var virtualItem)) + if (!_virtualItemQuery.TryComp(heldItem, out var virtualItem)) continue; - if (!TryComp(heldItem, out var blocker)) + if (!_blockerQuery.TryComp(heldItem, out var blocker)) continue; if (virtualItem.BlockingEntity != target) diff --git a/Content.IntegrationTests/Tests/_Scp/ScpHoldingTest.cs b/Content.IntegrationTests/Tests/_Scp/ScpHoldingTest.cs index 26235e7791d..d0563b4594a 100644 --- a/Content.IntegrationTests/Tests/_Scp/ScpHoldingTest.cs +++ b/Content.IntegrationTests/Tests/_Scp/ScpHoldingTest.cs @@ -109,7 +109,7 @@ await server.WaitAssertion(() => var held = entMan.GetComponent(target); Assert.Multiple(() => { - Assert.That(held.FullHold, Is.False); + Assert.That(HasFullHold(entMan, target), Is.False); Assert.That(held.Holders, Has.Count.EqualTo(1)); Assert.That(statusEffects.HasStatusEffect(target, "StatusEffectScpHeld"), Is.True); Assert.That(alerts.IsShowingAlert(target, "ScpHoldGrabbed"), Is.True); @@ -228,7 +228,7 @@ await server.WaitAssertion(() => Assert.Multiple(() => { - Assert.That(held.FullHold, Is.False); + Assert.That(HasFullHold(sEntMan, target), Is.False); Assert.That(held.PrimaryHolder, Is.EqualTo(holder)); Assert.That(held.Holders, Has.Count.EqualTo(1)); Assert.That(move.Cancelled, Is.False); @@ -237,7 +237,7 @@ await server.WaitAssertion(() => Assert.That(distance, Is.GreaterThan(0.18f)); Assert.That(distance, Is.LessThan(0.7f)); Assert.That(contacts, Does.Not.Contain(target)); - Assert.That(holderState.SlowdownEnabled, Is.True); + Assert.That(HasHolderSlowdown(sEntMan, holder), Is.True); Assert.That(holderSpeed.WalkSpeedModifier, Is.LessThan(0.6f)); Assert.That(holderSpeed.SprintSpeedModifier, Is.LessThan(0.6f)); Assert.That(puller.Pulling, Is.Null); @@ -272,7 +272,7 @@ await client.WaitAssertion(() => Assert.Multiple(() => { - Assert.That(held.FullHold, Is.False); + Assert.That(HasFullHold(cEntMan, clientTarget), Is.False); Assert.That(held.PrimaryHolder, Is.EqualTo(clientHolder)); Assert.That(held.Holders, Has.Count.EqualTo(1)); Assert.That(distance, Is.GreaterThan(0.18f)); @@ -280,7 +280,7 @@ await client.WaitAssertion(() => Assert.That(collide.Cancelled, Is.True); Assert.That(targetCollide.Cancelled, Is.True); Assert.That(contacts, Does.Not.Contain(clientTarget)); - Assert.That(holderState.SlowdownEnabled, Is.True); + Assert.That(HasHolderSlowdown(cEntMan, clientHolder), Is.True); Assert.That(holderSpeed.WalkSpeedModifier, Is.LessThan(0.6f)); Assert.That(holderSpeed.SprintSpeedModifier, Is.LessThan(0.6f)); Assert.That(puller.Pulling, Is.Null); @@ -386,6 +386,141 @@ await client.WaitAssertion(() => await pair.CleanReturnAsync(); } + [Test] + public async Task PlayerHolderSlowdownAppliesOnGrabAndClearsOnRelease() + { + await using var pair = await PoolManager.GetServerClient(new PoolSettings + { + Fresh = true, + Connected = true, + DummyTicker = false, + }); + + var server = pair.Server; + var client = pair.Client; + var sEntMan = server.EntMan; + var cEntMan = client.EntMan; + var sTransform = server.System(); + var holding = server.System(); + var map = await pair.CreateTestMap(); + + var serverPlayer = pair.Player!.AttachedEntity!.Value; + EntityUid target = default; + var serverBaseWalk = 1f; + var serverBaseSprint = 1f; + var clientBaseWalk = 1f; + var clientBaseSprint = 1f; + + await server.WaitPost(() => + { + sTransform.SetCoordinates(serverPlayer, map.GridCoords); + sEntMan.EnsureComponent(serverPlayer); + target = sEntMan.SpawnEntity("MobHuman", map.GridCoords.Offset(new Vector2(0.1f, 0f))); + }); + + await pair.RunTicksSync(10); + await pair.SyncTicks(targetDelta: 1); + + var clientPlayer = EntityUid.Invalid; + await client.WaitAssertion(() => + { + clientPlayer = client.AttachedEntity!.Value; + _ = ToClientEntity(sEntMan, cEntMan, target); + }); + + await server.WaitAssertion(() => + { + var speed = sEntMan.GetComponent(serverPlayer); + serverBaseWalk = speed.WalkSpeedModifier; + serverBaseSprint = speed.SprintSpeedModifier; + + Assert.Multiple(() => + { + Assert.That(HasHolderSlowdown(sEntMan, serverPlayer), Is.False); + Assert.That(serverBaseWalk, Is.GreaterThan(0f)); + Assert.That(serverBaseSprint, Is.GreaterThan(0f)); + }); + }); + + await client.WaitAssertion(() => + { + var speed = cEntMan.GetComponent(clientPlayer); + clientBaseWalk = speed.WalkSpeedModifier; + clientBaseSprint = speed.SprintSpeedModifier; + + Assert.Multiple(() => + { + Assert.That(HasHolderSlowdown(cEntMan, clientPlayer), Is.False); + Assert.That(clientBaseWalk, Is.GreaterThan(0f)); + Assert.That(clientBaseSprint, Is.GreaterThan(0f)); + }); + }); + + await server.WaitPost(() => StartHold(sEntMan, holding, serverPlayer, target)); + + await pair.RunTicksSync(10); + await pair.SyncTicks(targetDelta: 1); + + await server.WaitAssertion(() => + { + var speed = sEntMan.GetComponent(serverPlayer); + Assert.Multiple(() => + { + Assert.That(sEntMan.HasComponent(serverPlayer), Is.True); + Assert.That(HasHolderSlowdown(sEntMan, serverPlayer), Is.True); + Assert.That(speed.WalkSpeedModifier, Is.LessThan(serverBaseWalk * 0.75f)); + Assert.That(speed.SprintSpeedModifier, Is.LessThan(serverBaseSprint * 0.75f)); + }); + }); + + await client.WaitAssertion(() => + { + var speed = cEntMan.GetComponent(clientPlayer); + Assert.Multiple(() => + { + Assert.That(cEntMan.HasComponent(clientPlayer), Is.True); + Assert.That(HasHolderSlowdown(cEntMan, clientPlayer), Is.True); + Assert.That(speed.WalkSpeedModifier, Is.LessThan(clientBaseWalk * 0.75f)); + Assert.That(speed.SprintSpeedModifier, Is.LessThan(clientBaseSprint * 0.75f)); + }); + }); + + await server.WaitPost(() => + { + var holdComp = sEntMan.GetComponent(serverPlayer); + Assert.That(holding.TryToggleHold((serverPlayer, holdComp), target), Is.True); + }); + + await pair.RunTicksSync(10); + await pair.SyncTicks(targetDelta: 1); + + await server.WaitAssertion(() => + { + var speed = sEntMan.GetComponent(serverPlayer); + Assert.Multiple(() => + { + Assert.That(sEntMan.HasComponent(serverPlayer), Is.False); + Assert.That(HasHolderSlowdown(sEntMan, serverPlayer), Is.False); + Assert.That(speed.WalkSpeedModifier, Is.EqualTo(serverBaseWalk).Within(0.001f)); + Assert.That(speed.SprintSpeedModifier, Is.EqualTo(serverBaseSprint).Within(0.001f)); + }); + }); + + await client.WaitAssertion(() => + { + var speed = cEntMan.GetComponent(clientPlayer); + Assert.Multiple(() => + { + Assert.That(cEntMan.HasComponent(clientPlayer), Is.False); + Assert.That(HasHolderSlowdown(cEntMan, clientPlayer), Is.False); + Assert.That(speed.WalkSpeedModifier, Is.EqualTo(clientBaseWalk).Within(0.001f)); + Assert.That(speed.SprintSpeedModifier, Is.EqualTo(clientBaseSprint).Within(0.001f)); + }); + }); + + await pair.CleanReturnAsync(); + } + [Test] public async Task PullAttemptOnHoldableTargetRedirectsToHoldAndReplacesVanillaPull() { @@ -486,7 +621,7 @@ await server.WaitAssertion(() => Assert.Multiple(() => { - Assert.That(held.FullHold, Is.True); + Assert.That(HasFullHold(entMan, target), Is.True); Assert.That(held.RequiredHolderCount, Is.EqualTo(2)); Assert.That(held.Holders, Has.Count.EqualTo(2)); Assert.That(move.Cancelled, Is.True); @@ -501,6 +636,102 @@ await server.WaitAssertion(() => await pair.CleanReturnAsync(); } + [Test] + public async Task FullHoldVictimBlockersStayStableOnServerAndClient() + { + await using var pair = await PoolManager.GetServerClient(new PoolSettings + { + Fresh = true, + Connected = true, + DummyTicker = false, + }); + + var server = pair.Server; + var client = pair.Client; + var sEntMan = server.EntMan; + var cEntMan = client.EntMan; + var sTransform = server.System(); + var sHandsSystem = server.System(); + var cHandsSystem = client.System(); + var holding = server.System(); + var map = await pair.CreateTestMap(); + + var serverPlayer = pair.Player!.AttachedEntity!.Value; + EntityUid holderOne = default; + EntityUid holderTwo = default; + + await server.WaitPost(() => + { + sTransform.SetCoordinates(serverPlayer, map.GridCoords); + holderOne = sEntMan.SpawnEntity(HolderPrototype, map.GridCoords.Offset(new Vector2(0.5f, 0f))); + holderTwo = sEntMan.SpawnEntity(HolderPrototype, map.GridCoords.Offset(new Vector2(-0.5f, 0f))); + StartHold(sEntMan, holding, holderOne, serverPlayer); + StartHold(sEntMan, holding, holderTwo, serverPlayer); + }); + + await pair.RunTicksSync(10); + await pair.SyncTicks(targetDelta: 1); + + EntityUid[] initialServerBlockers = []; + await server.WaitAssertion(() => + { + var hands = sEntMan.GetComponent(serverPlayer); + + Assert.Multiple(() => + { + Assert.That(HasFullHold(sEntMan, serverPlayer), Is.True); + Assert.That(CountBlockingVirtualHands(sEntMan, sHandsSystem, serverPlayer, hands), Is.EqualTo(hands.SortedHands.Count)); + }); + + initialServerBlockers = GetHeldHandBlockers(sEntMan, sHandsSystem, serverPlayer, hands); + Assert.That(initialServerBlockers, Has.Length.EqualTo(hands.SortedHands.Count)); + }); + + var clientPlayer = EntityUid.Invalid; + EntityUid[] initialClientBlockers = []; + await client.WaitAssertion(() => + { + clientPlayer = client.AttachedEntity!.Value; + var hands = cEntMan.GetComponent(clientPlayer); + + Assert.Multiple(() => + { + Assert.That(HasFullHold(cEntMan, clientPlayer), Is.True); + Assert.That(CountBlockingVirtualHands(cEntMan, cHandsSystem, clientPlayer, hands), Is.EqualTo(hands.SortedHands.Count)); + }); + + initialClientBlockers = GetHeldHandBlockers(cEntMan, cHandsSystem, clientPlayer, hands); + Assert.That(initialClientBlockers, Has.Length.EqualTo(hands.SortedHands.Count)); + }); + + await pair.RunTicksSync(8); + await pair.SyncTicks(targetDelta: 1); + + await server.WaitAssertion(() => + { + var hands = sEntMan.GetComponent(serverPlayer); + + Assert.Multiple(() => + { + Assert.That(HasFullHold(sEntMan, serverPlayer), Is.True); + Assert.That(GetHeldHandBlockers(sEntMan, sHandsSystem, serverPlayer, hands), Is.EqualTo(initialServerBlockers)); + }); + }); + + await client.WaitAssertion(() => + { + var hands = cEntMan.GetComponent(clientPlayer); + + Assert.Multiple(() => + { + Assert.That(HasFullHold(cEntMan, clientPlayer), Is.True); + Assert.That(GetHeldHandBlockers(cEntMan, cHandsSystem, clientPlayer, hands), Is.EqualTo(initialClientBlockers)); + }); + }); + + await pair.CleanReturnAsync(); + } + [Test] public async Task TargetWithoutScpHoldableCannotBeHeld() { @@ -727,7 +958,7 @@ await server.WaitAssertion(() => Assert.Multiple(() => { Assert.That(held.RequiredHolderCount, Is.EqualTo(3)); - Assert.That(held.FullHold, Is.False); + Assert.That(HasFullHold(entMan, target), Is.False); Assert.That(held.Holders, Has.Count.EqualTo(2)); }); }); @@ -744,7 +975,7 @@ await server.WaitAssertion(() => Assert.Multiple(() => { - Assert.That(held.FullHold, Is.True); + Assert.That(HasFullHold(entMan, target), Is.True); Assert.That(held.RequiredHolderCount, Is.EqualTo(3)); Assert.That(hands.SortedHands.Count, Is.EqualTo(3)); Assert.That(CountBlockingVirtualHands(entMan, handsSystem, target, hands), Is.EqualTo(3)); @@ -768,7 +999,7 @@ await server.WaitAssertion(() => Assert.Multiple(() => { Assert.That(held.RequiredHolderCount, Is.EqualTo(2)); - Assert.That(held.FullHold, Is.True); + Assert.That(HasFullHold(entMan, target), Is.True); Assert.That(hands.SortedHands.Count, Is.EqualTo(2)); Assert.That(CountBlockingVirtualHands(entMan, handsSystem, target, hands), Is.EqualTo(2)); Assert.That(VictimHandsUseHolderIcons(entMan, handsSystem, target, hands, holderOne, holderTwo, holderThree), Is.True); @@ -810,8 +1041,8 @@ await server.WaitAssertion(() => var held = entMan.GetComponent(target); Assert.Multiple(() => { - Assert.That(held.FullHold, Is.True); - Assert.That(held.BreakoutDoAfterId, Is.Null); + Assert.That(HasFullHold(entMan, target), Is.True); + Assert.That(HasBreakoutAttempt(entMan, target), Is.False); }); }); @@ -825,7 +1056,7 @@ await server.WaitAssertion(() => var held = entMan.GetComponent(target); Assert.Multiple(() => { - Assert.That(held.BreakoutDoAfterId, Is.Not.Null); + Assert.That(HasBreakoutAttempt(entMan, target), Is.True); Assert.That(CountAttachedPrototype(entMan, holderOne, "WhistleExclamation"), Is.EqualTo(1)); Assert.That(CountAttachedPrototype(entMan, holderTwo, "WhistleExclamation"), Is.EqualTo(1)); }); @@ -869,6 +1100,99 @@ await server.WaitAssertion(() => await pair.CleanReturnAsync(); } + [Test] + public async Task FullBreakoutRestoresMovementOnServerAndClient() + { + await using var pair = await PoolManager.GetServerClient(new PoolSettings + { + Fresh = true, + Connected = true, + DummyTicker = false, + }); + + var server = pair.Server; + var client = pair.Client; + var sEntMan = server.EntMan; + var cEntMan = client.EntMan; + var timing = server.ResolveDependency(); + var sTransform = server.System(); + var holding = server.System(); + var map = await pair.CreateTestMap(); + + var serverPlayer = pair.Player!.AttachedEntity!.Value; + EntityUid holderOne = default; + EntityUid holderTwo = default; + + await server.WaitPost(() => + { + sTransform.SetCoordinates(serverPlayer, map.GridCoords); + holderOne = sEntMan.SpawnEntity(HolderPrototype, map.GridCoords.Offset(new Vector2(0.5f, 0f))); + holderTwo = sEntMan.SpawnEntity(HolderPrototype, map.GridCoords.Offset(new Vector2(-0.5f, 0f))); + StartHold(sEntMan, holding, holderOne, serverPlayer); + StartHold(sEntMan, holding, holderTwo, serverPlayer); + }); + + await pair.RunTicksSync(10); + await pair.SyncTicks(targetDelta: 1); + await pair.RunTicksSync(GetTickCount(timing, TimeSpan.FromSeconds(10))); + await pair.SyncTicks(targetDelta: 1); + + await server.WaitAssertion(() => + { + var mover = sEntMan.GetComponent(serverPlayer); + + Assert.Multiple(() => + { + Assert.That(HasFullHold(sEntMan, serverPlayer), Is.True); + Assert.That(mover.CanMove, Is.False); + }); + }); + + var clientPlayer = EntityUid.Invalid; + await client.WaitAssertion(() => + { + clientPlayer = client.AttachedEntity!.Value; + var mover = cEntMan.GetComponent(clientPlayer); + + Assert.Multiple(() => + { + Assert.That(HasFullHold(cEntMan, clientPlayer), Is.True); + Assert.That(mover.CanMove, Is.False); + }); + }); + + await server.WaitPost(() => RaiseMoveInput(sEntMan, serverPlayer)); + await pair.RunTicksSync(2); + await pair.RunTicksSync(GetTickCount(timing, TimeSpan.FromSeconds(5)) + 2); + await pair.SyncTicks(targetDelta: 1); + + await server.WaitAssertion(() => + { + var mover = sEntMan.GetComponent(serverPlayer); + + Assert.Multiple(() => + { + Assert.That(sEntMan.HasComponent(serverPlayer), Is.False); + Assert.That(sEntMan.HasComponent(serverPlayer), Is.False); + Assert.That(mover.CanMove, Is.True); + }); + }); + + await client.WaitAssertion(() => + { + var mover = cEntMan.GetComponent(clientPlayer); + + Assert.Multiple(() => + { + Assert.That(cEntMan.HasComponent(clientPlayer), Is.False); + Assert.That(cEntMan.HasComponent(clientPlayer), Is.False); + Assert.That(mover.CanMove, Is.True); + }); + }); + + await pair.CleanReturnAsync(); + } + [Test] public async Task FullBreakoutByAlertStartsAndCompletes() { @@ -909,7 +1233,7 @@ await server.WaitAssertion(() => var held = entMan.GetComponent(target); Assert.Multiple(() => { - Assert.That(held.BreakoutDoAfterId, Is.Not.Null); + Assert.That(HasBreakoutAttempt(entMan, target), Is.True); Assert.That(CountAttachedPrototype(entMan, holderOne, "WhistleExclamation"), Is.EqualTo(1)); Assert.That(CountAttachedPrototype(entMan, holderTwo, "WhistleExclamation"), Is.EqualTo(1)); }); @@ -1140,7 +1464,7 @@ await client.WaitAssertion(() => var holderHands = cEntMan.GetComponent(clientPlayer); Assert.Multiple(() => { - Assert.That(held.FullHold, Is.False); + Assert.That(HasFullHold(cEntMan, clientTarget), Is.False); Assert.That(held.Holders, Has.Count.EqualTo(1)); Assert.That(cEntMan.HasComponent(clientPlayer), Is.True); Assert.That(CountHolderHandBlockers(cEntMan, cHandsSystem, clientPlayer, clientTarget, holderHands), Is.EqualTo(1)); @@ -1183,7 +1507,7 @@ await server.WaitAssertion(() => var distance = GetDistance(sTransform, serverPlayer, target); Assert.Multiple(() => { - Assert.That(held.FullHold, Is.False); + Assert.That(HasFullHold(sEntMan, target), Is.False); Assert.That(held.Holders, Has.Count.EqualTo(1)); Assert.That(held.PrimaryHolder, Is.EqualTo(serverPlayer)); Assert.That(distance, Is.GreaterThan(0.18f)); @@ -1206,7 +1530,7 @@ await client.WaitAssertion(() => var distance = GetDistance(cTransform, clientPlayer, clientTarget); Assert.Multiple(() => { - Assert.That(held.FullHold, Is.False); + Assert.That(HasFullHold(cEntMan, clientTarget), Is.False); Assert.That(held.Holders, Has.Count.EqualTo(1)); Assert.That(held.PrimaryHolder, Is.EqualTo(clientPlayer)); Assert.That(distance, Is.GreaterThan(0.18f)); @@ -1440,7 +1764,7 @@ await client.WaitAssertion(() => var held = cEntMan.GetComponent(clientTarget); Assert.Multiple(() => { - Assert.That(held.FullHold, Is.False); + Assert.That(HasFullHold(cEntMan, clientTarget), Is.False); Assert.That(held.Holders, Has.Count.EqualTo(1)); }); }); @@ -1454,7 +1778,7 @@ await client.WaitAssertion(() => Assert.Multiple(() => { - Assert.That(held.FullHold, Is.True); + Assert.That(HasFullHold(cEntMan, clientTarget), Is.True); Assert.That(held.Holders, Has.Count.EqualTo(2)); Assert.That(CountBlockingVirtualHands(cEntMan, cHandsSystem, clientTarget, hands), Is.EqualTo(hands.SortedHands.Count)); Assert.That(VictimHandsUseHolderIcons(cEntMan, cHandsSystem, clientTarget, hands, clientHolderOne, clientPlayer), Is.True); @@ -1466,7 +1790,7 @@ await server.WaitAssertion(() => var held = sEntMan.GetComponent(target); Assert.Multiple(() => { - Assert.That(held.FullHold, Is.False); + Assert.That(HasFullHold(sEntMan, target), Is.False); Assert.That(held.Holders, Has.Count.EqualTo(1)); }); }); @@ -1479,7 +1803,7 @@ await server.WaitAssertion(() => var held = sEntMan.GetComponent(target); Assert.Multiple(() => { - Assert.That(held.FullHold, Is.True); + Assert.That(HasFullHold(sEntMan, target), Is.True); Assert.That(held.Holders, Has.Count.EqualTo(2)); }); }); @@ -1489,7 +1813,7 @@ await client.WaitAssertion(() => var held = cEntMan.GetComponent(clientTarget); Assert.Multiple(() => { - Assert.That(held.FullHold, Is.True); + Assert.That(HasFullHold(cEntMan, clientTarget), Is.True); Assert.That(held.Holders, Has.Count.EqualTo(2)); }); }); @@ -1551,7 +1875,7 @@ await server.WaitAssertion(() => Assert.Multiple(() => { - Assert.That(held.FullHold, Is.False); + Assert.That(HasFullHold(sEntMan, target), Is.False); Assert.That(held.Holders, Has.Count.EqualTo(2)); Assert.That(held.PrimaryHolder, Is.EqualTo(serverPlayer)); Assert.That(distance, Is.GreaterThan(0.18f)); @@ -1581,7 +1905,7 @@ await client.WaitAssertion(() => Assert.Multiple(() => { - Assert.That(held.FullHold, Is.False); + Assert.That(HasFullHold(cEntMan, clientTarget), Is.False); Assert.That(held.Holders, Has.Count.EqualTo(2)); Assert.That(held.PrimaryHolder, Is.EqualTo(clientPlayer)); Assert.That(distance, Is.GreaterThan(0.18f)); @@ -1632,7 +1956,7 @@ await client.WaitAssertion(() => Assert.Multiple(() => { - Assert.That(held.FullHold, Is.False); + Assert.That(HasFullHold(cEntMan, clientTarget), Is.False); Assert.That(held.Holders, Has.Count.EqualTo(1)); Assert.That(held.PrimaryHolder, Is.EqualTo(clientHolderTwo)); Assert.That(distance, Is.GreaterThan(0.18f)); @@ -1687,7 +2011,7 @@ await server.WaitAssertion(() => var held = sEntMan.GetComponent(serverPlayer); Assert.Multiple(() => { - Assert.That(held.FullHold, Is.True); + Assert.That(HasFullHold(sEntMan, serverPlayer), Is.True); }); }); @@ -1699,8 +2023,7 @@ await client.WaitAssertion(() => clientPlayer = client.AttachedEntity!.Value; clientHolderOne = ToClientEntity(sEntMan, cEntMan, holderOne); clientHolderTwo = ToClientEntity(sEntMan, cEntMan, holderTwo); - var held = cEntMan.GetComponent(clientPlayer); - Assert.That(held.FullHold, Is.True); + Assert.That(HasFullHold(cEntMan, clientPlayer), Is.True); }); await client.WaitPost(() => @@ -1710,7 +2033,7 @@ await client.WaitPost(() => var held = cEntMan.GetComponent(clientPlayer); Assert.Multiple(() => { - Assert.That(held.BreakoutDoAfterId, Is.Not.Null); + Assert.That(HasBreakoutAttempt(cEntMan, clientPlayer), Is.True); Assert.That(cEntMan.HasComponent(clientPlayer), Is.False); Assert.That(CountAttachedPrototype(cEntMan, clientHolderOne, "WhistleExclamation"), Is.EqualTo(1)); Assert.That(CountAttachedPrototype(cEntMan, clientHolderTwo, "WhistleExclamation"), Is.EqualTo(1)); @@ -1722,8 +2045,8 @@ await server.WaitAssertion(() => var held = sEntMan.GetComponent(serverPlayer); Assert.Multiple(() => { - Assert.That(held.FullHold, Is.True); - Assert.That(held.BreakoutDoAfterId, Is.Null); + Assert.That(HasFullHold(sEntMan, serverPlayer), Is.True); + Assert.That(HasBreakoutAttempt(sEntMan, serverPlayer), Is.False); }); }); @@ -1871,7 +2194,7 @@ await server.WaitAssertion(() => Assert.Multiple(() => { - Assert.That(held.FullHold, Is.False); + Assert.That(HasFullHold(entMan, target), Is.False); Assert.That(held.PrimaryHolder, Is.EqualTo(holderOne)); Assert.That(holderOnePuller.Pulling, Is.Null); Assert.That(holderTwoPuller.Pulling, Is.Null); @@ -1895,7 +2218,7 @@ await server.WaitAssertion(() => Assert.Multiple(() => { - Assert.That(held.FullHold, Is.False); + Assert.That(HasFullHold(entMan, target), Is.False); Assert.That(held.Holders, Has.Count.EqualTo(1)); Assert.That(held.PrimaryHolder, Is.EqualTo(holderTwo)); Assert.That(holderOnePuller.Pulling, Is.Null); @@ -1963,7 +2286,7 @@ await server.WaitAssertion(() => Assert.Multiple(() => { - Assert.That(held.FullHold, Is.False); + Assert.That(HasFullHold(sEntMan, serverPlayer), Is.False); Assert.That(held.Holders, Has.Count.EqualTo(1)); Assert.That(holderState.Target, Is.EqualTo(serverPlayer)); Assert.That(CountHolderHandBlockers(sEntMan, sHandsSystem, holder, serverPlayer, holderHands), Is.EqualTo(1)); @@ -1986,7 +2309,7 @@ await client.WaitAssertion(() => Assert.Multiple(() => { - Assert.That(held.FullHold, Is.False); + Assert.That(HasFullHold(cEntMan, clientPlayer), Is.False); Assert.That(held.Holders, Has.Count.EqualTo(1)); Assert.That(holderState.Target, Is.EqualTo(clientPlayer)); Assert.That(CountHolderHandBlockers(cEntMan, cHandsSystem, clientHolder, clientPlayer, holderHands), Is.EqualTo(1)); @@ -2049,6 +2372,34 @@ private static int CountBlockingVirtualHands(IEntityManager entMan, SharedHandsS entMan.HasComponent(item)); } + private static EntityUid[] GetHeldHandBlockers(IEntityManager entMan, SharedHandsSystem handsSystem, EntityUid uid, HandsComponent hands) + { + return handsSystem.EnumerateHeld((uid, hands)) + .Where(item => + entMan.TryGetComponent(item, out VirtualItemComponent? virtualItem) && + entMan.TryGetComponent(item, out ScpHeldHandBlockerComponent? blocker) && + blocker.Target == uid && + blocker.Holder == virtualItem.BlockingEntity && + entMan.HasComponent(item)) + .Order() + .ToArray(); + } + + private static bool HasFullHold(IEntityManager entMan, EntityUid uid) + { + return entMan.HasComponent(uid); + } + + private static bool HasBreakoutAttempt(IEntityManager entMan, EntityUid uid) + { + return entMan.HasComponent(uid); + } + + private static bool HasHolderSlowdown(IEntityManager entMan, EntityUid uid) + { + return entMan.HasComponent(uid); + } + private static bool VictimHandsUseHolderIcons(IEntityManager entMan, SharedHandsSystem handsSystem, EntityUid uid, HandsComponent hands, params EntityUid[] holders) { var expected = holders.ToHashSet(); diff --git a/Content.Server/_Scp/Holding/ScpHoldingSystem.cs b/Content.Server/_Scp/Holding/ScpHoldingSystem.cs index fe044ae038c..323b8206205 100644 --- a/Content.Server/_Scp/Holding/ScpHoldingSystem.cs +++ b/Content.Server/_Scp/Holding/ScpHoldingSystem.cs @@ -1,8 +1,42 @@ -using Content.Shared._Scp.Holding; +using Content.Shared.Hands; +using Content.Shared._Scp.Holding; namespace Content.Server._Scp.Holding; -public abstract class ScpHoldingSystem : SharedScpHoldingSystem +public sealed class ScpHoldingSystem : SharedScpHoldingSystem { + protected override bool ShouldShowHoldPopups => true; + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnHoldShutdown); + SubscribeLocalEvent(OnHandCountChanged); + } + + protected override void OnHeldStateShutdown(Entity held) + { + foreach (var holderUid in held.Comp.Holders) + { + if (TryComp(holderUid, out _)) + RemComp(holderUid); + } + } + + private void OnHoldShutdown(Entity ent, ref ComponentShutdown args) + { + if (!TryComp(ent.Owner, out var holder)) + return; + + if (holder.Target == null) + return; + + ReleaseHolderContribution(ent.Owner, holder.Target.Value, clearIfEmpty: true); + } + + private void OnHandCountChanged(Entity ent, ref HandCountChangedEvent args) + { + SyncHeldState(ent); + } } diff --git a/Content.Shared/_Scp/Holding/ScpBreakoutAttemptComponent.cs b/Content.Shared/_Scp/Holding/ScpBreakoutAttemptComponent.cs new file mode 100644 index 00000000000..4900b0f5ee0 --- /dev/null +++ b/Content.Shared/_Scp/Holding/ScpBreakoutAttemptComponent.cs @@ -0,0 +1,10 @@ +using Robust.Shared.GameStates; + +namespace Content.Shared._Scp.Holding; + +/// +/// Semantic state that marks an active breakout attempt during a full hold. +/// +[RegisterComponent, NetworkedComponent] +[Access(typeof(SharedScpHoldingSystem))] +public sealed partial class ScpBreakoutAttemptComponent : Component; diff --git a/Content.Shared/_Scp/Holding/ScpFullHeldComponent.cs b/Content.Shared/_Scp/Holding/ScpFullHeldComponent.cs new file mode 100644 index 00000000000..6b20d9606d2 --- /dev/null +++ b/Content.Shared/_Scp/Holding/ScpFullHeldComponent.cs @@ -0,0 +1,18 @@ +using Robust.Shared.GameStates; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom; + +namespace Content.Shared._Scp.Holding; + +/// +/// Runtime full-hold state stored on a target while it is immobilized. +/// +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState, AutoGenerateComponentPause] +[Access(typeof(SharedScpHoldingSystem))] +public sealed partial class ScpFullHeldComponent : Component +{ + /// + /// Timestamp when the current uninterrupted full hold started. + /// + [DataField(customTypeSerializer: typeof(TimeOffsetSerializer)), AutoNetworkedField, AutoPausedField] + public TimeSpan StartedAt; +} diff --git a/Content.Shared/_Scp/Holding/ScpHeldComponent.cs b/Content.Shared/_Scp/Holding/ScpHeldComponent.cs index 5663627e850..ff99fcf9bd6 100644 --- a/Content.Shared/_Scp/Holding/ScpHeldComponent.cs +++ b/Content.Shared/_Scp/Holding/ScpHeldComponent.cs @@ -10,82 +10,69 @@ namespace Content.Shared._Scp.Holding; [Access(typeof(SharedScpHoldingSystem))] public sealed partial class ScpHeldComponent : Component { - /// - /// Whether the target is currently in the immobile full hold stage. - /// - [AutoNetworkedField] - public bool FullHold; - /// /// Next timestamp when a soft breakout attempt may succeed. /// [DataField(customTypeSerializer: typeof(TimeOffsetSerializer)), AutoNetworkedField, AutoPausedField] public TimeSpan SoftEscapeAvailableAt; - /// - /// Timestamp when the current uninterrupted full hold started. - /// - [DataField(customTypeSerializer: typeof(TimeOffsetSerializer)), AutoNetworkedField, AutoPausedField] - public TimeSpan? FullHoldStartedAt; - /// /// Ordered holder list used for reassignment and contribution counting. /// - [AutoNetworkedField] - public List Holders = new(); + [AutoNetworkedField, ViewVariables] + public List Holders = []; /// /// Current primary holder used as the soft hold drag anchor. /// - [AutoNetworkedField] + [AutoNetworkedField, ViewVariables] public EntityUid? PrimaryHolder; /// /// Required contributor count for entering full hold. /// + [AutoNetworkedField, ViewVariables] public int RequiredHolderCount = 2; /// /// Copied soft breakout cooldown configuration from the initial holdable target. /// + /// Leave it unused for some time for balance reasons public TimeSpan SoftEscapeCooldown = TimeSpan.FromSeconds(1); /// /// Copied full hold delay configuration from the initial holdable target. /// - [AutoNetworkedField] + [AutoNetworkedField, ViewVariables] public TimeSpan FullHoldDelay = TimeSpan.FromSeconds(10); /// /// Copied full breakout duration configuration from the initial holdable target. /// - [AutoNetworkedField] + [AutoNetworkedField, ViewVariables] public TimeSpan FullBreakoutDuration = TimeSpan.FromSeconds(5); /// /// Copied post-breakout immunity duration from the initial holdable target. /// + [AutoNetworkedField, ViewVariables] public TimeSpan PostBreakoutImmunity = TimeSpan.FromSeconds(5); /// /// Copied maximum hold range from the initial holdable target. /// - [AutoNetworkedField] + [AutoNetworkedField, ViewVariables] public float HoldRange = 1f; /// /// Copied walk slowdown applied through . /// + [AutoNetworkedField, ViewVariables] public float WalkModifier = 0.5f; /// /// Copied sprint slowdown applied through . /// + [AutoNetworkedField, ViewVariables] public float SprintModifier = 0.5f; - - /// - /// Active breakout do-after id for a full hold, if one exists. - /// - [AutoNetworkedField] - public ushort? BreakoutDoAfterId; } diff --git a/Content.Shared/_Scp/Holding/ScpHeldHandBlockerComponent.cs b/Content.Shared/_Scp/Holding/ScpHeldHandBlockerComponent.cs index 88b1250a0ca..9efd567c2c3 100644 --- a/Content.Shared/_Scp/Holding/ScpHeldHandBlockerComponent.cs +++ b/Content.Shared/_Scp/Holding/ScpHeldHandBlockerComponent.cs @@ -5,17 +5,19 @@ namespace Content.Shared._Scp.Holding; /// /// Marks a victim hand placeholder virtual item created by SCP holding. /// -[RegisterComponent, NetworkedComponent] +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState(fieldDeltas: true)] [Access(typeof(SharedScpHoldingSystem))] public sealed partial class ScpHeldHandBlockerComponent : Component { /// /// Held target whose hand is occupied by this placeholder. /// + [AutoNetworkedField, ViewVariables] public EntityUid Target; /// /// Holder whose sprite is shown in this placeholder. /// + [AutoNetworkedField, ViewVariables] public EntityUid Holder; } diff --git a/Content.Shared/_Scp/Holding/ScpHoldHandBlockerComponent.cs b/Content.Shared/_Scp/Holding/ScpHoldHandBlockerComponent.cs index 606551c7168..85c2d67b321 100644 --- a/Content.Shared/_Scp/Holding/ScpHoldHandBlockerComponent.cs +++ b/Content.Shared/_Scp/Holding/ScpHoldHandBlockerComponent.cs @@ -5,13 +5,13 @@ namespace Content.Shared._Scp.Holding; /// /// Marks a virtual item that reserves one holder hand for an active SCP hold. /// -[RegisterComponent, NetworkedComponent, AutoGenerateComponentState(true)] +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState(raiseAfterAutoHandleState: true, fieldDeltas: true)] [Access(typeof(SharedScpHoldingSystem))] public sealed partial class ScpHoldHandBlockerComponent : Component { /// /// The held target represented by this virtual item. /// - [AutoNetworkedField] + [AutoNetworkedField, ViewVariables] public EntityUid Target; } diff --git a/Content.Shared/_Scp/Holding/ScpHoldableComponent.cs b/Content.Shared/_Scp/Holding/ScpHoldableComponent.cs index 0a09ba5551d..765e971bfdc 100644 --- a/Content.Shared/_Scp/Holding/ScpHoldableComponent.cs +++ b/Content.Shared/_Scp/Holding/ScpHoldableComponent.cs @@ -1,12 +1,14 @@ -using System; using Content.Shared.Whitelist; +using Robust.Shared.Audio; +using Robust.Shared.GameStates; +using Robust.Shared.Prototypes; namespace Content.Shared._Scp.Holding; /// /// Marks an entity as a valid target for the SCP holding mechanic and stores per-target hold tuning. /// -[RegisterComponent] +[RegisterComponent, NetworkedComponent] public sealed partial class ScpHoldableComponent : Component { /// @@ -45,6 +47,19 @@ public sealed partial class ScpHoldableComponent : Component [DataField] public TimeSpan PostBreakoutImmunity = TimeSpan.FromSeconds(5); + /// + /// Optional effect prototype spawned on each holder when a full-hold breakout attempt starts. + /// + [DataField] + public EntProtoId? BreakoutAttemptEffect = "WhistleExclamation"; + + /// + /// Optional sound played from the held target when a full-hold breakout attempt starts. + /// + [DataField] + public SoundSpecifier? BreakoutAttemptSound = new SoundCollectionSpecifier("storageRustle", + AudioParams.Default.WithVolume(-2f).WithMaxDistance(4f).WithVariation(0.15f)); + /// /// Maximum unobstructed range allowed between holder and target. /// diff --git a/Content.Shared/_Scp/Holding/ScpHolderComponent.cs b/Content.Shared/_Scp/Holding/ScpHolderComponent.cs index 6989c3dad0a..a33f1b572ef 100644 --- a/Content.Shared/_Scp/Holding/ScpHolderComponent.cs +++ b/Content.Shared/_Scp/Holding/ScpHolderComponent.cs @@ -5,31 +5,13 @@ namespace Content.Shared._Scp.Holding; /// /// Runtime contribution state stored on each active holder. /// -[RegisterComponent, NetworkedComponent, AutoGenerateComponentState(true)] +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] [Access(typeof(SharedScpHoldingSystem))] public sealed partial class ScpHolderComponent : Component { /// /// Target currently being contributed to. /// - [AutoNetworkedField] + [AutoNetworkedField, ViewVariables] public EntityUid? Target; - - /// - /// Whether this holder should currently receive the custom slowdown. - /// - [AutoNetworkedField] - public bool SlowdownEnabled; - - /// - /// Walk speed modifier used when is true. - /// - [AutoNetworkedField] - public float WalkModifier = 1f; - - /// - /// Sprint speed modifier used when is true. - /// - [AutoNetworkedField] - public float SprintModifier = 1f; } diff --git a/Content.Shared/_Scp/Holding/ScpHolderSlowdownComponent.cs b/Content.Shared/_Scp/Holding/ScpHolderSlowdownComponent.cs new file mode 100644 index 00000000000..59980516ba0 --- /dev/null +++ b/Content.Shared/_Scp/Holding/ScpHolderSlowdownComponent.cs @@ -0,0 +1,23 @@ +using Robust.Shared.GameStates; + +namespace Content.Shared._Scp.Holding; + +/// +/// Runtime slowdown state stored on an active holder while their movement is penalized. +/// +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState(true)] +[Access(typeof(SharedScpHoldingSystem))] +public sealed partial class ScpHolderSlowdownComponent : Component +{ + /// + /// Walk speed modifier applied while the holder contributes to an active hold. + /// + [AutoNetworkedField, ViewVariables] + public float WalkModifier = 1f; + + /// + /// Sprint speed modifier applied while the holder contributes to an active hold. + /// + [AutoNetworkedField, ViewVariables] + public float SprintModifier = 1f; +} diff --git a/Content.Shared/_Scp/Holding/SharedScpHoldingSystem.Actions.cs b/Content.Shared/_Scp/Holding/SharedScpHoldingSystem.Actions.cs index b9797c43225..0f5fa11d7b8 100644 --- a/Content.Shared/_Scp/Holding/SharedScpHoldingSystem.Actions.cs +++ b/Content.Shared/_Scp/Holding/SharedScpHoldingSystem.Actions.cs @@ -139,7 +139,7 @@ public bool CanToggleHold( { range = held.HoldRange; - if (held.FullHold) + if (_fullHeldQuery.HasComp(target)) { if (!quiet) PopupHolder(holder.Owner, "scp-hold-target-fully-held", ("target", target)); @@ -162,7 +162,7 @@ public bool CanToggleHold( public bool TryBreakOut(Entity held, bool viaMovement) { - return held.Comp.FullHold + return _fullHeldQuery.HasComp(held.Owner) ? TryStartFullBreakout(held, viaMovement) : TrySoftBreakOut(held, viaMovement); } @@ -176,20 +176,23 @@ public bool TryForceBreakOut(Entity held, bool viaMovement = return true; } - public void RefreshHeldState(Entity held) + protected bool IsFullHold(EntityUid uid) { - _alerts.ShowAlert(held.Owner, "ScpHoldGrabbed"); - SyncHeldStatusEffect(held.Owner); - SyncPlaceholderHands(held); - _actionBlocker.UpdateCanMove(held.Owner); - EnsureCombatModeDisabled(held.Owner); - _physics.UpdateIsPredicted(held.Owner); + return _fullHeldQuery.HasComp(uid); } - public void RefreshHolderState(Entity holder) + protected void ReconcileHeldAfterState(Entity held) + { + OnHeldStateRefreshed(held); + + if (_fullHeldQuery.HasComp(held.Owner)) + SyncPlaceholderHands(held); + } + + public void SyncHolderState(Entity holder) { SyncHolderHandBlocker(holder); - _movement.RefreshMovementSpeedModifiers(holder.Owner); + OnHolderStateRefreshed(holder); } private bool TrySoftBreakOut(Entity held, bool viaMovement) @@ -206,13 +209,16 @@ private bool TrySoftBreakOut(Entity held, bool viaMovement) private bool TryStartFullBreakout(Entity held, bool viaMovement) { - if (held.Comp.FullHoldStartedAt == null) + if (!_fullHeldQuery.TryComp(held.Owner, out var fullHeld)) + return false; + + if (fullHeld.StartedAt == TimeSpan.Zero) { PopupTarget(held.Owner, "scp-hold-breakout-too-early", ("seconds", 1)); return false; } - var breakoutAvailableAt = held.Comp.FullHoldStartedAt.Value + held.Comp.FullHoldDelay; + var breakoutAvailableAt = fullHeld.StartedAt + held.Comp.FullHoldDelay; if (_timing.CurTime < breakoutAvailableAt) { var remaining = breakoutAvailableAt - _timing.CurTime; @@ -221,7 +227,7 @@ private bool TryStartFullBreakout(Entity held, bool viaMovemen return false; } - if (held.Comp.BreakoutDoAfterId != null) + if (_breakoutAttemptQuery.HasComp(held.Owner)) return true; var doAfter = new DoAfterArgs( @@ -241,8 +247,7 @@ private bool TryStartFullBreakout(Entity held, bool viaMovemen if (!_doAfter.TryStartDoAfter(doAfter, out var id)) return false; - SetBreakoutDoAfterId(held, id.Value.Index); - ShowBreakoutAttemptFeedback(held); + StartBreakoutAttempt(held.Owner, id.Value); PopupTarget(held.Owner, "scp-hold-breakout-start"); return true; @@ -304,7 +309,7 @@ private void SetHoldAvailableAt(Entity holder, TimeSpan? holdA return; } - DirtyHoldField(holder, nameof(ScpHoldComponent.HoldAvailableAt)); + DirtyField(holder.Owner, holder.Comp, nameof(ScpHoldComponent.HoldAvailableAt)); } private bool CanPassHoldAttempt(EntityUid holderUid, EntityUid targetUid) @@ -317,7 +322,7 @@ private bool CanPassHoldAttempt(EntityUid holderUid, EntityUid targetUid) private void RaiseBreakoutEvent(Entity held, bool viaMovement, bool applyImmunity) { - var ev = new ScpHoldBreakoutEvent(viaMovement, held.Comp.FullHold, applyImmunity); + var ev = new ScpHoldBreakoutEvent(viaMovement, _fullHeldQuery.HasComp(held.Owner), applyImmunity); RaiseLocalEvent(held.Owner, ev); } diff --git a/Content.Shared/_Scp/Holding/SharedScpHoldingSystem.BreakoutAttempt.cs b/Content.Shared/_Scp/Holding/SharedScpHoldingSystem.BreakoutAttempt.cs new file mode 100644 index 00000000000..e26a323b5bb --- /dev/null +++ b/Content.Shared/_Scp/Holding/SharedScpHoldingSystem.BreakoutAttempt.cs @@ -0,0 +1,36 @@ +using Content.Shared.DoAfter; + +namespace Content.Shared._Scp.Holding; + +public abstract partial class SharedScpHoldingSystem +{ + /* + * Semantic breakout-attempt state plus private do-after handle tracking. + */ + + private void StartBreakoutAttempt(EntityUid uid, DoAfterId doAfterId) + { + _breakoutDoAfterIds[uid] = doAfterId; + EnsureComp(uid); + } + + private void EndBreakoutAttempt(EntityUid uid, bool cancelDoAfter) + { + var hadAttempt = _breakoutAttemptQuery.HasComp(uid); + var hadDoAfter = _breakoutDoAfterIds.Remove(uid, out var doAfterId); + + if (hadAttempt) + RemComp(uid); + + if (cancelDoAfter && hadDoAfter) + CancelBreakoutAttemptDoAfter(doAfterId); + } + + private void CancelBreakoutAttemptDoAfter(DoAfterId doAfterId) + { + if (!_doAfter.IsRunning(doAfterId)) + return; + + _doAfter.Cancel(doAfterId); + } +} diff --git a/Content.Shared/_Scp/Holding/SharedScpHoldingSystem.Events.cs b/Content.Shared/_Scp/Holding/SharedScpHoldingSystem.Events.cs index e7e1978ca6c..3157c9d4273 100644 --- a/Content.Shared/_Scp/Holding/SharedScpHoldingSystem.Events.cs +++ b/Content.Shared/_Scp/Holding/SharedScpHoldingSystem.Events.cs @@ -5,6 +5,7 @@ using Content.Shared.Movement.Events; using Content.Shared.Movement.Systems; using Content.Shared.Throwing; +using Robust.Shared.Maths; using Robust.Shared.Physics.Events; namespace Content.Shared._Scp.Holding; @@ -17,69 +18,46 @@ public abstract partial class SharedScpHoldingSystem private void SubscribeHoldingEvents() { - SubscribeLocalEvent(OnHoldShutdown); - SubscribeLocalEvent(OnHeldStartup); SubscribeLocalEvent(OnHeldShutdown); SubscribeLocalEvent(OnBreakoutAlert); SubscribeLocalEvent(OnBreakoutDoAfter); SubscribeLocalEvent(OnHeldMoveInput); - SubscribeLocalEvent(OnHandCountChanged); - SubscribeLocalEvent(OnHeldUpdateCanMove); SubscribeLocalEvent(OnHeldAttemptMobCollide); SubscribeLocalEvent(OnHeldAttemptMobTargetCollide); SubscribeLocalEvent(OnHeldCombatModeChanged); SubscribeLocalEvent(OnHeldPreventCollide); SubscribeLocalEvent(OnHoldRestrictedActionAttempt); + SubscribeLocalEvent(OnBreakoutAttemptStartup); + SubscribeLocalEvent(OnBreakoutAttemptShutdown); + SubscribeLocalEvent(OnFullHeldStartup); + SubscribeLocalEvent(OnFullHeldRemove); + SubscribeLocalEvent(OnFullHeldUpdateCanMove); SubscribeLocalEvent(OnHolderStartup); SubscribeLocalEvent(OnHolderShutdown); - SubscribeLocalEvent(OnHolderRefreshMoveSpeed); SubscribeLocalEvent(OnHolderBeforeThrow); SubscribeLocalEvent(OnHolderHandsModified); SubscribeLocalEvent(OnHolderPreventCollide); + SubscribeLocalEvent(OnHolderSlowdownRemove); + SubscribeLocalEvent(OnHolderSlowdownAfterState); + SubscribeLocalEvent(OnHolderSlowdownRefreshMoveSpeed); SubscribeLocalEvent(OnHolderBlockerDropped); } - private void OnHoldShutdown(Entity ent, ref ComponentShutdown args) - { - if (_net.IsClient) - return; - - if (!_holderQuery.TryComp(ent.Owner, out var holder)) - return; - - if (holder.Target == null) - return; - - ReleaseHolderContribution(ent.Owner, holder.Target.Value, clearIfEmpty: true); - } - private void OnHeldStartup(Entity ent, ref ComponentStartup args) { - RefreshHeldState(ent); + _alerts.ShowAlert(ent.Owner, "ScpHoldGrabbed"); + SyncHeldStatusEffect(ent.Owner); + EnsureCombatModeDisabled(ent.Owner); + OnHeldStateRefreshed(ent); } private void OnHeldShutdown(Entity ent, ref ComponentShutdown args) { _alerts.ClearAlert(ent.Owner, "ScpHoldGrabbed"); _statusEffects.TryRemoveStatusEffect(ent, GrabbedStatusEffect); - DeleteHeldHandBlockers(ent); - - if (!_timing.ApplyingState) - CancelBreakoutDoAfter(ent); - - if (!_net.IsClient) - { - foreach (var holderUid in ent.Comp.Holders) - { - if (_holderQuery.HasComp(holderUid)) - RemComp(holderUid); - } - } - - _actionBlocker.UpdateCanMove(ent.Owner); - _physics.UpdateIsPredicted(ent.Owner); + OnHeldStateShutdown(ent); } private void OnBreakoutAlert(Entity ent, ref ScpHoldBreakoutAlertEvent args) @@ -93,7 +71,7 @@ private void OnBreakoutAlert(Entity ent, ref ScpHoldBreakoutAl private void OnBreakoutDoAfter(Entity ent, ref ScpHoldBreakoutDoAfterEvent args) { - SetBreakoutDoAfterId(ent, null); + EndBreakoutAttempt(ent.Owner, cancelDoAfter: false); if (args.Handled) return; @@ -108,29 +86,33 @@ private void OnBreakoutDoAfter(Entity ent, ref ScpHoldBreakout args.Handled = true; } - private void OnHeldMoveInput(Entity ent, ref MoveInputEvent args) + private void OnBreakoutAttemptStartup(Entity ent, ref ComponentStartup args) { - if (!args.State) + if (!_heldQuery.TryComp(ent.Owner, out var held)) return; - TryBreakOut(ent, viaMovement: true); + ShowBreakoutAttemptFeedback((ent.Owner, held)); } - private void OnHandCountChanged(Entity ent, ref HandCountChangedEvent args) + private void OnBreakoutAttemptShutdown(Entity ent, ref ComponentShutdown args) { - if (_net.IsClient) + if (!_breakoutDoAfterIds.Remove(ent.Owner, out var doAfterId)) return; - SyncHeldState(ent); + CancelBreakoutAttemptDoAfter(doAfterId); } - private void OnHeldUpdateCanMove(Entity ent, ref UpdateCanMoveEvent args) + private void OnHeldMoveInput(Entity ent, ref MoveInputEvent args) { - if (ent.Comp.LifeStage > ComponentLifeStage.Running) + if (!IsBreakoutMovementPress(args)) return; - if (ent.Comp.FullHold) - args.Cancel(); + TryBreakOut(ent, viaMovement: true); + } + + private static void OnFullHeldUpdateCanMove(Entity ent, ref UpdateCanMoveEvent args) + { + args.Cancel(); } private static void OnHeldAttemptMobCollide(Entity ent, ref AttemptMobCollideEvent args) @@ -155,24 +137,50 @@ private void OnHeldPreventCollide(Entity ent, ref PreventColli } } + private void OnFullHeldStartup(Entity ent, ref ComponentStartup args) + { + if (_heldQuery.TryComp(ent.Owner, out var held)) + SyncPlaceholderHands((ent.Owner, held)); + + ZeroHeldVelocity(ent.Owner); + _actionBlocker.UpdateCanMove(ent.Owner); + } + + private void OnFullHeldRemove(Entity ent, ref ComponentRemove args) + { + DeleteHeldHandBlockers(ent.Owner); + _actionBlocker.UpdateCanMove(ent.Owner); + } + private void OnHolderStartup(Entity ent, ref ComponentStartup args) { - RefreshHolderState(ent); + SyncHolderState(ent); } private void OnHolderShutdown(Entity ent, ref ComponentShutdown args) { + var target = ent.Comp.Target; ent.Comp.Target = null; - ent.Comp.SlowdownEnabled = false; DeleteHolderHandBlockers(ent.Owner); + + if (!_timing.ApplyingState) + RemComp(ent.Owner); + + OnHolderStateShutdown(ent.Owner, target); + } + + private void OnHolderSlowdownRemove(Entity ent, ref ComponentRemove args) + { _movement.RefreshMovementSpeedModifiers(ent.Owner); } - private void OnHolderRefreshMoveSpeed(Entity ent, ref RefreshMovementSpeedModifiersEvent args) + private void OnHolderSlowdownAfterState(Entity ent, ref AfterAutoHandleStateEvent args) { - if (!ent.Comp.SlowdownEnabled) - return; + _movement.RefreshMovementSpeedModifiers(ent.Owner); + } + private void OnHolderSlowdownRefreshMoveSpeed(Entity ent, ref RefreshMovementSpeedModifiersEvent args) + { args.ModifySpeed(ent.Comp.WalkModifier, ent.Comp.SprintModifier); } @@ -202,7 +210,7 @@ private void OnHolderHandsModified(Entity ent, ref DidEquipH if (!_heldQuery.HasComp(ent.Comp.Target.Value)) return; - RefreshHolderState(ent); + SyncHolderState(ent); } private void OnHolderPreventCollide(Entity ent, ref PreventCollideEvent args) @@ -232,4 +240,24 @@ private void OnHolderBlockerDropped(Entity ent, ref ReleaseHolderContribution(args.User, ent.Comp.Target, clearIfEmpty: true); } + + private static bool IsBreakoutMovementPress(MoveInputEvent args) + { + if (!args.State) + return false; + + var pressedButton = args.Dir switch + { + Direction.East => MoveButtons.Right, + Direction.North => MoveButtons.Up, + Direction.West => MoveButtons.Left, + Direction.South => MoveButtons.Down, + _ => MoveButtons.None, + }; + + if (pressedButton == MoveButtons.None) + return false; + + return (args.OldMovement & pressedButton) == MoveButtons.None; + } } diff --git a/Content.Shared/_Scp/Holding/SharedScpHoldingSystem.Feedback.cs b/Content.Shared/_Scp/Holding/SharedScpHoldingSystem.Feedback.cs index ce0d91c147f..c9300b79b9d 100644 --- a/Content.Shared/_Scp/Holding/SharedScpHoldingSystem.Feedback.cs +++ b/Content.Shared/_Scp/Holding/SharedScpHoldingSystem.Feedback.cs @@ -1,45 +1,26 @@ using Content.Shared.Coordinates; using Content.Shared.Popups; -using Robust.Shared.Audio; using Robust.Shared.Audio.Systems; +using Robust.Shared.Audio; +using Robust.Shared.Prototypes; namespace Content.Shared._Scp.Holding; public abstract partial class SharedScpHoldingSystem { /* - * Feedback-local dependencies, breakout do-after tracking, and popup/audio helpers. + * Feedback-local dependencies plus popup/audio helpers. */ [Dependency] private readonly SharedAudioSystem _audio = default!; [Dependency] private readonly SharedPopupSystem _popup = default!; - private const string BreakoutAttemptEffect = "WhistleExclamation"; - private static readonly SoundSpecifier BreakoutAttemptSound = - new SoundCollectionSpecifier("storageRustle", - AudioParams.Default.WithVolume(-8f).WithMaxDistance(4f).WithVariation(0.15f)); - - private void CancelBreakoutDoAfter(Entity held) - { - if (held.Comp.BreakoutDoAfterId == null) - return; - - _doAfter.Cancel(held.Owner, held.Comp.BreakoutDoAfterId.Value); - SetBreakoutDoAfterId(held, null); - } - - private void SetBreakoutDoAfterId(Entity held, ushort? breakoutDoAfterId) + private void ShowBreakoutAttemptFeedback(Entity held) { - if (held.Comp.BreakoutDoAfterId == breakoutDoAfterId) + if (!CanShowBreakoutAttemptFeedback()) return; - held.Comp.BreakoutDoAfterId = breakoutDoAfterId; - DirtyHeldField(held, nameof(ScpHeldComponent.BreakoutDoAfterId)); - } - - private void ShowBreakoutAttemptFeedback(Entity held) - { - if (_net.IsClient && !_timing.IsFirstTimePredicted) + if (!TryComp(held.Owner, out var holdable)) return; foreach (var holderUid in held.Comp.Holders) @@ -50,15 +31,15 @@ private void ShowBreakoutAttemptFeedback(Entity held) if (holder.Target != held.Owner) continue; - SpawnBreakoutAttemptEffect(holderUid); + SpawnBreakoutAttemptEffect(holderUid, holdable.BreakoutAttemptEffect); } - PlayBreakoutAttemptSound(held.Owner); + PlayBreakoutAttemptSound(held.Owner, holdable.BreakoutAttemptSound); } private void PopupHolder(EntityUid holder, string key, params (string, object)[] args) { - if (_net.IsClient) + if (!ShouldShowHoldPopups) return; _popup.PopupEntity(Loc.GetString(key, args), holder, holder); @@ -66,31 +47,43 @@ private void PopupHolder(EntityUid holder, string key, params (string, object)[] private void PopupTarget(EntityUid target, string key, params (string, object)[] args) { - if (_net.IsClient) + if (!ShouldShowHoldPopups) return; _popup.PopupEntity(Loc.GetString(key, args), target, target); } - private void SpawnBreakoutAttemptEffect(EntityUid holderUid) + private void SpawnBreakoutAttemptEffect(EntityUid holderUid, EntProtoId? effect) { - if (_net.IsClient) + if (effect == null) + return; + + if (ShouldUsePredictedBreakoutFeedback) { - PredictedSpawnAttachedTo(BreakoutAttemptEffect, holderUid.ToCoordinates()); + PredictedSpawnAttachedTo(effect.Value, holderUid.ToCoordinates()); return; } - SpawnAttachedTo(BreakoutAttemptEffect, holderUid.ToCoordinates()); + SpawnAttachedTo(effect.Value, holderUid.ToCoordinates()); } - private void PlayBreakoutAttemptSound(EntityUid targetUid) + private void PlayBreakoutAttemptSound(EntityUid targetUid, SoundSpecifier? sound) { - if (_net.IsClient) + if (sound == null) + return; + + if (ShouldUsePredictedBreakoutFeedback) { - _audio.PlayPredicted(BreakoutAttemptSound, targetUid, targetUid); + _audio.PlayPredicted(sound, targetUid, targetUid); return; } - _audio.PlayPvs(BreakoutAttemptSound, targetUid); + _audio.PlayPvs(sound, targetUid); } + + protected virtual bool ShouldShowHoldPopups => false; + + protected virtual bool ShouldUsePredictedBreakoutFeedback => false; + + protected virtual bool CanShowBreakoutAttemptFeedback() => true; } diff --git a/Content.Shared/_Scp/Holding/SharedScpHoldingSystem.Hands.cs b/Content.Shared/_Scp/Holding/SharedScpHoldingSystem.Hands.cs index 04dc2cc447b..fe3886f8d8e 100644 --- a/Content.Shared/_Scp/Holding/SharedScpHoldingSystem.Hands.cs +++ b/Content.Shared/_Scp/Holding/SharedScpHoldingSystem.Hands.cs @@ -27,34 +27,110 @@ private void InitializeHandQueries() private void SyncPlaceholderHands(Entity held) { - DeleteHeldHandBlockers(held.Owner); + if (!_handsQuery.TryComp(held.Owner, out var hands)) + return; - if (!held.Comp.FullHold || !_handsQuery.TryComp(held.Owner, out var hands)) + if (!_fullHeldQuery.HasComp(held.Owner)) + { + DeleteHeldHandBlockers(held.Owner); + return; + } + + CollectPlaceholderIconHolders(held); + + if (_placeholderIcons.Count == 0) + { + DeleteHeldHandBlockers(held.Owner); return; + } + + var heldHands = new Entity(held.Owner, hands).AsNullable(); + DropHeldItemsForPlaceholders(heldHands); + DeleteInvalidHeldHandBlockers(heldHands); + EnsureHeldHandBlockers(heldHands); + } + + private void CollectPlaceholderIconHolders(Entity held) + { + _placeholderIcons.Clear(); - foreach (var hand in _hands.EnumerateHands((held.Owner, hands))) + foreach (var holderUid in held.Comp.Holders) + { + if (_holderQuery.TryComp(holderUid, out var holder) && + holder.Target == held.Owner) + { + _placeholderIcons.Add(holderUid); + } + } + } + + private void DropHeldItemsForPlaceholders(Entity held) + { + foreach (var hand in _hands.EnumerateHands(held)) { - if (!_hands.TryGetHeldItem((held.Owner, hands), hand, out var heldItem)) + if (!_hands.TryGetHeldItem(held, hand, out var heldItem)) continue; if (HasComp(heldItem.Value)) continue; - _hands.DoDrop((held.Owner, hands), hand, doDropInteraction: true); + _hands.DoDrop(held, hand, doDropInteraction: true); } + } - _placeholderIcons.Clear(); - foreach (var holderUid in held.Comp.Holders) + private void DeleteInvalidHeldHandBlockers(Entity held) + { + _virtualBlockersToDelete.Clear(); + + foreach (var heldItem in _hands.EnumerateHeld(held)) { - if (_holderQuery.TryComp(holderUid, out var holder) && holder.Target == held.Owner) - _placeholderIcons.Add(holderUid); + if (!TryComp(heldItem, out var virtualItem)) + continue; + + if (!TryComp(heldItem, out var blocker)) + continue; + + if (!TrySyncHeldHandBlocker((heldItem, blocker), virtualItem, held.Owner)) + { + _virtualBlockersToDelete.Add((heldItem, virtualItem)); + } } - if (_placeholderIcons.Count == 0) - return; + foreach (var virtualItem in _virtualBlockersToDelete) + { + _virtualItem.DeleteVirtualItem(virtualItem, held.Owner); + } + } + + private bool TrySyncHeldHandBlocker( + Entity blocker, + VirtualItemComponent virtualItem, + EntityUid heldUid) + { + if (!_placeholderIcons.Contains(virtualItem.BlockingEntity)) + return false; + + var dirtyTarget = blocker.Comp.Target != heldUid; + if (dirtyTarget) + { + blocker.Comp.Target = heldUid; + DirtyField(blocker.Owner, blocker.Comp, nameof(ScpHeldHandBlockerComponent.Target)); + } + var dirtyHolder = blocker.Comp.Holder != virtualItem.BlockingEntity; + if (dirtyHolder) + { + blocker.Comp.Holder = virtualItem.BlockingEntity; + DirtyField(blocker.Owner, blocker.Comp, nameof(ScpHeldHandBlockerComponent.Holder)); + } + + return true; + } + + private void EnsureHeldHandBlockers(Entity held) + { var iconIndex = 0; - while (_hands.TryGetEmptyHand((held.Owner, hands), out var emptyHand)) + while (_hands.TryGetEmptyHand(held, out var emptyHand)) { var holderUid = _placeholderIcons[iconIndex % _placeholderIcons.Count]; if (!_virtualItem.TrySpawnVirtualItemInHand(holderUid, held.Owner, out var virtualItem, empty: emptyHand, silent: true)) @@ -64,6 +140,7 @@ private void SyncPlaceholderHands(Entity held) var blocker = EnsureComp(virtualItem.Value); blocker.Target = held.Owner; blocker.Holder = holderUid; + Dirty(virtualItem.Value, blocker); iconIndex++; } @@ -110,7 +187,7 @@ private void SyncHolderHandBlocker(Entity holder) if (blocker.Target != currentTarget) { blocker.Target = currentTarget; - DirtyHandBlockerField((heldItem, blocker), nameof(ScpHoldHandBlockerComponent.Target)); + DirtyField(heldItem, blocker, nameof(ScpHoldHandBlockerComponent.Target)); } if (existingBlockerCreated) @@ -145,13 +222,10 @@ private void SyncHolderHandBlocker(Entity holder) if (!_virtualItem.TrySpawnVirtualItemInHand(holder.Comp.Target.Value, holder.Owner, out var spawnedVirtualItem, silent: true)) return; - var blockerCreated = !TryComp(spawnedVirtualItem.Value, out var blockerComp); + TryComp(spawnedVirtualItem.Value, out var blockerComp); blockerComp ??= EnsureComp(spawnedVirtualItem.Value); blockerComp.Target = holder.Comp.Target.Value; - DirtyHandBlockerField((spawnedVirtualItem.Value, blockerComp), nameof(ScpHoldHandBlockerComponent.Target)); - - if (blockerCreated) - Dirty(spawnedVirtualItem.Value, blockerComp); + Dirty(spawnedVirtualItem.Value, blockerComp); } private bool HasAvailableHolderHand(EntityUid holderUid) diff --git a/Content.Shared/_Scp/Holding/SharedScpHoldingSystem.Restrictions.cs b/Content.Shared/_Scp/Holding/SharedScpHoldingSystem.Restrictions.cs index c792229d10c..f1109e86a92 100644 --- a/Content.Shared/_Scp/Holding/SharedScpHoldingSystem.Restrictions.cs +++ b/Content.Shared/_Scp/Holding/SharedScpHoldingSystem.Restrictions.cs @@ -29,12 +29,12 @@ public bool IsHeldAtStage(EntityUid uid, ScpHoldStage stage) return _heldQuery.TryComp(uid, out var held) && IsHeldAtStage((uid, held), stage); } - private static bool IsHeldAtStage(Entity held, ScpHoldStage stage) + private bool IsHeldAtStage(Entity held, ScpHoldStage stage) { return stage switch { ScpHoldStage.Soft => true, - ScpHoldStage.Full => held.Comp.FullHold, + ScpHoldStage.Full => _fullHeldQuery.HasComp(held.Owner), _ => false, }; } diff --git a/Content.Shared/_Scp/Holding/SharedScpHoldingSystem.State.cs b/Content.Shared/_Scp/Holding/SharedScpHoldingSystem.State.cs index c72f9438e40..6cc18d40e2b 100644 --- a/Content.Shared/_Scp/Holding/SharedScpHoldingSystem.State.cs +++ b/Content.Shared/_Scp/Holding/SharedScpHoldingSystem.State.cs @@ -33,7 +33,7 @@ private void UpdateHeld(Entity held) var desiredSoftDragDistance = GetDesiredSoftDragDistance(held); var maintenanceRange = GetHoldMaintenanceRange(held.Comp.HoldRange, desiredSoftDragDistance); - if (!held.Comp.FullHold) + if (!_fullHeldQuery.HasComp(held.Owner)) UpdateSoftDrag(held, maintenanceRange, desiredSoftDragDistance); else ZeroHeldVelocity(held.Owner); @@ -64,7 +64,7 @@ private Entity EnsureHeldState(EntityUid target, ScpHoldableCo held ??= EnsureComp(target); if (created) - CopyConfig(config, held); + CopyConfig(target, config, held); held.RequiredHolderCount = GetRequiredHolderCount(target); return (target, held); @@ -75,20 +75,19 @@ private void AddHolderContribution(EntityUid holderUid, Entity if (!held.Comp.Holders.Contains(holderUid)) { held.Comp.Holders.Add(holderUid); - DirtyHeldField(held, nameof(ScpHeldComponent.Holders)); + Dirty(held); } var holderCreated = !_holderQuery.TryComp(holderUid, out var holder); holder ??= EnsureComp(holderUid); SetHolderTarget((holderUid, holder), held.Owner); - SetHolderSlowdown((holderUid, holder), false, held.Comp.WalkModifier, held.Comp.SprintModifier); - RefreshHolderState((holderUid, holder)); + SyncHolderState((holderUid, holder)); if (holderCreated) Dirty(holderUid, holder); } - private void ReleaseHolderContribution(EntityUid holderUid, EntityUid targetUid, bool clearIfEmpty) + protected void ReleaseHolderContribution(EntityUid holderUid, EntityUid targetUid, bool clearIfEmpty) { if (!_heldQuery.TryComp(targetUid, out var held)) return; @@ -104,10 +103,12 @@ private void ReleaseHolderContribution(EntityUid holderUid, EntityUid targetUid, } if (removed) - DirtyHeldField(targetUid, held, nameof(ScpHeldComponent.Holders)); + Dirty(targetUid, held); if (_holderQuery.HasComp(holderUid)) RemComp(holderUid); + else if (_holderSlowdownQuery.HasComp(holderUid)) + RemComp(holderUid); if (held.PrimaryHolder == holderUid) SetHeldPrimaryHolder((targetUid, held), null); @@ -122,7 +123,7 @@ private void ReleaseHolderContribution(EntityUid holderUid, EntityUid targetUid, SyncHeldState((targetUid, held)); } - private void SyncHeldState(Entity held) + protected void SyncHeldState(Entity held) { if (!_heldQuery.TryComp(held.Owner, out var heldComp)) return; @@ -158,37 +159,31 @@ private void SyncHeldState(Entity held) private void EnterFullHold(Entity held) { - if (!held.Comp.FullHold) + var fullHeldCreated = !_fullHeldQuery.TryComp(held.Owner, out var fullHeld); + fullHeld ??= EnsureComp(held.Owner); + + if (fullHeldCreated) { - held.Comp.FullHold = true; - held.Comp.FullHoldStartedAt = _timing.CurTime; - DirtyHeldFields( - held, - nameof(ScpHeldComponent.FullHold), - nameof(ScpHeldComponent.FullHoldStartedAt)); + fullHeld.StartedAt = _timing.CurTime; + Dirty(held.Owner, fullHeld); } UpdateHolderSlowdowns(held); + + if (fullHeldCreated) + return; + SyncPlaceholderHands(held); ZeroHeldVelocity(held.Owner); - _actionBlocker.UpdateCanMove(held.Owner); } private void ExitFullHold(Entity held) { - CancelBreakoutDoAfter(held); - - if (!held.Comp.FullHold && held.Comp.FullHoldStartedAt == null) + if (!_fullHeldQuery.HasComp(held.Owner)) return; - held.Comp.FullHold = false; - held.Comp.FullHoldStartedAt = null; - DirtyHeldFields( - held, - nameof(ScpHeldComponent.FullHold), - nameof(ScpHeldComponent.FullHoldStartedAt)); - SyncPlaceholderHands(held); - _actionBlocker.UpdateCanMove(held.Owner); + EndBreakoutAttempt(held.Owner, cancelDoAfter: true); + RemComp(held.Owner); } private bool EnsurePrimaryHolder(Entity held) @@ -218,9 +213,11 @@ private void ClearHoldState(Entity held, bool applyImmunity) if (_heldQuery.TryComp(held.Owner, out var refreshed)) held = (held.Owner, refreshed); - CancelBreakoutDoAfter(held); - DeleteHeldHandBlockers(held.Owner); - _actionBlocker.UpdateCanMove(held.Owner); + EndBreakoutAttempt(held.Owner, cancelDoAfter: true); + + if (_fullHeldQuery.HasComp(held.Owner)) + RemComp(held.Owner); + _holderCooldownsToApply.Clear(); foreach (var holderUid in held.Comp.Holders) @@ -230,6 +227,8 @@ private void ClearHoldState(Entity held, bool applyImmunity) if (_holderQuery.HasComp(holderUid)) RemComp(holderUid); + else if (_holderSlowdownQuery.HasComp(holderUid)) + RemComp(holderUid); } held.Comp.Holders.Clear(); @@ -237,13 +236,11 @@ private void ClearHoldState(Entity held, bool applyImmunity) if (applyImmunity) { - var immuneCreated = !TryComp(held.Owner, out var immune); - immune ??= EnsureComp(held.Owner); - immune.ExpiresAt = _timing.CurTime + held.Comp.PostBreakoutImmunity; - DirtyImmuneField((held.Owner, immune), nameof(ScpHoldImmuneComponent.ExpiresAt)); + if (!TryComp(held.Owner, out var immune)) + immune = EnsureComp(held.Owner); - if (immuneCreated) - Dirty(held.Owner, immune); + immune.ExpiresAt = _timing.CurTime + held.Comp.PostBreakoutImmunity; + Dirty(held.Owner, immune); } foreach (var holderUid in _holderCooldownsToApply) @@ -258,41 +255,26 @@ private void UpdateHolderSlowdowns(Entity held) { foreach (var holderUid in held.Comp.Holders) { - if (!_holderQuery.TryComp(holderUid, out var holder)) - continue; - - SetHolderSlowdown((holderUid, holder), true, held.Comp.WalkModifier, held.Comp.SprintModifier); + SetHolderSlowdown(holderUid, held.Comp.WalkModifier, held.Comp.SprintModifier); } } - private void SetHolderSlowdown(Entity holder, bool enabled, float walkModifier, float sprintModifier) + private void SetHolderSlowdown(EntityUid holderUid, float walkModifier, float sprintModifier) { - if (holder.Comp.SlowdownEnabled == enabled && - MathHelper.CloseTo(holder.Comp.WalkModifier, walkModifier) && - MathHelper.CloseTo(holder.Comp.SprintModifier, sprintModifier)) - { - return; - } + var slowdownCreated = !_holderSlowdownQuery.TryComp(holderUid, out var slowdown); + slowdown ??= EnsureComp(holderUid); - if (holder.Comp.SlowdownEnabled != enabled) + if (!slowdownCreated && + MathHelper.CloseTo(slowdown.WalkModifier, walkModifier) && + MathHelper.CloseTo(slowdown.SprintModifier, sprintModifier)) { - holder.Comp.SlowdownEnabled = enabled; - DirtyHolderField(holder, nameof(ScpHolderComponent.SlowdownEnabled)); - } - - if (!MathHelper.CloseTo(holder.Comp.WalkModifier, walkModifier)) - { - holder.Comp.WalkModifier = walkModifier; - DirtyHolderField(holder, nameof(ScpHolderComponent.WalkModifier)); - } - - if (!MathHelper.CloseTo(holder.Comp.SprintModifier, sprintModifier)) - { - holder.Comp.SprintModifier = sprintModifier; - DirtyHolderField(holder, nameof(ScpHolderComponent.SprintModifier)); + return; } - _movement.RefreshMovementSpeedModifiers(holder.Owner); + slowdown.WalkModifier = walkModifier; + slowdown.SprintModifier = sprintModifier; + Dirty(holderUid, slowdown); + _movement.RefreshMovementSpeedModifiers(holderUid); } private int GetRequiredHolderCount(EntityUid target) @@ -312,7 +294,7 @@ private int GetRequiredHolderCount(EntityUid target) return 2; } - private void CopyConfig(ScpHoldableComponent source, ScpHeldComponent target) + private void CopyConfig(EntityUid uid, ScpHoldableComponent source, ScpHeldComponent target) { target.SoftEscapeCooldown = source.SoftEscapeCooldown; target.FullHoldDelay = source.FullHoldDelay; @@ -322,7 +304,7 @@ private void CopyConfig(ScpHoldableComponent source, ScpHeldComponent target) target.WalkModifier = source.HolderWalkModifier; target.SprintModifier = source.HolderSprintModifier; target.SoftEscapeAvailableAt = _timing.CurTime; - target.FullHoldStartedAt = null; + Dirty(uid, target); } private bool ShouldReleaseHolder(EntityUid holderUid, EntityUid heldUid, float maintenanceRange) @@ -359,7 +341,7 @@ private void SetHolderTarget(Entity holder, EntityUid? targe return; holder.Comp.Target = target; - DirtyHolderField(holder, nameof(ScpHolderComponent.Target)); + Dirty(holder); } private void SetHeldPrimaryHolder(Entity held, EntityUid? primaryHolder) @@ -368,6 +350,6 @@ private void SetHeldPrimaryHolder(Entity held, EntityUid? prim return; held.Comp.PrimaryHolder = primaryHolder; - DirtyHeldField(held, nameof(ScpHeldComponent.PrimaryHolder)); + Dirty(held); } } diff --git a/Content.Shared/_Scp/Holding/SharedScpHoldingSystem.cs b/Content.Shared/_Scp/Holding/SharedScpHoldingSystem.cs index 68a69f5b9b7..14de1492c4c 100644 --- a/Content.Shared/_Scp/Holding/SharedScpHoldingSystem.cs +++ b/Content.Shared/_Scp/Holding/SharedScpHoldingSystem.cs @@ -6,7 +6,6 @@ using Content.Shared.StatusEffectNew; using Content.Shared.Whitelist; using Robust.Shared.Containers; -using Robust.Shared.Network; using Robust.Shared.Physics.Components; using Robust.Shared.Physics.Systems; using Robust.Shared.Prototypes; @@ -30,22 +29,29 @@ public abstract partial class SharedScpHoldingSystem : EntitySystem [Dependency] private readonly StatusEffectsSystem _statusEffects = default!; [Dependency] private readonly EntityWhitelistSystem _whitelist = default!; [Dependency] private readonly IGameTiming _timing = default!; - [Dependency] private readonly INetManager _net = default!; private static readonly EntProtoId GrabbedStatusEffect = "StatusEffectScpHeld"; + private readonly Dictionary _breakoutDoAfterIds = []; + + private EntityQuery _breakoutAttemptQuery; + private EntityQuery _fullHeldQuery; private EntityQuery _physicsQuery; private EntityQuery _heldQuery; private EntityQuery _holdQuery; private EntityQuery _holderQuery; + private EntityQuery _holderSlowdownQuery; public override void Initialize() { base.Initialize(); + _breakoutAttemptQuery = GetEntityQuery(); + _fullHeldQuery = GetEntityQuery(); _physicsQuery = GetEntityQuery(); _heldQuery = GetEntityQuery(); _holdQuery = GetEntityQuery(); _holderQuery = GetEntityQuery(); + _holderSlowdownQuery = GetEntityQuery(); InitializeHoldQueries(); InitializeHandQueries(); @@ -53,6 +59,12 @@ public override void Initialize() SubscribeHoldingEvents(); } + public override void Shutdown() + { + base.Shutdown(); + _breakoutDoAfterIds.Clear(); + } + public override void Update(float frameTime) { base.Update(frameTime); @@ -67,56 +79,31 @@ public override void Update(float frameTime) var heldQuery = EntityQueryEnumerator(); while (heldQuery.MoveNext(out var uid, out var held)) { - if (ShouldSkipHeldUpdate(uid)) + if (!ShouldUpdateHeld(uid, held)) continue; UpdateHeld((uid, held)); } } - private bool ShouldSkipHeldUpdate(EntityUid uid) - { - if (!_net.IsClient) - return false; - - if (!_physicsQuery.TryComp(uid, out var physics)) - return true; - - return !physics.Predict; - } - - private void DirtyHoldField(Entity holder, string fieldName) - { - DirtyField(holder.AsNullable(), fieldName); - } - - private void DirtyHeldField(Entity held, string fieldName) - { - Dirty(held); - } - - private void DirtyHeldField(EntityUid uid, ScpHeldComponent held, string fieldName) + protected virtual bool ShouldUpdateHeld(EntityUid uid, ScpHeldComponent held) { - Dirty(uid, held); + return true; } - private void DirtyHeldFields(Entity held, params string[] fieldNames) + protected virtual void OnHeldStateRefreshed(Entity held) { - Dirty(held); } - private void DirtyHolderField(Entity holder, string fieldName) + protected virtual void OnHeldStateShutdown(Entity held) { - Dirty(holder); } - private void DirtyImmuneField(Entity immune, string fieldName) + protected virtual void OnHolderStateRefreshed(Entity holder) { - Dirty(immune); } - private void DirtyHandBlockerField(Entity blocker, string fieldName) + protected virtual void OnHolderStateShutdown(EntityUid holderUid, EntityUid? target) { - Dirty(blocker); } } From a0c35b83ffab9a4f4c2f90cb02b6ba4224c75802 Mon Sep 17 00:00:00 2001 From: drdth Date: Fri, 17 Apr 2026 03:32:18 +0300 Subject: [PATCH 10/27] refactor: some minor refactors + separate systems from components --- .../_Scp/Holding/ScpHoldingSystem.cs | 3 +- .../_Scp/Holding/ScpHoldingSystem.cs | 3 +- .../PullingSystem.ScpHolding.cs | 3 +- .../ScpBreakoutAttemptComponent.cs | 3 +- .../{ => Components}/ScpFullHeldComponent.cs | 3 +- .../Holding/Components/ScpHeldComponent.cs | 37 +++++++++ .../ScpHeldHandBlockerComponent.cs | 3 +- .../{ => Components}/ScpHoldComponent.cs | 3 +- .../ScpHoldHandBlockerComponent.cs | 3 +- .../ScpHoldImmuneComponent.cs | 3 +- .../ScpHoldRestrictedComponent.cs | 2 +- .../{ => Components}/ScpHoldableComponent.cs | 11 +-- .../{ => Components}/ScpHolderComponent.cs | 3 +- .../ScpHolderSlowdownComponent.cs | 3 +- .../_Scp/Holding/ScpHeldComponent.cs | 78 ------------------- .../_Scp/Holding/ScpHoldingEvents.cs | 28 ++----- .../SharedScpHoldingSystem.Actions.cs | 38 +++++---- .../SharedScpHoldingSystem.BreakoutAttempt.cs | 3 +- .../SharedScpHoldingSystem.Drag.cs | 7 +- .../SharedScpHoldingSystem.Events.cs | 4 +- .../SharedScpHoldingSystem.Feedback.cs | 3 +- .../SharedScpHoldingSystem.Hands.cs | 3 +- .../SharedScpHoldingSystem.Restrictions.cs | 3 +- .../SharedScpHoldingSystem.State.cs | 65 +++++++++------- .../{ => Systems}/SharedScpHoldingSystem.cs | 3 +- 25 files changed, 146 insertions(+), 172 deletions(-) rename Content.Shared/_Scp/Holding/{ => Compatibility}/PullingSystem.ScpHolding.cs (96%) rename Content.Shared/_Scp/Holding/{ => Components}/ScpBreakoutAttemptComponent.cs (75%) rename Content.Shared/_Scp/Holding/{ => Components}/ScpFullHeldComponent.cs (87%) create mode 100644 Content.Shared/_Scp/Holding/Components/ScpHeldComponent.cs rename Content.Shared/_Scp/Holding/{ => Components}/ScpHeldHandBlockerComponent.cs (87%) rename Content.Shared/_Scp/Holding/{ => Components}/ScpHoldComponent.cs (92%) rename Content.Shared/_Scp/Holding/{ => Components}/ScpHoldHandBlockerComponent.cs (85%) rename Content.Shared/_Scp/Holding/{ => Components}/ScpHoldImmuneComponent.cs (87%) rename Content.Shared/_Scp/Holding/{ => Components}/ScpHoldRestrictedComponent.cs (80%) rename Content.Shared/_Scp/Holding/{ => Components}/ScpHoldableComponent.cs (88%) rename Content.Shared/_Scp/Holding/{ => Components}/ScpHolderComponent.cs (82%) rename Content.Shared/_Scp/Holding/{ => Components}/ScpHolderSlowdownComponent.cs (88%) delete mode 100644 Content.Shared/_Scp/Holding/ScpHeldComponent.cs rename Content.Shared/_Scp/Holding/{ => Systems}/SharedScpHoldingSystem.Actions.cs (94%) rename Content.Shared/_Scp/Holding/{ => Systems}/SharedScpHoldingSystem.BreakoutAttempt.cs (91%) rename Content.Shared/_Scp/Holding/{ => Systems}/SharedScpHoldingSystem.Drag.cs (96%) rename Content.Shared/_Scp/Holding/{ => Systems}/SharedScpHoldingSystem.Events.cs (99%) rename Content.Shared/_Scp/Holding/{ => Systems}/SharedScpHoldingSystem.Feedback.cs (96%) rename Content.Shared/_Scp/Holding/{ => Systems}/SharedScpHoldingSystem.Hands.cs (98%) rename Content.Shared/_Scp/Holding/{ => Systems}/SharedScpHoldingSystem.Restrictions.cs (94%) rename Content.Shared/_Scp/Holding/{ => Systems}/SharedScpHoldingSystem.State.cs (84%) rename Content.Shared/_Scp/Holding/{ => Systems}/SharedScpHoldingSystem.cs (97%) diff --git a/Content.Client/_Scp/Holding/ScpHoldingSystem.cs b/Content.Client/_Scp/Holding/ScpHoldingSystem.cs index ce1a418e203..8fb90899a8a 100644 --- a/Content.Client/_Scp/Holding/ScpHoldingSystem.cs +++ b/Content.Client/_Scp/Holding/ScpHoldingSystem.cs @@ -1,6 +1,7 @@ using Content.Client.Hands.Systems; using Content.Client.Inventory; -using Content.Shared._Scp.Holding; +using Content.Shared._Scp.Holding.Components; +using Content.Shared._Scp.Holding.Systems; using Content.Shared.Hands; using Content.Shared.Hands.Components; using Content.Shared.Inventory.VirtualItem; diff --git a/Content.Server/_Scp/Holding/ScpHoldingSystem.cs b/Content.Server/_Scp/Holding/ScpHoldingSystem.cs index 323b8206205..bb019367ef0 100644 --- a/Content.Server/_Scp/Holding/ScpHoldingSystem.cs +++ b/Content.Server/_Scp/Holding/ScpHoldingSystem.cs @@ -1,5 +1,6 @@ using Content.Shared.Hands; -using Content.Shared._Scp.Holding; +using Content.Shared._Scp.Holding.Components; +using Content.Shared._Scp.Holding.Systems; namespace Content.Server._Scp.Holding; diff --git a/Content.Shared/_Scp/Holding/PullingSystem.ScpHolding.cs b/Content.Shared/_Scp/Holding/Compatibility/PullingSystem.ScpHolding.cs similarity index 96% rename from Content.Shared/_Scp/Holding/PullingSystem.ScpHolding.cs rename to Content.Shared/_Scp/Holding/Compatibility/PullingSystem.ScpHolding.cs index ee78115692b..5fc332c8609 100644 --- a/Content.Shared/_Scp/Holding/PullingSystem.ScpHolding.cs +++ b/Content.Shared/_Scp/Holding/Compatibility/PullingSystem.ScpHolding.cs @@ -1,4 +1,5 @@ -using Content.Shared._Scp.Holding; +using Content.Shared._Scp.Holding.Components; +using Content.Shared._Scp.Holding.Systems; using Content.Shared.Movement.Pulling.Components; #pragma warning disable IDE0130 diff --git a/Content.Shared/_Scp/Holding/ScpBreakoutAttemptComponent.cs b/Content.Shared/_Scp/Holding/Components/ScpBreakoutAttemptComponent.cs similarity index 75% rename from Content.Shared/_Scp/Holding/ScpBreakoutAttemptComponent.cs rename to Content.Shared/_Scp/Holding/Components/ScpBreakoutAttemptComponent.cs index 4900b0f5ee0..557360f0f77 100644 --- a/Content.Shared/_Scp/Holding/ScpBreakoutAttemptComponent.cs +++ b/Content.Shared/_Scp/Holding/Components/ScpBreakoutAttemptComponent.cs @@ -1,6 +1,7 @@ +using Content.Shared._Scp.Holding.Systems; using Robust.Shared.GameStates; -namespace Content.Shared._Scp.Holding; +namespace Content.Shared._Scp.Holding.Components; /// /// Semantic state that marks an active breakout attempt during a full hold. diff --git a/Content.Shared/_Scp/Holding/ScpFullHeldComponent.cs b/Content.Shared/_Scp/Holding/Components/ScpFullHeldComponent.cs similarity index 87% rename from Content.Shared/_Scp/Holding/ScpFullHeldComponent.cs rename to Content.Shared/_Scp/Holding/Components/ScpFullHeldComponent.cs index 6b20d9606d2..cd20d639691 100644 --- a/Content.Shared/_Scp/Holding/ScpFullHeldComponent.cs +++ b/Content.Shared/_Scp/Holding/Components/ScpFullHeldComponent.cs @@ -1,7 +1,8 @@ +using Content.Shared._Scp.Holding.Systems; using Robust.Shared.GameStates; using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom; -namespace Content.Shared._Scp.Holding; +namespace Content.Shared._Scp.Holding.Components; /// /// Runtime full-hold state stored on a target while it is immobilized. diff --git a/Content.Shared/_Scp/Holding/Components/ScpHeldComponent.cs b/Content.Shared/_Scp/Holding/Components/ScpHeldComponent.cs new file mode 100644 index 00000000000..d5a9931b81e --- /dev/null +++ b/Content.Shared/_Scp/Holding/Components/ScpHeldComponent.cs @@ -0,0 +1,37 @@ +using Content.Shared._Scp.Holding.Systems; +using Robust.Shared.GameStates; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom; + +namespace Content.Shared._Scp.Holding.Components; + +/// +/// Runtime state stored on a target while at least one holder is contributing. +/// +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState(true), AutoGenerateComponentPause] +[Access(typeof(SharedScpHoldingSystem))] +public sealed partial class ScpHeldComponent : Component +{ + /// + /// Next timestamp when a soft breakout attempt may succeed. + /// + [DataField(customTypeSerializer: typeof(TimeOffsetSerializer)), AutoNetworkedField, AutoPausedField] + public TimeSpan SoftEscapeAvailableAt; + + /// + /// Ordered holder list used for reassignment and contribution counting. + /// + [AutoNetworkedField, ViewVariables] + public List Holders = []; + + /// + /// Current primary holder used as the soft hold drag anchor. + /// + [AutoNetworkedField, ViewVariables] + public EntityUid? PrimaryHolder; + + /// + /// Required contributor count for entering full hold. + /// + [AutoNetworkedField, ViewVariables] + public int RequiredHolderCount = 2; +} diff --git a/Content.Shared/_Scp/Holding/ScpHeldHandBlockerComponent.cs b/Content.Shared/_Scp/Holding/Components/ScpHeldHandBlockerComponent.cs similarity index 87% rename from Content.Shared/_Scp/Holding/ScpHeldHandBlockerComponent.cs rename to Content.Shared/_Scp/Holding/Components/ScpHeldHandBlockerComponent.cs index 9efd567c2c3..b2403651cfb 100644 --- a/Content.Shared/_Scp/Holding/ScpHeldHandBlockerComponent.cs +++ b/Content.Shared/_Scp/Holding/Components/ScpHeldHandBlockerComponent.cs @@ -1,6 +1,7 @@ +using Content.Shared._Scp.Holding.Systems; using Robust.Shared.GameStates; -namespace Content.Shared._Scp.Holding; +namespace Content.Shared._Scp.Holding.Components; /// /// Marks a victim hand placeholder virtual item created by SCP holding. diff --git a/Content.Shared/_Scp/Holding/ScpHoldComponent.cs b/Content.Shared/_Scp/Holding/Components/ScpHoldComponent.cs similarity index 92% rename from Content.Shared/_Scp/Holding/ScpHoldComponent.cs rename to Content.Shared/_Scp/Holding/Components/ScpHoldComponent.cs index 1b5e56bf91e..aa7501d0532 100644 --- a/Content.Shared/_Scp/Holding/ScpHoldComponent.cs +++ b/Content.Shared/_Scp/Holding/Components/ScpHoldComponent.cs @@ -1,7 +1,8 @@ +using Content.Shared._Scp.Holding.Systems; using Content.Shared.Whitelist; using Robust.Shared.GameStates; -namespace Content.Shared._Scp.Holding; +namespace Content.Shared._Scp.Holding.Components; /// /// Grants the owner the ability to contribute to SCP holding. diff --git a/Content.Shared/_Scp/Holding/ScpHoldHandBlockerComponent.cs b/Content.Shared/_Scp/Holding/Components/ScpHoldHandBlockerComponent.cs similarity index 85% rename from Content.Shared/_Scp/Holding/ScpHoldHandBlockerComponent.cs rename to Content.Shared/_Scp/Holding/Components/ScpHoldHandBlockerComponent.cs index 85c2d67b321..b8b8ac6165e 100644 --- a/Content.Shared/_Scp/Holding/ScpHoldHandBlockerComponent.cs +++ b/Content.Shared/_Scp/Holding/Components/ScpHoldHandBlockerComponent.cs @@ -1,6 +1,7 @@ +using Content.Shared._Scp.Holding.Systems; using Robust.Shared.GameStates; -namespace Content.Shared._Scp.Holding; +namespace Content.Shared._Scp.Holding.Components; /// /// Marks a virtual item that reserves one holder hand for an active SCP hold. diff --git a/Content.Shared/_Scp/Holding/ScpHoldImmuneComponent.cs b/Content.Shared/_Scp/Holding/Components/ScpHoldImmuneComponent.cs similarity index 87% rename from Content.Shared/_Scp/Holding/ScpHoldImmuneComponent.cs rename to Content.Shared/_Scp/Holding/Components/ScpHoldImmuneComponent.cs index fc603688b31..b2b0035a066 100644 --- a/Content.Shared/_Scp/Holding/ScpHoldImmuneComponent.cs +++ b/Content.Shared/_Scp/Holding/Components/ScpHoldImmuneComponent.cs @@ -1,7 +1,8 @@ +using Content.Shared._Scp.Holding.Systems; using Robust.Shared.GameStates; using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom; -namespace Content.Shared._Scp.Holding; +namespace Content.Shared._Scp.Holding.Components; /// /// Prevents the target from being held again for a short period after a successful full breakout. diff --git a/Content.Shared/_Scp/Holding/ScpHoldRestrictedComponent.cs b/Content.Shared/_Scp/Holding/Components/ScpHoldRestrictedComponent.cs similarity index 80% rename from Content.Shared/_Scp/Holding/ScpHoldRestrictedComponent.cs rename to Content.Shared/_Scp/Holding/Components/ScpHoldRestrictedComponent.cs index bbd0c66cbad..a4ea1ecd808 100644 --- a/Content.Shared/_Scp/Holding/ScpHoldRestrictedComponent.cs +++ b/Content.Shared/_Scp/Holding/Components/ScpHoldRestrictedComponent.cs @@ -1,6 +1,6 @@ using Robust.Shared.GameStates; -namespace Content.Shared._Scp.Holding; +namespace Content.Shared._Scp.Holding.Components; [RegisterComponent, NetworkedComponent] public sealed partial class ScpHoldRestrictedComponent : Component diff --git a/Content.Shared/_Scp/Holding/ScpHoldableComponent.cs b/Content.Shared/_Scp/Holding/Components/ScpHoldableComponent.cs similarity index 88% rename from Content.Shared/_Scp/Holding/ScpHoldableComponent.cs rename to Content.Shared/_Scp/Holding/Components/ScpHoldableComponent.cs index 765e971bfdc..dd29580f822 100644 --- a/Content.Shared/_Scp/Holding/ScpHoldableComponent.cs +++ b/Content.Shared/_Scp/Holding/Components/ScpHoldableComponent.cs @@ -1,14 +1,13 @@ using Content.Shared.Whitelist; using Robust.Shared.Audio; -using Robust.Shared.GameStates; using Robust.Shared.Prototypes; -namespace Content.Shared._Scp.Holding; +namespace Content.Shared._Scp.Holding.Components; /// /// Marks an entity as a valid target for the SCP holding mechanic and stores per-target hold tuning. /// -[RegisterComponent, NetworkedComponent] +[RegisterComponent] public sealed partial class ScpHoldableComponent : Component { /// @@ -23,12 +22,6 @@ public sealed partial class ScpHoldableComponent : Component [DataField] public EntityWhitelist? HolderBlacklist; - /// - /// Minimum delay between successful soft breakout attempts while the hold is active. - /// - [DataField] - public TimeSpan SoftEscapeCooldown = TimeSpan.FromSeconds(1); - /// /// Minimum uninterrupted full hold duration before a breakout do-after may start. /// diff --git a/Content.Shared/_Scp/Holding/ScpHolderComponent.cs b/Content.Shared/_Scp/Holding/Components/ScpHolderComponent.cs similarity index 82% rename from Content.Shared/_Scp/Holding/ScpHolderComponent.cs rename to Content.Shared/_Scp/Holding/Components/ScpHolderComponent.cs index a33f1b572ef..8744c019cc1 100644 --- a/Content.Shared/_Scp/Holding/ScpHolderComponent.cs +++ b/Content.Shared/_Scp/Holding/Components/ScpHolderComponent.cs @@ -1,6 +1,7 @@ +using Content.Shared._Scp.Holding.Systems; using Robust.Shared.GameStates; -namespace Content.Shared._Scp.Holding; +namespace Content.Shared._Scp.Holding.Components; /// /// Runtime contribution state stored on each active holder. diff --git a/Content.Shared/_Scp/Holding/ScpHolderSlowdownComponent.cs b/Content.Shared/_Scp/Holding/Components/ScpHolderSlowdownComponent.cs similarity index 88% rename from Content.Shared/_Scp/Holding/ScpHolderSlowdownComponent.cs rename to Content.Shared/_Scp/Holding/Components/ScpHolderSlowdownComponent.cs index 59980516ba0..f411632cdf8 100644 --- a/Content.Shared/_Scp/Holding/ScpHolderSlowdownComponent.cs +++ b/Content.Shared/_Scp/Holding/Components/ScpHolderSlowdownComponent.cs @@ -1,6 +1,7 @@ +using Content.Shared._Scp.Holding.Systems; using Robust.Shared.GameStates; -namespace Content.Shared._Scp.Holding; +namespace Content.Shared._Scp.Holding.Components; /// /// Runtime slowdown state stored on an active holder while their movement is penalized. diff --git a/Content.Shared/_Scp/Holding/ScpHeldComponent.cs b/Content.Shared/_Scp/Holding/ScpHeldComponent.cs deleted file mode 100644 index ff99fcf9bd6..00000000000 --- a/Content.Shared/_Scp/Holding/ScpHeldComponent.cs +++ /dev/null @@ -1,78 +0,0 @@ -using Robust.Shared.GameStates; -using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom; - -namespace Content.Shared._Scp.Holding; - -/// -/// Runtime state stored on a target while at least one holder is contributing. -/// -[RegisterComponent, NetworkedComponent, AutoGenerateComponentState(true), AutoGenerateComponentPause] -[Access(typeof(SharedScpHoldingSystem))] -public sealed partial class ScpHeldComponent : Component -{ - /// - /// Next timestamp when a soft breakout attempt may succeed. - /// - [DataField(customTypeSerializer: typeof(TimeOffsetSerializer)), AutoNetworkedField, AutoPausedField] - public TimeSpan SoftEscapeAvailableAt; - - /// - /// Ordered holder list used for reassignment and contribution counting. - /// - [AutoNetworkedField, ViewVariables] - public List Holders = []; - - /// - /// Current primary holder used as the soft hold drag anchor. - /// - [AutoNetworkedField, ViewVariables] - public EntityUid? PrimaryHolder; - - /// - /// Required contributor count for entering full hold. - /// - [AutoNetworkedField, ViewVariables] - public int RequiredHolderCount = 2; - - /// - /// Copied soft breakout cooldown configuration from the initial holdable target. - /// - /// Leave it unused for some time for balance reasons - public TimeSpan SoftEscapeCooldown = TimeSpan.FromSeconds(1); - - /// - /// Copied full hold delay configuration from the initial holdable target. - /// - [AutoNetworkedField, ViewVariables] - public TimeSpan FullHoldDelay = TimeSpan.FromSeconds(10); - - /// - /// Copied full breakout duration configuration from the initial holdable target. - /// - [AutoNetworkedField, ViewVariables] - public TimeSpan FullBreakoutDuration = TimeSpan.FromSeconds(5); - - /// - /// Copied post-breakout immunity duration from the initial holdable target. - /// - [AutoNetworkedField, ViewVariables] - public TimeSpan PostBreakoutImmunity = TimeSpan.FromSeconds(5); - - /// - /// Copied maximum hold range from the initial holdable target. - /// - [AutoNetworkedField, ViewVariables] - public float HoldRange = 1f; - - /// - /// Copied walk slowdown applied through . - /// - [AutoNetworkedField, ViewVariables] - public float WalkModifier = 0.5f; - - /// - /// Copied sprint slowdown applied through . - /// - [AutoNetworkedField, ViewVariables] - public float SprintModifier = 0.5f; -} diff --git a/Content.Shared/_Scp/Holding/ScpHoldingEvents.cs b/Content.Shared/_Scp/Holding/ScpHoldingEvents.cs index 41d4e815d98..97d24eba5a2 100644 --- a/Content.Shared/_Scp/Holding/ScpHoldingEvents.cs +++ b/Content.Shared/_Scp/Holding/ScpHoldingEvents.cs @@ -1,36 +1,22 @@ using Content.Shared.Alert; using Content.Shared.DoAfter; -using Robust.Shared.GameObjects; using Robust.Shared.Serialization; namespace Content.Shared._Scp.Holding; public sealed partial class ScpHoldBreakoutAlertEvent : BaseAlertEvent; -public sealed partial class ScpHoldAttemptEvent(EntityUid holder, EntityUid target) : CancellableEntityEventArgs +[ByRefEvent] +public record struct ScpHoldAttemptEvent(EntityUid Holder, EntityUid Target) { - public EntityUid Holder { get; } = holder; - public EntityUid Target { get; } = target; + public bool Cancelled; } -public sealed partial class ScpHoldBreakoutEvent(bool viaMovement, bool wasFullHold, bool appliedImmunity) : EntityEventArgs -{ - public bool ViaMovement { get; } = viaMovement; - public bool WasFullHold { get; } = wasFullHold; - public bool AppliedImmunity { get; } = appliedImmunity; -} +[ByRefEvent] +public readonly record struct ScpHoldBreakoutEvent(bool ViaMovement, bool WasFullHold, bool AppliedImmunity); [Serializable, NetSerializable] -public sealed partial class ScpHoldBreakoutDoAfterEvent : SimpleDoAfterEvent +public sealed partial class ScpHoldBreakoutDoAfterEvent(bool viaMovement = false) : SimpleDoAfterEvent { - public bool ViaMovement; - - public ScpHoldBreakoutDoAfterEvent() - { - } - - public ScpHoldBreakoutDoAfterEvent(bool viaMovement) - { - ViaMovement = viaMovement; - } + public bool ViaMovement = viaMovement; } diff --git a/Content.Shared/_Scp/Holding/SharedScpHoldingSystem.Actions.cs b/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.Actions.cs similarity index 94% rename from Content.Shared/_Scp/Holding/SharedScpHoldingSystem.Actions.cs rename to Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.Actions.cs index 0f5fa11d7b8..cee98d6d889 100644 --- a/Content.Shared/_Scp/Holding/SharedScpHoldingSystem.Actions.cs +++ b/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.Actions.cs @@ -1,8 +1,9 @@ +using Content.Shared._Scp.Holding.Components; using Content.Shared.DoAfter; using Content.Shared.Movement.Components; using Robust.Shared.Physics; -namespace Content.Shared._Scp.Holding; +namespace Content.Shared._Scp.Holding.Systems; public abstract partial class SharedScpHoldingSystem { @@ -39,14 +40,10 @@ public bool TryToggleHold(Entity holder, EntityUid target, boo if (!CanToggleHold(holder, target, checkAttempt: !attemptChecked)) return false; - var holdable = _holdableQuery.Comp(target); - var held = EnsureHeldState(target, holdable, out var heldCreated); + var held = EnsureHeldState(target); AddHolderContribution(holder.Owner, held); SyncHeldState(held); - if (heldCreated) - Dirty(held); - StartHoldCooldown(holder); return true; } @@ -68,6 +65,7 @@ public bool CanToggleHold( { if (!quiet) PopupHolder(holder.Owner, "scp-hold-target-not-holdable", ("target", target)); + return false; } @@ -75,6 +73,7 @@ public bool CanToggleHold( { if (!quiet) PopupHolder(holder.Owner, "scp-hold-target-invalid", ("target", target)); + return false; } @@ -82,6 +81,7 @@ public bool CanToggleHold( { if (!quiet) PopupHolder(holder.Owner, "scp-hold-target-invalid", ("target", target)); + return false; } @@ -89,6 +89,7 @@ public bool CanToggleHold( { if (!quiet) PopupHolder(holder.Owner, "scp-hold-target-invalid", ("target", target)); + return false; } @@ -96,6 +97,7 @@ public bool CanToggleHold( { if (!quiet) PopupHolder(holder.Owner, "scp-hold-target-invalid", ("target", target)); + return false; } @@ -103,6 +105,7 @@ public bool CanToggleHold( { if (!quiet) PopupHolder(holder.Owner, "scp-hold-target-invalid", ("target", target)); + return false; } @@ -110,6 +113,7 @@ public bool CanToggleHold( { if (!quiet) PopupHolder(holder.Owner, "scp-hold-target-invalid", ("target", target)); + return false; } @@ -117,6 +121,7 @@ public bool CanToggleHold( { if (!quiet) PopupHolder(holder.Owner, "scp-hold-target-invalid", ("target", target)); + return false; } @@ -124,6 +129,7 @@ public bool CanToggleHold( { if (!quiet) PopupHolder(holder.Owner, "scp-hold-target-immune", ("target", target)); + return false; } @@ -131,18 +137,18 @@ public bool CanToggleHold( { if (!quiet) PopupHolder(holder.Owner, "scp-hold-holder-no-free-hand", ("target", target)); + return false; } var range = holdable.HoldRange; - if (_heldQuery.TryComp(target, out var held)) + if (_heldQuery.HasComp(target)) { - range = held.HoldRange; - if (_fullHeldQuery.HasComp(target)) { if (!quiet) PopupHolder(holder.Owner, "scp-hold-target-fully-held", ("target", target)); + return false; } } @@ -151,6 +157,7 @@ public bool CanToggleHold( { if (!quiet) PopupHolder(holder.Owner, "scp-hold-target-too-far", ("target", target)); + return false; } @@ -212,13 +219,16 @@ private bool TryStartFullBreakout(Entity held, bool viaMovemen if (!_fullHeldQuery.TryComp(held.Owner, out var fullHeld)) return false; + if (!TryGetHeldHoldable(held, out var holdable)) + return false; + if (fullHeld.StartedAt == TimeSpan.Zero) { PopupTarget(held.Owner, "scp-hold-breakout-too-early", ("seconds", 1)); return false; } - var breakoutAvailableAt = fullHeld.StartedAt + held.Comp.FullHoldDelay; + var breakoutAvailableAt = fullHeld.StartedAt + holdable.FullHoldDelay; if (_timing.CurTime < breakoutAvailableAt) { var remaining = breakoutAvailableAt - _timing.CurTime; @@ -233,7 +243,7 @@ private bool TryStartFullBreakout(Entity held, bool viaMovemen var doAfter = new DoAfterArgs( EntityManager, held.Owner, - held.Comp.FullBreakoutDuration, + holdable.FullBreakoutDuration, new ScpHoldBreakoutDoAfterEvent(viaMovement), held.Owner, target: held.Owner) @@ -315,15 +325,15 @@ private void SetHoldAvailableAt(Entity holder, TimeSpan? holdA private bool CanPassHoldAttempt(EntityUid holderUid, EntityUid targetUid) { var attempt = new ScpHoldAttemptEvent(holderUid, targetUid); - RaiseLocalEvent(targetUid, attempt); - RaiseLocalEvent(holderUid, attempt); + RaiseLocalEvent(targetUid, ref attempt); + RaiseLocalEvent(holderUid, ref attempt); return !attempt.Cancelled; } private void RaiseBreakoutEvent(Entity held, bool viaMovement, bool applyImmunity) { var ev = new ScpHoldBreakoutEvent(viaMovement, _fullHeldQuery.HasComp(held.Owner), applyImmunity); - RaiseLocalEvent(held.Owner, ev); + RaiseLocalEvent(held.Owner, ref ev); } private void BreakOut(Entity held, bool viaMovement, bool applyImmunity) diff --git a/Content.Shared/_Scp/Holding/SharedScpHoldingSystem.BreakoutAttempt.cs b/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.BreakoutAttempt.cs similarity index 91% rename from Content.Shared/_Scp/Holding/SharedScpHoldingSystem.BreakoutAttempt.cs rename to Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.BreakoutAttempt.cs index e26a323b5bb..ca143b7d75a 100644 --- a/Content.Shared/_Scp/Holding/SharedScpHoldingSystem.BreakoutAttempt.cs +++ b/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.BreakoutAttempt.cs @@ -1,6 +1,7 @@ +using Content.Shared._Scp.Holding.Components; using Content.Shared.DoAfter; -namespace Content.Shared._Scp.Holding; +namespace Content.Shared._Scp.Holding.Systems; public abstract partial class SharedScpHoldingSystem { diff --git a/Content.Shared/_Scp/Holding/SharedScpHoldingSystem.Drag.cs b/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.Drag.cs similarity index 96% rename from Content.Shared/_Scp/Holding/SharedScpHoldingSystem.Drag.cs rename to Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.Drag.cs index 958e894dc1f..9d3c8c78122 100644 --- a/Content.Shared/_Scp/Holding/SharedScpHoldingSystem.Drag.cs +++ b/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.Drag.cs @@ -1,8 +1,9 @@ using System.Numerics; +using Content.Shared._Scp.Holding.Components; using Content.Shared.Interaction; using Robust.Shared.Physics.Components; -namespace Content.Shared._Scp.Holding; +namespace Content.Shared._Scp.Holding.Systems; public abstract partial class SharedScpHoldingSystem { @@ -82,9 +83,9 @@ private void UpdateSoftDrag(Entity held, float maintenanceRang ApplyHeldVelocity(held.Owner, desiredVelocity, heldPhysics); } - private float GetDesiredSoftDragDistance(Entity held) + private static float GetDesiredSoftDragDistance(float holdRange) { - return GetBaseSoftDragDistance(held.Comp.HoldRange); + return GetBaseSoftDragDistance(holdRange); } private static float GetHoldMaintenanceRange(float configuredRange, float desiredSoftDragDistance) diff --git a/Content.Shared/_Scp/Holding/SharedScpHoldingSystem.Events.cs b/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.Events.cs similarity index 99% rename from Content.Shared/_Scp/Holding/SharedScpHoldingSystem.Events.cs rename to Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.Events.cs index 3157c9d4273..4e505b689bd 100644 --- a/Content.Shared/_Scp/Holding/SharedScpHoldingSystem.Events.cs +++ b/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.Events.cs @@ -1,3 +1,4 @@ +using Content.Shared._Scp.Holding.Components; using Content.Shared.Actions.Events; using Content.Shared.CombatMode; using Content.Shared.Hands; @@ -5,10 +6,9 @@ using Content.Shared.Movement.Events; using Content.Shared.Movement.Systems; using Content.Shared.Throwing; -using Robust.Shared.Maths; using Robust.Shared.Physics.Events; -namespace Content.Shared._Scp.Holding; +namespace Content.Shared._Scp.Holding.Systems; public abstract partial class SharedScpHoldingSystem { diff --git a/Content.Shared/_Scp/Holding/SharedScpHoldingSystem.Feedback.cs b/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.Feedback.cs similarity index 96% rename from Content.Shared/_Scp/Holding/SharedScpHoldingSystem.Feedback.cs rename to Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.Feedback.cs index c9300b79b9d..b9a2a9b2f09 100644 --- a/Content.Shared/_Scp/Holding/SharedScpHoldingSystem.Feedback.cs +++ b/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.Feedback.cs @@ -1,10 +1,11 @@ +using Content.Shared._Scp.Holding.Components; using Content.Shared.Coordinates; using Content.Shared.Popups; using Robust.Shared.Audio.Systems; using Robust.Shared.Audio; using Robust.Shared.Prototypes; -namespace Content.Shared._Scp.Holding; +namespace Content.Shared._Scp.Holding.Systems; public abstract partial class SharedScpHoldingSystem { diff --git a/Content.Shared/_Scp/Holding/SharedScpHoldingSystem.Hands.cs b/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.Hands.cs similarity index 98% rename from Content.Shared/_Scp/Holding/SharedScpHoldingSystem.Hands.cs rename to Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.Hands.cs index fe3886f8d8e..050600c19c6 100644 --- a/Content.Shared/_Scp/Holding/SharedScpHoldingSystem.Hands.cs +++ b/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.Hands.cs @@ -1,10 +1,11 @@ +using Content.Shared._Scp.Holding.Components; using Content.Shared.Hands.Components; using Content.Shared.Hands.EntitySystems; using Content.Shared.Interaction.Components; using Content.Shared.Inventory.VirtualItem; using Content.Shared.StatusEffectNew.Components; -namespace Content.Shared._Scp.Holding; +namespace Content.Shared._Scp.Holding.Systems; public abstract partial class SharedScpHoldingSystem { diff --git a/Content.Shared/_Scp/Holding/SharedScpHoldingSystem.Restrictions.cs b/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.Restrictions.cs similarity index 94% rename from Content.Shared/_Scp/Holding/SharedScpHoldingSystem.Restrictions.cs rename to Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.Restrictions.cs index f1109e86a92..367cf12b98e 100644 --- a/Content.Shared/_Scp/Holding/SharedScpHoldingSystem.Restrictions.cs +++ b/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.Restrictions.cs @@ -1,7 +1,8 @@ +using Content.Shared._Scp.Holding.Components; using Content.Shared.Actions.Events; using Content.Shared.CombatMode; -namespace Content.Shared._Scp.Holding; +namespace Content.Shared._Scp.Holding.Systems; public abstract partial class SharedScpHoldingSystem { diff --git a/Content.Shared/_Scp/Holding/SharedScpHoldingSystem.State.cs b/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.State.cs similarity index 84% rename from Content.Shared/_Scp/Holding/SharedScpHoldingSystem.State.cs rename to Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.State.cs index 6cc18d40e2b..f6842b528f0 100644 --- a/Content.Shared/_Scp/Holding/SharedScpHoldingSystem.State.cs +++ b/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.State.cs @@ -1,8 +1,10 @@ +using System.Diagnostics.CodeAnalysis; +using Content.Shared._Scp.Holding.Components; using Content.Shared.Body.Components; using Content.Shared.Body.Part; using Content.Shared.Body.Systems; -namespace Content.Shared._Scp.Holding; +namespace Content.Shared._Scp.Holding.Systems; public abstract partial class SharedScpHoldingSystem { @@ -24,14 +26,17 @@ private void InitializeStateQueries() private void UpdateHeld(Entity held) { + if (!TryGetHeldHoldable(held, out var holdable)) + return; + if (!EnsurePrimaryHolder(held)) { ClearHoldState(held, applyImmunity: false); return; } - var desiredSoftDragDistance = GetDesiredSoftDragDistance(held); - var maintenanceRange = GetHoldMaintenanceRange(held.Comp.HoldRange, desiredSoftDragDistance); + var desiredSoftDragDistance = GetDesiredSoftDragDistance(holdable.HoldRange); + var maintenanceRange = GetHoldMaintenanceRange(holdable.HoldRange, desiredSoftDragDistance); if (!_fullHeldQuery.HasComp(held.Owner)) UpdateSoftDrag(held, maintenanceRange, desiredSoftDragDistance); @@ -58,13 +63,13 @@ private void UpdateHeld(Entity held) SyncHeldState((held.Owner, refreshed)); } - private Entity EnsureHeldState(EntityUid target, ScpHoldableComponent config, out bool created) + private Entity EnsureHeldState(EntityUid target) { - created = !_heldQuery.TryComp(target, out var held); + var created = !_heldQuery.TryComp(target, out var held); held ??= EnsureComp(target); if (created) - CopyConfig(target, config, held); + held.SoftEscapeAvailableAt = _timing.CurTime; held.RequiredHolderCount = GetRequiredHolderCount(target); return (target, held); @@ -129,6 +134,10 @@ protected void SyncHeldState(Entity held) return; held.Comp = heldComp; + + if (!TryGetHeldHoldable(held, out var holdable)) + return; + held.Comp.RequiredHolderCount = GetRequiredHolderCount(held.Owner); if (held.Comp.Holders.Count == 0) @@ -145,19 +154,19 @@ protected void SyncHeldState(Entity held) if (held.Comp.Holders.Count >= held.Comp.RequiredHolderCount) { - EnterFullHold(held); + EnterFullHold(held, holdable); return; } ExitFullHold(held); - var desiredSoftDragDistance = GetDesiredSoftDragDistance(held); - var maintenanceRange = GetHoldMaintenanceRange(held.Comp.HoldRange, desiredSoftDragDistance); + var desiredSoftDragDistance = GetDesiredSoftDragDistance(holdable.HoldRange); + var maintenanceRange = GetHoldMaintenanceRange(holdable.HoldRange, desiredSoftDragDistance); UpdateSoftDrag(held, maintenanceRange, desiredSoftDragDistance); - UpdateHolderSlowdowns(held); + UpdateHolderSlowdowns(held, holdable); SyncPlaceholderHands(held); } - private void EnterFullHold(Entity held) + private void EnterFullHold(Entity held, ScpHoldableComponent holdable) { var fullHeldCreated = !_fullHeldQuery.TryComp(held.Owner, out var fullHeld); fullHeld ??= EnsureComp(held.Owner); @@ -168,7 +177,7 @@ private void EnterFullHold(Entity held) Dirty(held.Owner, fullHeld); } - UpdateHolderSlowdowns(held); + UpdateHolderSlowdowns(held, holdable); if (fullHeldCreated) return; @@ -236,11 +245,14 @@ private void ClearHoldState(Entity held, bool applyImmunity) if (applyImmunity) { - if (!TryComp(held.Owner, out var immune)) - immune = EnsureComp(held.Owner); + if (_holdableQuery.TryComp(held.Owner, out var holdable)) + { + if (!TryComp(held.Owner, out var immune)) + immune = EnsureComp(held.Owner); - immune.ExpiresAt = _timing.CurTime + held.Comp.PostBreakoutImmunity; - Dirty(held.Owner, immune); + immune.ExpiresAt = _timing.CurTime + holdable.PostBreakoutImmunity; + Dirty(held.Owner, immune); + } } foreach (var holderUid in _holderCooldownsToApply) @@ -251,11 +263,11 @@ private void ClearHoldState(Entity held, bool applyImmunity) RemComp(held.Owner); } - private void UpdateHolderSlowdowns(Entity held) + private void UpdateHolderSlowdowns(Entity held, ScpHoldableComponent holdable) { foreach (var holderUid in held.Comp.Holders) { - SetHolderSlowdown(holderUid, held.Comp.WalkModifier, held.Comp.SprintModifier); + SetHolderSlowdown(holderUid, holdable.HolderWalkModifier, holdable.HolderSprintModifier); } } @@ -294,17 +306,14 @@ private int GetRequiredHolderCount(EntityUid target) return 2; } - private void CopyConfig(EntityUid uid, ScpHoldableComponent source, ScpHeldComponent target) + private bool TryGetHeldHoldable(Entity held, [NotNullWhen(true)] out ScpHoldableComponent? holdable) { - target.SoftEscapeCooldown = source.SoftEscapeCooldown; - target.FullHoldDelay = source.FullHoldDelay; - target.FullBreakoutDuration = source.FullBreakoutDuration; - target.PostBreakoutImmunity = source.PostBreakoutImmunity; - target.HoldRange = source.HoldRange; - target.WalkModifier = source.HolderWalkModifier; - target.SprintModifier = source.HolderSprintModifier; - target.SoftEscapeAvailableAt = _timing.CurTime; - Dirty(uid, target); + if (_holdableQuery.TryComp(held.Owner, out holdable)) + return true; + + ClearHoldState(held, applyImmunity: false); + holdable = null; + return false; } private bool ShouldReleaseHolder(EntityUid holderUid, EntityUid heldUid, float maintenanceRange) diff --git a/Content.Shared/_Scp/Holding/SharedScpHoldingSystem.cs b/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.cs similarity index 97% rename from Content.Shared/_Scp/Holding/SharedScpHoldingSystem.cs rename to Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.cs index 14de1492c4c..5b14ad03a76 100644 --- a/Content.Shared/_Scp/Holding/SharedScpHoldingSystem.cs +++ b/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.cs @@ -1,3 +1,4 @@ +using Content.Shared._Scp.Holding.Components; using Content.Shared.ActionBlocker; using Content.Shared.Alert; using Content.Shared.DoAfter; @@ -11,7 +12,7 @@ using Robust.Shared.Prototypes; using Robust.Shared.Timing; -namespace Content.Shared._Scp.Holding; +namespace Content.Shared._Scp.Holding.Systems; public abstract partial class SharedScpHoldingSystem : EntitySystem { From 0946250f856c23b2b9b995ca8953ae355f10c11a Mon Sep 17 00:00:00 2001 From: drdth Date: Fri, 17 Apr 2026 08:36:26 +0300 Subject: [PATCH 11/27] refactor: big renaming, move contants to component --- .../_Scp/Holding/ScpHoldingSystem.cs | 32 +- .../Tests/_Scp/ScpHoldingTest.cs | 462 +++++++++++----- .../_Scp/ScpHoldingTwoClientCombatTest.cs | 516 ++++++++++++++++++ .../_Scp/Holding/ScpHoldingSystem.Feedback.cs | 22 + .../_Scp/Holding/ScpHoldingSystem.cs | 19 +- .../Compatibility/PullingSystem.ScpHolding.cs | 12 +- ...onent.cs => ActiveScpHoldableComponent.cs} | 4 +- .../Components/ActiveScpHolderComponent.cs | 18 + ...ctiveStateScpHoldableFullHoldComponent.cs} | 2 +- ... ActiveStateScpHolderSlowdownComponent.cs} | 2 +- .../Holding/Components/ScpHoldComponent.cs | 37 -- .../Components/ScpHoldableComponent.cs | 60 ++ .../Holding/Components/ScpHolderComponent.cs | 29 +- .../_Scp/Holding/ScpHoldingEvents.cs | 9 +- .../Systems/SharedScpHoldingSystem.Actions.cs | 48 +- .../Systems/SharedScpHoldingSystem.Drag.cs | 54 +- .../Systems/SharedScpHoldingSystem.Events.cs | 110 ++-- .../SharedScpHoldingSystem.Feedback.cs | 28 +- .../Systems/SharedScpHoldingSystem.Hands.cs | 42 +- .../SharedScpHoldingSystem.Restrictions.cs | 78 ++- .../Systems/SharedScpHoldingSystem.State.cs | 100 ++-- .../Holding/Systems/SharedScpHoldingSystem.cs | 31 +- .../Systems/SharedScp096System.Holding.cs | 8 +- .../Prototypes/Entities/Mobs/Species/base.yml | 2 +- .../Administration/security_commander.yml | 2 +- ...xternal_administrative_zone_commandant.yml | 2 +- .../external_administrative_zone_officer.yml | 2 +- .../field_medical_specialist.yml | 2 +- ...r_external_administrative_zone_officer.yml | 2 +- ...r_external_administrative_zone_officer.yml | 2 +- .../Roles/Jobs/LowAccessPersonnel/class_d.yml | 2 +- .../LowAccessPersonnel/class_d_botanist.yml | 2 +- .../Jobs/LowAccessPersonnel/class_d_cook.yml | 2 +- .../LowAccessPersonnel/class_d_janitor.yml | 2 +- .../heavy_containment_zone_commandant.yml | 2 +- .../heavy_containment_zone_officer.yml | 2 +- .../junior_heavy_containment_zone_officer.yml | 2 +- .../senior_heavy_containment_zone_officer.yml | 2 +- 38 files changed, 1298 insertions(+), 455 deletions(-) create mode 100644 Content.IntegrationTests/Tests/_Scp/ScpHoldingTwoClientCombatTest.cs create mode 100644 Content.Server/_Scp/Holding/ScpHoldingSystem.Feedback.cs rename Content.Shared/_Scp/Holding/Components/{ScpHeldComponent.cs => ActiveScpHoldableComponent.cs} (90%) create mode 100644 Content.Shared/_Scp/Holding/Components/ActiveScpHolderComponent.cs rename Content.Shared/_Scp/Holding/Components/{ScpFullHeldComponent.cs => ActiveStateScpHoldableFullHoldComponent.cs} (89%) rename Content.Shared/_Scp/Holding/Components/{ScpHolderSlowdownComponent.cs => ActiveStateScpHolderSlowdownComponent.cs} (90%) delete mode 100644 Content.Shared/_Scp/Holding/Components/ScpHoldComponent.cs diff --git a/Content.Client/_Scp/Holding/ScpHoldingSystem.cs b/Content.Client/_Scp/Holding/ScpHoldingSystem.cs index 8fb90899a8a..1c2776a7898 100644 --- a/Content.Client/_Scp/Holding/ScpHoldingSystem.cs +++ b/Content.Client/_Scp/Holding/ScpHoldingSystem.cs @@ -30,7 +30,7 @@ public sealed class ScpHoldingSystem : SharedScpHoldingSystem private EntityQuery _handsQuery; private EntityQuery _physicsQuery; private EntityQuery _blockerQuery; - private EntityQuery _holderQuery; + private EntityQuery _activeHolderQuery; private EntityQuery _virtualItemQuery; public override void Initialize() @@ -40,12 +40,12 @@ public override void Initialize() _handsQuery = GetEntityQuery(); _physicsQuery = GetEntityQuery(); _blockerQuery = GetEntityQuery(); - _holderQuery = GetEntityQuery(); + _activeHolderQuery = GetEntityQuery(); _virtualItemQuery = GetEntityQuery(); - SubscribeLocalEvent(OnHeldAfterState); + SubscribeLocalEvent(OnHeldAfterState); SubscribeLocalEvent(OnBlockerUnequipped); - SubscribeLocalEvent(OnUpdateHeldPredicted); + SubscribeLocalEvent(OnUpdateHeldPredicted); } public override void Update(float frameTime) @@ -65,7 +65,7 @@ public override void Update(float frameTime) if (ShouldSuppressBlockerRespawn(local, _suppressedTarget)) DeleteSuppressedBlockers(local, _suppressedTarget!.Value); - if (!_holderQuery.TryComp(local, out var localHolder)) + if (!_activeHolderQuery.TryComp(local, out var localHolder)) { UpdateTrackedLocalHeldTarget(null); return; @@ -81,7 +81,7 @@ public override void Update(float frameTime) SyncHolderState((local, localHolder)); } - private void OnHeldAfterState(Entity ent, ref AfterAutoHandleStateEvent args) + private void OnHeldAfterState(Entity ent, ref AfterAutoHandleStateEvent args) { ReconcileHeldAfterState(ent); } @@ -91,7 +91,7 @@ private void OnBlockerUnequipped(Entity ent, ref Go if (_player.LocalEntity != args.User) return; - if (_holderQuery.TryComp(args.User, out var holder)) + if (_activeHolderQuery.TryComp(args.User, out var holder)) { if (holder.Target == ent.Comp.Target) return; @@ -100,7 +100,7 @@ private void OnBlockerUnequipped(Entity ent, ref Go SuppressBlockerRespawn(args.User, ent.Comp.Target); } - private void OnUpdateHeldPredicted(Entity ent, ref UpdateIsPredictedEvent args) + private void OnUpdateHeldPredicted(Entity ent, ref UpdateIsPredictedEvent args) { if (_player.LocalEntity is not { Valid: true } local) return; @@ -111,7 +111,7 @@ private void OnUpdateHeldPredicted(Entity ent, ref UpdateIsPre return; } - if (_holderQuery.TryComp(local, out var localHolder)) + if (_activeHolderQuery.TryComp(local, out var localHolder)) { if (localHolder.Target == ent.Owner) { @@ -135,7 +135,7 @@ private void OnUpdateHeldPredicted(Entity ent, ref UpdateIsPre protected override bool ShouldUsePredictedBreakoutFeedback => true; - protected override bool ShouldUpdateHeld(EntityUid uid, ScpHeldComponent held) + protected override bool ShouldUpdateHeld(EntityUid uid, ActiveScpHoldableComponent held) { return _physicsQuery.TryComp(uid, out var physics) && physics.Predict; } @@ -145,19 +145,19 @@ protected override bool CanShowBreakoutAttemptFeedback() return _timing.IsFirstTimePredicted; } - protected override void OnHeldStateRefreshed(Entity held) + protected override void OnHeldStateRefreshed(Entity held) { - _physics.UpdateIsPredicted(held.Owner); + _physics.UpdateIsPredicted(held); } - protected override void OnHeldStateShutdown(Entity held) + protected override void OnHeldStateShutdown(Entity held) { - _physics.UpdateIsPredicted(held.Owner); + _physics.UpdateIsPredicted(held); } - protected override void OnHolderStateRefreshed(Entity holder) + protected override void OnHolderStateRefreshed(Entity holder) { - UpdateTrackedLocalHeldTarget(holder.Owner, holder.Comp.Target); + UpdateTrackedLocalHeldTarget(holder, holder.Comp.Target); } protected override void OnHolderStateShutdown(EntityUid holderUid, EntityUid? target) diff --git a/Content.IntegrationTests/Tests/_Scp/ScpHoldingTest.cs b/Content.IntegrationTests/Tests/_Scp/ScpHoldingTest.cs index d0563b4594a..14b8983e452 100644 --- a/Content.IntegrationTests/Tests/_Scp/ScpHoldingTest.cs +++ b/Content.IntegrationTests/Tests/_Scp/ScpHoldingTest.cs @@ -7,6 +7,8 @@ using Content.Shared.Alert; using Content.Server.Body.Systems; using Content.Shared._Scp.Holding; +using Content.Shared._Scp.Holding.Components; +using Content.Shared._Scp.Holding.Systems; using Content.Shared.Body.Components; using Content.Shared.Body.Part; using Content.Shared.Hands.Components; @@ -46,7 +48,7 @@ public sealed class ScpHoldingTest private const string TestListenerComponentName = "TestListener"; private static readonly ProtoId GrabbedAlertId = "ScpHoldGrabbed"; private static readonly FieldInfo SoftEscapeAvailableAtField = - typeof(ScpHeldComponent).GetField(nameof(ScpHeldComponent.SoftEscapeAvailableAt))!; + typeof(ActiveScpHoldableComponent).GetField(nameof(ActiveScpHoldableComponent.SoftEscapeAvailableAt))!; private static EntityWhitelist CreateComponentWhitelist(params string[] components) { @@ -62,12 +64,12 @@ private static EntityWhitelist CreateComponentWhitelist(params string[] componen id: ScpHoldingTestHolder parent: MobHuman components: - - type: ScpHold + - type: ScpHolder - type: entity id: ScpHoldingTestHolderHoldableWhitelisted parent: ScpHoldingTestHolder components: - - type: ScpHold + - type: ScpHolder holdableWhitelist: components: - TestListener @@ -75,7 +77,7 @@ private static EntityWhitelist CreateComponentWhitelist(params string[] componen id: ScpHoldingTestHolderHoldableBlacklisted parent: ScpHoldingTestHolder components: - - type: ScpHold + - type: ScpHolder holdableBlacklist: components: - TestListener @@ -106,7 +108,7 @@ await server.WaitPost(() => await server.WaitAssertion(() => { - var held = entMan.GetComponent(target); + var held = entMan.GetComponent(target); Assert.Multiple(() => { Assert.That(HasFullHold(entMan, target), Is.False); @@ -121,13 +123,13 @@ await server.WaitAssertion(() => await server.WaitAssertion(() => { - Assert.That(entMan.HasComponent(target), Is.False); + Assert.That(entMan.HasComponent(target), Is.False); }); await server.WaitPost(() => { StartHold(entMan, holding, holder, target); - var held = entMan.GetComponent(target); + var held = entMan.GetComponent(target); SetSoftEscapeAvailableAt(held, timing.CurTime + TimeSpan.FromSeconds(1)); }); @@ -136,7 +138,7 @@ await server.WaitPost(() => await server.WaitAssertion(() => { - Assert.That(entMan.HasComponent(target), Is.True); + Assert.That(entMan.HasComponent(target), Is.True); }); await server.WaitPost(() => @@ -148,12 +150,12 @@ await server.WaitPost(() => await server.WaitAssertion(() => { - Assert.That(entMan.HasComponent(target), Is.True); + Assert.That(entMan.HasComponent(target), Is.True); }); await server.WaitPost(() => { - var held = entMan.GetComponent(target); + var held = entMan.GetComponent(target); SetSoftEscapeAvailableAt(held, timing.CurTime); var alert = proto.Index(GrabbedAlertId); Assert.That(alerts.ActivateAlert(target, alert), Is.True); @@ -162,7 +164,7 @@ await server.WaitPost(() => await server.WaitAssertion(() => { - Assert.That(entMan.HasComponent(target), Is.False); + Assert.That(entMan.HasComponent(target), Is.False); }); await pair.CleanReturnAsync(); @@ -210,8 +212,8 @@ await server.WaitPost(() => await server.WaitAssertion(() => { - var held = sEntMan.GetComponent(target); - var holderState = sEntMan.GetComponent(holder); + var held = sEntMan.GetComponent(target); + var holderState = sEntMan.GetComponent(holder); var holderSpeed = sEntMan.GetComponent(holder); var holderHands = sEntMan.GetComponent(holder); var puller = sEntMan.GetComponent(holder); @@ -256,8 +258,8 @@ await client.WaitAssertion(() => clientHolder = ToClientEntity(sEntMan, cEntMan, holder); clientTarget = ToClientEntity(sEntMan, cEntMan, target); - var held = cEntMan.GetComponent(clientTarget); - var holderState = cEntMan.GetComponent(clientHolder); + var held = cEntMan.GetComponent(clientTarget); + var holderState = cEntMan.GetComponent(clientHolder); var holderSpeed = cEntMan.GetComponent(clientHolder); var holderHands = cEntMan.GetComponent(clientHolder); var puller = cEntMan.GetComponent(clientHolder); @@ -340,12 +342,12 @@ await server.WaitPost(() => await server.WaitAssertion(() => { - Assert.That(sEntMan.HasComponent(target), Is.True); + Assert.That(sEntMan.HasComponent(target), Is.True); }); await client.WaitAssertion(() => { - Assert.That(cEntMan.HasComponent(clientTarget), Is.True); + Assert.That(cEntMan.HasComponent(clientTarget), Is.True); }); await server.WaitPost(() => @@ -414,7 +416,7 @@ public async Task PlayerHolderSlowdownAppliesOnGrabAndClearsOnRelease() await server.WaitPost(() => { sTransform.SetCoordinates(serverPlayer, map.GridCoords); - sEntMan.EnsureComponent(serverPlayer); + sEntMan.EnsureComponent(serverPlayer); target = sEntMan.SpawnEntity("MobHuman", map.GridCoords.Offset(new Vector2(0.1f, 0f))); }); @@ -466,7 +468,7 @@ await server.WaitAssertion(() => var speed = sEntMan.GetComponent(serverPlayer); Assert.Multiple(() => { - Assert.That(sEntMan.HasComponent(serverPlayer), Is.True); + Assert.That(sEntMan.HasComponent(serverPlayer), Is.True); Assert.That(HasHolderSlowdown(sEntMan, serverPlayer), Is.True); Assert.That(speed.WalkSpeedModifier, Is.LessThan(serverBaseWalk * 0.75f)); Assert.That(speed.SprintSpeedModifier, Is.LessThan(serverBaseSprint * 0.75f)); @@ -478,7 +480,7 @@ await client.WaitAssertion(() => var speed = cEntMan.GetComponent(clientPlayer); Assert.Multiple(() => { - Assert.That(cEntMan.HasComponent(clientPlayer), Is.True); + Assert.That(cEntMan.HasComponent(clientPlayer), Is.True); Assert.That(HasHolderSlowdown(cEntMan, clientPlayer), Is.True); Assert.That(speed.WalkSpeedModifier, Is.LessThan(clientBaseWalk * 0.75f)); Assert.That(speed.SprintSpeedModifier, Is.LessThan(clientBaseSprint * 0.75f)); @@ -487,7 +489,7 @@ await client.WaitAssertion(() => await server.WaitPost(() => { - var holdComp = sEntMan.GetComponent(serverPlayer); + var holdComp = sEntMan.GetComponent(serverPlayer); Assert.That(holding.TryToggleHold((serverPlayer, holdComp), target), Is.True); }); @@ -499,7 +501,7 @@ await server.WaitAssertion(() => var speed = sEntMan.GetComponent(serverPlayer); Assert.Multiple(() => { - Assert.That(sEntMan.HasComponent(serverPlayer), Is.False); + Assert.That(sEntMan.HasComponent(serverPlayer), Is.False); Assert.That(HasHolderSlowdown(sEntMan, serverPlayer), Is.False); Assert.That(speed.WalkSpeedModifier, Is.EqualTo(serverBaseWalk).Within(0.001f)); Assert.That(speed.SprintSpeedModifier, Is.EqualTo(serverBaseSprint).Within(0.001f)); @@ -511,7 +513,7 @@ await client.WaitAssertion(() => var speed = cEntMan.GetComponent(clientPlayer); Assert.Multiple(() => { - Assert.That(cEntMan.HasComponent(clientPlayer), Is.False); + Assert.That(cEntMan.HasComponent(clientPlayer), Is.False); Assert.That(HasHolderSlowdown(cEntMan, clientPlayer), Is.False); Assert.That(speed.WalkSpeedModifier, Is.EqualTo(clientBaseWalk).Within(0.001f)); Assert.That(speed.SprintSpeedModifier, Is.EqualTo(clientBaseSprint).Within(0.001f)); @@ -555,7 +557,7 @@ await server.WaitAssertion(() => { Assert.That(holderPuller.Pulling, Is.EqualTo(plainTarget)); Assert.That(plainPullable.Puller, Is.EqualTo(holder)); - Assert.That(entMan.HasComponent(holdTarget), Is.False); + Assert.That(entMan.HasComponent(holdTarget), Is.False); }); }); @@ -568,7 +570,7 @@ await server.WaitPost(() => await server.WaitAssertion(() => { var holderPuller = entMan.GetComponent(holder); - var holderState = entMan.GetComponent(holder); + var holderState = entMan.GetComponent(holder); var plainPullable = entMan.GetComponent(plainTarget); var holdPullable = entMan.GetComponent(holdTarget); @@ -578,7 +580,7 @@ await server.WaitAssertion(() => Assert.That(holderState.Target, Is.EqualTo(holdTarget)); Assert.That(plainPullable.Puller, Is.Null); Assert.That(holdPullable.Puller, Is.Null); - Assert.That(entMan.HasComponent(holdTarget), Is.True); + Assert.That(entMan.HasComponent(holdTarget), Is.True); }); }); @@ -611,7 +613,7 @@ await server.WaitPost(() => await server.WaitAssertion(() => { - var held = entMan.GetComponent(target); + var held = entMan.GetComponent(target); var hands = entMan.GetComponent(target); var holderOnePuller = entMan.GetComponent(holderOne); var holderTwoPuller = entMan.GetComponent(holderTwo); @@ -753,13 +755,13 @@ await server.WaitPost(() => await server.WaitAssertion(() => { - var holdComp = entMan.GetComponent(holder); + var holdComp = entMan.GetComponent(holder); Assert.Multiple(() => { Assert.That(holding.CanToggleHold((holder, holdComp), target, quiet: true), Is.False); Assert.That(holding.TryToggleHold((holder, holdComp), target), Is.False); - Assert.That(entMan.HasComponent(target), Is.False); + Assert.That(entMan.HasComponent(target), Is.False); }); }); @@ -808,9 +810,9 @@ await server.WaitPost(() => await server.WaitAssertion(() => { - var successHold = entMan.GetComponent(successHolder); - var blockedHold = entMan.GetComponent(blockedHolder); - var blacklistHold = entMan.GetComponent(blacklistHolder); + var successHold = entMan.GetComponent(successHolder); + var blockedHold = entMan.GetComponent(blockedHolder); + var blacklistHold = entMan.GetComponent(blacklistHolder); Assert.Multiple(() => { @@ -829,8 +831,8 @@ await server.WaitAssertion(() => Assert.That(holding.CanToggleHold((successHolder, successHold), successTarget, quiet: true), Is.True); Assert.That(holding.TryToggleHold((successHolder, successHold), successTarget), Is.True); - Assert.That(entMan.HasComponent(blockedTarget), Is.False); - Assert.That(entMan.HasComponent(successTarget), Is.True); + Assert.That(entMan.HasComponent(blockedTarget), Is.False); + Assert.That(entMan.HasComponent(successTarget), Is.True); }); }); @@ -862,12 +864,12 @@ await server.WaitPost(() => await server.WaitAssertion(() => { - var holdComp = entMan.GetComponent(holder); + var holdComp = entMan.GetComponent(holder); Assert.Multiple(() => { Assert.That(holding.TryToggleHold((holder, holdComp), target), Is.False); - Assert.That(entMan.HasComponent(target), Is.False); + Assert.That(entMan.HasComponent(target), Is.False); Assert.That(attempts.Count(target), Is.EqualTo(1)); Assert.That(attempts.Count(holder), Is.EqualTo(1)); }); @@ -906,7 +908,7 @@ await server.WaitAssertion(() => Assert.Multiple(() => { - Assert.That(entMan.HasComponent(target), Is.False); + Assert.That(entMan.HasComponent(target), Is.False); Assert.That(breakouts.Count(target), Is.EqualTo(1)); Assert.That(breakout.ViaMovement, Is.True); Assert.That(breakout.WasFullHold, Is.False); @@ -954,7 +956,7 @@ await server.WaitPost(() => await server.WaitAssertion(() => { - var held = entMan.GetComponent(target); + var held = entMan.GetComponent(target); Assert.Multiple(() => { Assert.That(held.RequiredHolderCount, Is.EqualTo(3)); @@ -970,7 +972,7 @@ await server.WaitPost(() => await server.WaitAssertion(() => { - var held = entMan.GetComponent(target); + var held = entMan.GetComponent(target); var hands = entMan.GetComponent(target); Assert.Multiple(() => @@ -993,7 +995,7 @@ await server.WaitPost(() => await server.WaitAssertion(() => { - var held = entMan.GetComponent(target); + var held = entMan.GetComponent(target); var hands = entMan.GetComponent(target); Assert.Multiple(() => @@ -1038,7 +1040,7 @@ await server.WaitPost(() => await server.WaitAssertion(() => { - var held = entMan.GetComponent(target); + var held = entMan.GetComponent(target); Assert.Multiple(() => { Assert.That(HasFullHold(entMan, target), Is.True); @@ -1053,7 +1055,7 @@ await server.WaitAssertion(() => await server.WaitAssertion(() => { - var held = entMan.GetComponent(target); + var held = entMan.GetComponent(target); Assert.Multiple(() => { Assert.That(HasBreakoutAttempt(entMan, target), Is.True); @@ -1068,33 +1070,33 @@ await server.WaitAssertion(() => { Assert.Multiple(() => { - Assert.That(entMan.HasComponent(target), Is.False); + Assert.That(entMan.HasComponent(target), Is.False); Assert.That(entMan.HasComponent(target), Is.True); }); }); await server.WaitPost(() => { - var holdComp = entMan.GetComponent(holderOne); + var holdComp = entMan.GetComponent(holderOne); Assert.That(holding.TryToggleHold((holderOne, holdComp), target), Is.False); }); await server.WaitAssertion(() => { - Assert.That(entMan.HasComponent(target), Is.False); + Assert.That(entMan.HasComponent(target), Is.False); }); await server.WaitRunTicks(GetTickCount(timing, TimeSpan.FromSeconds(5))); await server.WaitPost(() => { - var holdComp = entMan.GetComponent(holderOne); + var holdComp = entMan.GetComponent(holderOne); Assert.That(holding.TryToggleHold((holderOne, holdComp), target), Is.True); }); await server.WaitAssertion(() => { - Assert.That(entMan.HasComponent(target), Is.True); + Assert.That(entMan.HasComponent(target), Is.True); }); await pair.CleanReturnAsync(); @@ -1172,8 +1174,8 @@ await server.WaitAssertion(() => Assert.Multiple(() => { - Assert.That(sEntMan.HasComponent(serverPlayer), Is.False); - Assert.That(sEntMan.HasComponent(serverPlayer), Is.False); + Assert.That(sEntMan.HasComponent(serverPlayer), Is.False); + Assert.That(sEntMan.HasComponent(serverPlayer), Is.False); Assert.That(mover.CanMove, Is.True); }); }); @@ -1184,8 +1186,8 @@ await client.WaitAssertion(() => Assert.Multiple(() => { - Assert.That(cEntMan.HasComponent(clientPlayer), Is.False); - Assert.That(cEntMan.HasComponent(clientPlayer), Is.False); + Assert.That(cEntMan.HasComponent(clientPlayer), Is.False); + Assert.That(cEntMan.HasComponent(clientPlayer), Is.False); Assert.That(mover.CanMove, Is.True); }); }); @@ -1230,7 +1232,7 @@ await server.WaitPost(() => await server.WaitAssertion(() => { - var held = entMan.GetComponent(target); + var held = entMan.GetComponent(target); Assert.Multiple(() => { Assert.That(HasBreakoutAttempt(entMan, target), Is.True); @@ -1243,7 +1245,7 @@ await server.WaitAssertion(() => await server.WaitAssertion(() => { - Assert.That(entMan.HasComponent(target), Is.False); + Assert.That(entMan.HasComponent(target), Is.False); }); await pair.CleanReturnAsync(); @@ -1282,8 +1284,8 @@ await server.WaitPost(() => await server.WaitAssertion(() => { - Assert.That(entMan.HasComponent(target), Is.False); - Assert.That(entMan.HasComponent(holder), Is.False); + Assert.That(entMan.HasComponent(target), Is.False); + Assert.That(entMan.HasComponent(holder), Is.False); }); await server.WaitPost(() => StartHold(entMan, holding, holder, target)); @@ -1303,8 +1305,8 @@ await server.WaitPost(() => await server.WaitAssertion(() => { - Assert.That(entMan.HasComponent(target), Is.False); - Assert.That(entMan.HasComponent(holder), Is.False); + Assert.That(entMan.HasComponent(target), Is.False); + Assert.That(entMan.HasComponent(holder), Is.False); }); await pair.CleanReturnAsync(); @@ -1336,7 +1338,7 @@ public async Task ClientDroppingHolderBlockerReleasesWithoutRespawnFlicker() await server.WaitPost(() => { sTransform.SetCoordinates(serverPlayer, map.GridCoords); - sEntMan.EnsureComponent(serverPlayer); + sEntMan.EnsureComponent(serverPlayer); target = sEntMan.SpawnEntity("MobHuman", map.GridCoords.Offset(new Vector2(0.1f, 0f))); StartHold(sEntMan, holding, serverPlayer, target); }); @@ -1354,8 +1356,8 @@ await client.WaitAssertion(() => Assert.Multiple(() => { - Assert.That(cEntMan.HasComponent(clientPlayer), Is.True); - Assert.That(cEntMan.HasComponent(clientTarget), Is.True); + Assert.That(cEntMan.HasComponent(clientPlayer), Is.True); + Assert.That(cEntMan.HasComponent(clientTarget), Is.True); Assert.That(CountHolderTargetVirtualItems(cEntMan, cHandsSystem, clientPlayer, clientTarget, hands), Is.EqualTo(1)); Assert.That(CountPrototypeEntities(cEntMan, "clientsideclone"), Is.EqualTo(0)); }); @@ -1371,8 +1373,8 @@ await client.WaitPost(() => Assert.That(cHandsSystem.TryDrop((clientPlayer, hands), blocker, dropLocation), Is.True); Assert.Multiple(() => { - Assert.That(cEntMan.HasComponent(clientTarget), Is.False); - Assert.That(cEntMan.HasComponent(clientPlayer), Is.False); + Assert.That(cEntMan.HasComponent(clientTarget), Is.False); + Assert.That(cEntMan.HasComponent(clientPlayer), Is.False); Assert.That(CountHolderTargetVirtualItems(cEntMan, cHandsSystem, clientPlayer, clientTarget, hands), Is.EqualTo(0)); Assert.That(CountPrototypeEntities(cEntMan, "clientsideclone"), Is.EqualTo(0)); }); @@ -1435,7 +1437,7 @@ public async Task ClientPullAttemptPredictsSoftHoldBeforeServerAck() await server.WaitPost(() => { sTransform.SetCoordinates(serverPlayer, map.GridCoords); - sEntMan.EnsureComponent(serverPlayer); + sEntMan.EnsureComponent(serverPlayer); target = sEntMan.SpawnEntity("MobHuman", map.GridCoords.Offset(new Vector2(0.1f, 0f))); }); @@ -1451,7 +1453,7 @@ await client.WaitAssertion(() => Assert.Multiple(() => { - Assert.That(cEntMan.HasComponent(clientPlayer), Is.True); + Assert.That(cEntMan.HasComponent(clientPlayer), Is.True); Assert.That(cEntMan.EntityExists(clientTarget), Is.True); }); }); @@ -1460,13 +1462,13 @@ await client.WaitAssertion(() => await client.WaitAssertion(() => { - var held = cEntMan.GetComponent(clientTarget); + var held = cEntMan.GetComponent(clientTarget); var holderHands = cEntMan.GetComponent(clientPlayer); Assert.Multiple(() => { Assert.That(HasFullHold(cEntMan, clientTarget), Is.False); Assert.That(held.Holders, Has.Count.EqualTo(1)); - Assert.That(cEntMan.HasComponent(clientPlayer), Is.True); + Assert.That(cEntMan.HasComponent(clientPlayer), Is.True); Assert.That(CountHolderHandBlockers(cEntMan, cHandsSystem, clientPlayer, clientTarget, holderHands), Is.EqualTo(1)); Assert.That(CountHolderTargetVirtualItems(cEntMan, cHandsSystem, clientPlayer, clientTarget, holderHands), Is.EqualTo(1)); Assert.That(cStatusEffects.HasStatusEffect(clientTarget, "StatusEffectScpHeld"), Is.True); @@ -1475,7 +1477,7 @@ await client.WaitAssertion(() => await server.WaitAssertion(() => { - Assert.That(sEntMan.HasComponent(target), Is.False); + Assert.That(sEntMan.HasComponent(target), Is.False); }); var maxPredictedClientBlockers = 1; @@ -1500,7 +1502,7 @@ await client.WaitPost(() => await server.WaitAssertion(() => { - var held = sEntMan.GetComponent(target); + var held = sEntMan.GetComponent(target); var holderHands = sEntMan.GetComponent(serverPlayer); var puller = sEntMan.GetComponent(serverPlayer); var pullable = sEntMan.GetComponent(target); @@ -1523,7 +1525,7 @@ await server.WaitAssertion(() => await client.WaitAssertion(() => { - var held = cEntMan.GetComponent(clientTarget); + var held = cEntMan.GetComponent(clientTarget); var holderHands = cEntMan.GetComponent(clientPlayer); var puller = cEntMan.GetComponent(clientPlayer); var pullable = cEntMan.GetComponent(clientTarget); @@ -1546,6 +1548,112 @@ await client.WaitAssertion(() => await pair.CleanReturnAsync(); } + // Fire added start - compare real client pull path against direct server hold helper + [Test] + public async Task ClientPullHeldTargetWithCombatModeEnabled_DisablesCombatModeInPredictionAndAfterAck() + { + await using var pair = await PoolManager.GetServerClient(new PoolSettings + { + Fresh = true, + Connected = true, + DummyTicker = false, + }); + + var server = pair.Server; + var client = pair.Client; + var sEntMan = server.EntMan; + var cEntMan = client.EntMan; + var cTiming = client.ResolveDependency(); + var sTransform = server.System(); + var sCombatMode = server.System(); + var map = await pair.CreateTestMap(); + + var serverPlayer = pair.Player!.AttachedEntity!.Value; + EntityUid target = default; + EntityUid serverCombatAction = default; + + await server.WaitPost(() => + { + sTransform.SetCoordinates(serverPlayer, map.GridCoords); + sEntMan.EnsureComponent(serverPlayer); + target = sEntMan.SpawnEntity("MobHuman", map.GridCoords.Offset(new Vector2(0.1f, 0f))); + + var combat = sEntMan.GetComponent(target); + sCombatMode.SetInCombatMode(target, true, combat); + serverCombatAction = GetCombatToggleAction(sEntMan, target); + }); + + await pair.RunTicksSync(10); + await pair.SyncTicks(targetDelta: 1); + + EntityUid clientPlayer = default; + EntityUid clientTarget = default; + EntityUid clientCombatAction = default; + await client.WaitAssertion(() => + { + clientPlayer = client.AttachedEntity!.Value; + clientTarget = ToClientEntity(sEntMan, cEntMan, target); + clientCombatAction = ToClientEntity(sEntMan, cEntMan, serverCombatAction); + + Assert.Multiple(() => + { + Assert.That(IsInCombatMode(sEntMan, target), Is.True); + Assert.That(IsActionToggled(sEntMan, serverCombatAction), Is.True); + Assert.That(cEntMan.HasComponent(clientPlayer), Is.True); + Assert.That(IsInCombatMode(cEntMan, clientTarget), Is.True); + Assert.That(IsActionToggled(cEntMan, clientCombatAction), Is.True); + }); + }); + + await PressClientPullKey(client, cEntMan, cTiming, clientTarget); + + await client.WaitAssertion(() => + { + clientCombatAction = ToClientEntity(sEntMan, cEntMan, serverCombatAction); + + Assert.Multiple(() => + { + Assert.That(cEntMan.HasComponent(clientTarget), Is.True); + Assert.That(cEntMan.HasComponent(clientPlayer), Is.True); + Assert.That(IsInCombatMode(cEntMan, clientTarget), Is.False); + Assert.That(IsActionToggled(cEntMan, clientCombatAction), Is.False); + }); + }); + + await server.WaitAssertion(() => + { + Assert.That(sEntMan.HasComponent(target), Is.False); + }); + + await pair.RunTicksSync(10); + await pair.SyncTicks(targetDelta: 1); + + await server.WaitAssertion(() => + { + Assert.Multiple(() => + { + Assert.That(sEntMan.HasComponent(target), Is.True); + Assert.That(IsInCombatMode(sEntMan, target), Is.False); + Assert.That(IsActionToggled(sEntMan, serverCombatAction), Is.False); + }); + }); + + await client.WaitAssertion(() => + { + clientCombatAction = ToClientEntity(sEntMan, cEntMan, serverCombatAction); + + Assert.Multiple(() => + { + Assert.That(cEntMan.HasComponent(clientTarget), Is.True); + Assert.That(IsInCombatMode(cEntMan, clientTarget), Is.False); + Assert.That(IsActionToggled(cEntMan, clientCombatAction), Is.False); + }); + }); + + await pair.CleanReturnAsync(); + } + // Fire added end + [Test] public async Task ClientPullCooldownAndFullBreakoutPenaltyReplicate() { @@ -1574,7 +1682,7 @@ public async Task ClientPullCooldownAndFullBreakoutPenaltyReplicate() await server.WaitPost(() => { sTransform.SetCoordinates(serverPlayer, map.GridCoords); - sEntMan.EnsureComponent(serverPlayer); + sEntMan.EnsureComponent(serverPlayer); firstTarget = sEntMan.SpawnEntity("MobHuman", map.GridCoords.Offset(new Vector2(0.4f, 0f))); breakoutTarget = sEntMan.SpawnEntity("MobHuman", map.GridCoords.Offset(new Vector2(0.4f, 0.6f))); holderTwo = sEntMan.SpawnEntity(HolderPrototype, map.GridCoords.Offset(new Vector2(-0.4f, 0.6f))); @@ -1601,8 +1709,8 @@ await client.WaitAssertion(() => { Assert.Multiple(() => { - Assert.That(cEntMan.HasComponent(clientFirstTarget), Is.True); - Assert.That(cEntMan.HasComponent(clientBreakoutTarget), Is.False); + Assert.That(cEntMan.HasComponent(clientFirstTarget), Is.True); + Assert.That(cEntMan.HasComponent(clientBreakoutTarget), Is.False); Assert.That(GetHoldCooldownRemaining(cEntMan, clientPlayer, cTiming), Is.GreaterThan(TimeSpan.Zero)); }); }); @@ -1614,8 +1722,8 @@ await server.WaitAssertion(() => { Assert.Multiple(() => { - Assert.That(sEntMan.HasComponent(firstTarget), Is.True); - Assert.That(sEntMan.HasComponent(breakoutTarget), Is.False); + Assert.That(sEntMan.HasComponent(firstTarget), Is.True); + Assert.That(sEntMan.HasComponent(breakoutTarget), Is.False); Assert.That(GetHoldCooldownRemaining(sEntMan, serverPlayer, sTiming), Is.GreaterThan(TimeSpan.Zero)); }); }); @@ -1632,7 +1740,7 @@ await client.WaitAssertion(() => await client.WaitAssertion(() => { - Assert.That(cEntMan.HasComponent(clientFirstTarget), Is.False); + Assert.That(cEntMan.HasComponent(clientFirstTarget), Is.False); }); await pair.RunTicksSync(10); @@ -1640,7 +1748,7 @@ await client.WaitAssertion(() => await server.WaitAssertion(() => { - Assert.That(sEntMan.HasComponent(firstTarget), Is.False); + Assert.That(sEntMan.HasComponent(firstTarget), Is.False); }); await pair.RunTicksSync(GetTickCount(sTiming, TimeSpan.FromSeconds(1)) + 1); @@ -1650,7 +1758,7 @@ await server.WaitAssertion(() => await client.WaitAssertion(() => { - Assert.That(cEntMan.HasComponent(clientBreakoutTarget), Is.True); + Assert.That(cEntMan.HasComponent(clientBreakoutTarget), Is.True); }); await pair.RunTicksSync(10); @@ -1658,7 +1766,7 @@ await client.WaitAssertion(() => await server.WaitAssertion(() => { - Assert.That(sEntMan.HasComponent(breakoutTarget), Is.True); + Assert.That(sEntMan.HasComponent(breakoutTarget), Is.True); }); await server.WaitPost(() => @@ -1681,7 +1789,7 @@ await server.WaitAssertion(() => { Assert.Multiple(() => { - Assert.That(sEntMan.HasComponent(breakoutTarget), Is.False); + Assert.That(sEntMan.HasComponent(breakoutTarget), Is.False); Assert.That(GetHoldCooldownRemaining(sEntMan, serverPlayer, sTiming), Is.GreaterThanOrEqualTo(TimeSpan.FromSeconds(1.5))); Assert.That(GetHoldCooldownRemaining(sEntMan, holderTwo, sTiming), Is.GreaterThanOrEqualTo(TimeSpan.FromSeconds(1.5))); }); @@ -1691,7 +1799,7 @@ await client.WaitAssertion(() => { Assert.Multiple(() => { - Assert.That(cEntMan.HasComponent(clientBreakoutTarget), Is.False); + Assert.That(cEntMan.HasComponent(clientBreakoutTarget), Is.False); Assert.That(GetHoldCooldownRemaining(cEntMan, clientPlayer, cTiming), Is.GreaterThanOrEqualTo(TimeSpan.FromSeconds(1.5))); }); }); @@ -1700,7 +1808,7 @@ await client.WaitAssertion(() => await client.WaitAssertion(() => { - Assert.That(cEntMan.HasComponent(clientFirstTarget), Is.False); + Assert.That(cEntMan.HasComponent(clientFirstTarget), Is.False); }); await pair.RunTicksSync(5); @@ -1708,7 +1816,7 @@ await client.WaitAssertion(() => await server.WaitAssertion(() => { - Assert.That(sEntMan.HasComponent(firstTarget), Is.False); + Assert.That(sEntMan.HasComponent(firstTarget), Is.False); }); await pair.CleanReturnAsync(); @@ -1742,7 +1850,7 @@ public async Task ClientSecondPullPredictsFullHoldBeforeServerAck() await server.WaitPost(() => { sTransform.SetCoordinates(serverPlayer, map.GridCoords); - sEntMan.EnsureComponent(serverPlayer); + sEntMan.EnsureComponent(serverPlayer); holderOne = sEntMan.SpawnEntity(HolderPrototype, map.GridCoords); target = sEntMan.SpawnEntity("MobHuman", map.GridCoords.Offset(new Vector2(0.5f, 0f))); @@ -1761,7 +1869,7 @@ await client.WaitAssertion(() => clientTarget = ToClientEntity(sEntMan, cEntMan, target); clientHolderOne = ToClientEntity(sEntMan, cEntMan, holderOne); - var held = cEntMan.GetComponent(clientTarget); + var held = cEntMan.GetComponent(clientTarget); Assert.Multiple(() => { Assert.That(HasFullHold(cEntMan, clientTarget), Is.False); @@ -1773,7 +1881,7 @@ await client.WaitAssertion(() => await client.WaitAssertion(() => { - var held = cEntMan.GetComponent(clientTarget); + var held = cEntMan.GetComponent(clientTarget); var hands = cEntMan.GetComponent(clientTarget); Assert.Multiple(() => @@ -1787,7 +1895,7 @@ await client.WaitAssertion(() => await server.WaitAssertion(() => { - var held = sEntMan.GetComponent(target); + var held = sEntMan.GetComponent(target); Assert.Multiple(() => { Assert.That(HasFullHold(sEntMan, target), Is.False); @@ -1800,7 +1908,7 @@ await server.WaitAssertion(() => await server.WaitAssertion(() => { - var held = sEntMan.GetComponent(target); + var held = sEntMan.GetComponent(target); Assert.Multiple(() => { Assert.That(HasFullHold(sEntMan, target), Is.True); @@ -1810,7 +1918,7 @@ await server.WaitAssertion(() => await client.WaitAssertion(() => { - var held = cEntMan.GetComponent(clientTarget); + var held = cEntMan.GetComponent(clientTarget); Assert.Multiple(() => { Assert.That(HasFullHold(cEntMan, clientTarget), Is.True); @@ -1850,7 +1958,7 @@ public async Task ClientPrimaryReassignmentKeepsCustomDragAndReconcilesCleanly() await server.WaitPost(() => { sTransform.SetCoordinates(serverPlayer, map.GridCoords); - sEntMan.EnsureComponent(serverPlayer); + sEntMan.EnsureComponent(serverPlayer); target = sEntMan.SpawnEntity("MobHuman", map.GridCoords.Offset(new Vector2(0.95f, 0f))); host.ExecuteCommand(null, $"addhand {sEntMan.GetNetEntity(target)}"); @@ -1866,7 +1974,7 @@ await server.WaitPost(() => await server.WaitAssertion(() => { - var held = sEntMan.GetComponent(target); + var held = sEntMan.GetComponent(target); var serverPlayerPuller = sEntMan.GetComponent(serverPlayer); var holderTwoPuller = sEntMan.GetComponent(holderTwo); var pullable = sEntMan.GetComponent(target); @@ -1896,7 +2004,7 @@ await client.WaitAssertion(() => clientTarget = ToClientEntity(sEntMan, cEntMan, target); clientHolderTwo = ToClientEntity(sEntMan, cEntMan, holderTwo); - var held = cEntMan.GetComponent(clientTarget); + var held = cEntMan.GetComponent(clientTarget); var playerPuller = cEntMan.GetComponent(clientPlayer); var holderTwoPuller = cEntMan.GetComponent(clientHolderTwo); var pullable = cEntMan.GetComponent(clientTarget); @@ -1919,7 +2027,7 @@ await client.WaitAssertion(() => await server.WaitPost(() => { - var holdComp = sEntMan.GetComponent(serverPlayer); + var holdComp = sEntMan.GetComponent(serverPlayer); Assert.That(holding.TryToggleHold((serverPlayer, holdComp), target), Is.True); }); @@ -1928,7 +2036,7 @@ await server.WaitPost(() => await server.WaitAssertion(() => { - var held = sEntMan.GetComponent(target); + var held = sEntMan.GetComponent(target); var pullable = sEntMan.GetComponent(target); var holderTwoPuller = sEntMan.GetComponent(holderTwo); var distance = GetDistance(sTransform, holderTwo, target); @@ -1948,7 +2056,7 @@ await server.WaitAssertion(() => await client.WaitAssertion(() => { - var held = cEntMan.GetComponent(clientTarget); + var held = cEntMan.GetComponent(clientTarget); var pullable = cEntMan.GetComponent(clientTarget); var holderTwoPuller = cEntMan.GetComponent(clientHolderTwo); var distance = GetDistance(cTransform, clientHolderTwo, clientTarget); @@ -2008,7 +2116,7 @@ await server.WaitPost(() => await pair.SyncTicks(targetDelta: 1); await server.WaitAssertion(() => { - var held = sEntMan.GetComponent(serverPlayer); + var held = sEntMan.GetComponent(serverPlayer); Assert.Multiple(() => { Assert.That(HasFullHold(sEntMan, serverPlayer), Is.True); @@ -2030,7 +2138,7 @@ await client.WaitPost(() => { cEntMan.RaisePredictiveEvent(new ClickAlertEvent("ScpHoldGrabbed")); - var held = cEntMan.GetComponent(clientPlayer); + var held = cEntMan.GetComponent(clientPlayer); Assert.Multiple(() => { Assert.That(HasBreakoutAttempt(cEntMan, clientPlayer), Is.True); @@ -2042,7 +2150,7 @@ await client.WaitPost(() => await server.WaitAssertion(() => { - var held = sEntMan.GetComponent(serverPlayer); + var held = sEntMan.GetComponent(serverPlayer); Assert.Multiple(() => { Assert.That(HasFullHold(sEntMan, serverPlayer), Is.True); @@ -2057,7 +2165,7 @@ await server.WaitAssertion(() => { Assert.Multiple(() => { - Assert.That(sEntMan.HasComponent(serverPlayer), Is.False); + Assert.That(sEntMan.HasComponent(serverPlayer), Is.False); Assert.That(sEntMan.HasComponent(serverPlayer), Is.True); }); }); @@ -2066,7 +2174,7 @@ await client.WaitAssertion(() => { Assert.Multiple(() => { - Assert.That(cEntMan.HasComponent(clientPlayer), Is.False); + Assert.That(cEntMan.HasComponent(clientPlayer), Is.False); Assert.That(cEntMan.HasComponent(clientPlayer), Is.True); }); }); @@ -2114,7 +2222,7 @@ await client.WaitAssertion(() => Assert.Multiple(() => { - Assert.That(cEntMan.HasComponent(clientPlayer), Is.True); + Assert.That(cEntMan.HasComponent(clientPlayer), Is.True); Assert.That(cAlerts.IsShowingAlert(clientPlayer, "ScpHoldGrabbed"), Is.True); }); }); @@ -2125,14 +2233,14 @@ await client.WaitPost(() => Assert.Multiple(() => { - Assert.That(cEntMan.HasComponent(clientPlayer), Is.False); + Assert.That(cEntMan.HasComponent(clientPlayer), Is.False); Assert.That(cAlerts.IsShowingAlert(clientPlayer, "ScpHoldGrabbed"), Is.False); }); }); await server.WaitAssertion(() => { - Assert.That(sEntMan.HasComponent(serverPlayer), Is.True); + Assert.That(sEntMan.HasComponent(serverPlayer), Is.True); Assert.That(sAlerts.IsShowingAlert(serverPlayer, "ScpHoldGrabbed"), Is.True); }); @@ -2141,13 +2249,13 @@ await server.WaitAssertion(() => await server.WaitAssertion(() => { - Assert.That(sEntMan.HasComponent(serverPlayer), Is.False); + Assert.That(sEntMan.HasComponent(serverPlayer), Is.False); Assert.That(sAlerts.IsShowingAlert(serverPlayer, "ScpHoldGrabbed"), Is.False); }); await client.WaitAssertion(() => { - Assert.That(cEntMan.HasComponent(clientPlayer), Is.False); + Assert.That(cEntMan.HasComponent(clientPlayer), Is.False); Assert.That(cAlerts.IsShowingAlert(clientPlayer, "ScpHoldGrabbed"), Is.False); }); @@ -2187,7 +2295,7 @@ await server.WaitPost(() => await server.WaitAssertion(() => { - var held = entMan.GetComponent(target); + var held = entMan.GetComponent(target); var holderOnePuller = entMan.GetComponent(holderOne); var holderTwoPuller = entMan.GetComponent(holderTwo); var pullable = entMan.GetComponent(target); @@ -2204,14 +2312,14 @@ await server.WaitAssertion(() => await server.WaitPost(() => { - var holderComp = entMan.GetComponent(holderOne); + var holderComp = entMan.GetComponent(holderOne); Assert.That(holding.TryToggleHold((holderOne, holderComp), target), Is.True); }); await server.WaitRunTicks(4); await server.WaitAssertion(() => { - var held = entMan.GetComponent(target); + var held = entMan.GetComponent(target); var holderOnePuller = entMan.GetComponent(holderOne); var holderTwoPuller = entMan.GetComponent(holderTwo); var pullable = entMan.GetComponent(target); @@ -2235,7 +2343,7 @@ await server.WaitPost(() => await server.WaitAssertion(() => { - Assert.That(entMan.HasComponent(target), Is.False); + Assert.That(entMan.HasComponent(target), Is.False); }); await pair.CleanReturnAsync(); @@ -2280,8 +2388,8 @@ await server.WaitPost(() => await server.WaitAssertion(() => { - var held = sEntMan.GetComponent(serverPlayer); - var holderState = sEntMan.GetComponent(holder); + var held = sEntMan.GetComponent(serverPlayer); + var holderState = sEntMan.GetComponent(holder); var holderHands = sEntMan.GetComponent(holder); Assert.Multiple(() => @@ -2303,8 +2411,8 @@ await client.WaitAssertion(() => clientPlayer = client.AttachedEntity!.Value; clientHolder = ToClientEntity(sEntMan, cEntMan, holder); - var held = cEntMan.GetComponent(clientPlayer); - var holderState = cEntMan.GetComponent(clientHolder); + var held = cEntMan.GetComponent(clientPlayer); + var holderState = cEntMan.GetComponent(clientHolder); var holderHands = cEntMan.GetComponent(clientHolder); Assert.Multiple(() => @@ -2333,9 +2441,9 @@ await server.WaitAssertion(() => Assert.Multiple(() => { - Assert.That(sEntMan.HasComponent(serverPlayer), Is.False); + Assert.That(sEntMan.HasComponent(serverPlayer), Is.False); Assert.That(sEntMan.HasComponent(serverPlayer), Is.False); - Assert.That(sEntMan.HasComponent(holder), Is.False); + Assert.That(sEntMan.HasComponent(holder), Is.False); Assert.That(CountHolderHandBlockers(sEntMan, sHandsSystem, holder, serverPlayer, holderHands), Is.EqualTo(0)); Assert.That(CountHolderTargetVirtualItems(sEntMan, sHandsSystem, holder, serverPlayer, holderHands), Is.EqualTo(0)); Assert.That(sStatusEffects.HasStatusEffect(serverPlayer, "StatusEffectScpHeld"), Is.False); @@ -2349,9 +2457,9 @@ await client.WaitAssertion(() => Assert.Multiple(() => { - Assert.That(cEntMan.HasComponent(clientPlayer), Is.False); + Assert.That(cEntMan.HasComponent(clientPlayer), Is.False); Assert.That(cEntMan.HasComponent(clientPlayer), Is.False); - Assert.That(cEntMan.HasComponent(clientHolder), Is.False); + Assert.That(cEntMan.HasComponent(clientHolder), Is.False); Assert.That(CountHolderHandBlockers(cEntMan, cHandsSystem, clientHolder, clientPlayer, holderHands), Is.EqualTo(0)); Assert.That(CountHolderTargetVirtualItems(cEntMan, cHandsSystem, clientHolder, clientPlayer, holderHands), Is.EqualTo(0)); Assert.That(cStatusEffects.HasStatusEffect(clientPlayer, "StatusEffectScpHeld"), Is.False); @@ -2362,6 +2470,96 @@ await client.WaitAssertion(() => await pair.CleanReturnAsync(); } + // Fire added start - verify hold disables combat mode consistently + [Test] + public async Task ConnectedTargetHeldWithCombatModeEnabled_DisablesCombatModeAndCombatAction() + { + await using var pair = await PoolManager.GetServerClient(new PoolSettings + { + Fresh = true, + Connected = true, + DummyTicker = false, + }); + + var server = pair.Server; + var client = pair.Client; + var sEntMan = server.EntMan; + var cEntMan = client.EntMan; + var sTransform = server.System(); + var sCombatMode = server.System(); + var holding = server.System(); + var map = await pair.CreateTestMap(); + + var serverPlayer = pair.Player!.AttachedEntity!.Value; + EntityUid clientPlayer = default; + EntityUid holder = default; + EntityUid serverCombatAction = default; + EntityUid clientCombatAction = default; + + await server.WaitPost(() => + { + sTransform.SetCoordinates(serverPlayer, map.GridCoords.Offset(new Vector2(0.1f, 0f))); + holder = sEntMan.SpawnEntity(HolderPrototype, map.GridCoords); + + var combat = sEntMan.GetComponent(serverPlayer); + sCombatMode.SetInCombatMode(serverPlayer, true, combat); + serverCombatAction = GetCombatToggleAction(sEntMan, serverPlayer); + }); + + await pair.RunTicksSync(5); + await pair.SyncTicks(targetDelta: 1); + + await server.WaitAssertion(() => + { + Assert.Multiple(() => + { + Assert.That(IsInCombatMode(sEntMan, serverPlayer), Is.True); + Assert.That(IsActionToggled(sEntMan, serverCombatAction), Is.True); + }); + }); + + await client.WaitAssertion(() => + { + clientPlayer = client.AttachedEntity!.Value; + clientCombatAction = ToClientEntity(sEntMan, cEntMan, serverCombatAction); + + Assert.Multiple(() => + { + Assert.That(IsInCombatMode(cEntMan, clientPlayer), Is.True); + Assert.That(IsActionToggled(cEntMan, clientCombatAction), Is.True); + }); + }); + + await server.WaitPost(() => StartHold(sEntMan, holding, holder, serverPlayer)); + await pair.RunTicksSync(5); + await pair.SyncTicks(targetDelta: 1); + + await server.WaitAssertion(() => + { + Assert.Multiple(() => + { + Assert.That(sEntMan.HasComponent(serverPlayer), Is.True); + Assert.That(IsInCombatMode(sEntMan, serverPlayer), Is.False); + Assert.That(IsActionToggled(sEntMan, serverCombatAction), Is.False); + }); + }); + + await client.WaitAssertion(() => + { + clientCombatAction = ToClientEntity(sEntMan, cEntMan, serverCombatAction); + + Assert.Multiple(() => + { + Assert.That(cEntMan.HasComponent(clientPlayer), Is.True); + Assert.That(IsInCombatMode(cEntMan, clientPlayer), Is.False); + Assert.That(IsActionToggled(cEntMan, clientCombatAction), Is.False); + }); + }); + + await pair.CleanReturnAsync(); + } + // Fire added end + private static int CountBlockingVirtualHands(IEntityManager entMan, SharedHandsSystem handsSystem, EntityUid uid, HandsComponent hands) { return handsSystem.EnumerateHeld((uid, hands)).Count(item => @@ -2387,7 +2585,7 @@ private static EntityUid[] GetHeldHandBlockers(IEntityManager entMan, SharedHand private static bool HasFullHold(IEntityManager entMan, EntityUid uid) { - return entMan.HasComponent(uid); + return entMan.HasComponent(uid); } private static bool HasBreakoutAttempt(IEntityManager entMan, EntityUid uid) @@ -2397,8 +2595,28 @@ private static bool HasBreakoutAttempt(IEntityManager entMan, EntityUid uid) private static bool HasHolderSlowdown(IEntityManager entMan, EntityUid uid) { - return entMan.HasComponent(uid); + return entMan.HasComponent(uid); + } + + // Fire added start - verify hold disables combat mode consistently + private static EntityUid GetCombatToggleAction(IEntityManager entMan, EntityUid uid) + { + var combat = entMan.GetComponent(uid); + + Assert.That(combat.CombatToggleActionEntity, Is.Not.Null); + return combat.CombatToggleActionEntity!.Value; + } + + private static bool IsInCombatMode(IEntityManager entMan, EntityUid uid) + { + return entMan.GetComponent(uid).IsInCombatMode; + } + + private static bool IsActionToggled(IEntityManager entMan, EntityUid uid) + { + return entMan.GetComponent(uid).Toggled; } + // Fire added end private static bool VictimHandsUseHolderIcons(IEntityManager entMan, SharedHandsSystem handsSystem, EntityUid uid, HandsComponent hands, params EntityUid[] holders) { @@ -2516,7 +2734,7 @@ private static int GetTickCount(IGameTiming timing, TimeSpan duration) private static TimeSpan GetHoldCooldownRemaining(IEntityManager entMan, EntityUid holder, IGameTiming timing) { - if (!entMan.TryGetComponent(holder, out ScpHoldComponent? holdComp) || + if (!entMan.TryGetComponent(holder, out ScpHolderComponent? holdComp) || holdComp.HoldAvailableAt is not { } cooldownEnd || cooldownEnd <= timing.CurTime) { @@ -2557,7 +2775,7 @@ private static async Task SendClientPullInput( await client.WaitPost(() => inputSystem.HandleInputCommand(client.Session!, ContentKeyFunctions.TryPullObject, message)); } - private static void SetSoftEscapeAvailableAt(ScpHeldComponent held, TimeSpan value) + private static void SetSoftEscapeAvailableAt(ActiveScpHoldableComponent held, TimeSpan value) { SoftEscapeAvailableAtField.SetValue(held, value); } @@ -2571,7 +2789,7 @@ private static void RaiseMoveInput(IEntityManager entMan, EntityUid uid) private static void StartHold(IEntityManager entMan, SharedScpHoldingSystem holding, EntityUid holder, EntityUid target) { - var holdComp = entMan.GetComponent(holder); + var holdComp = entMan.GetComponent(holder); Assert.That(holding.TryToggleHold((holder, holdComp), target), Is.True); } @@ -2597,6 +2815,6 @@ public override void Initialize() private static void OnAttempt(Entity ent, ref ScpHoldAttemptEvent args) { - args.Cancel(); + args.Cancelled = true; } } diff --git a/Content.IntegrationTests/Tests/_Scp/ScpHoldingTwoClientCombatTest.cs b/Content.IntegrationTests/Tests/_Scp/ScpHoldingTwoClientCombatTest.cs new file mode 100644 index 00000000000..ec5478c532f --- /dev/null +++ b/Content.IntegrationTests/Tests/_Scp/ScpHoldingTwoClientCombatTest.cs @@ -0,0 +1,516 @@ +#nullable enable +using System.Numerics; +using Content.Client.Actions; +using Content.Client.Gameplay; +using Content.Client.IoC; +using Content.Client.Parallax.Managers; +using Content.IntegrationTests._Sunrise; +using Content.Server.Mind; +using Content.Shared._Scp.Holding.Components; +using Content.Shared._Scp.Holding.Systems; +using Content.Shared.Actions.Components; +using Content.Shared.CombatMode; +using Content.Shared.Input; +using Content.Shared.Movement.Components; +using Content.Shared.Movement.Events; +using Content.Shared.Movement.Systems; +using Content.Shared.Players; +using Content.Shared.Weapons.Melee.Events; +using Content.Shared.Weapons.Melee; +using Robust.Client.Audio.Midi; +using Robust.Client.GameObjects; +using Robust.Client.Input; +using Robust.Client.State; +using Robust.Server.Player; +using Robust.Shared.ContentPack; +using Robust.Shared.GameObjects; +using Robust.Shared.Input; +using Robust.Shared.IoC; +using Robust.Shared.Map; +using Robust.Shared.Maths; +using Robust.Shared.Network; +using Robust.Shared.Player; +using Robust.Shared.Timing; +using Robust.UnitTesting; + +namespace Content.IntegrationTests.Tests._Scp; + +[TestFixture] +public sealed class ScpHoldingTwoClientCombatTest +{ + [Test] + public async Task TwoClients_TargetClientHeldAfterOwnCombatToggle_DisablesCombatModeForTargetClient() + { + using var server = new RobustIntegrationTest.ServerIntegrationInstance(CreateServerOptions()); + using var targetClient = new RobustIntegrationTest.ClientIntegrationInstance(CreateClientOptions()); + using var holderClient = new RobustIntegrationTest.ClientIntegrationInstance(CreateClientOptions()); + + await Task.WhenAll(server.WaitIdleAsync(), targetClient.WaitIdleAsync(), holderClient.WaitIdleAsync()); + + await targetClient.Connect(server); + await holderClient.Connect(server); + await RunTicks(server, targetClient, holderClient, 10); + + var sEntMan = server.EntMan; + var targetEntMan = targetClient.EntMan; + var holderEntMan = holderClient.EntMan; + var targetTiming = targetClient.ResolveDependency(); + var holderTiming = holderClient.ResolveDependency(); + var targetActions = targetClient.System(); + var sTransform = server.System(); + var sCombatMode = server.System(); + var sPlayerMan = server.ResolveDependency(); + var targetState = targetClient.ResolveDependency(); + var holderState = holderClient.ResolveDependency(); + + Assert.That(targetClient.User, Is.Not.Null); + Assert.That(holderClient.User, Is.Not.Null); + + var targetSession = sPlayerMan.GetSessionById(targetClient.User!.Value); + var holderSession = sPlayerMan.GetSessionById(holderClient.User!.Value); + + EntityUid sTarget = default; + EntityUid sHolder = default; + EntityUid sCombatAction = default; + + await targetClient.WaitPost(() => targetState.RequestStateChange()); + await holderClient.WaitPost(() => holderState.RequestStateChange()); + await RunTicks(server, targetClient, holderClient, 20); + + await server.WaitAssertion(() => + { + Assert.Multiple(() => + { + Assert.That(targetSession.AttachedEntity, Is.Not.Null); + Assert.That(holderSession.AttachedEntity, Is.Not.Null); + }); + + sTarget = targetSession.AttachedEntity!.Value; + sHolder = holderSession.AttachedEntity!.Value; + sEntMan.EnsureComponent(sHolder); + + var targetCoords = sEntMan.GetComponent(sTarget).Coordinates; + sTransform.SetCoordinates(sHolder, new EntityCoordinates(targetCoords.EntityId, targetCoords.Position + new Vector2(0.1f, 0f))); + + sCombatAction = GetCombatToggleAction(sEntMan, sTarget); + }); + await RunTicks(server, targetClient, holderClient, 10); + + EntityUid cTargetOnTargetClient = default; + EntityUid cCombatActionOnTargetClient = default; + EntityUid cHolderOnTargetClient = default; + EntityUid cTargetOnHolderClient = default; + EntityUid cHolderOnHolderClient = default; + + await targetClient.WaitAssertion(() => + { + cTargetOnTargetClient = ToClientEntity(sEntMan, targetEntMan, sTarget); + cCombatActionOnTargetClient = ToClientEntity(sEntMan, targetEntMan, sCombatAction); + cHolderOnTargetClient = ToClientEntity(sEntMan, targetEntMan, sHolder); + + Assert.Multiple(() => + { + Assert.That(targetClient.AttachedEntity, Is.EqualTo(cTargetOnTargetClient)); + Assert.That(targetEntMan.EntityExists(cTargetOnTargetClient), Is.True); + Assert.That(targetEntMan.EntityExists(cCombatActionOnTargetClient), Is.True); + Assert.That(targetEntMan.EntityExists(cHolderOnTargetClient), Is.True); + }); + }); + + await holderClient.WaitAssertion(() => + { + cTargetOnHolderClient = ToClientEntity(sEntMan, holderEntMan, sTarget); + cHolderOnHolderClient = ToClientEntity(sEntMan, holderEntMan, sHolder); + + Assert.Multiple(() => + { + Assert.That(holderClient.AttachedEntity, Is.EqualTo(cHolderOnHolderClient)); + Assert.That(holderEntMan.EntityExists(cTargetOnHolderClient), Is.True); + }); + }); + + await targetClient.WaitPost(() => + { + var action = targetEntMan.GetComponent(cCombatActionOnTargetClient); + targetActions.TriggerAction((cCombatActionOnTargetClient, action)); + }); + await RunTicks(server, targetClient, holderClient, 10); + + await server.WaitAssertion(() => + { + Assert.Multiple(() => + { + Assert.That(IsInCombatMode(sEntMan, sTarget), Is.True); + Assert.That(IsActionToggled(sEntMan, sCombatAction), Is.True); + }); + }); + + await targetClient.WaitAssertion(() => + { + Assert.Multiple(() => + { + Assert.That(IsInCombatMode(targetEntMan, cTargetOnTargetClient), Is.True); + Assert.That(IsActionToggled(targetEntMan, cCombatActionOnTargetClient), Is.True); + }); + }); + + await SendClientPullInput(holderClient, holderEntMan, holderTiming, cTargetOnHolderClient, BoundKeyState.Down); + await RunTicks(server, targetClient, holderClient, 2); + await SendClientPullInput(holderClient, holderEntMan, holderTiming, cTargetOnHolderClient, BoundKeyState.Up); + await RunTicks(server, targetClient, holderClient, 15); + + await server.WaitAssertion(() => + { + Assert.Multiple(() => + { + Assert.That(sEntMan.HasComponent(sTarget), Is.True); + Assert.That(IsInCombatMode(sEntMan, sTarget), Is.False); + Assert.That(IsActionToggled(sEntMan, sCombatAction), Is.False); + Assert.That(IsActionEnabled(sEntMan, sCombatAction), Is.False); + }); + }); + + await RunTicks(server, targetClient, holderClient, 1); + + await targetClient.WaitPost(() => + { + cCombatActionOnTargetClient = ToClientEntity(sEntMan, targetEntMan, sCombatAction); + + Assert.Multiple(() => + { + Assert.That(IsInCombatMode(targetEntMan, cTargetOnTargetClient), Is.False); + Assert.That(IsActionToggled(targetEntMan, cCombatActionOnTargetClient), Is.False); + Assert.That(IsActionEnabled(targetEntMan, cCombatActionOnTargetClient), Is.False); + }); + }); + + EntityUid sWeapon = default; + TimeSpan weaponCooldownBeforeAttack = default; + + await server.WaitPost(() => + { + var meleeSystem = server.System(); + Assert.That(meleeSystem.TryGetWeapon(sTarget, out sWeapon, out var melee), Is.True); + Assert.That(melee, Is.Not.Null); + weaponCooldownBeforeAttack = melee!.NextAttack; + }); + + await targetClient.WaitPost(() => + { + var cWeaponOnTargetClient = ToClientEntity(sEntMan, targetEntMan, sWeapon); + var targetCoords = targetEntMan.GetComponent(cHolderOnTargetClient).Coordinates; + + targetEntMan.RaisePredictiveEvent(new LightAttackEvent( + targetEntMan.GetNetEntity(cHolderOnTargetClient), + targetEntMan.GetNetEntity(cWeaponOnTargetClient), + targetEntMan.GetNetCoordinates(targetCoords))); + }); + await RunTicks(server, targetClient, holderClient, 3); + + await server.WaitAssertion(() => + { + var melee = sEntMan.GetComponent(sWeapon); + + Assert.Multiple(() => + { + Assert.That(IsInCombatMode(sEntMan, sTarget), Is.False); + Assert.That(melee.NextAttack, Is.EqualTo(weaponCooldownBeforeAttack)); + }); + }); + + await holderClient.WaitAssertion(() => + { + var cCombatActionOnHolderClient = ToClientEntity(sEntMan, holderEntMan, sCombatAction); + + Assert.Multiple(() => + { + Assert.That(holderEntMan.HasComponent(cTargetOnHolderClient), Is.True); + Assert.That(IsInCombatMode(holderEntMan, cTargetOnHolderClient), Is.False); + Assert.That(IsActionToggled(holderEntMan, cCombatActionOnHolderClient), Is.False); + Assert.That(IsActionEnabled(holderEntMan, cCombatActionOnHolderClient), Is.False); + }); + }); + + await targetClient.WaitAssertion(() => + { + cCombatActionOnTargetClient = ToClientEntity(sEntMan, targetEntMan, sCombatAction); + + Assert.Multiple(() => + { + Assert.That(targetEntMan.HasComponent(cTargetOnTargetClient), Is.True); + Assert.That(IsInCombatMode(targetEntMan, cTargetOnTargetClient), Is.False); + Assert.That(IsActionToggled(targetEntMan, cCombatActionOnTargetClient), Is.False); + Assert.That(IsActionEnabled(targetEntMan, cCombatActionOnTargetClient), Is.False); + }); + }); + } + + [Test] + public async Task VisitingHeldTargetAfterBreakout_ReEnablesCombatAction() + { + using var server = new RobustIntegrationTest.ServerIntegrationInstance(CreateServerOptions()); + using var client = new RobustIntegrationTest.ClientIntegrationInstance(CreateClientOptions()); + + await Task.WhenAll(server.WaitIdleAsync(), client.WaitIdleAsync()); + + await client.Connect(server); + await RunTicks(server, client, 10); + + var sEntMan = server.EntMan; + var cEntMan = client.EntMan; + var sHolding = server.System(); + var sMind = server.System(); + var sPlayerMan = server.ResolveDependency(); + var cState = client.ResolveDependency(); + var cActions = client.System(); + + Assert.That(client.User, Is.Not.Null); + var session = sPlayerMan.GetSessionById(client.User!.Value); + + EntityUid sHolder = default; + EntityUid sTarget = default; + EntityUid sCombatAction = default; + EntityUid sMindId = default; + + await client.WaitPost(() => cState.RequestStateChange()); + await RunTicks(server, client, 20); + + await server.WaitAssertion(() => + { + Assert.Multiple(() => + { + Assert.That(session.AttachedEntity, Is.Not.Null); + Assert.That(session.ContentData()?.Mind, Is.Not.Null); + }); + + sHolder = session.AttachedEntity!.Value; + sMindId = session.ContentData()!.Mind!.Value; + + sEntMan.EnsureComponent(sHolder); + + var holderCoords = sEntMan.GetComponent(sHolder).Coordinates; + sTarget = sEntMan.SpawnEntity("MobHuman", holderCoords.Offset(new Vector2(0.1f, 0f))); + sCombatAction = GetCombatToggleAction(sEntMan, sTarget); + + var holder = sEntMan.GetComponent(sHolder); + Assert.That(sHolding.TryToggleHold((sHolder, holder), sTarget), Is.True); + }); + await RunTicks(server, client, 10); + + EntityUid cTarget = default; + EntityUid cCombatAction = default; + + await client.WaitAssertion(() => + { + cTarget = ToClientEntity(sEntMan, cEntMan, sTarget); + cCombatAction = ToClientEntity(sEntMan, cEntMan, sCombatAction); + + Assert.Multiple(() => + { + Assert.That(cEntMan.HasComponent(cTarget), Is.True); + Assert.That(IsActionEnabled(cEntMan, cCombatAction), Is.False); + }); + }); + + await server.WaitPost(() => sMind.Visit(sMindId, sTarget)); + await RunTicks(server, client, 10); + + await client.WaitAssertion(() => + { + cTarget = ToClientEntity(sEntMan, cEntMan, sTarget); + cCombatAction = ToClientEntity(sEntMan, cEntMan, sCombatAction); + + Assert.Multiple(() => + { + Assert.That(client.AttachedEntity, Is.EqualTo(cTarget)); + Assert.That(cEntMan.HasComponent(cTarget), Is.True); + Assert.That(IsActionEnabled(cEntMan, cCombatAction), Is.False); + }); + }); + + await server.WaitPost(() => RaiseMoveInput(sEntMan, sTarget)); + await RunTicks(server, client, 10); + + await server.WaitAssertion(() => + { + Assert.Multiple(() => + { + Assert.That(sEntMan.HasComponent(sTarget), Is.False); + Assert.That(IsActionEnabled(sEntMan, sCombatAction), Is.True); + }); + }); + + await client.WaitAssertion(() => + { + cTarget = ToClientEntity(sEntMan, cEntMan, sTarget); + cCombatAction = ToClientEntity(sEntMan, cEntMan, sCombatAction); + + Assert.Multiple(() => + { + Assert.That(client.AttachedEntity, Is.EqualTo(cTarget)); + Assert.That(cEntMan.HasComponent(cTarget), Is.False); + Assert.That(IsActionEnabled(cEntMan, cCombatAction), Is.True); + }); + }); + + await client.WaitPost(() => + { + var action = cEntMan.GetComponent(cCombatAction); + cActions.TriggerAction((cCombatAction, action)); + }); + await RunTicks(server, client, 10); + + await server.WaitAssertion(() => + { + Assert.Multiple(() => + { + Assert.That(IsInCombatMode(sEntMan, sTarget), Is.True); + Assert.That(IsActionToggled(sEntMan, sCombatAction), Is.True); + }); + }); + + await client.WaitAssertion(() => + { + Assert.Multiple(() => + { + Assert.That(IsInCombatMode(cEntMan, cTarget), Is.True); + Assert.That(IsActionToggled(cEntMan, cCombatAction), Is.True); + }); + }); + } + + private static RobustIntegrationTest.ServerIntegrationOptions CreateServerOptions() + { + return new RobustIntegrationTest.ServerIntegrationOptions + { + Pool = false, + ContentStart = true, + LoadTestAssembly = false, + ContentAssemblies = + [ + typeof(Shared.Entry.EntryPoint).Assembly, + typeof(Server.Entry.EntryPoint).Assembly + ], + Options = new() + { + LoadConfigAndUserData = false, + }, + }; + } + + private static RobustIntegrationTest.ClientIntegrationOptions CreateClientOptions() + { + var opts = new RobustIntegrationTest.ClientIntegrationOptions + { + Pool = false, + ContentStart = true, + LoadTestAssembly = false, + ContentAssemblies = + [ + typeof(Shared.Entry.EntryPoint).Assembly, + typeof(Client.Entry.EntryPoint).Assembly + ], + Options = new() + { + LoadConfigAndUserData = false, + }, + }; + + opts.InitIoC = () => + { + IoCManager.Register(true); + }; + + opts.BeforeStart += () => + { + IoCManager.Resolve().SetModuleBaseCallbacks(new ClientModuleTestingCallbacks + { + ClientBeforeIoC = () => IoCManager.Register(true) + }); + }; + + return opts; + } + + private static async Task RunTicks( + RobustIntegrationTest.ServerIntegrationInstance server, + RobustIntegrationTest.ClientIntegrationInstance targetClient, + RobustIntegrationTest.ClientIntegrationInstance holderClient, + int ticks) + { + for (var i = 0; i < ticks; i++) + { + await server.WaitRunTicks(1); + await targetClient.WaitRunTicks(1); + await holderClient.WaitRunTicks(1); + } + } + + private static async Task RunTicks( + RobustIntegrationTest.ServerIntegrationInstance server, + RobustIntegrationTest.ClientIntegrationInstance client, + int ticks) + { + for (var i = 0; i < ticks; i++) + { + await server.WaitRunTicks(1); + await client.WaitRunTicks(1); + } + } + + private static EntityUid GetCombatToggleAction(IEntityManager entMan, EntityUid uid) + { + var combat = entMan.GetComponent(uid); + + Assert.That(combat.CombatToggleActionEntity, Is.Not.Null); + return combat.CombatToggleActionEntity!.Value; + } + + private static bool IsInCombatMode(IEntityManager entMan, EntityUid uid) + { + return entMan.GetComponent(uid).IsInCombatMode; + } + + private static bool IsActionToggled(IEntityManager entMan, EntityUid uid) + { + return entMan.GetComponent(uid).Toggled; + } + + private static bool IsActionEnabled(IEntityManager entMan, EntityUid uid) + { + return entMan.GetComponent(uid).Enabled; + } + + private static EntityUid ToClientEntity(IEntityManager serverEntMan, IEntityManager clientEntMan, EntityUid serverEntity) + { + return clientEntMan.GetEntity(serverEntMan.GetNetEntity(serverEntity)); + } + + private static async Task SendClientPullInput( + RobustIntegrationTest.ClientIntegrationInstance client, + IEntityManager entMan, + IGameTiming timing, + EntityUid cursorEntity, + BoundKeyState state) + { + var inputManager = client.ResolveDependency(); + var funcId = inputManager.NetworkBindMap.KeyFunctionID(ContentKeyFunctions.TryPullObject); + var transform = entMan.GetComponent(cursorEntity); + var inputSystem = client.System(); + var message = new ClientFullInputCmdMessage(timing.CurTick, timing.TickFraction, funcId) + { + State = state, + Coordinates = transform.Coordinates, + Uid = cursorEntity, + }; + + await client.WaitPost(() => inputSystem.HandleInputCommand(client.Session!, ContentKeyFunctions.TryPullObject, message)); + } + + private static void RaiseMoveInput(IEntityManager entMan, EntityUid uid) + { + var mover = entMan.GetComponent(uid); + var move = new MoveInputEvent((uid, mover), MoveButtons.None, Direction.East, true); + entMan.EventBus.RaiseLocalEvent(uid, ref move); + } +} diff --git a/Content.Server/_Scp/Holding/ScpHoldingSystem.Feedback.cs b/Content.Server/_Scp/Holding/ScpHoldingSystem.Feedback.cs new file mode 100644 index 00000000000..a3720699bdb --- /dev/null +++ b/Content.Server/_Scp/Holding/ScpHoldingSystem.Feedback.cs @@ -0,0 +1,22 @@ +using Content.Server.Popups; + +namespace Content.Server._Scp.Holding; + +public sealed partial class ScpHoldingSystem +{ + [Dependency] private readonly PopupSystem _popup = default!; + + protected override void PopupHolder(EntityUid holder, string key, params (string, object)[] args) + { + base.PopupHolder(holder, key, args); + + _popup.PopupEntity(Loc.GetString(key, args), holder, holder); + } + + protected override void PopupTarget(EntityUid target, string key, params (string, object)[] args) + { + base.PopupTarget(target, key, args); + + _popup.PopupEntity(Loc.GetString(key, args), target, target); + } +} diff --git a/Content.Server/_Scp/Holding/ScpHoldingSystem.cs b/Content.Server/_Scp/Holding/ScpHoldingSystem.cs index bb019367ef0..5ed39a91d90 100644 --- a/Content.Server/_Scp/Holding/ScpHoldingSystem.cs +++ b/Content.Server/_Scp/Holding/ScpHoldingSystem.cs @@ -4,30 +4,27 @@ namespace Content.Server._Scp.Holding; -public sealed class ScpHoldingSystem : SharedScpHoldingSystem +public sealed partial class ScpHoldingSystem : SharedScpHoldingSystem { - protected override bool ShouldShowHoldPopups => true; - public override void Initialize() { base.Initialize(); - SubscribeLocalEvent(OnHoldShutdown); - SubscribeLocalEvent(OnHandCountChanged); + SubscribeLocalEvent(OnHoldShutdown); + SubscribeLocalEvent(OnHandCountChanged); } - protected override void OnHeldStateShutdown(Entity held) + protected override void OnHeldStateShutdown(Entity held) { foreach (var holderUid in held.Comp.Holders) { - if (TryComp(holderUid, out _)) - RemComp(holderUid); + RemComp(holderUid); } } - private void OnHoldShutdown(Entity ent, ref ComponentShutdown args) + private void OnHoldShutdown(Entity ent, ref ComponentShutdown args) { - if (!TryComp(ent.Owner, out var holder)) + if (!TryComp(ent.Owner, out var holder)) return; if (holder.Target == null) @@ -36,7 +33,7 @@ private void OnHoldShutdown(Entity ent, ref ComponentShutdown ReleaseHolderContribution(ent.Owner, holder.Target.Value, clearIfEmpty: true); } - private void OnHandCountChanged(Entity ent, ref HandCountChangedEvent args) + private void OnHandCountChanged(Entity ent, ref HandCountChangedEvent args) { SyncHeldState(ent); } diff --git a/Content.Shared/_Scp/Holding/Compatibility/PullingSystem.ScpHolding.cs b/Content.Shared/_Scp/Holding/Compatibility/PullingSystem.ScpHolding.cs index 5fc332c8609..3eae22f2bf8 100644 --- a/Content.Shared/_Scp/Holding/Compatibility/PullingSystem.ScpHolding.cs +++ b/Content.Shared/_Scp/Holding/Compatibility/PullingSystem.ScpHolding.cs @@ -10,16 +10,16 @@ public sealed partial class PullingSystem [Dependency] private readonly SharedScpHoldingSystem _scpHolding = default!; private EntityQuery _pullableQuery; - private EntityQuery _scpHoldQuery; + private EntityQuery _scpHolderConfigQuery; private EntityQuery _scpHoldableQuery; - private EntityQuery _scpHolderQuery; + private EntityQuery _scpActiveHolderQuery; private void InitializeScpHolding() { _pullableQuery = GetEntityQuery(); - _scpHoldQuery = GetEntityQuery(); + _scpHolderConfigQuery = GetEntityQuery(); _scpHoldableQuery = GetEntityQuery(); - _scpHolderQuery = GetEntityQuery(); + _scpActiveHolderQuery = GetEntityQuery(); } private bool TryRedirectPullToScpHold(EntityUid pullerUid, EntityUid pullableUid, @@ -27,7 +27,7 @@ private bool TryRedirectPullToScpHold(EntityUid pullerUid, EntityUid pullableUid { result = false; - if (!_scpHoldQuery.TryComp(pullerUid, out var holdComp) || + if (!_scpHolderConfigQuery.TryComp(pullerUid, out var holdComp) || !_scpHoldableQuery.HasComp(pullableUid)) { return false; @@ -35,7 +35,7 @@ private bool TryRedirectPullToScpHold(EntityUid pullerUid, EntityUid pullableUid var holder = (pullerUid, holdComp); - if (_scpHolderQuery.TryComp(pullerUid, out var activeHolder) && + if (_scpActiveHolderQuery.TryComp(pullerUid, out var activeHolder) && activeHolder.Target != null) { result = _scpHolding.TryToggleHold(holder, pullableUid); diff --git a/Content.Shared/_Scp/Holding/Components/ScpHeldComponent.cs b/Content.Shared/_Scp/Holding/Components/ActiveScpHoldableComponent.cs similarity index 90% rename from Content.Shared/_Scp/Holding/Components/ScpHeldComponent.cs rename to Content.Shared/_Scp/Holding/Components/ActiveScpHoldableComponent.cs index d5a9931b81e..6c72f1a8e61 100644 --- a/Content.Shared/_Scp/Holding/Components/ScpHeldComponent.cs +++ b/Content.Shared/_Scp/Holding/Components/ActiveScpHoldableComponent.cs @@ -1,5 +1,7 @@ using Content.Shared._Scp.Holding.Systems; +using Content.Shared.Alert; using Robust.Shared.GameStates; +using Robust.Shared.Prototypes; using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom; namespace Content.Shared._Scp.Holding.Components; @@ -9,7 +11,7 @@ namespace Content.Shared._Scp.Holding.Components; /// [RegisterComponent, NetworkedComponent, AutoGenerateComponentState(true), AutoGenerateComponentPause] [Access(typeof(SharedScpHoldingSystem))] -public sealed partial class ScpHeldComponent : Component +public sealed partial class ActiveScpHoldableComponent : Component { /// /// Next timestamp when a soft breakout attempt may succeed. diff --git a/Content.Shared/_Scp/Holding/Components/ActiveScpHolderComponent.cs b/Content.Shared/_Scp/Holding/Components/ActiveScpHolderComponent.cs new file mode 100644 index 00000000000..8fd19eb7195 --- /dev/null +++ b/Content.Shared/_Scp/Holding/Components/ActiveScpHolderComponent.cs @@ -0,0 +1,18 @@ +using Content.Shared._Scp.Holding.Systems; +using Robust.Shared.GameStates; + +namespace Content.Shared._Scp.Holding.Components; + +/// +/// Runtime contribution state stored on each active holder. +/// +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] +[Access(typeof(SharedScpHoldingSystem))] +public sealed partial class ActiveScpHolderComponent : Component +{ + /// + /// Target currently being contributed to. + /// + [AutoNetworkedField, ViewVariables] + public EntityUid? Target; +} diff --git a/Content.Shared/_Scp/Holding/Components/ScpFullHeldComponent.cs b/Content.Shared/_Scp/Holding/Components/ActiveStateScpHoldableFullHoldComponent.cs similarity index 89% rename from Content.Shared/_Scp/Holding/Components/ScpFullHeldComponent.cs rename to Content.Shared/_Scp/Holding/Components/ActiveStateScpHoldableFullHoldComponent.cs index cd20d639691..775f6ccd933 100644 --- a/Content.Shared/_Scp/Holding/Components/ScpFullHeldComponent.cs +++ b/Content.Shared/_Scp/Holding/Components/ActiveStateScpHoldableFullHoldComponent.cs @@ -9,7 +9,7 @@ namespace Content.Shared._Scp.Holding.Components; /// [RegisterComponent, NetworkedComponent, AutoGenerateComponentState, AutoGenerateComponentPause] [Access(typeof(SharedScpHoldingSystem))] -public sealed partial class ScpFullHeldComponent : Component +public sealed partial class ActiveStateScpHoldableFullHoldComponent : Component { /// /// Timestamp when the current uninterrupted full hold started. diff --git a/Content.Shared/_Scp/Holding/Components/ScpHolderSlowdownComponent.cs b/Content.Shared/_Scp/Holding/Components/ActiveStateScpHolderSlowdownComponent.cs similarity index 90% rename from Content.Shared/_Scp/Holding/Components/ScpHolderSlowdownComponent.cs rename to Content.Shared/_Scp/Holding/Components/ActiveStateScpHolderSlowdownComponent.cs index f411632cdf8..d3518f13e68 100644 --- a/Content.Shared/_Scp/Holding/Components/ScpHolderSlowdownComponent.cs +++ b/Content.Shared/_Scp/Holding/Components/ActiveStateScpHolderSlowdownComponent.cs @@ -8,7 +8,7 @@ namespace Content.Shared._Scp.Holding.Components; /// [RegisterComponent, NetworkedComponent, AutoGenerateComponentState(true)] [Access(typeof(SharedScpHoldingSystem))] -public sealed partial class ScpHolderSlowdownComponent : Component +public sealed partial class ActiveStateScpHolderSlowdownComponent : Component { /// /// Walk speed modifier applied while the holder contributes to an active hold. diff --git a/Content.Shared/_Scp/Holding/Components/ScpHoldComponent.cs b/Content.Shared/_Scp/Holding/Components/ScpHoldComponent.cs deleted file mode 100644 index aa7501d0532..00000000000 --- a/Content.Shared/_Scp/Holding/Components/ScpHoldComponent.cs +++ /dev/null @@ -1,37 +0,0 @@ -using Content.Shared._Scp.Holding.Systems; -using Content.Shared.Whitelist; -using Robust.Shared.GameStates; - -namespace Content.Shared._Scp.Holding.Components; - -/// -/// Grants the owner the ability to contribute to SCP holding. -/// -[RegisterComponent, NetworkedComponent, AutoGenerateComponentState(fieldDeltas: true), AutoGenerateComponentPause] -[Access(typeof(SharedScpHoldingSystem))] -public sealed partial class ScpHoldComponent : Component -{ - /// - /// Next timestamp when this entity may start a new hold contribution. - /// - [AutoNetworkedField, AutoPausedField] - public TimeSpan? HoldAvailableAt; - - /// - /// Optional whitelist of entities this holder may grab. - /// - [DataField] - public EntityWhitelist? HoldableWhitelist; - - /// - /// Optional blacklist of entities this holder may not grab. - /// - [DataField] - public EntityWhitelist? HoldableBlacklist; - - /// - /// Cooldown applied after each successful hold contribution start. - /// - [DataField] - public TimeSpan HoldActionCooldown = TimeSpan.FromSeconds(1); -} diff --git a/Content.Shared/_Scp/Holding/Components/ScpHoldableComponent.cs b/Content.Shared/_Scp/Holding/Components/ScpHoldableComponent.cs index dd29580f822..8f1cf76da66 100644 --- a/Content.Shared/_Scp/Holding/Components/ScpHoldableComponent.cs +++ b/Content.Shared/_Scp/Holding/Components/ScpHoldableComponent.cs @@ -59,6 +59,66 @@ public sealed partial class ScpHoldableComponent : Component [DataField] public float HoldRange = 1f; + /// + /// Scales the preferred soft-drag distance from the configured hold range. + /// + [DataField] + public float SoftDragDistanceFactor = 0.3f; + + /// + /// Lower clamp for the preferred soft-drag distance. + /// + [DataField] + public float SoftDragMinimumDistance = 0.4f; + + /// + /// Upper clamp for the preferred soft-drag distance. + /// + [DataField] + public float SoftDragMaximumDistance = 0.6f; + + /// + /// Distance where the system snaps to the holder-facing direction instead of offset. + /// + [DataField] + public float SoftDragSnapTolerance = 0.03f; + + /// + /// Distance where the held target is considered settled and only matches holder velocity. + /// + [DataField] + public float SoftDragSettleTolerance = 0.08f; + + /// + /// Minimum velocity used to derive drag direction from holder movement. + /// + [DataField] + public float SoftDragVelocityDirectionThreshold = 0.05f; + + /// + /// Minimum time window used to catch the held target back up to its desired position. + /// + [DataField] + public float SoftDragCatchUpTime = 0.05f; + + /// + /// Maximum correction speed applied while soft-dragging the held target. + /// + [DataField] + public float SoftDragMaximumCorrectionSpeed = 6f; + + /// + /// Extra correction strength applied when the held target moves away from its desired position. + /// + [DataField] + public float SoftDragAwayVelocityStrength = 0.6f; + + /// + /// Velocity difference threshold before the held body's velocity is updated. + /// + [DataField] + public float SoftDragVelocityTolerance = 0.05f; + /// /// Walk speed modifier applied to holders while they move this target. /// Lower values make the target heavier to move. diff --git a/Content.Shared/_Scp/Holding/Components/ScpHolderComponent.cs b/Content.Shared/_Scp/Holding/Components/ScpHolderComponent.cs index 8744c019cc1..9cf07cbf47c 100644 --- a/Content.Shared/_Scp/Holding/Components/ScpHolderComponent.cs +++ b/Content.Shared/_Scp/Holding/Components/ScpHolderComponent.cs @@ -1,18 +1,37 @@ using Content.Shared._Scp.Holding.Systems; +using Content.Shared.Whitelist; using Robust.Shared.GameStates; namespace Content.Shared._Scp.Holding.Components; /// -/// Runtime contribution state stored on each active holder. +/// Grants the owner the ability to contribute to SCP holding. /// -[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState(fieldDeltas: true), AutoGenerateComponentPause] [Access(typeof(SharedScpHoldingSystem))] public sealed partial class ScpHolderComponent : Component { /// - /// Target currently being contributed to. + /// Next timestamp when this entity may start a new hold contribution. /// - [AutoNetworkedField, ViewVariables] - public EntityUid? Target; + [AutoNetworkedField, AutoPausedField] + public TimeSpan? HoldAvailableAt; + + /// + /// Optional whitelist of entities this holder may grab. + /// + [DataField] + public EntityWhitelist? HoldableWhitelist; + + /// + /// Optional blacklist of entities this holder may not grab. + /// + [DataField] + public EntityWhitelist? HoldableBlacklist; + + /// + /// Cooldown applied after each successful hold contribution start. + /// + [DataField] + public TimeSpan HoldActionCooldown = TimeSpan.FromSeconds(1); } diff --git a/Content.Shared/_Scp/Holding/ScpHoldingEvents.cs b/Content.Shared/_Scp/Holding/ScpHoldingEvents.cs index 97d24eba5a2..ed7492b63fd 100644 --- a/Content.Shared/_Scp/Holding/ScpHoldingEvents.cs +++ b/Content.Shared/_Scp/Holding/ScpHoldingEvents.cs @@ -16,7 +16,12 @@ public record struct ScpHoldAttemptEvent(EntityUid Holder, EntityUid Target) public readonly record struct ScpHoldBreakoutEvent(bool ViaMovement, bool WasFullHold, bool AppliedImmunity); [Serializable, NetSerializable] -public sealed partial class ScpHoldBreakoutDoAfterEvent(bool viaMovement = false) : SimpleDoAfterEvent +public sealed partial class ScpHoldBreakoutDoAfterEvent : SimpleDoAfterEvent { - public bool ViaMovement = viaMovement; + public bool ViaMovement; + + public ScpHoldBreakoutDoAfterEvent(bool viaMovement = false) + { + ViaMovement = viaMovement; + } } diff --git a/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.Actions.cs b/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.Actions.cs index cee98d6d889..5f82a03307b 100644 --- a/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.Actions.cs +++ b/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.Actions.cs @@ -20,9 +20,9 @@ private void InitializeHoldQueries() _holdableQuery = GetEntityQuery(); } - public bool TryToggleHold(Entity holder, EntityUid target, bool attemptChecked = false) + public bool TryToggleHold(Entity holder, EntityUid target, bool attemptChecked = false) { - if (_holderQuery.TryComp(holder.Owner, out var activeHolder) && activeHolder.Target != null) + if (_activeHolderQuery.TryComp(holder.Owner, out var activeHolder) && activeHolder.Target != null) { if (activeHolder.Target.Value == target) { @@ -49,7 +49,7 @@ public bool TryToggleHold(Entity holder, EntityUid target, boo } public bool CanToggleHold( - Entity holder, + Entity holder, EntityUid target, bool quiet = false, bool ignoreHandAvailability = false, @@ -142,9 +142,9 @@ public bool CanToggleHold( } var range = holdable.HoldRange; - if (_heldQuery.HasComp(target)) + if (_activeHoldableQuery.HasComp(target)) { - if (_fullHeldQuery.HasComp(target)) + if (_activeHoldableFullHoldStateQuery.HasComp(target)) { if (!quiet) PopupHolder(holder.Owner, "scp-hold-target-fully-held", ("target", target)); @@ -167,14 +167,14 @@ public bool CanToggleHold( return true; } - public bool TryBreakOut(Entity held, bool viaMovement) + public bool TryBreakOut(Entity held, bool viaMovement) { - return _fullHeldQuery.HasComp(held.Owner) + return _activeHoldableFullHoldStateQuery.HasComp(held.Owner) ? TryStartFullBreakout(held, viaMovement) : TrySoftBreakOut(held, viaMovement); } - public bool TryForceBreakOut(Entity held, bool viaMovement = false, bool applyImmunity = false) + public bool TryForceBreakOut(Entity held, bool viaMovement = false, bool applyImmunity = false) { if (!Resolve(held, ref held.Comp, false)) return false; @@ -185,24 +185,24 @@ public bool TryForceBreakOut(Entity held, bool viaMovement = protected bool IsFullHold(EntityUid uid) { - return _fullHeldQuery.HasComp(uid); + return _activeHoldableFullHoldStateQuery.HasComp(uid); } - protected void ReconcileHeldAfterState(Entity held) + protected void ReconcileHeldAfterState(Entity held) { OnHeldStateRefreshed(held); - if (_fullHeldQuery.HasComp(held.Owner)) + if (_activeHoldableFullHoldStateQuery.HasComp(held.Owner)) SyncPlaceholderHands(held); } - public void SyncHolderState(Entity holder) + public void SyncHolderState(Entity holder) { SyncHolderHandBlocker(holder); OnHolderStateRefreshed(holder); } - private bool TrySoftBreakOut(Entity held, bool viaMovement) + private bool TrySoftBreakOut(Entity held, bool viaMovement) { if (_timing.CurTime < held.Comp.SoftEscapeAvailableAt) return false; @@ -214,9 +214,9 @@ private bool TrySoftBreakOut(Entity held, bool viaMovement) return true; } - private bool TryStartFullBreakout(Entity held, bool viaMovement) + private bool TryStartFullBreakout(Entity held, bool viaMovement) { - if (!_fullHeldQuery.TryComp(held.Owner, out var fullHeld)) + if (!_activeHoldableFullHoldStateQuery.TryComp(held.Owner, out var fullHeld)) return false; if (!TryGetHeldHoldable(held, out var holdable)) @@ -263,7 +263,7 @@ private bool TryStartFullBreakout(Entity held, bool viaMovemen return true; } - private bool CanStartHold(Entity holder, bool quiet = false) + private bool CanStartHold(Entity holder, bool quiet = false) { if (!IsHoldCoolingDown(holder, out var remaining)) return true; @@ -277,7 +277,7 @@ private bool CanStartHold(Entity holder, bool quiet = false) return false; } - private bool IsHoldCoolingDown(Entity holder, out TimeSpan remaining) + private bool IsHoldCoolingDown(Entity holder, out TimeSpan remaining) { remaining = TimeSpan.Zero; @@ -288,14 +288,14 @@ private bool IsHoldCoolingDown(Entity holder, out TimeSpan rem return true; } - private void StartHoldCooldown(Entity holder) + private void StartHoldCooldown(Entity holder) { SetHoldAvailableAt(holder, _timing.CurTime + holder.Comp.HoldActionCooldown); } private void ApplyFullBreakoutHolderCooldown(EntityUid holderUid) { - if (!_holdQuery.TryComp(holderUid, out var hold)) + if (!_holderConfigQuery.TryComp(holderUid, out var hold)) return; var cooldownEnd = _timing.CurTime + TimeSpan.FromTicks(hold.HoldActionCooldown.Ticks * 2); @@ -305,7 +305,7 @@ private void ApplyFullBreakoutHolderCooldown(EntityUid holderUid) SetHoldAvailableAt((holderUid, hold), cooldownEnd); } - private void SetHoldAvailableAt(Entity holder, TimeSpan? holdAvailableAt) + private void SetHoldAvailableAt(Entity holder, TimeSpan? holdAvailableAt) { if (holder.Comp.HoldAvailableAt == holdAvailableAt) return; @@ -319,7 +319,7 @@ private void SetHoldAvailableAt(Entity holder, TimeSpan? holdA return; } - DirtyField(holder.Owner, holder.Comp, nameof(ScpHoldComponent.HoldAvailableAt)); + DirtyField(holder.Owner, holder.Comp, nameof(ScpHolderComponent.HoldAvailableAt)); } private bool CanPassHoldAttempt(EntityUid holderUid, EntityUid targetUid) @@ -330,13 +330,13 @@ private bool CanPassHoldAttempt(EntityUid holderUid, EntityUid targetUid) return !attempt.Cancelled; } - private void RaiseBreakoutEvent(Entity held, bool viaMovement, bool applyImmunity) + private void RaiseBreakoutEvent(Entity held, bool viaMovement, bool applyImmunity) { - var ev = new ScpHoldBreakoutEvent(viaMovement, _fullHeldQuery.HasComp(held.Owner), applyImmunity); + var ev = new ScpHoldBreakoutEvent(viaMovement, _activeHoldableFullHoldStateQuery.HasComp(held.Owner), applyImmunity); RaiseLocalEvent(held.Owner, ref ev); } - private void BreakOut(Entity held, bool viaMovement, bool applyImmunity) + private void BreakOut(Entity held, bool viaMovement, bool applyImmunity) { RaiseBreakoutEvent(held, viaMovement, applyImmunity); ClearHoldState(held, applyImmunity); diff --git a/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.Drag.cs b/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.Drag.cs index 9d3c8c78122..c0ab991b31a 100644 --- a/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.Drag.cs +++ b/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.Drag.cs @@ -13,24 +13,13 @@ public abstract partial class SharedScpHoldingSystem [Dependency] private readonly SharedTransformSystem _transform = default!; - private const float SoftDragDistanceFactor = 0.3f; - private const float SoftDragMinimumDistance = 0.4f; - private const float SoftDragMaximumDistance = 0.6f; - private const float SoftDragSnapTolerance = 0.03f; - private const float SoftDragSettleTolerance = 0.08f; - private const float SoftDragVelocityDirectionThreshold = 0.05f; - private const float SoftDragCatchUpTime = 0.05f; - private const float SoftDragMaximumCorrectionSpeed = 6f; - private const float SoftDragAwayVelocityStrength = 0.6f; - private const float SoftDragVelocityTolerance = 0.05f; - - private void UpdateSoftDrag(Entity held, float maintenanceRange, float desiredDistance) + private void UpdateSoftDrag(Entity held, ScpHoldableComponent holdable, float maintenanceRange, float desiredDistance) { if (held.Comp.PrimaryHolder == null) return; var primaryHolder = held.Comp.PrimaryHolder.Value; - if (!_holderQuery.TryComp(primaryHolder, out var holder)) + if (!_activeHolderQuery.TryComp(primaryHolder, out var holder)) return; if (holder.Target != held.Owner) @@ -56,67 +45,68 @@ private void UpdateSoftDrag(Entity held, float maintenanceRang var holderVelocity = _physicsQuery.TryComp(primaryHolder, out var holderPhysics) ? holderPhysics.LinearVelocity : Vector2.Zero; - var direction = GetSoftDragDirection(primaryHolder, holderVelocity, offset, distance); + var velocityDirectionThresholdSquared = holdable.SoftDragVelocityDirectionThreshold * holdable.SoftDragVelocityDirectionThreshold; + var direction = GetSoftDragDirection(primaryHolder, holdable, holderVelocity, offset, distance, velocityDirectionThresholdSquared); var desiredPosition = holderCoords.Position + direction * desiredDistance; var correction = desiredPosition - heldCoords.Position; var correctionDistance = correction.Length(); Vector2 desiredVelocity; - if (correctionDistance <= SoftDragSettleTolerance) + if (correctionDistance <= holdable.SoftDragSettleTolerance) { - desiredVelocity = holderVelocity.LengthSquared() > SoftDragVelocityDirectionThreshold * SoftDragVelocityDirectionThreshold + desiredVelocity = holderVelocity.LengthSquared() > velocityDirectionThresholdSquared ? holderVelocity : Vector2.Zero; } else { var correctionDirection = correction / correctionDistance; - var correctionSpeed = Math.Min(correctionDistance / GetSoftDragCatchUpTime(), SoftDragMaximumCorrectionSpeed); + var correctionSpeed = Math.Min(correctionDistance / GetSoftDragCatchUpTime(holdable), holdable.SoftDragMaximumCorrectionSpeed); desiredVelocity = holderVelocity + correctionDirection * correctionSpeed; var relativeVelocity = heldPhysics.LinearVelocity - holderVelocity; var awaySpeed = MathF.Max(0f, -Vector2.Dot(relativeVelocity, correctionDirection)); if (awaySpeed > 0f) - desiredVelocity += correctionDirection * awaySpeed * SoftDragAwayVelocityStrength; + desiredVelocity += correctionDirection * awaySpeed * holdable.SoftDragAwayVelocityStrength; } - ApplyHeldVelocity(held.Owner, desiredVelocity, heldPhysics); + ApplyHeldVelocity(held.Owner, desiredVelocity, heldPhysics, holdable); } - private static float GetDesiredSoftDragDistance(float holdRange) + private static float GetDesiredSoftDragDistance(ScpHoldableComponent holdable) { - return GetBaseSoftDragDistance(holdRange); + return GetBaseSoftDragDistance(holdable); } - private static float GetHoldMaintenanceRange(float configuredRange, float desiredSoftDragDistance) + private static float GetHoldMaintenanceRange(ScpHoldableComponent holdable, float desiredSoftDragDistance) { - return MathF.Max(MathF.Max(configuredRange, SharedInteractionSystem.InteractionRange), desiredSoftDragDistance + SoftDragSnapTolerance); + return MathF.Max(MathF.Max(holdable.HoldRange, SharedInteractionSystem.InteractionRange), desiredSoftDragDistance + holdable.SoftDragSnapTolerance); } - private static float GetBaseSoftDragDistance(float holdRange) + private static float GetBaseSoftDragDistance(ScpHoldableComponent holdable) { - return Math.Clamp(holdRange * SoftDragDistanceFactor, SoftDragMinimumDistance, SoftDragMaximumDistance); + return Math.Clamp(holdable.HoldRange * holdable.SoftDragDistanceFactor, holdable.SoftDragMinimumDistance, holdable.SoftDragMaximumDistance); } - private float GetSoftDragCatchUpTime() + private float GetSoftDragCatchUpTime(ScpHoldableComponent holdable) { - return MathF.Max((float)_timing.TickPeriod.TotalSeconds, SoftDragCatchUpTime); + return MathF.Max((float)_timing.TickPeriod.TotalSeconds, holdable.SoftDragCatchUpTime); } - private Vector2 GetSoftDragDirection(EntityUid holderUid, Vector2 holderVelocity, Vector2 offset, float distance) + private Vector2 GetSoftDragDirection(EntityUid holderUid, ScpHoldableComponent holdable, Vector2 holderVelocity, Vector2 offset, float distance, float velocityDirectionThresholdSquared) { - if (distance > SoftDragSnapTolerance) + if (distance > holdable.SoftDragSnapTolerance) return offset / distance; - if (holderVelocity.LengthSquared() > SoftDragVelocityDirectionThreshold * SoftDragVelocityDirectionThreshold) + if (holderVelocity.LengthSquared() > velocityDirectionThresholdSquared) return -Vector2.Normalize(holderVelocity); return Transform(holderUid).LocalRotation.ToWorldVec(); } - private void ApplyHeldVelocity(EntityUid uid, Vector2 desiredVelocity, PhysicsComponent physics) + private void ApplyHeldVelocity(EntityUid uid, Vector2 desiredVelocity, PhysicsComponent physics, ScpHoldableComponent holdable) { - if (Vector2.DistanceSquared(physics.LinearVelocity, desiredVelocity) > SoftDragVelocityTolerance * SoftDragVelocityTolerance) + if (Vector2.DistanceSquared(physics.LinearVelocity, desiredVelocity) > holdable.SoftDragVelocityTolerance * holdable.SoftDragVelocityTolerance) _physics.SetLinearVelocity(uid, desiredVelocity, body: physics); if (!MathHelper.CloseTo(physics.AngularVelocity, 0f)) diff --git a/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.Events.cs b/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.Events.cs index 4e505b689bd..93ae4471314 100644 --- a/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.Events.cs +++ b/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.Events.cs @@ -1,12 +1,12 @@ using Content.Shared._Scp.Holding.Components; -using Content.Shared.Actions.Events; -using Content.Shared.CombatMode; +using Content.Shared.Alert; using Content.Shared.Hands; using Content.Shared.Hands.EntitySystems; using Content.Shared.Movement.Events; using Content.Shared.Movement.Systems; using Content.Shared.Throwing; using Robust.Shared.Physics.Events; +using Robust.Shared.Prototypes; namespace Content.Shared._Scp.Holding.Systems; @@ -16,51 +16,57 @@ public abstract partial class SharedScpHoldingSystem * Event subscription wiring plus routing/lifecycle reactions for held and holder entities. */ + private static readonly ProtoId HeldAlert = "ScpHoldGrabbed"; + private void SubscribeHoldingEvents() { - SubscribeLocalEvent(OnHeldStartup); - SubscribeLocalEvent(OnHeldShutdown); - SubscribeLocalEvent(OnBreakoutAlert); - SubscribeLocalEvent(OnBreakoutDoAfter); - SubscribeLocalEvent(OnHeldMoveInput); - SubscribeLocalEvent(OnHeldAttemptMobCollide); - SubscribeLocalEvent(OnHeldAttemptMobTargetCollide); - SubscribeLocalEvent(OnHeldCombatModeChanged); - SubscribeLocalEvent(OnHeldPreventCollide); - SubscribeLocalEvent(OnHoldRestrictedActionAttempt); + SubscribeLocalEvent(OnHeldStartup); + SubscribeLocalEvent(OnHeldShutdown); + SubscribeLocalEvent(OnHeldRemove); + SubscribeLocalEvent(OnBreakoutAlert); + SubscribeLocalEvent(OnBreakoutDoAfter); + SubscribeLocalEvent(OnHeldMoveInput); + SubscribeLocalEvent(OnHeldAttemptMobCollide); + SubscribeLocalEvent(OnHeldAttemptMobTargetCollide); + SubscribeLocalEvent(OnHeldPreventCollide); SubscribeLocalEvent(OnBreakoutAttemptStartup); SubscribeLocalEvent(OnBreakoutAttemptShutdown); - SubscribeLocalEvent(OnFullHeldStartup); - SubscribeLocalEvent(OnFullHeldRemove); - SubscribeLocalEvent(OnFullHeldUpdateCanMove); - - SubscribeLocalEvent(OnHolderStartup); - SubscribeLocalEvent(OnHolderShutdown); - SubscribeLocalEvent(OnHolderBeforeThrow); - SubscribeLocalEvent(OnHolderHandsModified); - SubscribeLocalEvent(OnHolderPreventCollide); - SubscribeLocalEvent(OnHolderSlowdownRemove); - SubscribeLocalEvent(OnHolderSlowdownAfterState); - SubscribeLocalEvent(OnHolderSlowdownRefreshMoveSpeed); + SubscribeLocalEvent(OnFullHeldStartup); + SubscribeLocalEvent(OnFullHeldRemove); + SubscribeLocalEvent(OnFullHeldUpdateCanMove); + + SubscribeLocalEvent(OnHolderStartup); + SubscribeLocalEvent(OnHolderShutdown); + SubscribeLocalEvent(OnHolderBeforeThrow); + SubscribeLocalEvent(OnHolderHandsModified); + SubscribeLocalEvent(OnHolderPreventCollide); + SubscribeLocalEvent(OnHolderSlowdownRemove); + SubscribeLocalEvent(OnHolderSlowdownAfterState); + SubscribeLocalEvent(OnHolderSlowdownRefreshMoveSpeed); SubscribeLocalEvent(OnHolderBlockerDropped); } - private void OnHeldStartup(Entity ent, ref ComponentStartup args) + private void OnHeldStartup(Entity ent, ref ComponentStartup args) { - _alerts.ShowAlert(ent.Owner, "ScpHoldGrabbed"); + _alerts.ShowAlert(ent.Owner, HeldAlert); SyncHeldStatusEffect(ent.Owner); - EnsureCombatModeDisabled(ent.Owner); OnHeldStateRefreshed(ent); + ValidateAllActions(ent.Owner); } - private void OnHeldShutdown(Entity ent, ref ComponentShutdown args) + private void OnHeldShutdown(Entity ent, ref ComponentShutdown args) { - _alerts.ClearAlert(ent.Owner, "ScpHoldGrabbed"); + _alerts.ClearAlert(ent.Owner, HeldAlert); _statusEffects.TryRemoveStatusEffect(ent, GrabbedStatusEffect); OnHeldStateShutdown(ent); } - private void OnBreakoutAlert(Entity ent, ref ScpHoldBreakoutAlertEvent args) + private void OnHeldRemove(Entity ent, ref ComponentRemove args) + { + ValidateAllActions(ent.Owner); + } + + private void OnBreakoutAlert(Entity ent, ref ScpHoldBreakoutAlertEvent args) { if (args.Handled) return; @@ -69,7 +75,7 @@ private void OnBreakoutAlert(Entity ent, ref ScpHoldBreakoutAl TryBreakOut(ent, viaMovement: false); } - private void OnBreakoutDoAfter(Entity ent, ref ScpHoldBreakoutDoAfterEvent args) + private void OnBreakoutDoAfter(Entity ent, ref ScpHoldBreakoutDoAfterEvent args) { EndBreakoutAttempt(ent.Owner, cancelDoAfter: false); @@ -88,7 +94,7 @@ private void OnBreakoutDoAfter(Entity ent, ref ScpHoldBreakout private void OnBreakoutAttemptStartup(Entity ent, ref ComponentStartup args) { - if (!_heldQuery.TryComp(ent.Owner, out var held)) + if (!_activeHoldableQuery.TryComp(ent.Owner, out var held)) return; ShowBreakoutAttemptFeedback((ent.Owner, held)); @@ -102,7 +108,7 @@ private void OnBreakoutAttemptShutdown(Entity ent, CancelBreakoutAttemptDoAfter(doAfterId); } - private void OnHeldMoveInput(Entity ent, ref MoveInputEvent args) + private void OnHeldMoveInput(Entity ent, ref MoveInputEvent args) { if (!IsBreakoutMovementPress(args)) return; @@ -110,81 +116,81 @@ private void OnHeldMoveInput(Entity ent, ref MoveInputEvent ar TryBreakOut(ent, viaMovement: true); } - private static void OnFullHeldUpdateCanMove(Entity ent, ref UpdateCanMoveEvent args) + private static void OnFullHeldUpdateCanMove(Entity ent, ref UpdateCanMoveEvent args) { args.Cancel(); } - private static void OnHeldAttemptMobCollide(Entity ent, ref AttemptMobCollideEvent args) + private static void OnHeldAttemptMobCollide(Entity ent, ref AttemptMobCollideEvent args) { args.Cancelled = true; } - private static void OnHeldAttemptMobTargetCollide(Entity ent, ref AttemptMobTargetCollideEvent args) + private static void OnHeldAttemptMobTargetCollide(Entity ent, ref AttemptMobTargetCollideEvent args) { args.Cancelled = true; } - private void OnHeldPreventCollide(Entity ent, ref PreventCollideEvent args) + private void OnHeldPreventCollide(Entity ent, ref PreventCollideEvent args) { if (args.Cancelled) return; - if (_holderQuery.TryComp(args.OtherEntity, out var holder) && + if (_activeHolderQuery.TryComp(args.OtherEntity, out var holder) && holder.Target == ent.Owner) { args.Cancelled = true; } } - private void OnFullHeldStartup(Entity ent, ref ComponentStartup args) + private void OnFullHeldStartup(Entity ent, ref ComponentStartup args) { - if (_heldQuery.TryComp(ent.Owner, out var held)) + if (_activeHoldableQuery.TryComp(ent.Owner, out var held)) SyncPlaceholderHands((ent.Owner, held)); ZeroHeldVelocity(ent.Owner); _actionBlocker.UpdateCanMove(ent.Owner); } - private void OnFullHeldRemove(Entity ent, ref ComponentRemove args) + private void OnFullHeldRemove(Entity ent, ref ComponentRemove args) { DeleteHeldHandBlockers(ent.Owner); _actionBlocker.UpdateCanMove(ent.Owner); } - private void OnHolderStartup(Entity ent, ref ComponentStartup args) + private void OnHolderStartup(Entity ent, ref ComponentStartup args) { SyncHolderState(ent); } - private void OnHolderShutdown(Entity ent, ref ComponentShutdown args) + private void OnHolderShutdown(Entity ent, ref ComponentShutdown args) { var target = ent.Comp.Target; ent.Comp.Target = null; DeleteHolderHandBlockers(ent.Owner); if (!_timing.ApplyingState) - RemComp(ent.Owner); + RemComp(ent.Owner); OnHolderStateShutdown(ent.Owner, target); } - private void OnHolderSlowdownRemove(Entity ent, ref ComponentRemove args) + private void OnHolderSlowdownRemove(Entity ent, ref ComponentRemove args) { _movement.RefreshMovementSpeedModifiers(ent.Owner); } - private void OnHolderSlowdownAfterState(Entity ent, ref AfterAutoHandleStateEvent args) + private void OnHolderSlowdownAfterState(Entity ent, ref AfterAutoHandleStateEvent args) { _movement.RefreshMovementSpeedModifiers(ent.Owner); } - private void OnHolderSlowdownRefreshMoveSpeed(Entity ent, ref RefreshMovementSpeedModifiersEvent args) + private void OnHolderSlowdownRefreshMoveSpeed(Entity ent, ref RefreshMovementSpeedModifiersEvent args) { args.ModifySpeed(ent.Comp.WalkModifier, ent.Comp.SprintModifier); } - private void OnHolderBeforeThrow(Entity ent, ref BeforeThrowEvent args) + private void OnHolderBeforeThrow(Entity ent, ref BeforeThrowEvent args) { if (ent.Comp.Target == null) return; @@ -199,7 +205,7 @@ private void OnHolderBeforeThrow(Entity ent, ref BeforeThrow args.Cancelled = true; } - private void OnHolderHandsModified(Entity ent, ref DidEquipHandEvent args) + private void OnHolderHandsModified(Entity ent, ref DidEquipHandEvent args) { if (ent.Comp.LifeStage > ComponentLifeStage.Running) return; @@ -207,13 +213,13 @@ private void OnHolderHandsModified(Entity ent, ref DidEquipH if (ent.Comp.Target == null) return; - if (!_heldQuery.HasComp(ent.Comp.Target.Value)) + if (!_activeHoldableQuery.HasComp(ent.Comp.Target.Value)) return; SyncHolderState(ent); } - private void OnHolderPreventCollide(Entity ent, ref PreventCollideEvent args) + private void OnHolderPreventCollide(Entity ent, ref PreventCollideEvent args) { if (args.Cancelled) return; @@ -229,7 +235,7 @@ private void OnHolderPreventCollide(Entity ent, ref PreventC private void OnHolderBlockerDropped(Entity ent, ref GettingDroppedAttemptEvent args) { - if (!_holderQuery.TryComp(args.User, out var holder)) + if (!_activeHolderQuery.TryComp(args.User, out var holder)) return; if (holder.Target == null) diff --git a/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.Feedback.cs b/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.Feedback.cs index b9a2a9b2f09..e5517e4e1c2 100644 --- a/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.Feedback.cs +++ b/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.Feedback.cs @@ -1,6 +1,5 @@ using Content.Shared._Scp.Holding.Components; using Content.Shared.Coordinates; -using Content.Shared.Popups; using Robust.Shared.Audio.Systems; using Robust.Shared.Audio; using Robust.Shared.Prototypes; @@ -14,19 +13,18 @@ public abstract partial class SharedScpHoldingSystem */ [Dependency] private readonly SharedAudioSystem _audio = default!; - [Dependency] private readonly SharedPopupSystem _popup = default!; - private void ShowBreakoutAttemptFeedback(Entity held) + private void ShowBreakoutAttemptFeedback(Entity held) { if (!CanShowBreakoutAttemptFeedback()) return; - if (!TryComp(held.Owner, out var holdable)) + if (!TryComp(held, out var holdable)) return; foreach (var holderUid in held.Comp.Holders) { - if (!_holderQuery.TryComp(holderUid, out var holder)) + if (!_activeHolderQuery.TryComp(holderUid, out var holder)) continue; if (holder.Target != held.Owner) @@ -38,22 +36,6 @@ private void ShowBreakoutAttemptFeedback(Entity held) PlayBreakoutAttemptSound(held.Owner, holdable.BreakoutAttemptSound); } - private void PopupHolder(EntityUid holder, string key, params (string, object)[] args) - { - if (!ShouldShowHoldPopups) - return; - - _popup.PopupEntity(Loc.GetString(key, args), holder, holder); - } - - private void PopupTarget(EntityUid target, string key, params (string, object)[] args) - { - if (!ShouldShowHoldPopups) - return; - - _popup.PopupEntity(Loc.GetString(key, args), target, target); - } - private void SpawnBreakoutAttemptEffect(EntityUid holderUid, EntProtoId? effect) { if (effect == null) @@ -82,7 +64,9 @@ private void PlayBreakoutAttemptSound(EntityUid targetUid, SoundSpecifier? sound _audio.PlayPvs(sound, targetUid); } - protected virtual bool ShouldShowHoldPopups => false; + protected virtual void PopupHolder(EntityUid holder, string key, params (string, object)[] args) { } + + protected virtual void PopupTarget(EntityUid target, string key, params (string, object)[] args) { } protected virtual bool ShouldUsePredictedBreakoutFeedback => false; diff --git a/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.Hands.cs b/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.Hands.cs index 050600c19c6..b3d7749a51d 100644 --- a/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.Hands.cs +++ b/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.Hands.cs @@ -20,18 +20,26 @@ public abstract partial class SharedScpHoldingSystem private readonly List> _virtualBlockersToDelete = []; private EntityQuery _handsQuery; + private EntityQuery _virtualItemQuery; + private EntityQuery _heldHandBlockerQuery; + private EntityQuery _holdHandBlockerQuery; + private EntityQuery _unremoveableQuery; private void InitializeHandQueries() { _handsQuery = GetEntityQuery(); + _virtualItemQuery = GetEntityQuery(); + _heldHandBlockerQuery = GetEntityQuery(); + _holdHandBlockerQuery = GetEntityQuery(); + _unremoveableQuery = GetEntityQuery(); } - private void SyncPlaceholderHands(Entity held) + private void SyncPlaceholderHands(Entity held) { if (!_handsQuery.TryComp(held.Owner, out var hands)) return; - if (!_fullHeldQuery.HasComp(held.Owner)) + if (!_activeHoldableFullHoldStateQuery.HasComp(held.Owner)) { DeleteHeldHandBlockers(held.Owner); return; @@ -51,13 +59,13 @@ private void SyncPlaceholderHands(Entity held) EnsureHeldHandBlockers(heldHands); } - private void CollectPlaceholderIconHolders(Entity held) + private void CollectPlaceholderIconHolders(Entity held) { _placeholderIcons.Clear(); foreach (var holderUid in held.Comp.Holders) { - if (_holderQuery.TryComp(holderUid, out var holder) && + if (_activeHolderQuery.TryComp(holderUid, out var holder) && holder.Target == held.Owner) { _placeholderIcons.Add(holderUid); @@ -72,7 +80,7 @@ private void DropHeldItemsForPlaceholders(Entity held) if (!_hands.TryGetHeldItem(held, hand, out var heldItem)) continue; - if (HasComp(heldItem.Value)) + if (_unremoveableQuery.HasComp(heldItem.Value)) continue; _hands.DoDrop(held, hand, doDropInteraction: true); @@ -85,10 +93,10 @@ private void DeleteInvalidHeldHandBlockers(Entity held) foreach (var heldItem in _hands.EnumerateHeld(held)) { - if (!TryComp(heldItem, out var virtualItem)) + if (!_virtualItemQuery.TryComp(heldItem, out var virtualItem)) continue; - if (!TryComp(heldItem, out var blocker)) + if (!_heldHandBlockerQuery.TryComp(heldItem, out var blocker)) continue; if (!TrySyncHeldHandBlocker((heldItem, blocker), virtualItem, held.Owner)) @@ -159,7 +167,7 @@ private void SyncHeldStatusEffect(EntityUid target) PredictedTrySpawnInContainer(GrabbedStatusEffect, target, StatusEffectContainerComponent.ContainerId, out _); } - private void SyncHolderHandBlocker(Entity holder) + private void SyncHolderHandBlocker(Entity holder) { _virtualBlockersToDelete.Clear(); EntityUid? validBlocker = null; @@ -167,10 +175,8 @@ private void SyncHolderHandBlocker(Entity holder) foreach (var heldItem in _hands.EnumerateHeld(holder.Owner)) { - if (!TryComp(heldItem, out var virtualItem)) - { + if (!_virtualItemQuery.TryComp(heldItem, out var virtualItem)) continue; - } var matchesCurrentTarget = holder.Comp.LifeStage <= ComponentLifeStage.Running && target != null && @@ -182,7 +188,7 @@ private void SyncHolderHandBlocker(Entity holder) { validBlocker = heldItem; RemComp(heldItem); - var existingBlockerCreated = !TryComp(heldItem, out var blocker); + var existingBlockerCreated = !_holdHandBlockerQuery.TryComp(heldItem, out var blocker); blocker ??= EnsureComp(heldItem); var currentTarget = target!.Value; if (blocker.Target != currentTarget) @@ -198,7 +204,7 @@ private void SyncHolderHandBlocker(Entity holder) } } - if (TryComp(heldItem, out _) || matchesCurrentTarget) + if (_holdHandBlockerQuery.HasComp(heldItem) || matchesCurrentTarget) _virtualBlockersToDelete.Add((heldItem, virtualItem)); } @@ -223,7 +229,7 @@ private void SyncHolderHandBlocker(Entity holder) if (!_virtualItem.TrySpawnVirtualItemInHand(holder.Comp.Target.Value, holder.Owner, out var spawnedVirtualItem, silent: true)) return; - TryComp(spawnedVirtualItem.Value, out var blockerComp); + _holdHandBlockerQuery.TryComp(spawnedVirtualItem.Value, out var blockerComp); blockerComp ??= EnsureComp(spawnedVirtualItem.Value); blockerComp.Target = holder.Comp.Target.Value; Dirty(spawnedVirtualItem.Value, blockerComp); @@ -241,8 +247,8 @@ private void DeleteHolderHandBlockers(EntityUid holderUid) foreach (var heldItem in _hands.EnumerateHeld(holderUid)) { - if (TryComp(heldItem, out _) && - TryComp(heldItem, out var virtualItem)) + if (_holdHandBlockerQuery.HasComp(heldItem) && + _virtualItemQuery.TryComp(heldItem, out var virtualItem)) { _virtualBlockersToDelete.Add((heldItem, virtualItem)); } @@ -260,8 +266,8 @@ private void DeleteHeldHandBlockers(EntityUid heldUid) foreach (var heldItem in _hands.EnumerateHeld(heldUid)) { - if (TryComp(heldItem, out _) && - TryComp(heldItem, out var virtualItem)) + if (_heldHandBlockerQuery.HasComp(heldItem) && + _virtualItemQuery.TryComp(heldItem, out var virtualItem)) { _virtualBlockersToDelete.Add((heldItem, virtualItem)); } diff --git a/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.Restrictions.cs b/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.Restrictions.cs index 367cf12b98e..4a525e28a2b 100644 --- a/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.Restrictions.cs +++ b/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.Restrictions.cs @@ -1,54 +1,88 @@ using Content.Shared._Scp.Holding.Components; +using Content.Shared.Actions; +using Content.Shared.Actions.Components; using Content.Shared.Actions.Events; using Content.Shared.CombatMode; +using Content.Shared.Popups; namespace Content.Shared._Scp.Holding.Systems; public abstract partial class SharedScpHoldingSystem { [Dependency] private readonly SharedCombatModeSystem _combatMode = default!; + [Dependency] private readonly SharedPopupSystem _popup = default!; + [Dependency] private readonly SharedActionsSystem _actions = default!; - private void OnHoldRestrictedActionAttempt(Entity ent, ref ActionAttemptEvent args) + private void InitializeRestrictions() { - if (args.Cancelled || !IsHeldAtStage(args.User, ent.Comp.Stage)) + SubscribeLocalEvent(OnRestrictionInit); + SubscribeLocalEvent(OnRestrictRemove); + SubscribeLocalEvent(OnHoldRestrictedActionAttempt); + } + + private void OnRestrictionInit(Entity ent, ref ComponentInit args) + { + ValidateActions(ent.AsNullable()); + } + + private void OnRestrictRemove(Entity ent, ref ComponentShutdown args) + { + ValidateActions(ent.AsNullable()); + } + + private void ValidateAllActions(Entity ent) + { + if (!Resolve(ent, ref ent.Comp, false)) return; - args.Cancelled = true; - _popup.PopupClient(Loc.GetString("scp-hold-action-restricted"), args.User, args.User); + foreach (var action in ent.Comp.Actions) + { + ValidateActions(action); + } + } + + private void ValidateActions(Entity ent, ActionComponent? comp = null) + { + if (!Resolve(ent, ref ent.Comp, false)) + return; + + if (!Resolve(ent, ref comp)) + return; + + if (!comp.AttachedEntity.HasValue) + return; + + var shouldBeBlocked = IsHeldAtStage(comp.AttachedEntity.Value, ent.Comp.Stage); + if (shouldBeBlocked) + { + if (TryComp(comp.AttachedEntity.Value, out var combat)) + _combatMode.SetInCombatMode(comp.AttachedEntity.Value, false, combat); + } + + _actions.SetEnabled(ent.Owner, !shouldBeBlocked); } - private void OnHeldCombatModeChanged(Entity ent, ref CombatModeChangedEvent args) + private void OnHoldRestrictedActionAttempt(Entity ent, ref ActionAttemptEvent args) { - if (!args.IsInCombatMode) + if (args.Cancelled || !IsHeldAtStage(args.User, ent.Comp.Stage)) return; - EnsureCombatModeDisabled(ent.Owner); + _popup.PopupClient(Loc.GetString("scp-hold-action-restricted"), args.User, args.User); + args.Cancelled = true; } public bool IsHeldAtStage(EntityUid uid, ScpHoldStage stage) { - return _heldQuery.TryComp(uid, out var held) && IsHeldAtStage((uid, held), stage); + return _activeHoldableQuery.TryComp(uid, out var held) && IsHeldAtStage((uid, held), stage); } - private bool IsHeldAtStage(Entity held, ScpHoldStage stage) + private bool IsHeldAtStage(Entity held, ScpHoldStage stage) { return stage switch { ScpHoldStage.Soft => true, - ScpHoldStage.Full => _fullHeldQuery.HasComp(held.Owner), + ScpHoldStage.Full => _activeHoldableFullHoldStateQuery.HasComp(held.Owner), _ => false, }; } - - private void EnsureCombatModeDisabled(EntityUid uid) - { - if (!IsHeldAtStage(uid, ScpHoldStage.Soft) || - !TryComp(uid, out var combatMode) || - !combatMode.IsInCombatMode) - { - return; - } - - _combatMode.SetInCombatMode(uid, false, combatMode); - } } diff --git a/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.State.cs b/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.State.cs index f6842b528f0..20b33299cac 100644 --- a/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.State.cs +++ b/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.State.cs @@ -24,7 +24,7 @@ private void InitializeStateQueries() _bodyQuery = GetEntityQuery(); } - private void UpdateHeld(Entity held) + private void UpdateHeld(Entity held) { if (!TryGetHeldHoldable(held, out var holdable)) return; @@ -35,11 +35,11 @@ private void UpdateHeld(Entity held) return; } - var desiredSoftDragDistance = GetDesiredSoftDragDistance(holdable.HoldRange); - var maintenanceRange = GetHoldMaintenanceRange(holdable.HoldRange, desiredSoftDragDistance); + var desiredSoftDragDistance = GetDesiredSoftDragDistance(holdable); + var maintenanceRange = GetHoldMaintenanceRange(holdable, desiredSoftDragDistance); - if (!_fullHeldQuery.HasComp(held.Owner)) - UpdateSoftDrag(held, maintenanceRange, desiredSoftDragDistance); + if (!_activeHoldableFullHoldStateQuery.HasComp(held.Owner)) + UpdateSoftDrag(held, holdable, maintenanceRange, desiredSoftDragDistance); else ZeroHeldVelocity(held.Owner); @@ -55,18 +55,18 @@ private void UpdateHeld(Entity held) { ReleaseHolderContribution(holderUid, held.Owner, clearIfEmpty: false); - if (!_heldQuery.TryComp(held.Owner, out _)) + if (!_activeHoldableQuery.TryComp(held.Owner, out _)) return; } - if (_heldQuery.TryComp(held.Owner, out var refreshed)) + if (_activeHoldableQuery.TryComp(held.Owner, out var refreshed)) SyncHeldState((held.Owner, refreshed)); } - private Entity EnsureHeldState(EntityUid target) + private Entity EnsureHeldState(EntityUid target) { - var created = !_heldQuery.TryComp(target, out var held); - held ??= EnsureComp(target); + var created = !_activeHoldableQuery.TryComp(target, out var held); + held ??= EnsureComp(target); if (created) held.SoftEscapeAvailableAt = _timing.CurTime; @@ -75,7 +75,7 @@ private Entity EnsureHeldState(EntityUid target) return (target, held); } - private void AddHolderContribution(EntityUid holderUid, Entity held) + private void AddHolderContribution(EntityUid holderUid, Entity held) { if (!held.Comp.Holders.Contains(holderUid)) { @@ -83,8 +83,8 @@ private void AddHolderContribution(EntityUid holderUid, Entity Dirty(held); } - var holderCreated = !_holderQuery.TryComp(holderUid, out var holder); - holder ??= EnsureComp(holderUid); + var holderCreated = !_activeHolderQuery.TryComp(holderUid, out var holder); + holder ??= EnsureComp(holderUid); SetHolderTarget((holderUid, holder), held.Owner); SyncHolderState((holderUid, holder)); @@ -94,7 +94,7 @@ private void AddHolderContribution(EntityUid holderUid, Entity protected void ReleaseHolderContribution(EntityUid holderUid, EntityUid targetUid, bool clearIfEmpty) { - if (!_heldQuery.TryComp(targetUid, out var held)) + if (!_activeHoldableQuery.TryComp(targetUid, out var held)) return; var removed = false; @@ -110,10 +110,10 @@ protected void ReleaseHolderContribution(EntityUid holderUid, EntityUid targetUi if (removed) Dirty(targetUid, held); - if (_holderQuery.HasComp(holderUid)) - RemComp(holderUid); - else if (_holderSlowdownQuery.HasComp(holderUid)) - RemComp(holderUid); + if (_activeHolderQuery.HasComp(holderUid)) + RemComp(holderUid); + else if (_activeHolderSlowdownStateQuery.HasComp(holderUid)) + RemComp(holderUid); if (held.PrimaryHolder == holderUid) SetHeldPrimaryHolder((targetUid, held), null); @@ -128,9 +128,9 @@ protected void ReleaseHolderContribution(EntityUid holderUid, EntityUid targetUi SyncHeldState((targetUid, held)); } - protected void SyncHeldState(Entity held) + protected void SyncHeldState(Entity held) { - if (!_heldQuery.TryComp(held.Owner, out var heldComp)) + if (!_activeHoldableQuery.TryComp(held.Owner, out var heldComp)) return; held.Comp = heldComp; @@ -159,17 +159,17 @@ protected void SyncHeldState(Entity held) } ExitFullHold(held); - var desiredSoftDragDistance = GetDesiredSoftDragDistance(holdable.HoldRange); - var maintenanceRange = GetHoldMaintenanceRange(holdable.HoldRange, desiredSoftDragDistance); - UpdateSoftDrag(held, maintenanceRange, desiredSoftDragDistance); + var desiredSoftDragDistance = GetDesiredSoftDragDistance(holdable); + var maintenanceRange = GetHoldMaintenanceRange(holdable, desiredSoftDragDistance); + UpdateSoftDrag(held, holdable, maintenanceRange, desiredSoftDragDistance); UpdateHolderSlowdowns(held, holdable); SyncPlaceholderHands(held); } - private void EnterFullHold(Entity held, ScpHoldableComponent holdable) + private void EnterFullHold(Entity held, ScpHoldableComponent holdable) { - var fullHeldCreated = !_fullHeldQuery.TryComp(held.Owner, out var fullHeld); - fullHeld ??= EnsureComp(held.Owner); + var fullHeldCreated = !_activeHoldableFullHoldStateQuery.TryComp(held.Owner, out var fullHeld); + fullHeld ??= EnsureComp(held.Owner); if (fullHeldCreated) { @@ -186,16 +186,16 @@ private void EnterFullHold(Entity held, ScpHoldableComponent h ZeroHeldVelocity(held.Owner); } - private void ExitFullHold(Entity held) + private void ExitFullHold(Entity held) { - if (!_fullHeldQuery.HasComp(held.Owner)) + if (!_activeHoldableFullHoldStateQuery.HasComp(held.Owner)) return; EndBreakoutAttempt(held.Owner, cancelDoAfter: true); - RemComp(held.Owner); + RemComp(held.Owner); } - private bool EnsurePrimaryHolder(Entity held) + private bool EnsurePrimaryHolder(Entity held) { if (held.Comp.PrimaryHolder != null && IsValidPrimaryHolder(held, held.Comp.PrimaryHolder.Value)) return true; @@ -204,7 +204,7 @@ private bool EnsurePrimaryHolder(Entity held) foreach (var holderUid in held.Comp.Holders) { - if (!_holderQuery.TryComp(holderUid, out var holder)) + if (!_activeHolderQuery.TryComp(holderUid, out var holder)) continue; if (holder.Target != held.Owner) @@ -217,15 +217,15 @@ private bool EnsurePrimaryHolder(Entity held) return false; } - private void ClearHoldState(Entity held, bool applyImmunity) + private void ClearHoldState(Entity held, bool applyImmunity) { - if (_heldQuery.TryComp(held.Owner, out var refreshed)) + if (_activeHoldableQuery.TryComp(held.Owner, out var refreshed)) held = (held.Owner, refreshed); EndBreakoutAttempt(held.Owner, cancelDoAfter: true); - if (_fullHeldQuery.HasComp(held.Owner)) - RemComp(held.Owner); + if (_activeHoldableFullHoldStateQuery.HasComp(held.Owner)) + RemComp(held.Owner); _holderCooldownsToApply.Clear(); @@ -234,10 +234,10 @@ private void ClearHoldState(Entity held, bool applyImmunity) if (applyImmunity) _holderCooldownsToApply.Add(holderUid); - if (_holderQuery.HasComp(holderUid)) - RemComp(holderUid); - else if (_holderSlowdownQuery.HasComp(holderUid)) - RemComp(holderUid); + if (_activeHolderQuery.HasComp(holderUid)) + RemComp(holderUid); + else if (_activeHolderSlowdownStateQuery.HasComp(holderUid)) + RemComp(holderUid); } held.Comp.Holders.Clear(); @@ -260,10 +260,10 @@ private void ClearHoldState(Entity held, bool applyImmunity) ApplyFullBreakoutHolderCooldown(holderUid); } - RemComp(held.Owner); + RemComp(held.Owner); } - private void UpdateHolderSlowdowns(Entity held, ScpHoldableComponent holdable) + private void UpdateHolderSlowdowns(Entity held, ScpHoldableComponent holdable) { foreach (var holderUid in held.Comp.Holders) { @@ -273,8 +273,8 @@ private void UpdateHolderSlowdowns(Entity held, ScpHoldableCom private void SetHolderSlowdown(EntityUid holderUid, float walkModifier, float sprintModifier) { - var slowdownCreated = !_holderSlowdownQuery.TryComp(holderUid, out var slowdown); - slowdown ??= EnsureComp(holderUid); + var slowdownCreated = !_activeHolderSlowdownStateQuery.TryComp(holderUid, out var slowdown); + slowdown ??= EnsureComp(holderUid); if (!slowdownCreated && MathHelper.CloseTo(slowdown.WalkModifier, walkModifier) && @@ -306,7 +306,7 @@ private int GetRequiredHolderCount(EntityUid target) return 2; } - private bool TryGetHeldHoldable(Entity held, [NotNullWhen(true)] out ScpHoldableComponent? holdable) + private bool TryGetHeldHoldable(Entity held, [NotNullWhen(true)] out ScpHoldableComponent? holdable) { if (_holdableQuery.TryComp(held.Owner, out holdable)) return true; @@ -318,10 +318,10 @@ private bool TryGetHeldHoldable(Entity held, [NotNullWhen(true private bool ShouldReleaseHolder(EntityUid holderUid, EntityUid heldUid, float maintenanceRange) { - if (!_holdQuery.HasComp(holderUid)) + if (!_holderConfigQuery.HasComp(holderUid)) return true; - if (!_holderQuery.TryComp(holderUid, out var holder)) + if (!_activeHolderQuery.TryComp(holderUid, out var holder)) return true; if (holder.Target != heldUid) @@ -333,9 +333,9 @@ private bool ShouldReleaseHolder(EntityUid holderUid, EntityUid heldUid, float m return !_interaction.InRangeUnobstructed(holderUid, heldUid, maintenanceRange); } - private bool IsValidPrimaryHolder(Entity held, EntityUid primaryHolderUid) + private bool IsValidPrimaryHolder(Entity held, EntityUid primaryHolderUid) { - if (!_holderQuery.TryComp(primaryHolderUid, out var holder)) + if (!_activeHolderQuery.TryComp(primaryHolderUid, out var holder)) return false; if (holder.Target != held.Owner) @@ -344,7 +344,7 @@ private bool IsValidPrimaryHolder(Entity held, EntityUid prima return held.Comp.Holders.Contains(primaryHolderUid); } - private void SetHolderTarget(Entity holder, EntityUid? target) + private void SetHolderTarget(Entity holder, EntityUid? target) { if (holder.Comp.Target == target) return; @@ -353,7 +353,7 @@ private void SetHolderTarget(Entity holder, EntityUid? targe Dirty(holder); } - private void SetHeldPrimaryHolder(Entity held, EntityUid? primaryHolder) + private void SetHeldPrimaryHolder(Entity held, EntityUid? primaryHolder) { if (held.Comp.PrimaryHolder == primaryHolder) return; diff --git a/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.cs b/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.cs index 5b14ad03a76..9e9eb57707e 100644 --- a/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.cs +++ b/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.cs @@ -35,29 +35,30 @@ public abstract partial class SharedScpHoldingSystem : EntitySystem private readonly Dictionary _breakoutDoAfterIds = []; private EntityQuery _breakoutAttemptQuery; - private EntityQuery _fullHeldQuery; + private EntityQuery _activeHoldableFullHoldStateQuery; private EntityQuery _physicsQuery; - private EntityQuery _heldQuery; - private EntityQuery _holdQuery; - private EntityQuery _holderQuery; - private EntityQuery _holderSlowdownQuery; + private EntityQuery _activeHoldableQuery; + private EntityQuery _holderConfigQuery; + private EntityQuery _activeHolderQuery; + private EntityQuery _activeHolderSlowdownStateQuery; public override void Initialize() { base.Initialize(); _breakoutAttemptQuery = GetEntityQuery(); - _fullHeldQuery = GetEntityQuery(); + _activeHoldableFullHoldStateQuery = GetEntityQuery(); _physicsQuery = GetEntityQuery(); - _heldQuery = GetEntityQuery(); - _holdQuery = GetEntityQuery(); - _holderQuery = GetEntityQuery(); - _holderSlowdownQuery = GetEntityQuery(); + _activeHoldableQuery = GetEntityQuery(); + _holderConfigQuery = GetEntityQuery(); + _activeHolderQuery = GetEntityQuery(); + _activeHolderSlowdownStateQuery = GetEntityQuery(); InitializeHoldQueries(); InitializeHandQueries(); InitializeStateQueries(); SubscribeHoldingEvents(); + InitializeRestrictions(); } public override void Shutdown() @@ -77,7 +78,7 @@ public override void Update(float frameTime) RemCompDeferred(uid); } - var heldQuery = EntityQueryEnumerator(); + var heldQuery = EntityQueryEnumerator(); while (heldQuery.MoveNext(out var uid, out var held)) { if (!ShouldUpdateHeld(uid, held)) @@ -87,20 +88,20 @@ public override void Update(float frameTime) } } - protected virtual bool ShouldUpdateHeld(EntityUid uid, ScpHeldComponent held) + protected virtual bool ShouldUpdateHeld(EntityUid uid, ActiveScpHoldableComponent held) { return true; } - protected virtual void OnHeldStateRefreshed(Entity held) + protected virtual void OnHeldStateRefreshed(Entity held) { } - protected virtual void OnHeldStateShutdown(Entity held) + protected virtual void OnHeldStateShutdown(Entity held) { } - protected virtual void OnHolderStateRefreshed(Entity holder) + protected virtual void OnHolderStateRefreshed(Entity holder) { } diff --git a/Content.Shared/_Scp/Scp096/Main/Systems/SharedScp096System.Holding.cs b/Content.Shared/_Scp/Scp096/Main/Systems/SharedScp096System.Holding.cs index 334ece1a628..a1a2be919d4 100644 --- a/Content.Shared/_Scp/Scp096/Main/Systems/SharedScp096System.Holding.cs +++ b/Content.Shared/_Scp/Scp096/Main/Systems/SharedScp096System.Holding.cs @@ -1,5 +1,7 @@ using System.Numerics; using Content.Shared._Scp.Holding; +using Content.Shared._Scp.Holding.Components; +using Content.Shared._Scp.Holding.Systems; using Content.Shared._Scp.Scp096.Main.Components; using Robust.Shared.Physics.Systems; @@ -20,7 +22,7 @@ private void InitializeHolding() private void OnHoldAttempt(Entity ent, ref ScpHoldAttemptEvent args) { if (IsInHoldRestrictedState(ent.Owner)) - args.Cancel(); + args.Cancelled = true; } private void OnHoldBreakout(Entity ent, ref ScpHoldBreakoutEvent args) @@ -31,7 +33,7 @@ private void OnHoldBreakout(Entity ent, ref ScpHoldBreakoutEven if (!args.WasFullHold && !IsInHoldRestrictedState(ent.Owner)) return; - if (!TryComp(ent.Owner, out var held)) + if (!TryComp(ent.Owner, out var held)) return; var scpPosition = _transform.GetWorldPosition(ent.Owner); @@ -50,7 +52,7 @@ protected bool IsInHoldRestrictedState(EntityUid uid) protected void TryBreakOutOfHold(EntityUid uid) { - _holding.TryForceBreakOut((uid, (ScpHeldComponent?) null)); + _holding.TryForceBreakOut((uid, (ActiveScpHoldableComponent?) null)); } private void ApplyHoldBreakoutEffects(Entity ent, EntityUid holderUid, Vector2 scpPosition) diff --git a/Resources/Prototypes/Entities/Mobs/Species/base.yml b/Resources/Prototypes/Entities/Mobs/Species/base.yml index 2ed83ae1f95..f5fd8a7565e 100644 --- a/Resources/Prototypes/Entities/Mobs/Species/base.yml +++ b/Resources/Prototypes/Entities/Mobs/Species/base.yml @@ -278,7 +278,7 @@ - type: CritHeartbeat # Sunrise-End # Fire start - - type: ScpHold # TODO: Убрать перед мержем + - type: ScpHolder # TODO: Убрать перед мержем - type: ScpHoldable - type: FieldOfView - type: Blinkable diff --git a/Resources/Prototypes/_Scp/Roles/Jobs/Administration/security_commander.yml b/Resources/Prototypes/_Scp/Roles/Jobs/Administration/security_commander.yml index 9e80eb00110..123f201d142 100644 --- a/Resources/Prototypes/_Scp/Roles/Jobs/Administration/security_commander.yml +++ b/Resources/Prototypes/_Scp/Roles/Jobs/Administration/security_commander.yml @@ -33,7 +33,7 @@ class: B - type: AccessLevel level: Four - - type: ScpHold + - type: ScpHolder holdableWhitelist: components: - ClassDAppearance diff --git a/Resources/Prototypes/_Scp/Roles/Jobs/CommandantSquad/external_administrative_zone_commandant.yml b/Resources/Prototypes/_Scp/Roles/Jobs/CommandantSquad/external_administrative_zone_commandant.yml index ada744bf5d5..8ea1fcd0a32 100644 --- a/Resources/Prototypes/_Scp/Roles/Jobs/CommandantSquad/external_administrative_zone_commandant.yml +++ b/Resources/Prototypes/_Scp/Roles/Jobs/CommandantSquad/external_administrative_zone_commandant.yml @@ -28,7 +28,7 @@ class: C - type: AccessLevel level: Three - - type: ScpHold + - type: ScpHolder holdableWhitelist: components: - HumanoidAppearance diff --git a/Resources/Prototypes/_Scp/Roles/Jobs/CommandantSquad/external_administrative_zone_officer.yml b/Resources/Prototypes/_Scp/Roles/Jobs/CommandantSquad/external_administrative_zone_officer.yml index 43e61dd14f7..6208db982ef 100644 --- a/Resources/Prototypes/_Scp/Roles/Jobs/CommandantSquad/external_administrative_zone_officer.yml +++ b/Resources/Prototypes/_Scp/Roles/Jobs/CommandantSquad/external_administrative_zone_officer.yml @@ -28,7 +28,7 @@ class: C - type: AccessLevel level: Three - - type: ScpHold + - type: ScpHolder holdableWhitelist: components: - HumanoidAppearance diff --git a/Resources/Prototypes/_Scp/Roles/Jobs/CommandantSquad/field_medical_specialist.yml b/Resources/Prototypes/_Scp/Roles/Jobs/CommandantSquad/field_medical_specialist.yml index 3a06e87df35..88b9d322534 100644 --- a/Resources/Prototypes/_Scp/Roles/Jobs/CommandantSquad/field_medical_specialist.yml +++ b/Resources/Prototypes/_Scp/Roles/Jobs/CommandantSquad/field_medical_specialist.yml @@ -31,7 +31,7 @@ class: C - type: AccessLevel level: Three - - type: ScpHold + - type: ScpHolder holdableWhitelist: components: - HumanoidAppearance diff --git a/Resources/Prototypes/_Scp/Roles/Jobs/CommandantSquad/junior_external_administrative_zone_officer.yml b/Resources/Prototypes/_Scp/Roles/Jobs/CommandantSquad/junior_external_administrative_zone_officer.yml index f83018f592f..cca447f6666 100644 --- a/Resources/Prototypes/_Scp/Roles/Jobs/CommandantSquad/junior_external_administrative_zone_officer.yml +++ b/Resources/Prototypes/_Scp/Roles/Jobs/CommandantSquad/junior_external_administrative_zone_officer.yml @@ -27,7 +27,7 @@ class: C - type: AccessLevel level: Three - - type: ScpHold + - type: ScpHolder holdableWhitelist: components: - HumanoidAppearance diff --git a/Resources/Prototypes/_Scp/Roles/Jobs/CommandantSquad/senior_external_administrative_zone_officer.yml b/Resources/Prototypes/_Scp/Roles/Jobs/CommandantSquad/senior_external_administrative_zone_officer.yml index d3709e6686d..7ebe0f1464a 100644 --- a/Resources/Prototypes/_Scp/Roles/Jobs/CommandantSquad/senior_external_administrative_zone_officer.yml +++ b/Resources/Prototypes/_Scp/Roles/Jobs/CommandantSquad/senior_external_administrative_zone_officer.yml @@ -28,7 +28,7 @@ class: C - type: AccessLevel level: Three - - type: ScpHold + - type: ScpHolder holdableWhitelist: components: - HumanoidAppearance diff --git a/Resources/Prototypes/_Scp/Roles/Jobs/LowAccessPersonnel/class_d.yml b/Resources/Prototypes/_Scp/Roles/Jobs/LowAccessPersonnel/class_d.yml index abf63f08253..ed04e5fabcf 100644 --- a/Resources/Prototypes/_Scp/Roles/Jobs/LowAccessPersonnel/class_d.yml +++ b/Resources/Prototypes/_Scp/Roles/Jobs/LowAccessPersonnel/class_d.yml @@ -18,7 +18,7 @@ - ExternalAdministrativeZoneSecurityService includeJobName: true requiredGameRunLevel: InRound - - type: ScpHold + - type: ScpHolder holdableWhitelist: components: - Scp096 diff --git a/Resources/Prototypes/_Scp/Roles/Jobs/LowAccessPersonnel/class_d_botanist.yml b/Resources/Prototypes/_Scp/Roles/Jobs/LowAccessPersonnel/class_d_botanist.yml index 67eeb5fca00..57951816fc0 100644 --- a/Resources/Prototypes/_Scp/Roles/Jobs/LowAccessPersonnel/class_d_botanist.yml +++ b/Resources/Prototypes/_Scp/Roles/Jobs/LowAccessPersonnel/class_d_botanist.yml @@ -23,7 +23,7 @@ - ExternalAdministrativeZoneSecurityService includeJobName: true requiredGameRunLevel: InRound - - type: ScpHold + - type: ScpHolder holdableWhitelist: components: - Scp096 diff --git a/Resources/Prototypes/_Scp/Roles/Jobs/LowAccessPersonnel/class_d_cook.yml b/Resources/Prototypes/_Scp/Roles/Jobs/LowAccessPersonnel/class_d_cook.yml index fa4d0e42134..fb00bcd70b3 100644 --- a/Resources/Prototypes/_Scp/Roles/Jobs/LowAccessPersonnel/class_d_cook.yml +++ b/Resources/Prototypes/_Scp/Roles/Jobs/LowAccessPersonnel/class_d_cook.yml @@ -23,7 +23,7 @@ - ExternalAdministrativeZoneSecurityService includeJobName: true requiredGameRunLevel: InRound - - type: ScpHold + - type: ScpHolder holdableWhitelist: components: - Scp096 diff --git a/Resources/Prototypes/_Scp/Roles/Jobs/LowAccessPersonnel/class_d_janitor.yml b/Resources/Prototypes/_Scp/Roles/Jobs/LowAccessPersonnel/class_d_janitor.yml index b316f0af7c1..0cd5dbdd3c6 100644 --- a/Resources/Prototypes/_Scp/Roles/Jobs/LowAccessPersonnel/class_d_janitor.yml +++ b/Resources/Prototypes/_Scp/Roles/Jobs/LowAccessPersonnel/class_d_janitor.yml @@ -20,7 +20,7 @@ - ExternalAdministrativeZoneSecurityService includeJobName: true requiredGameRunLevel: InRound - - type: ScpHold + - type: ScpHolder holdableWhitelist: components: - Scp096 diff --git a/Resources/Prototypes/_Scp/Roles/Jobs/SpecialPurposeSquad/heavy_containment_zone_commandant.yml b/Resources/Prototypes/_Scp/Roles/Jobs/SpecialPurposeSquad/heavy_containment_zone_commandant.yml index 676697729c5..6eac3a5a809 100644 --- a/Resources/Prototypes/_Scp/Roles/Jobs/SpecialPurposeSquad/heavy_containment_zone_commandant.yml +++ b/Resources/Prototypes/_Scp/Roles/Jobs/SpecialPurposeSquad/heavy_containment_zone_commandant.yml @@ -28,7 +28,7 @@ class: C - type: AccessLevel level: Three - - type: ScpHold + - type: ScpHolder holdableWhitelist: components: - Scp096 diff --git a/Resources/Prototypes/_Scp/Roles/Jobs/SpecialPurposeSquad/heavy_containment_zone_officer.yml b/Resources/Prototypes/_Scp/Roles/Jobs/SpecialPurposeSquad/heavy_containment_zone_officer.yml index 5c988ff75ce..ee9bb872ba4 100644 --- a/Resources/Prototypes/_Scp/Roles/Jobs/SpecialPurposeSquad/heavy_containment_zone_officer.yml +++ b/Resources/Prototypes/_Scp/Roles/Jobs/SpecialPurposeSquad/heavy_containment_zone_officer.yml @@ -28,7 +28,7 @@ class: C - type: AccessLevel level: Three - - type: ScpHold + - type: ScpHolder holdableWhitelist: components: - Scp096 diff --git a/Resources/Prototypes/_Scp/Roles/Jobs/SpecialPurposeSquad/junior_heavy_containment_zone_officer.yml b/Resources/Prototypes/_Scp/Roles/Jobs/SpecialPurposeSquad/junior_heavy_containment_zone_officer.yml index cf3914e9b3d..54065100cfe 100644 --- a/Resources/Prototypes/_Scp/Roles/Jobs/SpecialPurposeSquad/junior_heavy_containment_zone_officer.yml +++ b/Resources/Prototypes/_Scp/Roles/Jobs/SpecialPurposeSquad/junior_heavy_containment_zone_officer.yml @@ -27,7 +27,7 @@ class: C - type: AccessLevel level: Three - - type: ScpHold + - type: ScpHolder holdableWhitelist: components: - Scp096 diff --git a/Resources/Prototypes/_Scp/Roles/Jobs/SpecialPurposeSquad/senior_heavy_containment_zone_officer.yml b/Resources/Prototypes/_Scp/Roles/Jobs/SpecialPurposeSquad/senior_heavy_containment_zone_officer.yml index 82e0a99ca57..bfe0afb2589 100644 --- a/Resources/Prototypes/_Scp/Roles/Jobs/SpecialPurposeSquad/senior_heavy_containment_zone_officer.yml +++ b/Resources/Prototypes/_Scp/Roles/Jobs/SpecialPurposeSquad/senior_heavy_containment_zone_officer.yml @@ -28,7 +28,7 @@ class: C - type: AccessLevel level: Three - - type: ScpHold + - type: ScpHolder holdableWhitelist: components: - Scp096 From 86b6d06a5c24f2ffde116d1a9cff245f7ba1c182 Mon Sep 17 00:00:00 2001 From: drdth Date: Fri, 17 Apr 2026 09:59:18 +0300 Subject: [PATCH 12/27] refactor: clean up code structure --- .../Tests/_Scp/ScpHoldingTest.cs | 25 ++ .../SharedScpHoldingSystem.BreakoutAttempt.cs | 90 +++++- .../Systems/SharedScpHoldingSystem.Drag.cs | 53 ++++ .../Systems/SharedScpHoldingSystem.Events.cs | 269 ------------------ .../Systems/SharedScpHoldingSystem.Hands.cs | 67 ++++- .../SharedScpHoldingSystem.Lifecycle.cs | 103 +++++++ .../Systems/SharedScpHoldingSystem.State.cs | 10 + .../Holding/Systems/SharedScpHoldingSystem.cs | 24 +- 8 files changed, 339 insertions(+), 302 deletions(-) delete mode 100644 Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.Events.cs create mode 100644 Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.Lifecycle.cs diff --git a/Content.IntegrationTests/Tests/_Scp/ScpHoldingTest.cs b/Content.IntegrationTests/Tests/_Scp/ScpHoldingTest.cs index 14b8983e452..9ae63455be9 100644 --- a/Content.IntegrationTests/Tests/_Scp/ScpHoldingTest.cs +++ b/Content.IntegrationTests/Tests/_Scp/ScpHoldingTest.cs @@ -22,6 +22,7 @@ using Content.Shared.Movement.Pulling.Systems; using Content.Shared.Movement.Systems; using Content.Shared.StatusEffectNew; +using Content.Shared.StatusEffectNew.Components; using Content.Shared.Throwing; using Robust.Client.Input; using Robust.Server.Console; @@ -83,6 +84,30 @@ private static EntityWhitelist CreateComponentWhitelist(params string[] componen - TestListener """; + [Test] + public async Task HoldAppliesStatusEffectImmediately() + { + await using var pair = await PoolManager.GetServerClient(new PoolSettings { Fresh = true }); + var server = pair.Server; + var entMan = server.EntMan; + var statusEffects = server.System(); + var holding = server.System(); + var map = await pair.CreateTestMap(); + + await server.WaitPost(() => + { + var holder = entMan.SpawnEntity(HolderPrototype, map.GridCoords); + var target = entMan.SpawnEntity("MobHuman", map.GridCoords); + StartHold(entMan, holding, holder, target); + + Assert.That(statusEffects.TryGetStatusEffect(target, "StatusEffectScpHeld", out var effect), Is.True); + Assert.That(effect, Is.Not.Null); + Assert.That(entMan.GetComponent(effect!.Value).Applied, Is.True); + }); + + await pair.CleanReturnAsync(); + } + [Test] public async Task SoftHoldBreakoutByMovementAndAlertRespectsCooldown() { diff --git a/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.BreakoutAttempt.cs b/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.BreakoutAttempt.cs index ca143b7d75a..77ea64276af 100644 --- a/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.BreakoutAttempt.cs +++ b/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.BreakoutAttempt.cs @@ -1,14 +1,32 @@ using Content.Shared._Scp.Holding.Components; using Content.Shared.DoAfter; +using Content.Shared.Movement.Events; +using Content.Shared.Movement.Systems; namespace Content.Shared._Scp.Holding.Systems; public abstract partial class SharedScpHoldingSystem { /* - * Semantic breakout-attempt state plus private do-after handle tracking. + * Breakout-attempt query cache, event routing, semantic state, and do-after handle tracking. */ + private EntityQuery _breakoutAttemptQuery; + + private void InitializeBreakoutAttemptQueries() + { + _breakoutAttemptQuery = GetEntityQuery(); + } + + private void InitializeBreakoutAttemptEvents() + { + SubscribeLocalEvent(OnBreakoutAlert); + SubscribeLocalEvent(OnBreakoutDoAfter); + SubscribeLocalEvent(OnHeldMoveInput); + SubscribeLocalEvent(OnBreakoutAttemptStartup); + SubscribeLocalEvent(OnBreakoutAttemptShutdown); + } + private void StartBreakoutAttempt(EntityUid uid, DoAfterId doAfterId) { _breakoutDoAfterIds[uid] = doAfterId; @@ -34,4 +52,74 @@ private void CancelBreakoutAttemptDoAfter(DoAfterId doAfterId) _doAfter.Cancel(doAfterId); } + + private void OnBreakoutAlert(Entity ent, ref ScpHoldBreakoutAlertEvent args) + { + if (args.Handled) + return; + + args.Handled = true; + TryBreakOut(ent, viaMovement: false); + } + + private void OnBreakoutDoAfter(Entity ent, ref ScpHoldBreakoutDoAfterEvent args) + { + EndBreakoutAttempt(ent.Owner, cancelDoAfter: false); + + if (args.Handled) + return; + + if (args.Cancelled) + { + PopupTarget(ent.Owner, "scp-hold-breakout-interrupted"); + return; + } + + BreakOut(ent, args.ViaMovement, applyImmunity: true); + args.Handled = true; + } + + private void OnBreakoutAttemptStartup(Entity ent, ref ComponentStartup args) + { + if (!_activeHoldableQuery.TryComp(ent.Owner, out var held)) + return; + + ShowBreakoutAttemptFeedback((ent.Owner, held)); + } + + private void OnBreakoutAttemptShutdown(Entity ent, ref ComponentShutdown args) + { + if (!_breakoutDoAfterIds.Remove(ent.Owner, out var doAfterId)) + return; + + CancelBreakoutAttemptDoAfter(doAfterId); + } + + private void OnHeldMoveInput(Entity ent, ref MoveInputEvent args) + { + if (!IsBreakoutMovementPress(args)) + return; + + TryBreakOut(ent, viaMovement: true); + } + + private static bool IsBreakoutMovementPress(MoveInputEvent args) + { + if (!args.State) + return false; + + var pressedButton = args.Dir switch + { + Direction.East => MoveButtons.Right, + Direction.North => MoveButtons.Up, + Direction.West => MoveButtons.Left, + Direction.South => MoveButtons.Down, + _ => MoveButtons.None, + }; + + if (pressedButton == MoveButtons.None) + return false; + + return (args.OldMovement & pressedButton) == MoveButtons.None; + } } diff --git a/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.Drag.cs b/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.Drag.cs index c0ab991b31a..82309121bff 100644 --- a/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.Drag.cs +++ b/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.Drag.cs @@ -1,7 +1,9 @@ using System.Numerics; using Content.Shared._Scp.Holding.Components; using Content.Shared.Interaction; +using Content.Shared.Movement.Systems; using Robust.Shared.Physics.Components; +using Robust.Shared.Physics.Events; namespace Content.Shared._Scp.Holding.Systems; @@ -13,6 +15,21 @@ public abstract partial class SharedScpHoldingSystem [Dependency] private readonly SharedTransformSystem _transform = default!; + private EntityQuery _physicsQuery; + + private void InitializeDragQueries() + { + _physicsQuery = GetEntityQuery(); + } + + private void InitializeDragEvents() + { + SubscribeLocalEvent(OnHeldAttemptMobCollide); + SubscribeLocalEvent(OnHeldAttemptMobTargetCollide); + SubscribeLocalEvent(OnHeldPreventCollide); + SubscribeLocalEvent(OnHolderPreventCollide); + } + private void UpdateSoftDrag(Entity held, ScpHoldableComponent holdable, float maintenanceRange, float desiredDistance) { if (held.Comp.PrimaryHolder == null) @@ -124,4 +141,40 @@ private void ZeroHeldVelocity(EntityUid uid) _physics.SetLinearVelocity(uid, Vector2.Zero, body: physics); _physics.SetAngularVelocity(uid, 0f, body: physics); } + + private static void OnHeldAttemptMobCollide(Entity ent, ref AttemptMobCollideEvent args) + { + args.Cancelled = true; + } + + private static void OnHeldAttemptMobTargetCollide(Entity ent, ref AttemptMobTargetCollideEvent args) + { + args.Cancelled = true; + } + + private void OnHeldPreventCollide(Entity ent, ref PreventCollideEvent args) + { + if (args.Cancelled) + return; + + if (_activeHolderQuery.TryComp(args.OtherEntity, out var holder) && + holder.Target == ent.Owner) + { + args.Cancelled = true; + } + } + + private void OnHolderPreventCollide(Entity ent, ref PreventCollideEvent args) + { + if (args.Cancelled) + return; + + if (ent.Comp.Target == null) + return; + + if (ent.Comp.Target != args.OtherEntity) + return; + + args.Cancelled = true; + } } diff --git a/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.Events.cs b/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.Events.cs deleted file mode 100644 index 93ae4471314..00000000000 --- a/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.Events.cs +++ /dev/null @@ -1,269 +0,0 @@ -using Content.Shared._Scp.Holding.Components; -using Content.Shared.Alert; -using Content.Shared.Hands; -using Content.Shared.Hands.EntitySystems; -using Content.Shared.Movement.Events; -using Content.Shared.Movement.Systems; -using Content.Shared.Throwing; -using Robust.Shared.Physics.Events; -using Robust.Shared.Prototypes; - -namespace Content.Shared._Scp.Holding.Systems; - -public abstract partial class SharedScpHoldingSystem -{ - /* - * Event subscription wiring plus routing/lifecycle reactions for held and holder entities. - */ - - private static readonly ProtoId HeldAlert = "ScpHoldGrabbed"; - - private void SubscribeHoldingEvents() - { - SubscribeLocalEvent(OnHeldStartup); - SubscribeLocalEvent(OnHeldShutdown); - SubscribeLocalEvent(OnHeldRemove); - SubscribeLocalEvent(OnBreakoutAlert); - SubscribeLocalEvent(OnBreakoutDoAfter); - SubscribeLocalEvent(OnHeldMoveInput); - SubscribeLocalEvent(OnHeldAttemptMobCollide); - SubscribeLocalEvent(OnHeldAttemptMobTargetCollide); - SubscribeLocalEvent(OnHeldPreventCollide); - SubscribeLocalEvent(OnBreakoutAttemptStartup); - SubscribeLocalEvent(OnBreakoutAttemptShutdown); - SubscribeLocalEvent(OnFullHeldStartup); - SubscribeLocalEvent(OnFullHeldRemove); - SubscribeLocalEvent(OnFullHeldUpdateCanMove); - - SubscribeLocalEvent(OnHolderStartup); - SubscribeLocalEvent(OnHolderShutdown); - SubscribeLocalEvent(OnHolderBeforeThrow); - SubscribeLocalEvent(OnHolderHandsModified); - SubscribeLocalEvent(OnHolderPreventCollide); - SubscribeLocalEvent(OnHolderSlowdownRemove); - SubscribeLocalEvent(OnHolderSlowdownAfterState); - SubscribeLocalEvent(OnHolderSlowdownRefreshMoveSpeed); - SubscribeLocalEvent(OnHolderBlockerDropped); - } - - private void OnHeldStartup(Entity ent, ref ComponentStartup args) - { - _alerts.ShowAlert(ent.Owner, HeldAlert); - SyncHeldStatusEffect(ent.Owner); - OnHeldStateRefreshed(ent); - ValidateAllActions(ent.Owner); - } - - private void OnHeldShutdown(Entity ent, ref ComponentShutdown args) - { - _alerts.ClearAlert(ent.Owner, HeldAlert); - _statusEffects.TryRemoveStatusEffect(ent, GrabbedStatusEffect); - OnHeldStateShutdown(ent); - } - - private void OnHeldRemove(Entity ent, ref ComponentRemove args) - { - ValidateAllActions(ent.Owner); - } - - private void OnBreakoutAlert(Entity ent, ref ScpHoldBreakoutAlertEvent args) - { - if (args.Handled) - return; - - args.Handled = true; - TryBreakOut(ent, viaMovement: false); - } - - private void OnBreakoutDoAfter(Entity ent, ref ScpHoldBreakoutDoAfterEvent args) - { - EndBreakoutAttempt(ent.Owner, cancelDoAfter: false); - - if (args.Handled) - return; - - if (args.Cancelled) - { - PopupTarget(ent.Owner, "scp-hold-breakout-interrupted"); - return; - } - - BreakOut(ent, args.ViaMovement, applyImmunity: true); - args.Handled = true; - } - - private void OnBreakoutAttemptStartup(Entity ent, ref ComponentStartup args) - { - if (!_activeHoldableQuery.TryComp(ent.Owner, out var held)) - return; - - ShowBreakoutAttemptFeedback((ent.Owner, held)); - } - - private void OnBreakoutAttemptShutdown(Entity ent, ref ComponentShutdown args) - { - if (!_breakoutDoAfterIds.Remove(ent.Owner, out var doAfterId)) - return; - - CancelBreakoutAttemptDoAfter(doAfterId); - } - - private void OnHeldMoveInput(Entity ent, ref MoveInputEvent args) - { - if (!IsBreakoutMovementPress(args)) - return; - - TryBreakOut(ent, viaMovement: true); - } - - private static void OnFullHeldUpdateCanMove(Entity ent, ref UpdateCanMoveEvent args) - { - args.Cancel(); - } - - private static void OnHeldAttemptMobCollide(Entity ent, ref AttemptMobCollideEvent args) - { - args.Cancelled = true; - } - - private static void OnHeldAttemptMobTargetCollide(Entity ent, ref AttemptMobTargetCollideEvent args) - { - args.Cancelled = true; - } - - private void OnHeldPreventCollide(Entity ent, ref PreventCollideEvent args) - { - if (args.Cancelled) - return; - - if (_activeHolderQuery.TryComp(args.OtherEntity, out var holder) && - holder.Target == ent.Owner) - { - args.Cancelled = true; - } - } - - private void OnFullHeldStartup(Entity ent, ref ComponentStartup args) - { - if (_activeHoldableQuery.TryComp(ent.Owner, out var held)) - SyncPlaceholderHands((ent.Owner, held)); - - ZeroHeldVelocity(ent.Owner); - _actionBlocker.UpdateCanMove(ent.Owner); - } - - private void OnFullHeldRemove(Entity ent, ref ComponentRemove args) - { - DeleteHeldHandBlockers(ent.Owner); - _actionBlocker.UpdateCanMove(ent.Owner); - } - - private void OnHolderStartup(Entity ent, ref ComponentStartup args) - { - SyncHolderState(ent); - } - - private void OnHolderShutdown(Entity ent, ref ComponentShutdown args) - { - var target = ent.Comp.Target; - ent.Comp.Target = null; - DeleteHolderHandBlockers(ent.Owner); - - if (!_timing.ApplyingState) - RemComp(ent.Owner); - - OnHolderStateShutdown(ent.Owner, target); - } - - private void OnHolderSlowdownRemove(Entity ent, ref ComponentRemove args) - { - _movement.RefreshMovementSpeedModifiers(ent.Owner); - } - - private void OnHolderSlowdownAfterState(Entity ent, ref AfterAutoHandleStateEvent args) - { - _movement.RefreshMovementSpeedModifiers(ent.Owner); - } - - private void OnHolderSlowdownRefreshMoveSpeed(Entity ent, ref RefreshMovementSpeedModifiersEvent args) - { - args.ModifySpeed(ent.Comp.WalkModifier, ent.Comp.SprintModifier); - } - - private void OnHolderBeforeThrow(Entity ent, ref BeforeThrowEvent args) - { - if (ent.Comp.Target == null) - return; - - if (!TryComp(args.ItemUid, out var blocker)) - return; - - if (blocker.Target != ent.Comp.Target.Value) - return; - - ReleaseHolderContribution(ent.Owner, ent.Comp.Target.Value, clearIfEmpty: true); - args.Cancelled = true; - } - - private void OnHolderHandsModified(Entity ent, ref DidEquipHandEvent args) - { - if (ent.Comp.LifeStage > ComponentLifeStage.Running) - return; - - if (ent.Comp.Target == null) - return; - - if (!_activeHoldableQuery.HasComp(ent.Comp.Target.Value)) - return; - - SyncHolderState(ent); - } - - private void OnHolderPreventCollide(Entity ent, ref PreventCollideEvent args) - { - if (args.Cancelled) - return; - - if (ent.Comp.Target == null) - return; - - if (ent.Comp.Target != args.OtherEntity) - return; - - args.Cancelled = true; - } - - private void OnHolderBlockerDropped(Entity ent, ref GettingDroppedAttemptEvent args) - { - if (!_activeHolderQuery.TryComp(args.User, out var holder)) - return; - - if (holder.Target == null) - return; - - if (holder.Target != ent.Comp.Target) - return; - - ReleaseHolderContribution(args.User, ent.Comp.Target, clearIfEmpty: true); - } - - private static bool IsBreakoutMovementPress(MoveInputEvent args) - { - if (!args.State) - return false; - - var pressedButton = args.Dir switch - { - Direction.East => MoveButtons.Right, - Direction.North => MoveButtons.Up, - Direction.West => MoveButtons.Left, - Direction.South => MoveButtons.Down, - _ => MoveButtons.None, - }; - - if (pressedButton == MoveButtons.None) - return false; - - return (args.OldMovement & pressedButton) == MoveButtons.None; - } -} diff --git a/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.Hands.cs b/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.Hands.cs index b3d7749a51d..7903d740658 100644 --- a/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.Hands.cs +++ b/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.Hands.cs @@ -1,9 +1,10 @@ using Content.Shared._Scp.Holding.Components; +using Content.Shared.Hands; using Content.Shared.Hands.Components; using Content.Shared.Hands.EntitySystems; using Content.Shared.Interaction.Components; using Content.Shared.Inventory.VirtualItem; -using Content.Shared.StatusEffectNew.Components; +using Content.Shared.Throwing; namespace Content.Shared._Scp.Holding.Systems; @@ -34,6 +35,13 @@ private void InitializeHandQueries() _unremoveableQuery = GetEntityQuery(); } + private void InitializeHandEvents() + { + SubscribeLocalEvent(OnHolderBeforeThrow); + SubscribeLocalEvent(OnHolderHandsModified); + SubscribeLocalEvent(OnHolderBlockerDropped); + } + private void SyncPlaceholderHands(Entity held) { if (!_handsQuery.TryComp(held.Owner, out var hands)) @@ -53,7 +61,7 @@ private void SyncPlaceholderHands(Entity held) return; } - var heldHands = new Entity(held.Owner, hands).AsNullable(); + var heldHands = (held.Owner, hands); DropHeldItemsForPlaceholders(heldHands); DeleteInvalidHeldHandBlockers(heldHands); EnsureHeldHandBlockers(heldHands); @@ -155,18 +163,6 @@ private void EnsureHeldHandBlockers(Entity held) } } - private void SyncHeldStatusEffect(EntityUid target) - { - if (_statusEffects.HasStatusEffect(target, GrabbedStatusEffect) || - !_statusEffects.CanAddStatusEffect(target, GrabbedStatusEffect)) - { - return; - } - - EnsureComp(target); - PredictedTrySpawnInContainer(GrabbedStatusEffect, target, StatusEffectContainerComponent.ContainerId, out _); - } - private void SyncHolderHandBlocker(Entity holder) { _virtualBlockersToDelete.Clear(); @@ -278,4 +274,47 @@ private void DeleteHeldHandBlockers(EntityUid heldUid) _virtualItem.DeleteVirtualItem(virtualItem, heldUid); } } + + private void OnHolderBeforeThrow(Entity ent, ref BeforeThrowEvent args) + { + if (ent.Comp.Target == null) + return; + + if (!TryComp(args.ItemUid, out var blocker)) + return; + + if (blocker.Target != ent.Comp.Target.Value) + return; + + ReleaseHolderContribution(ent.Owner, ent.Comp.Target.Value, clearIfEmpty: true); + args.Cancelled = true; + } + + private void OnHolderHandsModified(Entity ent, ref DidEquipHandEvent args) + { + if (ent.Comp.LifeStage > ComponentLifeStage.Running) + return; + + if (ent.Comp.Target == null) + return; + + if (!_activeHoldableQuery.HasComp(ent.Comp.Target.Value)) + return; + + SyncHolderState(ent); + } + + private void OnHolderBlockerDropped(Entity ent, ref GettingDroppedAttemptEvent args) + { + if (!_activeHolderQuery.TryComp(args.User, out var holder)) + return; + + if (holder.Target == null) + return; + + if (holder.Target != ent.Comp.Target) + return; + + ReleaseHolderContribution(args.User, ent.Comp.Target, clearIfEmpty: true); + } } diff --git a/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.Lifecycle.cs b/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.Lifecycle.cs new file mode 100644 index 00000000000..4f924f20e87 --- /dev/null +++ b/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.Lifecycle.cs @@ -0,0 +1,103 @@ +using Content.Shared._Scp.Holding.Components; +using Content.Shared.Alert; +using Content.Shared.Movement.Events; +using Content.Shared.Movement.Systems; +using Robust.Shared.Prototypes; + +namespace Content.Shared._Scp.Holding.Systems; + +public abstract partial class SharedScpHoldingSystem +{ + /* + * Held/holder lifecycle wiring and state refresh reactions. + */ + + private static readonly ProtoId HeldAlert = "ScpHoldGrabbed"; + + private void InitializeLifecycleEvents() + { + SubscribeLocalEvent(OnHeldStartup); + SubscribeLocalEvent(OnHeldShutdown); + SubscribeLocalEvent(OnHeldRemove); + SubscribeLocalEvent(OnFullHeldStartup); + SubscribeLocalEvent(OnFullHeldRemove); + SubscribeLocalEvent(OnFullHeldUpdateCanMove); + SubscribeLocalEvent(OnHolderStartup); + SubscribeLocalEvent(OnHolderShutdown); + SubscribeLocalEvent(OnHolderSlowdownRemove); + SubscribeLocalEvent(OnHolderSlowdownAfterState); + SubscribeLocalEvent(OnHolderSlowdownRefreshMoveSpeed); + } + + private void OnHeldStartup(Entity ent, ref ComponentStartup args) + { + _alerts.ShowAlert(ent.Owner, HeldAlert); + _statusEffects.TrySetStatusEffectDuration(ent, GrabbedStatusEffect); + OnHeldStateRefreshed(ent); + ValidateAllActions(ent.Owner); + } + + private void OnHeldShutdown(Entity ent, ref ComponentShutdown args) + { + _alerts.ClearAlert(ent.Owner, HeldAlert); + _statusEffects.TryRemoveStatusEffect(ent, GrabbedStatusEffect); + OnHeldStateShutdown(ent); + } + + private void OnHeldRemove(Entity ent, ref ComponentRemove args) + { + ValidateAllActions(ent.Owner); + } + + private static void OnFullHeldUpdateCanMove(Entity ent, ref UpdateCanMoveEvent args) + { + args.Cancel(); + } + + private void OnFullHeldStartup(Entity ent, ref ComponentStartup args) + { + if (_activeHoldableQuery.TryComp(ent.Owner, out var held)) + SyncPlaceholderHands((ent.Owner, held)); + + ZeroHeldVelocity(ent.Owner); + _actionBlocker.UpdateCanMove(ent.Owner); + } + + private void OnFullHeldRemove(Entity ent, ref ComponentRemove args) + { + DeleteHeldHandBlockers(ent.Owner); + _actionBlocker.UpdateCanMove(ent.Owner); + } + + private void OnHolderStartup(Entity ent, ref ComponentStartup args) + { + SyncHolderState(ent); + } + + private void OnHolderShutdown(Entity ent, ref ComponentShutdown args) + { + var target = ent.Comp.Target; + ent.Comp.Target = null; + DeleteHolderHandBlockers(ent.Owner); + + if (!_timing.ApplyingState) + RemComp(ent.Owner); + + OnHolderStateShutdown(ent.Owner, target); + } + + private void OnHolderSlowdownRemove(Entity ent, ref ComponentRemove args) + { + _movement.RefreshMovementSpeedModifiers(ent.Owner); + } + + private void OnHolderSlowdownAfterState(Entity ent, ref AfterAutoHandleStateEvent args) + { + _movement.RefreshMovementSpeedModifiers(ent.Owner); + } + + private void OnHolderSlowdownRefreshMoveSpeed(Entity ent, ref RefreshMovementSpeedModifiersEvent args) + { + args.ModifySpeed(ent.Comp.WalkModifier, ent.Comp.SprintModifier); + } +} diff --git a/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.State.cs b/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.State.cs index 20b33299cac..8b682af674c 100644 --- a/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.State.cs +++ b/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.State.cs @@ -17,10 +17,20 @@ public abstract partial class SharedScpHoldingSystem private readonly List _holdersToRemove = []; private readonly List _holderCooldownsToApply = []; + private EntityQuery _activeHoldableFullHoldStateQuery; + private EntityQuery _activeHoldableQuery; + private EntityQuery _holderConfigQuery; + private EntityQuery _activeHolderQuery; + private EntityQuery _activeHolderSlowdownStateQuery; private EntityQuery _bodyQuery; private void InitializeStateQueries() { + _activeHoldableFullHoldStateQuery = GetEntityQuery(); + _activeHoldableQuery = GetEntityQuery(); + _holderConfigQuery = GetEntityQuery(); + _activeHolderQuery = GetEntityQuery(); + _activeHolderSlowdownStateQuery = GetEntityQuery(); _bodyQuery = GetEntityQuery(); } diff --git a/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.cs b/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.cs index 9e9eb57707e..ebca9fe830a 100644 --- a/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.cs +++ b/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.cs @@ -7,7 +7,6 @@ using Content.Shared.StatusEffectNew; using Content.Shared.Whitelist; using Robust.Shared.Containers; -using Robust.Shared.Physics.Components; using Robust.Shared.Physics.Systems; using Robust.Shared.Prototypes; using Robust.Shared.Timing; @@ -34,30 +33,19 @@ public abstract partial class SharedScpHoldingSystem : EntitySystem private static readonly EntProtoId GrabbedStatusEffect = "StatusEffectScpHeld"; private readonly Dictionary _breakoutDoAfterIds = []; - private EntityQuery _breakoutAttemptQuery; - private EntityQuery _activeHoldableFullHoldStateQuery; - private EntityQuery _physicsQuery; - private EntityQuery _activeHoldableQuery; - private EntityQuery _holderConfigQuery; - private EntityQuery _activeHolderQuery; - private EntityQuery _activeHolderSlowdownStateQuery; - public override void Initialize() { base.Initialize(); - _breakoutAttemptQuery = GetEntityQuery(); - _activeHoldableFullHoldStateQuery = GetEntityQuery(); - _physicsQuery = GetEntityQuery(); - _activeHoldableQuery = GetEntityQuery(); - _holderConfigQuery = GetEntityQuery(); - _activeHolderQuery = GetEntityQuery(); - _activeHolderSlowdownStateQuery = GetEntityQuery(); - InitializeHoldQueries(); + InitializeBreakoutAttemptQueries(); + InitializeDragQueries(); InitializeHandQueries(); InitializeStateQueries(); - SubscribeHoldingEvents(); + InitializeLifecycleEvents(); + InitializeBreakoutAttemptEvents(); + InitializeDragEvents(); + InitializeHandEvents(); InitializeRestrictions(); } From 59b70a9ef4540eb650dc07714795e3513b477a06 Mon Sep 17 00:00:00 2001 From: drdth Date: Fri, 17 Apr 2026 23:45:50 +0300 Subject: [PATCH 13/27] refactor: virtual blocker --- .../_Scp/Holding/ScpHoldingSystem.cs | 171 +++++---- .../Tests/_Scp/ScpHoldingTest.cs | 344 +++++++++++++++--- .../Components/ActiveScpHolderComponent.cs | 2 +- .../Components/ScpHeldHandBlockerComponent.cs | 13 +- .../Components/ScpHoldHandBlockerComponent.cs | 7 +- .../Systems/SharedScpHoldingSystem.Actions.cs | 38 +- .../Systems/SharedScpHoldingSystem.Hands.cs | 134 +++---- .../SharedScpHoldingSystem.Lifecycle.cs | 6 + 8 files changed, 519 insertions(+), 196 deletions(-) diff --git a/Content.Client/_Scp/Holding/ScpHoldingSystem.cs b/Content.Client/_Scp/Holding/ScpHoldingSystem.cs index 1c2776a7898..d30fc6c783b 100644 --- a/Content.Client/_Scp/Holding/ScpHoldingSystem.cs +++ b/Content.Client/_Scp/Holding/ScpHoldingSystem.cs @@ -16,16 +16,11 @@ public sealed 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!; + [Dependency] private readonly VirtualItemSystem _virtualItem = default!; - private static readonly TimeSpan BlockerRespawnSuppressionDuration = TimeSpan.FromSeconds(0.5f); - - private EntityUid? _suppressedHolder; - private EntityUid? _suppressedTarget; private EntityUid? _trackedHolderTarget; - private TimeSpan _suppressedUntil; private EntityQuery _handsQuery; private EntityQuery _physicsQuery; @@ -44,7 +39,9 @@ public override void Initialize() _virtualItemQuery = GetEntityQuery(); SubscribeLocalEvent(OnHeldAfterState); - SubscribeLocalEvent(OnBlockerUnequipped); + SubscribeLocalEvent(OnHolderAfterState); + SubscribeLocalEvent(OnBlockerStartup); + SubscribeLocalEvent(OnBlockerEquipped); SubscribeLocalEvent(OnUpdateHeldPredicted); } @@ -52,33 +49,19 @@ public override void Update(float frameTime) { base.Update(frameTime); - if (_timing.CurTime >= _suppressedUntil) - ClearBlockerRespawnSuppression(); - if (_player.LocalEntity is not { Valid: true } local) { UpdateTrackedLocalHeldTarget(null); - ClearBlockerRespawnSuppression(); return; } - if (ShouldSuppressBlockerRespawn(local, _suppressedTarget)) - DeleteSuppressedBlockers(local, _suppressedTarget!.Value); - if (!_activeHolderQuery.TryComp(local, out var localHolder)) { UpdateTrackedLocalHeldTarget(null); return; } - if (ShouldSuppressBlockerRespawn(local, localHolder.Target)) - { - DeleteSuppressedBlockers(local, localHolder.Target!.Value); - UpdateTrackedLocalHeldTarget(localHolder.Target); - return; - } - - SyncHolderState((local, localHolder)); + ReconcileLocalHolderState((local, localHolder)); } private void OnHeldAfterState(Entity ent, ref AfterAutoHandleStateEvent args) @@ -86,18 +69,28 @@ private void OnHeldAfterState(Entity ent, ref AfterA ReconcileHeldAfterState(ent); } - private void OnBlockerUnequipped(Entity ent, ref GotUnequippedHandEvent args) + private void OnHolderAfterState(Entity ent, ref AfterAutoHandleStateEvent args) { - if (_player.LocalEntity != args.User) + if (_player.LocalEntity != ent.Owner) return; - if (_activeHolderQuery.TryComp(args.User, out var holder)) - { - if (holder.Target == ent.Comp.Target) - return; - } + ReconcileLocalHolderState(ent); + } + + private void OnBlockerStartup(Entity ent, ref ComponentStartup args) + { + if (!_timing.ApplyingState) + return; + + ReconcileLocalHolderBlocker(ent.Owner); + } + + private void OnBlockerEquipped(Entity ent, ref GotEquippedHandEvent args) + { + if (!_timing.ApplyingState) + return; - SuppressBlockerRespawn(args.User, ent.Comp.Target); + ReconcileLocalHolderBlocker(ent.Owner, args.User); } private void OnUpdateHeldPredicted(Entity ent, ref UpdateIsPredictedEvent args) @@ -165,26 +158,95 @@ protected override void OnHolderStateShutdown(EntityUid holderUid, EntityUid? ta UpdateTrackedLocalHeldTarget(holderUid, null, target); } - private void SuppressBlockerRespawn(EntityUid holder, EntityUid target) + private void ReconcileLocalHolderBlocker(EntityUid blocker, EntityUid? holderUid = null) { - _suppressedHolder = holder; - _suppressedTarget = target; - _suppressedUntil = _timing.CurTime + BlockerRespawnSuppressionDuration; + holderUid ??= _player.LocalEntity; + + if (holderUid is not { Valid: true } holder) + return; + + if (!_activeHolderQuery.TryComp(holder, out var activeHolder) || + 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) || + !_hands.IsHolding((holder, hands), blocker)) + { + return; + } + + ReconcileLocalHolderState((holder, activeHolder)); } - private void ClearBlockerRespawnSuppression() + private void ReconcileLocalHolderState(Entity holder) { - _suppressedHolder = null; - _suppressedTarget = null; - _suppressedUntil = TimeSpan.Zero; + UpdateTrackedLocalHeldTarget(holder, holder.Comp.Target); + ReconcileLocalHolderBlockerSteadyState(holder); } - private bool ShouldSuppressBlockerRespawn(EntityUid holder, EntityUid? target) + private void ReconcileLocalHolderBlockerSteadyState(Entity holder) { - return _suppressedHolder == holder && - _suppressedTarget != null && - target == _suppressedTarget && - _timing.CurTime < _suppressedUntil; + if (holder.Comp.Target == null || + !_handsQuery.TryComp(holder.Owner, out var hands)) + { + return; + } + + var target = holder.Comp.Target.Value; + EntityUid? authoritativeBlocker = null; + EntityUid? predictedBlocker = null; + + foreach (var heldItem in _hands.EnumerateHeld((holder.Owner, hands))) + { + if (!_virtualItemQuery.TryComp(heldItem, out var virtualItem) || + virtualItem.BlockingEntity != target) + { + continue; + } + + if (!IsClientSide(heldItem)) + { + authoritativeBlocker ??= heldItem; + continue; + } + + if (!_blockerQuery.HasComp(heldItem)) + continue; + + if (predictedBlocker == null) + { + predictedBlocker = heldItem; + continue; + } + + QueueDel(heldItem); + } + + if (authoritativeBlocker != null) + { + if (predictedBlocker != null) + QueueDel(predictedBlocker.Value); + return; + } + + if (_timing.ApplyingState || + predictedBlocker != null || + !_hands.TryGetEmptyHand((holder.Owner, hands), out var emptyHand) || + !_virtualItem.TrySpawnVirtualItem(target, holder.Owner, out var spawnedVirtualItem)) + { + return; + } + + EnsureComp(spawnedVirtualItem.Value); + _hands.DoPickup(holder.Owner, emptyHand, spawnedVirtualItem.Value, hands); } private void UpdateTrackedLocalHeldTarget(EntityUid? currentTarget, EntityUid? previousTarget = null) @@ -210,27 +272,4 @@ private void UpdateTrackedLocalHeldTarget(EntityUid holderUid, EntityUid? curren UpdateTrackedLocalHeldTarget(currentTarget, previousTarget); } - - private void DeleteSuppressedBlockers(EntityUid holder, EntityUid target) - { - if (!_handsQuery.TryComp(holder, out var hands)) - return; - - foreach (var heldItem in _hands.EnumerateHeld((holder, hands))) - { - if (!_virtualItemQuery.TryComp(heldItem, out var virtualItem)) - continue; - - if (!_blockerQuery.TryComp(heldItem, out var blocker)) - continue; - - if (virtualItem.BlockingEntity != target) - continue; - - if (blocker.Target != target) - continue; - - _virtualItem.DeleteVirtualItem((heldItem, virtualItem), holder); - } - } } diff --git a/Content.IntegrationTests/Tests/_Scp/ScpHoldingTest.cs b/Content.IntegrationTests/Tests/_Scp/ScpHoldingTest.cs index 9ae63455be9..30521cf32d4 100644 --- a/Content.IntegrationTests/Tests/_Scp/ScpHoldingTest.cs +++ b/Content.IntegrationTests/Tests/_Scp/ScpHoldingTest.cs @@ -48,6 +48,8 @@ public sealed class ScpHoldingTest private const string HoldableBlacklistedHolderPrototype = "ScpHoldingTestHolderHoldableBlacklisted"; private const string TestListenerComponentName = "TestListener"; private static readonly ProtoId GrabbedAlertId = "ScpHoldGrabbed"; + private static readonly FieldInfo ActiveScpHolderTargetField = + typeof(ActiveScpHolderComponent).GetField(nameof(ActiveScpHolderComponent.Target))!; private static readonly FieldInfo SoftEscapeAvailableAtField = typeof(ActiveScpHoldableComponent).GetField(nameof(ActiveScpHoldableComponent.SoftEscapeAvailableAt))!; @@ -108,6 +110,90 @@ await server.WaitPost(() => await pair.CleanReturnAsync(); } + [Test] + public async Task SyncHolderState_DoesNotAdoptForeignVirtualItemWithSameBlockingEntity() + { + await using var pair = await PoolManager.GetServerClient(new PoolSettings { Fresh = true }); + var server = pair.Server; + var entMan = server.EntMan; + var handsSystem = server.System(); + var holding = server.System(); + var virtualItem = server.System(); + var map = await pair.CreateTestMap(); + + EntityUid holder = default; + EntityUid target = default; + + await server.WaitPost(() => + { + holder = entMan.SpawnEntity(HolderPrototype, map.GridCoords); + target = entMan.SpawnEntity("MobHuman", map.GridCoords.Offset(new Vector2(0.1f, 0f))); + + var holderState = entMan.EnsureComponent(holder); + SetHolderTarget(holderState, target); + + Assert.That(virtualItem.TrySpawnVirtualItemInHand(target, holder, out _), Is.True); + + holding.SyncHolderState((holder, holderState)); + + var hands = entMan.GetComponent(holder); + Assert.Multiple(() => + { + Assert.That(CountHolderHandBlockers(entMan, handsSystem, holder, target, hands), Is.EqualTo(1)); + Assert.That(CountHolderTargetVirtualItems(entMan, handsSystem, holder, target, hands), Is.EqualTo(2)); + }); + }); + + await pair.CleanReturnAsync(); + } + + [Test] + public async Task SyncHolderState_DeletesDuplicateTaggedHolderBlockers() + { + await using var pair = await PoolManager.GetServerClient(new PoolSettings { Fresh = true }); + var server = pair.Server; + var entMan = server.EntMan; + var handsSystem = server.System(); + var holding = server.System(); + var virtualItem = server.System(); + var map = await pair.CreateTestMap(); + + EntityUid holder = default; + EntityUid target = default; + + await server.WaitPost(() => + { + holder = entMan.SpawnEntity(HolderPrototype, map.GridCoords); + target = entMan.SpawnEntity("MobHuman", map.GridCoords.Offset(new Vector2(0.1f, 0f))); + + var holderState = entMan.EnsureComponent(holder); + SetHolderTarget(holderState, target); + + Assert.That(virtualItem.TrySpawnVirtualItemInHand(target, holder, out var blocker1), Is.True); + Assert.That(virtualItem.TrySpawnVirtualItemInHand(target, holder, out var blocker2), Is.True); + + entMan.EnsureComponent(blocker1!.Value); + entMan.EnsureComponent(blocker2!.Value); + + holding.SyncHolderState((holder, holderState)); + }); + + await server.WaitRunTicks(2); + + await server.WaitAssertion(() => + { + var hands = entMan.GetComponent(holder); + + Assert.Multiple(() => + { + Assert.That(CountHolderHandBlockers(entMan, handsSystem, holder, target, hands), Is.EqualTo(1)); + Assert.That(CountHolderTargetVirtualItems(entMan, handsSystem, holder, target, hands), Is.EqualTo(1)); + }); + }); + + await pair.CleanReturnAsync(); + } + [Test] public async Task SoftHoldBreakoutByMovementAndAlertRespectsCooldown() { @@ -652,7 +738,7 @@ await server.WaitAssertion(() => Assert.That(held.RequiredHolderCount, Is.EqualTo(2)); Assert.That(held.Holders, Has.Count.EqualTo(2)); Assert.That(move.Cancelled, Is.True); - Assert.That(CountBlockingVirtualHands(entMan, handsSystem, target, hands), Is.EqualTo(hands.SortedHands.Count)); + Assert.That(CountBlockingVirtualHands(entMan, handsSystem, target, hands, holderOne, holderTwo), Is.EqualTo(hands.SortedHands.Count)); Assert.That(VictimHandsUseHolderIcons(entMan, handsSystem, target, hands, holderOne, holderTwo), Is.True); Assert.That(holderOnePuller.Pulling, Is.Null); Assert.That(holderTwoPuller.Pulling, Is.Null); @@ -677,6 +763,7 @@ public async Task FullHoldVictimBlockersStayStableOnServerAndClient() var client = pair.Client; var sEntMan = server.EntMan; var cEntMan = client.EntMan; + var cTiming = client.ResolveDependency(); var sTransform = server.System(); var sHandsSystem = server.System(); var cHandsSystem = client.System(); @@ -707,10 +794,10 @@ await server.WaitAssertion(() => Assert.Multiple(() => { Assert.That(HasFullHold(sEntMan, serverPlayer), Is.True); - Assert.That(CountBlockingVirtualHands(sEntMan, sHandsSystem, serverPlayer, hands), Is.EqualTo(hands.SortedHands.Count)); + Assert.That(CountBlockingVirtualHands(sEntMan, sHandsSystem, serverPlayer, hands, holderOne, holderTwo), Is.EqualTo(hands.SortedHands.Count)); }); - initialServerBlockers = GetHeldHandBlockers(sEntMan, sHandsSystem, serverPlayer, hands); + initialServerBlockers = GetHeldHandBlockers(sEntMan, sHandsSystem, serverPlayer, hands, holderOne, holderTwo); Assert.That(initialServerBlockers, Has.Length.EqualTo(hands.SortedHands.Count)); }); @@ -724,10 +811,10 @@ await client.WaitAssertion(() => Assert.Multiple(() => { Assert.That(HasFullHold(cEntMan, clientPlayer), Is.True); - Assert.That(CountBlockingVirtualHands(cEntMan, cHandsSystem, clientPlayer, hands), Is.EqualTo(hands.SortedHands.Count)); + Assert.That(CountBlockingVirtualHands(cEntMan, cHandsSystem, clientPlayer, hands, holderOne, holderTwo), Is.EqualTo(hands.SortedHands.Count)); }); - initialClientBlockers = GetHeldHandBlockers(cEntMan, cHandsSystem, clientPlayer, hands); + initialClientBlockers = GetHeldHandBlockers(cEntMan, cHandsSystem, clientPlayer, hands, holderOne, holderTwo); Assert.That(initialClientBlockers, Has.Length.EqualTo(hands.SortedHands.Count)); }); @@ -741,7 +828,7 @@ await server.WaitAssertion(() => Assert.Multiple(() => { Assert.That(HasFullHold(sEntMan, serverPlayer), Is.True); - Assert.That(GetHeldHandBlockers(sEntMan, sHandsSystem, serverPlayer, hands), Is.EqualTo(initialServerBlockers)); + Assert.That(GetHeldHandBlockers(sEntMan, sHandsSystem, serverPlayer, hands, holderOne, holderTwo), Is.EqualTo(initialServerBlockers)); }); }); @@ -752,7 +839,7 @@ await client.WaitAssertion(() => Assert.Multiple(() => { Assert.That(HasFullHold(cEntMan, clientPlayer), Is.True); - Assert.That(GetHeldHandBlockers(cEntMan, cHandsSystem, clientPlayer, hands), Is.EqualTo(initialClientBlockers)); + Assert.That(GetHeldHandBlockers(cEntMan, cHandsSystem, clientPlayer, hands, holderOne, holderTwo), Is.EqualTo(initialClientBlockers)); }); }); @@ -1005,7 +1092,7 @@ await server.WaitAssertion(() => Assert.That(HasFullHold(entMan, target), Is.True); Assert.That(held.RequiredHolderCount, Is.EqualTo(3)); Assert.That(hands.SortedHands.Count, Is.EqualTo(3)); - Assert.That(CountBlockingVirtualHands(entMan, handsSystem, target, hands), Is.EqualTo(3)); + Assert.That(CountBlockingVirtualHands(entMan, handsSystem, target, hands, holderOne, holderTwo, holderThree), Is.EqualTo(3)); Assert.That(VictimHandsUseHolderIcons(entMan, handsSystem, target, hands, holderOne, holderTwo, holderThree), Is.True); }); }); @@ -1028,7 +1115,7 @@ await server.WaitAssertion(() => Assert.That(held.RequiredHolderCount, Is.EqualTo(2)); Assert.That(HasFullHold(entMan, target), Is.True); Assert.That(hands.SortedHands.Count, Is.EqualTo(2)); - Assert.That(CountBlockingVirtualHands(entMan, handsSystem, target, hands), Is.EqualTo(2)); + Assert.That(CountBlockingVirtualHands(entMan, handsSystem, target, hands, holderOne, holderTwo, holderThree), Is.EqualTo(2)); Assert.That(VictimHandsUseHolderIcons(entMan, handsSystem, target, hands, holderOne, holderTwo, holderThree), Is.True); }); }); @@ -1277,7 +1364,7 @@ await server.WaitAssertion(() => } [Test] - public async Task DroppingOrThrowingHolderBlockerReleasesHold() + public async Task DroppingHolderBlockerReleasesHold() { await using var pair = await PoolManager.GetServerClient(new PoolSettings { Fresh = true }); var server = pair.Server; @@ -1303,7 +1390,7 @@ await server.WaitPost(() => var blocker = FindHolderHandBlocker(entMan, handsSystem, holder, target, hands); Assert.That(blocker, Is.Not.EqualTo(EntityUid.Invalid)); - Assert.That(handsSystem.TryDrop((holder, hands), blocker), Is.True); + Assert.That(handsSystem.TryDrop((holder, hands), blocker), Is.False); }); await server.WaitRunTicks(2); @@ -1313,7 +1400,28 @@ await server.WaitAssertion(() => Assert.That(entMan.HasComponent(holder), Is.False); }); - await server.WaitPost(() => StartHold(entMan, holding, holder, target)); + await pair.CleanReturnAsync(); + } + + [Test] + public async Task ThrowingHolderBlockerReleasesHold() + { + await using var pair = await PoolManager.GetServerClient(new PoolSettings { Fresh = true }); + var server = pair.Server; + var entMan = server.EntMan; + var handsSystem = server.System(); + var holding = server.System(); + var map = await pair.CreateTestMap(); + + EntityUid holder = default; + EntityUid target = default; + + await server.WaitPost(() => + { + holder = entMan.SpawnEntity(HolderPrototype, map.GridCoords); + target = entMan.SpawnEntity("MobHuman", map.GridCoords.Offset(new Vector2(0.1f, 0f))); + StartHold(entMan, holding, holder, target); + }); await server.WaitRunTicks(2); await server.WaitPost(() => @@ -1337,6 +1445,48 @@ await server.WaitAssertion(() => await pair.CleanReturnAsync(); } + [Test] + public async Task GettingDroppedAttemptOnHolderBlockerReleasesHold() + { + await using var pair = await PoolManager.GetServerClient(new PoolSettings { Fresh = true }); + var server = pair.Server; + var entMan = server.EntMan; + var handsSystem = server.System(); + var holding = server.System(); + var map = await pair.CreateTestMap(); + + EntityUid holder = default; + EntityUid target = default; + + await server.WaitPost(() => + { + holder = entMan.SpawnEntity(HolderPrototype, map.GridCoords); + target = entMan.SpawnEntity("MobHuman", map.GridCoords.Offset(new Vector2(0.1f, 0f))); + StartHold(entMan, holding, holder, target); + }); + await server.WaitRunTicks(2); + + await server.WaitPost(() => + { + var hands = entMan.GetComponent(holder); + var blocker = FindHolderHandBlocker(entMan, handsSystem, holder, target, hands); + var dropAttempt = new GettingDroppedAttemptEvent(holder); + + Assert.That(blocker, Is.Not.EqualTo(EntityUid.Invalid)); + entMan.EventBus.RaiseLocalEvent(blocker, ref dropAttempt); + Assert.That(dropAttempt.Cancelled, Is.True); + }); + await server.WaitRunTicks(2); + + await server.WaitAssertion(() => + { + Assert.That(entMan.HasComponent(target), Is.False); + Assert.That(entMan.HasComponent(holder), Is.False); + }); + + await pair.CleanReturnAsync(); + } + [Test] public async Task ClientDroppingHolderBlockerReleasesWithoutRespawnFlicker() { @@ -1351,6 +1501,7 @@ public async Task ClientDroppingHolderBlockerReleasesWithoutRespawnFlicker() var client = pair.Client; var sEntMan = server.EntMan; var cEntMan = client.EntMan; + var cTiming = client.ResolveDependency(); var sTransform = server.System(); var sHandsSystem = server.System(); var cHandsSystem = client.System(); @@ -1379,23 +1530,35 @@ await client.WaitAssertion(() => clientTarget = ToClientEntity(sEntMan, cEntMan, target); var hands = cEntMan.GetComponent(clientPlayer); - Assert.Multiple(() => - { - Assert.That(cEntMan.HasComponent(clientPlayer), Is.True); - Assert.That(cEntMan.HasComponent(clientTarget), Is.True); - Assert.That(CountHolderTargetVirtualItems(cEntMan, cHandsSystem, clientPlayer, clientTarget, hands), Is.EqualTo(1)); - Assert.That(CountPrototypeEntities(cEntMan, "clientsideclone"), Is.EqualTo(0)); - }); + Assert.Multiple(() => + { + Assert.That(cEntMan.HasComponent(clientPlayer), Is.True); + Assert.That(cEntMan.HasComponent(clientTarget), Is.True); + Assert.That( + CountHolderTargetVirtualItems(cEntMan, cHandsSystem, clientPlayer, clientTarget, hands), + Is.EqualTo(1), + DescribeHolderTargetVirtualItems(cEntMan, cHandsSystem, clientPlayer, clientTarget, hands)); + Assert.That(CountPrototypeEntities(cEntMan, "clientsideclone"), Is.EqualTo(0)); + }); }); await client.WaitPost(() => { var hands = cEntMan.GetComponent(clientPlayer); var blocker = FindHolderHandBlocker(cEntMan, cHandsSystem, clientPlayer, clientTarget, hands); - var dropLocation = new EntityCoordinates(clientPlayer, new Vector2(0.5f, 0f)); Assert.That(blocker, Is.Not.EqualTo(EntityUid.Invalid)); - Assert.That(cHandsSystem.TryDrop((clientPlayer, hands), blocker, dropLocation), Is.True); + Assert.That(cHandsSystem.IsHolding((clientPlayer, hands), blocker, out var blockerHand), Is.True); + + if (hands.ActiveHandId != blockerHand) + Assert.That(cHandsSystem.TrySetActiveHand((clientPlayer, hands), blockerHand), Is.True); + }); + + await PressClientDropKey(client, cEntMan, cTiming, clientTarget); + + await client.WaitAssertion(() => + { + var hands = cEntMan.GetComponent(clientPlayer); Assert.Multiple(() => { Assert.That(cEntMan.HasComponent(clientTarget), Is.False); @@ -1406,6 +1569,9 @@ await client.WaitPost(() => }); var maxClientBlockers = 0; + var holderRespawned = false; + var heldRespawned = false; + string? blockerTimeline = null; for (var i = 0; i < 12; i++) { await pair.RunTicksSync(1); @@ -1415,14 +1581,28 @@ await client.WaitPost(() => if (!cEntMan.TryGetComponent(clientPlayer, out var hands)) return; - maxClientBlockers = Math.Max(maxClientBlockers, - CountHolderTargetVirtualItems(cEntMan, cHandsSystem, clientPlayer, clientTarget, hands)); + var blockerCount = CountHolderTargetVirtualItems(cEntMan, cHandsSystem, clientPlayer, clientTarget, hands); + var hasHolder = cEntMan.HasComponent(clientPlayer); + var hasHeld = cEntMan.HasComponent(clientTarget); + + maxClientBlockers = Math.Max(maxClientBlockers, blockerCount); + holderRespawned |= hasHolder; + heldRespawned |= hasHeld; + + if (blockerCount > 0 || hasHolder || hasHeld) + { + blockerTimeline = + $"tick={i}, holder={hasHolder}, held={hasHeld}, " + + $"items=[{DescribeHolderTargetVirtualItems(cEntMan, cHandsSystem, clientPlayer, clientTarget, hands)}]"; + } }); } Assert.Multiple(() => { - Assert.That(maxClientBlockers, Is.EqualTo(0)); + Assert.That(maxClientBlockers, Is.EqualTo(0), blockerTimeline); + Assert.That(holderRespawned, Is.False, blockerTimeline); + Assert.That(heldRespawned, Is.False, blockerTimeline); Assert.That(CountPrototypeEntities(cEntMan, "clientsideclone"), Is.EqualTo(0)); }); @@ -1570,6 +1750,42 @@ await client.WaitAssertion(() => }); }); + var minClientBlockersAfterAck = int.MaxValue; + var maxClientBlockersAfterAck = 0; + string? blockerTimelineAfterAck = null; + for (var i = 0; i < 12; i++) + { + await pair.RunTicksSync(1); + await pair.SyncTicks(targetDelta: 1); + await client.WaitPost(() => + { + if (!cEntMan.TryGetComponent(clientPlayer, out var holderHands)) + return; + + var blockerCount = CountHolderTargetVirtualItems(cEntMan, cHandsSystem, clientPlayer, clientTarget, holderHands); + minClientBlockersAfterAck = Math.Min(minClientBlockersAfterAck, blockerCount); + maxClientBlockersAfterAck = Math.Max(maxClientBlockersAfterAck, blockerCount); + + if (blockerCount != 1 || + !cEntMan.HasComponent(clientPlayer) || + !cEntMan.HasComponent(clientTarget)) + { + var distance = GetDistance(cTransform, clientPlayer, clientTarget); + blockerTimelineAfterAck = + $"tick={i}, blockers={blockerCount}, distance={distance:0.000}, " + + $"hasHolder={cEntMan.HasComponent(clientPlayer)}, " + + $"hasHeld={cEntMan.HasComponent(clientTarget)}, " + + $"items=[{DescribeHolderTargetVirtualItems(cEntMan, cHandsSystem, clientPlayer, clientTarget, holderHands)}]"; + } + }); + } + + Assert.Multiple(() => + { + Assert.That(minClientBlockersAfterAck, Is.EqualTo(1), blockerTimelineAfterAck); + Assert.That(maxClientBlockersAfterAck, Is.EqualTo(1), blockerTimelineAfterAck); + }); + await pair.CleanReturnAsync(); } @@ -1913,7 +2129,7 @@ await client.WaitAssertion(() => { Assert.That(HasFullHold(cEntMan, clientTarget), Is.True); Assert.That(held.Holders, Has.Count.EqualTo(2)); - Assert.That(CountBlockingVirtualHands(cEntMan, cHandsSystem, clientTarget, hands), Is.EqualTo(hands.SortedHands.Count)); + Assert.That(CountBlockingVirtualHands(cEntMan, cHandsSystem, clientTarget, hands, clientHolderOne, clientPlayer), Is.EqualTo(hands.SortedHands.Count)); Assert.That(VictimHandsUseHolderIcons(cEntMan, cHandsSystem, clientTarget, hands, clientHolderOne, clientPlayer), Is.True); }); }); @@ -2585,24 +2801,26 @@ await client.WaitAssertion(() => } // Fire added end - private static int CountBlockingVirtualHands(IEntityManager entMan, SharedHandsSystem handsSystem, EntityUid uid, HandsComponent hands) + private static int CountBlockingVirtualHands(IEntityManager entMan, SharedHandsSystem handsSystem, EntityUid uid, HandsComponent hands, params EntityUid[] holders) { + var expected = holders.ToHashSet(); + return handsSystem.EnumerateHeld((uid, hands)).Count(item => entMan.TryGetComponent(item, out VirtualItemComponent? virtualItem) && - entMan.TryGetComponent(item, out ScpHeldHandBlockerComponent? blocker) && - blocker.Target == uid && - blocker.Holder == virtualItem.BlockingEntity && + entMan.HasComponent(item) && + expected.Contains(virtualItem.BlockingEntity) && entMan.HasComponent(item)); } - private static EntityUid[] GetHeldHandBlockers(IEntityManager entMan, SharedHandsSystem handsSystem, EntityUid uid, HandsComponent hands) + private static EntityUid[] GetHeldHandBlockers(IEntityManager entMan, SharedHandsSystem handsSystem, EntityUid uid, HandsComponent hands, params EntityUid[] holders) { + var expected = holders.ToHashSet(); + return handsSystem.EnumerateHeld((uid, hands)) .Where(item => entMan.TryGetComponent(item, out VirtualItemComponent? virtualItem) && - entMan.TryGetComponent(item, out ScpHeldHandBlockerComponent? blocker) && - blocker.Target == uid && - blocker.Holder == virtualItem.BlockingEntity && + entMan.HasComponent(item) && + expected.Contains(virtualItem.BlockingEntity) && entMan.HasComponent(item)) .Order() .ToArray(); @@ -2651,11 +2869,9 @@ private static bool VictimHandsUseHolderIcons(IEntityManager entMan, SharedHands foreach (var item in handsSystem.EnumerateHeld((uid, hands))) { if (!entMan.TryGetComponent(item, out VirtualItemComponent? virtualItem) || - !entMan.TryGetComponent(item, out ScpHeldHandBlockerComponent? blocker) || - blocker.Target != uid || - blocker.Holder != virtualItem.BlockingEntity || + !entMan.HasComponent(item) || !entMan.HasComponent(item) || - !expected.Contains(blocker.Holder)) + !expected.Contains(virtualItem.BlockingEntity)) { return false; } @@ -2705,8 +2921,7 @@ private static int CountHolderHandBlockers(IEntityManager entMan, SharedHandsSys { return handsSystem.EnumerateHeld((holder, hands)).Count(item => entMan.TryGetComponent(item, out VirtualItemComponent? virtualItem) && - entMan.TryGetComponent(item, out ScpHoldHandBlockerComponent? blocker) && - blocker.Target == target && + entMan.HasComponent(item) && virtualItem.BlockingEntity == target); } @@ -2717,13 +2932,24 @@ private static int CountHolderTargetVirtualItems(IEntityManager entMan, SharedHa virtualItem.BlockingEntity == target); } + private static string DescribeHolderTargetVirtualItems(IEntityManager entMan, SharedHandsSystem handsSystem, EntityUid holder, EntityUid target, HandsComponent hands) + { + var items = handsSystem.EnumerateHeld((holder, hands)) + .Where(item => + entMan.TryGetComponent(item, out VirtualItemComponent? virtualItem) && + virtualItem.BlockingEntity == target) + .Select(item => + $"{item}:client={entMan.GetComponent(item).NetEntity.IsClientSide()},marker={entMan.HasComponent(item)}"); + + return string.Join(", ", items); + } + private static EntityUid FindHolderHandBlocker(IEntityManager entMan, SharedHandsSystem handsSystem, EntityUid holder, EntityUid target, HandsComponent hands) { foreach (var item in handsSystem.EnumerateHeld((holder, hands))) { if (entMan.TryGetComponent(item, out VirtualItemComponent? virtualItem) && - entMan.TryGetComponent(item, out ScpHoldHandBlockerComponent? blocker) && - blocker.Target == target && + entMan.HasComponent(item) && virtualItem.BlockingEntity == target) { return item; @@ -2779,6 +3005,16 @@ private static async Task PressClientPullKey( await SendClientPullInput(client, entMan, timing, cursorEntity, BoundKeyState.Up); } + private static async Task PressClientDropKey( + RobustIntegrationTest.ClientIntegrationInstance client, + IEntityManager entMan, + IGameTiming timing, + EntityUid cursorEntity) + { + await SendClientDropInput(client, entMan, timing, cursorEntity, BoundKeyState.Down); + await SendClientDropInput(client, entMan, timing, cursorEntity, BoundKeyState.Up); + } + private static async Task SendClientPullInput( RobustIntegrationTest.ClientIntegrationInstance client, IEntityManager entMan, @@ -2800,11 +3036,37 @@ private static async Task SendClientPullInput( await client.WaitPost(() => inputSystem.HandleInputCommand(client.Session!, ContentKeyFunctions.TryPullObject, message)); } + private static async Task SendClientDropInput( + RobustIntegrationTest.ClientIntegrationInstance client, + IEntityManager entMan, + IGameTiming timing, + EntityUid cursorEntity, + BoundKeyState state) + { + var inputManager = client.ResolveDependency(); + var funcId = inputManager.NetworkBindMap.KeyFunctionID(ContentKeyFunctions.Drop); + var transform = entMan.GetComponent(cursorEntity); + var inputSystem = client.System(); + var message = new ClientFullInputCmdMessage(timing.CurTick, timing.TickFraction, funcId) + { + State = state, + Coordinates = transform.Coordinates, + Uid = cursorEntity, + }; + + await client.WaitPost(() => inputSystem.HandleInputCommand(client.Session!, ContentKeyFunctions.Drop, message)); + } + private static void SetSoftEscapeAvailableAt(ActiveScpHoldableComponent held, TimeSpan value) { SoftEscapeAvailableAtField.SetValue(held, value); } + private static void SetHolderTarget(ActiveScpHolderComponent holder, EntityUid? value) + { + ActiveScpHolderTargetField.SetValue(holder, value); + } + private static void RaiseMoveInput(IEntityManager entMan, EntityUid uid) { var mover = entMan.GetComponent(uid); diff --git a/Content.Shared/_Scp/Holding/Components/ActiveScpHolderComponent.cs b/Content.Shared/_Scp/Holding/Components/ActiveScpHolderComponent.cs index 8fd19eb7195..cd375e8ee52 100644 --- a/Content.Shared/_Scp/Holding/Components/ActiveScpHolderComponent.cs +++ b/Content.Shared/_Scp/Holding/Components/ActiveScpHolderComponent.cs @@ -6,7 +6,7 @@ namespace Content.Shared._Scp.Holding.Components; /// /// Runtime contribution state stored on each active holder. /// -[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState(raiseAfterAutoHandleState: true)] [Access(typeof(SharedScpHoldingSystem))] public sealed partial class ActiveScpHolderComponent : Component { diff --git a/Content.Shared/_Scp/Holding/Components/ScpHeldHandBlockerComponent.cs b/Content.Shared/_Scp/Holding/Components/ScpHeldHandBlockerComponent.cs index b2403651cfb..e31e2c1addf 100644 --- a/Content.Shared/_Scp/Holding/Components/ScpHeldHandBlockerComponent.cs +++ b/Content.Shared/_Scp/Holding/Components/ScpHeldHandBlockerComponent.cs @@ -6,19 +6,8 @@ namespace Content.Shared._Scp.Holding.Components; /// /// Marks a victim hand placeholder virtual item created by SCP holding. /// -[RegisterComponent, NetworkedComponent, AutoGenerateComponentState(fieldDeltas: true)] +[RegisterComponent, NetworkedComponent] [Access(typeof(SharedScpHoldingSystem))] public sealed partial class ScpHeldHandBlockerComponent : Component { - /// - /// Held target whose hand is occupied by this placeholder. - /// - [AutoNetworkedField, ViewVariables] - public EntityUid Target; - - /// - /// Holder whose sprite is shown in this placeholder. - /// - [AutoNetworkedField, ViewVariables] - public EntityUid Holder; } diff --git a/Content.Shared/_Scp/Holding/Components/ScpHoldHandBlockerComponent.cs b/Content.Shared/_Scp/Holding/Components/ScpHoldHandBlockerComponent.cs index b8b8ac6165e..f1e945eb306 100644 --- a/Content.Shared/_Scp/Holding/Components/ScpHoldHandBlockerComponent.cs +++ b/Content.Shared/_Scp/Holding/Components/ScpHoldHandBlockerComponent.cs @@ -6,13 +6,8 @@ namespace Content.Shared._Scp.Holding.Components; /// /// Marks a virtual item that reserves one holder hand for an active SCP hold. /// -[RegisterComponent, NetworkedComponent, AutoGenerateComponentState(raiseAfterAutoHandleState: true, fieldDeltas: true)] +[RegisterComponent, NetworkedComponent] [Access(typeof(SharedScpHoldingSystem))] public sealed partial class ScpHoldHandBlockerComponent : Component { - /// - /// The held target represented by this virtual item. - /// - [AutoNetworkedField, ViewVariables] - public EntityUid Target; } diff --git a/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.Actions.cs b/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.Actions.cs index 5f82a03307b..f9a3ca465d6 100644 --- a/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.Actions.cs +++ b/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.Actions.cs @@ -25,10 +25,7 @@ public bool TryToggleHold(Entity holder, EntityUid target, b if (_activeHolderQuery.TryComp(holder.Owner, out var activeHolder) && activeHolder.Target != null) { if (activeHolder.Target.Value == target) - { - ReleaseHolderContribution(holder.Owner, target, clearIfEmpty: true); - return true; - } + return TryReleaseHold(holder, target); PopupHolder(holder.Owner, "scp-hold-already-holding-other"); return false; @@ -48,6 +45,34 @@ public bool TryToggleHold(Entity holder, EntityUid target, b return true; } + public bool TryReleaseHold(Entity holder, EntityUid target) + { + if (!CanReleaseHold(holder, target)) + return false; + + ReleaseHolderContribution(holder.Owner, target, clearIfEmpty: true); + return true; + } + + public bool CanReleaseHold(Entity holder, EntityUid target, bool quiet = false) + { + if (!_activeHolderQuery.TryComp(holder.Owner, out var activeHolder) || + activeHolder.Target == null) + { + return false; + } + + if (activeHolder.Target != target) + { + if (!quiet) + PopupHolder(holder.Owner, "scp-hold-already-holding-other"); + + return false; + } + + return true; + } + public bool CanToggleHold( Entity holder, EntityUid target, @@ -183,11 +208,6 @@ public bool TryForceBreakOut(Entity held, bool viaM return true; } - protected bool IsFullHold(EntityUid uid) - { - return _activeHoldableFullHoldStateQuery.HasComp(uid); - } - protected void ReconcileHeldAfterState(Entity held) { OnHeldStateRefreshed(held); diff --git a/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.Hands.cs b/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.Hands.cs index 7903d740658..4bb4cdc510c 100644 --- a/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.Hands.cs +++ b/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.Hands.cs @@ -39,7 +39,8 @@ private void InitializeHandEvents() { SubscribeLocalEvent(OnHolderBeforeThrow); SubscribeLocalEvent(OnHolderHandsModified); - SubscribeLocalEvent(OnHolderBlockerDropped); + SubscribeLocalEvent(OnHolderVirtualItemDeleted); + SubscribeLocalEvent(OnHolderBlockerGettingDropped); } private void SyncPlaceholderHands(Entity held) @@ -107,7 +108,7 @@ private void DeleteInvalidHeldHandBlockers(Entity held) if (!_heldHandBlockerQuery.TryComp(heldItem, out var blocker)) continue; - if (!TrySyncHeldHandBlocker((heldItem, blocker), virtualItem, held.Owner)) + if (!IsValidHeldHandBlocker(virtualItem)) { _virtualBlockersToDelete.Add((heldItem, virtualItem)); } @@ -119,29 +120,9 @@ private void DeleteInvalidHeldHandBlockers(Entity held) } } - private bool TrySyncHeldHandBlocker( - Entity blocker, - VirtualItemComponent virtualItem, - EntityUid heldUid) + private bool IsValidHeldHandBlocker(VirtualItemComponent virtualItem) { - if (!_placeholderIcons.Contains(virtualItem.BlockingEntity)) - return false; - - var dirtyTarget = blocker.Comp.Target != heldUid; - if (dirtyTarget) - { - blocker.Comp.Target = heldUid; - DirtyField(blocker.Owner, blocker.Comp, nameof(ScpHeldHandBlockerComponent.Target)); - } - - var dirtyHolder = blocker.Comp.Holder != virtualItem.BlockingEntity; - if (dirtyHolder) - { - blocker.Comp.Holder = virtualItem.BlockingEntity; - DirtyField(blocker.Owner, blocker.Comp, nameof(ScpHeldHandBlockerComponent.Holder)); - } - - return true; + return _placeholderIcons.Contains(virtualItem.BlockingEntity); } private void EnsureHeldHandBlockers(Entity held) @@ -150,14 +131,12 @@ private void EnsureHeldHandBlockers(Entity held) while (_hands.TryGetEmptyHand(held, out var emptyHand)) { var holderUid = _placeholderIcons[iconIndex % _placeholderIcons.Count]; - if (!_virtualItem.TrySpawnVirtualItemInHand(holderUid, held.Owner, out var virtualItem, empty: emptyHand, silent: true)) + if (!_virtualItem.TrySpawnVirtualItem(holderUid, held.Owner, out var virtualItem)) break; EnsureComp(virtualItem.Value); - var blocker = EnsureComp(virtualItem.Value); - blocker.Target = held.Owner; - blocker.Holder = holderUid; - Dirty(virtualItem.Value, blocker); + EnsureComp(virtualItem.Value); + _hands.DoPickup(held.Owner, emptyHand, virtualItem.Value, held.Comp); iconIndex++; } @@ -168,67 +147,55 @@ private void SyncHolderHandBlocker(Entity holder) _virtualBlockersToDelete.Clear(); EntityUid? validBlocker = null; var target = holder.Comp.Target; + var holderActive = holder.Comp.LifeStage <= ComponentLifeStage.Running; foreach (var heldItem in _hands.EnumerateHeld(holder.Owner)) { if (!_virtualItemQuery.TryComp(heldItem, out var virtualItem)) continue; - var matchesCurrentTarget = holder.Comp.LifeStage <= ComponentLifeStage.Running && + var ownedBlocker = _holdHandBlockerQuery.HasComp(heldItem); + var matchesCurrentTarget = holderActive && target != null && virtualItem.BlockingEntity == target.Value; - if (matchesCurrentTarget) + if (ownedBlocker && matchesCurrentTarget) { if (validBlocker == null) { validBlocker = heldItem; RemComp(heldItem); - var existingBlockerCreated = !_holdHandBlockerQuery.TryComp(heldItem, out var blocker); - blocker ??= EnsureComp(heldItem); - var currentTarget = target!.Value; - if (blocker.Target != currentTarget) - { - blocker.Target = currentTarget; - DirtyField(heldItem, blocker, nameof(ScpHoldHandBlockerComponent.Target)); - } - - if (existingBlockerCreated) - Dirty(heldItem, blocker); - continue; } } - if (_holdHandBlockerQuery.HasComp(heldItem) || matchesCurrentTarget) + if (ownedBlocker) _virtualBlockersToDelete.Add((heldItem, virtualItem)); } foreach (var virtualItem in _virtualBlockersToDelete) { - _virtualItem.DeleteVirtualItem(virtualItem, holder.Owner); + RemoveHolderHandBlocker(holder.Owner, virtualItem); } - if (holder.Comp.LifeStage > ComponentLifeStage.Running || - holder.Comp.Target == null || + if (!holderActive || + target == null || validBlocker != null) { return; } if (!_handsQuery.TryComp(holder.Owner, out var hands) || - !_hands.TryGetEmptyHand((holder.Owner, hands), out _)) + !_hands.TryGetEmptyHand((holder.Owner, hands), out var emptyHand)) { return; } - if (!_virtualItem.TrySpawnVirtualItemInHand(holder.Comp.Target.Value, holder.Owner, out var spawnedVirtualItem, silent: true)) + if (!_virtualItem.TrySpawnVirtualItem(target.Value, holder.Owner, out var spawnedVirtualItem)) return; - _holdHandBlockerQuery.TryComp(spawnedVirtualItem.Value, out var blockerComp); - blockerComp ??= EnsureComp(spawnedVirtualItem.Value); - blockerComp.Target = holder.Comp.Target.Value; - Dirty(spawnedVirtualItem.Value, blockerComp); + EnsureComp(spawnedVirtualItem.Value); + _hands.DoPickup(holder.Owner, emptyHand, spawnedVirtualItem.Value, hands); } private bool HasAvailableHolderHand(EntityUid holderUid) @@ -237,6 +204,35 @@ private bool HasAvailableHolderHand(EntityUid holderUid) _hands.TryGetEmptyHand((holderUid, hands), out _); } + private bool HasOwnedHolderHandBlocker(EntityUid holderUid, EntityUid targetUid) + { + foreach (var heldItem in _hands.EnumerateHeld(holderUid)) + { + if (!_holdHandBlockerQuery.HasComp(heldItem) || + !_virtualItemQuery.TryComp(heldItem, out var virtualItem) || + virtualItem.BlockingEntity != targetUid) + { + continue; + } + + return true; + } + + return false; + } + + private void RemoveHolderHandBlocker(EntityUid holderUid, Entity virtualItem) + { + if (_handsQuery.TryComp(holderUid, out var hands) && + _hands.IsHolding((holderUid, hands), virtualItem.Owner, out var hand)) + { + _hands.DoDrop((holderUid, hands), hand, doDropInteraction: false, log: false); + return; + } + + _virtualItem.DeleteVirtualItem(virtualItem, holderUid); + } + private void DeleteHolderHandBlockers(EntityUid holderUid) { _virtualBlockersToDelete.Clear(); @@ -252,7 +248,7 @@ private void DeleteHolderHandBlockers(EntityUid holderUid) foreach (var virtualItem in _virtualBlockersToDelete) { - _virtualItem.DeleteVirtualItem(virtualItem, holderUid); + RemoveHolderHandBlocker(holderUid, virtualItem); } } @@ -280,10 +276,13 @@ private void OnHolderBeforeThrow(Entity ent, ref Befor if (ent.Comp.Target == null) return; - if (!TryComp(args.ItemUid, out var blocker)) + if (!TryComp(args.ItemUid, out _)) + return; + + if (!_virtualItemQuery.TryComp(args.ItemUid, out var virtualItem)) return; - if (blocker.Target != ent.Comp.Target.Value) + if (virtualItem.BlockingEntity != ent.Comp.Target.Value) return; ReleaseHolderContribution(ent.Owner, ent.Comp.Target.Value, clearIfEmpty: true); @@ -304,17 +303,30 @@ private void OnHolderHandsModified(Entity ent, ref Did SyncHolderState(ent); } - private void OnHolderBlockerDropped(Entity ent, ref GettingDroppedAttemptEvent args) + private void OnHolderBlockerGettingDropped(Entity ent, ref GettingDroppedAttemptEvent args) { - if (!_activeHolderQuery.TryComp(args.User, out var holder)) + if (!_virtualItemQuery.TryComp(ent.Owner, out var virtualItem) || + !TryComp(args.User, out var holder) || + !TryReleaseHold((args.User, holder), virtualItem.BlockingEntity)) + { return; + } - if (holder.Target == null) + args.Cancelled = true; + } + + private void OnHolderVirtualItemDeleted(Entity ent, ref VirtualItemDeletedEvent args) + { + if (_timing.ApplyingState || + ent.Comp.Target == null || + ent.Comp.Target != args.BlockingEntity) + { return; + } - if (holder.Target != ent.Comp.Target) + if (HasOwnedHolderHandBlocker(ent.Owner, args.BlockingEntity)) return; - ReleaseHolderContribution(args.User, ent.Comp.Target, clearIfEmpty: true); + ReleaseHolderContribution(ent.Owner, args.BlockingEntity, clearIfEmpty: true); } } diff --git a/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.Lifecycle.cs b/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.Lifecycle.cs index 4f924f20e87..4ea89985959 100644 --- a/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.Lifecycle.cs +++ b/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.Lifecycle.cs @@ -71,6 +71,12 @@ private void OnFullHeldRemove(Entity en private void OnHolderStartup(Entity ent, ref ComponentStartup args) { + if (_timing.ApplyingState) + { + OnHolderStateRefreshed(ent); + return; + } + SyncHolderState(ent); } From 50f0d3873e23f86572903b14e57deb43ab0bfd97 Mon Sep 17 00:00:00 2001 From: drdth Date: Sat, 18 Apr 2026 01:05:10 +0300 Subject: [PATCH 14/27] fix: previous refactor --- .../_Scp/Holding/ScpHoldingSystem.Feedback.cs | 54 +++++++++++++++ .../_Scp/Holding/ScpHoldingSystem.cs | 38 ++++------- .../_Scp/Holding/ScpHoldingSystem.Feedback.cs | 46 +++++++++++-- .../Components/ActiveScpHoldableComponent.cs | 2 - .../Components/ScpHoldableComponent.cs | 43 ++++++------ .../Holding/Components/ScpHolderComponent.cs | 6 +- .../Systems/SharedScpHoldingSystem.Actions.cs | 63 ++++++++--------- .../SharedScpHoldingSystem.BreakoutAttempt.cs | 2 +- .../SharedScpHoldingSystem.Feedback.cs | 68 +------------------ .../Systems/SharedScpHoldingSystem.Hands.cs | 6 +- .../SharedScpHoldingSystem.Lifecycle.cs | 7 -- .../Systems/SharedScpHoldingSystem.State.cs | 2 +- .../Holding/Systems/SharedScpHoldingSystem.cs | 32 ++++----- 13 files changed, 179 insertions(+), 190 deletions(-) create mode 100644 Content.Client/_Scp/Holding/ScpHoldingSystem.Feedback.cs diff --git a/Content.Client/_Scp/Holding/ScpHoldingSystem.Feedback.cs b/Content.Client/_Scp/Holding/ScpHoldingSystem.Feedback.cs new file mode 100644 index 00000000000..59ce2f0f533 --- /dev/null +++ b/Content.Client/_Scp/Holding/ScpHoldingSystem.Feedback.cs @@ -0,0 +1,54 @@ +using Content.Shared._Scp.Holding.Components; +using Content.Shared.Coordinates; +using Robust.Shared.Audio; +using Robust.Shared.Audio.Systems; +using Robust.Shared.Prototypes; + +namespace Content.Client._Scp.Holding; + +public sealed partial class ScpHoldingSystem +{ + [Dependency] private readonly SharedAudioSystem _audio = default!; + + protected override void Popup(EntityUid target, string key, params (string, object)[] args) + { + } + + protected override void ShowBreakoutAttemptFeedback(Entity held) + { + if (!_timing.IsFirstTimePredicted) + return; + + if (!TryComp(held, out var holdable)) + return; + + foreach (var holderUid in held.Comp.Holders) + { + if (!TryComp(holderUid, out var holder)) + continue; + + if (holder.Target != held.Owner) + continue; + + SpawnBreakoutAttemptEffect(holderUid, holdable.BreakoutAttemptEffect); + } + + PlayBreakoutAttemptSound(held.Owner, holdable.BreakoutAttemptSound); + } + + private void SpawnBreakoutAttemptEffect(EntityUid holderUid, EntProtoId? effect) + { + if (effect == null) + return; + + PredictedSpawnAttachedTo(effect.Value, holderUid.ToCoordinates()); + } + + private void PlayBreakoutAttemptSound(EntityUid targetUid, SoundSpecifier? sound) + { + if (sound == null) + return; + + _audio.PlayPredicted(sound, targetUid, targetUid); + } +} diff --git a/Content.Client/_Scp/Holding/ScpHoldingSystem.cs b/Content.Client/_Scp/Holding/ScpHoldingSystem.cs index d30fc6c783b..06dad9690f6 100644 --- a/Content.Client/_Scp/Holding/ScpHoldingSystem.cs +++ b/Content.Client/_Scp/Holding/ScpHoldingSystem.cs @@ -12,7 +12,7 @@ namespace Content.Client._Scp.Holding; -public sealed class ScpHoldingSystem : SharedScpHoldingSystem +public sealed partial class ScpHoldingSystem : SharedScpHoldingSystem { [Dependency] private readonly HandsSystem _hands = default!; [Dependency] private readonly Robust.Client.Physics.PhysicsSystem _physics = default!; @@ -23,7 +23,6 @@ public sealed class ScpHoldingSystem : SharedScpHoldingSystem private EntityUid? _trackedHolderTarget; private EntityQuery _handsQuery; - private EntityQuery _physicsQuery; private EntityQuery _blockerQuery; private EntityQuery _activeHolderQuery; private EntityQuery _virtualItemQuery; @@ -33,7 +32,6 @@ public override void Initialize() base.Initialize(); _handsQuery = GetEntityQuery(); - _physicsQuery = GetEntityQuery(); _blockerQuery = GetEntityQuery(); _activeHolderQuery = GetEntityQuery(); _virtualItemQuery = GetEntityQuery(); @@ -126,21 +124,25 @@ private void OnUpdateHeldPredicted(Entity ent, ref U args.BlockPrediction = true; } - protected override bool ShouldUsePredictedBreakoutFeedback => true; - - protected override bool ShouldUpdateHeld(EntityUid uid, ActiveScpHoldableComponent held) + private void ReconcileHeldAfterState(Entity held) { - return _physicsQuery.TryComp(uid, out var physics) && physics.Predict; - } + _physics.UpdateIsPredicted(held); - protected override bool CanShowBreakoutAttemptFeedback() - { - return _timing.IsFirstTimePredicted; + if (HasComp(held.Owner)) + SyncPlaceholderHands(held); } - protected override void OnHeldStateRefreshed(Entity held) + protected override void UpdateHeldStates() { - _physics.UpdateIsPredicted(held); + var query = EntityQueryEnumerator(); + 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 held) @@ -148,16 +150,6 @@ protected override void OnHeldStateShutdown(Entity h _physics.UpdateIsPredicted(held); } - protected override void OnHolderStateRefreshed(Entity holder) - { - UpdateTrackedLocalHeldTarget(holder, holder.Comp.Target); - } - - protected override void OnHolderStateShutdown(EntityUid holderUid, EntityUid? target) - { - UpdateTrackedLocalHeldTarget(holderUid, null, target); - } - private void ReconcileLocalHolderBlocker(EntityUid blocker, EntityUid? holderUid = null) { holderUid ??= _player.LocalEntity; diff --git a/Content.Server/_Scp/Holding/ScpHoldingSystem.Feedback.cs b/Content.Server/_Scp/Holding/ScpHoldingSystem.Feedback.cs index a3720699bdb..f06c85eb6c6 100644 --- a/Content.Server/_Scp/Holding/ScpHoldingSystem.Feedback.cs +++ b/Content.Server/_Scp/Holding/ScpHoldingSystem.Feedback.cs @@ -1,22 +1,54 @@ -using Content.Server.Popups; +using Content.Server.Popups; +using Content.Shared._Scp.Holding.Components; +using Content.Shared.Coordinates; +using Robust.Shared.Audio; +using Robust.Shared.Audio.Systems; +using Robust.Shared.Prototypes; namespace Content.Server._Scp.Holding; public sealed partial class ScpHoldingSystem { + [Dependency] private readonly SharedAudioSystem _audio = default!; [Dependency] private readonly PopupSystem _popup = default!; - protected override void PopupHolder(EntityUid holder, string key, params (string, object)[] args) + protected override void Popup(EntityUid target, string key, params (string, object)[] args) { - base.PopupHolder(holder, key, args); + _popup.PopupEntity(Loc.GetString(key, args), target, target); + } + + protected override void ShowBreakoutAttemptFeedback(Entity held) + { + if (!TryComp(held, out var holdable)) + return; + + foreach (var holderUid in held.Comp.Holders) + { + if (!TryComp(holderUid, out var holder)) + continue; + + if (holder.Target != held.Owner) + continue; - _popup.PopupEntity(Loc.GetString(key, args), holder, holder); + SpawnBreakoutAttemptEffect(holderUid, holdable.BreakoutAttemptEffect); + } + + PlayBreakoutAttemptSound(held.Owner, holdable.BreakoutAttemptSound); } - protected override void PopupTarget(EntityUid target, string key, params (string, object)[] args) + private void SpawnBreakoutAttemptEffect(EntityUid holderUid, EntProtoId? effect) { - base.PopupTarget(target, key, args); + if (effect == null) + return; - _popup.PopupEntity(Loc.GetString(key, args), target, target); + SpawnAttachedTo(effect.Value, holderUid.ToCoordinates()); + } + + private void PlayBreakoutAttemptSound(EntityUid targetUid, SoundSpecifier? sound) + { + if (sound == null) + return; + + _audio.PlayPvs(sound, targetUid); } } diff --git a/Content.Shared/_Scp/Holding/Components/ActiveScpHoldableComponent.cs b/Content.Shared/_Scp/Holding/Components/ActiveScpHoldableComponent.cs index 6c72f1a8e61..65081725e47 100644 --- a/Content.Shared/_Scp/Holding/Components/ActiveScpHoldableComponent.cs +++ b/Content.Shared/_Scp/Holding/Components/ActiveScpHoldableComponent.cs @@ -1,7 +1,5 @@ using Content.Shared._Scp.Holding.Systems; -using Content.Shared.Alert; using Robust.Shared.GameStates; -using Robust.Shared.Prototypes; using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom; namespace Content.Shared._Scp.Holding.Components; diff --git a/Content.Shared/_Scp/Holding/Components/ScpHoldableComponent.cs b/Content.Shared/_Scp/Holding/Components/ScpHoldableComponent.cs index 8f1cf76da66..635874c58c4 100644 --- a/Content.Shared/_Scp/Holding/Components/ScpHoldableComponent.cs +++ b/Content.Shared/_Scp/Holding/Components/ScpHoldableComponent.cs @@ -1,5 +1,6 @@ using Content.Shared.Whitelist; using Robust.Shared.Audio; +using Robust.Shared.GameStates; using Robust.Shared.Prototypes; namespace Content.Shared._Scp.Holding.Components; @@ -7,129 +8,129 @@ namespace Content.Shared._Scp.Holding.Components; /// /// Marks an entity as a valid target for the SCP holding mechanic and stores per-target hold tuning. /// -[RegisterComponent] +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] public sealed partial class ScpHoldableComponent : Component { /// /// Optional whitelist of entities that may hold this target. /// - [DataField] + [DataField, AutoNetworkedField] public EntityWhitelist? HolderWhitelist; /// /// Optional blacklist of entities that may not hold this target. /// - [DataField] + [DataField, AutoNetworkedField] public EntityWhitelist? HolderBlacklist; /// /// Minimum uninterrupted full hold duration before a breakout do-after may start. /// - [DataField] + [DataField, AutoNetworkedField] public TimeSpan FullHoldDelay = TimeSpan.FromSeconds(10); /// /// Duration of the visible breakout do-after for a full hold. /// - [DataField] + [DataField, AutoNetworkedField] public TimeSpan FullBreakoutDuration = TimeSpan.FromSeconds(5); /// /// Duration of immunity after a successful full breakout. /// - [DataField] + [DataField, AutoNetworkedField] public TimeSpan PostBreakoutImmunity = TimeSpan.FromSeconds(5); /// /// Optional effect prototype spawned on each holder when a full-hold breakout attempt starts. /// - [DataField] + [DataField, AutoNetworkedField] public EntProtoId? BreakoutAttemptEffect = "WhistleExclamation"; /// /// Optional sound played from the held target when a full-hold breakout attempt starts. /// - [DataField] + [DataField, AutoNetworkedField] public SoundSpecifier? BreakoutAttemptSound = new SoundCollectionSpecifier("storageRustle", AudioParams.Default.WithVolume(-2f).WithMaxDistance(4f).WithVariation(0.15f)); /// /// Maximum unobstructed range allowed between holder and target. /// - [DataField] + [DataField, AutoNetworkedField] public float HoldRange = 1f; /// /// Scales the preferred soft-drag distance from the configured hold range. /// - [DataField] + [DataField, AutoNetworkedField] public float SoftDragDistanceFactor = 0.3f; /// /// Lower clamp for the preferred soft-drag distance. /// - [DataField] + [DataField, AutoNetworkedField] public float SoftDragMinimumDistance = 0.4f; /// /// Upper clamp for the preferred soft-drag distance. /// - [DataField] + [DataField, AutoNetworkedField] public float SoftDragMaximumDistance = 0.6f; /// /// Distance where the system snaps to the holder-facing direction instead of offset. /// - [DataField] + [DataField, AutoNetworkedField] public float SoftDragSnapTolerance = 0.03f; /// /// Distance where the held target is considered settled and only matches holder velocity. /// - [DataField] + [DataField, AutoNetworkedField] public float SoftDragSettleTolerance = 0.08f; /// /// Minimum velocity used to derive drag direction from holder movement. /// - [DataField] + [DataField, AutoNetworkedField] public float SoftDragVelocityDirectionThreshold = 0.05f; /// /// Minimum time window used to catch the held target back up to its desired position. /// - [DataField] + [DataField, AutoNetworkedField] public float SoftDragCatchUpTime = 0.05f; /// /// Maximum correction speed applied while soft-dragging the held target. /// - [DataField] + [DataField, AutoNetworkedField] public float SoftDragMaximumCorrectionSpeed = 6f; /// /// Extra correction strength applied when the held target moves away from its desired position. /// - [DataField] + [DataField, AutoNetworkedField] public float SoftDragAwayVelocityStrength = 0.6f; /// /// Velocity difference threshold before the held body's velocity is updated. /// - [DataField] + [DataField, AutoNetworkedField] public float SoftDragVelocityTolerance = 0.05f; /// /// Walk speed modifier applied to holders while they move this target. /// Lower values make the target heavier to move. /// - [DataField] + [DataField, AutoNetworkedField] public float HolderWalkModifier = 0.5f; /// /// Sprint speed modifier applied to holders while they move this target. /// Lower values make the target heavier to move. /// - [DataField] + [DataField, AutoNetworkedField] public float HolderSprintModifier = 0.5f; } diff --git a/Content.Shared/_Scp/Holding/Components/ScpHolderComponent.cs b/Content.Shared/_Scp/Holding/Components/ScpHolderComponent.cs index 9cf07cbf47c..e8992e97883 100644 --- a/Content.Shared/_Scp/Holding/Components/ScpHolderComponent.cs +++ b/Content.Shared/_Scp/Holding/Components/ScpHolderComponent.cs @@ -20,18 +20,18 @@ public sealed partial class ScpHolderComponent : Component /// /// Optional whitelist of entities this holder may grab. /// - [DataField] + [DataField, AutoNetworkedField] public EntityWhitelist? HoldableWhitelist; /// /// Optional blacklist of entities this holder may not grab. /// - [DataField] + [DataField, AutoNetworkedField] public EntityWhitelist? HoldableBlacklist; /// /// Cooldown applied after each successful hold contribution start. /// - [DataField] + [DataField, AutoNetworkedField] public TimeSpan HoldActionCooldown = TimeSpan.FromSeconds(1); } diff --git a/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.Actions.cs b/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.Actions.cs index f9a3ca465d6..d339fe74161 100644 --- a/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.Actions.cs +++ b/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.Actions.cs @@ -22,12 +22,12 @@ private void InitializeHoldQueries() public bool TryToggleHold(Entity holder, EntityUid target, bool attemptChecked = false) { - if (_activeHolderQuery.TryComp(holder.Owner, out var activeHolder) && activeHolder.Target != null) + if (_activeHolderQuery.TryComp(holder, out var activeHolder) && activeHolder.Target != null) { if (activeHolder.Target.Value == target) return TryReleaseHold(holder, target); - PopupHolder(holder.Owner, "scp-hold-already-holding-other"); + Popup(holder, "scp-hold-already-holding-other"); return false; } @@ -38,7 +38,7 @@ public bool TryToggleHold(Entity holder, EntityUid target, b return false; var held = EnsureHeldState(target); - AddHolderContribution(holder.Owner, held); + AddHolderContribution(holder, held); SyncHeldState(held); StartHoldCooldown(holder); @@ -50,13 +50,13 @@ public bool TryReleaseHold(Entity holder, EntityUid target) if (!CanReleaseHold(holder, target)) return false; - ReleaseHolderContribution(holder.Owner, target, clearIfEmpty: true); + ReleaseHolderContribution(holder, target, clearIfEmpty: true); return true; } public bool CanReleaseHold(Entity holder, EntityUid target, bool quiet = false) { - if (!_activeHolderQuery.TryComp(holder.Owner, out var activeHolder) || + if (!_activeHolderQuery.TryComp(holder, out var activeHolder) || activeHolder.Target == null) { return false; @@ -65,7 +65,7 @@ public bool CanReleaseHold(Entity holder, EntityUid target, if (activeHolder.Target != target) { if (!quiet) - PopupHolder(holder.Owner, "scp-hold-already-holding-other"); + Popup(holder, "scp-hold-already-holding-other"); return false; } @@ -89,7 +89,7 @@ public bool CanToggleHold( if (!_holdableQuery.TryComp(target, out var holdable)) { if (!quiet) - PopupHolder(holder.Owner, "scp-hold-target-not-holdable", ("target", target)); + Popup(holder, "scp-hold-target-not-holdable", ("target", target)); return false; } @@ -97,23 +97,23 @@ public bool CanToggleHold( if (!_whitelist.CheckBoth(target, holder.Comp.HoldableBlacklist, holder.Comp.HoldableWhitelist)) { if (!quiet) - PopupHolder(holder.Owner, "scp-hold-target-invalid", ("target", target)); + Popup(holder, "scp-hold-target-invalid", ("target", target)); return false; } - if (!_whitelist.CheckBoth(holder.Owner, holdable.HolderBlacklist, holdable.HolderWhitelist)) + if (!_whitelist.CheckBoth(holder, holdable.HolderBlacklist, holdable.HolderWhitelist)) { if (!quiet) - PopupHolder(holder.Owner, "scp-hold-target-invalid", ("target", target)); + Popup(holder, "scp-hold-target-invalid", ("target", target)); return false; } - if (!_moverQuery.HasComp(holder.Owner)) + if (!_moverQuery.HasComp(holder)) { if (!quiet) - PopupHolder(holder.Owner, "scp-hold-target-invalid", ("target", target)); + Popup(holder, "scp-hold-target-invalid", ("target", target)); return false; } @@ -121,7 +121,7 @@ public bool CanToggleHold( if (!_moverQuery.HasComp(target)) { if (!quiet) - PopupHolder(holder.Owner, "scp-hold-target-invalid", ("target", target)); + Popup(holder, "scp-hold-target-invalid", ("target", target)); return false; } @@ -129,7 +129,7 @@ public bool CanToggleHold( if (!_physicsQuery.TryComp(target, out var targetPhysics)) { if (!quiet) - PopupHolder(holder.Owner, "scp-hold-target-invalid", ("target", target)); + Popup(holder, "scp-hold-target-invalid", ("target", target)); return false; } @@ -137,7 +137,7 @@ public bool CanToggleHold( if (targetPhysics.BodyType == BodyType.Static) { if (!quiet) - PopupHolder(holder.Owner, "scp-hold-target-invalid", ("target", target)); + Popup(holder, "scp-hold-target-invalid", ("target", target)); return false; } @@ -145,7 +145,7 @@ public bool CanToggleHold( if (!_container.IsInSameOrNoContainer(holder.Owner, target)) { if (!quiet) - PopupHolder(holder.Owner, "scp-hold-target-invalid", ("target", target)); + Popup(holder, "scp-hold-target-invalid", ("target", target)); return false; } @@ -153,15 +153,15 @@ public bool CanToggleHold( if (TryComp(target, out _)) { if (!quiet) - PopupHolder(holder.Owner, "scp-hold-target-immune", ("target", target)); + Popup(holder, "scp-hold-target-immune", ("target", target)); return false; } - if (!ignoreHandAvailability && !HasAvailableHolderHand(holder.Owner)) + if (!ignoreHandAvailability && !HasAvailableHolderHand(holder)) { if (!quiet) - PopupHolder(holder.Owner, "scp-hold-holder-no-free-hand", ("target", target)); + Popup(holder, "scp-hold-holder-no-free-hand", ("target", target)); return false; } @@ -172,7 +172,7 @@ public bool CanToggleHold( if (_activeHoldableFullHoldStateQuery.HasComp(target)) { if (!quiet) - PopupHolder(holder.Owner, "scp-hold-target-fully-held", ("target", target)); + Popup(holder.Owner, "scp-hold-target-fully-held", ("target", target)); return false; } @@ -181,12 +181,12 @@ public bool CanToggleHold( if (!_interaction.InRangeUnobstructed(holder.Owner, target, range)) { if (!quiet) - PopupHolder(holder.Owner, "scp-hold-target-too-far", ("target", target)); + Popup(holder, "scp-hold-target-too-far", ("target", target)); return false; } - if (checkAttempt && !CanPassHoldAttempt(holder.Owner, target)) + if (checkAttempt && !CanPassHoldAttempt(holder, target)) return false; return true; @@ -208,18 +208,9 @@ public bool TryForceBreakOut(Entity held, bool viaM return true; } - protected void ReconcileHeldAfterState(Entity held) - { - OnHeldStateRefreshed(held); - - if (_activeHoldableFullHoldStateQuery.HasComp(held.Owner)) - SyncPlaceholderHands(held); - } - public void SyncHolderState(Entity holder) { SyncHolderHandBlocker(holder); - OnHolderStateRefreshed(holder); } private bool TrySoftBreakOut(Entity held, bool viaMovement) @@ -228,7 +219,7 @@ private bool TrySoftBreakOut(Entity held, bool viaMo return false; if (!viaMovement) - PopupTarget(held.Owner, "scp-hold-breakout-start"); + Popup(held, "scp-hold-breakout-start"); BreakOut(held, viaMovement, applyImmunity: false); return true; @@ -244,7 +235,7 @@ private bool TryStartFullBreakout(Entity held, bool if (fullHeld.StartedAt == TimeSpan.Zero) { - PopupTarget(held.Owner, "scp-hold-breakout-too-early", ("seconds", 1)); + Popup(held, "scp-hold-breakout-too-early", ("seconds", 1)); return false; } @@ -253,7 +244,7 @@ private bool TryStartFullBreakout(Entity held, bool { var remaining = breakoutAvailableAt - _timing.CurTime; var remainingSeconds = Math.Max(1, (int)Math.Ceiling(remaining.TotalSeconds)); - PopupTarget(held.Owner, "scp-hold-breakout-too-early", ("seconds", remainingSeconds)); + Popup(held, "scp-hold-breakout-too-early", ("seconds", remainingSeconds)); return false; } @@ -279,7 +270,7 @@ private bool TryStartFullBreakout(Entity held, bool StartBreakoutAttempt(held.Owner, id.Value); - PopupTarget(held.Owner, "scp-hold-breakout-start"); + Popup(held, "scp-hold-breakout-start"); return true; } @@ -291,7 +282,7 @@ private bool CanStartHold(Entity holder, bool quiet = false) if (!quiet) { var remainingSeconds = Math.Max(1, (int)Math.Ceiling(remaining.TotalSeconds)); - PopupHolder(holder.Owner, "scp-hold-holder-action-on-cooldown", ("seconds", remainingSeconds)); + Popup(holder, "scp-hold-holder-action-on-cooldown", ("seconds", remainingSeconds)); } return false; diff --git a/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.BreakoutAttempt.cs b/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.BreakoutAttempt.cs index 77ea64276af..ad86c2bb8b6 100644 --- a/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.BreakoutAttempt.cs +++ b/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.BreakoutAttempt.cs @@ -71,7 +71,7 @@ private void OnBreakoutDoAfter(Entity ent, ref ScpHo if (args.Cancelled) { - PopupTarget(ent.Owner, "scp-hold-breakout-interrupted"); + Popup(ent, "scp-hold-breakout-interrupted"); return; } diff --git a/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.Feedback.cs b/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.Feedback.cs index e5517e4e1c2..586ffc8ab7a 100644 --- a/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.Feedback.cs +++ b/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.Feedback.cs @@ -1,74 +1,10 @@ using Content.Shared._Scp.Holding.Components; -using Content.Shared.Coordinates; -using Robust.Shared.Audio.Systems; -using Robust.Shared.Audio; -using Robust.Shared.Prototypes; namespace Content.Shared._Scp.Holding.Systems; public abstract partial class SharedScpHoldingSystem { - /* - * Feedback-local dependencies plus popup/audio helpers. - */ + protected abstract void ShowBreakoutAttemptFeedback(Entity held); - [Dependency] private readonly SharedAudioSystem _audio = default!; - - private void ShowBreakoutAttemptFeedback(Entity held) - { - if (!CanShowBreakoutAttemptFeedback()) - return; - - if (!TryComp(held, out var holdable)) - return; - - foreach (var holderUid in held.Comp.Holders) - { - if (!_activeHolderQuery.TryComp(holderUid, out var holder)) - continue; - - if (holder.Target != held.Owner) - continue; - - SpawnBreakoutAttemptEffect(holderUid, holdable.BreakoutAttemptEffect); - } - - PlayBreakoutAttemptSound(held.Owner, holdable.BreakoutAttemptSound); - } - - private void SpawnBreakoutAttemptEffect(EntityUid holderUid, EntProtoId? effect) - { - if (effect == null) - return; - - if (ShouldUsePredictedBreakoutFeedback) - { - PredictedSpawnAttachedTo(effect.Value, holderUid.ToCoordinates()); - return; - } - - SpawnAttachedTo(effect.Value, holderUid.ToCoordinates()); - } - - private void PlayBreakoutAttemptSound(EntityUid targetUid, SoundSpecifier? sound) - { - if (sound == null) - return; - - if (ShouldUsePredictedBreakoutFeedback) - { - _audio.PlayPredicted(sound, targetUid, targetUid); - return; - } - - _audio.PlayPvs(sound, targetUid); - } - - protected virtual void PopupHolder(EntityUid holder, string key, params (string, object)[] args) { } - - protected virtual void PopupTarget(EntityUid target, string key, params (string, object)[] args) { } - - protected virtual bool ShouldUsePredictedBreakoutFeedback => false; - - protected virtual bool CanShowBreakoutAttemptFeedback() => true; + protected abstract void Popup(EntityUid target, string key, params (string, object)[] args); } diff --git a/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.Hands.cs b/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.Hands.cs index 4bb4cdc510c..6f0ed9a7d46 100644 --- a/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.Hands.cs +++ b/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.Hands.cs @@ -43,7 +43,7 @@ private void InitializeHandEvents() SubscribeLocalEvent(OnHolderBlockerGettingDropped); } - private void SyncPlaceholderHands(Entity held) + protected void SyncPlaceholderHands(Entity held) { if (!_handsQuery.TryComp(held.Owner, out var hands)) return; @@ -102,10 +102,10 @@ private void DeleteInvalidHeldHandBlockers(Entity held) foreach (var heldItem in _hands.EnumerateHeld(held)) { - if (!_virtualItemQuery.TryComp(heldItem, out var virtualItem)) + if (!_heldHandBlockerQuery.HasComp(heldItem)) continue; - if (!_heldHandBlockerQuery.TryComp(heldItem, out var blocker)) + if (!_virtualItemQuery.TryComp(heldItem, out var virtualItem)) continue; if (!IsValidHeldHandBlocker(virtualItem)) diff --git a/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.Lifecycle.cs b/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.Lifecycle.cs index 4ea89985959..982ca10b8e6 100644 --- a/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.Lifecycle.cs +++ b/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.Lifecycle.cs @@ -33,7 +33,6 @@ private void OnHeldStartup(Entity ent, ref Component { _alerts.ShowAlert(ent.Owner, HeldAlert); _statusEffects.TrySetStatusEffectDuration(ent, GrabbedStatusEffect); - OnHeldStateRefreshed(ent); ValidateAllActions(ent.Owner); } @@ -72,24 +71,18 @@ private void OnFullHeldRemove(Entity en private void OnHolderStartup(Entity ent, ref ComponentStartup args) { if (_timing.ApplyingState) - { - OnHolderStateRefreshed(ent); return; - } SyncHolderState(ent); } private void OnHolderShutdown(Entity ent, ref ComponentShutdown args) { - var target = ent.Comp.Target; ent.Comp.Target = null; DeleteHolderHandBlockers(ent.Owner); if (!_timing.ApplyingState) RemComp(ent.Owner); - - OnHolderStateShutdown(ent.Owner, target); } private void OnHolderSlowdownRemove(Entity ent, ref ComponentRemove args) diff --git a/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.State.cs b/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.State.cs index 8b682af674c..0f64971626b 100644 --- a/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.State.cs +++ b/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.State.cs @@ -34,7 +34,7 @@ private void InitializeStateQueries() _bodyQuery = GetEntityQuery(); } - private void UpdateHeld(Entity held) + protected void UpdateHeld(Entity held) { if (!TryGetHeldHoldable(held, out var holdable)) return; diff --git a/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.cs b/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.cs index ebca9fe830a..983a1d42db8 100644 --- a/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.cs +++ b/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.cs @@ -56,6 +56,12 @@ public override void Shutdown() } public override void Update(float frameTime) + { + UpdateSharedState(frameTime); + UpdateHeldStates(); + } + + protected void UpdateSharedState(float frameTime) { base.Update(frameTime); @@ -65,35 +71,21 @@ public override void Update(float frameTime) if (_timing.CurTime >= immune.ExpiresAt) RemCompDeferred(uid); } + } + protected void UpdateAllHeldStates() + { var heldQuery = EntityQueryEnumerator(); while (heldQuery.MoveNext(out var uid, out var held)) { - if (!ShouldUpdateHeld(uid, held)) - continue; - UpdateHeld((uid, held)); } } - protected virtual bool ShouldUpdateHeld(EntityUid uid, ActiveScpHoldableComponent held) - { - return true; - } - - protected virtual void OnHeldStateRefreshed(Entity held) + protected virtual void UpdateHeldStates() { + UpdateAllHeldStates(); } - protected virtual void OnHeldStateShutdown(Entity held) - { - } - - protected virtual void OnHolderStateRefreshed(Entity holder) - { - } - - protected virtual void OnHolderStateShutdown(EntityUid holderUid, EntityUid? target) - { - } + protected abstract void OnHeldStateShutdown(Entity held); } From 807521baa11b7cae86c1d5ea01e61c5efcef657e Mon Sep 17 00:00:00 2001 From: drdth Date: Sat, 18 Apr 2026 03:25:55 +0300 Subject: [PATCH 15/27] fix: fixes --- .../Tests/_Scp/ScpHoldingTest.cs | 110 ++++++++++++++---- .../Components/ActiveScpHoldableComponent.cs | 8 +- .../Systems/SharedScpHoldingSystem.Drag.cs | 27 +++-- .../Systems/SharedScpHoldingSystem.State.cs | 47 ++------ .../Scp096/Main/Components/Scp096Component.cs | 2 +- .../Systems/SharedScp096System.Holding.cs | 47 ++++++-- 6 files changed, 156 insertions(+), 85 deletions(-) diff --git a/Content.IntegrationTests/Tests/_Scp/ScpHoldingTest.cs b/Content.IntegrationTests/Tests/_Scp/ScpHoldingTest.cs index 30521cf32d4..c692e369e8b 100644 --- a/Content.IntegrationTests/Tests/_Scp/ScpHoldingTest.cs +++ b/Content.IntegrationTests/Tests/_Scp/ScpHoldingTest.cs @@ -11,6 +11,8 @@ using Content.Shared._Scp.Holding.Systems; using Content.Shared.Body.Components; using Content.Shared.Body.Part; +using Content.Shared.Damage.Components; +using Content.Shared.FixedPoint; using Content.Shared.Hands.Components; using Content.Shared.Hands.EntitySystems; using Content.Shared.Interaction.Components; @@ -36,6 +38,7 @@ using Robust.Shared.Prototypes; using Robust.Shared.Timing; using Robust.UnitTesting; +using Content.Shared.Stunnable; using Content.Shared.Whitelist; namespace Content.IntegrationTests.Tests._Scp; @@ -86,6 +89,12 @@ private static EntityWhitelist CreateComponentWhitelist(params string[] componen - TestListener """; + [Test] + public void ActiveScpHoldableStateDoesNotStorePrimaryHolder() + { + Assert.That(typeof(ActiveScpHoldableComponent).GetField("PrimaryHolder"), Is.Null); + } + [Test] public async Task HoldAppliesStatusEffectImmediately() { @@ -342,8 +351,7 @@ await server.WaitAssertion(() => Assert.Multiple(() => { Assert.That(HasFullHold(sEntMan, target), Is.False); - Assert.That(held.PrimaryHolder, Is.EqualTo(holder)); - Assert.That(held.Holders, Has.Count.EqualTo(1)); + Assert.That(held.Holders, Is.EqualTo(new[] { holder })); Assert.That(move.Cancelled, Is.False); Assert.That(collide.Cancelled, Is.True); Assert.That(targetCollide.Cancelled, Is.True); @@ -386,8 +394,7 @@ await client.WaitAssertion(() => Assert.Multiple(() => { Assert.That(HasFullHold(cEntMan, clientTarget), Is.False); - Assert.That(held.PrimaryHolder, Is.EqualTo(clientHolder)); - Assert.That(held.Holders, Has.Count.EqualTo(1)); + Assert.That(held.Holders, Is.EqualTo(new[] { clientHolder })); Assert.That(distance, Is.GreaterThan(0.18f)); Assert.That(distance, Is.LessThan(0.55f)); Assert.That(collide.Cancelled, Is.True); @@ -1214,6 +1221,76 @@ await server.WaitAssertion(() => await pair.CleanReturnAsync(); } + [Test] + public async Task Scp096FullBreakoutAffectsAllHoldersAndThrowsOverlappingHoldersApart() + { + await using var pair = await PoolManager.GetServerClient(new PoolSettings { Fresh = true }); + var server = pair.Server; + var entMan = server.EntMan; + var holding = server.System(); + var physics = server.System(); + var transform = server.System(); + var map = await pair.CreateTestMap(); + + EntityUid holderOne = default; + EntityUid holderTwo = default; + EntityUid target = default; + + await server.WaitPost(() => + { + holderOne = entMan.SpawnEntity(HolderPrototype, map.GridCoords); + holderTwo = entMan.SpawnEntity(HolderPrototype, map.GridCoords); + target = entMan.SpawnEntity("Scp096", map.GridCoords); + + var holdable = entMan.GetComponent(target); + holdable.FullHoldDelay = TimeSpan.Zero; + holdable.FullBreakoutDuration = TimeSpan.Zero; + + StartHold(entMan, holding, holderOne, target); + StartHold(entMan, holding, holderTwo, target); + }); + await server.WaitRunTicks(2); + + await server.WaitAssertion(() => + { + Assert.That(HasFullHold(entMan, target), Is.True); + }); + + await server.WaitPost(() => + { + transform.SetCoordinates(holderOne, map.GridCoords); + transform.SetCoordinates(holderTwo, map.GridCoords); + transform.SetCoordinates(target, map.GridCoords); + RaiseMoveInput(entMan, target); + }); + await server.WaitRunTicks(4); + + await server.WaitAssertion(() => + { + var holderOneDamage = entMan.GetComponent(holderOne); + var holderTwoDamage = entMan.GetComponent(holderTwo); + var holderOneVelocity = physics.GetMapLinearVelocity(holderOne); + var holderTwoVelocity = physics.GetMapLinearVelocity(holderTwo); + + Assert.Multiple(() => + { + Assert.That(entMan.HasComponent(target), Is.False); + Assert.That(entMan.HasComponent(target), Is.False); + Assert.That(entMan.HasComponent(holderOne), Is.False); + Assert.That(entMan.HasComponent(holderTwo), Is.False); + Assert.That(entMan.HasComponent(holderOne), Is.True); + Assert.That(entMan.HasComponent(holderTwo), Is.True); + Assert.That(holderOneDamage.TotalDamage, Is.GreaterThan(FixedPoint2.Zero)); + Assert.That(holderTwoDamage.TotalDamage, Is.GreaterThan(FixedPoint2.Zero)); + Assert.That(MathF.Abs(holderOneVelocity.X), Is.GreaterThan(1f)); + Assert.That(MathF.Abs(holderTwoVelocity.X), Is.GreaterThan(1f)); + Assert.That(MathF.Sign(holderOneVelocity.X), Is.EqualTo(-MathF.Sign(holderTwoVelocity.X))); + }); + }); + + await pair.CleanReturnAsync(); + } + [Test] public async Task FullBreakoutRestoresMovementOnServerAndClient() { @@ -1715,8 +1792,7 @@ await server.WaitAssertion(() => Assert.Multiple(() => { Assert.That(HasFullHold(sEntMan, target), Is.False); - Assert.That(held.Holders, Has.Count.EqualTo(1)); - Assert.That(held.PrimaryHolder, Is.EqualTo(serverPlayer)); + Assert.That(held.Holders, Is.EqualTo(new[] { serverPlayer })); Assert.That(distance, Is.GreaterThan(0.18f)); Assert.That(distance, Is.LessThan(0.55f)); Assert.That(puller.Pulling, Is.Null); @@ -1738,8 +1814,7 @@ await client.WaitAssertion(() => Assert.Multiple(() => { Assert.That(HasFullHold(cEntMan, clientTarget), Is.False); - Assert.That(held.Holders, Has.Count.EqualTo(1)); - Assert.That(held.PrimaryHolder, Is.EqualTo(clientPlayer)); + Assert.That(held.Holders, Is.EqualTo(new[] { clientPlayer })); Assert.That(distance, Is.GreaterThan(0.18f)); Assert.That(distance, Is.LessThan(0.55f)); Assert.That(puller.Pulling, Is.Null); @@ -2171,7 +2246,7 @@ await client.WaitAssertion(() => } [Test] - public async Task ClientPrimaryReassignmentKeepsCustomDragAndReconcilesCleanly() + public async Task ClientAnchorReassignmentKeepsCustomDragAndReconcilesCleanly() { await using var pair = await PoolManager.GetServerClient(new PoolSettings { @@ -2225,8 +2300,7 @@ await server.WaitAssertion(() => Assert.Multiple(() => { Assert.That(HasFullHold(sEntMan, target), Is.False); - Assert.That(held.Holders, Has.Count.EqualTo(2)); - Assert.That(held.PrimaryHolder, Is.EqualTo(serverPlayer)); + Assert.That(held.Holders, Is.EqualTo(new[] { serverPlayer, holderTwo })); Assert.That(distance, Is.GreaterThan(0.18f)); Assert.That(distance, Is.LessThan(0.5f)); Assert.That(contacts, Does.Not.Contain(target)); @@ -2255,8 +2329,7 @@ await client.WaitAssertion(() => Assert.Multiple(() => { Assert.That(HasFullHold(cEntMan, clientTarget), Is.False); - Assert.That(held.Holders, Has.Count.EqualTo(2)); - Assert.That(held.PrimaryHolder, Is.EqualTo(clientPlayer)); + Assert.That(held.Holders, Is.EqualTo(new[] { clientPlayer, clientHolderTwo })); Assert.That(distance, Is.GreaterThan(0.18f)); Assert.That(distance, Is.LessThan(0.5f)); Assert.That(contacts, Does.Not.Contain(clientTarget)); @@ -2285,8 +2358,7 @@ await server.WaitAssertion(() => Assert.Multiple(() => { - Assert.That(held.Holders, Has.Count.EqualTo(1)); - Assert.That(held.PrimaryHolder, Is.EqualTo(holderTwo)); + Assert.That(held.Holders, Is.EqualTo(new[] { holderTwo })); Assert.That(distance, Is.GreaterThan(0.18f)); Assert.That(distance, Is.LessThan(0.55f)); Assert.That(contacts, Does.Not.Contain(target)); @@ -2306,8 +2378,7 @@ await client.WaitAssertion(() => Assert.Multiple(() => { Assert.That(HasFullHold(cEntMan, clientTarget), Is.False); - Assert.That(held.Holders, Has.Count.EqualTo(1)); - Assert.That(held.PrimaryHolder, Is.EqualTo(clientHolderTwo)); + Assert.That(held.Holders, Is.EqualTo(new[] { clientHolderTwo })); Assert.That(distance, Is.GreaterThan(0.18f)); Assert.That(distance, Is.LessThan(0.7f)); Assert.That(contacts, Does.Not.Contain(clientTarget)); @@ -2544,7 +2615,7 @@ await server.WaitAssertion(() => Assert.Multiple(() => { Assert.That(HasFullHold(entMan, target), Is.False); - Assert.That(held.PrimaryHolder, Is.EqualTo(holderOne)); + Assert.That(held.Holders, Is.EqualTo(new[] { holderOne, holderTwo })); Assert.That(holderOnePuller.Pulling, Is.Null); Assert.That(holderTwoPuller.Pulling, Is.Null); Assert.That(pullable.Puller, Is.Null); @@ -2568,8 +2639,7 @@ await server.WaitAssertion(() => Assert.Multiple(() => { Assert.That(HasFullHold(entMan, target), Is.False); - Assert.That(held.Holders, Has.Count.EqualTo(1)); - Assert.That(held.PrimaryHolder, Is.EqualTo(holderTwo)); + Assert.That(held.Holders, Is.EqualTo(new[] { holderTwo })); Assert.That(holderOnePuller.Pulling, Is.Null); Assert.That(holderTwoPuller.Pulling, Is.Null); Assert.That(pullable.Puller, Is.Null); diff --git a/Content.Shared/_Scp/Holding/Components/ActiveScpHoldableComponent.cs b/Content.Shared/_Scp/Holding/Components/ActiveScpHoldableComponent.cs index 65081725e47..302e9f89c0d 100644 --- a/Content.Shared/_Scp/Holding/Components/ActiveScpHoldableComponent.cs +++ b/Content.Shared/_Scp/Holding/Components/ActiveScpHoldableComponent.cs @@ -18,17 +18,11 @@ public sealed partial class ActiveScpHoldableComponent : Component public TimeSpan SoftEscapeAvailableAt; /// - /// Ordered holder list used for reassignment and contribution counting. + /// Ordered holder list used for reassignment, contribution counting, and drag-anchor selection. /// [AutoNetworkedField, ViewVariables] public List Holders = []; - /// - /// Current primary holder used as the soft hold drag anchor. - /// - [AutoNetworkedField, ViewVariables] - public EntityUid? PrimaryHolder; - /// /// Required contributor count for entering full hold. /// diff --git a/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.Drag.cs b/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.Drag.cs index 82309121bff..0c59afcdc5b 100644 --- a/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.Drag.cs +++ b/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.Drag.cs @@ -30,28 +30,27 @@ private void InitializeDragEvents() SubscribeLocalEvent(OnHolderPreventCollide); } - private void UpdateSoftDrag(Entity held, ScpHoldableComponent holdable, float maintenanceRange, float desiredDistance) + private void UpdateSoftDrag( + Entity held, + ScpHoldableComponent holdable, + EntityUid dragAnchor, + ActiveScpHolderComponent anchorHolder, + float maintenanceRange, + float desiredDistance) { - if (held.Comp.PrimaryHolder == null) + if (anchorHolder.Target != held.Owner) return; - var primaryHolder = held.Comp.PrimaryHolder.Value; - if (!_activeHolderQuery.TryComp(primaryHolder, out var holder)) + if (!_container.IsInSameOrNoContainer(dragAnchor, held.Owner)) return; - if (holder.Target != held.Owner) - return; - - if (!_container.IsInSameOrNoContainer(primaryHolder, held.Owner)) - return; - - if (!_interaction.InRangeUnobstructed(primaryHolder, held.Owner, maintenanceRange)) + if (!_interaction.InRangeUnobstructed(dragAnchor, held.Owner, maintenanceRange)) return; if (!_physicsQuery.TryComp(held.Owner, out var heldPhysics)) return; - var holderCoords = _transform.GetMapCoordinates(primaryHolder); + var holderCoords = _transform.GetMapCoordinates(dragAnchor); var heldCoords = _transform.GetMapCoordinates(held.Owner); if (holderCoords.MapId != heldCoords.MapId) @@ -59,11 +58,11 @@ private void UpdateSoftDrag(Entity held, ScpHoldable var offset = heldCoords.Position - holderCoords.Position; var distance = offset.Length(); - var holderVelocity = _physicsQuery.TryComp(primaryHolder, out var holderPhysics) + var holderVelocity = _physicsQuery.TryComp(dragAnchor, out var holderPhysics) ? holderPhysics.LinearVelocity : Vector2.Zero; var velocityDirectionThresholdSquared = holdable.SoftDragVelocityDirectionThreshold * holdable.SoftDragVelocityDirectionThreshold; - var direction = GetSoftDragDirection(primaryHolder, holdable, holderVelocity, offset, distance, velocityDirectionThresholdSquared); + var direction = GetSoftDragDirection(dragAnchor, holdable, holderVelocity, offset, distance, velocityDirectionThresholdSquared); var desiredPosition = holderCoords.Position + direction * desiredDistance; var correction = desiredPosition - heldCoords.Position; var correctionDistance = correction.Length(); diff --git a/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.State.cs b/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.State.cs index 0f64971626b..9f5c40e001b 100644 --- a/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.State.cs +++ b/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.State.cs @@ -39,7 +39,7 @@ protected void UpdateHeld(Entity held) if (!TryGetHeldHoldable(held, out var holdable)) return; - if (!EnsurePrimaryHolder(held)) + if (!TryGetDragAnchorHolder(held, out var dragAnchorUid, out var dragAnchor)) { ClearHoldState(held, applyImmunity: false); return; @@ -49,7 +49,7 @@ protected void UpdateHeld(Entity held) var maintenanceRange = GetHoldMaintenanceRange(holdable, desiredSoftDragDistance); if (!_activeHoldableFullHoldStateQuery.HasComp(held.Owner)) - UpdateSoftDrag(held, holdable, maintenanceRange, desiredSoftDragDistance); + UpdateSoftDrag(held, holdable, dragAnchorUid, dragAnchor, maintenanceRange, desiredSoftDragDistance); else ZeroHeldVelocity(held.Owner); @@ -125,9 +125,6 @@ protected void ReleaseHolderContribution(EntityUid holderUid, EntityUid targetUi else if (_activeHolderSlowdownStateQuery.HasComp(holderUid)) RemComp(holderUid); - if (held.PrimaryHolder == holderUid) - SetHeldPrimaryHolder((targetUid, held), null); - if (held.Holders.Count == 0) { if (clearIfEmpty) @@ -156,7 +153,7 @@ protected void SyncHeldState(Entity held) return; } - if (!EnsurePrimaryHolder(held)) + if (!TryGetDragAnchorHolder(held, out var dragAnchorUid, out var dragAnchor)) { ClearHoldState(held, applyImmunity: false); return; @@ -171,7 +168,7 @@ protected void SyncHeldState(Entity held) ExitFullHold(held); var desiredSoftDragDistance = GetDesiredSoftDragDistance(holdable); var maintenanceRange = GetHoldMaintenanceRange(holdable, desiredSoftDragDistance); - UpdateSoftDrag(held, holdable, maintenanceRange, desiredSoftDragDistance); + UpdateSoftDrag(held, holdable, dragAnchorUid, dragAnchor, maintenanceRange, desiredSoftDragDistance); UpdateHolderSlowdowns(held, holdable); SyncPlaceholderHands(held); } @@ -205,13 +202,11 @@ private void ExitFullHold(Entity held) RemComp(held.Owner); } - private bool EnsurePrimaryHolder(Entity held) + private bool TryGetDragAnchorHolder( + Entity held, + out EntityUid dragAnchorUid, + out ActiveScpHolderComponent dragAnchor) { - if (held.Comp.PrimaryHolder != null && IsValidPrimaryHolder(held, held.Comp.PrimaryHolder.Value)) - return true; - - SetHeldPrimaryHolder(held, null); - foreach (var holderUid in held.Comp.Holders) { if (!_activeHolderQuery.TryComp(holderUid, out var holder)) @@ -220,10 +215,13 @@ private bool EnsurePrimaryHolder(Entity held) if (holder.Target != held.Owner) continue; - SetHeldPrimaryHolder(held, holderUid); + dragAnchorUid = holderUid; + dragAnchor = holder; return true; } + dragAnchorUid = default; + dragAnchor = default!; return false; } @@ -251,7 +249,6 @@ private void ClearHoldState(Entity held, bool applyI } held.Comp.Holders.Clear(); - held.Comp.PrimaryHolder = null; if (applyImmunity) { @@ -343,17 +340,6 @@ private bool ShouldReleaseHolder(EntityUid holderUid, EntityUid heldUid, float m return !_interaction.InRangeUnobstructed(holderUid, heldUid, maintenanceRange); } - private bool IsValidPrimaryHolder(Entity held, EntityUid primaryHolderUid) - { - if (!_activeHolderQuery.TryComp(primaryHolderUid, out var holder)) - return false; - - if (holder.Target != held.Owner) - return false; - - return held.Comp.Holders.Contains(primaryHolderUid); - } - private void SetHolderTarget(Entity holder, EntityUid? target) { if (holder.Comp.Target == target) @@ -362,13 +348,4 @@ private void SetHolderTarget(Entity holder, EntityUid? holder.Comp.Target = target; Dirty(holder); } - - private void SetHeldPrimaryHolder(Entity held, EntityUid? primaryHolder) - { - if (held.Comp.PrimaryHolder == primaryHolder) - return; - - held.Comp.PrimaryHolder = primaryHolder; - Dirty(held); - } } diff --git a/Content.Shared/_Scp/Scp096/Main/Components/Scp096Component.cs b/Content.Shared/_Scp/Scp096/Main/Components/Scp096Component.cs index c188d0df355..977d8e686e2 100644 --- a/Content.Shared/_Scp/Scp096/Main/Components/Scp096Component.cs +++ b/Content.Shared/_Scp/Scp096/Main/Components/Scp096Component.cs @@ -162,7 +162,7 @@ public sealed partial class Scp096Component : Component /// Сила отталкивания удерживающих после вырывания. /// [DataField] - public float HoldBreakoutImpulse = 10f; + public float HoldBreakoutImpulse = 40f; #endregion diff --git a/Content.Shared/_Scp/Scp096/Main/Systems/SharedScp096System.Holding.cs b/Content.Shared/_Scp/Scp096/Main/Systems/SharedScp096System.Holding.cs index a1a2be919d4..b8e236636ff 100644 --- a/Content.Shared/_Scp/Scp096/Main/Systems/SharedScp096System.Holding.cs +++ b/Content.Shared/_Scp/Scp096/Main/Systems/SharedScp096System.Holding.cs @@ -3,6 +3,7 @@ using Content.Shared._Scp.Holding.Components; using Content.Shared._Scp.Holding.Systems; using Content.Shared._Scp.Scp096.Main.Components; +using Robust.Shared.Physics.Components; using Robust.Shared.Physics.Systems; namespace Content.Shared._Scp.Scp096.Main.Systems; @@ -37,9 +38,21 @@ private void OnHoldBreakout(Entity ent, ref ScpHoldBreakoutEven return; var scpPosition = _transform.GetWorldPosition(ent.Owner); - foreach (var holderUid in held.Holders) + var holderCount = held.Holders.Count; + if (holderCount == 0) + return; + + var holders = new (EntityUid HolderUid, Vector2 Position)[holderCount]; + for (var i = 0; i < holderCount; i++) + { + var holderUid = held.Holders[i]; + holders[i] = (holderUid, _transform.GetWorldPosition(holderUid)); + } + + for (var i = 0; i < holderCount; i++) { - ApplyHoldBreakoutEffects(ent, holderUid, scpPosition); + var holder = holders[i]; + ApplyHoldBreakoutEffects(ent, holder.HolderUid, holder.Position, scpPosition, i, holderCount); } } @@ -55,16 +68,34 @@ protected void TryBreakOutOfHold(EntityUid uid) _holding.TryForceBreakOut((uid, (ActiveScpHoldableComponent?) null)); } - private void ApplyHoldBreakoutEffects(Entity ent, EntityUid holderUid, Vector2 scpPosition) + private void ApplyHoldBreakoutEffects( + Entity ent, + EntityUid holderUid, + Vector2 holderPosition, + Vector2 scpPosition, + int holderIndex, + int holderCount) { _damageable.TryChangeDamage(holderUid, ent.Comp.HoldBreakoutDamage, origin: ent.Owner); _stun.TryUpdateParalyzeDuration(holderUid, ent.Comp.HoldBreakoutParalyzeTime); - var direction = _transform.GetWorldPosition(holderUid) - scpPosition; - direction = direction.LengthSquared() < 0.001f - ? Vector2.UnitY - : Vector2.Normalize(direction); + if (!TryComp(holderUid, out var physics)) + return; + + var direction = GetHoldBreakoutDirection(holderPosition, scpPosition, holderIndex, holderCount); + _physics.ApplyLinearImpulse(holderUid, direction * physics.Mass * ent.Comp.HoldBreakoutImpulse, body: physics); + } + + private static Vector2 GetHoldBreakoutDirection(Vector2 holderPosition, Vector2 scpPosition, int holderIndex, int holderCount) + { + var direction = holderPosition - scpPosition; + if (direction.LengthSquared() >= 0.001f) + return Vector2.Normalize(direction); + + if (holderCount <= 0) + return Vector2.UnitX; - _physics.ApplyLinearImpulse(holderUid, direction * ent.Comp.HoldBreakoutImpulse); + var angle = 2f * MathF.PI * holderIndex / holderCount; + return new Vector2(MathF.Cos(angle), MathF.Sin(angle)); } } From 2781dc346f92e9dc1a462cf0335f4a709452108a Mon Sep 17 00:00:00 2001 From: ThereDrD <88589686+ThereDrD0@users.noreply.github.com> Date: Sat, 18 Apr 2026 03:52:15 +0300 Subject: [PATCH 16/27] Update Content.Server/_Scp/Holding/ScpHoldingSystem.cs Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- Content.Server/_Scp/Holding/ScpHoldingSystem.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Content.Server/_Scp/Holding/ScpHoldingSystem.cs b/Content.Server/_Scp/Holding/ScpHoldingSystem.cs index 5ed39a91d90..2256bbbce80 100644 --- a/Content.Server/_Scp/Holding/ScpHoldingSystem.cs +++ b/Content.Server/_Scp/Holding/ScpHoldingSystem.cs @@ -16,7 +16,7 @@ public override void Initialize() protected override void OnHeldStateShutdown(Entity held) { - foreach (var holderUid in held.Comp.Holders) + foreach (var holderUid in held.Comp.Holders.ToArray()) { RemComp(holderUid); } From c0eefbdddadfe68e8c011a7452bec741bf20f7be Mon Sep 17 00:00:00 2001 From: drdth Date: Sat, 18 Apr 2026 04:10:23 +0300 Subject: [PATCH 17/27] fix: ai review --- .../_Scp/ScpHeadsetEncryptionKeysTest.cs | 33 - .../Tests/_Scp/ScpHoldingTest.cs | 3177 ----------------- .../_Scp/ScpHoldingTwoClientCombatTest.cs | 516 --- .../Components/ScpHoldRestrictedComponent.cs | 4 +- .../Systems/SharedScpHoldingSystem.State.cs | 2 + 5 files changed, 4 insertions(+), 3728 deletions(-) delete mode 100644 Content.IntegrationTests/Tests/_Scp/ScpHeadsetEncryptionKeysTest.cs delete mode 100644 Content.IntegrationTests/Tests/_Scp/ScpHoldingTest.cs delete mode 100644 Content.IntegrationTests/Tests/_Scp/ScpHoldingTwoClientCombatTest.cs diff --git a/Content.IntegrationTests/Tests/_Scp/ScpHeadsetEncryptionKeysTest.cs b/Content.IntegrationTests/Tests/_Scp/ScpHeadsetEncryptionKeysTest.cs deleted file mode 100644 index 54b7878f772..00000000000 --- a/Content.IntegrationTests/Tests/_Scp/ScpHeadsetEncryptionKeysTest.cs +++ /dev/null @@ -1,33 +0,0 @@ -using System.Linq; -using Content.IntegrationTests.Tests.Interaction; -using Content.Shared.Radio.Components; - -namespace Content.IntegrationTests.Tests._Scp; - -public sealed class ScpHeadsetEncryptionKeysTest : InteractionTest -{ - [Test] - public async Task ScpHeadsetsRejectCommonKeys() - { - await SpawnTarget("ClothingHeadsetScientificService"); - var comp = Comp(); - - Assert.Multiple(() => - { - Assert.That(comp.KeyContainer.ContainedEntities, Has.Count.EqualTo(1)); - Assert.That(comp.DefaultChannel, Is.EqualTo("ScientificService")); - Assert.That(comp.Channels, Has.Count.EqualTo(1)); - Assert.That(comp.Channels.First(), Is.EqualTo("ScientificService")); - }); - - await InteractUsing("EncryptionKeyCommon"); - - Assert.Multiple(() => - { - Assert.That(comp.KeyContainer.ContainedEntities, Has.Count.EqualTo(1)); - Assert.That(comp.DefaultChannel, Is.EqualTo("ScientificService")); - Assert.That(comp.Channels, Has.Count.EqualTo(1)); - Assert.That(comp.Channels.First(), Is.EqualTo("ScientificService")); - }); - } -} diff --git a/Content.IntegrationTests/Tests/_Scp/ScpHoldingTest.cs b/Content.IntegrationTests/Tests/_Scp/ScpHoldingTest.cs deleted file mode 100644 index c692e369e8b..00000000000 --- a/Content.IntegrationTests/Tests/_Scp/ScpHoldingTest.cs +++ /dev/null @@ -1,3177 +0,0 @@ -#nullable enable -using System; -using System.Linq; -using System.Numerics; -using System.Reflection; -using Content.IntegrationTests.Tests.Helpers; -using Content.Shared.Alert; -using Content.Server.Body.Systems; -using Content.Shared._Scp.Holding; -using Content.Shared._Scp.Holding.Components; -using Content.Shared._Scp.Holding.Systems; -using Content.Shared.Body.Components; -using Content.Shared.Body.Part; -using Content.Shared.Damage.Components; -using Content.Shared.FixedPoint; -using Content.Shared.Hands.Components; -using Content.Shared.Hands.EntitySystems; -using Content.Shared.Interaction.Components; -using Content.Shared.Input; -using Content.Shared.Inventory.VirtualItem; -using Content.Shared.Movement.Components; -using Content.Shared.Movement.Events; -using Content.Shared.Movement.Pulling.Components; -using Content.Shared.Movement.Pulling.Systems; -using Content.Shared.Movement.Systems; -using Content.Shared.StatusEffectNew; -using Content.Shared.StatusEffectNew.Components; -using Content.Shared.Throwing; -using Robust.Client.Input; -using Robust.Server.Console; -using Robust.Client.Physics; -using Robust.Shared.GameObjects; -using Robust.Shared.Input; -using Robust.Shared.Map; -using Robust.Shared.Maths; -using Robust.Shared.Physics.Components; -using Robust.Shared.Physics.Systems; -using Robust.Shared.Prototypes; -using Robust.Shared.Timing; -using Robust.UnitTesting; -using Content.Shared.Stunnable; -using Content.Shared.Whitelist; - -namespace Content.IntegrationTests.Tests._Scp; - -[TestFixture] -public sealed class ScpHoldingTest -{ - private const string HolderPrototype = "ScpHoldingTestHolder"; - private const string HoldableWhitelistedHolderPrototype = "ScpHoldingTestHolderHoldableWhitelisted"; - private const string HoldableBlacklistedHolderPrototype = "ScpHoldingTestHolderHoldableBlacklisted"; - private const string TestListenerComponentName = "TestListener"; - private static readonly ProtoId GrabbedAlertId = "ScpHoldGrabbed"; - private static readonly FieldInfo ActiveScpHolderTargetField = - typeof(ActiveScpHolderComponent).GetField(nameof(ActiveScpHolderComponent.Target))!; - private static readonly FieldInfo SoftEscapeAvailableAtField = - typeof(ActiveScpHoldableComponent).GetField(nameof(ActiveScpHoldableComponent.SoftEscapeAvailableAt))!; - - private static EntityWhitelist CreateComponentWhitelist(params string[] components) - { - return new EntityWhitelist - { - Components = components, - }; - } - - [TestPrototypes] - private const string Prototypes = """ -- type: entity - id: ScpHoldingTestHolder - parent: MobHuman - components: - - type: ScpHolder -- type: entity - id: ScpHoldingTestHolderHoldableWhitelisted - parent: ScpHoldingTestHolder - components: - - type: ScpHolder - holdableWhitelist: - components: - - TestListener -- type: entity - id: ScpHoldingTestHolderHoldableBlacklisted - parent: ScpHoldingTestHolder - components: - - type: ScpHolder - holdableBlacklist: - components: - - TestListener -"""; - - [Test] - public void ActiveScpHoldableStateDoesNotStorePrimaryHolder() - { - Assert.That(typeof(ActiveScpHoldableComponent).GetField("PrimaryHolder"), Is.Null); - } - - [Test] - public async Task HoldAppliesStatusEffectImmediately() - { - await using var pair = await PoolManager.GetServerClient(new PoolSettings { Fresh = true }); - var server = pair.Server; - var entMan = server.EntMan; - var statusEffects = server.System(); - var holding = server.System(); - var map = await pair.CreateTestMap(); - - await server.WaitPost(() => - { - var holder = entMan.SpawnEntity(HolderPrototype, map.GridCoords); - var target = entMan.SpawnEntity("MobHuman", map.GridCoords); - StartHold(entMan, holding, holder, target); - - Assert.That(statusEffects.TryGetStatusEffect(target, "StatusEffectScpHeld", out var effect), Is.True); - Assert.That(effect, Is.Not.Null); - Assert.That(entMan.GetComponent(effect!.Value).Applied, Is.True); - }); - - await pair.CleanReturnAsync(); - } - - [Test] - public async Task SyncHolderState_DoesNotAdoptForeignVirtualItemWithSameBlockingEntity() - { - await using var pair = await PoolManager.GetServerClient(new PoolSettings { Fresh = true }); - var server = pair.Server; - var entMan = server.EntMan; - var handsSystem = server.System(); - var holding = server.System(); - var virtualItem = server.System(); - var map = await pair.CreateTestMap(); - - EntityUid holder = default; - EntityUid target = default; - - await server.WaitPost(() => - { - holder = entMan.SpawnEntity(HolderPrototype, map.GridCoords); - target = entMan.SpawnEntity("MobHuman", map.GridCoords.Offset(new Vector2(0.1f, 0f))); - - var holderState = entMan.EnsureComponent(holder); - SetHolderTarget(holderState, target); - - Assert.That(virtualItem.TrySpawnVirtualItemInHand(target, holder, out _), Is.True); - - holding.SyncHolderState((holder, holderState)); - - var hands = entMan.GetComponent(holder); - Assert.Multiple(() => - { - Assert.That(CountHolderHandBlockers(entMan, handsSystem, holder, target, hands), Is.EqualTo(1)); - Assert.That(CountHolderTargetVirtualItems(entMan, handsSystem, holder, target, hands), Is.EqualTo(2)); - }); - }); - - await pair.CleanReturnAsync(); - } - - [Test] - public async Task SyncHolderState_DeletesDuplicateTaggedHolderBlockers() - { - await using var pair = await PoolManager.GetServerClient(new PoolSettings { Fresh = true }); - var server = pair.Server; - var entMan = server.EntMan; - var handsSystem = server.System(); - var holding = server.System(); - var virtualItem = server.System(); - var map = await pair.CreateTestMap(); - - EntityUid holder = default; - EntityUid target = default; - - await server.WaitPost(() => - { - holder = entMan.SpawnEntity(HolderPrototype, map.GridCoords); - target = entMan.SpawnEntity("MobHuman", map.GridCoords.Offset(new Vector2(0.1f, 0f))); - - var holderState = entMan.EnsureComponent(holder); - SetHolderTarget(holderState, target); - - Assert.That(virtualItem.TrySpawnVirtualItemInHand(target, holder, out var blocker1), Is.True); - Assert.That(virtualItem.TrySpawnVirtualItemInHand(target, holder, out var blocker2), Is.True); - - entMan.EnsureComponent(blocker1!.Value); - entMan.EnsureComponent(blocker2!.Value); - - holding.SyncHolderState((holder, holderState)); - }); - - await server.WaitRunTicks(2); - - await server.WaitAssertion(() => - { - var hands = entMan.GetComponent(holder); - - Assert.Multiple(() => - { - Assert.That(CountHolderHandBlockers(entMan, handsSystem, holder, target, hands), Is.EqualTo(1)); - Assert.That(CountHolderTargetVirtualItems(entMan, handsSystem, holder, target, hands), Is.EqualTo(1)); - }); - }); - - await pair.CleanReturnAsync(); - } - - [Test] - public async Task SoftHoldBreakoutByMovementAndAlertRespectsCooldown() - { - await using var pair = await PoolManager.GetServerClient(new PoolSettings { Fresh = true }); - var server = pair.Server; - var entMan = server.EntMan; - var alerts = server.System(); - var timing = server.ResolveDependency(); - var statusEffects = server.System(); - var proto = server.ResolveDependency(); - var holding = server.System(); - var map = await pair.CreateTestMap(); - - EntityUid holder = default; - EntityUid target = default; - - await server.WaitPost(() => - { - holder = entMan.SpawnEntity(HolderPrototype, map.GridCoords); - target = entMan.SpawnEntity("MobHuman", map.GridCoords); - StartHold(entMan, holding, holder, target); - }); - - await server.WaitAssertion(() => - { - var held = entMan.GetComponent(target); - Assert.Multiple(() => - { - Assert.That(HasFullHold(entMan, target), Is.False); - Assert.That(held.Holders, Has.Count.EqualTo(1)); - Assert.That(statusEffects.HasStatusEffect(target, "StatusEffectScpHeld"), Is.True); - Assert.That(alerts.IsShowingAlert(target, "ScpHoldGrabbed"), Is.True); - }); - }); - - await server.WaitPost(() => RaiseMoveInput(entMan, target)); - await server.WaitRunTicks(2); - - await server.WaitAssertion(() => - { - Assert.That(entMan.HasComponent(target), Is.False); - }); - - await server.WaitPost(() => - { - StartHold(entMan, holding, holder, target); - var held = entMan.GetComponent(target); - SetSoftEscapeAvailableAt(held, timing.CurTime + TimeSpan.FromSeconds(1)); - }); - - await server.WaitPost(() => RaiseMoveInput(entMan, target)); - await server.WaitRunTicks(2); - - await server.WaitAssertion(() => - { - Assert.That(entMan.HasComponent(target), Is.True); - }); - - await server.WaitPost(() => - { - var alert = proto.Index(GrabbedAlertId); - Assert.That(alerts.ActivateAlert(target, alert), Is.True); - }); - await server.WaitRunTicks(2); - - await server.WaitAssertion(() => - { - Assert.That(entMan.HasComponent(target), Is.True); - }); - - await server.WaitPost(() => - { - var held = entMan.GetComponent(target); - SetSoftEscapeAvailableAt(held, timing.CurTime); - var alert = proto.Index(GrabbedAlertId); - Assert.That(alerts.ActivateAlert(target, alert), Is.True); - }); - await server.WaitRunTicks(2); - - await server.WaitAssertion(() => - { - Assert.That(entMan.HasComponent(target), Is.False); - }); - - await pair.CleanReturnAsync(); - } - - [Test] - public async Task SoftHoldUsesCustomDragAndLeavesVanillaPullIdle() - { - await using var pair = await PoolManager.GetServerClient(new PoolSettings - { - Fresh = true, - Connected = true, - DummyTicker = false, - }); - - var server = pair.Server; - var client = pair.Client; - var sEntMan = server.EntMan; - var cEntMan = client.EntMan; - var sPhysics = server.System(); - var cPhysics = client.System(); - var sTransform = server.System(); - var cTransform = client.System(); - var sAlerts = server.System(); - var cAlerts = client.System(); - var sStatusEffects = server.System(); - var cStatusEffects = client.System(); - var sHandsSystem = server.System(); - var cHandsSystem = client.System(); - var holding = server.System(); - var map = await pair.CreateTestMap(); - - EntityUid holder = default; - EntityUid target = default; - - await server.WaitPost(() => - { - holder = sEntMan.SpawnEntity(HolderPrototype, map.GridCoords); - target = sEntMan.SpawnEntity("MobHuman", map.GridCoords.Offset(new Vector2(0.1f, 0f))); - StartHold(sEntMan, holding, holder, target); - }); - - await pair.RunTicksSync(10); - await pair.SyncTicks(targetDelta: 1); - - await server.WaitAssertion(() => - { - var held = sEntMan.GetComponent(target); - var holderState = sEntMan.GetComponent(holder); - var holderSpeed = sEntMan.GetComponent(holder); - var holderHands = sEntMan.GetComponent(holder); - var puller = sEntMan.GetComponent(holder); - var pullable = sEntMan.GetComponent(target); - var move = new UpdateCanMoveEvent(target); - var collide = new AttemptMobCollideEvent(); - var targetCollide = new AttemptMobTargetCollideEvent(); - var distance = GetDistance(sTransform, holder, target); - var contacts = sPhysics.GetContactingEntities(holder); - - sEntMan.EventBus.RaiseLocalEvent(target, move); - sEntMan.EventBus.RaiseLocalEvent(target, ref collide); - sEntMan.EventBus.RaiseLocalEvent(target, ref targetCollide); - - Assert.Multiple(() => - { - Assert.That(HasFullHold(sEntMan, target), Is.False); - Assert.That(held.Holders, Is.EqualTo(new[] { holder })); - Assert.That(move.Cancelled, Is.False); - Assert.That(collide.Cancelled, Is.True); - Assert.That(targetCollide.Cancelled, Is.True); - Assert.That(distance, Is.GreaterThan(0.18f)); - Assert.That(distance, Is.LessThan(0.7f)); - Assert.That(contacts, Does.Not.Contain(target)); - Assert.That(HasHolderSlowdown(sEntMan, holder), Is.True); - Assert.That(holderSpeed.WalkSpeedModifier, Is.LessThan(0.6f)); - Assert.That(holderSpeed.SprintSpeedModifier, Is.LessThan(0.6f)); - Assert.That(puller.Pulling, Is.Null); - Assert.That(pullable.Puller, Is.Null); - Assert.That(CountHolderHandBlockers(sEntMan, sHandsSystem, holder, target, holderHands), Is.EqualTo(1)); - Assert.That(CountHolderTargetVirtualItems(sEntMan, sHandsSystem, holder, target, holderHands), Is.EqualTo(1)); - Assert.That(sStatusEffects.HasStatusEffect(target, "StatusEffectScpHeld"), Is.True); - Assert.That(sAlerts.IsShowingAlert(target, "ScpHoldGrabbed"), Is.True); - }); - }); - - EntityUid clientHolder = default; - EntityUid clientTarget = default; - await client.WaitAssertion(() => - { - clientHolder = ToClientEntity(sEntMan, cEntMan, holder); - clientTarget = ToClientEntity(sEntMan, cEntMan, target); - - var held = cEntMan.GetComponent(clientTarget); - var holderState = cEntMan.GetComponent(clientHolder); - var holderSpeed = cEntMan.GetComponent(clientHolder); - var holderHands = cEntMan.GetComponent(clientHolder); - var puller = cEntMan.GetComponent(clientHolder); - var pullable = cEntMan.GetComponent(clientTarget); - var collide = new AttemptMobCollideEvent(); - var targetCollide = new AttemptMobTargetCollideEvent(); - var distance = GetDistance(cTransform, clientHolder, clientTarget); - var contacts = cPhysics.GetContactingEntities(clientHolder); - - cEntMan.EventBus.RaiseLocalEvent(clientTarget, ref collide); - cEntMan.EventBus.RaiseLocalEvent(clientTarget, ref targetCollide); - - Assert.Multiple(() => - { - Assert.That(HasFullHold(cEntMan, clientTarget), Is.False); - Assert.That(held.Holders, Is.EqualTo(new[] { clientHolder })); - Assert.That(distance, Is.GreaterThan(0.18f)); - Assert.That(distance, Is.LessThan(0.55f)); - Assert.That(collide.Cancelled, Is.True); - Assert.That(targetCollide.Cancelled, Is.True); - Assert.That(contacts, Does.Not.Contain(clientTarget)); - Assert.That(HasHolderSlowdown(cEntMan, clientHolder), Is.True); - Assert.That(holderSpeed.WalkSpeedModifier, Is.LessThan(0.6f)); - Assert.That(holderSpeed.SprintSpeedModifier, Is.LessThan(0.6f)); - Assert.That(puller.Pulling, Is.Null); - Assert.That(pullable.Puller, Is.Null); - Assert.That(CountHolderHandBlockers(cEntMan, cHandsSystem, clientHolder, clientTarget, holderHands), Is.EqualTo(1)); - Assert.That(CountHolderTargetVirtualItems(cEntMan, cHandsSystem, clientHolder, clientTarget, holderHands), Is.EqualTo(1)); - Assert.That(cStatusEffects.HasStatusEffect(clientTarget, "StatusEffectScpHeld"), Is.True); - }); - }); - - var serverDistanceSamples = new float[24]; - var clientDistanceSamples = new float[24]; - await server.WaitPost(() => - { - var holderPhysics = sEntMan.GetComponent(holder); - sPhysics.SetLinearVelocity(holder, new Vector2(4f, 0f), body: holderPhysics); - }); - - for (var i = 0; i < 16; i++) - { - var sampleIndex = i; - await pair.RunTicksSync(1); - await pair.SyncTicks(targetDelta: 1); - await server.WaitPost(() => serverDistanceSamples[sampleIndex] = GetDistance(sTransform, holder, target)); - await client.WaitPost(() => clientDistanceSamples[sampleIndex] = GetDistance(cTransform, clientHolder, clientTarget)); - } - - await server.WaitPost(() => - { - var holderPhysics = sEntMan.GetComponent(holder); - sPhysics.SetLinearVelocity(holder, Vector2.Zero, body: holderPhysics); - }); - - for (var i = 16; i < serverDistanceSamples.Length; i++) - { - var sampleIndex = i; - await pair.RunTicksSync(1); - await pair.SyncTicks(targetDelta: 1); - await server.WaitPost(() => serverDistanceSamples[sampleIndex] = GetDistance(sTransform, holder, target)); - await client.WaitPost(() => clientDistanceSamples[sampleIndex] = GetDistance(cTransform, clientHolder, clientTarget)); - } - - Assert.Multiple(() => - { - Assert.That(serverDistanceSamples.Max(), Is.LessThan(0.6f)); - Assert.That(serverDistanceSamples.Min(), Is.GreaterThan(0.16f)); - Assert.That(GetLargestDistanceStep(serverDistanceSamples), Is.LessThan(0.2f)); - Assert.That(serverDistanceSamples[^1], Is.GreaterThan(0.18f)); - Assert.That(serverDistanceSamples[^1], Is.LessThan(0.4f)); - - Assert.That(clientDistanceSamples.Max(), Is.LessThan(0.6f)); - Assert.That(clientDistanceSamples.Min(), Is.GreaterThan(0.16f)); - Assert.That(GetLargestDistanceStep(clientDistanceSamples), Is.LessThan(0.2f)); - Assert.That(clientDistanceSamples[^1], Is.GreaterThan(0.18f)); - Assert.That(clientDistanceSamples[^1], Is.LessThan(0.4f)); - }); - - await server.WaitAssertion(() => - { - Assert.That(sEntMan.HasComponent(target), Is.True); - }); - - await client.WaitAssertion(() => - { - Assert.That(cEntMan.HasComponent(clientTarget), Is.True); - }); - - await server.WaitPost(() => - { - var targetCoords = sEntMan.GetComponent(target).Coordinates; - sTransform.SetCoordinates(holder, targetCoords); - }); - - await pair.RunTicksSync(10); - await pair.SyncTicks(targetDelta: 1); - - await server.WaitAssertion(() => - { - var distance = GetDistance(sTransform, holder, target); - var contacts = sPhysics.GetContactingEntities(holder); - - Assert.Multiple(() => - { - Assert.That(distance, Is.GreaterThan(0.16f)); - Assert.That(distance, Is.LessThan(0.55f)); - Assert.That(contacts, Does.Not.Contain(target)); - }); - }); - - await client.WaitAssertion(() => - { - var distance = GetDistance(cTransform, clientHolder, clientTarget); - var contacts = cPhysics.GetContactingEntities(clientHolder); - - Assert.Multiple(() => - { - Assert.That(distance, Is.GreaterThan(0.16f)); - Assert.That(distance, Is.LessThan(0.55f)); - Assert.That(contacts, Does.Not.Contain(clientTarget)); - }); - }); - - await pair.CleanReturnAsync(); - } - - [Test] - public async Task PlayerHolderSlowdownAppliesOnGrabAndClearsOnRelease() - { - await using var pair = await PoolManager.GetServerClient(new PoolSettings - { - Fresh = true, - Connected = true, - DummyTicker = false, - }); - - var server = pair.Server; - var client = pair.Client; - var sEntMan = server.EntMan; - var cEntMan = client.EntMan; - var sTransform = server.System(); - var holding = server.System(); - var map = await pair.CreateTestMap(); - - var serverPlayer = pair.Player!.AttachedEntity!.Value; - EntityUid target = default; - var serverBaseWalk = 1f; - var serverBaseSprint = 1f; - var clientBaseWalk = 1f; - var clientBaseSprint = 1f; - - await server.WaitPost(() => - { - sTransform.SetCoordinates(serverPlayer, map.GridCoords); - sEntMan.EnsureComponent(serverPlayer); - target = sEntMan.SpawnEntity("MobHuman", map.GridCoords.Offset(new Vector2(0.1f, 0f))); - }); - - await pair.RunTicksSync(10); - await pair.SyncTicks(targetDelta: 1); - - var clientPlayer = EntityUid.Invalid; - await client.WaitAssertion(() => - { - clientPlayer = client.AttachedEntity!.Value; - _ = ToClientEntity(sEntMan, cEntMan, target); - }); - - await server.WaitAssertion(() => - { - var speed = sEntMan.GetComponent(serverPlayer); - serverBaseWalk = speed.WalkSpeedModifier; - serverBaseSprint = speed.SprintSpeedModifier; - - Assert.Multiple(() => - { - Assert.That(HasHolderSlowdown(sEntMan, serverPlayer), Is.False); - Assert.That(serverBaseWalk, Is.GreaterThan(0f)); - Assert.That(serverBaseSprint, Is.GreaterThan(0f)); - }); - }); - - await client.WaitAssertion(() => - { - var speed = cEntMan.GetComponent(clientPlayer); - clientBaseWalk = speed.WalkSpeedModifier; - clientBaseSprint = speed.SprintSpeedModifier; - - Assert.Multiple(() => - { - Assert.That(HasHolderSlowdown(cEntMan, clientPlayer), Is.False); - Assert.That(clientBaseWalk, Is.GreaterThan(0f)); - Assert.That(clientBaseSprint, Is.GreaterThan(0f)); - }); - }); - - await server.WaitPost(() => StartHold(sEntMan, holding, serverPlayer, target)); - - await pair.RunTicksSync(10); - await pair.SyncTicks(targetDelta: 1); - - await server.WaitAssertion(() => - { - var speed = sEntMan.GetComponent(serverPlayer); - Assert.Multiple(() => - { - Assert.That(sEntMan.HasComponent(serverPlayer), Is.True); - Assert.That(HasHolderSlowdown(sEntMan, serverPlayer), Is.True); - Assert.That(speed.WalkSpeedModifier, Is.LessThan(serverBaseWalk * 0.75f)); - Assert.That(speed.SprintSpeedModifier, Is.LessThan(serverBaseSprint * 0.75f)); - }); - }); - - await client.WaitAssertion(() => - { - var speed = cEntMan.GetComponent(clientPlayer); - Assert.Multiple(() => - { - Assert.That(cEntMan.HasComponent(clientPlayer), Is.True); - Assert.That(HasHolderSlowdown(cEntMan, clientPlayer), Is.True); - Assert.That(speed.WalkSpeedModifier, Is.LessThan(clientBaseWalk * 0.75f)); - Assert.That(speed.SprintSpeedModifier, Is.LessThan(clientBaseSprint * 0.75f)); - }); - }); - - await server.WaitPost(() => - { - var holdComp = sEntMan.GetComponent(serverPlayer); - Assert.That(holding.TryToggleHold((serverPlayer, holdComp), target), Is.True); - }); - - await pair.RunTicksSync(10); - await pair.SyncTicks(targetDelta: 1); - - await server.WaitAssertion(() => - { - var speed = sEntMan.GetComponent(serverPlayer); - Assert.Multiple(() => - { - Assert.That(sEntMan.HasComponent(serverPlayer), Is.False); - Assert.That(HasHolderSlowdown(sEntMan, serverPlayer), Is.False); - Assert.That(speed.WalkSpeedModifier, Is.EqualTo(serverBaseWalk).Within(0.001f)); - Assert.That(speed.SprintSpeedModifier, Is.EqualTo(serverBaseSprint).Within(0.001f)); - }); - }); - - await client.WaitAssertion(() => - { - var speed = cEntMan.GetComponent(clientPlayer); - Assert.Multiple(() => - { - Assert.That(cEntMan.HasComponent(clientPlayer), Is.False); - Assert.That(HasHolderSlowdown(cEntMan, clientPlayer), Is.False); - Assert.That(speed.WalkSpeedModifier, Is.EqualTo(clientBaseWalk).Within(0.001f)); - Assert.That(speed.SprintSpeedModifier, Is.EqualTo(clientBaseSprint).Within(0.001f)); - }); - }); - - await pair.CleanReturnAsync(); - } - - [Test] - public async Task PullAttemptOnHoldableTargetRedirectsToHoldAndReplacesVanillaPull() - { - await using var pair = await PoolManager.GetServerClient(new PoolSettings { Fresh = true }); - var server = pair.Server; - var entMan = server.EntMan; - var pulling = server.System(); - var map = await pair.CreateTestMap(); - - EntityUid holder = default; - EntityUid plainTarget = default; - EntityUid holdTarget = default; - - await server.WaitPost(() => - { - holder = entMan.SpawnEntity(HolderPrototype, map.GridCoords); - plainTarget = entMan.SpawnEntity("MobHuman", map.GridCoords.Offset(new Vector2(0.1f, 0f))); - holdTarget = entMan.SpawnEntity("MobHuman", map.GridCoords.Offset(new Vector2(-0.1f, 0f))); - - entMan.RemoveComponent(plainTarget); - - Assert.That(pulling.TryStartPull(holder, plainTarget), Is.True); - }); - await server.WaitRunTicks(2); - - await server.WaitAssertion(() => - { - var holderPuller = entMan.GetComponent(holder); - var plainPullable = entMan.GetComponent(plainTarget); - - Assert.Multiple(() => - { - Assert.That(holderPuller.Pulling, Is.EqualTo(plainTarget)); - Assert.That(plainPullable.Puller, Is.EqualTo(holder)); - Assert.That(entMan.HasComponent(holdTarget), Is.False); - }); - }); - - await server.WaitPost(() => - { - Assert.That(pulling.TryStartPull(holder, holdTarget), Is.True); - }); - await server.WaitRunTicks(2); - - await server.WaitAssertion(() => - { - var holderPuller = entMan.GetComponent(holder); - var holderState = entMan.GetComponent(holder); - var plainPullable = entMan.GetComponent(plainTarget); - var holdPullable = entMan.GetComponent(holdTarget); - - Assert.Multiple(() => - { - Assert.That(holderPuller.Pulling, Is.Null); - Assert.That(holderState.Target, Is.EqualTo(holdTarget)); - Assert.That(plainPullable.Puller, Is.Null); - Assert.That(holdPullable.Puller, Is.Null); - Assert.That(entMan.HasComponent(holdTarget), Is.True); - }); - }); - - await pair.CleanReturnAsync(); - } - - [Test] - public async Task SecondHolderEntersFullHoldAndFillsHands() - { - await using var pair = await PoolManager.GetServerClient(new PoolSettings { Fresh = true }); - var server = pair.Server; - var entMan = server.EntMan; - var holding = server.System(); - var handsSystem = server.System(); - var map = await pair.CreateTestMap(); - - EntityUid holderOne = default; - EntityUid holderTwo = default; - EntityUid target = default; - - await server.WaitPost(() => - { - holderOne = entMan.SpawnEntity(HolderPrototype, map.GridCoords); - holderTwo = entMan.SpawnEntity(HolderPrototype, map.GridCoords); - target = entMan.SpawnEntity("MobHuman", map.GridCoords); - - StartHold(entMan, holding, holderOne, target); - StartHold(entMan, holding, holderTwo, target); - }); - - await server.WaitAssertion(() => - { - var held = entMan.GetComponent(target); - var hands = entMan.GetComponent(target); - var holderOnePuller = entMan.GetComponent(holderOne); - var holderTwoPuller = entMan.GetComponent(holderTwo); - var pullable = entMan.GetComponent(target); - var move = new UpdateCanMoveEvent(target); - entMan.EventBus.RaiseLocalEvent(target, move); - - Assert.Multiple(() => - { - Assert.That(HasFullHold(entMan, target), Is.True); - Assert.That(held.RequiredHolderCount, Is.EqualTo(2)); - Assert.That(held.Holders, Has.Count.EqualTo(2)); - Assert.That(move.Cancelled, Is.True); - Assert.That(CountBlockingVirtualHands(entMan, handsSystem, target, hands, holderOne, holderTwo), Is.EqualTo(hands.SortedHands.Count)); - Assert.That(VictimHandsUseHolderIcons(entMan, handsSystem, target, hands, holderOne, holderTwo), Is.True); - Assert.That(holderOnePuller.Pulling, Is.Null); - Assert.That(holderTwoPuller.Pulling, Is.Null); - Assert.That(pullable.Puller, Is.Null); - }); - }); - - await pair.CleanReturnAsync(); - } - - [Test] - public async Task FullHoldVictimBlockersStayStableOnServerAndClient() - { - await using var pair = await PoolManager.GetServerClient(new PoolSettings - { - Fresh = true, - Connected = true, - DummyTicker = false, - }); - - var server = pair.Server; - var client = pair.Client; - var sEntMan = server.EntMan; - var cEntMan = client.EntMan; - var cTiming = client.ResolveDependency(); - var sTransform = server.System(); - var sHandsSystem = server.System(); - var cHandsSystem = client.System(); - var holding = server.System(); - var map = await pair.CreateTestMap(); - - var serverPlayer = pair.Player!.AttachedEntity!.Value; - EntityUid holderOne = default; - EntityUid holderTwo = default; - - await server.WaitPost(() => - { - sTransform.SetCoordinates(serverPlayer, map.GridCoords); - holderOne = sEntMan.SpawnEntity(HolderPrototype, map.GridCoords.Offset(new Vector2(0.5f, 0f))); - holderTwo = sEntMan.SpawnEntity(HolderPrototype, map.GridCoords.Offset(new Vector2(-0.5f, 0f))); - StartHold(sEntMan, holding, holderOne, serverPlayer); - StartHold(sEntMan, holding, holderTwo, serverPlayer); - }); - - await pair.RunTicksSync(10); - await pair.SyncTicks(targetDelta: 1); - - EntityUid[] initialServerBlockers = []; - await server.WaitAssertion(() => - { - var hands = sEntMan.GetComponent(serverPlayer); - - Assert.Multiple(() => - { - Assert.That(HasFullHold(sEntMan, serverPlayer), Is.True); - Assert.That(CountBlockingVirtualHands(sEntMan, sHandsSystem, serverPlayer, hands, holderOne, holderTwo), Is.EqualTo(hands.SortedHands.Count)); - }); - - initialServerBlockers = GetHeldHandBlockers(sEntMan, sHandsSystem, serverPlayer, hands, holderOne, holderTwo); - Assert.That(initialServerBlockers, Has.Length.EqualTo(hands.SortedHands.Count)); - }); - - var clientPlayer = EntityUid.Invalid; - EntityUid[] initialClientBlockers = []; - await client.WaitAssertion(() => - { - clientPlayer = client.AttachedEntity!.Value; - var hands = cEntMan.GetComponent(clientPlayer); - - Assert.Multiple(() => - { - Assert.That(HasFullHold(cEntMan, clientPlayer), Is.True); - Assert.That(CountBlockingVirtualHands(cEntMan, cHandsSystem, clientPlayer, hands, holderOne, holderTwo), Is.EqualTo(hands.SortedHands.Count)); - }); - - initialClientBlockers = GetHeldHandBlockers(cEntMan, cHandsSystem, clientPlayer, hands, holderOne, holderTwo); - Assert.That(initialClientBlockers, Has.Length.EqualTo(hands.SortedHands.Count)); - }); - - await pair.RunTicksSync(8); - await pair.SyncTicks(targetDelta: 1); - - await server.WaitAssertion(() => - { - var hands = sEntMan.GetComponent(serverPlayer); - - Assert.Multiple(() => - { - Assert.That(HasFullHold(sEntMan, serverPlayer), Is.True); - Assert.That(GetHeldHandBlockers(sEntMan, sHandsSystem, serverPlayer, hands, holderOne, holderTwo), Is.EqualTo(initialServerBlockers)); - }); - }); - - await client.WaitAssertion(() => - { - var hands = cEntMan.GetComponent(clientPlayer); - - Assert.Multiple(() => - { - Assert.That(HasFullHold(cEntMan, clientPlayer), Is.True); - Assert.That(GetHeldHandBlockers(cEntMan, cHandsSystem, clientPlayer, hands, holderOne, holderTwo), Is.EqualTo(initialClientBlockers)); - }); - }); - - await pair.CleanReturnAsync(); - } - - [Test] - public async Task TargetWithoutScpHoldableCannotBeHeld() - { - await using var pair = await PoolManager.GetServerClient(new PoolSettings { Fresh = true }); - var server = pair.Server; - var entMan = server.EntMan; - var holding = server.System(); - var map = await pair.CreateTestMap(); - - EntityUid holder = default; - EntityUid target = default; - - await server.WaitPost(() => - { - holder = entMan.SpawnEntity(HolderPrototype, map.GridCoords); - target = entMan.SpawnEntity("MobHuman", map.GridCoords); - entMan.RemoveComponent(target); - }); - - await server.WaitAssertion(() => - { - var holdComp = entMan.GetComponent(holder); - - Assert.Multiple(() => - { - Assert.That(holding.CanToggleHold((holder, holdComp), target, quiet: true), Is.False); - Assert.That(holding.TryToggleHold((holder, holdComp), target), Is.False); - Assert.That(entMan.HasComponent(target), Is.False); - }); - }); - - await pair.CleanReturnAsync(); - } - - [Test] - public async Task HolderAndHoldableFiltersUseCheckBoth() - { - await using var pair = await PoolManager.GetServerClient(new PoolSettings { Fresh = true }); - var server = pair.Server; - var entMan = server.EntMan; - var holding = server.System(); - var map = await pair.CreateTestMap(); - - EntityUid successHolder = default; - EntityUid blockedHolder = default; - EntityUid blacklistHolder = default; - EntityUid successTarget = default; - EntityUid blockedTarget = default; - EntityUid blacklistTarget = default; - EntityUid holderBlacklistTarget = default; - - await server.WaitPost(() => - { - successHolder = entMan.SpawnEntity(HoldableWhitelistedHolderPrototype, map.GridCoords); - blockedHolder = entMan.SpawnEntity(HolderPrototype, map.GridCoords); - blacklistHolder = entMan.SpawnEntity(HoldableBlacklistedHolderPrototype, map.GridCoords); - successTarget = entMan.SpawnEntity("MobHuman", map.GridCoords); - blockedTarget = entMan.SpawnEntity("MobHuman", map.GridCoords); - blacklistTarget = entMan.SpawnEntity("MobHuman", map.GridCoords); - holderBlacklistTarget = entMan.SpawnEntity("MobHuman", map.GridCoords); - - entMan.AddComponent(successHolder); - entMan.AddComponent(blacklistHolder); - entMan.AddComponent(successTarget); - entMan.AddComponent(blacklistTarget); - entMan.AddComponent(holderBlacklistTarget); - - var successTargetHoldable = entMan.GetComponent(successTarget); - successTargetHoldable.HolderWhitelist = CreateComponentWhitelist(TestListenerComponentName); - - var holderBlacklistHoldable = entMan.GetComponent(holderBlacklistTarget); - holderBlacklistHoldable.HolderBlacklist = CreateComponentWhitelist(TestListenerComponentName); - }); - - await server.WaitAssertion(() => - { - var successHold = entMan.GetComponent(successHolder); - var blockedHold = entMan.GetComponent(blockedHolder); - var blacklistHold = entMan.GetComponent(blacklistHolder); - - Assert.Multiple(() => - { - Assert.That(holding.CanToggleHold((successHolder, successHold), blockedTarget, quiet: true), Is.False); - Assert.That(holding.TryToggleHold((successHolder, successHold), blockedTarget), Is.False); - - Assert.That(holding.CanToggleHold((blockedHolder, blockedHold), successTarget, quiet: true), Is.False); - Assert.That(holding.TryToggleHold((blockedHolder, blockedHold), successTarget), Is.False); - - Assert.That(holding.CanToggleHold((blacklistHolder, blacklistHold), blacklistTarget, quiet: true), Is.False); - Assert.That(holding.TryToggleHold((blacklistHolder, blacklistHold), blacklistTarget), Is.False); - - Assert.That(holding.CanToggleHold((successHolder, successHold), holderBlacklistTarget, quiet: true), Is.False); - Assert.That(holding.TryToggleHold((successHolder, successHold), holderBlacklistTarget), Is.False); - - Assert.That(holding.CanToggleHold((successHolder, successHold), successTarget, quiet: true), Is.True); - Assert.That(holding.TryToggleHold((successHolder, successHold), successTarget), Is.True); - - Assert.That(entMan.HasComponent(blockedTarget), Is.False); - Assert.That(entMan.HasComponent(successTarget), Is.True); - }); - }); - - await pair.CleanReturnAsync(); - } - - [Test] - public async Task HoldAttemptEventCanCancelGrab() - { - await using var pair = await PoolManager.GetServerClient(new PoolSettings { Fresh = true }); - var server = pair.Server; - var entMan = server.EntMan; - var holding = server.System(); - var attempts = server.System(); - _ = server.System(); - var map = await pair.CreateTestMap(); - - EntityUid holder = default; - EntityUid target = default; - - await server.WaitPost(() => - { - holder = entMan.SpawnEntity(HolderPrototype, map.GridCoords); - target = entMan.SpawnEntity("MobHuman", map.GridCoords); - entMan.AddComponent(holder); - entMan.AddComponent(target); - entMan.AddComponent(target); - }); - - await server.WaitAssertion(() => - { - var holdComp = entMan.GetComponent(holder); - - Assert.Multiple(() => - { - Assert.That(holding.TryToggleHold((holder, holdComp), target), Is.False); - Assert.That(entMan.HasComponent(target), Is.False); - Assert.That(attempts.Count(target), Is.EqualTo(1)); - Assert.That(attempts.Count(holder), Is.EqualTo(1)); - }); - }); - - await pair.CleanReturnAsync(); - } - - [Test] - public async Task BreakoutEventRaisedWhenTargetEscapes() - { - await using var pair = await PoolManager.GetServerClient(new PoolSettings { Fresh = true }); - var server = pair.Server; - var entMan = server.EntMan; - var holding = server.System(); - var breakouts = server.System(); - var map = await pair.CreateTestMap(); - - EntityUid holder = default; - EntityUid target = default; - - await server.WaitPost(() => - { - holder = entMan.SpawnEntity(HolderPrototype, map.GridCoords); - target = entMan.SpawnEntity("MobHuman", map.GridCoords); - entMan.AddComponent(target); - StartHold(entMan, holding, holder, target); - }); - - await server.WaitPost(() => RaiseMoveInput(entMan, target)); - await server.WaitRunTicks(2); - - await server.WaitAssertion(() => - { - var breakout = breakouts.GetEvents(target).Single(); - - Assert.Multiple(() => - { - Assert.That(entMan.HasComponent(target), Is.False); - Assert.That(breakouts.Count(target), Is.EqualTo(1)); - Assert.That(breakout.ViaMovement, Is.True); - Assert.That(breakout.WasFullHold, Is.False); - Assert.That(breakout.AppliedImmunity, Is.False); - }); - }); - - await pair.CleanReturnAsync(); - } - - [Test] - public async Task MultiHandTargetNeedsMatchingHolderCountAndResyncsOnHandLoss() - { - await using var pair = await PoolManager.GetServerClient(new PoolSettings { Fresh = true }); - var server = pair.Server; - var entMan = server.EntMan; - var host = server.ResolveDependency(); - var holding = server.System(); - var handsSystem = server.System(); - var transform = server.System(); - var bodySystem = server.System(); - var map = await pair.CreateTestMap(); - - EntityUid holderOne = default; - EntityUid holderTwo = default; - EntityUid holderThree = default; - EntityUid target = default; - - await server.WaitPost(() => - { - holderOne = entMan.SpawnEntity(HolderPrototype, map.GridCoords); - holderTwo = entMan.SpawnEntity(HolderPrototype, map.GridCoords); - holderThree = entMan.SpawnEntity(HolderPrototype, map.GridCoords); - target = entMan.SpawnEntity("MobHuman", map.GridCoords); - - host.ExecuteCommand(null, $"addhand {entMan.GetNetEntity(target)}"); - }); - await server.WaitRunTicks(2); - - await server.WaitPost(() => - { - StartHold(entMan, holding, holderOne, target); - StartHold(entMan, holding, holderTwo, target); - }); - - await server.WaitAssertion(() => - { - var held = entMan.GetComponent(target); - Assert.Multiple(() => - { - Assert.That(held.RequiredHolderCount, Is.EqualTo(3)); - Assert.That(HasFullHold(entMan, target), Is.False); - Assert.That(held.Holders, Has.Count.EqualTo(2)); - }); - }); - - await server.WaitPost(() => - { - StartHold(entMan, holding, holderThree, target); - }); - - await server.WaitAssertion(() => - { - var held = entMan.GetComponent(target); - var hands = entMan.GetComponent(target); - - Assert.Multiple(() => - { - Assert.That(HasFullHold(entMan, target), Is.True); - Assert.That(held.RequiredHolderCount, Is.EqualTo(3)); - Assert.That(hands.SortedHands.Count, Is.EqualTo(3)); - Assert.That(CountBlockingVirtualHands(entMan, handsSystem, target, hands, holderOne, holderTwo, holderThree), Is.EqualTo(3)); - Assert.That(VictimHandsUseHolderIcons(entMan, handsSystem, target, hands, holderOne, holderTwo, holderThree), Is.True); - }); - }); - - await server.WaitPost(() => - { - var body = entMan.GetComponent(target); - var removedHand = bodySystem.GetBodyChildrenOfType(target, BodyPartType.Hand, body).First().Id; - transform.AttachToGridOrMap(removedHand); - }); - await server.WaitRunTicks(2); - - await server.WaitAssertion(() => - { - var held = entMan.GetComponent(target); - var hands = entMan.GetComponent(target); - - Assert.Multiple(() => - { - Assert.That(held.RequiredHolderCount, Is.EqualTo(2)); - Assert.That(HasFullHold(entMan, target), Is.True); - Assert.That(hands.SortedHands.Count, Is.EqualTo(2)); - Assert.That(CountBlockingVirtualHands(entMan, handsSystem, target, hands, holderOne, holderTwo, holderThree), Is.EqualTo(2)); - Assert.That(VictimHandsUseHolderIcons(entMan, handsSystem, target, hands, holderOne, holderTwo, holderThree), Is.True); - }); - }); - - await pair.CleanReturnAsync(); - } - - [Test] - public async Task FullBreakoutByMovementAppliesImmunity() - { - await using var pair = await PoolManager.GetServerClient(new PoolSettings { Fresh = true }); - var server = pair.Server; - var entMan = server.EntMan; - var timing = server.ResolveDependency(); - var holding = server.System(); - var map = await pair.CreateTestMap(); - - EntityUid holderOne = default; - EntityUid holderTwo = default; - EntityUid target = default; - - await server.WaitPost(() => - { - holderOne = entMan.SpawnEntity(HolderPrototype, map.GridCoords); - holderTwo = entMan.SpawnEntity(HolderPrototype, map.GridCoords); - target = entMan.SpawnEntity("MobHuman", map.GridCoords); - - StartHold(entMan, holding, holderOne, target); - StartHold(entMan, holding, holderTwo, target); - }); - - await server.WaitPost(() => RaiseMoveInput(entMan, target)); - await server.WaitRunTicks(2); - - await server.WaitAssertion(() => - { - var held = entMan.GetComponent(target); - Assert.Multiple(() => - { - Assert.That(HasFullHold(entMan, target), Is.True); - Assert.That(HasBreakoutAttempt(entMan, target), Is.False); - }); - }); - - await server.WaitRunTicks(GetTickCount(timing, TimeSpan.FromSeconds(10))); - - await server.WaitPost(() => RaiseMoveInput(entMan, target)); - await server.WaitRunTicks(2); - - await server.WaitAssertion(() => - { - var held = entMan.GetComponent(target); - Assert.Multiple(() => - { - Assert.That(HasBreakoutAttempt(entMan, target), Is.True); - Assert.That(CountAttachedPrototype(entMan, holderOne, "WhistleExclamation"), Is.EqualTo(1)); - Assert.That(CountAttachedPrototype(entMan, holderTwo, "WhistleExclamation"), Is.EqualTo(1)); - }); - }); - - await server.WaitRunTicks(GetTickCount(timing, TimeSpan.FromSeconds(5))); - - await server.WaitAssertion(() => - { - Assert.Multiple(() => - { - Assert.That(entMan.HasComponent(target), Is.False); - Assert.That(entMan.HasComponent(target), Is.True); - }); - }); - - await server.WaitPost(() => - { - var holdComp = entMan.GetComponent(holderOne); - Assert.That(holding.TryToggleHold((holderOne, holdComp), target), Is.False); - }); - - await server.WaitAssertion(() => - { - Assert.That(entMan.HasComponent(target), Is.False); - }); - - await server.WaitRunTicks(GetTickCount(timing, TimeSpan.FromSeconds(5))); - - await server.WaitPost(() => - { - var holdComp = entMan.GetComponent(holderOne); - Assert.That(holding.TryToggleHold((holderOne, holdComp), target), Is.True); - }); - - await server.WaitAssertion(() => - { - Assert.That(entMan.HasComponent(target), Is.True); - }); - - await pair.CleanReturnAsync(); - } - - [Test] - public async Task Scp096FullBreakoutAffectsAllHoldersAndThrowsOverlappingHoldersApart() - { - await using var pair = await PoolManager.GetServerClient(new PoolSettings { Fresh = true }); - var server = pair.Server; - var entMan = server.EntMan; - var holding = server.System(); - var physics = server.System(); - var transform = server.System(); - var map = await pair.CreateTestMap(); - - EntityUid holderOne = default; - EntityUid holderTwo = default; - EntityUid target = default; - - await server.WaitPost(() => - { - holderOne = entMan.SpawnEntity(HolderPrototype, map.GridCoords); - holderTwo = entMan.SpawnEntity(HolderPrototype, map.GridCoords); - target = entMan.SpawnEntity("Scp096", map.GridCoords); - - var holdable = entMan.GetComponent(target); - holdable.FullHoldDelay = TimeSpan.Zero; - holdable.FullBreakoutDuration = TimeSpan.Zero; - - StartHold(entMan, holding, holderOne, target); - StartHold(entMan, holding, holderTwo, target); - }); - await server.WaitRunTicks(2); - - await server.WaitAssertion(() => - { - Assert.That(HasFullHold(entMan, target), Is.True); - }); - - await server.WaitPost(() => - { - transform.SetCoordinates(holderOne, map.GridCoords); - transform.SetCoordinates(holderTwo, map.GridCoords); - transform.SetCoordinates(target, map.GridCoords); - RaiseMoveInput(entMan, target); - }); - await server.WaitRunTicks(4); - - await server.WaitAssertion(() => - { - var holderOneDamage = entMan.GetComponent(holderOne); - var holderTwoDamage = entMan.GetComponent(holderTwo); - var holderOneVelocity = physics.GetMapLinearVelocity(holderOne); - var holderTwoVelocity = physics.GetMapLinearVelocity(holderTwo); - - Assert.Multiple(() => - { - Assert.That(entMan.HasComponent(target), Is.False); - Assert.That(entMan.HasComponent(target), Is.False); - Assert.That(entMan.HasComponent(holderOne), Is.False); - Assert.That(entMan.HasComponent(holderTwo), Is.False); - Assert.That(entMan.HasComponent(holderOne), Is.True); - Assert.That(entMan.HasComponent(holderTwo), Is.True); - Assert.That(holderOneDamage.TotalDamage, Is.GreaterThan(FixedPoint2.Zero)); - Assert.That(holderTwoDamage.TotalDamage, Is.GreaterThan(FixedPoint2.Zero)); - Assert.That(MathF.Abs(holderOneVelocity.X), Is.GreaterThan(1f)); - Assert.That(MathF.Abs(holderTwoVelocity.X), Is.GreaterThan(1f)); - Assert.That(MathF.Sign(holderOneVelocity.X), Is.EqualTo(-MathF.Sign(holderTwoVelocity.X))); - }); - }); - - await pair.CleanReturnAsync(); - } - - [Test] - public async Task FullBreakoutRestoresMovementOnServerAndClient() - { - await using var pair = await PoolManager.GetServerClient(new PoolSettings - { - Fresh = true, - Connected = true, - DummyTicker = false, - }); - - var server = pair.Server; - var client = pair.Client; - var sEntMan = server.EntMan; - var cEntMan = client.EntMan; - var timing = server.ResolveDependency(); - var sTransform = server.System(); - var holding = server.System(); - var map = await pair.CreateTestMap(); - - var serverPlayer = pair.Player!.AttachedEntity!.Value; - EntityUid holderOne = default; - EntityUid holderTwo = default; - - await server.WaitPost(() => - { - sTransform.SetCoordinates(serverPlayer, map.GridCoords); - holderOne = sEntMan.SpawnEntity(HolderPrototype, map.GridCoords.Offset(new Vector2(0.5f, 0f))); - holderTwo = sEntMan.SpawnEntity(HolderPrototype, map.GridCoords.Offset(new Vector2(-0.5f, 0f))); - StartHold(sEntMan, holding, holderOne, serverPlayer); - StartHold(sEntMan, holding, holderTwo, serverPlayer); - }); - - await pair.RunTicksSync(10); - await pair.SyncTicks(targetDelta: 1); - await pair.RunTicksSync(GetTickCount(timing, TimeSpan.FromSeconds(10))); - await pair.SyncTicks(targetDelta: 1); - - await server.WaitAssertion(() => - { - var mover = sEntMan.GetComponent(serverPlayer); - - Assert.Multiple(() => - { - Assert.That(HasFullHold(sEntMan, serverPlayer), Is.True); - Assert.That(mover.CanMove, Is.False); - }); - }); - - var clientPlayer = EntityUid.Invalid; - await client.WaitAssertion(() => - { - clientPlayer = client.AttachedEntity!.Value; - var mover = cEntMan.GetComponent(clientPlayer); - - Assert.Multiple(() => - { - Assert.That(HasFullHold(cEntMan, clientPlayer), Is.True); - Assert.That(mover.CanMove, Is.False); - }); - }); - - await server.WaitPost(() => RaiseMoveInput(sEntMan, serverPlayer)); - await pair.RunTicksSync(2); - await pair.RunTicksSync(GetTickCount(timing, TimeSpan.FromSeconds(5)) + 2); - await pair.SyncTicks(targetDelta: 1); - - await server.WaitAssertion(() => - { - var mover = sEntMan.GetComponent(serverPlayer); - - Assert.Multiple(() => - { - Assert.That(sEntMan.HasComponent(serverPlayer), Is.False); - Assert.That(sEntMan.HasComponent(serverPlayer), Is.False); - Assert.That(mover.CanMove, Is.True); - }); - }); - - await client.WaitAssertion(() => - { - var mover = cEntMan.GetComponent(clientPlayer); - - Assert.Multiple(() => - { - Assert.That(cEntMan.HasComponent(clientPlayer), Is.False); - Assert.That(cEntMan.HasComponent(clientPlayer), Is.False); - Assert.That(mover.CanMove, Is.True); - }); - }); - - await pair.CleanReturnAsync(); - } - - [Test] - public async Task FullBreakoutByAlertStartsAndCompletes() - { - await using var pair = await PoolManager.GetServerClient(new PoolSettings { Fresh = true }); - var server = pair.Server; - var entMan = server.EntMan; - var alerts = server.System(); - var timing = server.ResolveDependency(); - var proto = server.ResolveDependency(); - var holding = server.System(); - var map = await pair.CreateTestMap(); - - EntityUid holderOne = default; - EntityUid holderTwo = default; - EntityUid target = default; - - await server.WaitPost(() => - { - holderOne = entMan.SpawnEntity(HolderPrototype, map.GridCoords); - holderTwo = entMan.SpawnEntity(HolderPrototype, map.GridCoords); - target = entMan.SpawnEntity("MobHuman", map.GridCoords); - - StartHold(entMan, holding, holderOne, target); - StartHold(entMan, holding, holderTwo, target); - }); - - await server.WaitRunTicks(GetTickCount(timing, TimeSpan.FromSeconds(10))); - - await server.WaitPost(() => - { - var alert = proto.Index(GrabbedAlertId); - Assert.That(alerts.ActivateAlert(target, alert), Is.True); - }); - await server.WaitRunTicks(2); - - await server.WaitAssertion(() => - { - var held = entMan.GetComponent(target); - Assert.Multiple(() => - { - Assert.That(HasBreakoutAttempt(entMan, target), Is.True); - Assert.That(CountAttachedPrototype(entMan, holderOne, "WhistleExclamation"), Is.EqualTo(1)); - Assert.That(CountAttachedPrototype(entMan, holderTwo, "WhistleExclamation"), Is.EqualTo(1)); - }); - }); - - await server.WaitRunTicks(GetTickCount(timing, TimeSpan.FromSeconds(5))); - - await server.WaitAssertion(() => - { - Assert.That(entMan.HasComponent(target), Is.False); - }); - - await pair.CleanReturnAsync(); - } - - [Test] - public async Task DroppingHolderBlockerReleasesHold() - { - await using var pair = await PoolManager.GetServerClient(new PoolSettings { Fresh = true }); - var server = pair.Server; - var entMan = server.EntMan; - var handsSystem = server.System(); - var holding = server.System(); - var map = await pair.CreateTestMap(); - - EntityUid holder = default; - EntityUid target = default; - - await server.WaitPost(() => - { - holder = entMan.SpawnEntity(HolderPrototype, map.GridCoords); - target = entMan.SpawnEntity("MobHuman", map.GridCoords.Offset(new Vector2(0.1f, 0f))); - StartHold(entMan, holding, holder, target); - }); - await server.WaitRunTicks(2); - - await server.WaitPost(() => - { - var hands = entMan.GetComponent(holder); - var blocker = FindHolderHandBlocker(entMan, handsSystem, holder, target, hands); - - Assert.That(blocker, Is.Not.EqualTo(EntityUid.Invalid)); - Assert.That(handsSystem.TryDrop((holder, hands), blocker), Is.False); - }); - await server.WaitRunTicks(2); - - await server.WaitAssertion(() => - { - Assert.That(entMan.HasComponent(target), Is.False); - Assert.That(entMan.HasComponent(holder), Is.False); - }); - - await pair.CleanReturnAsync(); - } - - [Test] - public async Task ThrowingHolderBlockerReleasesHold() - { - await using var pair = await PoolManager.GetServerClient(new PoolSettings { Fresh = true }); - var server = pair.Server; - var entMan = server.EntMan; - var handsSystem = server.System(); - var holding = server.System(); - var map = await pair.CreateTestMap(); - - EntityUid holder = default; - EntityUid target = default; - - await server.WaitPost(() => - { - holder = entMan.SpawnEntity(HolderPrototype, map.GridCoords); - target = entMan.SpawnEntity("MobHuman", map.GridCoords.Offset(new Vector2(0.1f, 0f))); - StartHold(entMan, holding, holder, target); - }); - await server.WaitRunTicks(2); - - await server.WaitPost(() => - { - var hands = entMan.GetComponent(holder); - var blocker = FindHolderHandBlocker(entMan, handsSystem, holder, target, hands); - var throwEvent = new BeforeThrowEvent(blocker, Vector2.UnitX, 5f, holder); - - Assert.That(blocker, Is.Not.EqualTo(EntityUid.Invalid)); - entMan.EventBus.RaiseLocalEvent(holder, ref throwEvent); - Assert.That(throwEvent.Cancelled, Is.True); - }); - await server.WaitRunTicks(2); - - await server.WaitAssertion(() => - { - Assert.That(entMan.HasComponent(target), Is.False); - Assert.That(entMan.HasComponent(holder), Is.False); - }); - - await pair.CleanReturnAsync(); - } - - [Test] - public async Task GettingDroppedAttemptOnHolderBlockerReleasesHold() - { - await using var pair = await PoolManager.GetServerClient(new PoolSettings { Fresh = true }); - var server = pair.Server; - var entMan = server.EntMan; - var handsSystem = server.System(); - var holding = server.System(); - var map = await pair.CreateTestMap(); - - EntityUid holder = default; - EntityUid target = default; - - await server.WaitPost(() => - { - holder = entMan.SpawnEntity(HolderPrototype, map.GridCoords); - target = entMan.SpawnEntity("MobHuman", map.GridCoords.Offset(new Vector2(0.1f, 0f))); - StartHold(entMan, holding, holder, target); - }); - await server.WaitRunTicks(2); - - await server.WaitPost(() => - { - var hands = entMan.GetComponent(holder); - var blocker = FindHolderHandBlocker(entMan, handsSystem, holder, target, hands); - var dropAttempt = new GettingDroppedAttemptEvent(holder); - - Assert.That(blocker, Is.Not.EqualTo(EntityUid.Invalid)); - entMan.EventBus.RaiseLocalEvent(blocker, ref dropAttempt); - Assert.That(dropAttempt.Cancelled, Is.True); - }); - await server.WaitRunTicks(2); - - await server.WaitAssertion(() => - { - Assert.That(entMan.HasComponent(target), Is.False); - Assert.That(entMan.HasComponent(holder), Is.False); - }); - - await pair.CleanReturnAsync(); - } - - [Test] - public async Task ClientDroppingHolderBlockerReleasesWithoutRespawnFlicker() - { - await using var pair = await PoolManager.GetServerClient(new PoolSettings - { - Fresh = true, - Connected = true, - DummyTicker = false, - }); - - var server = pair.Server; - var client = pair.Client; - var sEntMan = server.EntMan; - var cEntMan = client.EntMan; - var cTiming = client.ResolveDependency(); - var sTransform = server.System(); - var sHandsSystem = server.System(); - var cHandsSystem = client.System(); - var holding = server.System(); - var map = await pair.CreateTestMap(); - - var serverPlayer = pair.Player!.AttachedEntity!.Value; - EntityUid target = default; - - await server.WaitPost(() => - { - sTransform.SetCoordinates(serverPlayer, map.GridCoords); - sEntMan.EnsureComponent(serverPlayer); - target = sEntMan.SpawnEntity("MobHuman", map.GridCoords.Offset(new Vector2(0.1f, 0f))); - StartHold(sEntMan, holding, serverPlayer, target); - }); - - await pair.RunTicksSync(10); - await pair.SyncTicks(targetDelta: 1); - - var clientPlayer = EntityUid.Invalid; - var clientTarget = EntityUid.Invalid; - await client.WaitAssertion(() => - { - clientPlayer = client.AttachedEntity!.Value; - clientTarget = ToClientEntity(sEntMan, cEntMan, target); - var hands = cEntMan.GetComponent(clientPlayer); - - Assert.Multiple(() => - { - Assert.That(cEntMan.HasComponent(clientPlayer), Is.True); - Assert.That(cEntMan.HasComponent(clientTarget), Is.True); - Assert.That( - CountHolderTargetVirtualItems(cEntMan, cHandsSystem, clientPlayer, clientTarget, hands), - Is.EqualTo(1), - DescribeHolderTargetVirtualItems(cEntMan, cHandsSystem, clientPlayer, clientTarget, hands)); - Assert.That(CountPrototypeEntities(cEntMan, "clientsideclone"), Is.EqualTo(0)); - }); - }); - - await client.WaitPost(() => - { - var hands = cEntMan.GetComponent(clientPlayer); - var blocker = FindHolderHandBlocker(cEntMan, cHandsSystem, clientPlayer, clientTarget, hands); - - Assert.That(blocker, Is.Not.EqualTo(EntityUid.Invalid)); - Assert.That(cHandsSystem.IsHolding((clientPlayer, hands), blocker, out var blockerHand), Is.True); - - if (hands.ActiveHandId != blockerHand) - Assert.That(cHandsSystem.TrySetActiveHand((clientPlayer, hands), blockerHand), Is.True); - }); - - await PressClientDropKey(client, cEntMan, cTiming, clientTarget); - - await client.WaitAssertion(() => - { - var hands = cEntMan.GetComponent(clientPlayer); - Assert.Multiple(() => - { - Assert.That(cEntMan.HasComponent(clientTarget), Is.False); - Assert.That(cEntMan.HasComponent(clientPlayer), Is.False); - Assert.That(CountHolderTargetVirtualItems(cEntMan, cHandsSystem, clientPlayer, clientTarget, hands), Is.EqualTo(0)); - Assert.That(CountPrototypeEntities(cEntMan, "clientsideclone"), Is.EqualTo(0)); - }); - }); - - var maxClientBlockers = 0; - var holderRespawned = false; - var heldRespawned = false; - string? blockerTimeline = null; - for (var i = 0; i < 12; i++) - { - await pair.RunTicksSync(1); - await pair.SyncTicks(targetDelta: 1); - await client.WaitPost(() => - { - if (!cEntMan.TryGetComponent(clientPlayer, out var hands)) - return; - - var blockerCount = CountHolderTargetVirtualItems(cEntMan, cHandsSystem, clientPlayer, clientTarget, hands); - var hasHolder = cEntMan.HasComponent(clientPlayer); - var hasHeld = cEntMan.HasComponent(clientTarget); - - maxClientBlockers = Math.Max(maxClientBlockers, blockerCount); - holderRespawned |= hasHolder; - heldRespawned |= hasHeld; - - if (blockerCount > 0 || hasHolder || hasHeld) - { - blockerTimeline = - $"tick={i}, holder={hasHolder}, held={hasHeld}, " + - $"items=[{DescribeHolderTargetVirtualItems(cEntMan, cHandsSystem, clientPlayer, clientTarget, hands)}]"; - } - }); - } - - Assert.Multiple(() => - { - Assert.That(maxClientBlockers, Is.EqualTo(0), blockerTimeline); - Assert.That(holderRespawned, Is.False, blockerTimeline); - Assert.That(heldRespawned, Is.False, blockerTimeline); - Assert.That(CountPrototypeEntities(cEntMan, "clientsideclone"), Is.EqualTo(0)); - }); - - await pair.CleanReturnAsync(); - } - - [Test] - public async Task ClientPullAttemptPredictsSoftHoldBeforeServerAck() - { - await using var pair = await PoolManager.GetServerClient(new PoolSettings - { - Fresh = true, - Connected = true, - DummyTicker = false, - }); - - var server = pair.Server; - var client = pair.Client; - var sEntMan = server.EntMan; - var cEntMan = client.EntMan; - var cTiming = client.ResolveDependency(); - var sPhysics = server.System(); - var cPhysics = client.System(); - var sTransform = server.System(); - var cTransform = client.System(); - var sAlerts = server.System(); - var cAlerts = client.System(); - var sStatusEffects = server.System(); - var cStatusEffects = client.System(); - var sHandsSystem = server.System(); - var cHandsSystem = client.System(); - var map = await pair.CreateTestMap(); - - var serverPlayer = pair.Player!.AttachedEntity!.Value; - EntityUid target = default; - - await server.WaitPost(() => - { - sTransform.SetCoordinates(serverPlayer, map.GridCoords); - sEntMan.EnsureComponent(serverPlayer); - target = sEntMan.SpawnEntity("MobHuman", map.GridCoords.Offset(new Vector2(0.1f, 0f))); - }); - - await pair.RunTicksSync(10); - await pair.SyncTicks(targetDelta: 1); - - var clientPlayer = EntityUid.Invalid; - var clientTarget = EntityUid.Invalid; - await client.WaitAssertion(() => - { - clientPlayer = client.AttachedEntity!.Value; - clientTarget = ToClientEntity(sEntMan, cEntMan, target); - - Assert.Multiple(() => - { - Assert.That(cEntMan.HasComponent(clientPlayer), Is.True); - Assert.That(cEntMan.EntityExists(clientTarget), Is.True); - }); - }); - - await PressClientPullKey(client, cEntMan, cTiming, clientTarget); - - await client.WaitAssertion(() => - { - var held = cEntMan.GetComponent(clientTarget); - var holderHands = cEntMan.GetComponent(clientPlayer); - Assert.Multiple(() => - { - Assert.That(HasFullHold(cEntMan, clientTarget), Is.False); - Assert.That(held.Holders, Has.Count.EqualTo(1)); - Assert.That(cEntMan.HasComponent(clientPlayer), Is.True); - Assert.That(CountHolderHandBlockers(cEntMan, cHandsSystem, clientPlayer, clientTarget, holderHands), Is.EqualTo(1)); - Assert.That(CountHolderTargetVirtualItems(cEntMan, cHandsSystem, clientPlayer, clientTarget, holderHands), Is.EqualTo(1)); - Assert.That(cStatusEffects.HasStatusEffect(clientTarget, "StatusEffectScpHeld"), Is.True); - }); - }); - - await server.WaitAssertion(() => - { - Assert.That(sEntMan.HasComponent(target), Is.False); - }); - - var maxPredictedClientBlockers = 1; - for (var i = 0; i < 6; i++) - { - await pair.RunTicksSync(1); - await pair.SyncTicks(targetDelta: 1); - await client.WaitPost(() => - { - if (!cEntMan.TryGetComponent(clientPlayer, out var holderHands)) - return; - - maxPredictedClientBlockers = Math.Max(maxPredictedClientBlockers, - CountHolderTargetVirtualItems(cEntMan, cHandsSystem, clientPlayer, clientTarget, holderHands)); - }); - } - - Assert.That(maxPredictedClientBlockers, Is.EqualTo(1)); - - await pair.RunTicksSync(10); - await pair.SyncTicks(targetDelta: 1); - - await server.WaitAssertion(() => - { - var held = sEntMan.GetComponent(target); - var holderHands = sEntMan.GetComponent(serverPlayer); - var puller = sEntMan.GetComponent(serverPlayer); - var pullable = sEntMan.GetComponent(target); - var distance = GetDistance(sTransform, serverPlayer, target); - Assert.Multiple(() => - { - Assert.That(HasFullHold(sEntMan, target), Is.False); - Assert.That(held.Holders, Is.EqualTo(new[] { serverPlayer })); - Assert.That(distance, Is.GreaterThan(0.18f)); - Assert.That(distance, Is.LessThan(0.55f)); - Assert.That(puller.Pulling, Is.Null); - Assert.That(pullable.Puller, Is.Null); - Assert.That(CountHolderHandBlockers(sEntMan, sHandsSystem, serverPlayer, target, holderHands), Is.EqualTo(1)); - Assert.That(CountHolderTargetVirtualItems(sEntMan, sHandsSystem, serverPlayer, target, holderHands), Is.EqualTo(1)); - Assert.That(sStatusEffects.HasStatusEffect(target, "StatusEffectScpHeld"), Is.True); - Assert.That(sAlerts.IsShowingAlert(target, "ScpHoldGrabbed"), Is.True); - }); - }); - - await client.WaitAssertion(() => - { - var held = cEntMan.GetComponent(clientTarget); - var holderHands = cEntMan.GetComponent(clientPlayer); - var puller = cEntMan.GetComponent(clientPlayer); - var pullable = cEntMan.GetComponent(clientTarget); - var distance = GetDistance(cTransform, clientPlayer, clientTarget); - Assert.Multiple(() => - { - Assert.That(HasFullHold(cEntMan, clientTarget), Is.False); - Assert.That(held.Holders, Is.EqualTo(new[] { clientPlayer })); - Assert.That(distance, Is.GreaterThan(0.18f)); - Assert.That(distance, Is.LessThan(0.55f)); - Assert.That(puller.Pulling, Is.Null); - Assert.That(pullable.Puller, Is.Null); - Assert.That(CountHolderHandBlockers(cEntMan, cHandsSystem, clientPlayer, clientTarget, holderHands), Is.EqualTo(1)); - Assert.That(CountHolderTargetVirtualItems(cEntMan, cHandsSystem, clientPlayer, clientTarget, holderHands), Is.EqualTo(1)); - Assert.That(cStatusEffects.HasStatusEffect(clientTarget, "StatusEffectScpHeld"), Is.True); - }); - }); - - var minClientBlockersAfterAck = int.MaxValue; - var maxClientBlockersAfterAck = 0; - string? blockerTimelineAfterAck = null; - for (var i = 0; i < 12; i++) - { - await pair.RunTicksSync(1); - await pair.SyncTicks(targetDelta: 1); - await client.WaitPost(() => - { - if (!cEntMan.TryGetComponent(clientPlayer, out var holderHands)) - return; - - var blockerCount = CountHolderTargetVirtualItems(cEntMan, cHandsSystem, clientPlayer, clientTarget, holderHands); - minClientBlockersAfterAck = Math.Min(minClientBlockersAfterAck, blockerCount); - maxClientBlockersAfterAck = Math.Max(maxClientBlockersAfterAck, blockerCount); - - if (blockerCount != 1 || - !cEntMan.HasComponent(clientPlayer) || - !cEntMan.HasComponent(clientTarget)) - { - var distance = GetDistance(cTransform, clientPlayer, clientTarget); - blockerTimelineAfterAck = - $"tick={i}, blockers={blockerCount}, distance={distance:0.000}, " + - $"hasHolder={cEntMan.HasComponent(clientPlayer)}, " + - $"hasHeld={cEntMan.HasComponent(clientTarget)}, " + - $"items=[{DescribeHolderTargetVirtualItems(cEntMan, cHandsSystem, clientPlayer, clientTarget, holderHands)}]"; - } - }); - } - - Assert.Multiple(() => - { - Assert.That(minClientBlockersAfterAck, Is.EqualTo(1), blockerTimelineAfterAck); - Assert.That(maxClientBlockersAfterAck, Is.EqualTo(1), blockerTimelineAfterAck); - }); - - await pair.CleanReturnAsync(); - } - - // Fire added start - compare real client pull path against direct server hold helper - [Test] - public async Task ClientPullHeldTargetWithCombatModeEnabled_DisablesCombatModeInPredictionAndAfterAck() - { - await using var pair = await PoolManager.GetServerClient(new PoolSettings - { - Fresh = true, - Connected = true, - DummyTicker = false, - }); - - var server = pair.Server; - var client = pair.Client; - var sEntMan = server.EntMan; - var cEntMan = client.EntMan; - var cTiming = client.ResolveDependency(); - var sTransform = server.System(); - var sCombatMode = server.System(); - var map = await pair.CreateTestMap(); - - var serverPlayer = pair.Player!.AttachedEntity!.Value; - EntityUid target = default; - EntityUid serverCombatAction = default; - - await server.WaitPost(() => - { - sTransform.SetCoordinates(serverPlayer, map.GridCoords); - sEntMan.EnsureComponent(serverPlayer); - target = sEntMan.SpawnEntity("MobHuman", map.GridCoords.Offset(new Vector2(0.1f, 0f))); - - var combat = sEntMan.GetComponent(target); - sCombatMode.SetInCombatMode(target, true, combat); - serverCombatAction = GetCombatToggleAction(sEntMan, target); - }); - - await pair.RunTicksSync(10); - await pair.SyncTicks(targetDelta: 1); - - EntityUid clientPlayer = default; - EntityUid clientTarget = default; - EntityUid clientCombatAction = default; - await client.WaitAssertion(() => - { - clientPlayer = client.AttachedEntity!.Value; - clientTarget = ToClientEntity(sEntMan, cEntMan, target); - clientCombatAction = ToClientEntity(sEntMan, cEntMan, serverCombatAction); - - Assert.Multiple(() => - { - Assert.That(IsInCombatMode(sEntMan, target), Is.True); - Assert.That(IsActionToggled(sEntMan, serverCombatAction), Is.True); - Assert.That(cEntMan.HasComponent(clientPlayer), Is.True); - Assert.That(IsInCombatMode(cEntMan, clientTarget), Is.True); - Assert.That(IsActionToggled(cEntMan, clientCombatAction), Is.True); - }); - }); - - await PressClientPullKey(client, cEntMan, cTiming, clientTarget); - - await client.WaitAssertion(() => - { - clientCombatAction = ToClientEntity(sEntMan, cEntMan, serverCombatAction); - - Assert.Multiple(() => - { - Assert.That(cEntMan.HasComponent(clientTarget), Is.True); - Assert.That(cEntMan.HasComponent(clientPlayer), Is.True); - Assert.That(IsInCombatMode(cEntMan, clientTarget), Is.False); - Assert.That(IsActionToggled(cEntMan, clientCombatAction), Is.False); - }); - }); - - await server.WaitAssertion(() => - { - Assert.That(sEntMan.HasComponent(target), Is.False); - }); - - await pair.RunTicksSync(10); - await pair.SyncTicks(targetDelta: 1); - - await server.WaitAssertion(() => - { - Assert.Multiple(() => - { - Assert.That(sEntMan.HasComponent(target), Is.True); - Assert.That(IsInCombatMode(sEntMan, target), Is.False); - Assert.That(IsActionToggled(sEntMan, serverCombatAction), Is.False); - }); - }); - - await client.WaitAssertion(() => - { - clientCombatAction = ToClientEntity(sEntMan, cEntMan, serverCombatAction); - - Assert.Multiple(() => - { - Assert.That(cEntMan.HasComponent(clientTarget), Is.True); - Assert.That(IsInCombatMode(cEntMan, clientTarget), Is.False); - Assert.That(IsActionToggled(cEntMan, clientCombatAction), Is.False); - }); - }); - - await pair.CleanReturnAsync(); - } - // Fire added end - - [Test] - public async Task ClientPullCooldownAndFullBreakoutPenaltyReplicate() - { - await using var pair = await PoolManager.GetServerClient(new PoolSettings - { - Fresh = true, - Connected = true, - DummyTicker = false, - }); - - var server = pair.Server; - var client = pair.Client; - var sEntMan = server.EntMan; - var cEntMan = client.EntMan; - var sTiming = server.ResolveDependency(); - var cTiming = client.ResolveDependency(); - var sTransform = server.System(); - var holding = server.System(); - var map = await pair.CreateTestMap(); - - var serverPlayer = pair.Player!.AttachedEntity!.Value; - EntityUid firstTarget = default; - EntityUid breakoutTarget = default; - EntityUid holderTwo = default; - - await server.WaitPost(() => - { - sTransform.SetCoordinates(serverPlayer, map.GridCoords); - sEntMan.EnsureComponent(serverPlayer); - firstTarget = sEntMan.SpawnEntity("MobHuman", map.GridCoords.Offset(new Vector2(0.4f, 0f))); - breakoutTarget = sEntMan.SpawnEntity("MobHuman", map.GridCoords.Offset(new Vector2(0.4f, 0.6f))); - holderTwo = sEntMan.SpawnEntity(HolderPrototype, map.GridCoords.Offset(new Vector2(-0.4f, 0.6f))); - }); - - await pair.RunTicksSync(10); - await pair.SyncTicks(targetDelta: 1); - - EntityUid clientPlayer = default; - EntityUid clientFirstTarget = default; - EntityUid clientBreakoutTarget = default; - - await client.WaitAssertion(() => - { - clientPlayer = client.AttachedEntity!.Value; - clientFirstTarget = ToClientEntity(sEntMan, cEntMan, firstTarget); - clientBreakoutTarget = ToClientEntity(sEntMan, cEntMan, breakoutTarget); - }); - - await PressClientPullKey(client, cEntMan, cTiming, clientFirstTarget); - await PressClientPullKey(client, cEntMan, cTiming, clientBreakoutTarget); - - await client.WaitAssertion(() => - { - Assert.Multiple(() => - { - Assert.That(cEntMan.HasComponent(clientFirstTarget), Is.True); - Assert.That(cEntMan.HasComponent(clientBreakoutTarget), Is.False); - Assert.That(GetHoldCooldownRemaining(cEntMan, clientPlayer, cTiming), Is.GreaterThan(TimeSpan.Zero)); - }); - }); - - await pair.RunTicksSync(10); - await pair.SyncTicks(targetDelta: 1); - - await server.WaitAssertion(() => - { - Assert.Multiple(() => - { - Assert.That(sEntMan.HasComponent(firstTarget), Is.True); - Assert.That(sEntMan.HasComponent(breakoutTarget), Is.False); - Assert.That(GetHoldCooldownRemaining(sEntMan, serverPlayer, sTiming), Is.GreaterThan(TimeSpan.Zero)); - }); - }); - - await client.WaitAssertion(() => - { - Assert.That(GetHoldCooldownRemaining(cEntMan, clientPlayer, cTiming), Is.GreaterThan(TimeSpan.Zero)); - }); - - await pair.RunTicksSync(GetTickCount(sTiming, TimeSpan.FromSeconds(1)) + 1); - await pair.SyncTicks(targetDelta: 1); - - await PressClientPullKey(client, cEntMan, cTiming, clientFirstTarget); - - await client.WaitAssertion(() => - { - Assert.That(cEntMan.HasComponent(clientFirstTarget), Is.False); - }); - - await pair.RunTicksSync(10); - await pair.SyncTicks(targetDelta: 1); - - await server.WaitAssertion(() => - { - Assert.That(sEntMan.HasComponent(firstTarget), Is.False); - }); - - await pair.RunTicksSync(GetTickCount(sTiming, TimeSpan.FromSeconds(1)) + 1); - await pair.SyncTicks(targetDelta: 1); - - await PressClientPullKey(client, cEntMan, cTiming, clientBreakoutTarget); - - await client.WaitAssertion(() => - { - Assert.That(cEntMan.HasComponent(clientBreakoutTarget), Is.True); - }); - - await pair.RunTicksSync(10); - await pair.SyncTicks(targetDelta: 1); - - await server.WaitAssertion(() => - { - Assert.That(sEntMan.HasComponent(breakoutTarget), Is.True); - }); - - await server.WaitPost(() => - { - StartHold(sEntMan, holding, holderTwo, breakoutTarget); - }); - - await pair.RunTicksSync(10); - await pair.SyncTicks(targetDelta: 1); - await pair.RunTicksSync(GetTickCount(sTiming, TimeSpan.FromSeconds(10))); - await pair.SyncTicks(targetDelta: 1); - - await server.WaitPost(() => RaiseMoveInput(sEntMan, breakoutTarget)); - await pair.RunTicksSync(2); - await pair.SyncTicks(targetDelta: 1); - await pair.RunTicksSync(GetTickCount(sTiming, TimeSpan.FromSeconds(5)) + 5); - await pair.SyncTicks(targetDelta: 1); - - await server.WaitAssertion(() => - { - Assert.Multiple(() => - { - Assert.That(sEntMan.HasComponent(breakoutTarget), Is.False); - Assert.That(GetHoldCooldownRemaining(sEntMan, serverPlayer, sTiming), Is.GreaterThanOrEqualTo(TimeSpan.FromSeconds(1.5))); - Assert.That(GetHoldCooldownRemaining(sEntMan, holderTwo, sTiming), Is.GreaterThanOrEqualTo(TimeSpan.FromSeconds(1.5))); - }); - }); - - await client.WaitAssertion(() => - { - Assert.Multiple(() => - { - Assert.That(cEntMan.HasComponent(clientBreakoutTarget), Is.False); - Assert.That(GetHoldCooldownRemaining(cEntMan, clientPlayer, cTiming), Is.GreaterThanOrEqualTo(TimeSpan.FromSeconds(1.5))); - }); - }); - - await PressClientPullKey(client, cEntMan, cTiming, clientFirstTarget); - - await client.WaitAssertion(() => - { - Assert.That(cEntMan.HasComponent(clientFirstTarget), Is.False); - }); - - await pair.RunTicksSync(5); - await pair.SyncTicks(targetDelta: 1); - - await server.WaitAssertion(() => - { - Assert.That(sEntMan.HasComponent(firstTarget), Is.False); - }); - - await pair.CleanReturnAsync(); - } - - [Test] - public async Task ClientSecondPullPredictsFullHoldBeforeServerAck() - { - await using var pair = await PoolManager.GetServerClient(new PoolSettings - { - Fresh = true, - Connected = true, - DummyTicker = false, - }); - - var server = pair.Server; - var client = pair.Client; - var sEntMan = server.EntMan; - var cEntMan = client.EntMan; - var cTiming = client.ResolveDependency(); - var sTransform = server.System(); - var sHandsSystem = server.System(); - var cHandsSystem = client.System(); - var holding = server.System(); - var map = await pair.CreateTestMap(); - - var serverPlayer = pair.Player!.AttachedEntity!.Value; - EntityUid holderOne = default; - EntityUid target = default; - - await server.WaitPost(() => - { - sTransform.SetCoordinates(serverPlayer, map.GridCoords); - sEntMan.EnsureComponent(serverPlayer); - - holderOne = sEntMan.SpawnEntity(HolderPrototype, map.GridCoords); - target = sEntMan.SpawnEntity("MobHuman", map.GridCoords.Offset(new Vector2(0.5f, 0f))); - StartHold(sEntMan, holding, holderOne, target); - }); - - await pair.RunTicksSync(10); - await pair.SyncTicks(targetDelta: 1); - - var clientPlayer = EntityUid.Invalid; - var clientTarget = EntityUid.Invalid; - var clientHolderOne = EntityUid.Invalid; - await client.WaitAssertion(() => - { - clientPlayer = client.AttachedEntity!.Value; - clientTarget = ToClientEntity(sEntMan, cEntMan, target); - clientHolderOne = ToClientEntity(sEntMan, cEntMan, holderOne); - - var held = cEntMan.GetComponent(clientTarget); - Assert.Multiple(() => - { - Assert.That(HasFullHold(cEntMan, clientTarget), Is.False); - Assert.That(held.Holders, Has.Count.EqualTo(1)); - }); - }); - - await PressClientPullKey(client, cEntMan, cTiming, clientTarget); - - await client.WaitAssertion(() => - { - var held = cEntMan.GetComponent(clientTarget); - var hands = cEntMan.GetComponent(clientTarget); - - Assert.Multiple(() => - { - Assert.That(HasFullHold(cEntMan, clientTarget), Is.True); - Assert.That(held.Holders, Has.Count.EqualTo(2)); - Assert.That(CountBlockingVirtualHands(cEntMan, cHandsSystem, clientTarget, hands, clientHolderOne, clientPlayer), Is.EqualTo(hands.SortedHands.Count)); - Assert.That(VictimHandsUseHolderIcons(cEntMan, cHandsSystem, clientTarget, hands, clientHolderOne, clientPlayer), Is.True); - }); - }); - - await server.WaitAssertion(() => - { - var held = sEntMan.GetComponent(target); - Assert.Multiple(() => - { - Assert.That(HasFullHold(sEntMan, target), Is.False); - Assert.That(held.Holders, Has.Count.EqualTo(1)); - }); - }); - - await pair.RunTicksSync(10); - await pair.SyncTicks(targetDelta: 1); - - await server.WaitAssertion(() => - { - var held = sEntMan.GetComponent(target); - Assert.Multiple(() => - { - Assert.That(HasFullHold(sEntMan, target), Is.True); - Assert.That(held.Holders, Has.Count.EqualTo(2)); - }); - }); - - await client.WaitAssertion(() => - { - var held = cEntMan.GetComponent(clientTarget); - Assert.Multiple(() => - { - Assert.That(HasFullHold(cEntMan, clientTarget), Is.True); - Assert.That(held.Holders, Has.Count.EqualTo(2)); - }); - }); - - await pair.CleanReturnAsync(); - } - - [Test] - public async Task ClientAnchorReassignmentKeepsCustomDragAndReconcilesCleanly() - { - await using var pair = await PoolManager.GetServerClient(new PoolSettings - { - Fresh = true, - Connected = true, - DummyTicker = false, - }); - - var server = pair.Server; - var client = pair.Client; - var sEntMan = server.EntMan; - var cEntMan = client.EntMan; - var sPhysics = server.System(); - var cPhysics = client.System(); - var host = server.ResolveDependency(); - var sTransform = server.System(); - var cTransform = client.System(); - var holding = server.System(); - var map = await pair.CreateTestMap(); - - var serverPlayer = pair.Player!.AttachedEntity!.Value; - EntityUid holderTwo = default; - EntityUid target = default; - - await server.WaitPost(() => - { - sTransform.SetCoordinates(serverPlayer, map.GridCoords); - sEntMan.EnsureComponent(serverPlayer); - - target = sEntMan.SpawnEntity("MobHuman", map.GridCoords.Offset(new Vector2(0.95f, 0f))); - host.ExecuteCommand(null, $"addhand {sEntMan.GetNetEntity(target)}"); - - holderTwo = sEntMan.SpawnEntity(HolderPrototype, map.GridCoords.Offset(new Vector2(-0.5f, 0f))); - - StartHold(sEntMan, holding, serverPlayer, target); - StartHold(sEntMan, holding, holderTwo, target); - }); - - await pair.RunTicksSync(10); - await pair.SyncTicks(targetDelta: 1); - - await server.WaitAssertion(() => - { - var held = sEntMan.GetComponent(target); - var serverPlayerPuller = sEntMan.GetComponent(serverPlayer); - var holderTwoPuller = sEntMan.GetComponent(holderTwo); - var pullable = sEntMan.GetComponent(target); - var distance = GetDistance(sTransform, serverPlayer, target); - var contacts = sPhysics.GetContactingEntities(serverPlayer); - - Assert.Multiple(() => - { - Assert.That(HasFullHold(sEntMan, target), Is.False); - Assert.That(held.Holders, Is.EqualTo(new[] { serverPlayer, holderTwo })); - Assert.That(distance, Is.GreaterThan(0.18f)); - Assert.That(distance, Is.LessThan(0.5f)); - Assert.That(contacts, Does.Not.Contain(target)); - Assert.That(serverPlayerPuller.Pulling, Is.Null); - Assert.That(holderTwoPuller.Pulling, Is.Null); - Assert.That(pullable.Puller, Is.Null); - }); - }); - - var clientPlayer = EntityUid.Invalid; - var clientTarget = EntityUid.Invalid; - var clientHolderTwo = EntityUid.Invalid; - await client.WaitAssertion(() => - { - clientPlayer = client.AttachedEntity!.Value; - clientTarget = ToClientEntity(sEntMan, cEntMan, target); - clientHolderTwo = ToClientEntity(sEntMan, cEntMan, holderTwo); - - var held = cEntMan.GetComponent(clientTarget); - var playerPuller = cEntMan.GetComponent(clientPlayer); - var holderTwoPuller = cEntMan.GetComponent(clientHolderTwo); - var pullable = cEntMan.GetComponent(clientTarget); - var distance = GetDistance(cTransform, clientPlayer, clientTarget); - var contacts = cPhysics.GetContactingEntities(clientPlayer); - - Assert.Multiple(() => - { - Assert.That(HasFullHold(cEntMan, clientTarget), Is.False); - Assert.That(held.Holders, Is.EqualTo(new[] { clientPlayer, clientHolderTwo })); - Assert.That(distance, Is.GreaterThan(0.18f)); - Assert.That(distance, Is.LessThan(0.5f)); - Assert.That(contacts, Does.Not.Contain(clientTarget)); - Assert.That(playerPuller.Pulling, Is.Null); - Assert.That(holderTwoPuller.Pulling, Is.Null); - Assert.That(pullable.Puller, Is.Null); - }); - }); - - await server.WaitPost(() => - { - var holdComp = sEntMan.GetComponent(serverPlayer); - Assert.That(holding.TryToggleHold((serverPlayer, holdComp), target), Is.True); - }); - - await pair.RunTicksSync(5); - await pair.SyncTicks(targetDelta: 1); - - await server.WaitAssertion(() => - { - var held = sEntMan.GetComponent(target); - var pullable = sEntMan.GetComponent(target); - var holderTwoPuller = sEntMan.GetComponent(holderTwo); - var distance = GetDistance(sTransform, holderTwo, target); - var contacts = sPhysics.GetContactingEntities(holderTwo); - - Assert.Multiple(() => - { - Assert.That(held.Holders, Is.EqualTo(new[] { holderTwo })); - Assert.That(distance, Is.GreaterThan(0.18f)); - Assert.That(distance, Is.LessThan(0.55f)); - Assert.That(contacts, Does.Not.Contain(target)); - Assert.That(holderTwoPuller.Pulling, Is.Null); - Assert.That(pullable.Puller, Is.Null); - }); - }); - - await client.WaitAssertion(() => - { - var held = cEntMan.GetComponent(clientTarget); - var pullable = cEntMan.GetComponent(clientTarget); - var holderTwoPuller = cEntMan.GetComponent(clientHolderTwo); - var distance = GetDistance(cTransform, clientHolderTwo, clientTarget); - var contacts = cPhysics.GetContactingEntities(clientHolderTwo); - - Assert.Multiple(() => - { - Assert.That(HasFullHold(cEntMan, clientTarget), Is.False); - Assert.That(held.Holders, Is.EqualTo(new[] { clientHolderTwo })); - Assert.That(distance, Is.GreaterThan(0.18f)); - Assert.That(distance, Is.LessThan(0.7f)); - Assert.That(contacts, Does.Not.Contain(clientTarget)); - Assert.That(holderTwoPuller.Pulling, Is.Null); - Assert.That(pullable.Puller, Is.Null); - }); - }); - - await pair.CleanReturnAsync(); - } - - [Test] - public async Task ClientFullBreakoutAlertPredictsDoAfterAndReconciles() - { - await using var pair = await PoolManager.GetServerClient(new PoolSettings - { - Fresh = true, - Connected = true, - DummyTicker = false, - }); - - var server = pair.Server; - var client = pair.Client; - var sEntMan = server.EntMan; - var cEntMan = client.EntMan; - var timing = server.ResolveDependency(); - var sTransform = server.System(); - var holding = server.System(); - var map = await pair.CreateTestMap(); - - var serverPlayer = pair.Player!.AttachedEntity!.Value; - EntityUid holderOne = default; - EntityUid holderTwo = default; - - await server.WaitPost(() => - { - sTransform.SetCoordinates(serverPlayer, map.GridCoords); - holderOne = sEntMan.SpawnEntity(HolderPrototype, map.GridCoords.Offset(new Vector2(0.5f, 0f))); - holderTwo = sEntMan.SpawnEntity(HolderPrototype, map.GridCoords.Offset(new Vector2(-0.5f, 0f))); - StartHold(sEntMan, holding, holderOne, serverPlayer); - StartHold(sEntMan, holding, holderTwo, serverPlayer); - }); - - await pair.RunTicksSync(10); - await pair.SyncTicks(targetDelta: 1); - await pair.RunTicksSync(GetTickCount(timing, TimeSpan.FromSeconds(10))); - await pair.SyncTicks(targetDelta: 1); - await server.WaitAssertion(() => - { - var held = sEntMan.GetComponent(serverPlayer); - Assert.Multiple(() => - { - Assert.That(HasFullHold(sEntMan, serverPlayer), Is.True); - }); - }); - - var clientPlayer = EntityUid.Invalid; - var clientHolderOne = EntityUid.Invalid; - var clientHolderTwo = EntityUid.Invalid; - await client.WaitAssertion(() => - { - clientPlayer = client.AttachedEntity!.Value; - clientHolderOne = ToClientEntity(sEntMan, cEntMan, holderOne); - clientHolderTwo = ToClientEntity(sEntMan, cEntMan, holderTwo); - Assert.That(HasFullHold(cEntMan, clientPlayer), Is.True); - }); - - await client.WaitPost(() => - { - cEntMan.RaisePredictiveEvent(new ClickAlertEvent("ScpHoldGrabbed")); - - var held = cEntMan.GetComponent(clientPlayer); - Assert.Multiple(() => - { - Assert.That(HasBreakoutAttempt(cEntMan, clientPlayer), Is.True); - Assert.That(cEntMan.HasComponent(clientPlayer), Is.False); - Assert.That(CountAttachedPrototype(cEntMan, clientHolderOne, "WhistleExclamation"), Is.EqualTo(1)); - Assert.That(CountAttachedPrototype(cEntMan, clientHolderTwo, "WhistleExclamation"), Is.EqualTo(1)); - }); - }); - - await server.WaitAssertion(() => - { - var held = sEntMan.GetComponent(serverPlayer); - Assert.Multiple(() => - { - Assert.That(HasFullHold(sEntMan, serverPlayer), Is.True); - Assert.That(HasBreakoutAttempt(sEntMan, serverPlayer), Is.False); - }); - }); - - await pair.RunTicksSync(GetTickCount(timing, TimeSpan.FromSeconds(5)) + 5); - await pair.SyncTicks(targetDelta: 1); - - await server.WaitAssertion(() => - { - Assert.Multiple(() => - { - Assert.That(sEntMan.HasComponent(serverPlayer), Is.False); - Assert.That(sEntMan.HasComponent(serverPlayer), Is.True); - }); - }); - - await client.WaitAssertion(() => - { - Assert.Multiple(() => - { - Assert.That(cEntMan.HasComponent(clientPlayer), Is.False); - Assert.That(cEntMan.HasComponent(clientPlayer), Is.True); - }); - }); - - await pair.CleanReturnAsync(); - } - - [Test] - public async Task ClientGrabbedAlertPredictsSoftBreakout() - { - await using var pair = await PoolManager.GetServerClient(new PoolSettings - { - Fresh = true, - Connected = true, - DummyTicker = false, - }); - - var server = pair.Server; - var client = pair.Client; - var sEntMan = server.EntMan; - var cEntMan = client.EntMan; - var sTransform = server.System(); - var holding = server.System(); - var sAlerts = server.System(); - var cAlerts = client.System(); - var map = await pair.CreateTestMap(); - - var serverPlayer = pair.Player!.AttachedEntity!.Value; - EntityUid holder = default; - - await server.WaitPost(() => - { - sTransform.SetCoordinates(serverPlayer, map.GridCoords); - holder = sEntMan.SpawnEntity(HolderPrototype, map.GridCoords.Offset(new Vector2(0.1f, 0f))); - StartHold(sEntMan, holding, holder, serverPlayer); - }); - - await pair.RunTicksSync(10); - await pair.SyncTicks(targetDelta: 1); - - var clientPlayer = EntityUid.Invalid; - await client.WaitAssertion(() => - { - clientPlayer = client.AttachedEntity!.Value; - - Assert.Multiple(() => - { - Assert.That(cEntMan.HasComponent(clientPlayer), Is.True); - Assert.That(cAlerts.IsShowingAlert(clientPlayer, "ScpHoldGrabbed"), Is.True); - }); - }); - - await client.WaitPost(() => - { - cEntMan.RaisePredictiveEvent(new ClickAlertEvent("ScpHoldGrabbed")); - - Assert.Multiple(() => - { - Assert.That(cEntMan.HasComponent(clientPlayer), Is.False); - Assert.That(cAlerts.IsShowingAlert(clientPlayer, "ScpHoldGrabbed"), Is.False); - }); - }); - - await server.WaitAssertion(() => - { - Assert.That(sEntMan.HasComponent(serverPlayer), Is.True); - Assert.That(sAlerts.IsShowingAlert(serverPlayer, "ScpHoldGrabbed"), Is.True); - }); - - await pair.RunTicksSync(10); - await pair.SyncTicks(targetDelta: 1); - - await server.WaitAssertion(() => - { - Assert.That(sEntMan.HasComponent(serverPlayer), Is.False); - Assert.That(sAlerts.IsShowingAlert(serverPlayer, "ScpHoldGrabbed"), Is.False); - }); - - await client.WaitAssertion(() => - { - Assert.That(cEntMan.HasComponent(clientPlayer), Is.False); - Assert.That(cAlerts.IsShowingAlert(clientPlayer, "ScpHoldGrabbed"), Is.False); - }); - - await pair.CleanReturnAsync(); - } - - [Test] - public async Task ReleaseAndRangeLossReassignOrClearHold() - { - await using var pair = await PoolManager.GetServerClient(new PoolSettings { Fresh = true }); - var server = pair.Server; - var entMan = server.EntMan; - var host = server.ResolveDependency(); - var holding = server.System(); - var transform = server.System(); - var map = await pair.CreateTestMap(); - - EntityUid holderOne = default; - EntityUid holderTwo = default; - EntityUid target = default; - - await server.WaitPost(() => - { - holderOne = entMan.SpawnEntity(HolderPrototype, map.GridCoords); - holderTwo = entMan.SpawnEntity(HolderPrototype, map.GridCoords); - target = entMan.SpawnEntity("MobHuman", map.GridCoords); - - host.ExecuteCommand(null, $"addhand {entMan.GetNetEntity(target)}"); - }); - await server.WaitRunTicks(2); - - await server.WaitPost(() => - { - StartHold(entMan, holding, holderOne, target); - StartHold(entMan, holding, holderTwo, target); - }); - - await server.WaitAssertion(() => - { - var held = entMan.GetComponent(target); - var holderOnePuller = entMan.GetComponent(holderOne); - var holderTwoPuller = entMan.GetComponent(holderTwo); - var pullable = entMan.GetComponent(target); - - Assert.Multiple(() => - { - Assert.That(HasFullHold(entMan, target), Is.False); - Assert.That(held.Holders, Is.EqualTo(new[] { holderOne, holderTwo })); - Assert.That(holderOnePuller.Pulling, Is.Null); - Assert.That(holderTwoPuller.Pulling, Is.Null); - Assert.That(pullable.Puller, Is.Null); - }); - }); - - await server.WaitPost(() => - { - var holderComp = entMan.GetComponent(holderOne); - Assert.That(holding.TryToggleHold((holderOne, holderComp), target), Is.True); - }); - await server.WaitRunTicks(4); - - await server.WaitAssertion(() => - { - var held = entMan.GetComponent(target); - var holderOnePuller = entMan.GetComponent(holderOne); - var holderTwoPuller = entMan.GetComponent(holderTwo); - var pullable = entMan.GetComponent(target); - - Assert.Multiple(() => - { - Assert.That(HasFullHold(entMan, target), Is.False); - Assert.That(held.Holders, Is.EqualTo(new[] { holderTwo })); - Assert.That(holderOnePuller.Pulling, Is.Null); - Assert.That(holderTwoPuller.Pulling, Is.Null); - Assert.That(pullable.Puller, Is.Null); - }); - }); - - await server.WaitPost(() => - { - transform.SetCoordinates(holderTwo, map.GridCoords.Offset(new Vector2(10f, 0f))); - }); - await server.WaitRunTicks(2); - - await server.WaitAssertion(() => - { - Assert.That(entMan.HasComponent(target), Is.False); - }); - - await pair.CleanReturnAsync(); - } - - [Test] - public async Task SoftHoldTargetTeleportClearsStateOnServerAndClient() - { - await using var pair = await PoolManager.GetServerClient(new PoolSettings - { - Fresh = true, - Connected = true, - DummyTicker = false, - }); - - var server = pair.Server; - var client = pair.Client; - var sEntMan = server.EntMan; - var cEntMan = client.EntMan; - var holding = server.System(); - var sTransform = server.System(); - var sAlerts = server.System(); - var cAlerts = client.System(); - var sStatusEffects = server.System(); - var cStatusEffects = client.System(); - var sHandsSystem = server.System(); - var cHandsSystem = client.System(); - var map = await pair.CreateTestMap(); - - var serverPlayer = pair.Player!.AttachedEntity!.Value; - EntityUid holder = default; - - await server.WaitPost(() => - { - sTransform.SetCoordinates(serverPlayer, map.GridCoords); - holder = sEntMan.SpawnEntity(HolderPrototype, map.GridCoords.Offset(new Vector2(0.1f, 0f))); - StartHold(sEntMan, holding, holder, serverPlayer); - }); - - await pair.RunTicksSync(10); - await pair.SyncTicks(targetDelta: 1); - - await server.WaitAssertion(() => - { - var held = sEntMan.GetComponent(serverPlayer); - var holderState = sEntMan.GetComponent(holder); - var holderHands = sEntMan.GetComponent(holder); - - Assert.Multiple(() => - { - Assert.That(HasFullHold(sEntMan, serverPlayer), Is.False); - Assert.That(held.Holders, Has.Count.EqualTo(1)); - Assert.That(holderState.Target, Is.EqualTo(serverPlayer)); - Assert.That(CountHolderHandBlockers(sEntMan, sHandsSystem, holder, serverPlayer, holderHands), Is.EqualTo(1)); - Assert.That(CountHolderTargetVirtualItems(sEntMan, sHandsSystem, holder, serverPlayer, holderHands), Is.EqualTo(1)); - Assert.That(sStatusEffects.HasStatusEffect(serverPlayer, "StatusEffectScpHeld"), Is.True); - Assert.That(sAlerts.IsShowingAlert(serverPlayer, "ScpHoldGrabbed"), Is.True); - }); - }); - - var clientPlayer = EntityUid.Invalid; - var clientHolder = EntityUid.Invalid; - await client.WaitAssertion(() => - { - clientPlayer = client.AttachedEntity!.Value; - clientHolder = ToClientEntity(sEntMan, cEntMan, holder); - - var held = cEntMan.GetComponent(clientPlayer); - var holderState = cEntMan.GetComponent(clientHolder); - var holderHands = cEntMan.GetComponent(clientHolder); - - Assert.Multiple(() => - { - Assert.That(HasFullHold(cEntMan, clientPlayer), Is.False); - Assert.That(held.Holders, Has.Count.EqualTo(1)); - Assert.That(holderState.Target, Is.EqualTo(clientPlayer)); - Assert.That(CountHolderHandBlockers(cEntMan, cHandsSystem, clientHolder, clientPlayer, holderHands), Is.EqualTo(1)); - Assert.That(CountHolderTargetVirtualItems(cEntMan, cHandsSystem, clientHolder, clientPlayer, holderHands), Is.EqualTo(1)); - Assert.That(cStatusEffects.HasStatusEffect(clientPlayer, "StatusEffectScpHeld"), Is.True); - Assert.That(cAlerts.IsShowingAlert(clientPlayer, "ScpHoldGrabbed"), Is.True); - }); - }); - - await server.WaitPost(() => - { - sTransform.SetCoordinates(serverPlayer, map.GridCoords.Offset(new Vector2(10f, 0f))); - }); - - await pair.RunTicksSync(10); - await pair.SyncTicks(targetDelta: 1); - - await server.WaitAssertion(() => - { - var holderHands = sEntMan.GetComponent(holder); - - Assert.Multiple(() => - { - Assert.That(sEntMan.HasComponent(serverPlayer), Is.False); - Assert.That(sEntMan.HasComponent(serverPlayer), Is.False); - Assert.That(sEntMan.HasComponent(holder), Is.False); - Assert.That(CountHolderHandBlockers(sEntMan, sHandsSystem, holder, serverPlayer, holderHands), Is.EqualTo(0)); - Assert.That(CountHolderTargetVirtualItems(sEntMan, sHandsSystem, holder, serverPlayer, holderHands), Is.EqualTo(0)); - Assert.That(sStatusEffects.HasStatusEffect(serverPlayer, "StatusEffectScpHeld"), Is.False); - Assert.That(sAlerts.IsShowingAlert(serverPlayer, "ScpHoldGrabbed"), Is.False); - }); - }); - - await client.WaitAssertion(() => - { - var holderHands = cEntMan.GetComponent(clientHolder); - - Assert.Multiple(() => - { - Assert.That(cEntMan.HasComponent(clientPlayer), Is.False); - Assert.That(cEntMan.HasComponent(clientPlayer), Is.False); - Assert.That(cEntMan.HasComponent(clientHolder), Is.False); - Assert.That(CountHolderHandBlockers(cEntMan, cHandsSystem, clientHolder, clientPlayer, holderHands), Is.EqualTo(0)); - Assert.That(CountHolderTargetVirtualItems(cEntMan, cHandsSystem, clientHolder, clientPlayer, holderHands), Is.EqualTo(0)); - Assert.That(cStatusEffects.HasStatusEffect(clientPlayer, "StatusEffectScpHeld"), Is.False); - Assert.That(cAlerts.IsShowingAlert(clientPlayer, "ScpHoldGrabbed"), Is.False); - }); - }); - - await pair.CleanReturnAsync(); - } - - // Fire added start - verify hold disables combat mode consistently - [Test] - public async Task ConnectedTargetHeldWithCombatModeEnabled_DisablesCombatModeAndCombatAction() - { - await using var pair = await PoolManager.GetServerClient(new PoolSettings - { - Fresh = true, - Connected = true, - DummyTicker = false, - }); - - var server = pair.Server; - var client = pair.Client; - var sEntMan = server.EntMan; - var cEntMan = client.EntMan; - var sTransform = server.System(); - var sCombatMode = server.System(); - var holding = server.System(); - var map = await pair.CreateTestMap(); - - var serverPlayer = pair.Player!.AttachedEntity!.Value; - EntityUid clientPlayer = default; - EntityUid holder = default; - EntityUid serverCombatAction = default; - EntityUid clientCombatAction = default; - - await server.WaitPost(() => - { - sTransform.SetCoordinates(serverPlayer, map.GridCoords.Offset(new Vector2(0.1f, 0f))); - holder = sEntMan.SpawnEntity(HolderPrototype, map.GridCoords); - - var combat = sEntMan.GetComponent(serverPlayer); - sCombatMode.SetInCombatMode(serverPlayer, true, combat); - serverCombatAction = GetCombatToggleAction(sEntMan, serverPlayer); - }); - - await pair.RunTicksSync(5); - await pair.SyncTicks(targetDelta: 1); - - await server.WaitAssertion(() => - { - Assert.Multiple(() => - { - Assert.That(IsInCombatMode(sEntMan, serverPlayer), Is.True); - Assert.That(IsActionToggled(sEntMan, serverCombatAction), Is.True); - }); - }); - - await client.WaitAssertion(() => - { - clientPlayer = client.AttachedEntity!.Value; - clientCombatAction = ToClientEntity(sEntMan, cEntMan, serverCombatAction); - - Assert.Multiple(() => - { - Assert.That(IsInCombatMode(cEntMan, clientPlayer), Is.True); - Assert.That(IsActionToggled(cEntMan, clientCombatAction), Is.True); - }); - }); - - await server.WaitPost(() => StartHold(sEntMan, holding, holder, serverPlayer)); - await pair.RunTicksSync(5); - await pair.SyncTicks(targetDelta: 1); - - await server.WaitAssertion(() => - { - Assert.Multiple(() => - { - Assert.That(sEntMan.HasComponent(serverPlayer), Is.True); - Assert.That(IsInCombatMode(sEntMan, serverPlayer), Is.False); - Assert.That(IsActionToggled(sEntMan, serverCombatAction), Is.False); - }); - }); - - await client.WaitAssertion(() => - { - clientCombatAction = ToClientEntity(sEntMan, cEntMan, serverCombatAction); - - Assert.Multiple(() => - { - Assert.That(cEntMan.HasComponent(clientPlayer), Is.True); - Assert.That(IsInCombatMode(cEntMan, clientPlayer), Is.False); - Assert.That(IsActionToggled(cEntMan, clientCombatAction), Is.False); - }); - }); - - await pair.CleanReturnAsync(); - } - // Fire added end - - private static int CountBlockingVirtualHands(IEntityManager entMan, SharedHandsSystem handsSystem, EntityUid uid, HandsComponent hands, params EntityUid[] holders) - { - var expected = holders.ToHashSet(); - - return handsSystem.EnumerateHeld((uid, hands)).Count(item => - entMan.TryGetComponent(item, out VirtualItemComponent? virtualItem) && - entMan.HasComponent(item) && - expected.Contains(virtualItem.BlockingEntity) && - entMan.HasComponent(item)); - } - - private static EntityUid[] GetHeldHandBlockers(IEntityManager entMan, SharedHandsSystem handsSystem, EntityUid uid, HandsComponent hands, params EntityUid[] holders) - { - var expected = holders.ToHashSet(); - - return handsSystem.EnumerateHeld((uid, hands)) - .Where(item => - entMan.TryGetComponent(item, out VirtualItemComponent? virtualItem) && - entMan.HasComponent(item) && - expected.Contains(virtualItem.BlockingEntity) && - entMan.HasComponent(item)) - .Order() - .ToArray(); - } - - private static bool HasFullHold(IEntityManager entMan, EntityUid uid) - { - return entMan.HasComponent(uid); - } - - private static bool HasBreakoutAttempt(IEntityManager entMan, EntityUid uid) - { - return entMan.HasComponent(uid); - } - - private static bool HasHolderSlowdown(IEntityManager entMan, EntityUid uid) - { - return entMan.HasComponent(uid); - } - - // Fire added start - verify hold disables combat mode consistently - private static EntityUid GetCombatToggleAction(IEntityManager entMan, EntityUid uid) - { - var combat = entMan.GetComponent(uid); - - Assert.That(combat.CombatToggleActionEntity, Is.Not.Null); - return combat.CombatToggleActionEntity!.Value; - } - - private static bool IsInCombatMode(IEntityManager entMan, EntityUid uid) - { - return entMan.GetComponent(uid).IsInCombatMode; - } - - private static bool IsActionToggled(IEntityManager entMan, EntityUid uid) - { - return entMan.GetComponent(uid).Toggled; - } - // Fire added end - - private static bool VictimHandsUseHolderIcons(IEntityManager entMan, SharedHandsSystem handsSystem, EntityUid uid, HandsComponent hands, params EntityUid[] holders) - { - var expected = holders.ToHashSet(); - var count = 0; - - foreach (var item in handsSystem.EnumerateHeld((uid, hands))) - { - if (!entMan.TryGetComponent(item, out VirtualItemComponent? virtualItem) || - !entMan.HasComponent(item) || - !entMan.HasComponent(item) || - !expected.Contains(virtualItem.BlockingEntity)) - { - return false; - } - - count++; - } - - return count == hands.SortedHands.Count; - } - - private static int CountAttachedPrototype(IEntityManager entMan, EntityUid parent, string prototypeId) - { - var count = 0; - var enumerator = entMan.GetComponent(parent).ChildEnumerator; - - while (enumerator.MoveNext(out var child)) - { - if (entMan.TryGetComponent(child, out MetaDataComponent? metadata) && - !metadata.Deleted && - metadata.EntityPrototype?.ID == prototypeId) - { - count++; - } - } - - return count; - } - - private static int CountPrototypeEntities(IEntityManager entMan, string prototypeId) - { - var count = 0; - var query = entMan.AllEntityQueryEnumerator(); - - while (query.MoveNext(out _, out var metadata)) - { - if (!metadata.Deleted && - metadata.EntityPrototype?.ID == prototypeId) - { - count++; - } - } - - return count; - } - - private static int CountHolderHandBlockers(IEntityManager entMan, SharedHandsSystem handsSystem, EntityUid holder, EntityUid target, HandsComponent hands) - { - return handsSystem.EnumerateHeld((holder, hands)).Count(item => - entMan.TryGetComponent(item, out VirtualItemComponent? virtualItem) && - entMan.HasComponent(item) && - virtualItem.BlockingEntity == target); - } - - private static int CountHolderTargetVirtualItems(IEntityManager entMan, SharedHandsSystem handsSystem, EntityUid holder, EntityUid target, HandsComponent hands) - { - return handsSystem.EnumerateHeld((holder, hands)).Count(item => - entMan.TryGetComponent(item, out VirtualItemComponent? virtualItem) && - virtualItem.BlockingEntity == target); - } - - private static string DescribeHolderTargetVirtualItems(IEntityManager entMan, SharedHandsSystem handsSystem, EntityUid holder, EntityUid target, HandsComponent hands) - { - var items = handsSystem.EnumerateHeld((holder, hands)) - .Where(item => - entMan.TryGetComponent(item, out VirtualItemComponent? virtualItem) && - virtualItem.BlockingEntity == target) - .Select(item => - $"{item}:client={entMan.GetComponent(item).NetEntity.IsClientSide()},marker={entMan.HasComponent(item)}"); - - return string.Join(", ", items); - } - - private static EntityUid FindHolderHandBlocker(IEntityManager entMan, SharedHandsSystem handsSystem, EntityUid holder, EntityUid target, HandsComponent hands) - { - foreach (var item in handsSystem.EnumerateHeld((holder, hands))) - { - if (entMan.TryGetComponent(item, out VirtualItemComponent? virtualItem) && - entMan.HasComponent(item) && - virtualItem.BlockingEntity == target) - { - return item; - } - } - - return EntityUid.Invalid; - } - - private static float GetDistance(SharedTransformSystem transform, EntityUid first, EntityUid second) - { - return Vector2.Distance( - transform.GetMapCoordinates(first).Position, - transform.GetMapCoordinates(second).Position); - } - - private static float GetLargestDistanceStep(float[] samples) - { - var largest = 0f; - - for (var i = 1; i < samples.Length; i++) - { - largest = Math.Max(largest, MathF.Abs(samples[i] - samples[i - 1])); - } - - return largest; - } - - private static int GetTickCount(IGameTiming timing, TimeSpan duration) - { - return Math.Max(1, (int)Math.Ceiling(duration.TotalSeconds / timing.TickPeriod.TotalSeconds) + 1); - } - - private static TimeSpan GetHoldCooldownRemaining(IEntityManager entMan, EntityUid holder, IGameTiming timing) - { - if (!entMan.TryGetComponent(holder, out ScpHolderComponent? holdComp) || - holdComp.HoldAvailableAt is not { } cooldownEnd || - cooldownEnd <= timing.CurTime) - { - return TimeSpan.Zero; - } - - return cooldownEnd - timing.CurTime; - } - - private static async Task PressClientPullKey( - RobustIntegrationTest.ClientIntegrationInstance client, - IEntityManager entMan, - IGameTiming timing, - EntityUid cursorEntity) - { - await SendClientPullInput(client, entMan, timing, cursorEntity, BoundKeyState.Down); - await SendClientPullInput(client, entMan, timing, cursorEntity, BoundKeyState.Up); - } - - private static async Task PressClientDropKey( - RobustIntegrationTest.ClientIntegrationInstance client, - IEntityManager entMan, - IGameTiming timing, - EntityUid cursorEntity) - { - await SendClientDropInput(client, entMan, timing, cursorEntity, BoundKeyState.Down); - await SendClientDropInput(client, entMan, timing, cursorEntity, BoundKeyState.Up); - } - - private static async Task SendClientPullInput( - RobustIntegrationTest.ClientIntegrationInstance client, - IEntityManager entMan, - IGameTiming timing, - EntityUid cursorEntity, - BoundKeyState state) - { - var inputManager = client.ResolveDependency(); - var funcId = inputManager.NetworkBindMap.KeyFunctionID(ContentKeyFunctions.TryPullObject); - var transform = entMan.GetComponent(cursorEntity); - var inputSystem = client.System(); - var message = new ClientFullInputCmdMessage(timing.CurTick, timing.TickFraction, funcId) - { - State = state, - Coordinates = transform.Coordinates, - Uid = cursorEntity, - }; - - await client.WaitPost(() => inputSystem.HandleInputCommand(client.Session!, ContentKeyFunctions.TryPullObject, message)); - } - - private static async Task SendClientDropInput( - RobustIntegrationTest.ClientIntegrationInstance client, - IEntityManager entMan, - IGameTiming timing, - EntityUid cursorEntity, - BoundKeyState state) - { - var inputManager = client.ResolveDependency(); - var funcId = inputManager.NetworkBindMap.KeyFunctionID(ContentKeyFunctions.Drop); - var transform = entMan.GetComponent(cursorEntity); - var inputSystem = client.System(); - var message = new ClientFullInputCmdMessage(timing.CurTick, timing.TickFraction, funcId) - { - State = state, - Coordinates = transform.Coordinates, - Uid = cursorEntity, - }; - - await client.WaitPost(() => inputSystem.HandleInputCommand(client.Session!, ContentKeyFunctions.Drop, message)); - } - - private static void SetSoftEscapeAvailableAt(ActiveScpHoldableComponent held, TimeSpan value) - { - SoftEscapeAvailableAtField.SetValue(held, value); - } - - private static void SetHolderTarget(ActiveScpHolderComponent holder, EntityUid? value) - { - ActiveScpHolderTargetField.SetValue(holder, value); - } - - private static void RaiseMoveInput(IEntityManager entMan, EntityUid uid) - { - var mover = entMan.GetComponent(uid); - var move = new MoveInputEvent((uid, mover), MoveButtons.None, Direction.East, true); - entMan.EventBus.RaiseLocalEvent(uid, ref move); - } - - private static void StartHold(IEntityManager entMan, SharedScpHoldingSystem holding, EntityUid holder, EntityUid target) - { - var holdComp = entMan.GetComponent(holder); - Assert.That(holding.TryToggleHold((holder, holdComp), target), Is.True); - } - - private static EntityUid ToClientEntity(IEntityManager serverEntMan, IEntityManager clientEntMan, EntityUid serverEntity) - { - return clientEntMan.GetEntity(serverEntMan.GetNetEntity(serverEntity)); - } -} - -[RegisterComponent] -public sealed partial class ScpHoldAttemptCancelTestComponent : Component; - -public sealed class ScpHoldAttemptListenerSystem : TestListenerSystem; - -public sealed class ScpHoldBreakoutListenerSystem : TestListenerSystem; - -public sealed class ScpHoldAttemptCancelSystem : EntitySystem -{ - public override void Initialize() - { - SubscribeLocalEvent(OnAttempt); - } - - private static void OnAttempt(Entity ent, ref ScpHoldAttemptEvent args) - { - args.Cancelled = true; - } -} diff --git a/Content.IntegrationTests/Tests/_Scp/ScpHoldingTwoClientCombatTest.cs b/Content.IntegrationTests/Tests/_Scp/ScpHoldingTwoClientCombatTest.cs deleted file mode 100644 index ec5478c532f..00000000000 --- a/Content.IntegrationTests/Tests/_Scp/ScpHoldingTwoClientCombatTest.cs +++ /dev/null @@ -1,516 +0,0 @@ -#nullable enable -using System.Numerics; -using Content.Client.Actions; -using Content.Client.Gameplay; -using Content.Client.IoC; -using Content.Client.Parallax.Managers; -using Content.IntegrationTests._Sunrise; -using Content.Server.Mind; -using Content.Shared._Scp.Holding.Components; -using Content.Shared._Scp.Holding.Systems; -using Content.Shared.Actions.Components; -using Content.Shared.CombatMode; -using Content.Shared.Input; -using Content.Shared.Movement.Components; -using Content.Shared.Movement.Events; -using Content.Shared.Movement.Systems; -using Content.Shared.Players; -using Content.Shared.Weapons.Melee.Events; -using Content.Shared.Weapons.Melee; -using Robust.Client.Audio.Midi; -using Robust.Client.GameObjects; -using Robust.Client.Input; -using Robust.Client.State; -using Robust.Server.Player; -using Robust.Shared.ContentPack; -using Robust.Shared.GameObjects; -using Robust.Shared.Input; -using Robust.Shared.IoC; -using Robust.Shared.Map; -using Robust.Shared.Maths; -using Robust.Shared.Network; -using Robust.Shared.Player; -using Robust.Shared.Timing; -using Robust.UnitTesting; - -namespace Content.IntegrationTests.Tests._Scp; - -[TestFixture] -public sealed class ScpHoldingTwoClientCombatTest -{ - [Test] - public async Task TwoClients_TargetClientHeldAfterOwnCombatToggle_DisablesCombatModeForTargetClient() - { - using var server = new RobustIntegrationTest.ServerIntegrationInstance(CreateServerOptions()); - using var targetClient = new RobustIntegrationTest.ClientIntegrationInstance(CreateClientOptions()); - using var holderClient = new RobustIntegrationTest.ClientIntegrationInstance(CreateClientOptions()); - - await Task.WhenAll(server.WaitIdleAsync(), targetClient.WaitIdleAsync(), holderClient.WaitIdleAsync()); - - await targetClient.Connect(server); - await holderClient.Connect(server); - await RunTicks(server, targetClient, holderClient, 10); - - var sEntMan = server.EntMan; - var targetEntMan = targetClient.EntMan; - var holderEntMan = holderClient.EntMan; - var targetTiming = targetClient.ResolveDependency(); - var holderTiming = holderClient.ResolveDependency(); - var targetActions = targetClient.System(); - var sTransform = server.System(); - var sCombatMode = server.System(); - var sPlayerMan = server.ResolveDependency(); - var targetState = targetClient.ResolveDependency(); - var holderState = holderClient.ResolveDependency(); - - Assert.That(targetClient.User, Is.Not.Null); - Assert.That(holderClient.User, Is.Not.Null); - - var targetSession = sPlayerMan.GetSessionById(targetClient.User!.Value); - var holderSession = sPlayerMan.GetSessionById(holderClient.User!.Value); - - EntityUid sTarget = default; - EntityUid sHolder = default; - EntityUid sCombatAction = default; - - await targetClient.WaitPost(() => targetState.RequestStateChange()); - await holderClient.WaitPost(() => holderState.RequestStateChange()); - await RunTicks(server, targetClient, holderClient, 20); - - await server.WaitAssertion(() => - { - Assert.Multiple(() => - { - Assert.That(targetSession.AttachedEntity, Is.Not.Null); - Assert.That(holderSession.AttachedEntity, Is.Not.Null); - }); - - sTarget = targetSession.AttachedEntity!.Value; - sHolder = holderSession.AttachedEntity!.Value; - sEntMan.EnsureComponent(sHolder); - - var targetCoords = sEntMan.GetComponent(sTarget).Coordinates; - sTransform.SetCoordinates(sHolder, new EntityCoordinates(targetCoords.EntityId, targetCoords.Position + new Vector2(0.1f, 0f))); - - sCombatAction = GetCombatToggleAction(sEntMan, sTarget); - }); - await RunTicks(server, targetClient, holderClient, 10); - - EntityUid cTargetOnTargetClient = default; - EntityUid cCombatActionOnTargetClient = default; - EntityUid cHolderOnTargetClient = default; - EntityUid cTargetOnHolderClient = default; - EntityUid cHolderOnHolderClient = default; - - await targetClient.WaitAssertion(() => - { - cTargetOnTargetClient = ToClientEntity(sEntMan, targetEntMan, sTarget); - cCombatActionOnTargetClient = ToClientEntity(sEntMan, targetEntMan, sCombatAction); - cHolderOnTargetClient = ToClientEntity(sEntMan, targetEntMan, sHolder); - - Assert.Multiple(() => - { - Assert.That(targetClient.AttachedEntity, Is.EqualTo(cTargetOnTargetClient)); - Assert.That(targetEntMan.EntityExists(cTargetOnTargetClient), Is.True); - Assert.That(targetEntMan.EntityExists(cCombatActionOnTargetClient), Is.True); - Assert.That(targetEntMan.EntityExists(cHolderOnTargetClient), Is.True); - }); - }); - - await holderClient.WaitAssertion(() => - { - cTargetOnHolderClient = ToClientEntity(sEntMan, holderEntMan, sTarget); - cHolderOnHolderClient = ToClientEntity(sEntMan, holderEntMan, sHolder); - - Assert.Multiple(() => - { - Assert.That(holderClient.AttachedEntity, Is.EqualTo(cHolderOnHolderClient)); - Assert.That(holderEntMan.EntityExists(cTargetOnHolderClient), Is.True); - }); - }); - - await targetClient.WaitPost(() => - { - var action = targetEntMan.GetComponent(cCombatActionOnTargetClient); - targetActions.TriggerAction((cCombatActionOnTargetClient, action)); - }); - await RunTicks(server, targetClient, holderClient, 10); - - await server.WaitAssertion(() => - { - Assert.Multiple(() => - { - Assert.That(IsInCombatMode(sEntMan, sTarget), Is.True); - Assert.That(IsActionToggled(sEntMan, sCombatAction), Is.True); - }); - }); - - await targetClient.WaitAssertion(() => - { - Assert.Multiple(() => - { - Assert.That(IsInCombatMode(targetEntMan, cTargetOnTargetClient), Is.True); - Assert.That(IsActionToggled(targetEntMan, cCombatActionOnTargetClient), Is.True); - }); - }); - - await SendClientPullInput(holderClient, holderEntMan, holderTiming, cTargetOnHolderClient, BoundKeyState.Down); - await RunTicks(server, targetClient, holderClient, 2); - await SendClientPullInput(holderClient, holderEntMan, holderTiming, cTargetOnHolderClient, BoundKeyState.Up); - await RunTicks(server, targetClient, holderClient, 15); - - await server.WaitAssertion(() => - { - Assert.Multiple(() => - { - Assert.That(sEntMan.HasComponent(sTarget), Is.True); - Assert.That(IsInCombatMode(sEntMan, sTarget), Is.False); - Assert.That(IsActionToggled(sEntMan, sCombatAction), Is.False); - Assert.That(IsActionEnabled(sEntMan, sCombatAction), Is.False); - }); - }); - - await RunTicks(server, targetClient, holderClient, 1); - - await targetClient.WaitPost(() => - { - cCombatActionOnTargetClient = ToClientEntity(sEntMan, targetEntMan, sCombatAction); - - Assert.Multiple(() => - { - Assert.That(IsInCombatMode(targetEntMan, cTargetOnTargetClient), Is.False); - Assert.That(IsActionToggled(targetEntMan, cCombatActionOnTargetClient), Is.False); - Assert.That(IsActionEnabled(targetEntMan, cCombatActionOnTargetClient), Is.False); - }); - }); - - EntityUid sWeapon = default; - TimeSpan weaponCooldownBeforeAttack = default; - - await server.WaitPost(() => - { - var meleeSystem = server.System(); - Assert.That(meleeSystem.TryGetWeapon(sTarget, out sWeapon, out var melee), Is.True); - Assert.That(melee, Is.Not.Null); - weaponCooldownBeforeAttack = melee!.NextAttack; - }); - - await targetClient.WaitPost(() => - { - var cWeaponOnTargetClient = ToClientEntity(sEntMan, targetEntMan, sWeapon); - var targetCoords = targetEntMan.GetComponent(cHolderOnTargetClient).Coordinates; - - targetEntMan.RaisePredictiveEvent(new LightAttackEvent( - targetEntMan.GetNetEntity(cHolderOnTargetClient), - targetEntMan.GetNetEntity(cWeaponOnTargetClient), - targetEntMan.GetNetCoordinates(targetCoords))); - }); - await RunTicks(server, targetClient, holderClient, 3); - - await server.WaitAssertion(() => - { - var melee = sEntMan.GetComponent(sWeapon); - - Assert.Multiple(() => - { - Assert.That(IsInCombatMode(sEntMan, sTarget), Is.False); - Assert.That(melee.NextAttack, Is.EqualTo(weaponCooldownBeforeAttack)); - }); - }); - - await holderClient.WaitAssertion(() => - { - var cCombatActionOnHolderClient = ToClientEntity(sEntMan, holderEntMan, sCombatAction); - - Assert.Multiple(() => - { - Assert.That(holderEntMan.HasComponent(cTargetOnHolderClient), Is.True); - Assert.That(IsInCombatMode(holderEntMan, cTargetOnHolderClient), Is.False); - Assert.That(IsActionToggled(holderEntMan, cCombatActionOnHolderClient), Is.False); - Assert.That(IsActionEnabled(holderEntMan, cCombatActionOnHolderClient), Is.False); - }); - }); - - await targetClient.WaitAssertion(() => - { - cCombatActionOnTargetClient = ToClientEntity(sEntMan, targetEntMan, sCombatAction); - - Assert.Multiple(() => - { - Assert.That(targetEntMan.HasComponent(cTargetOnTargetClient), Is.True); - Assert.That(IsInCombatMode(targetEntMan, cTargetOnTargetClient), Is.False); - Assert.That(IsActionToggled(targetEntMan, cCombatActionOnTargetClient), Is.False); - Assert.That(IsActionEnabled(targetEntMan, cCombatActionOnTargetClient), Is.False); - }); - }); - } - - [Test] - public async Task VisitingHeldTargetAfterBreakout_ReEnablesCombatAction() - { - using var server = new RobustIntegrationTest.ServerIntegrationInstance(CreateServerOptions()); - using var client = new RobustIntegrationTest.ClientIntegrationInstance(CreateClientOptions()); - - await Task.WhenAll(server.WaitIdleAsync(), client.WaitIdleAsync()); - - await client.Connect(server); - await RunTicks(server, client, 10); - - var sEntMan = server.EntMan; - var cEntMan = client.EntMan; - var sHolding = server.System(); - var sMind = server.System(); - var sPlayerMan = server.ResolveDependency(); - var cState = client.ResolveDependency(); - var cActions = client.System(); - - Assert.That(client.User, Is.Not.Null); - var session = sPlayerMan.GetSessionById(client.User!.Value); - - EntityUid sHolder = default; - EntityUid sTarget = default; - EntityUid sCombatAction = default; - EntityUid sMindId = default; - - await client.WaitPost(() => cState.RequestStateChange()); - await RunTicks(server, client, 20); - - await server.WaitAssertion(() => - { - Assert.Multiple(() => - { - Assert.That(session.AttachedEntity, Is.Not.Null); - Assert.That(session.ContentData()?.Mind, Is.Not.Null); - }); - - sHolder = session.AttachedEntity!.Value; - sMindId = session.ContentData()!.Mind!.Value; - - sEntMan.EnsureComponent(sHolder); - - var holderCoords = sEntMan.GetComponent(sHolder).Coordinates; - sTarget = sEntMan.SpawnEntity("MobHuman", holderCoords.Offset(new Vector2(0.1f, 0f))); - sCombatAction = GetCombatToggleAction(sEntMan, sTarget); - - var holder = sEntMan.GetComponent(sHolder); - Assert.That(sHolding.TryToggleHold((sHolder, holder), sTarget), Is.True); - }); - await RunTicks(server, client, 10); - - EntityUid cTarget = default; - EntityUid cCombatAction = default; - - await client.WaitAssertion(() => - { - cTarget = ToClientEntity(sEntMan, cEntMan, sTarget); - cCombatAction = ToClientEntity(sEntMan, cEntMan, sCombatAction); - - Assert.Multiple(() => - { - Assert.That(cEntMan.HasComponent(cTarget), Is.True); - Assert.That(IsActionEnabled(cEntMan, cCombatAction), Is.False); - }); - }); - - await server.WaitPost(() => sMind.Visit(sMindId, sTarget)); - await RunTicks(server, client, 10); - - await client.WaitAssertion(() => - { - cTarget = ToClientEntity(sEntMan, cEntMan, sTarget); - cCombatAction = ToClientEntity(sEntMan, cEntMan, sCombatAction); - - Assert.Multiple(() => - { - Assert.That(client.AttachedEntity, Is.EqualTo(cTarget)); - Assert.That(cEntMan.HasComponent(cTarget), Is.True); - Assert.That(IsActionEnabled(cEntMan, cCombatAction), Is.False); - }); - }); - - await server.WaitPost(() => RaiseMoveInput(sEntMan, sTarget)); - await RunTicks(server, client, 10); - - await server.WaitAssertion(() => - { - Assert.Multiple(() => - { - Assert.That(sEntMan.HasComponent(sTarget), Is.False); - Assert.That(IsActionEnabled(sEntMan, sCombatAction), Is.True); - }); - }); - - await client.WaitAssertion(() => - { - cTarget = ToClientEntity(sEntMan, cEntMan, sTarget); - cCombatAction = ToClientEntity(sEntMan, cEntMan, sCombatAction); - - Assert.Multiple(() => - { - Assert.That(client.AttachedEntity, Is.EqualTo(cTarget)); - Assert.That(cEntMan.HasComponent(cTarget), Is.False); - Assert.That(IsActionEnabled(cEntMan, cCombatAction), Is.True); - }); - }); - - await client.WaitPost(() => - { - var action = cEntMan.GetComponent(cCombatAction); - cActions.TriggerAction((cCombatAction, action)); - }); - await RunTicks(server, client, 10); - - await server.WaitAssertion(() => - { - Assert.Multiple(() => - { - Assert.That(IsInCombatMode(sEntMan, sTarget), Is.True); - Assert.That(IsActionToggled(sEntMan, sCombatAction), Is.True); - }); - }); - - await client.WaitAssertion(() => - { - Assert.Multiple(() => - { - Assert.That(IsInCombatMode(cEntMan, cTarget), Is.True); - Assert.That(IsActionToggled(cEntMan, cCombatAction), Is.True); - }); - }); - } - - private static RobustIntegrationTest.ServerIntegrationOptions CreateServerOptions() - { - return new RobustIntegrationTest.ServerIntegrationOptions - { - Pool = false, - ContentStart = true, - LoadTestAssembly = false, - ContentAssemblies = - [ - typeof(Shared.Entry.EntryPoint).Assembly, - typeof(Server.Entry.EntryPoint).Assembly - ], - Options = new() - { - LoadConfigAndUserData = false, - }, - }; - } - - private static RobustIntegrationTest.ClientIntegrationOptions CreateClientOptions() - { - var opts = new RobustIntegrationTest.ClientIntegrationOptions - { - Pool = false, - ContentStart = true, - LoadTestAssembly = false, - ContentAssemblies = - [ - typeof(Shared.Entry.EntryPoint).Assembly, - typeof(Client.Entry.EntryPoint).Assembly - ], - Options = new() - { - LoadConfigAndUserData = false, - }, - }; - - opts.InitIoC = () => - { - IoCManager.Register(true); - }; - - opts.BeforeStart += () => - { - IoCManager.Resolve().SetModuleBaseCallbacks(new ClientModuleTestingCallbacks - { - ClientBeforeIoC = () => IoCManager.Register(true) - }); - }; - - return opts; - } - - private static async Task RunTicks( - RobustIntegrationTest.ServerIntegrationInstance server, - RobustIntegrationTest.ClientIntegrationInstance targetClient, - RobustIntegrationTest.ClientIntegrationInstance holderClient, - int ticks) - { - for (var i = 0; i < ticks; i++) - { - await server.WaitRunTicks(1); - await targetClient.WaitRunTicks(1); - await holderClient.WaitRunTicks(1); - } - } - - private static async Task RunTicks( - RobustIntegrationTest.ServerIntegrationInstance server, - RobustIntegrationTest.ClientIntegrationInstance client, - int ticks) - { - for (var i = 0; i < ticks; i++) - { - await server.WaitRunTicks(1); - await client.WaitRunTicks(1); - } - } - - private static EntityUid GetCombatToggleAction(IEntityManager entMan, EntityUid uid) - { - var combat = entMan.GetComponent(uid); - - Assert.That(combat.CombatToggleActionEntity, Is.Not.Null); - return combat.CombatToggleActionEntity!.Value; - } - - private static bool IsInCombatMode(IEntityManager entMan, EntityUid uid) - { - return entMan.GetComponent(uid).IsInCombatMode; - } - - private static bool IsActionToggled(IEntityManager entMan, EntityUid uid) - { - return entMan.GetComponent(uid).Toggled; - } - - private static bool IsActionEnabled(IEntityManager entMan, EntityUid uid) - { - return entMan.GetComponent(uid).Enabled; - } - - private static EntityUid ToClientEntity(IEntityManager serverEntMan, IEntityManager clientEntMan, EntityUid serverEntity) - { - return clientEntMan.GetEntity(serverEntMan.GetNetEntity(serverEntity)); - } - - private static async Task SendClientPullInput( - RobustIntegrationTest.ClientIntegrationInstance client, - IEntityManager entMan, - IGameTiming timing, - EntityUid cursorEntity, - BoundKeyState state) - { - var inputManager = client.ResolveDependency(); - var funcId = inputManager.NetworkBindMap.KeyFunctionID(ContentKeyFunctions.TryPullObject); - var transform = entMan.GetComponent(cursorEntity); - var inputSystem = client.System(); - var message = new ClientFullInputCmdMessage(timing.CurTick, timing.TickFraction, funcId) - { - State = state, - Coordinates = transform.Coordinates, - Uid = cursorEntity, - }; - - await client.WaitPost(() => inputSystem.HandleInputCommand(client.Session!, ContentKeyFunctions.TryPullObject, message)); - } - - private static void RaiseMoveInput(IEntityManager entMan, EntityUid uid) - { - var mover = entMan.GetComponent(uid); - var move = new MoveInputEvent((uid, mover), MoveButtons.None, Direction.East, true); - entMan.EventBus.RaiseLocalEvent(uid, ref move); - } -} diff --git a/Content.Shared/_Scp/Holding/Components/ScpHoldRestrictedComponent.cs b/Content.Shared/_Scp/Holding/Components/ScpHoldRestrictedComponent.cs index a4ea1ecd808..42a52fd8613 100644 --- a/Content.Shared/_Scp/Holding/Components/ScpHoldRestrictedComponent.cs +++ b/Content.Shared/_Scp/Holding/Components/ScpHoldRestrictedComponent.cs @@ -2,9 +2,9 @@ namespace Content.Shared._Scp.Holding.Components; -[RegisterComponent, NetworkedComponent] +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] public sealed partial class ScpHoldRestrictedComponent : Component { - [DataField] + [DataField, AutoNetworkedField] public ScpHoldStage Stage = ScpHoldStage.Full; } diff --git a/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.State.cs b/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.State.cs index 9f5c40e001b..38403b8e74f 100644 --- a/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.State.cs +++ b/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.State.cs @@ -82,6 +82,7 @@ private Entity EnsureHeldState(EntityUid target) held.SoftEscapeAvailableAt = _timing.CurTime; held.RequiredHolderCount = GetRequiredHolderCount(target); + Dirty(target, held); return (target, held); } @@ -146,6 +147,7 @@ protected void SyncHeldState(Entity held) return; held.Comp.RequiredHolderCount = GetRequiredHolderCount(held.Owner); + Dirty(held.Owner, held.Comp); if (held.Comp.Holders.Count == 0) { From 7fde9762a126eaaf36dcf58753994eb159ab4714 Mon Sep 17 00:00:00 2001 From: drdth Date: Sat, 18 Apr 2026 16:34:07 +0300 Subject: [PATCH 18/27] add: port move by cursor and add HolderHandsRequired --- .../_Scp/Holding/ScpHoldingSystem.cs | 90 ++-- .../Tests/_Scp/ScpHoldingCursorMoveTest.cs | 497 ++++++++++++++++++ .../Movement/Systems/PullController.cs | 9 + ...tiveStateScpHoldableCursorMoveComponent.cs | 31 ++ .../Components/ScpHoldableComponent.cs | 6 + .../Systems/SharedScpHoldingSystem.Actions.cs | 20 +- .../SharedScpHoldingSystem.CursorMove.cs | 286 ++++++++++ .../Systems/SharedScpHoldingSystem.Drag.cs | 6 + .../Systems/SharedScpHoldingSystem.Hands.cs | 58 +- .../Systems/SharedScpHoldingSystem.State.cs | 9 + .../Holding/Systems/SharedScpHoldingSystem.cs | 2 + .../en-US/_strings/_scp/holding/holding.ftl | 2 +- .../ru-RU/_strings/_scp/holding/holding.ftl | 2 +- .../Entities/Mobs/Player/Scp/Main/scp096.yml | 1 + 14 files changed, 961 insertions(+), 58 deletions(-) create mode 100644 Content.IntegrationTests/Tests/_Scp/ScpHoldingCursorMoveTest.cs create mode 100644 Content.Shared/_Scp/Holding/Components/ActiveStateScpHoldableCursorMoveComponent.cs create mode 100644 Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.CursorMove.cs diff --git a/Content.Client/_Scp/Holding/ScpHoldingSystem.cs b/Content.Client/_Scp/Holding/ScpHoldingSystem.cs index 06dad9690f6..a45a323ac89 100644 --- a/Content.Client/_Scp/Holding/ScpHoldingSystem.cs +++ b/Content.Client/_Scp/Holding/ScpHoldingSystem.cs @@ -1,3 +1,4 @@ +using System.Numerics; using Content.Client.Hands.Systems; using Content.Client.Inventory; using Content.Shared._Scp.Holding.Components; @@ -16,13 +17,16 @@ 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!; - [Dependency] private readonly VirtualItemSystem _virtualItem = default!; private EntityUid? _trackedHolderTarget; + private readonly List _authoritativeBlockers = []; + private readonly List _predictedBlockers = []; private EntityQuery _handsQuery; + private EntityQuery _holdableQuery; private EntityQuery _blockerQuery; private EntityQuery _activeHolderQuery; private EntityQuery _virtualItemQuery; @@ -32,6 +36,7 @@ public override void Initialize() base.Initialize(); _handsQuery = GetEntityQuery(); + _holdableQuery = GetEntityQuery(); _blockerQuery = GetEntityQuery(); _activeHolderQuery = GetEntityQuery(); _virtualItemQuery = GetEntityQuery(); @@ -102,6 +107,12 @@ private void OnUpdateHeldPredicted(Entity ent, ref U return; } + if (HasComp(ent.Owner)) + { + args.BlockPrediction = true; + return; + } + if (_activeHolderQuery.TryComp(local, out var localHolder)) { if (localHolder.Target == ent.Owner) @@ -157,11 +168,11 @@ private void ReconcileLocalHolderBlocker(EntityUid blocker, EntityUid? holderUid if (holderUid is not { Valid: true } holder) return; - if (!_activeHolderQuery.TryComp(holder, out var activeHolder) || - activeHolder.Target == null) - { + if (!_activeHolderQuery.TryComp(holder, out var activeHolder)) + return; + + if (activeHolder.Target == null) return; - } if (!_virtualItemQuery.TryComp(blocker, out var virtualItem)) return; @@ -169,11 +180,11 @@ private void ReconcileLocalHolderBlocker(EntityUid blocker, EntityUid? holderUid if (virtualItem.BlockingEntity != activeHolder.Target.Value) return; - if (!_handsQuery.TryComp(holder, out var hands) || - !_hands.IsHolding((holder, hands), blocker)) - { + if (!_handsQuery.TryComp(holder, out var hands)) + return; + + if (!_hands.IsHolding((holder, hands), blocker)) return; - } ReconcileLocalHolderState((holder, activeHolder)); } @@ -186,59 +197,64 @@ private void ReconcileLocalHolderState(Entity holder) private void ReconcileLocalHolderBlockerSteadyState(Entity holder) { - if (holder.Comp.Target == null || - !_handsQuery.TryComp(holder.Owner, out var hands)) - { + if (holder.Comp.Target == null) + return; + + if (!_handsQuery.TryComp(holder.Owner, out var hands)) return; - } var target = holder.Comp.Target.Value; - EntityUid? authoritativeBlocker = null; - EntityUid? predictedBlocker = null; + if (!_holdableQuery.TryComp(target, out var holdable)) + return; + + var requiredHolderHandCount = GetRequiredHolderHandCount(holdable); + _authoritativeBlockers.Clear(); + _predictedBlockers.Clear(); foreach (var heldItem in _hands.EnumerateHeld((holder.Owner, hands))) { - if (!_virtualItemQuery.TryComp(heldItem, out var virtualItem) || - virtualItem.BlockingEntity != target) - { + if (!_virtualItemQuery.TryComp(heldItem, out var virtualItem)) + continue; + + if (virtualItem.BlockingEntity != target) continue; - } if (!IsClientSide(heldItem)) { - authoritativeBlocker ??= heldItem; + _authoritativeBlockers.Add(heldItem); continue; } if (!_blockerQuery.HasComp(heldItem)) continue; - if (predictedBlocker == null) - { - predictedBlocker = heldItem; - continue; - } - - QueueDel(heldItem); + _predictedBlockers.Add(heldItem); } - if (authoritativeBlocker != null) + var requiredPredictedBlockerCount = Math.Max(requiredHolderHandCount - _authoritativeBlockers.Count, 0); + for (var i = requiredPredictedBlockerCount; i < _predictedBlockers.Count; i++) { - if (predictedBlocker != null) - QueueDel(predictedBlocker.Value); - return; + QueueDel(_predictedBlockers[i]); } - if (_timing.ApplyingState || - predictedBlocker != null || - !_hands.TryGetEmptyHand((holder.Owner, hands), out var emptyHand) || - !_virtualItem.TrySpawnVirtualItem(target, holder.Owner, out var spawnedVirtualItem)) + if (_timing.ApplyingState) { return; } - EnsureComp(spawnedVirtualItem.Value); - _hands.DoPickup(holder.Owner, emptyHand, spawnedVirtualItem.Value, hands); + var currentPredictedBlockerCount = Math.Min(_predictedBlockers.Count, requiredPredictedBlockerCount); + while (currentPredictedBlockerCount < requiredPredictedBlockerCount) + { + if (!_hands.TryGetEmptyHand((holder.Owner, hands), out var emptyHand)) + break; + + if (!_virtualItem.TrySpawnVirtualItem(target, holder.Owner, out var spawnedVirtualItem)) + break; + + EnsureComp(spawnedVirtualItem.Value); + _hands.DoPickup(holder.Owner, emptyHand, spawnedVirtualItem.Value, hands); + currentPredictedBlockerCount++; + } } private void UpdateTrackedLocalHeldTarget(EntityUid? currentTarget, EntityUid? previousTarget = null) diff --git a/Content.IntegrationTests/Tests/_Scp/ScpHoldingCursorMoveTest.cs b/Content.IntegrationTests/Tests/_Scp/ScpHoldingCursorMoveTest.cs new file mode 100644 index 00000000000..aa4d9d102df --- /dev/null +++ b/Content.IntegrationTests/Tests/_Scp/ScpHoldingCursorMoveTest.cs @@ -0,0 +1,497 @@ +#nullable enable +using System.Collections.Generic; +using System.Numerics; +using Content.IntegrationTests.Tests.Movement; +using Content.Server._Scp.Holding; +using Content.Shared.Hands.EntitySystems; +using Content.Shared.Inventory.VirtualItem; +using Content.Server.Movement.Components; +using Content.Shared._Scp.Holding.Components; +using Content.Shared.Input; +using Content.Shared.Interaction; +using Robust.Server.Console; +using Robust.Shared.GameObjects; +using Robust.Shared.Input; +using Robust.Shared.Map; +using Robust.Shared.Maths; +using Robust.Shared.Physics.Components; + +namespace Content.IntegrationTests.Tests._Scp; + +[TestFixture] +public sealed class ScpHoldingCursorMoveTest : MovementTest +{ + private const float PositionTolerance = 0.15f; + + private IServerConsoleHost _consoleHost = default!; + private SharedHandsSystem _hands = default!; + private ScpHoldingSystem _holding = default!; + private readonly List _spawnedServerHolders = []; + + [SetUp] + public override async Task Setup() + { + await base.Setup(); + + _consoleHost = Server.ResolveDependency(); + _hands = Server.System(); + _holding = Server.System(); + + await Server.WaitPost(() => + { + SEntMan.EnsureComponent(SPlayer); + }); + + await RunTicks(1); + } + + [TearDown] + public async Task TearDownScpHolding() + { + await Server.WaitPost(() => + { + ReleaseHoldIfActive(SPlayer); + + foreach (var holderUid in _spawnedServerHolders) + { + ReleaseHoldIfActive(holderUid); + } + + foreach (var holderUid in _spawnedServerHolders) + { + if (SEntMan.EntityExists(holderUid)) + SEntMan.DeleteEntity(holderUid); + } + }); + + await RunTicks(5); + _spawnedServerHolders.Clear(); + } + + [Test] + public async Task SoftHoldCursorMoveUsesClampAndBridge() + { + await SpawnTarget("MobHuman"); + await PrepareTargetForHolding(STarget!.Value); + await StartPlayerHold(); + + var holdable = SEntMan.GetComponent(STarget.Value); + var maintenanceRange = GetMaintenanceRange(holdable); + var playerPosition = Transform.GetWorldPosition(SPlayer); + var farTarget = SEntMan.GetCoordinates(PlayerCoords).Offset(new Vector2(5f, 0f)); + + await PressKey(ContentKeyFunctions.MovePulledObject, coordinates: SEntMan.GetNetCoordinates(farTarget)); + await RunTicks(20); + + await Server.WaitAssertion(() => + { + var heldPosition = Transform.GetWorldPosition(STarget.Value); + Assert.Multiple(() => + { + Assert.That(heldPosition.X, Is.EqualTo(playerPosition.X + maintenanceRange).Within(PositionTolerance)); + Assert.That(SEntMan.HasComponent(STarget.Value), Is.True); + Assert.That(SEntMan.HasComponent(STarget.Value), Is.False); + Assert.That(SEntMan.HasComponent(STarget.Value), Is.True); + Assert.That(SEntMan.HasComponent(STarget.Value), Is.False); + }); + }); + } + + [Test] + public async Task MultiHolderCursorMoveUsesLastValidCommand() + { + await SpawnTarget("MobHuman"); + await PrepareTargetForHolding(STarget!.Value); + await AddHand(Target!.Value); + + var secondHolder = await SpawnHolder(1.8f); + await StartPlayerHold(); + await StartServerHold(secondHolder, STarget.Value); + + await Server.WaitPost(() => + { + var held = SEntMan.GetComponent(STarget.Value); + Assert.Multiple(() => + { + Assert.That(held.Holders, Has.Count.EqualTo(2)); + Assert.That(held.Holders[0], Is.EqualTo(SPlayer)); + Assert.That(held.Holders[1], Is.EqualTo(secondHolder)); + Assert.That(SEntMan.HasComponent(STarget.Value), Is.False); + }); + }); + + var firstPoint = SEntMan.GetCoordinates(PlayerCoords).Offset(new Vector2(0.5f, 0f)); + var secondPoint = SEntMan.GetCoordinates(PlayerCoords).Offset(new Vector2(1.5f, 0f)); + + await Server.WaitPost(() => + { + Assert.That(_holding.TryMoveHeldToCursor(SPlayer, firstPoint), Is.True); + }); + await RunTicks(5); + + await Server.WaitPost(() => + { + Assert.That(_holding.TryMoveHeldToCursor(secondHolder, secondPoint), Is.True); + }); + await RunTicks(20); + + await Server.WaitAssertion(() => + { + var heldPosition = Transform.GetWorldPosition(STarget.Value); + var cursorMove = SEntMan.GetComponent(STarget.Value); + + Assert.Multiple(() => + { + Assert.That(heldPosition.X, Is.EqualTo(2.0f).Within(PositionTolerance)); + Assert.That(cursorMove.Holder, Is.EqualTo(secondHolder)); + Assert.That(SEntMan.HasComponent(STarget.Value), Is.False); + }); + }); + } + + [Test] + public async Task FullHoldIgnoresMovePulledObject() + { + await SpawnTarget("MobHuman"); + await PrepareTargetForHolding(STarget!.Value); + + var secondHolder = await SpawnHolder(1.8f); + await StartPlayerHold(); + await StartServerHold(secondHolder, STarget.Value); + + await Server.WaitAssertion(() => + { + Assert.That(SEntMan.HasComponent(STarget!.Value), Is.True); + }); + + var initialPosition = Transform.GetWorldPosition(STarget.Value); + var farTarget = SEntMan.GetCoordinates(PlayerCoords).Offset(new Vector2(5f, 0f)); + + await PressKey(ContentKeyFunctions.MovePulledObject, coordinates: SEntMan.GetNetCoordinates(farTarget)); + await RunTicks(20); + + await Server.WaitAssertion(() => + { + var heldPosition = Transform.GetWorldPosition(STarget.Value); + Assert.Multiple(() => + { + Assert.That(Vector2.Distance(heldPosition, initialPosition), Is.LessThanOrEqualTo(0.05f)); + Assert.That(SEntMan.HasComponent(STarget.Value), Is.True); + Assert.That(SEntMan.HasComponent(STarget.Value), Is.False); + Assert.That(SEntMan.HasComponent(STarget.Value), Is.False); + }); + }); + } + + [Test] + public async Task HolderMovementInvalidatesCursorMoveAndReturnsToSoftDrag() + { + await SpawnTarget("MobHuman"); + await PrepareTargetForHolding(STarget!.Value); + await StartPlayerHold(); + + var parkedTarget = SEntMan.GetCoordinates(PlayerCoords).Offset(new Vector2(1.5f, 0f)); + await PressKey(ContentKeyFunctions.MovePulledObject, coordinates: SEntMan.GetNetCoordinates(parkedTarget)); + await RunTicks(20); + + await Server.WaitAssertion(() => + { + Assert.That(SEntMan.HasComponent(STarget!.Value), Is.True); + Assert.That(Transform.GetWorldPosition(STarget.Value).X, Is.EqualTo(2.0f).Within(PositionTolerance)); + }); + + var parkedX = Transform.GetWorldPosition(STarget.Value).X; + var maintenanceRange = GetMaintenanceRange(SEntMan.GetComponent(STarget.Value)); + + await Move(DirectionFlag.West, 1f); + await RunTicks(10); + + await Server.WaitAssertion(() => + { + var heldPosition = Transform.GetWorldPosition(STarget!.Value); + var playerPosition = Transform.GetWorldPosition(SPlayer); + + Assert.Multiple(() => + { + Assert.That(SEntMan.HasComponent(STarget.Value), Is.False); + Assert.That(heldPosition.X, Is.LessThan(parkedX - 0.25f)); + Assert.That((heldPosition - playerPosition).Length(), Is.LessThanOrEqualTo(maintenanceRange + 0.2f)); + }); + }); + } + + [Test] + public async Task ClientCursorMoveWaitsForServerAndBecomesAuthoritative() + { + await SpawnTarget("MobHuman"); + await PrepareTargetForHolding(STarget!.Value); + await StartPlayerHold(); + await RunTicks(10); + + await Client.WaitAssertion(() => + { + Assert.That(CTarget, Is.Not.Null); + Assert.That(CEntMan.HasComponent(CTarget!.Value), Is.True); + }); + + var parkedTarget = SEntMan.GetCoordinates(PlayerCoords).Offset(new Vector2(1.5f, 0f)); + var parkedTargetNetCoords = SEntMan.GetNetCoordinates(parkedTarget); + + var initialServerPosition = Vector2.Zero; + var initialClientPosition = Vector2.Zero; + + await Server.WaitPost(() => + { + initialServerPosition = Transform.GetWorldPosition(STarget!.Value); + }); + + await Client.WaitPost(() => + { + initialClientPosition = CEntMan.System().GetWorldPosition(CTarget!.Value); + }); + + await SetKey(ContentKeyFunctions.MovePulledObject, BoundKeyState.Down, coordinates: parkedTargetNetCoords); + await Client.WaitRunTicks(1); + + var firstTickClientPosition = Vector2.Zero; + var authoritativeServerPosition = Vector2.Zero; + var hasCursorMoveAfterFirstTick = false; + + await Client.WaitPost(() => + { + firstTickClientPosition = CEntMan.System().GetWorldPosition(CTarget!.Value); + hasCursorMoveAfterFirstTick = CEntMan.HasComponent(CTarget.Value); + }); + + await Server.WaitPost(() => + { + authoritativeServerPosition = Transform.GetWorldPosition(STarget!.Value); + }); + + Assert.Multiple(() => + { + Assert.That(hasCursorMoveAfterFirstTick, Is.False, + $"Client created cursor-move state before the server processed input. initialClientX={initialClientPosition.X:F4}; firstTickClientX={firstTickClientPosition.X:F4}; initialServerX={initialServerPosition.X:F4}; serverX={authoritativeServerPosition.X:F4}"); + Assert.That(firstTickClientPosition.X, Is.EqualTo(initialClientPosition.X).Within(0.01f), + $"Client moved the held target before the server processed input. initialClientX={initialClientPosition.X:F4}; firstTickClientX={firstTickClientPosition.X:F4}; initialServerX={initialServerPosition.X:F4}; serverX={authoritativeServerPosition.X:F4}"); + Assert.That(authoritativeServerPosition.X, Is.EqualTo(initialServerPosition.X).Within(0.001f), + $"Server must remain unchanged until it processes the input. initialClientX={initialClientPosition.X:F4}; firstTickClientX={firstTickClientPosition.X:F4}; initialServerX={initialServerPosition.X:F4}; serverX={authoritativeServerPosition.X:F4}"); + }); + + await RunTicks(10); + + await Client.WaitAssertion(() => + { + var clientPosition = CEntMan.System().GetWorldPosition(CTarget!.Value); + var clientPhysicsPredict = CEntMan.GetComponent(CTarget.Value).Predict; + + Assert.Multiple(() => + { + Assert.That(CEntMan.HasComponent(CTarget.Value), Is.True); + Assert.That(clientPhysicsPredict, Is.False, + $"Held target must stay authoritative like PullMoving. initialClientX={initialClientPosition.X:F4}; currentClientX={clientPosition.X:F4}"); + Assert.That(clientPosition.X, Is.GreaterThan(initialClientPosition.X + 0.1f), + $"Held target did not start moving after the server processed cursor input. initialClientX={initialClientPosition.X:F4}; currentClientX={clientPosition.X:F4}"); + }); + }); + + await SetKey(ContentKeyFunctions.MovePulledObject, BoundKeyState.Up, coordinates: parkedTargetNetCoords); + await RunTicks(1); + } + + [Test] + public async Task ClientCursorMoveDoesNotSnapToCursorPoint() + { + await Client.WaitPost(() => + { + Client.CfgMan.SetCVar("net.fakelagmin", 0.35f); + Client.CfgMan.SetCVar("net.fakelagrand", 0f); + }); + + try + { + await SpawnTarget("MobHuman"); + await PrepareTargetForHolding(STarget!.Value); + await StartPlayerHold(); + + await Client.WaitAssertion(() => + { + Assert.That(CTarget, Is.Not.Null); + Assert.That(CEntMan.HasComponent(CTarget!.Value), Is.True); + }); + + var parkedTarget = SEntMan.GetCoordinates(PlayerCoords).Offset(new Vector2(1.5f, 0f)); + var parkedTargetNetCoords = SEntMan.GetNetCoordinates(parkedTarget); + var sampledPositions = new List(); + + await Client.WaitPost(() => + { + sampledPositions.Add(CEntMan.System().GetWorldPosition(CTarget!.Value).X); + }); + + await SetKey(ContentKeyFunctions.MovePulledObject, BoundKeyState.Down, coordinates: parkedTargetNetCoords); + + for (var i = 0; i < 12; i++) + { + await RunTicks(1); + await Client.WaitPost(() => + { + sampledPositions.Add(CEntMan.System().GetWorldPosition(CTarget!.Value).X); + }); + } + + var maxDelta = 0f; + for (var i = 1; i < sampledPositions.Count; i++) + { + maxDelta = MathF.Max(maxDelta, MathF.Abs(sampledPositions[i] - sampledPositions[i - 1])); + } + + Assert.That(maxDelta, Is.LessThanOrEqualTo(0.35f), + $"Held target snapped too far in a single client tick. positions=[{string.Join(", ", sampledPositions.ConvertAll(x => x.ToString("F4")))}]"); + + await SetKey(ContentKeyFunctions.MovePulledObject, BoundKeyState.Up, coordinates: parkedTargetNetCoords); + await RunTicks(1); + } + finally + { + await Client.WaitPost(() => + { + Client.CfgMan.SetCVar("net.fakelagmin", 0f); + Client.CfgMan.SetCVar("net.fakelagrand", 0f); + }); + } + } + + [Test] + public async Task HolderHandsRequiredTwoDoesNotImmediatelyReleaseHold() + { + await SpawnTarget("MobHuman"); + await PrepareTargetForHolding(STarget!.Value); + + await Server.WaitPost(() => + { + var holdable = SEntMan.GetComponent(STarget.Value); + holdable.HolderHandsRequired = 2; + SEntMan.Dirty(STarget.Value, holdable); + }); + + await RunTicks(1); + await StartServerHold(SPlayer, STarget.Value); + + await Server.WaitAssertion(() => + { + var activeHolder = SEntMan.GetComponent(SPlayer); + + Assert.Multiple(() => + { + Assert.That(activeHolder.Target, Is.EqualTo(STarget)); + Assert.That(SEntMan.HasComponent(STarget.Value), Is.True); + Assert.That(CountHolderBlockers(SPlayer, STarget.Value), Is.EqualTo(2)); + Assert.That(_hands.CountFreeHands(SPlayer), Is.EqualTo(0)); + }); + }); + } + + private async Task PrepareTargetForHolding(EntityUid targetUid) + { + await Server.WaitPost(() => + { + SEntMan.EnsureComponent(targetUid); + }); + + await RunTicks(1); + } + + private async Task SpawnHolder(float playerOffsetX) + { + var coords = SEntMan.GetCoordinates(PlayerCoords).Offset(new Vector2(playerOffsetX, 0f)); + var holder = await SpawnEntity("MobHuman", coords); + + await Server.WaitPost(() => + { + SEntMan.EnsureComponent(holder); + }); + + await RunTicks(1); + _spawnedServerHolders.Add(holder); + return holder; + } + + private async Task AddHand(NetEntity target) + { + await Server.WaitPost(() => + { + _consoleHost.ExecuteCommand(null, $"addhand {target}"); + }); + + await RunTicks(1); + } + + private async Task StartPlayerHold() + { + await PressKey(ContentKeyFunctions.TryPullObject); + await RunTicks(5); + + await Server.WaitAssertion(() => + { + Assert.Multiple(() => + { + Assert.That(SEntMan.HasComponent(SPlayer), Is.True); + Assert.That(SEntMan.GetComponent(SPlayer).Target, Is.EqualTo(STarget)); + Assert.That(SEntMan.HasComponent(STarget!.Value), Is.True); + }); + }); + } + + private async Task StartServerHold(EntityUid holderUid, EntityUid targetUid) + { + await Server.WaitPost(() => + { + var holder = SEntMan.GetComponent(holderUid); + Assert.That(_holding.TryToggleHold((holderUid, holder), targetUid), Is.True); + }); + + await RunTicks(5); + } + + private static float GetMaintenanceRange(ScpHoldableComponent holdable) + { + var desiredSoftDragDistance = Math.Clamp( + holdable.HoldRange * holdable.SoftDragDistanceFactor, + holdable.SoftDragMinimumDistance, + holdable.SoftDragMaximumDistance); + + return MathF.Max( + MathF.Max(holdable.HoldRange, SharedInteractionSystem.InteractionRange), + desiredSoftDragDistance + holdable.SoftDragSnapTolerance); + } + + private void ReleaseHoldIfActive(EntityUid holderUid) + { + if (!SEntMan.EntityExists(holderUid) || + !SEntMan.TryGetComponent(holderUid, out ScpHolderComponent? holder) || + !SEntMan.TryGetComponent(holderUid, out ActiveScpHolderComponent? activeHolder) || + activeHolder.Target == null) + { + return; + } + + _holding.TryReleaseHold((holderUid, holder), activeHolder.Target.Value); + } + + private int CountHolderBlockers(EntityUid holderUid, EntityUid targetUid) + { + var blockerCount = 0; + foreach (var heldItem in _hands.EnumerateHeld(holderUid)) + { + if (SEntMan.HasComponent(heldItem) && + SEntMan.TryGetComponent(heldItem, out var virtualItem) && + virtualItem.BlockingEntity == targetUid) + { + blockerCount++; + } + } + + return blockerCount; + } +} diff --git a/Content.Server/Movement/Systems/PullController.cs b/Content.Server/Movement/Systems/PullController.cs index f2bbdc247b1..20b7e7235bb 100644 --- a/Content.Server/Movement/Systems/PullController.cs +++ b/Content.Server/Movement/Systems/PullController.cs @@ -1,4 +1,7 @@ using System.Numerics; +using Content.Server._Scp.Holding; +using Content.Shared._Scp.Holding.Components; +using Content.Shared._Scp.Holding.Systems; using Content.Server.Movement.Components; using Content.Server.Physics.Controllers; using Content.Shared.ActionBlocker; @@ -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!; /// @@ -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 + if (!_pullerQuery.TryComp(player, out var pullerComp)) return false; diff --git a/Content.Shared/_Scp/Holding/Components/ActiveStateScpHoldableCursorMoveComponent.cs b/Content.Shared/_Scp/Holding/Components/ActiveStateScpHoldableCursorMoveComponent.cs new file mode 100644 index 00000000000..2d19a35b58e --- /dev/null +++ b/Content.Shared/_Scp/Holding/Components/ActiveStateScpHoldableCursorMoveComponent.cs @@ -0,0 +1,31 @@ +using Content.Shared._Scp.Holding.Systems; +using Robust.Shared.GameStates; +using Robust.Shared.Map; + +namespace Content.Shared._Scp.Holding.Components; + +/// +/// Runtime cursor-move state stored on a held target while it is being moved or parked at a cursor-selected point. +/// +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] +[Access(typeof(SharedScpHoldingSystem))] +public sealed partial class ActiveStateScpHoldableCursorMoveComponent : Component +{ + /// + /// Holder that issued the most recent valid cursor-move command. + /// + [AutoNetworkedField, ViewVariables] + public EntityUid Holder; + + /// + /// Clamped cursor target stored in entity coordinates for shared prediction and reconciliation. + /// + [AutoNetworkedField, ViewVariables] + public EntityCoordinates TargetCoordinates = EntityCoordinates.Invalid; + + /// + /// True while the held target is still travelling toward the stored cursor point. + /// + [AutoNetworkedField, ViewVariables] + public bool Active = true; +} diff --git a/Content.Shared/_Scp/Holding/Components/ScpHoldableComponent.cs b/Content.Shared/_Scp/Holding/Components/ScpHoldableComponent.cs index 635874c58c4..8c3e3c55257 100644 --- a/Content.Shared/_Scp/Holding/Components/ScpHoldableComponent.cs +++ b/Content.Shared/_Scp/Holding/Components/ScpHoldableComponent.cs @@ -23,6 +23,12 @@ public sealed partial class ScpHoldableComponent : Component [DataField, AutoNetworkedField] public EntityWhitelist? HolderBlacklist; + /// + /// Number of hands each holder must reserve to contribute to holding this target. + /// + [DataField, AutoNetworkedField] + public int HolderHandsRequired = 1; + /// /// Minimum uninterrupted full hold duration before a breakout do-after may start. /// diff --git a/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.Actions.cs b/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.Actions.cs index d339fe74161..7350ca60dfc 100644 --- a/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.Actions.cs +++ b/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.Actions.cs @@ -158,7 +158,8 @@ public bool CanToggleHold( return false; } - if (!ignoreHandAvailability && !HasAvailableHolderHand(holder)) + var requiredHolderHandCount = GetRequiredHolderHandCount(holdable); + if (!ignoreHandAvailability && !HasAvailableHolderHands(holder.Owner, requiredHolderHandCount)) { if (!quiet) Popup(holder, "scp-hold-holder-no-free-hand", ("target", target)); @@ -192,6 +193,23 @@ public bool CanToggleHold( return true; } + protected static int GetRequiredHolderHandCount(ScpHoldableComponent holdable) + { + return Math.Max(1, holdable.HolderHandsRequired); + } + + protected bool TryGetRequiredHolderHandCount(EntityUid targetUid, out int requiredHolderHandCount) + { + if (_holdableQuery.TryComp(targetUid, out var holdable)) + { + requiredHolderHandCount = GetRequiredHolderHandCount(holdable); + return true; + } + + requiredHolderHandCount = 0; + return false; + } + public bool TryBreakOut(Entity held, bool viaMovement) { return _activeHoldableFullHoldStateQuery.HasComp(held.Owner) diff --git a/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.CursorMove.cs b/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.CursorMove.cs new file mode 100644 index 00000000000..6db8a8f2ae4 --- /dev/null +++ b/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.CursorMove.cs @@ -0,0 +1,286 @@ +using System.Diagnostics.CodeAnalysis; +using System.Numerics; +using Content.Shared._Scp.Holding.Components; +using Robust.Shared.Map; + +namespace Content.Shared._Scp.Holding.Systems; + +public abstract partial class SharedScpHoldingSystem +{ + /* + * Cursor-move input validation, runtime state, and movement update helpers. + */ + + private EntityQuery _activeHoldableCursorMoveQuery; + + private void InitializeCursorMoveQueries() + { + _activeHoldableCursorMoveQuery = GetEntityQuery(); + } + + private void InitializeCursorMoveEvents() + { + SubscribeLocalEvent(OnHolderMove); + } + + public bool TryMoveHeldToCursor(EntityUid holderUid, EntityCoordinates cursorCoords) + { + if (!_activeHolderQuery.TryComp(holderUid, out var activeHolder)) + return false; + + if (activeHolder.Target == null) + return false; + + if (!CanMoveHeldToCursor(holderUid, cursorCoords, out var held, out var clampedCoords)) + return false; + + SetCursorMoveState(held.Value, holderUid, clampedCoords, active: true); + return true; + } + + private bool CanMoveHeldToCursor( + EntityUid holderUid, + EntityCoordinates cursorCoords, + [NotNullWhen(true)] out Entity? held, + out EntityCoordinates clampedCoords, + bool quiet = false) + { + _ = quiet; + held = null; + clampedCoords = EntityCoordinates.Invalid; + + if (!_activeHolderQuery.TryComp(holderUid, out var holder)) + return false; + + if (holder.Target is not { } heldUid) + return false; + + if (!_activeHoldableQuery.TryComp(heldUid, out var heldComponent)) + return false; + + if (!heldComponent.Holders.Contains(holderUid)) + return false; + + if (_activeHoldableFullHoldStateQuery.HasComp(heldUid)) + return false; + + held = (heldUid, heldComponent); + + if (!TryGetHeldHoldable(held.Value, out var holdable)) + return false; + + if (!_container.IsInSameOrNoContainer(holderUid, heldUid)) + return false; + + var desiredSoftDragDistance = GetDesiredSoftDragDistance(holdable); + var maintenanceRange = GetHoldMaintenanceRange(holdable, desiredSoftDragDistance); + + if (!_interaction.InRangeUnobstructed(holderUid, heldUid, maintenanceRange)) + return false; + + return TryClampHeldCursorMoveTargetCoordinates(holderUid, cursorCoords, maintenanceRange, out clampedCoords); + } + + private bool TryGetValidatedCursorMoveState( + Entity held, + ScpHoldableComponent holdable, + [NotNullWhen(true)] out ActiveStateScpHoldableCursorMoveComponent? cursorMove, + out EntityUid holderUid) + { + cursorMove = null; + holderUid = default; + + if (!_activeHoldableCursorMoveQuery.TryComp(held.Owner, out var moveState)) + return false; + + if (!moveState.TargetCoordinates.IsValid(EntityManager)) + { + ClearCursorMoveState(held.Owner); + return false; + } + + if (_activeHoldableFullHoldStateQuery.HasComp(held.Owner)) + { + ClearCursorMoveState(held.Owner); + return false; + } + + if (!_activeHolderQuery.TryComp(moveState.Holder, out var holder)) + { + ClearCursorMoveState(held.Owner); + return false; + } + + if (holder.Target != held.Owner) + { + ClearCursorMoveState(held.Owner); + return false; + } + + if (!held.Comp.Holders.Contains(moveState.Holder)) + { + ClearCursorMoveState(held.Owner); + return false; + } + + if (!_container.IsInSameOrNoContainer(moveState.Holder, held.Owner)) + { + ClearCursorMoveState(held.Owner); + return false; + } + + var desiredSoftDragDistance = GetDesiredSoftDragDistance(holdable); + var maintenanceRange = GetHoldMaintenanceRange(holdable, desiredSoftDragDistance); + var holderCoords = _transform.GetMapCoordinates(moveState.Holder); + var targetCoords = _transform.ToMapCoordinates(moveState.TargetCoordinates); + + if (holderCoords.MapId != targetCoords.MapId) + { + ClearCursorMoveState(held.Owner); + return false; + } + + if ((targetCoords.Position - holderCoords.Position).LengthSquared() > maintenanceRange * maintenanceRange) + { + ClearCursorMoveState(held.Owner); + return false; + } + + cursorMove = moveState; + holderUid = moveState.Holder; + return true; + } + + private bool TryClampHeldCursorMoveTargetCoordinates( + EntityUid holderUid, + EntityCoordinates cursorCoords, + float maintenanceRange, + out EntityCoordinates clampedCoords) + { + clampedCoords = EntityCoordinates.Invalid; + + if (!cursorCoords.IsValid(EntityManager)) + return false; + + var holderCoords = _transform.GetMapCoordinates(holderUid); + var cursorMapCoords = _transform.ToMapCoordinates(cursorCoords); + + if (holderCoords.MapId != cursorMapCoords.MapId) + return false; + + var offset = cursorMapCoords.Position - holderCoords.Position; + var distance = offset.Length(); + var clampedPosition = cursorMapCoords.Position; + + if (distance > maintenanceRange && distance > 0f) + clampedPosition = holderCoords.Position + offset / distance * maintenanceRange; + + clampedCoords = _transform.ToCoordinates(new MapCoordinates(clampedPosition, holderCoords.MapId)); + return clampedCoords.IsValid(EntityManager); + } + + private void SetCursorMoveState( + Entity held, + EntityUid holderUid, + EntityCoordinates targetCoordinates, + bool active) + { + var cursorMove = EnsureComp(held.Owner); + if (cursorMove.Holder == holderUid && + cursorMove.TargetCoordinates == targetCoordinates && + cursorMove.Active == active) + { + return; + } + + cursorMove.Holder = holderUid; + cursorMove.TargetCoordinates = targetCoordinates; + cursorMove.Active = active; + Dirty(held.Owner, cursorMove); + } + + private void ClearCursorMoveState(EntityUid heldUid) + { + if (_activeHoldableCursorMoveQuery.HasComp(heldUid)) + RemComp(heldUid); + } + + private void UpdateCursorMoveDrag( + Entity held, + ScpHoldableComponent holdable, + EntityUid holderUid, + ActiveStateScpHoldableCursorMoveComponent cursorMove) + { + if (!_physicsQuery.TryComp(held.Owner, out var heldPhysics)) + return; + + if (!_container.IsInSameOrNoContainer(holderUid, held.Owner)) + { + ClearCursorMoveState(held.Owner); + return; + } + + var targetCoords = _transform.ToMapCoordinates(cursorMove.TargetCoordinates); + var heldCoords = _transform.GetMapCoordinates(held.Owner); + + if (targetCoords.MapId != heldCoords.MapId) + { + ClearCursorMoveState(held.Owner); + return; + } + + var correction = targetCoords.Position - heldCoords.Position; + var correctionDistance = correction.Length(); + + if (!cursorMove.Active && correctionDistance > holdable.SoftDragSettleTolerance) + { + cursorMove.Active = true; + Dirty(held.Owner, cursorMove); + } + + Vector2 desiredVelocity; + if (correctionDistance <= holdable.SoftDragSettleTolerance) + { + if (cursorMove.Active) + { + cursorMove.Active = false; + Dirty(held.Owner, cursorMove); + } + + desiredVelocity = Vector2.Zero; + } + else + { + var correctionDirection = correction / correctionDistance; + var correctionSpeed = Math.Min( + correctionDistance / GetSoftDragCatchUpTime(holdable), + holdable.SoftDragMaximumCorrectionSpeed); + + desiredVelocity = correctionDirection * correctionSpeed; + + var relativeVelocity = heldPhysics.LinearVelocity; + var awaySpeed = MathF.Max(0f, -Vector2.Dot(relativeVelocity, correctionDirection)); + if (awaySpeed > 0f) + desiredVelocity += correctionDirection * awaySpeed * holdable.SoftDragAwayVelocityStrength; + } + + ApplyHeldVelocity(held.Owner, desiredVelocity, heldPhysics, holdable); + } + + private void OnHolderMove(Entity ent, ref MoveEvent args) + { + if (_timing.ApplyingState) + return; + + if (ent.Comp.Target == null) + return; + + if (args.NewPosition.EntityId == args.OldPosition.EntityId && + (args.NewPosition.Position - args.OldPosition.Position).LengthSquared() <= 0f) + { + return; + } + + ClearCursorMoveState(ent.Comp.Target.Value); + } +} diff --git a/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.Drag.cs b/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.Drag.cs index 0c59afcdc5b..8ff48964ef1 100644 --- a/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.Drag.cs +++ b/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.Drag.cs @@ -38,6 +38,12 @@ private void UpdateSoftDrag( float maintenanceRange, float desiredDistance) { + if (TryGetValidatedCursorMoveState(held, holdable, out var cursorMove, out var cursorHolderUid)) + { + UpdateCursorMoveDrag(held, holdable, cursorHolderUid, cursorMove); + return; + } + if (anchorHolder.Target != held.Owner) return; diff --git a/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.Hands.cs b/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.Hands.cs index 6f0ed9a7d46..b71da3d0a47 100644 --- a/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.Hands.cs +++ b/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.Hands.cs @@ -145,9 +145,17 @@ private void EnsureHeldHandBlockers(Entity held) private void SyncHolderHandBlocker(Entity holder) { _virtualBlockersToDelete.Clear(); - EntityUid? validBlocker = null; var target = holder.Comp.Target; var holderActive = holder.Comp.LifeStage <= ComponentLifeStage.Running; + var validBlockerCount = 0; + var requiredHolderHandCount = 0; + + if (holderActive && + _activeHoldableQuery.HasComp(target) && + TryGetRequiredHolderHandCount(target.Value, out var resolvedRequiredHolderHandCount)) + { + requiredHolderHandCount = resolvedRequiredHolderHandCount; + } foreach (var heldItem in _hands.EnumerateHeld(holder.Owner)) { @@ -161,9 +169,9 @@ private void SyncHolderHandBlocker(Entity holder) if (ownedBlocker && matchesCurrentTarget) { - if (validBlocker == null) + if (validBlockerCount < requiredHolderHandCount) { - validBlocker = heldItem; + validBlockerCount++; RemComp(heldItem); continue; } @@ -179,33 +187,44 @@ private void SyncHolderHandBlocker(Entity holder) } if (!holderActive || - target == null || - validBlocker != null) + target == null) { return; } - if (!_handsQuery.TryComp(holder.Owner, out var hands) || - !_hands.TryGetEmptyHand((holder.Owner, hands), out var emptyHand)) + if (!_handsQuery.TryComp(holder.Owner, out var hands)) { + ReleaseHolderContribution(holder.Owner, target.Value, clearIfEmpty: true); return; } - if (!_virtualItem.TrySpawnVirtualItem(target.Value, holder.Owner, out var spawnedVirtualItem)) - return; + while (validBlockerCount < requiredHolderHandCount) + { + if (!_hands.TryGetEmptyHand((holder.Owner, hands), out var emptyHand) || + !_virtualItem.TrySpawnVirtualItem(target.Value, holder.Owner, out var spawnedVirtualItem)) + { + break; + } + + EnsureComp(spawnedVirtualItem.Value); + _hands.DoPickup(holder.Owner, emptyHand, spawnedVirtualItem.Value, hands); + validBlockerCount++; + } - EnsureComp(spawnedVirtualItem.Value); - _hands.DoPickup(holder.Owner, emptyHand, spawnedVirtualItem.Value, hands); + validBlockerCount = CountOwnedHolderHandBlockers(holder.Owner, target.Value); + if (validBlockerCount < requiredHolderHandCount) + ReleaseHolderContribution(holder.Owner, target.Value, clearIfEmpty: true); } - private bool HasAvailableHolderHand(EntityUid holderUid) + private bool HasAvailableHolderHands(EntityUid holderUid, int requiredHandCount) { return _handsQuery.TryComp(holderUid, out var hands) && - _hands.TryGetEmptyHand((holderUid, hands), out _); + _hands.CountFreeHands((holderUid, hands)) >= requiredHandCount; } - private bool HasOwnedHolderHandBlocker(EntityUid holderUid, EntityUid targetUid) + private int CountOwnedHolderHandBlockers(EntityUid holderUid, EntityUid targetUid) { + var blockerCount = 0; foreach (var heldItem in _hands.EnumerateHeld(holderUid)) { if (!_holdHandBlockerQuery.HasComp(heldItem) || @@ -215,10 +234,10 @@ private bool HasOwnedHolderHandBlocker(EntityUid holderUid, EntityUid targetUid) continue; } - return true; + blockerCount++; } - return false; + return blockerCount; } private void RemoveHolderHandBlocker(EntityUid holderUid, Entity virtualItem) @@ -324,9 +343,12 @@ private void OnHolderVirtualItemDeleted(Entity ent, re return; } - if (HasOwnedHolderHandBlocker(ent.Owner, args.BlockingEntity)) + if (!TryGetRequiredHolderHandCount(args.BlockingEntity, out var requiredHolderHandCount)) return; - ReleaseHolderContribution(ent.Owner, args.BlockingEntity, clearIfEmpty: true); + if (CountOwnedHolderHandBlockers(ent.Owner, args.BlockingEntity) >= requiredHolderHandCount) + return; + + SyncHolderState(ent); } } diff --git a/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.State.cs b/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.State.cs index 38403b8e74f..9d152562b32 100644 --- a/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.State.cs +++ b/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.State.cs @@ -51,7 +51,10 @@ protected void UpdateHeld(Entity held) if (!_activeHoldableFullHoldStateQuery.HasComp(held.Owner)) UpdateSoftDrag(held, holdable, dragAnchorUid, dragAnchor, maintenanceRange, desiredSoftDragDistance); else + { + ClearCursorMoveState(held.Owner); ZeroHeldVelocity(held.Owner); + } _holdersToRemove.Clear(); @@ -119,7 +122,10 @@ protected void ReleaseHolderContribution(EntityUid holderUid, EntityUid targetUi } if (removed) + { + ClearCursorMoveState(targetUid); Dirty(targetUid, held); + } if (_activeHolderQuery.HasComp(holderUid)) RemComp(holderUid); @@ -177,6 +183,8 @@ protected void SyncHeldState(Entity held) private void EnterFullHold(Entity held, ScpHoldableComponent holdable) { + ClearCursorMoveState(held.Owner); + var fullHeldCreated = !_activeHoldableFullHoldStateQuery.TryComp(held.Owner, out var fullHeld); fullHeld ??= EnsureComp(held.Owner); @@ -232,6 +240,7 @@ private void ClearHoldState(Entity held, bool applyI if (_activeHoldableQuery.TryComp(held.Owner, out var refreshed)) held = (held.Owner, refreshed); + ClearCursorMoveState(held.Owner); EndBreakoutAttempt(held.Owner, cancelDoAfter: true); if (_activeHoldableFullHoldStateQuery.HasComp(held.Owner)) diff --git a/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.cs b/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.cs index 983a1d42db8..d2dce98dd4e 100644 --- a/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.cs +++ b/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.cs @@ -39,11 +39,13 @@ public override void Initialize() InitializeHoldQueries(); InitializeBreakoutAttemptQueries(); + InitializeCursorMoveQueries(); InitializeDragQueries(); InitializeHandQueries(); InitializeStateQueries(); InitializeLifecycleEvents(); InitializeBreakoutAttemptEvents(); + InitializeCursorMoveEvents(); InitializeDragEvents(); InitializeHandEvents(); InitializeRestrictions(); diff --git a/Resources/Locale/en-US/_strings/_scp/holding/holding.ftl b/Resources/Locale/en-US/_strings/_scp/holding/holding.ftl index eb92da2b002..01238b487ad 100644 --- a/Resources/Locale/en-US/_strings/_scp/holding/holding.ftl +++ b/Resources/Locale/en-US/_strings/_scp/holding/holding.ftl @@ -4,7 +4,7 @@ scp-hold-target-not-holdable = {CAPITALIZE(THE($target))} cannot be grabbed with scp-hold-target-immune = {CAPITALIZE(THE($target))} cannot be held yet. scp-hold-target-fully-held = {CAPITALIZE(THE($target))} is already fully held. scp-hold-target-too-far = You are too far away to hold {THE($target)}. -scp-hold-holder-no-free-hand = You need a free hand to grab {THE($target)}. +scp-hold-holder-no-free-hand = You need enough free hands to grab {THE($target)}. scp-hold-holder-action-on-cooldown = You can grab again in {$seconds} s. scp-hold-breakout-too-early = You can try to break free in {$seconds} s. scp-hold-breakout-start = You start trying to break free. diff --git a/Resources/Locale/ru-RU/_strings/_scp/holding/holding.ftl b/Resources/Locale/ru-RU/_strings/_scp/holding/holding.ftl index 419c6565090..6e305544d3e 100644 --- a/Resources/Locale/ru-RU/_strings/_scp/holding/holding.ftl +++ b/Resources/Locale/ru-RU/_strings/_scp/holding/holding.ftl @@ -4,7 +4,7 @@ scp-hold-target-not-holdable = {CAPITALIZE(THE($target))} нельзя схва scp-hold-target-immune = {CAPITALIZE(THE($target))} пока нельзя удержать. scp-hold-target-fully-held = {CAPITALIZE(THE($target))} уже полностью удерживается. scp-hold-target-too-far = Вы слишком далеко, чтобы удерживать {THE($target)}. -scp-hold-holder-no-free-hand = Чтобы схватить {THE($target)}, нужна свободная рука. +scp-hold-holder-no-free-hand = Чтобы схватить {THE($target)}, нужно достаточно свободных рук. scp-hold-holder-action-on-cooldown = Схватить снова можно через {$seconds} с. scp-hold-breakout-too-early = Попытаться вырваться можно через {$seconds} с. scp-hold-breakout-start = Вы начинаете вырываться. diff --git a/Resources/Prototypes/_Scp/Entities/Mobs/Player/Scp/Main/scp096.yml b/Resources/Prototypes/_Scp/Entities/Mobs/Player/Scp/Main/scp096.yml index 3be48c69350..beb2a738f6a 100644 --- a/Resources/Prototypes/_Scp/Entities/Mobs/Player/Scp/Main/scp096.yml +++ b/Resources/Prototypes/_Scp/Entities/Mobs/Player/Scp/Main/scp096.yml @@ -116,6 +116,7 @@ showInHands: false disableExplosionRecursion: true - type: ScpHoldable + holderHandsRequired: 2 - type: GhostPanelAntagonistMarker name: ghost-panel-antagonist-scp-name description: ghost-panel-antagonist-scp-description From 8577e59d9733605ec6f378f3d219b832d6b49e01 Mon Sep 17 00:00:00 2001 From: drdth Date: Sun, 19 Apr 2026 07:11:37 +0300 Subject: [PATCH 19/27] refactor: remove feeedback partial, remove ent.Owner --- .../_Scp/Holding/ScpHoldingSystem.Feedback.cs | 54 -------- .../_Scp/Holding/ScpHoldingSystem.cs | 34 ++--- .../_Scp/Holding/ScpHoldingSystem.Feedback.cs | 54 -------- .../_Scp/Holding/ScpHoldingSystem.cs | 4 +- .../Components/ScpHoldableComponent.cs | 4 +- .../Systems/SharedScpHoldingSystem.Actions.cs | 67 +++++---- .../SharedScpHoldingSystem.BreakoutAttempt.cs | 51 ++++++- .../SharedScpHoldingSystem.CursorMove.cs | 40 +++--- .../Systems/SharedScpHoldingSystem.Drag.cs | 10 +- .../SharedScpHoldingSystem.Feedback.cs | 10 -- .../Systems/SharedScpHoldingSystem.Hands.cs | 128 +++++++++--------- .../SharedScpHoldingSystem.Lifecycle.cs | 22 +-- .../SharedScpHoldingSystem.Restrictions.cs | 2 +- .../Systems/SharedScpHoldingSystem.State.cs | 104 +++++++------- .../Holding/Systems/SharedScpHoldingSystem.cs | 8 +- .../Entities/Mobs/Player/Scp/Main/scp096.yml | 2 + 16 files changed, 254 insertions(+), 340 deletions(-) delete mode 100644 Content.Client/_Scp/Holding/ScpHoldingSystem.Feedback.cs delete mode 100644 Content.Server/_Scp/Holding/ScpHoldingSystem.Feedback.cs delete mode 100644 Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.Feedback.cs diff --git a/Content.Client/_Scp/Holding/ScpHoldingSystem.Feedback.cs b/Content.Client/_Scp/Holding/ScpHoldingSystem.Feedback.cs deleted file mode 100644 index 59ce2f0f533..00000000000 --- a/Content.Client/_Scp/Holding/ScpHoldingSystem.Feedback.cs +++ /dev/null @@ -1,54 +0,0 @@ -using Content.Shared._Scp.Holding.Components; -using Content.Shared.Coordinates; -using Robust.Shared.Audio; -using Robust.Shared.Audio.Systems; -using Robust.Shared.Prototypes; - -namespace Content.Client._Scp.Holding; - -public sealed partial class ScpHoldingSystem -{ - [Dependency] private readonly SharedAudioSystem _audio = default!; - - protected override void Popup(EntityUid target, string key, params (string, object)[] args) - { - } - - protected override void ShowBreakoutAttemptFeedback(Entity held) - { - if (!_timing.IsFirstTimePredicted) - return; - - if (!TryComp(held, out var holdable)) - return; - - foreach (var holderUid in held.Comp.Holders) - { - if (!TryComp(holderUid, out var holder)) - continue; - - if (holder.Target != held.Owner) - continue; - - SpawnBreakoutAttemptEffect(holderUid, holdable.BreakoutAttemptEffect); - } - - PlayBreakoutAttemptSound(held.Owner, holdable.BreakoutAttemptSound); - } - - private void SpawnBreakoutAttemptEffect(EntityUid holderUid, EntProtoId? effect) - { - if (effect == null) - return; - - PredictedSpawnAttachedTo(effect.Value, holderUid.ToCoordinates()); - } - - private void PlayBreakoutAttemptSound(EntityUid targetUid, SoundSpecifier? sound) - { - if (sound == null) - return; - - _audio.PlayPredicted(sound, targetUid, targetUid); - } -} diff --git a/Content.Client/_Scp/Holding/ScpHoldingSystem.cs b/Content.Client/_Scp/Holding/ScpHoldingSystem.cs index a45a323ac89..16f04dc8df4 100644 --- a/Content.Client/_Scp/Holding/ScpHoldingSystem.cs +++ b/Content.Client/_Scp/Holding/ScpHoldingSystem.cs @@ -74,7 +74,7 @@ private void OnHeldAfterState(Entity ent, ref AfterA private void OnHolderAfterState(Entity ent, ref AfterAutoHandleStateEvent args) { - if (_player.LocalEntity != ent.Owner) + if (_player.LocalEntity != ent) return; ReconcileLocalHolderState(ent); @@ -85,7 +85,7 @@ private void OnBlockerStartup(Entity ent, ref Compo if (!_timing.ApplyingState) return; - ReconcileLocalHolderBlocker(ent.Owner); + ReconcileLocalHolderBlocker(ent); } private void OnBlockerEquipped(Entity ent, ref GotEquippedHandEvent args) @@ -93,7 +93,7 @@ private void OnBlockerEquipped(Entity ent, ref GotE if (!_timing.ApplyingState) return; - ReconcileLocalHolderBlocker(ent.Owner, args.User); + ReconcileLocalHolderBlocker(ent, args.User); } private void OnUpdateHeldPredicted(Entity ent, ref UpdateIsPredictedEvent args) @@ -101,13 +101,13 @@ private void OnUpdateHeldPredicted(Entity ent, ref U if (_player.LocalEntity is not { Valid: true } local) return; - if (ent.Owner == local) - { - args.IsPredicted = true; - return; - } + if ((EntityUid) ent == local) + { + args.IsPredicted = true; + return; + } - if (HasComp(ent.Owner)) + if (HasComp(ent)) { args.BlockPrediction = true; return; @@ -115,7 +115,7 @@ private void OnUpdateHeldPredicted(Entity ent, ref U if (_activeHolderQuery.TryComp(local, out var localHolder)) { - if (localHolder.Target == ent.Owner) + if (localHolder.Target == ent) { args.IsPredicted = true; return; @@ -139,7 +139,7 @@ private void ReconcileHeldAfterState(Entity held) { _physics.UpdateIsPredicted(held); - if (HasComp(held.Owner)) + if (HasComp(held)) SyncPlaceholderHands(held); } @@ -200,7 +200,7 @@ private void ReconcileLocalHolderBlockerSteadyState(Entity holderHands = (holder, hands); + + foreach (var heldItem in _hands.EnumerateHeld(holderHands)) { if (!_virtualItemQuery.TryComp(heldItem, out var virtualItem)) continue; @@ -245,14 +247,14 @@ private void ReconcileLocalHolderBlockerSteadyState(Entity(spawnedVirtualItem.Value); - _hands.DoPickup(holder.Owner, emptyHand, spawnedVirtualItem.Value, hands); + _hands.DoPickup(holder, emptyHand, spawnedVirtualItem.Value, hands); currentPredictedBlockerCount++; } } diff --git a/Content.Server/_Scp/Holding/ScpHoldingSystem.Feedback.cs b/Content.Server/_Scp/Holding/ScpHoldingSystem.Feedback.cs deleted file mode 100644 index f06c85eb6c6..00000000000 --- a/Content.Server/_Scp/Holding/ScpHoldingSystem.Feedback.cs +++ /dev/null @@ -1,54 +0,0 @@ -using Content.Server.Popups; -using Content.Shared._Scp.Holding.Components; -using Content.Shared.Coordinates; -using Robust.Shared.Audio; -using Robust.Shared.Audio.Systems; -using Robust.Shared.Prototypes; - -namespace Content.Server._Scp.Holding; - -public sealed partial class ScpHoldingSystem -{ - [Dependency] private readonly SharedAudioSystem _audio = default!; - [Dependency] private readonly PopupSystem _popup = default!; - - protected override void Popup(EntityUid target, string key, params (string, object)[] args) - { - _popup.PopupEntity(Loc.GetString(key, args), target, target); - } - - protected override void ShowBreakoutAttemptFeedback(Entity held) - { - if (!TryComp(held, out var holdable)) - return; - - foreach (var holderUid in held.Comp.Holders) - { - if (!TryComp(holderUid, out var holder)) - continue; - - if (holder.Target != held.Owner) - continue; - - SpawnBreakoutAttemptEffect(holderUid, holdable.BreakoutAttemptEffect); - } - - PlayBreakoutAttemptSound(held.Owner, holdable.BreakoutAttemptSound); - } - - private void SpawnBreakoutAttemptEffect(EntityUid holderUid, EntProtoId? effect) - { - if (effect == null) - return; - - SpawnAttachedTo(effect.Value, holderUid.ToCoordinates()); - } - - private void PlayBreakoutAttemptSound(EntityUid targetUid, SoundSpecifier? sound) - { - if (sound == null) - return; - - _audio.PlayPvs(sound, targetUid); - } -} diff --git a/Content.Server/_Scp/Holding/ScpHoldingSystem.cs b/Content.Server/_Scp/Holding/ScpHoldingSystem.cs index 2256bbbce80..5e35960338e 100644 --- a/Content.Server/_Scp/Holding/ScpHoldingSystem.cs +++ b/Content.Server/_Scp/Holding/ScpHoldingSystem.cs @@ -24,13 +24,13 @@ protected override void OnHeldStateShutdown(Entity h private void OnHoldShutdown(Entity ent, ref ComponentShutdown args) { - if (!TryComp(ent.Owner, out var holder)) + if (!TryComp(ent, out var holder)) return; if (holder.Target == null) return; - ReleaseHolderContribution(ent.Owner, holder.Target.Value, clearIfEmpty: true); + ReleaseHolderContribution(ent, holder.Target.Value, clearIfEmpty: true); } private void OnHandCountChanged(Entity ent, ref HandCountChangedEvent args) diff --git a/Content.Shared/_Scp/Holding/Components/ScpHoldableComponent.cs b/Content.Shared/_Scp/Holding/Components/ScpHoldableComponent.cs index 8c3e3c55257..aa51b5a15be 100644 --- a/Content.Shared/_Scp/Holding/Components/ScpHoldableComponent.cs +++ b/Content.Shared/_Scp/Holding/Components/ScpHoldableComponent.cs @@ -131,12 +131,12 @@ public sealed partial class ScpHoldableComponent : Component /// Lower values make the target heavier to move. /// [DataField, AutoNetworkedField] - public float HolderWalkModifier = 0.5f; + public float HolderWalkModifier = 0.7f; /// /// Sprint speed modifier applied to holders while they move this target. /// Lower values make the target heavier to move. /// [DataField, AutoNetworkedField] - public float HolderSprintModifier = 0.5f; + public float HolderSprintModifier = 0.7f; } diff --git a/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.Actions.cs b/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.Actions.cs index 7350ca60dfc..5ec5d79ce61 100644 --- a/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.Actions.cs +++ b/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.Actions.cs @@ -27,7 +27,7 @@ public bool TryToggleHold(Entity holder, EntityUid target, b if (activeHolder.Target.Value == target) return TryReleaseHold(holder, target); - Popup(holder, "scp-hold-already-holding-other"); + _popup.PopupClient("scp-hold-already-holding-other", holder); return false; } @@ -65,7 +65,7 @@ public bool CanReleaseHold(Entity holder, EntityUid target, if (activeHolder.Target != target) { if (!quiet) - Popup(holder, "scp-hold-already-holding-other"); + _popup.PopupClient("scp-hold-already-holding-other", holder); return false; } @@ -89,7 +89,7 @@ public bool CanToggleHold( if (!_holdableQuery.TryComp(target, out var holdable)) { if (!quiet) - Popup(holder, "scp-hold-target-not-holdable", ("target", target)); + _popup.PopupClient(Loc.GetString("scp-hold-target-not-holdable", ("target", target)), holder); return false; } @@ -97,7 +97,7 @@ public bool CanToggleHold( if (!_whitelist.CheckBoth(target, holder.Comp.HoldableBlacklist, holder.Comp.HoldableWhitelist)) { if (!quiet) - Popup(holder, "scp-hold-target-invalid", ("target", target)); + _popup.PopupClient(Loc.GetString("scp-hold-target-invalid", ("target", target)), holder); return false; } @@ -105,7 +105,7 @@ public bool CanToggleHold( if (!_whitelist.CheckBoth(holder, holdable.HolderBlacklist, holdable.HolderWhitelist)) { if (!quiet) - Popup(holder, "scp-hold-target-invalid", ("target", target)); + _popup.PopupClient(Loc.GetString("scp-hold-target-invalid", ("target", target)), holder); return false; } @@ -113,7 +113,7 @@ public bool CanToggleHold( if (!_moverQuery.HasComp(holder)) { if (!quiet) - Popup(holder, "scp-hold-target-invalid", ("target", target)); + _popup.PopupClient(Loc.GetString("scp-hold-target-invalid", ("target", target)), holder); return false; } @@ -121,7 +121,7 @@ public bool CanToggleHold( if (!_moverQuery.HasComp(target)) { if (!quiet) - Popup(holder, "scp-hold-target-invalid", ("target", target)); + _popup.PopupClient(Loc.GetString("scp-hold-target-invalid", ("target", target)), holder); return false; } @@ -129,7 +129,7 @@ public bool CanToggleHold( if (!_physicsQuery.TryComp(target, out var targetPhysics)) { if (!quiet) - Popup(holder, "scp-hold-target-invalid", ("target", target)); + _popup.PopupClient(Loc.GetString("scp-hold-target-invalid", ("target", target)), holder); return false; } @@ -137,7 +137,7 @@ public bool CanToggleHold( if (targetPhysics.BodyType == BodyType.Static) { if (!quiet) - Popup(holder, "scp-hold-target-invalid", ("target", target)); + _popup.PopupClient(Loc.GetString("scp-hold-target-invalid", ("target", target)), holder); return false; } @@ -145,7 +145,7 @@ public bool CanToggleHold( if (!_container.IsInSameOrNoContainer(holder.Owner, target)) { if (!quiet) - Popup(holder, "scp-hold-target-invalid", ("target", target)); + _popup.PopupClient(Loc.GetString("scp-hold-target-invalid", ("target", target)), holder); return false; } @@ -153,16 +153,16 @@ public bool CanToggleHold( if (TryComp(target, out _)) { if (!quiet) - Popup(holder, "scp-hold-target-immune", ("target", target)); + _popup.PopupClient(Loc.GetString("scp-hold-target-immune", ("target", target)), holder); return false; } var requiredHolderHandCount = GetRequiredHolderHandCount(holdable); - if (!ignoreHandAvailability && !HasAvailableHolderHands(holder.Owner, requiredHolderHandCount)) + if (!ignoreHandAvailability && !HasAvailableHolderHands(holder, requiredHolderHandCount)) { if (!quiet) - Popup(holder, "scp-hold-holder-no-free-hand", ("target", target)); + _popup.PopupClient(Loc.GetString("scp-hold-holder-no-free-hand", ("target", target)), holder); return false; } @@ -173,7 +173,7 @@ public bool CanToggleHold( if (_activeHoldableFullHoldStateQuery.HasComp(target)) { if (!quiet) - Popup(holder.Owner, "scp-hold-target-fully-held", ("target", target)); + _popup.PopupClient(Loc.GetString("scp-hold-target-fully-held", ("target", target)), holder); return false; } @@ -182,7 +182,7 @@ public bool CanToggleHold( if (!_interaction.InRangeUnobstructed(holder.Owner, target, range)) { if (!quiet) - Popup(holder, "scp-hold-target-too-far", ("target", target)); + _popup.PopupClient(Loc.GetString("scp-hold-target-too-far", ("target", target)), holder); return false; } @@ -212,7 +212,7 @@ protected bool TryGetRequiredHolderHandCount(EntityUid targetUid, out int requir public bool TryBreakOut(Entity held, bool viaMovement) { - return _activeHoldableFullHoldStateQuery.HasComp(held.Owner) + return _activeHoldableFullHoldStateQuery.HasComp(held) ? TryStartFullBreakout(held, viaMovement) : TrySoftBreakOut(held, viaMovement); } @@ -222,7 +222,7 @@ public bool TryForceBreakOut(Entity held, bool viaM if (!Resolve(held, ref held.Comp, false)) return false; - BreakOut((held.Owner, held.Comp), viaMovement, applyImmunity); + BreakOut(held!, viaMovement, applyImmunity); return true; } @@ -237,7 +237,7 @@ private bool TrySoftBreakOut(Entity held, bool viaMo return false; if (!viaMovement) - Popup(held, "scp-hold-breakout-start"); + _popup.PopupClient(Loc.GetString("scp-hold-breakout-start"), held); BreakOut(held, viaMovement, applyImmunity: false); return true; @@ -245,7 +245,7 @@ private bool TrySoftBreakOut(Entity held, bool viaMo private bool TryStartFullBreakout(Entity held, bool viaMovement) { - if (!_activeHoldableFullHoldStateQuery.TryComp(held.Owner, out var fullHeld)) + if (!_activeHoldableFullHoldStateQuery.TryComp(held, out var fullHeld)) return false; if (!TryGetHeldHoldable(held, out var holdable)) @@ -253,7 +253,7 @@ private bool TryStartFullBreakout(Entity held, bool if (fullHeld.StartedAt == TimeSpan.Zero) { - Popup(held, "scp-hold-breakout-too-early", ("seconds", 1)); + _popup.PopupClient(Loc.GetString("scp-hold-breakout-too-early", ("seconds", 1)), held); return false; } @@ -262,20 +262,20 @@ private bool TryStartFullBreakout(Entity held, bool { var remaining = breakoutAvailableAt - _timing.CurTime; var remainingSeconds = Math.Max(1, (int)Math.Ceiling(remaining.TotalSeconds)); - Popup(held, "scp-hold-breakout-too-early", ("seconds", remainingSeconds)); + _popup.PopupClient(Loc.GetString("scp-hold-breakout-too-early", ("seconds", remainingSeconds)), held); return false; } - if (_breakoutAttemptQuery.HasComp(held.Owner)) + if (_breakoutAttemptQuery.HasComp(held)) return true; var doAfter = new DoAfterArgs( EntityManager, - held.Owner, + held, holdable.FullBreakoutDuration, new ScpHoldBreakoutDoAfterEvent(viaMovement), - held.Owner, - target: held.Owner) + held, + target: held) { BreakOnMove = true, BreakOnDamage = true, @@ -286,9 +286,9 @@ private bool TryStartFullBreakout(Entity held, bool if (!_doAfter.TryStartDoAfter(doAfter, out var id)) return false; - StartBreakoutAttempt(held.Owner, id.Value); + StartBreakoutAttempt(held, id.Value); - Popup(held, "scp-hold-breakout-start"); + _popup.PopupClient(Loc.GetString("scp-hold-breakout-start"), held); return true; } @@ -300,7 +300,7 @@ private bool CanStartHold(Entity holder, bool quiet = false) if (!quiet) { var remainingSeconds = Math.Max(1, (int)Math.Ceiling(remaining.TotalSeconds)); - Popup(holder, "scp-hold-holder-action-on-cooldown", ("seconds", remainingSeconds)); + _popup.PopupClient(Loc.GetString("scp-hold-holder-action-on-cooldown", ("seconds", remainingSeconds)), holder); } return false; @@ -348,7 +348,7 @@ private void SetHoldAvailableAt(Entity holder, TimeSpan? hol return; } - DirtyField(holder.Owner, holder.Comp, nameof(ScpHolderComponent.HoldAvailableAt)); + DirtyField(holder, holder.Comp, nameof(ScpHolderComponent.HoldAvailableAt)); } private bool CanPassHoldAttempt(EntityUid holderUid, EntityUid targetUid) @@ -359,15 +359,10 @@ private bool CanPassHoldAttempt(EntityUid holderUid, EntityUid targetUid) return !attempt.Cancelled; } - private void RaiseBreakoutEvent(Entity held, bool viaMovement, bool applyImmunity) - { - var ev = new ScpHoldBreakoutEvent(viaMovement, _activeHoldableFullHoldStateQuery.HasComp(held.Owner), applyImmunity); - RaiseLocalEvent(held.Owner, ref ev); - } - private void BreakOut(Entity held, bool viaMovement, bool applyImmunity) { - RaiseBreakoutEvent(held, viaMovement, applyImmunity); + var ev = new ScpHoldBreakoutEvent(viaMovement, _activeHoldableFullHoldStateQuery.HasComp(held), applyImmunity); + RaiseLocalEvent(held, ref ev); ClearHoldState(held, applyImmunity); } } diff --git a/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.BreakoutAttempt.cs b/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.BreakoutAttempt.cs index ad86c2bb8b6..e21646ef30b 100644 --- a/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.BreakoutAttempt.cs +++ b/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.BreakoutAttempt.cs @@ -1,7 +1,11 @@ using Content.Shared._Scp.Holding.Components; +using Content.Shared.Coordinates; using Content.Shared.DoAfter; using Content.Shared.Movement.Events; using Content.Shared.Movement.Systems; +using Robust.Shared.Audio; +using Robust.Shared.Audio.Systems; +using Robust.Shared.Prototypes; namespace Content.Shared._Scp.Holding.Systems; @@ -11,6 +15,8 @@ public abstract partial class SharedScpHoldingSystem * Breakout-attempt query cache, event routing, semantic state, and do-after handle tracking. */ + [Dependency] private readonly SharedAudioSystem _audio = default!; + private EntityQuery _breakoutAttemptQuery; private void InitializeBreakoutAttemptQueries() @@ -64,14 +70,14 @@ private void OnBreakoutAlert(Entity ent, ref ScpHold private void OnBreakoutDoAfter(Entity ent, ref ScpHoldBreakoutDoAfterEvent args) { - EndBreakoutAttempt(ent.Owner, cancelDoAfter: false); + EndBreakoutAttempt(ent, cancelDoAfter: false); if (args.Handled) return; if (args.Cancelled) { - Popup(ent, "scp-hold-breakout-interrupted"); + _popup.PopupClient(Loc.GetString("scp-hold-breakout-interrupted"), ent); return; } @@ -81,15 +87,15 @@ private void OnBreakoutDoAfter(Entity ent, ref ScpHo private void OnBreakoutAttemptStartup(Entity ent, ref ComponentStartup args) { - if (!_activeHoldableQuery.TryComp(ent.Owner, out var held)) + if (!_activeHoldableQuery.TryComp(ent, out var held)) return; - ShowBreakoutAttemptFeedback((ent.Owner, held)); + ShowBreakoutAttemptFeedback((ent, held)); } private void OnBreakoutAttemptShutdown(Entity ent, ref ComponentShutdown args) { - if (!_breakoutDoAfterIds.Remove(ent.Owner, out var doAfterId)) + if (!_breakoutDoAfterIds.Remove(ent, out var doAfterId)) return; CancelBreakoutAttemptDoAfter(doAfterId); @@ -122,4 +128,39 @@ private static bool IsBreakoutMovementPress(MoveInputEvent args) return (args.OldMovement & pressedButton) == MoveButtons.None; } + + private void ShowBreakoutAttemptFeedback(Entity held) + { + if (!_timing.IsFirstTimePredicted) + return; + + if (!TryComp(held, out var holdable)) + return; + + foreach (var holderUid in held.Comp.Holders) + { + if (!TryComp(holderUid, out var holder)) + continue; + + if (holder.Target != held) + continue; + + SpawnBreakoutAttemptEffect(holderUid, holdable.BreakoutAttemptEffect); + } + + PlayBreakoutAttemptSound(held, holdable.BreakoutAttemptSound); + } + + private void SpawnBreakoutAttemptEffect(EntityUid holderUid, EntProtoId? effect) + { + if (effect == null) + return; + + PredictedSpawnAttachedTo(effect.Value, holderUid.ToCoordinates()); + } + + private void PlayBreakoutAttemptSound(EntityUid targetUid, SoundSpecifier? sound) + { + _audio.PlayPredicted(sound, targetUid, targetUid); + } } diff --git a/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.CursorMove.cs b/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.CursorMove.cs index 6db8a8f2ae4..3ad3661f087 100644 --- a/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.CursorMove.cs +++ b/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.CursorMove.cs @@ -90,42 +90,42 @@ private bool TryGetValidatedCursorMoveState( cursorMove = null; holderUid = default; - if (!_activeHoldableCursorMoveQuery.TryComp(held.Owner, out var moveState)) + if (!_activeHoldableCursorMoveQuery.TryComp(held, out var moveState)) return false; if (!moveState.TargetCoordinates.IsValid(EntityManager)) { - ClearCursorMoveState(held.Owner); + ClearCursorMoveState(held); return false; } - if (_activeHoldableFullHoldStateQuery.HasComp(held.Owner)) + if (_activeHoldableFullHoldStateQuery.HasComp(held)) { - ClearCursorMoveState(held.Owner); + ClearCursorMoveState(held); return false; } if (!_activeHolderQuery.TryComp(moveState.Holder, out var holder)) { - ClearCursorMoveState(held.Owner); + ClearCursorMoveState(held); return false; } - if (holder.Target != held.Owner) + if (holder.Target != held) { - ClearCursorMoveState(held.Owner); + ClearCursorMoveState(held); return false; } if (!held.Comp.Holders.Contains(moveState.Holder)) { - ClearCursorMoveState(held.Owner); + ClearCursorMoveState(held); return false; } if (!_container.IsInSameOrNoContainer(moveState.Holder, held.Owner)) { - ClearCursorMoveState(held.Owner); + ClearCursorMoveState(held); return false; } @@ -136,13 +136,13 @@ private bool TryGetValidatedCursorMoveState( if (holderCoords.MapId != targetCoords.MapId) { - ClearCursorMoveState(held.Owner); + ClearCursorMoveState(held); return false; } if ((targetCoords.Position - holderCoords.Position).LengthSquared() > maintenanceRange * maintenanceRange) { - ClearCursorMoveState(held.Owner); + ClearCursorMoveState(held); return false; } @@ -185,7 +185,7 @@ private void SetCursorMoveState( EntityCoordinates targetCoordinates, bool active) { - var cursorMove = EnsureComp(held.Owner); + var cursorMove = EnsureComp(held); if (cursorMove.Holder == holderUid && cursorMove.TargetCoordinates == targetCoordinates && cursorMove.Active == active) @@ -196,7 +196,7 @@ private void SetCursorMoveState( cursorMove.Holder = holderUid; cursorMove.TargetCoordinates = targetCoordinates; cursorMove.Active = active; - Dirty(held.Owner, cursorMove); + Dirty(held, cursorMove); } private void ClearCursorMoveState(EntityUid heldUid) @@ -211,21 +211,21 @@ private void UpdateCursorMoveDrag( EntityUid holderUid, ActiveStateScpHoldableCursorMoveComponent cursorMove) { - if (!_physicsQuery.TryComp(held.Owner, out var heldPhysics)) + if (!_physicsQuery.TryComp(held, out var heldPhysics)) return; if (!_container.IsInSameOrNoContainer(holderUid, held.Owner)) { - ClearCursorMoveState(held.Owner); + ClearCursorMoveState(held); return; } var targetCoords = _transform.ToMapCoordinates(cursorMove.TargetCoordinates); - var heldCoords = _transform.GetMapCoordinates(held.Owner); + var heldCoords = _transform.GetMapCoordinates(held); if (targetCoords.MapId != heldCoords.MapId) { - ClearCursorMoveState(held.Owner); + ClearCursorMoveState(held); return; } @@ -235,7 +235,7 @@ private void UpdateCursorMoveDrag( if (!cursorMove.Active && correctionDistance > holdable.SoftDragSettleTolerance) { cursorMove.Active = true; - Dirty(held.Owner, cursorMove); + Dirty(held, cursorMove); } Vector2 desiredVelocity; @@ -244,7 +244,7 @@ private void UpdateCursorMoveDrag( if (cursorMove.Active) { cursorMove.Active = false; - Dirty(held.Owner, cursorMove); + Dirty(held, cursorMove); } desiredVelocity = Vector2.Zero; @@ -264,7 +264,7 @@ private void UpdateCursorMoveDrag( desiredVelocity += correctionDirection * awaySpeed * holdable.SoftDragAwayVelocityStrength; } - ApplyHeldVelocity(held.Owner, desiredVelocity, heldPhysics, holdable); + ApplyHeldVelocity(held, desiredVelocity, heldPhysics, holdable); } private void OnHolderMove(Entity ent, ref MoveEvent args) diff --git a/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.Drag.cs b/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.Drag.cs index 8ff48964ef1..2800dcb8a90 100644 --- a/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.Drag.cs +++ b/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.Drag.cs @@ -44,7 +44,7 @@ private void UpdateSoftDrag( return; } - if (anchorHolder.Target != held.Owner) + if (anchorHolder.Target != held) return; if (!_container.IsInSameOrNoContainer(dragAnchor, held.Owner)) @@ -53,11 +53,11 @@ private void UpdateSoftDrag( if (!_interaction.InRangeUnobstructed(dragAnchor, held.Owner, maintenanceRange)) return; - if (!_physicsQuery.TryComp(held.Owner, out var heldPhysics)) + if (!_physicsQuery.TryComp(held, out var heldPhysics)) return; var holderCoords = _transform.GetMapCoordinates(dragAnchor); - var heldCoords = _transform.GetMapCoordinates(held.Owner); + var heldCoords = _transform.GetMapCoordinates(held); if (holderCoords.MapId != heldCoords.MapId) return; @@ -92,7 +92,7 @@ private void UpdateSoftDrag( desiredVelocity += correctionDirection * awaySpeed * holdable.SoftDragAwayVelocityStrength; } - ApplyHeldVelocity(held.Owner, desiredVelocity, heldPhysics, holdable); + ApplyHeldVelocity(held, desiredVelocity, heldPhysics, holdable); } private static float GetDesiredSoftDragDistance(ScpHoldableComponent holdable) @@ -163,7 +163,7 @@ private void OnHeldPreventCollide(Entity ent, ref Pr return; if (_activeHolderQuery.TryComp(args.OtherEntity, out var holder) && - holder.Target == ent.Owner) + holder.Target == ent) { args.Cancelled = true; } diff --git a/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.Feedback.cs b/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.Feedback.cs deleted file mode 100644 index 586ffc8ab7a..00000000000 --- a/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.Feedback.cs +++ /dev/null @@ -1,10 +0,0 @@ -using Content.Shared._Scp.Holding.Components; - -namespace Content.Shared._Scp.Holding.Systems; - -public abstract partial class SharedScpHoldingSystem -{ - protected abstract void ShowBreakoutAttemptFeedback(Entity held); - - protected abstract void Popup(EntityUid target, string key, params (string, object)[] args); -} diff --git a/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.Hands.cs b/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.Hands.cs index b71da3d0a47..18268a48b93 100644 --- a/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.Hands.cs +++ b/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.Hands.cs @@ -45,12 +45,12 @@ private void InitializeHandEvents() protected void SyncPlaceholderHands(Entity held) { - if (!_handsQuery.TryComp(held.Owner, out var hands)) + if (!_handsQuery.TryComp(held, out var hands)) return; - if (!_activeHoldableFullHoldStateQuery.HasComp(held.Owner)) + if (!_activeHoldableFullHoldStateQuery.HasComp(held)) { - DeleteHeldHandBlockers(held.Owner); + DeleteHeldHandBlockers(held); return; } @@ -58,11 +58,11 @@ protected void SyncPlaceholderHands(Entity held) if (_placeholderIcons.Count == 0) { - DeleteHeldHandBlockers(held.Owner); + DeleteHeldHandBlockers(held); return; } - var heldHands = (held.Owner, hands); + var heldHands = (held, hands); DropHeldItemsForPlaceholders(heldHands); DeleteInvalidHeldHandBlockers(heldHands); EnsureHeldHandBlockers(heldHands); @@ -74,11 +74,13 @@ private void CollectPlaceholderIconHolders(Entity he foreach (var holderUid in held.Comp.Holders) { - if (_activeHolderQuery.TryComp(holderUid, out var holder) && - holder.Target == held.Owner) - { - _placeholderIcons.Add(holderUid); - } + if (!_activeHolderQuery.TryComp(holderUid, out var holder)) + continue; + + if (holder.Target != held) + continue; + + _placeholderIcons.Add(holderUid); } } @@ -109,14 +111,12 @@ private void DeleteInvalidHeldHandBlockers(Entity held) continue; if (!IsValidHeldHandBlocker(virtualItem)) - { _virtualBlockersToDelete.Add((heldItem, virtualItem)); - } } foreach (var virtualItem in _virtualBlockersToDelete) { - _virtualItem.DeleteVirtualItem(virtualItem, held.Owner); + _virtualItem.DeleteVirtualItem(virtualItem, held); } } @@ -131,12 +131,12 @@ private void EnsureHeldHandBlockers(Entity held) while (_hands.TryGetEmptyHand(held, out var emptyHand)) { var holderUid = _placeholderIcons[iconIndex % _placeholderIcons.Count]; - if (!_virtualItem.TrySpawnVirtualItem(holderUid, held.Owner, out var virtualItem)) + if (!_virtualItem.TrySpawnVirtualItem(holderUid, held, out var virtualItem)) break; EnsureComp(virtualItem.Value); EnsureComp(virtualItem.Value); - _hands.DoPickup(held.Owner, emptyHand, virtualItem.Value, held.Comp); + _hands.DoPickup(held, emptyHand, virtualItem.Value, held.Comp); iconIndex++; } @@ -150,22 +150,22 @@ private void SyncHolderHandBlocker(Entity holder) var validBlockerCount = 0; var requiredHolderHandCount = 0; - if (holderActive && - _activeHoldableQuery.HasComp(target) && - TryGetRequiredHolderHandCount(target.Value, out var resolvedRequiredHolderHandCount)) + if (holderActive + && _activeHoldableQuery.HasComp(target) + && TryGetRequiredHolderHandCount(target.Value, out var resolvedRequiredHolderHandCount)) { requiredHolderHandCount = resolvedRequiredHolderHandCount; } - foreach (var heldItem in _hands.EnumerateHeld(holder.Owner)) + foreach (var heldItem in _hands.EnumerateHeld((EntityUid) holder)) { if (!_virtualItemQuery.TryComp(heldItem, out var virtualItem)) continue; var ownedBlocker = _holdHandBlockerQuery.HasComp(heldItem); - var matchesCurrentTarget = holderActive && - target != null && - virtualItem.BlockingEntity == target.Value; + var matchesCurrentTarget = holderActive + && target != null + && virtualItem.BlockingEntity == target.Value; if (ownedBlocker && matchesCurrentTarget) { @@ -183,43 +183,42 @@ private void SyncHolderHandBlocker(Entity holder) foreach (var virtualItem in _virtualBlockersToDelete) { - RemoveHolderHandBlocker(holder.Owner, virtualItem); + RemoveHolderHandBlocker(holder, virtualItem); } - if (!holderActive || - target == null) - { + if (!holderActive || target == null) return; - } - if (!_handsQuery.TryComp(holder.Owner, out var hands)) + if (!_handsQuery.TryComp(holder, out var hands)) { - ReleaseHolderContribution(holder.Owner, target.Value, clearIfEmpty: true); + ReleaseHolderContribution(holder, target.Value, clearIfEmpty: true); return; } + var holderHands = (holder, hands); + while (validBlockerCount < requiredHolderHandCount) { - if (!_hands.TryGetEmptyHand((holder.Owner, hands), out var emptyHand) || - !_virtualItem.TrySpawnVirtualItem(target.Value, holder.Owner, out var spawnedVirtualItem)) - { + if (!_hands.TryGetEmptyHand(holderHands, out var emptyHand)) + break; + + if (!_virtualItem.TrySpawnVirtualItem(target.Value, holder, out var spawnedVirtualItem)) break; - } EnsureComp(spawnedVirtualItem.Value); - _hands.DoPickup(holder.Owner, emptyHand, spawnedVirtualItem.Value, hands); + _hands.DoPickup(holder, emptyHand, spawnedVirtualItem.Value, hands); validBlockerCount++; } - validBlockerCount = CountOwnedHolderHandBlockers(holder.Owner, target.Value); + validBlockerCount = CountOwnedHolderHandBlockers(holder, target.Value); if (validBlockerCount < requiredHolderHandCount) - ReleaseHolderContribution(holder.Owner, target.Value, clearIfEmpty: true); + ReleaseHolderContribution(holder, target.Value, clearIfEmpty: true); } private bool HasAvailableHolderHands(EntityUid holderUid, int requiredHandCount) { - return _handsQuery.TryComp(holderUid, out var hands) && - _hands.CountFreeHands((holderUid, hands)) >= requiredHandCount; + return _handsQuery.TryComp(holderUid, out var hands) + && _hands.CountFreeHands((holderUid, hands)) >= requiredHandCount; } private int CountOwnedHolderHandBlockers(EntityUid holderUid, EntityUid targetUid) @@ -227,12 +226,14 @@ private int CountOwnedHolderHandBlockers(EntityUid holderUid, EntityUid targetUi var blockerCount = 0; foreach (var heldItem in _hands.EnumerateHeld(holderUid)) { - if (!_holdHandBlockerQuery.HasComp(heldItem) || - !_virtualItemQuery.TryComp(heldItem, out var virtualItem) || - virtualItem.BlockingEntity != targetUid) - { + if (!_holdHandBlockerQuery.HasComp(heldItem)) + continue; + + if (!_virtualItemQuery.TryComp(heldItem, out var virtualItem)) + continue; + + if (virtualItem.BlockingEntity != targetUid) continue; - } blockerCount++; } @@ -243,7 +244,7 @@ private int CountOwnedHolderHandBlockers(EntityUid holderUid, EntityUid targetUi private void RemoveHolderHandBlocker(EntityUid holderUid, Entity virtualItem) { if (_handsQuery.TryComp(holderUid, out var hands) && - _hands.IsHolding((holderUid, hands), virtualItem.Owner, out var hand)) + _hands.IsHolding((holderUid, hands), virtualItem, out var hand)) { _hands.DoDrop((holderUid, hands), hand, doDropInteraction: false, log: false); return; @@ -258,11 +259,13 @@ private void DeleteHolderHandBlockers(EntityUid holderUid) foreach (var heldItem in _hands.EnumerateHeld(holderUid)) { - if (_holdHandBlockerQuery.HasComp(heldItem) && - _virtualItemQuery.TryComp(heldItem, out var virtualItem)) - { - _virtualBlockersToDelete.Add((heldItem, virtualItem)); - } + if (!_holdHandBlockerQuery.HasComp(heldItem)) + continue; + + if (!_virtualItemQuery.TryComp(heldItem, out var virtualItem)) + continue; + + _virtualBlockersToDelete.Add((heldItem, virtualItem)); } foreach (var virtualItem in _virtualBlockersToDelete) @@ -295,7 +298,7 @@ private void OnHolderBeforeThrow(Entity ent, ref Befor if (ent.Comp.Target == null) return; - if (!TryComp(args.ItemUid, out _)) + if (!HasComp(args.ItemUid)) return; if (!_virtualItemQuery.TryComp(args.ItemUid, out var virtualItem)) @@ -304,7 +307,7 @@ private void OnHolderBeforeThrow(Entity ent, ref Befor if (virtualItem.BlockingEntity != ent.Comp.Target.Value) return; - ReleaseHolderContribution(ent.Owner, ent.Comp.Target.Value, clearIfEmpty: true); + ReleaseHolderContribution(ent, ent.Comp.Target.Value, clearIfEmpty: true); args.Cancelled = true; } @@ -324,29 +327,30 @@ private void OnHolderHandsModified(Entity ent, ref Did private void OnHolderBlockerGettingDropped(Entity ent, ref GettingDroppedAttemptEvent args) { - if (!_virtualItemQuery.TryComp(ent.Owner, out var virtualItem) || - !TryComp(args.User, out var holder) || - !TryReleaseHold((args.User, holder), virtualItem.BlockingEntity)) - { + if (!_virtualItemQuery.TryComp(ent, out var virtualItem)) + return; + + if (!TryComp(args.User, out var holder)) + return; + + if (!TryReleaseHold((args.User, holder), virtualItem.BlockingEntity)) return; - } args.Cancelled = true; } private void OnHolderVirtualItemDeleted(Entity ent, ref VirtualItemDeletedEvent args) { - if (_timing.ApplyingState || - ent.Comp.Target == null || - ent.Comp.Target != args.BlockingEntity) - { + if (_timing.ApplyingState) + return; + + if (ent.Comp.Target == null || ent.Comp.Target != args.BlockingEntity) return; - } if (!TryGetRequiredHolderHandCount(args.BlockingEntity, out var requiredHolderHandCount)) return; - if (CountOwnedHolderHandBlockers(ent.Owner, args.BlockingEntity) >= requiredHolderHandCount) + if (CountOwnedHolderHandBlockers(ent, args.BlockingEntity) >= requiredHolderHandCount) return; SyncHolderState(ent); diff --git a/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.Lifecycle.cs b/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.Lifecycle.cs index 982ca10b8e6..323ea74b55c 100644 --- a/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.Lifecycle.cs +++ b/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.Lifecycle.cs @@ -48,24 +48,24 @@ private void OnHeldRemove(Entity ent, ref ComponentR ValidateAllActions(ent.Owner); } - private static void OnFullHeldUpdateCanMove(Entity ent, ref UpdateCanMoveEvent args) + private void OnFullHeldUpdateCanMove(Entity ent, ref UpdateCanMoveEvent args) { args.Cancel(); } private void OnFullHeldStartup(Entity ent, ref ComponentStartup args) { - if (_activeHoldableQuery.TryComp(ent.Owner, out var held)) - SyncPlaceholderHands((ent.Owner, held)); + if (_activeHoldableQuery.TryComp(ent, out var held)) + SyncPlaceholderHands((ent, held)); - ZeroHeldVelocity(ent.Owner); - _actionBlocker.UpdateCanMove(ent.Owner); + ZeroHeldVelocity(ent); + _actionBlocker.UpdateCanMove(ent); } private void OnFullHeldRemove(Entity ent, ref ComponentRemove args) { - DeleteHeldHandBlockers(ent.Owner); - _actionBlocker.UpdateCanMove(ent.Owner); + DeleteHeldHandBlockers(ent); + _actionBlocker.UpdateCanMove(ent); } private void OnHolderStartup(Entity ent, ref ComponentStartup args) @@ -79,20 +79,20 @@ private void OnHolderStartup(Entity ent, ref Component private void OnHolderShutdown(Entity ent, ref ComponentShutdown args) { ent.Comp.Target = null; - DeleteHolderHandBlockers(ent.Owner); + DeleteHolderHandBlockers(ent); if (!_timing.ApplyingState) - RemComp(ent.Owner); + RemComp(ent); } private void OnHolderSlowdownRemove(Entity ent, ref ComponentRemove args) { - _movement.RefreshMovementSpeedModifiers(ent.Owner); + _movement.RefreshMovementSpeedModifiers(ent); } private void OnHolderSlowdownAfterState(Entity ent, ref AfterAutoHandleStateEvent args) { - _movement.RefreshMovementSpeedModifiers(ent.Owner); + _movement.RefreshMovementSpeedModifiers(ent); } private void OnHolderSlowdownRefreshMoveSpeed(Entity ent, ref RefreshMovementSpeedModifiersEvent args) diff --git a/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.Restrictions.cs b/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.Restrictions.cs index 4a525e28a2b..9727bd66f98 100644 --- a/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.Restrictions.cs +++ b/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.Restrictions.cs @@ -81,7 +81,7 @@ private bool IsHeldAtStage(Entity held, ScpHoldStage return stage switch { ScpHoldStage.Soft => true, - ScpHoldStage.Full => _activeHoldableFullHoldStateQuery.HasComp(held.Owner), + ScpHoldStage.Full => _activeHoldableFullHoldStateQuery.HasComp(held), _ => false, }; } diff --git a/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.State.cs b/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.State.cs index 9d152562b32..d4e01c47aa0 100644 --- a/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.State.cs +++ b/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.State.cs @@ -1,6 +1,5 @@ using System.Diagnostics.CodeAnalysis; using Content.Shared._Scp.Holding.Components; -using Content.Shared.Body.Components; using Content.Shared.Body.Part; using Content.Shared.Body.Systems; @@ -22,7 +21,6 @@ public abstract partial class SharedScpHoldingSystem private EntityQuery _holderConfigQuery; private EntityQuery _activeHolderQuery; private EntityQuery _activeHolderSlowdownStateQuery; - private EntityQuery _bodyQuery; private void InitializeStateQueries() { @@ -31,7 +29,6 @@ private void InitializeStateQueries() _holderConfigQuery = GetEntityQuery(); _activeHolderQuery = GetEntityQuery(); _activeHolderSlowdownStateQuery = GetEntityQuery(); - _bodyQuery = GetEntityQuery(); } protected void UpdateHeld(Entity held) @@ -48,32 +45,31 @@ protected void UpdateHeld(Entity held) var desiredSoftDragDistance = GetDesiredSoftDragDistance(holdable); var maintenanceRange = GetHoldMaintenanceRange(holdable, desiredSoftDragDistance); - if (!_activeHoldableFullHoldStateQuery.HasComp(held.Owner)) + if (!_activeHoldableFullHoldStateQuery.HasComp(held)) + { UpdateSoftDrag(held, holdable, dragAnchorUid, dragAnchor, maintenanceRange, desiredSoftDragDistance); + } else { - ClearCursorMoveState(held.Owner); - ZeroHeldVelocity(held.Owner); + ClearCursorMoveState(held); + ZeroHeldVelocity(held); } _holdersToRemove.Clear(); foreach (var holderUid in held.Comp.Holders) { - if (ShouldReleaseHolder(holderUid, held.Owner, maintenanceRange)) + if (ShouldReleaseHolder(holderUid, held, maintenanceRange)) _holdersToRemove.Add(holderUid); } foreach (var holderUid in _holdersToRemove) { - ReleaseHolderContribution(holderUid, held.Owner, clearIfEmpty: false); - - if (!_activeHoldableQuery.TryComp(held.Owner, out _)) - return; + ReleaseHolderContribution(holderUid, held, clearIfEmpty: false); } - if (_activeHoldableQuery.TryComp(held.Owner, out var refreshed)) - SyncHeldState((held.Owner, refreshed)); + if (_activeHoldableQuery.TryComp(held, out var refreshed)) + SyncHeldState((held, refreshed)); } private Entity EnsureHeldState(EntityUid target) @@ -99,7 +95,7 @@ private void AddHolderContribution(EntityUid holderUid, Entity(holderUid); - SetHolderTarget((holderUid, holder), held.Owner); + SetHolderTarget((holderUid, holder), held); SyncHolderState((holderUid, holder)); if (holderCreated) @@ -114,11 +110,11 @@ protected void ReleaseHolderContribution(EntityUid holderUid, EntityUid targetUi var removed = false; for (var i = held.Holders.Count - 1; i >= 0; i--) { - if (held.Holders[i] == holderUid) - { - held.Holders.RemoveAt(i); - removed = true; - } + if (held.Holders[i] != holderUid) + continue; + + held.Holders.RemoveAt(i); + removed = true; } if (removed) @@ -144,7 +140,7 @@ protected void ReleaseHolderContribution(EntityUid holderUid, EntityUid targetUi protected void SyncHeldState(Entity held) { - if (!_activeHoldableQuery.TryComp(held.Owner, out var heldComp)) + if (!_activeHoldableQuery.TryComp(held, out var heldComp)) return; held.Comp = heldComp; @@ -152,8 +148,8 @@ protected void SyncHeldState(Entity held) if (!TryGetHeldHoldable(held, out var holdable)) return; - held.Comp.RequiredHolderCount = GetRequiredHolderCount(held.Owner); - Dirty(held.Owner, held.Comp); + held.Comp.RequiredHolderCount = GetRequiredHolderCount(held); + Dirty(held, held.Comp); if (held.Comp.Holders.Count == 0) { @@ -183,15 +179,15 @@ protected void SyncHeldState(Entity held) private void EnterFullHold(Entity held, ScpHoldableComponent holdable) { - ClearCursorMoveState(held.Owner); + ClearCursorMoveState(held); - var fullHeldCreated = !_activeHoldableFullHoldStateQuery.TryComp(held.Owner, out var fullHeld); - fullHeld ??= EnsureComp(held.Owner); + var fullHeldCreated = !_activeHoldableFullHoldStateQuery.TryComp(held, out var fullHeld); + fullHeld ??= EnsureComp(held); if (fullHeldCreated) { fullHeld.StartedAt = _timing.CurTime; - Dirty(held.Owner, fullHeld); + Dirty(held, fullHeld); } UpdateHolderSlowdowns(held, holdable); @@ -200,16 +196,16 @@ private void EnterFullHold(Entity held, ScpHoldableC return; SyncPlaceholderHands(held); - ZeroHeldVelocity(held.Owner); + ZeroHeldVelocity(held); } private void ExitFullHold(Entity held) { - if (!_activeHoldableFullHoldStateQuery.HasComp(held.Owner)) + if (!_activeHoldableFullHoldStateQuery.HasComp(held)) return; - EndBreakoutAttempt(held.Owner, cancelDoAfter: true); - RemComp(held.Owner); + EndBreakoutAttempt(held, cancelDoAfter: true); + RemComp(held); } private bool TryGetDragAnchorHolder( @@ -222,7 +218,7 @@ private bool TryGetDragAnchorHolder( if (!_activeHolderQuery.TryComp(holderUid, out var holder)) continue; - if (holder.Target != held.Owner) + if (holder.Target != held) continue; dragAnchorUid = holderUid; @@ -237,14 +233,14 @@ private bool TryGetDragAnchorHolder( private void ClearHoldState(Entity held, bool applyImmunity) { - if (_activeHoldableQuery.TryComp(held.Owner, out var refreshed)) - held = (held.Owner, refreshed); + if (_activeHoldableQuery.TryComp(held, out var refreshed)) + held = (held, refreshed); - ClearCursorMoveState(held.Owner); - EndBreakoutAttempt(held.Owner, cancelDoAfter: true); + ClearCursorMoveState(held); + EndBreakoutAttempt(held, cancelDoAfter: true); - if (_activeHoldableFullHoldStateQuery.HasComp(held.Owner)) - RemComp(held.Owner); + if (_activeHoldableFullHoldStateQuery.HasComp(held)) + RemComp(held); _holderCooldownsToApply.Clear(); @@ -263,13 +259,13 @@ private void ClearHoldState(Entity held, bool applyI if (applyImmunity) { - if (_holdableQuery.TryComp(held.Owner, out var holdable)) + if (_holdableQuery.TryComp(held, out var holdable)) { - if (!TryComp(held.Owner, out var immune)) - immune = EnsureComp(held.Owner); + if (!TryComp(held, out var immune)) + immune = EnsureComp(held); immune.ExpiresAt = _timing.CurTime + holdable.PostBreakoutImmunity; - Dirty(held.Owner, immune); + Dirty(held, immune); } } @@ -278,7 +274,7 @@ private void ClearHoldState(Entity held, bool applyI ApplyFullBreakoutHolderCooldown(holderUid); } - RemComp(held.Owner); + RemComp(held); } private void UpdateHolderSlowdowns(Entity held, ScpHoldableComponent holdable) @@ -309,24 +305,18 @@ private void SetHolderSlowdown(EntityUid holderUid, float walkModifier, float sp private int GetRequiredHolderCount(EntityUid target) { - if (_bodyQuery.TryComp(target, out var body)) + var handCount = 0; + foreach (var _ in _body.GetBodyChildrenOfType(target, BodyPartType.Hand)) { - var handCount = 0; - foreach (var _ in _body.GetBodyChildrenOfType(target, BodyPartType.Hand, body)) - { - handCount++; - } - - if (handCount > 0) - return handCount; + handCount++; } - return 2; + return handCount; } private bool TryGetHeldHoldable(Entity held, [NotNullWhen(true)] out ScpHoldableComponent? holdable) { - if (_holdableQuery.TryComp(held.Owner, out holdable)) + if (_holdableQuery.TryComp(held, out holdable)) return true; ClearHoldState(held, applyImmunity: false); @@ -334,7 +324,7 @@ private bool TryGetHeldHoldable(Entity held, [NotNul return false; } - private bool ShouldReleaseHolder(EntityUid holderUid, EntityUid heldUid, float maintenanceRange) + private bool ShouldReleaseHolder(EntityUid holderUid, Entity held, float maintenanceRange) { if (!_holderConfigQuery.HasComp(holderUid)) return true; @@ -342,13 +332,13 @@ private bool ShouldReleaseHolder(EntityUid holderUid, EntityUid heldUid, float m if (!_activeHolderQuery.TryComp(holderUid, out var holder)) return true; - if (holder.Target != heldUid) + if (holder.Target != held) return true; - if (!_container.IsInSameOrNoContainer(holderUid, heldUid)) + if (!_container.IsInSameOrNoContainer(holderUid, held.Owner)) return true; - return !_interaction.InRangeUnobstructed(holderUid, heldUid, maintenanceRange); + return !_interaction.InRangeUnobstructed(holderUid, held.Owner, maintenanceRange); } private void SetHolderTarget(Entity holder, EntityUid? target) diff --git a/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.cs b/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.cs index d2dce98dd4e..1b85f1dfacd 100644 --- a/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.cs +++ b/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.cs @@ -59,14 +59,12 @@ public override void Shutdown() public override void Update(float frameTime) { - UpdateSharedState(frameTime); + UpdateSharedState(); UpdateHeldStates(); } - protected void UpdateSharedState(float frameTime) + private void UpdateSharedState() { - base.Update(frameTime); - var immuneQuery = EntityQueryEnumerator(); while (immuneQuery.MoveNext(out var uid, out var immune)) { @@ -75,7 +73,7 @@ protected void UpdateSharedState(float frameTime) } } - protected void UpdateAllHeldStates() + private void UpdateAllHeldStates() { var heldQuery = EntityQueryEnumerator(); while (heldQuery.MoveNext(out var uid, out var held)) diff --git a/Resources/Prototypes/_Scp/Entities/Mobs/Player/Scp/Main/scp096.yml b/Resources/Prototypes/_Scp/Entities/Mobs/Player/Scp/Main/scp096.yml index beb2a738f6a..6b9dc5318b9 100644 --- a/Resources/Prototypes/_Scp/Entities/Mobs/Player/Scp/Main/scp096.yml +++ b/Resources/Prototypes/_Scp/Entities/Mobs/Player/Scp/Main/scp096.yml @@ -117,6 +117,8 @@ disableExplosionRecursion: true - type: ScpHoldable holderHandsRequired: 2 + holderWalkModifier: 0.25 + holderSprintModifier: 0.25 - type: GhostPanelAntagonistMarker name: ghost-panel-antagonist-scp-name description: ghost-panel-antagonist-scp-description From 5bff06651b639f8208d428a972d5f70df0aeece8 Mon Sep 17 00:00:00 2001 From: drdth Date: Sun, 19 Apr 2026 08:46:28 +0300 Subject: [PATCH 20/27] refactor: move alerts to new system --- .../Components/ScpHoldableComponent.cs | 19 ++++----- .../Holding/Components/ScpHolderComponent.cs | 11 +++++ .../SharedScpHoldingSystem.BreakoutAttempt.cs | 30 ++++--------- .../Other/WorldAlert/WorldAlertSettings.cs | 21 ++++++++++ .../_Scp/Other/WorldAlert/WorldAlertSystem.cs | 42 +++++++++++++++++++ .../_Scp/Entities/Effects/world_alerts.yml | 25 +++++++++++ 6 files changed, 115 insertions(+), 33 deletions(-) create mode 100644 Content.Shared/_Scp/Other/WorldAlert/WorldAlertSettings.cs create mode 100644 Content.Shared/_Scp/Other/WorldAlert/WorldAlertSystem.cs create mode 100644 Resources/Prototypes/_Scp/Entities/Effects/world_alerts.yml diff --git a/Content.Shared/_Scp/Holding/Components/ScpHoldableComponent.cs b/Content.Shared/_Scp/Holding/Components/ScpHoldableComponent.cs index aa51b5a15be..c4610f9d467 100644 --- a/Content.Shared/_Scp/Holding/Components/ScpHoldableComponent.cs +++ b/Content.Shared/_Scp/Holding/Components/ScpHoldableComponent.cs @@ -1,7 +1,7 @@ +using Content.Shared._Scp.Other.WorldAlert; using Content.Shared.Whitelist; using Robust.Shared.Audio; using Robust.Shared.GameStates; -using Robust.Shared.Prototypes; namespace Content.Shared._Scp.Holding.Components; @@ -48,17 +48,16 @@ public sealed partial class ScpHoldableComponent : Component public TimeSpan PostBreakoutImmunity = TimeSpan.FromSeconds(5); /// - /// Optional effect prototype spawned on each holder when a full-hold breakout attempt starts. + /// Optional visual and audio feedback played when a full-hold breakout attempt starts. /// [DataField, AutoNetworkedField] - public EntProtoId? BreakoutAttemptEffect = "WhistleExclamation"; - - /// - /// Optional sound played from the held target when a full-hold breakout attempt starts. - /// - [DataField, AutoNetworkedField] - public SoundSpecifier? BreakoutAttemptSound = new SoundCollectionSpecifier("storageRustle", - AudioParams.Default.WithVolume(-2f).WithMaxDistance(4f).WithVariation(0.15f)); + public WorldAlertSettings BreakoutAttemptAlertSettings = new() + { + Prototype = "WorldAlertHandcuffs", + Sound = new SoundCollectionSpecifier("storageRustle", + AudioParams.Default.WithVolume(-2f).WithMaxDistance(4f).WithVariation(0.15f)), + DirectSound = true, + }; /// /// Maximum unobstructed range allowed between holder and target. diff --git a/Content.Shared/_Scp/Holding/Components/ScpHolderComponent.cs b/Content.Shared/_Scp/Holding/Components/ScpHolderComponent.cs index e8992e97883..72a2564c9e8 100644 --- a/Content.Shared/_Scp/Holding/Components/ScpHolderComponent.cs +++ b/Content.Shared/_Scp/Holding/Components/ScpHolderComponent.cs @@ -1,5 +1,7 @@ using Content.Shared._Scp.Holding.Systems; +using Content.Shared._Scp.Other.WorldAlert; using Content.Shared.Whitelist; +using Robust.Shared.Audio; using Robust.Shared.GameStates; namespace Content.Shared._Scp.Holding.Components; @@ -34,4 +36,13 @@ public sealed partial class ScpHolderComponent : Component /// [DataField, AutoNetworkedField] public TimeSpan HoldActionCooldown = TimeSpan.FromSeconds(1); + + [DataField, AutoNetworkedField] + public WorldAlertSettings BreakoutAttemptAlertSettings = new() + { + Prototype = "WhistleExclamation", + Sound = new SoundCollectionSpecifier("storageRustle", + AudioParams.Default.WithVolume(-2f).WithMaxDistance(4f).WithVariation(0.15f)), + DirectSound = true, + }; } diff --git a/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.BreakoutAttempt.cs b/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.BreakoutAttempt.cs index e21646ef30b..995705a199b 100644 --- a/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.BreakoutAttempt.cs +++ b/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.BreakoutAttempt.cs @@ -1,11 +1,8 @@ using Content.Shared._Scp.Holding.Components; -using Content.Shared.Coordinates; +using Content.Shared._Scp.Other.WorldAlert; using Content.Shared.DoAfter; using Content.Shared.Movement.Events; using Content.Shared.Movement.Systems; -using Robust.Shared.Audio; -using Robust.Shared.Audio.Systems; -using Robust.Shared.Prototypes; namespace Content.Shared._Scp.Holding.Systems; @@ -15,7 +12,7 @@ public abstract partial class SharedScpHoldingSystem * Breakout-attempt query cache, event routing, semantic state, and do-after handle tracking. */ - [Dependency] private readonly SharedAudioSystem _audio = default!; + [Dependency] private readonly WorldAlertSystem _worldAlert = default!; private EntityQuery _breakoutAttemptQuery; @@ -64,8 +61,7 @@ private void OnBreakoutAlert(Entity ent, ref ScpHold if (args.Handled) return; - args.Handled = true; - TryBreakOut(ent, viaMovement: false); + args.Handled = TryBreakOut(ent, viaMovement: false); } private void OnBreakoutDoAfter(Entity ent, ref ScpHoldBreakoutDoAfterEvent args) @@ -139,28 +135,16 @@ private void ShowBreakoutAttemptFeedback(Entity held foreach (var holderUid in held.Comp.Holders) { - if (!TryComp(holderUid, out var holder)) + if (!_activeHolderQuery.TryComp(holderUid, out var holder)) continue; if (holder.Target != held) continue; - SpawnBreakoutAttemptEffect(holderUid, holdable.BreakoutAttemptEffect); + var settings = Comp(holderUid).BreakoutAttemptAlertSettings; + _worldAlert.TrySpawnAlert(holderUid, settings); } - PlayBreakoutAttemptSound(held, holdable.BreakoutAttemptSound); - } - - private void SpawnBreakoutAttemptEffect(EntityUid holderUid, EntProtoId? effect) - { - if (effect == null) - return; - - PredictedSpawnAttachedTo(effect.Value, holderUid.ToCoordinates()); - } - - private void PlayBreakoutAttemptSound(EntityUid targetUid, SoundSpecifier? sound) - { - _audio.PlayPredicted(sound, targetUid, targetUid); + _worldAlert.TrySpawnAlert(held, holdable.BreakoutAttemptAlertSettings); } } diff --git a/Content.Shared/_Scp/Other/WorldAlert/WorldAlertSettings.cs b/Content.Shared/_Scp/Other/WorldAlert/WorldAlertSettings.cs new file mode 100644 index 00000000000..0fe3ba69311 --- /dev/null +++ b/Content.Shared/_Scp/Other/WorldAlert/WorldAlertSettings.cs @@ -0,0 +1,21 @@ +using Robust.Shared.Audio; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization; + +namespace Content.Shared._Scp.Other.WorldAlert; + +[DataDefinition, Serializable, NetSerializable] +public partial record struct WorldAlertSettings +{ + [DataField] + public EntProtoId? Prototype; + + [DataField] + public SoundSpecifier? Sound; + + [DataField] + public bool DirectSound; + + [DataField] + public TimeSpan? Lifetime; +} diff --git a/Content.Shared/_Scp/Other/WorldAlert/WorldAlertSystem.cs b/Content.Shared/_Scp/Other/WorldAlert/WorldAlertSystem.cs new file mode 100644 index 00000000000..ae6dd83406f --- /dev/null +++ b/Content.Shared/_Scp/Other/WorldAlert/WorldAlertSystem.cs @@ -0,0 +1,42 @@ +using Content.Shared.Coordinates; +using Robust.Shared.Audio.Systems; +using Robust.Shared.Network; +using Robust.Shared.Spawners; + +namespace Content.Shared._Scp.Other.WorldAlert; + +public sealed class WorldAlertSystem : EntitySystem +{ + private const float DefaultLifetimeSeconds = 1f; + + [Dependency] private readonly SharedAudioSystem _audio = default!; + [Dependency] private readonly INetManager _net = default!; + + public bool TrySpawnAlert(EntityUid target, WorldAlertSettings settings, EntityUid? soundReceiver = null) + { + if (settings.Prototype == null) + return false; + + var alert = PredictedSpawnAttachedTo(settings.Prototype, target.ToCoordinates()); + EnsureTimedDespawn(alert, settings.Lifetime); + + soundReceiver ??= target; + if (settings.DirectSound && _net.IsServer) + _audio.PlayEntity(settings.Sound, target, soundReceiver.Value); + else + _audio.PlayPredicted(settings.Sound, target, soundReceiver.Value); + + return true; + } + + private void EnsureTimedDespawn(EntityUid uid, TimeSpan? lifetime) + { + if (HasComp(uid)) + return; + + var despawn = EnsureComp(uid); + despawn.Lifetime = lifetime.HasValue + ? (float) lifetime.Value.TotalSeconds + : DefaultLifetimeSeconds; + } +} diff --git a/Resources/Prototypes/_Scp/Entities/Effects/world_alerts.yml b/Resources/Prototypes/_Scp/Entities/Effects/world_alerts.yml new file mode 100644 index 00000000000..80b7b58d5cf --- /dev/null +++ b/Resources/Prototypes/_Scp/Entities/Effects/world_alerts.yml @@ -0,0 +1,25 @@ +- type: entity + id: BaseWorldAlert + abstract: true + categories: [ HideSpawnMenu ] + components: + - type: Sprite + drawdepth: Effects + offset: 0, 1 + noRot: true + - type: Transform + noRot: true + - type: TimedDespawn + lifetime: 0.8 + - type: Tag + tags: + - HideContextMenu + +- type: entity + id: WorldAlertHandcuffs + parent: BaseWorldAlert + categories: [ HideSpawnMenu ] + components: + - type: Sprite + sprite: Objects/Misc/handcuffs.rsi + state: handcuff From c9392e5f7c3beb0a00b81f1ecdf9393349b4b228 Mon Sep 17 00:00:00 2001 From: drdth Date: Sun, 19 Apr 2026 08:47:51 +0300 Subject: [PATCH 21/27] remove: sorry codex --- .../Tests/_Scp/ScpHoldingCursorMoveTest.cs | 497 ------------------ 1 file changed, 497 deletions(-) delete mode 100644 Content.IntegrationTests/Tests/_Scp/ScpHoldingCursorMoveTest.cs diff --git a/Content.IntegrationTests/Tests/_Scp/ScpHoldingCursorMoveTest.cs b/Content.IntegrationTests/Tests/_Scp/ScpHoldingCursorMoveTest.cs deleted file mode 100644 index aa4d9d102df..00000000000 --- a/Content.IntegrationTests/Tests/_Scp/ScpHoldingCursorMoveTest.cs +++ /dev/null @@ -1,497 +0,0 @@ -#nullable enable -using System.Collections.Generic; -using System.Numerics; -using Content.IntegrationTests.Tests.Movement; -using Content.Server._Scp.Holding; -using Content.Shared.Hands.EntitySystems; -using Content.Shared.Inventory.VirtualItem; -using Content.Server.Movement.Components; -using Content.Shared._Scp.Holding.Components; -using Content.Shared.Input; -using Content.Shared.Interaction; -using Robust.Server.Console; -using Robust.Shared.GameObjects; -using Robust.Shared.Input; -using Robust.Shared.Map; -using Robust.Shared.Maths; -using Robust.Shared.Physics.Components; - -namespace Content.IntegrationTests.Tests._Scp; - -[TestFixture] -public sealed class ScpHoldingCursorMoveTest : MovementTest -{ - private const float PositionTolerance = 0.15f; - - private IServerConsoleHost _consoleHost = default!; - private SharedHandsSystem _hands = default!; - private ScpHoldingSystem _holding = default!; - private readonly List _spawnedServerHolders = []; - - [SetUp] - public override async Task Setup() - { - await base.Setup(); - - _consoleHost = Server.ResolveDependency(); - _hands = Server.System(); - _holding = Server.System(); - - await Server.WaitPost(() => - { - SEntMan.EnsureComponent(SPlayer); - }); - - await RunTicks(1); - } - - [TearDown] - public async Task TearDownScpHolding() - { - await Server.WaitPost(() => - { - ReleaseHoldIfActive(SPlayer); - - foreach (var holderUid in _spawnedServerHolders) - { - ReleaseHoldIfActive(holderUid); - } - - foreach (var holderUid in _spawnedServerHolders) - { - if (SEntMan.EntityExists(holderUid)) - SEntMan.DeleteEntity(holderUid); - } - }); - - await RunTicks(5); - _spawnedServerHolders.Clear(); - } - - [Test] - public async Task SoftHoldCursorMoveUsesClampAndBridge() - { - await SpawnTarget("MobHuman"); - await PrepareTargetForHolding(STarget!.Value); - await StartPlayerHold(); - - var holdable = SEntMan.GetComponent(STarget.Value); - var maintenanceRange = GetMaintenanceRange(holdable); - var playerPosition = Transform.GetWorldPosition(SPlayer); - var farTarget = SEntMan.GetCoordinates(PlayerCoords).Offset(new Vector2(5f, 0f)); - - await PressKey(ContentKeyFunctions.MovePulledObject, coordinates: SEntMan.GetNetCoordinates(farTarget)); - await RunTicks(20); - - await Server.WaitAssertion(() => - { - var heldPosition = Transform.GetWorldPosition(STarget.Value); - Assert.Multiple(() => - { - Assert.That(heldPosition.X, Is.EqualTo(playerPosition.X + maintenanceRange).Within(PositionTolerance)); - Assert.That(SEntMan.HasComponent(STarget.Value), Is.True); - Assert.That(SEntMan.HasComponent(STarget.Value), Is.False); - Assert.That(SEntMan.HasComponent(STarget.Value), Is.True); - Assert.That(SEntMan.HasComponent(STarget.Value), Is.False); - }); - }); - } - - [Test] - public async Task MultiHolderCursorMoveUsesLastValidCommand() - { - await SpawnTarget("MobHuman"); - await PrepareTargetForHolding(STarget!.Value); - await AddHand(Target!.Value); - - var secondHolder = await SpawnHolder(1.8f); - await StartPlayerHold(); - await StartServerHold(secondHolder, STarget.Value); - - await Server.WaitPost(() => - { - var held = SEntMan.GetComponent(STarget.Value); - Assert.Multiple(() => - { - Assert.That(held.Holders, Has.Count.EqualTo(2)); - Assert.That(held.Holders[0], Is.EqualTo(SPlayer)); - Assert.That(held.Holders[1], Is.EqualTo(secondHolder)); - Assert.That(SEntMan.HasComponent(STarget.Value), Is.False); - }); - }); - - var firstPoint = SEntMan.GetCoordinates(PlayerCoords).Offset(new Vector2(0.5f, 0f)); - var secondPoint = SEntMan.GetCoordinates(PlayerCoords).Offset(new Vector2(1.5f, 0f)); - - await Server.WaitPost(() => - { - Assert.That(_holding.TryMoveHeldToCursor(SPlayer, firstPoint), Is.True); - }); - await RunTicks(5); - - await Server.WaitPost(() => - { - Assert.That(_holding.TryMoveHeldToCursor(secondHolder, secondPoint), Is.True); - }); - await RunTicks(20); - - await Server.WaitAssertion(() => - { - var heldPosition = Transform.GetWorldPosition(STarget.Value); - var cursorMove = SEntMan.GetComponent(STarget.Value); - - Assert.Multiple(() => - { - Assert.That(heldPosition.X, Is.EqualTo(2.0f).Within(PositionTolerance)); - Assert.That(cursorMove.Holder, Is.EqualTo(secondHolder)); - Assert.That(SEntMan.HasComponent(STarget.Value), Is.False); - }); - }); - } - - [Test] - public async Task FullHoldIgnoresMovePulledObject() - { - await SpawnTarget("MobHuman"); - await PrepareTargetForHolding(STarget!.Value); - - var secondHolder = await SpawnHolder(1.8f); - await StartPlayerHold(); - await StartServerHold(secondHolder, STarget.Value); - - await Server.WaitAssertion(() => - { - Assert.That(SEntMan.HasComponent(STarget!.Value), Is.True); - }); - - var initialPosition = Transform.GetWorldPosition(STarget.Value); - var farTarget = SEntMan.GetCoordinates(PlayerCoords).Offset(new Vector2(5f, 0f)); - - await PressKey(ContentKeyFunctions.MovePulledObject, coordinates: SEntMan.GetNetCoordinates(farTarget)); - await RunTicks(20); - - await Server.WaitAssertion(() => - { - var heldPosition = Transform.GetWorldPosition(STarget.Value); - Assert.Multiple(() => - { - Assert.That(Vector2.Distance(heldPosition, initialPosition), Is.LessThanOrEqualTo(0.05f)); - Assert.That(SEntMan.HasComponent(STarget.Value), Is.True); - Assert.That(SEntMan.HasComponent(STarget.Value), Is.False); - Assert.That(SEntMan.HasComponent(STarget.Value), Is.False); - }); - }); - } - - [Test] - public async Task HolderMovementInvalidatesCursorMoveAndReturnsToSoftDrag() - { - await SpawnTarget("MobHuman"); - await PrepareTargetForHolding(STarget!.Value); - await StartPlayerHold(); - - var parkedTarget = SEntMan.GetCoordinates(PlayerCoords).Offset(new Vector2(1.5f, 0f)); - await PressKey(ContentKeyFunctions.MovePulledObject, coordinates: SEntMan.GetNetCoordinates(parkedTarget)); - await RunTicks(20); - - await Server.WaitAssertion(() => - { - Assert.That(SEntMan.HasComponent(STarget!.Value), Is.True); - Assert.That(Transform.GetWorldPosition(STarget.Value).X, Is.EqualTo(2.0f).Within(PositionTolerance)); - }); - - var parkedX = Transform.GetWorldPosition(STarget.Value).X; - var maintenanceRange = GetMaintenanceRange(SEntMan.GetComponent(STarget.Value)); - - await Move(DirectionFlag.West, 1f); - await RunTicks(10); - - await Server.WaitAssertion(() => - { - var heldPosition = Transform.GetWorldPosition(STarget!.Value); - var playerPosition = Transform.GetWorldPosition(SPlayer); - - Assert.Multiple(() => - { - Assert.That(SEntMan.HasComponent(STarget.Value), Is.False); - Assert.That(heldPosition.X, Is.LessThan(parkedX - 0.25f)); - Assert.That((heldPosition - playerPosition).Length(), Is.LessThanOrEqualTo(maintenanceRange + 0.2f)); - }); - }); - } - - [Test] - public async Task ClientCursorMoveWaitsForServerAndBecomesAuthoritative() - { - await SpawnTarget("MobHuman"); - await PrepareTargetForHolding(STarget!.Value); - await StartPlayerHold(); - await RunTicks(10); - - await Client.WaitAssertion(() => - { - Assert.That(CTarget, Is.Not.Null); - Assert.That(CEntMan.HasComponent(CTarget!.Value), Is.True); - }); - - var parkedTarget = SEntMan.GetCoordinates(PlayerCoords).Offset(new Vector2(1.5f, 0f)); - var parkedTargetNetCoords = SEntMan.GetNetCoordinates(parkedTarget); - - var initialServerPosition = Vector2.Zero; - var initialClientPosition = Vector2.Zero; - - await Server.WaitPost(() => - { - initialServerPosition = Transform.GetWorldPosition(STarget!.Value); - }); - - await Client.WaitPost(() => - { - initialClientPosition = CEntMan.System().GetWorldPosition(CTarget!.Value); - }); - - await SetKey(ContentKeyFunctions.MovePulledObject, BoundKeyState.Down, coordinates: parkedTargetNetCoords); - await Client.WaitRunTicks(1); - - var firstTickClientPosition = Vector2.Zero; - var authoritativeServerPosition = Vector2.Zero; - var hasCursorMoveAfterFirstTick = false; - - await Client.WaitPost(() => - { - firstTickClientPosition = CEntMan.System().GetWorldPosition(CTarget!.Value); - hasCursorMoveAfterFirstTick = CEntMan.HasComponent(CTarget.Value); - }); - - await Server.WaitPost(() => - { - authoritativeServerPosition = Transform.GetWorldPosition(STarget!.Value); - }); - - Assert.Multiple(() => - { - Assert.That(hasCursorMoveAfterFirstTick, Is.False, - $"Client created cursor-move state before the server processed input. initialClientX={initialClientPosition.X:F4}; firstTickClientX={firstTickClientPosition.X:F4}; initialServerX={initialServerPosition.X:F4}; serverX={authoritativeServerPosition.X:F4}"); - Assert.That(firstTickClientPosition.X, Is.EqualTo(initialClientPosition.X).Within(0.01f), - $"Client moved the held target before the server processed input. initialClientX={initialClientPosition.X:F4}; firstTickClientX={firstTickClientPosition.X:F4}; initialServerX={initialServerPosition.X:F4}; serverX={authoritativeServerPosition.X:F4}"); - Assert.That(authoritativeServerPosition.X, Is.EqualTo(initialServerPosition.X).Within(0.001f), - $"Server must remain unchanged until it processes the input. initialClientX={initialClientPosition.X:F4}; firstTickClientX={firstTickClientPosition.X:F4}; initialServerX={initialServerPosition.X:F4}; serverX={authoritativeServerPosition.X:F4}"); - }); - - await RunTicks(10); - - await Client.WaitAssertion(() => - { - var clientPosition = CEntMan.System().GetWorldPosition(CTarget!.Value); - var clientPhysicsPredict = CEntMan.GetComponent(CTarget.Value).Predict; - - Assert.Multiple(() => - { - Assert.That(CEntMan.HasComponent(CTarget.Value), Is.True); - Assert.That(clientPhysicsPredict, Is.False, - $"Held target must stay authoritative like PullMoving. initialClientX={initialClientPosition.X:F4}; currentClientX={clientPosition.X:F4}"); - Assert.That(clientPosition.X, Is.GreaterThan(initialClientPosition.X + 0.1f), - $"Held target did not start moving after the server processed cursor input. initialClientX={initialClientPosition.X:F4}; currentClientX={clientPosition.X:F4}"); - }); - }); - - await SetKey(ContentKeyFunctions.MovePulledObject, BoundKeyState.Up, coordinates: parkedTargetNetCoords); - await RunTicks(1); - } - - [Test] - public async Task ClientCursorMoveDoesNotSnapToCursorPoint() - { - await Client.WaitPost(() => - { - Client.CfgMan.SetCVar("net.fakelagmin", 0.35f); - Client.CfgMan.SetCVar("net.fakelagrand", 0f); - }); - - try - { - await SpawnTarget("MobHuman"); - await PrepareTargetForHolding(STarget!.Value); - await StartPlayerHold(); - - await Client.WaitAssertion(() => - { - Assert.That(CTarget, Is.Not.Null); - Assert.That(CEntMan.HasComponent(CTarget!.Value), Is.True); - }); - - var parkedTarget = SEntMan.GetCoordinates(PlayerCoords).Offset(new Vector2(1.5f, 0f)); - var parkedTargetNetCoords = SEntMan.GetNetCoordinates(parkedTarget); - var sampledPositions = new List(); - - await Client.WaitPost(() => - { - sampledPositions.Add(CEntMan.System().GetWorldPosition(CTarget!.Value).X); - }); - - await SetKey(ContentKeyFunctions.MovePulledObject, BoundKeyState.Down, coordinates: parkedTargetNetCoords); - - for (var i = 0; i < 12; i++) - { - await RunTicks(1); - await Client.WaitPost(() => - { - sampledPositions.Add(CEntMan.System().GetWorldPosition(CTarget!.Value).X); - }); - } - - var maxDelta = 0f; - for (var i = 1; i < sampledPositions.Count; i++) - { - maxDelta = MathF.Max(maxDelta, MathF.Abs(sampledPositions[i] - sampledPositions[i - 1])); - } - - Assert.That(maxDelta, Is.LessThanOrEqualTo(0.35f), - $"Held target snapped too far in a single client tick. positions=[{string.Join(", ", sampledPositions.ConvertAll(x => x.ToString("F4")))}]"); - - await SetKey(ContentKeyFunctions.MovePulledObject, BoundKeyState.Up, coordinates: parkedTargetNetCoords); - await RunTicks(1); - } - finally - { - await Client.WaitPost(() => - { - Client.CfgMan.SetCVar("net.fakelagmin", 0f); - Client.CfgMan.SetCVar("net.fakelagrand", 0f); - }); - } - } - - [Test] - public async Task HolderHandsRequiredTwoDoesNotImmediatelyReleaseHold() - { - await SpawnTarget("MobHuman"); - await PrepareTargetForHolding(STarget!.Value); - - await Server.WaitPost(() => - { - var holdable = SEntMan.GetComponent(STarget.Value); - holdable.HolderHandsRequired = 2; - SEntMan.Dirty(STarget.Value, holdable); - }); - - await RunTicks(1); - await StartServerHold(SPlayer, STarget.Value); - - await Server.WaitAssertion(() => - { - var activeHolder = SEntMan.GetComponent(SPlayer); - - Assert.Multiple(() => - { - Assert.That(activeHolder.Target, Is.EqualTo(STarget)); - Assert.That(SEntMan.HasComponent(STarget.Value), Is.True); - Assert.That(CountHolderBlockers(SPlayer, STarget.Value), Is.EqualTo(2)); - Assert.That(_hands.CountFreeHands(SPlayer), Is.EqualTo(0)); - }); - }); - } - - private async Task PrepareTargetForHolding(EntityUid targetUid) - { - await Server.WaitPost(() => - { - SEntMan.EnsureComponent(targetUid); - }); - - await RunTicks(1); - } - - private async Task SpawnHolder(float playerOffsetX) - { - var coords = SEntMan.GetCoordinates(PlayerCoords).Offset(new Vector2(playerOffsetX, 0f)); - var holder = await SpawnEntity("MobHuman", coords); - - await Server.WaitPost(() => - { - SEntMan.EnsureComponent(holder); - }); - - await RunTicks(1); - _spawnedServerHolders.Add(holder); - return holder; - } - - private async Task AddHand(NetEntity target) - { - await Server.WaitPost(() => - { - _consoleHost.ExecuteCommand(null, $"addhand {target}"); - }); - - await RunTicks(1); - } - - private async Task StartPlayerHold() - { - await PressKey(ContentKeyFunctions.TryPullObject); - await RunTicks(5); - - await Server.WaitAssertion(() => - { - Assert.Multiple(() => - { - Assert.That(SEntMan.HasComponent(SPlayer), Is.True); - Assert.That(SEntMan.GetComponent(SPlayer).Target, Is.EqualTo(STarget)); - Assert.That(SEntMan.HasComponent(STarget!.Value), Is.True); - }); - }); - } - - private async Task StartServerHold(EntityUid holderUid, EntityUid targetUid) - { - await Server.WaitPost(() => - { - var holder = SEntMan.GetComponent(holderUid); - Assert.That(_holding.TryToggleHold((holderUid, holder), targetUid), Is.True); - }); - - await RunTicks(5); - } - - private static float GetMaintenanceRange(ScpHoldableComponent holdable) - { - var desiredSoftDragDistance = Math.Clamp( - holdable.HoldRange * holdable.SoftDragDistanceFactor, - holdable.SoftDragMinimumDistance, - holdable.SoftDragMaximumDistance); - - return MathF.Max( - MathF.Max(holdable.HoldRange, SharedInteractionSystem.InteractionRange), - desiredSoftDragDistance + holdable.SoftDragSnapTolerance); - } - - private void ReleaseHoldIfActive(EntityUid holderUid) - { - if (!SEntMan.EntityExists(holderUid) || - !SEntMan.TryGetComponent(holderUid, out ScpHolderComponent? holder) || - !SEntMan.TryGetComponent(holderUid, out ActiveScpHolderComponent? activeHolder) || - activeHolder.Target == null) - { - return; - } - - _holding.TryReleaseHold((holderUid, holder), activeHolder.Target.Value); - } - - private int CountHolderBlockers(EntityUid holderUid, EntityUid targetUid) - { - var blockerCount = 0; - foreach (var heldItem in _hands.EnumerateHeld(holderUid)) - { - if (SEntMan.HasComponent(heldItem) && - SEntMan.TryGetComponent(heldItem, out var virtualItem) && - virtualItem.BlockingEntity == targetUid) - { - blockerCount++; - } - } - - return blockerCount; - } -} From 173ea196585800202e0de6631d4d08e76abfcaa2 Mon Sep 17 00:00:00 2001 From: drdth Date: Sun, 19 Apr 2026 13:50:45 +0300 Subject: [PATCH 22/27] refactor: make multipulling more multi --- .../_Scp/Holding/ScpHoldingSystem.cs | 34 ++- .../Components/ActiveScpHoldableComponent.cs | 2 +- .../Components/ActiveScpHolderComponent.cs | 13 ++ ...tiveStateScpHoldableCursorMoveComponent.cs | 31 --- .../SharedScpHoldingSystem.CursorMove.cs | 198 ++++++++---------- .../Systems/SharedScpHoldingSystem.Drag.cs | 91 +++++--- .../SharedScpHoldingSystem.Lifecycle.cs | 1 - .../Systems/SharedScpHoldingSystem.State.cs | 95 +++------ .../Holding/Systems/SharedScpHoldingSystem.cs | 1 - 9 files changed, 212 insertions(+), 254 deletions(-) delete mode 100644 Content.Shared/_Scp/Holding/Components/ActiveStateScpHoldableCursorMoveComponent.cs diff --git a/Content.Client/_Scp/Holding/ScpHoldingSystem.cs b/Content.Client/_Scp/Holding/ScpHoldingSystem.cs index 16f04dc8df4..e3f2cbeb3b3 100644 --- a/Content.Client/_Scp/Holding/ScpHoldingSystem.cs +++ b/Content.Client/_Scp/Holding/ScpHoldingSystem.cs @@ -1,14 +1,17 @@ -using System.Numerics; 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; @@ -35,6 +38,10 @@ public override void Initialize() { base.Initialize(); + CommandBinds.Builder + .Bind(ContentKeyFunctions.MovePulledObject, new PointerInputCmdHandler(OnMoveHeldToCursor)) + .Register(); + _handsQuery = GetEntityQuery(); _holdableQuery = GetEntityQuery(); _blockerQuery = GetEntityQuery(); @@ -48,6 +55,12 @@ public override void Initialize() SubscribeLocalEvent(OnUpdateHeldPredicted); } + public override void Shutdown() + { + base.Shutdown(); + CommandBinds.Unregister(); + } + public override void Update(float frameTime) { base.Update(frameTime); @@ -101,15 +114,9 @@ private void OnUpdateHeldPredicted(Entity ent, ref U if (_player.LocalEntity is not { Valid: true } local) return; - if ((EntityUid) ent == local) - { - args.IsPredicted = true; - return; - } - - if (HasComp(ent)) + if (ent.Owner == local) { - args.BlockPrediction = true; + args.IsPredicted = true; return; } @@ -135,6 +142,15 @@ private void OnUpdateHeldPredicted(Entity ent, ref U args.BlockPrediction = true; } + 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 held) { _physics.UpdateIsPredicted(held); diff --git a/Content.Shared/_Scp/Holding/Components/ActiveScpHoldableComponent.cs b/Content.Shared/_Scp/Holding/Components/ActiveScpHoldableComponent.cs index 302e9f89c0d..05975e068ed 100644 --- a/Content.Shared/_Scp/Holding/Components/ActiveScpHoldableComponent.cs +++ b/Content.Shared/_Scp/Holding/Components/ActiveScpHoldableComponent.cs @@ -18,7 +18,7 @@ public sealed partial class ActiveScpHoldableComponent : Component public TimeSpan SoftEscapeAvailableAt; /// - /// Ordered holder list used for reassignment, contribution counting, and drag-anchor selection. + /// Ordered holder list used for contribution counting and per-holder runtime coordination. /// [AutoNetworkedField, ViewVariables] public List Holders = []; diff --git a/Content.Shared/_Scp/Holding/Components/ActiveScpHolderComponent.cs b/Content.Shared/_Scp/Holding/Components/ActiveScpHolderComponent.cs index cd375e8ee52..06b90646de9 100644 --- a/Content.Shared/_Scp/Holding/Components/ActiveScpHolderComponent.cs +++ b/Content.Shared/_Scp/Holding/Components/ActiveScpHolderComponent.cs @@ -1,5 +1,6 @@ using Content.Shared._Scp.Holding.Systems; using Robust.Shared.GameStates; +using Robust.Shared.Map; namespace Content.Shared._Scp.Holding.Components; @@ -15,4 +16,16 @@ public sealed partial class ActiveScpHolderComponent : Component /// [AutoNetworkedField, ViewVariables] public EntityUid? Target; + + /// + /// Per-holder cursor target used to contribute cursor-driven movement without a global leader. + /// + [AutoNetworkedField, ViewVariables] + public EntityCoordinates CursorTargetCoordinates = EntityCoordinates.Invalid; + + /// + /// True while this holder is still actively pulling the held target toward its stored cursor target. + /// + [AutoNetworkedField, ViewVariables] + public bool CursorMoveActive; } diff --git a/Content.Shared/_Scp/Holding/Components/ActiveStateScpHoldableCursorMoveComponent.cs b/Content.Shared/_Scp/Holding/Components/ActiveStateScpHoldableCursorMoveComponent.cs deleted file mode 100644 index 2d19a35b58e..00000000000 --- a/Content.Shared/_Scp/Holding/Components/ActiveStateScpHoldableCursorMoveComponent.cs +++ /dev/null @@ -1,31 +0,0 @@ -using Content.Shared._Scp.Holding.Systems; -using Robust.Shared.GameStates; -using Robust.Shared.Map; - -namespace Content.Shared._Scp.Holding.Components; - -/// -/// Runtime cursor-move state stored on a held target while it is being moved or parked at a cursor-selected point. -/// -[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] -[Access(typeof(SharedScpHoldingSystem))] -public sealed partial class ActiveStateScpHoldableCursorMoveComponent : Component -{ - /// - /// Holder that issued the most recent valid cursor-move command. - /// - [AutoNetworkedField, ViewVariables] - public EntityUid Holder; - - /// - /// Clamped cursor target stored in entity coordinates for shared prediction and reconciliation. - /// - [AutoNetworkedField, ViewVariables] - public EntityCoordinates TargetCoordinates = EntityCoordinates.Invalid; - - /// - /// True while the held target is still travelling toward the stored cursor point. - /// - [AutoNetworkedField, ViewVariables] - public bool Active = true; -} diff --git a/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.CursorMove.cs b/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.CursorMove.cs index 3ad3661f087..f8d1832fd98 100644 --- a/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.CursorMove.cs +++ b/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.CursorMove.cs @@ -2,22 +2,16 @@ using System.Numerics; using Content.Shared._Scp.Holding.Components; using Robust.Shared.Map; +using Robust.Shared.Physics.Components; namespace Content.Shared._Scp.Holding.Systems; public abstract partial class SharedScpHoldingSystem { /* - * Cursor-move input validation, runtime state, and movement update helpers. + * Cursor-move input validation, per-holder cursor intent, and cursor-based movement helpers. */ - private EntityQuery _activeHoldableCursorMoveQuery; - - private void InitializeCursorMoveQueries() - { - _activeHoldableCursorMoveQuery = GetEntityQuery(); - } - private void InitializeCursorMoveEvents() { SubscribeLocalEvent(OnHolderMove); @@ -31,10 +25,10 @@ public bool TryMoveHeldToCursor(EntityUid holderUid, EntityCoordinates cursorCoo if (activeHolder.Target == null) return false; - if (!CanMoveHeldToCursor(holderUid, cursorCoords, out var held, out var clampedCoords)) + if (!CanMoveHeldToCursor(holderUid, cursorCoords, out _, out var clampedCoords)) return false; - SetCursorMoveState(held.Value, holderUid, clampedCoords, active: true); + SetHolderCursorMoveState((holderUid, activeHolder), clampedCoords, active: true); return true; } @@ -45,7 +39,6 @@ private bool CanMoveHeldToCursor( out EntityCoordinates clampedCoords, bool quiet = false) { - _ = quiet; held = null; clampedCoords = EntityCoordinates.Invalid; @@ -61,9 +54,6 @@ private bool CanMoveHeldToCursor( if (!heldComponent.Holders.Contains(holderUid)) return false; - if (_activeHoldableFullHoldStateQuery.HasComp(heldUid)) - return false; - held = (heldUid, heldComponent); if (!TryGetHeldHoldable(held.Value, out var holdable)) @@ -81,73 +71,108 @@ private bool CanMoveHeldToCursor( return TryClampHeldCursorMoveTargetCoordinates(holderUid, cursorCoords, maintenanceRange, out clampedCoords); } - private bool TryGetValidatedCursorMoveState( + private bool TryGetHolderCursorDesiredVelocity( + Entity holder, Entity held, ScpHoldableComponent holdable, - [NotNullWhen(true)] out ActiveStateScpHoldableCursorMoveComponent? cursorMove, - out EntityUid holderUid) + float maintenanceRange, + PhysicsComponent heldPhysics, + out Vector2 desiredVelocity) { - cursorMove = null; - holderUid = default; + desiredVelocity = Vector2.Zero; - if (!_activeHoldableCursorMoveQuery.TryComp(held, out var moveState)) + if (!TryGetValidatedHolderCursorMoveState(holder, held, maintenanceRange, out var targetCoordinates)) return false; - if (!moveState.TargetCoordinates.IsValid(EntityManager)) + var targetCoords = _transform.ToMapCoordinates(targetCoordinates); + var heldCoords = _transform.GetMapCoordinates(held); + + if (targetCoords.MapId != heldCoords.MapId) { - ClearCursorMoveState(held); + ClearHolderCursorMoveState(holder); return false; } - if (_activeHoldableFullHoldStateQuery.HasComp(held)) + var correction = targetCoords.Position - heldCoords.Position; + var correctionDistance = correction.Length(); + + if (!holder.Comp.CursorMoveActive && correctionDistance > holdable.SoftDragSettleTolerance) { - ClearCursorMoveState(held); - return false; + holder.Comp.CursorMoveActive = true; + Dirty(holder); } - if (!_activeHolderQuery.TryComp(moveState.Holder, out var holder)) + if (correctionDistance <= holdable.SoftDragSettleTolerance) { - ClearCursorMoveState(held); - return false; + if (holder.Comp.CursorMoveActive) + { + holder.Comp.CursorMoveActive = false; + Dirty(holder); + } + + return true; } - if (holder.Target != held) + var correctionDirection = correction / correctionDistance; + var correctionSpeed = Math.Min( + correctionDistance / GetSoftDragCatchUpTime(holdable), + holdable.SoftDragMaximumCorrectionSpeed); + + desiredVelocity = correctionDirection * correctionSpeed; + + var relativeVelocity = heldPhysics.LinearVelocity; + var awaySpeed = MathF.Max(0f, -Vector2.Dot(relativeVelocity, correctionDirection)); + if (awaySpeed > 0f) + desiredVelocity += correctionDirection * awaySpeed * holdable.SoftDragAwayVelocityStrength; + + return true; + } + + private bool TryGetValidatedHolderCursorMoveState( + Entity holder, + Entity held, + float maintenanceRange, + out EntityCoordinates targetCoordinates) + { + targetCoordinates = EntityCoordinates.Invalid; + + if (holder.Comp.Target != held) { - ClearCursorMoveState(held); + ClearHolderCursorMoveState(holder); return false; } - if (!held.Comp.Holders.Contains(moveState.Holder)) + if (!held.Comp.Holders.Contains(holder.Owner)) { - ClearCursorMoveState(held); + ClearHolderCursorMoveState(holder); return false; } - if (!_container.IsInSameOrNoContainer(moveState.Holder, held.Owner)) + if (!holder.Comp.CursorTargetCoordinates.IsValid(EntityManager)) + return false; + + if (!_container.IsInSameOrNoContainer(holder.Owner, held.Owner)) { - ClearCursorMoveState(held); + ClearHolderCursorMoveState(holder); return false; } - var desiredSoftDragDistance = GetDesiredSoftDragDistance(holdable); - var maintenanceRange = GetHoldMaintenanceRange(holdable, desiredSoftDragDistance); - var holderCoords = _transform.GetMapCoordinates(moveState.Holder); - var targetCoords = _transform.ToMapCoordinates(moveState.TargetCoordinates); + var holderCoords = _transform.GetMapCoordinates(holder.Owner); + var targetCoords = _transform.ToMapCoordinates(holder.Comp.CursorTargetCoordinates); if (holderCoords.MapId != targetCoords.MapId) { - ClearCursorMoveState(held); + ClearHolderCursorMoveState(holder); return false; } if ((targetCoords.Position - holderCoords.Position).LengthSquared() > maintenanceRange * maintenanceRange) { - ClearCursorMoveState(held); + ClearHolderCursorMoveState(holder); return false; } - cursorMove = moveState; - holderUid = moveState.Holder; + targetCoordinates = holder.Comp.CursorTargetCoordinates; return true; } @@ -179,92 +204,47 @@ private bool TryClampHeldCursorMoveTargetCoordinates( return clampedCoords.IsValid(EntityManager); } - private void SetCursorMoveState( - Entity held, - EntityUid holderUid, + private void SetHolderCursorMoveState( + Entity holder, EntityCoordinates targetCoordinates, bool active) { - var cursorMove = EnsureComp(held); - if (cursorMove.Holder == holderUid && - cursorMove.TargetCoordinates == targetCoordinates && - cursorMove.Active == active) + if (holder.Comp.CursorTargetCoordinates == targetCoordinates + && holder.Comp.CursorMoveActive == active) { return; } - cursorMove.Holder = holderUid; - cursorMove.TargetCoordinates = targetCoordinates; - cursorMove.Active = active; - Dirty(held, cursorMove); + holder.Comp.CursorTargetCoordinates = targetCoordinates; + holder.Comp.CursorMoveActive = active; + Dirty(holder); } - private void ClearCursorMoveState(EntityUid heldUid) + private void ClearHolderCursorMoveState(EntityUid holderUid) { - if (_activeHoldableCursorMoveQuery.HasComp(heldUid)) - RemComp(heldUid); + if (_activeHolderQuery.TryComp(holderUid, out var holder)) + ClearHolderCursorMoveState((holderUid, holder)); } - private void UpdateCursorMoveDrag( - Entity held, - ScpHoldableComponent holdable, - EntityUid holderUid, - ActiveStateScpHoldableCursorMoveComponent cursorMove) + private void ClearHolderCursorMoveState(Entity holder) { - if (!_physicsQuery.TryComp(held, out var heldPhysics)) - return; - - if (!_container.IsInSameOrNoContainer(holderUid, held.Owner)) + if (holder.Comp.CursorTargetCoordinates == EntityCoordinates.Invalid + && !holder.Comp.CursorMoveActive) { - ClearCursorMoveState(held); return; } - var targetCoords = _transform.ToMapCoordinates(cursorMove.TargetCoordinates); - var heldCoords = _transform.GetMapCoordinates(held); - - if (targetCoords.MapId != heldCoords.MapId) - { - ClearCursorMoveState(held); - return; - } - - var correction = targetCoords.Position - heldCoords.Position; - var correctionDistance = correction.Length(); - - if (!cursorMove.Active && correctionDistance > holdable.SoftDragSettleTolerance) - { - cursorMove.Active = true; - Dirty(held, cursorMove); - } - - Vector2 desiredVelocity; - if (correctionDistance <= holdable.SoftDragSettleTolerance) - { - if (cursorMove.Active) - { - cursorMove.Active = false; - Dirty(held, cursorMove); - } + holder.Comp.CursorTargetCoordinates = EntityCoordinates.Invalid; + holder.Comp.CursorMoveActive = false; + Dirty(holder); + } - desiredVelocity = Vector2.Zero; - } - else + private void ClearHeldCursorMoveStates(Entity held) + { + foreach (var holderUid in held.Comp.Holders) { - var correctionDirection = correction / correctionDistance; - var correctionSpeed = Math.Min( - correctionDistance / GetSoftDragCatchUpTime(holdable), - holdable.SoftDragMaximumCorrectionSpeed); - - desiredVelocity = correctionDirection * correctionSpeed; - - var relativeVelocity = heldPhysics.LinearVelocity; - var awaySpeed = MathF.Max(0f, -Vector2.Dot(relativeVelocity, correctionDirection)); - if (awaySpeed > 0f) - desiredVelocity += correctionDirection * awaySpeed * holdable.SoftDragAwayVelocityStrength; + ClearHolderCursorMoveState(holderUid); } - - ApplyHeldVelocity(held, desiredVelocity, heldPhysics, holdable); } private void OnHolderMove(Entity ent, ref MoveEvent args) @@ -281,6 +261,6 @@ private void OnHolderMove(Entity ent, ref MoveEvent ar return; } - ClearCursorMoveState(ent.Comp.Target.Value); + ClearHolderCursorMoveState(ent); } } diff --git a/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.Drag.cs b/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.Drag.cs index 2800dcb8a90..539321da0ee 100644 --- a/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.Drag.cs +++ b/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.Drag.cs @@ -10,7 +10,7 @@ namespace Content.Shared._Scp.Holding.Systems; public abstract partial class SharedScpHoldingSystem { /* - * Drag-local dependencies, soft-drag movement, and helper calculations. + * Drag-local dependencies, aggregate holder movement, and helper calculations. */ [Dependency] private readonly SharedTransformSystem _transform = default!; @@ -33,66 +33,95 @@ private void InitializeDragEvents() private void UpdateSoftDrag( Entity held, ScpHoldableComponent holdable, - EntityUid dragAnchor, - ActiveScpHolderComponent anchorHolder, float maintenanceRange, float desiredDistance) { - if (TryGetValidatedCursorMoveState(held, holdable, out var cursorMove, out var cursorHolderUid)) - { - UpdateCursorMoveDrag(held, holdable, cursorHolderUid, cursorMove); + if (!_physicsQuery.TryComp(held, out var heldPhysics)) return; + + var aggregateDesiredVelocity = Vector2.Zero; + var contributingHolderCount = 0; + + foreach (var holderUid in held.Comp.Holders) + { + if (!_activeHolderQuery.TryComp(holderUid, out var holder)) + continue; + + if (holder.Target != held) + continue; + + Vector2 desiredVelocity; + if (!TryGetHolderCursorDesiredVelocity((holderUid, holder), held, holdable, maintenanceRange, heldPhysics, out desiredVelocity) && + !TryGetHolderMovementDesiredVelocity((holderUid, holder), held, holdable, maintenanceRange, desiredDistance, heldPhysics, out desiredVelocity)) + { + continue; + } + + aggregateDesiredVelocity += desiredVelocity; + contributingHolderCount++; } - if (anchorHolder.Target != held) + if (contributingHolderCount == 0) + { + ZeroHeldVelocity(held); return; + } - if (!_container.IsInSameOrNoContainer(dragAnchor, held.Owner)) - return; + ApplyHeldVelocity(held, aggregateDesiredVelocity / contributingHolderCount, heldPhysics, holdable); + } - if (!_interaction.InRangeUnobstructed(dragAnchor, held.Owner, maintenanceRange)) - return; + private bool TryGetHolderMovementDesiredVelocity( + Entity holder, + Entity held, + ScpHoldableComponent holdable, + float maintenanceRange, + float desiredDistance, + PhysicsComponent heldPhysics, + out Vector2 desiredVelocity) + { + desiredVelocity = Vector2.Zero; - if (!_physicsQuery.TryComp(held, out var heldPhysics)) - return; + if (!_container.IsInSameOrNoContainer(holder.Owner, held.Owner)) + return false; - var holderCoords = _transform.GetMapCoordinates(dragAnchor); - var heldCoords = _transform.GetMapCoordinates(held); + if (!_interaction.InRangeUnobstructed(holder.Owner, held.Owner, maintenanceRange)) + return false; + + var holderCoords = _transform.GetMapCoordinates(holder.Owner); + var heldCoords = _transform.GetMapCoordinates(held.Owner); if (holderCoords.MapId != heldCoords.MapId) - return; + return false; var offset = heldCoords.Position - holderCoords.Position; var distance = offset.Length(); - var holderVelocity = _physicsQuery.TryComp(dragAnchor, out var holderPhysics) + var holderVelocity = _physicsQuery.TryComp(holder, out var holderPhysics) ? holderPhysics.LinearVelocity : Vector2.Zero; var velocityDirectionThresholdSquared = holdable.SoftDragVelocityDirectionThreshold * holdable.SoftDragVelocityDirectionThreshold; - var direction = GetSoftDragDirection(dragAnchor, holdable, holderVelocity, offset, distance, velocityDirectionThresholdSquared); + var direction = GetSoftDragDirection(holder.Owner, holdable, holderVelocity, offset, distance, velocityDirectionThresholdSquared); var desiredPosition = holderCoords.Position + direction * desiredDistance; var correction = desiredPosition - heldCoords.Position; var correctionDistance = correction.Length(); - Vector2 desiredVelocity; if (correctionDistance <= holdable.SoftDragSettleTolerance) { desiredVelocity = holderVelocity.LengthSquared() > velocityDirectionThresholdSquared ? holderVelocity : Vector2.Zero; + return true; } - else - { - var correctionDirection = correction / correctionDistance; - var correctionSpeed = Math.Min(correctionDistance / GetSoftDragCatchUpTime(holdable), holdable.SoftDragMaximumCorrectionSpeed); - desiredVelocity = holderVelocity + correctionDirection * correctionSpeed; - - var relativeVelocity = heldPhysics.LinearVelocity - holderVelocity; - var awaySpeed = MathF.Max(0f, -Vector2.Dot(relativeVelocity, correctionDirection)); - if (awaySpeed > 0f) - desiredVelocity += correctionDirection * awaySpeed * holdable.SoftDragAwayVelocityStrength; - } - ApplyHeldVelocity(held, desiredVelocity, heldPhysics, holdable); + var correctionDirection = correction / correctionDistance; + var correctionSpeed = Math.Min(correctionDistance / GetSoftDragCatchUpTime(holdable), holdable.SoftDragMaximumCorrectionSpeed); + desiredVelocity = holderVelocity + correctionDirection * correctionSpeed; + + var relativeVelocity = heldPhysics.LinearVelocity - holderVelocity; + var awaySpeed = MathF.Max(0f, -Vector2.Dot(relativeVelocity, correctionDirection)); + if (awaySpeed > 0f) + desiredVelocity += correctionDirection * awaySpeed * holdable.SoftDragAwayVelocityStrength; + + return true; } private static float GetDesiredSoftDragDistance(ScpHoldableComponent holdable) diff --git a/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.Lifecycle.cs b/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.Lifecycle.cs index 323ea74b55c..1cdd49fa12c 100644 --- a/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.Lifecycle.cs +++ b/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.Lifecycle.cs @@ -58,7 +58,6 @@ private void OnFullHeldStartup(Entity e if (_activeHoldableQuery.TryComp(ent, out var held)) SyncPlaceholderHands((ent, held)); - ZeroHeldVelocity(ent); _actionBlocker.UpdateCanMove(ent); } diff --git a/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.State.cs b/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.State.cs index d4e01c47aa0..4c660b5f49f 100644 --- a/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.State.cs +++ b/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.State.cs @@ -36,25 +36,9 @@ protected void UpdateHeld(Entity held) if (!TryGetHeldHoldable(held, out var holdable)) return; - if (!TryGetDragAnchorHolder(held, out var dragAnchorUid, out var dragAnchor)) - { - ClearHoldState(held, applyImmunity: false); - return; - } - var desiredSoftDragDistance = GetDesiredSoftDragDistance(holdable); var maintenanceRange = GetHoldMaintenanceRange(holdable, desiredSoftDragDistance); - if (!_activeHoldableFullHoldStateQuery.HasComp(held)) - { - UpdateSoftDrag(held, holdable, dragAnchorUid, dragAnchor, maintenanceRange, desiredSoftDragDistance); - } - else - { - ClearCursorMoveState(held); - ZeroHeldVelocity(held); - } - _holdersToRemove.Clear(); foreach (var holderUid in held.Comp.Holders) @@ -68,8 +52,20 @@ protected void UpdateHeld(Entity held) ReleaseHolderContribution(holderUid, held, clearIfEmpty: false); } - if (_activeHoldableQuery.TryComp(held, out var refreshed)) - SyncHeldState((held, refreshed)); + if (!_activeHoldableQuery.TryComp(held, out var refreshed)) + return; + + held = (held, refreshed); + SyncHeldState(held); + + if (!_activeHoldableQuery.TryComp(held, out refreshed)) + return; + + held = (held, refreshed); + if (!TryGetHeldHoldable(held, out holdable)) + return; + + UpdateSoftDrag(held, holdable, maintenanceRange, desiredSoftDragDistance); } private Entity EnsureHeldState(EntityUid target) @@ -118,10 +114,7 @@ protected void ReleaseHolderContribution(EntityUid holderUid, EntityUid targetUi } if (removed) - { - ClearCursorMoveState(targetUid); Dirty(targetUid, held); - } if (_activeHolderQuery.HasComp(holderUid)) RemComp(holderUid); @@ -148,39 +141,30 @@ protected void SyncHeldState(Entity held) if (!TryGetHeldHoldable(held, out var holdable)) return; - held.Comp.RequiredHolderCount = GetRequiredHolderCount(held); - Dirty(held, held.Comp); - - if (held.Comp.Holders.Count == 0) + var requiredHolderCount = GetRequiredHolderCount(held); + if (held.Comp.RequiredHolderCount != requiredHolderCount) { - ClearHoldState(held, applyImmunity: false); - return; + held.Comp.RequiredHolderCount = requiredHolderCount; + Dirty(held, held.Comp); } - if (!TryGetDragAnchorHolder(held, out var dragAnchorUid, out var dragAnchor)) + if (held.Comp.Holders.Count == 0) { ClearHoldState(held, applyImmunity: false); return; } if (held.Comp.Holders.Count >= held.Comp.RequiredHolderCount) - { - EnterFullHold(held, holdable); - return; - } + EnterFullHold(held); + else + ExitFullHold(held); - ExitFullHold(held); - var desiredSoftDragDistance = GetDesiredSoftDragDistance(holdable); - var maintenanceRange = GetHoldMaintenanceRange(holdable, desiredSoftDragDistance); - UpdateSoftDrag(held, holdable, dragAnchorUid, dragAnchor, maintenanceRange, desiredSoftDragDistance); UpdateHolderSlowdowns(held, holdable); SyncPlaceholderHands(held); } - private void EnterFullHold(Entity held, ScpHoldableComponent holdable) + private void EnterFullHold(Entity held) { - ClearCursorMoveState(held); - var fullHeldCreated = !_activeHoldableFullHoldStateQuery.TryComp(held, out var fullHeld); fullHeld ??= EnsureComp(held); @@ -189,14 +173,6 @@ private void EnterFullHold(Entity held, ScpHoldableC fullHeld.StartedAt = _timing.CurTime; Dirty(held, fullHeld); } - - UpdateHolderSlowdowns(held, holdable); - - if (fullHeldCreated) - return; - - SyncPlaceholderHands(held); - ZeroHeldVelocity(held); } private void ExitFullHold(Entity held) @@ -208,35 +184,12 @@ private void ExitFullHold(Entity held) RemComp(held); } - private bool TryGetDragAnchorHolder( - Entity held, - out EntityUid dragAnchorUid, - out ActiveScpHolderComponent dragAnchor) - { - foreach (var holderUid in held.Comp.Holders) - { - if (!_activeHolderQuery.TryComp(holderUid, out var holder)) - continue; - - if (holder.Target != held) - continue; - - dragAnchorUid = holderUid; - dragAnchor = holder; - return true; - } - - dragAnchorUid = default; - dragAnchor = default!; - return false; - } - private void ClearHoldState(Entity held, bool applyImmunity) { if (_activeHoldableQuery.TryComp(held, out var refreshed)) held = (held, refreshed); - ClearCursorMoveState(held); + ClearHeldCursorMoveStates(held); EndBreakoutAttempt(held, cancelDoAfter: true); if (_activeHoldableFullHoldStateQuery.HasComp(held)) diff --git a/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.cs b/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.cs index 1b85f1dfacd..bff903de4ea 100644 --- a/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.cs +++ b/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.cs @@ -39,7 +39,6 @@ public override void Initialize() InitializeHoldQueries(); InitializeBreakoutAttemptQueries(); - InitializeCursorMoveQueries(); InitializeDragQueries(); InitializeHandQueries(); InitializeStateQueries(); From 33595cd3b6bcc95e4fca4db4d7b3cea2ff545e74 Mon Sep 17 00:00:00 2001 From: drdth Date: Mon, 20 Apr 2026 06:18:16 +0300 Subject: [PATCH 23/27] fix: move to cursor + handcuffs --- .../Components/ActiveScpHolderComponent.cs | 4 +- .../Systems/SharedScpHoldingSystem.Actions.cs | 13 +- .../SharedScpHoldingSystem.BreakoutAttempt.cs | 12 ++ ...edScpHoldingSystem.BreakoutRestrictions.cs | 24 ++++ .../SharedScpHoldingSystem.CursorMove.cs | 133 ++++++++++++++---- ...haredScpHoldingSystem.PullCompatibility.cs | 26 ++++ 6 files changed, 184 insertions(+), 28 deletions(-) create mode 100644 Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.BreakoutRestrictions.cs create mode 100644 Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.PullCompatibility.cs diff --git a/Content.Shared/_Scp/Holding/Components/ActiveScpHolderComponent.cs b/Content.Shared/_Scp/Holding/Components/ActiveScpHolderComponent.cs index 06b90646de9..c645daab384 100644 --- a/Content.Shared/_Scp/Holding/Components/ActiveScpHolderComponent.cs +++ b/Content.Shared/_Scp/Holding/Components/ActiveScpHolderComponent.cs @@ -18,7 +18,9 @@ public sealed partial class ActiveScpHolderComponent : Component public EntityUid? Target; /// - /// Per-holder cursor target used to contribute cursor-driven movement without a global leader. + /// Raw per-holder desired cursor target in world space. + /// Each tick it is clamped relative to the holder's current position, + /// so far clicks keep their direction even if the holder moves. /// [AutoNetworkedField, ViewVariables] public EntityCoordinates CursorTargetCoordinates = EntityCoordinates.Invalid; diff --git a/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.Actions.cs b/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.Actions.cs index 5ec5d79ce61..01ddb851bf7 100644 --- a/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.Actions.cs +++ b/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.Actions.cs @@ -187,8 +187,14 @@ public bool CanToggleHold( return false; } - if (checkAttempt && !CanPassHoldAttempt(holder, target)) - return false; + if (checkAttempt) + { + if (!CanPassPullAttempt(holder.Owner, target)) + return false; + + if (!CanPassHoldAttempt(holder, target)) + return false; + } return true; } @@ -212,6 +218,9 @@ protected bool TryGetRequiredHolderHandCount(EntityUid targetUid, out int requir public bool TryBreakOut(Entity held, bool viaMovement) { + if (IsBreakoutBlockedByCuffs(held)) + return false; + return _activeHoldableFullHoldStateQuery.HasComp(held) ? TryStartFullBreakout(held, viaMovement) : TrySoftBreakOut(held, viaMovement); diff --git a/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.BreakoutAttempt.cs b/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.BreakoutAttempt.cs index 995705a199b..4c9c8ad0378 100644 --- a/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.BreakoutAttempt.cs +++ b/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.BreakoutAttempt.cs @@ -61,6 +61,12 @@ private void OnBreakoutAlert(Entity ent, ref ScpHold if (args.Handled) return; + if (TryRedirectBreakoutAlertToUncuff(ent, args.User)) + { + args.Handled = true; + return; + } + args.Handled = TryBreakOut(ent, viaMovement: false); } @@ -77,6 +83,9 @@ private void OnBreakoutDoAfter(Entity ent, ref ScpHo return; } + if (IsBreakoutBlockedByCuffs(ent)) + return; + BreakOut(ent, args.ViaMovement, applyImmunity: true); args.Handled = true; } @@ -102,6 +111,9 @@ private void OnHeldMoveInput(Entity ent, ref MoveInp if (!IsBreakoutMovementPress(args)) return; + if (IsBreakoutBlockedByCuffs(ent)) + return; + TryBreakOut(ent, viaMovement: true); } diff --git a/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.BreakoutRestrictions.cs b/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.BreakoutRestrictions.cs new file mode 100644 index 00000000000..8b6b9674901 --- /dev/null +++ b/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.BreakoutRestrictions.cs @@ -0,0 +1,24 @@ +using Content.Shared._Scp.Holding.Components; +using Content.Shared.Cuffs; +using Content.Shared.Cuffs.Components; + +namespace Content.Shared._Scp.Holding.Systems; + +public abstract partial class SharedScpHoldingSystem +{ + [Dependency] private readonly SharedCuffableSystem _cuffable = default!; + + private bool IsBreakoutBlockedByCuffs(EntityUid heldUid) + { + return TryComp(heldUid, out var cuffable) && !cuffable.CanStillInteract; + } + + private bool TryRedirectBreakoutAlertToUncuff(Entity held, EntityUid user) + { + if (!TryComp(held, out var cuffable) || cuffable.CanStillInteract) + return false; + + _cuffable.TryUncuff((held.Owner, cuffable), user); + return true; + } +} diff --git a/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.CursorMove.cs b/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.CursorMove.cs index f8d1832fd98..ac8fbd4a56f 100644 --- a/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.CursorMove.cs +++ b/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.CursorMove.cs @@ -12,6 +12,8 @@ public abstract partial class SharedScpHoldingSystem * Cursor-move input validation, per-holder cursor intent, and cursor-based movement helpers. */ + private const float CursorMoveCancelMovementDistance = 0.005f; + private void InitializeCursorMoveEvents() { SubscribeLocalEvent(OnHolderMove); @@ -25,10 +27,10 @@ public bool TryMoveHeldToCursor(EntityUid holderUid, EntityCoordinates cursorCoo if (activeHolder.Target == null) return false; - if (!CanMoveHeldToCursor(holderUid, cursorCoords, out _, out var clampedCoords)) + if (!CanMoveHeldToCursor(holderUid, cursorCoords, out _, out var targetCoords)) return false; - SetHolderCursorMoveState((holderUid, activeHolder), clampedCoords, active: true); + SetHolderCursorMoveState((holderUid, activeHolder), targetCoords, active: true); return true; } @@ -36,11 +38,11 @@ private bool CanMoveHeldToCursor( EntityUid holderUid, EntityCoordinates cursorCoords, [NotNullWhen(true)] out Entity? held, - out EntityCoordinates clampedCoords, + out EntityCoordinates targetCoords, bool quiet = false) { held = null; - clampedCoords = EntityCoordinates.Invalid; + targetCoords = EntityCoordinates.Invalid; if (!_activeHolderQuery.TryComp(holderUid, out var holder)) return false; @@ -68,7 +70,7 @@ private bool CanMoveHeldToCursor( if (!_interaction.InRangeUnobstructed(holderUid, heldUid, maintenanceRange)) return false; - return TryClampHeldCursorMoveTargetCoordinates(holderUid, cursorCoords, maintenanceRange, out clampedCoords); + return TryNormalizeHeldCursorMoveTargetCoordinates(holderUid, cursorCoords, out targetCoords); } private bool TryGetHolderCursorDesiredVelocity( @@ -84,6 +86,7 @@ private bool TryGetHolderCursorDesiredVelocity( if (!TryGetValidatedHolderCursorMoveState(holder, held, maintenanceRange, out var targetCoordinates)) return false; + var holderCoords = _transform.GetMapCoordinates(holder); var targetCoords = _transform.ToMapCoordinates(targetCoordinates); var heldCoords = _transform.GetMapCoordinates(held); @@ -113,13 +116,16 @@ private bool TryGetHolderCursorDesiredVelocity( return true; } - var correctionDirection = correction / correctionDistance; - var correctionSpeed = Math.Min( - correctionDistance / GetSoftDragCatchUpTime(holdable), - holdable.SoftDragMaximumCorrectionSpeed); + desiredVelocity = GetHolderCursorCorrectionVelocity( + holderCoords.Position, + heldCoords.Position, + targetCoords.Position, + holdable); - desiredVelocity = correctionDirection * correctionSpeed; + if (desiredVelocity == Vector2.Zero) + return true; + var correctionDirection = Vector2.Normalize(desiredVelocity); var relativeVelocity = heldPhysics.LinearVelocity; var awaySpeed = MathF.Max(0f, -Vector2.Dot(relativeVelocity, correctionDirection)); if (awaySpeed > 0f) @@ -157,32 +163,91 @@ private bool TryGetValidatedHolderCursorMoveState( return false; } - var holderCoords = _transform.GetMapCoordinates(holder.Owner); - var targetCoords = _transform.ToMapCoordinates(holder.Comp.CursorTargetCoordinates); - - if (holderCoords.MapId != targetCoords.MapId) + if (!TryClampHeldCursorMoveTargetCoordinates( + holder.Owner, + holder.Comp.CursorTargetCoordinates, + maintenanceRange, + out targetCoordinates)) { ClearHolderCursorMoveState(holder); return false; } - if ((targetCoords.Position - holderCoords.Position).LengthSquared() > maintenanceRange * maintenanceRange) + return true; + } + + private Vector2 GetHolderCursorCorrectionVelocity( + Vector2 holderPosition, + Vector2 heldPosition, + Vector2 targetPosition, + ScpHoldableComponent holdable) + { + var correction = targetPosition - heldPosition; + var correctionDistance = correction.Length(); + if (correctionDistance <= holdable.SoftDragSettleTolerance) + return Vector2.Zero; + + var currentOffset = heldPosition - holderPosition; + var desiredOffset = targetPosition - holderPosition; + var currentDistance = currentOffset.Length(); + var desiredDistance = desiredOffset.Length(); + + if (currentDistance <= holdable.SoftDragSettleTolerance || + desiredDistance <= holdable.SoftDragSettleTolerance) { - ClearHolderCursorMoveState(holder); - return false; + return GetDirectCursorCorrectionVelocity(correction, correctionDistance, holdable); } - targetCoordinates = holder.Comp.CursorTargetCoordinates; - return true; + var catchUpTime = GetSoftDragCatchUpTime(holdable); + var currentDirection = currentOffset / currentDistance; + var desiredDirection = desiredOffset / desiredDistance; + var cross = currentDirection.X * desiredDirection.Y - currentDirection.Y * desiredDirection.X; + var dot = Math.Clamp(Vector2.Dot(currentDirection, desiredDirection), -1f, 1f); + var angleDelta = MathF.Atan2(cross, dot); + + var tangentDirection = cross >= 0f + ? new Vector2(-currentDirection.Y, currentDirection.X) + : new Vector2(currentDirection.Y, -currentDirection.X); + + var tangentialSpeed = Math.Min( + MathF.Abs(angleDelta) * MathF.Max(currentDistance, desiredDistance) / catchUpTime, + holdable.SoftDragMaximumCorrectionSpeed); + + var radialSpeed = Math.Clamp( + (desiredDistance - currentDistance) / catchUpTime, + -holdable.SoftDragMaximumCorrectionSpeed, + holdable.SoftDragMaximumCorrectionSpeed); + + var correctionVelocity = tangentDirection * tangentialSpeed + currentDirection * radialSpeed; + var maximumSpeedSquared = holdable.SoftDragMaximumCorrectionSpeed * holdable.SoftDragMaximumCorrectionSpeed; + if (correctionVelocity.LengthSquared() > maximumSpeedSquared) + correctionVelocity = Vector2.Normalize(correctionVelocity) * holdable.SoftDragMaximumCorrectionSpeed; + + return correctionVelocity; } - private bool TryClampHeldCursorMoveTargetCoordinates( + private Vector2 GetDirectCursorCorrectionVelocity( + Vector2 correction, + float correctionDistance, + ScpHoldableComponent holdable) + { + if (correctionDistance <= 0f) + return Vector2.Zero; + + var correctionDirection = correction / correctionDistance; + var correctionSpeed = Math.Min( + correctionDistance / GetSoftDragCatchUpTime(holdable), + holdable.SoftDragMaximumCorrectionSpeed); + + return correctionDirection * correctionSpeed; + } + + private bool TryNormalizeHeldCursorMoveTargetCoordinates( EntityUid holderUid, EntityCoordinates cursorCoords, - float maintenanceRange, - out EntityCoordinates clampedCoords) + out EntityCoordinates normalizedCoords) { - clampedCoords = EntityCoordinates.Invalid; + normalizedCoords = EntityCoordinates.Invalid; if (!cursorCoords.IsValid(EntityManager)) return false; @@ -193,6 +258,23 @@ private bool TryClampHeldCursorMoveTargetCoordinates( if (holderCoords.MapId != cursorMapCoords.MapId) return false; + normalizedCoords = _transform.ToCoordinates(cursorMapCoords); + return normalizedCoords.IsValid(EntityManager); + } + + private bool TryClampHeldCursorMoveTargetCoordinates( + EntityUid holderUid, + EntityCoordinates cursorCoords, + float maintenanceRange, + out EntityCoordinates clampedCoords) + { + clampedCoords = EntityCoordinates.Invalid; + + if (!TryNormalizeHeldCursorMoveTargetCoordinates(holderUid, cursorCoords, out var normalizedCoords)) + return false; + + var holderCoords = _transform.GetMapCoordinates(holderUid); + var cursorMapCoords = _transform.ToMapCoordinates(normalizedCoords); var offset = cursorMapCoords.Position - holderCoords.Position; var distance = offset.Length(); var clampedPosition = cursorMapCoords.Position; @@ -252,11 +334,12 @@ private void OnHolderMove(Entity ent, ref MoveEvent ar if (_timing.ApplyingState) return; - if (ent.Comp.Target == null) + if (ent.Comp.Target == null || ent.Comp.CursorTargetCoordinates == EntityCoordinates.Invalid) return; if (args.NewPosition.EntityId == args.OldPosition.EntityId && - (args.NewPosition.Position - args.OldPosition.Position).LengthSquared() <= 0f) + (args.NewPosition.Position - args.OldPosition.Position).LengthSquared() < + CursorMoveCancelMovementDistance * CursorMoveCancelMovementDistance) { return; } diff --git a/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.PullCompatibility.cs b/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.PullCompatibility.cs new file mode 100644 index 00000000000..22254614325 --- /dev/null +++ b/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.PullCompatibility.cs @@ -0,0 +1,26 @@ +using Content.Shared.Buckle.Components; +using Content.Shared.Pulling.Events; + +namespace Content.Shared._Scp.Holding.Systems; + +public abstract partial class SharedScpHoldingSystem +{ + private bool CanPassPullAttempt(EntityUid holderUid, EntityUid targetUid) + { + if (!_actionBlocker.CanInteract(holderUid, targetUid)) + return false; + + if (TryComp(targetUid, out var buckleComponent) && buckleComponent.Buckled) + return false; + + var beingPulledAttempt = new BeingPulledAttemptEvent(holderUid, targetUid); + RaiseLocalEvent(targetUid, beingPulledAttempt, true); + + if (beingPulledAttempt.Cancelled) + return false; + + var startPullAttempt = new StartPullAttemptEvent(holderUid, targetUid); + RaiseLocalEvent(holderUid, startPullAttempt, true); + return !startPullAttempt.Cancelled; + } +} From d4f02331e0f41be97a2ad3434af0578f3cc7670f Mon Sep 17 00:00:00 2001 From: drdth Date: Tue, 21 Apr 2026 06:22:48 +0300 Subject: [PATCH 24/27] fix: ai review 2 --- .../Movement/Pulling/Systems/PullingSystem.cs | 4 +- .../Compatibility/PullingSystem.ScpHolding.cs | 16 ++- .../Components/ActiveScpHoldableComponent.cs | 2 +- .../Components/ActiveScpHolderComponent.cs | 2 +- .../Systems/SharedScpHoldingSystem.Actions.cs | 10 +- .../SharedScpHoldingSystem.CursorMove.cs | 54 +++++---- .../Systems/SharedScpHoldingSystem.Hands.cs | 111 ++++++++++++------ .../SharedScpHoldingSystem.Lifecycle.cs | 1 - .../Systems/SharedScpHoldingSystem.State.cs | 32 ++--- .../_Scp/Other/WorldAlert/WorldAlertSystem.cs | 15 ++- .../Systems/SharedScp096System.Holding.cs | 6 +- .../en-US/_strings/_scp/holding/holding.ftl | 1 + .../ru-RU/_strings/_scp/holding/holding.ftl | 4 +- 13 files changed, 156 insertions(+), 102 deletions(-) diff --git a/Content.Shared/Movement/Pulling/Systems/PullingSystem.cs b/Content.Shared/Movement/Pulling/Systems/PullingSystem.cs index f7c3a5fcd44..0b2713b4c9d 100644 --- a/Content.Shared/Movement/Pulling/Systems/PullingSystem.cs +++ b/Content.Shared/Movement/Pulling/Systems/PullingSystem.cs @@ -530,8 +530,8 @@ public bool TryStartPull(EntityUid pullerUid, EntityUid pullableUid, return true; // Fire added start - redirect scp-hold-capable pull attempts into the hold flow - if (TryRedirectPullToScpHold(pullerUid, pullableUid, pullerComp, pullableComp, out var holdResult)) - return holdResult; + if (TryRedirectPullToScpHold(pullerUid, pullableUid, pullerComp, pullableComp, out var holdSuccess)) + return holdSuccess; // Fire added end if (!CanPull(pullerUid, pullableUid)) diff --git a/Content.Shared/_Scp/Holding/Compatibility/PullingSystem.ScpHolding.cs b/Content.Shared/_Scp/Holding/Compatibility/PullingSystem.ScpHolding.cs index 3eae22f2bf8..657ac816edc 100644 --- a/Content.Shared/_Scp/Holding/Compatibility/PullingSystem.ScpHolding.cs +++ b/Content.Shared/_Scp/Holding/Compatibility/PullingSystem.ScpHolding.cs @@ -22,10 +22,18 @@ private void InitializeScpHolding() _scpActiveHolderQuery = GetEntityQuery(); } + /// + /// Attempts to consume a pull request by redirecting it into SCP holding. + /// Returns when the pull attempt was handled, even if it was rejected. + /// The output indicates whether the redirect actually succeeded. + /// This includes the paths that validate via , + /// stop existing pulls via TryStopPull, and finally toggle the hold via + /// . + /// private bool TryRedirectPullToScpHold(EntityUid pullerUid, EntityUid pullableUid, - PullerComponent pullerComp, PullableComponent pullableComp, out bool result) + PullerComponent pullerComp, PullableComponent pullableComp, out bool success) { - result = false; + success = false; if (!_scpHolderConfigQuery.TryComp(pullerUid, out var holdComp) || !_scpHoldableQuery.HasComp(pullableUid)) @@ -38,7 +46,7 @@ private bool TryRedirectPullToScpHold(EntityUid pullerUid, EntityUid pullableUid if (_scpActiveHolderQuery.TryComp(pullerUid, out var activeHolder) && activeHolder.Target != null) { - result = _scpHolding.TryToggleHold(holder, pullableUid); + success = _scpHolding.TryToggleHold(holder, pullableUid); return true; } @@ -61,7 +69,7 @@ private bool TryRedirectPullToScpHold(EntityUid pullerUid, EntityUid pullableUid return true; } - result = _scpHolding.TryToggleHold(holder, pullableUid, attemptChecked: true); + success = _scpHolding.TryToggleHold(holder, pullableUid, attemptChecked: true); return true; } } diff --git a/Content.Shared/_Scp/Holding/Components/ActiveScpHoldableComponent.cs b/Content.Shared/_Scp/Holding/Components/ActiveScpHoldableComponent.cs index 05975e068ed..53e4c2e544e 100644 --- a/Content.Shared/_Scp/Holding/Components/ActiveScpHoldableComponent.cs +++ b/Content.Shared/_Scp/Holding/Components/ActiveScpHoldableComponent.cs @@ -7,7 +7,7 @@ namespace Content.Shared._Scp.Holding.Components; /// /// Runtime state stored on a target while at least one holder is contributing. /// -[RegisterComponent, NetworkedComponent, AutoGenerateComponentState(true), AutoGenerateComponentPause] +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState(true, true), AutoGenerateComponentPause] [Access(typeof(SharedScpHoldingSystem))] public sealed partial class ActiveScpHoldableComponent : Component { diff --git a/Content.Shared/_Scp/Holding/Components/ActiveScpHolderComponent.cs b/Content.Shared/_Scp/Holding/Components/ActiveScpHolderComponent.cs index c645daab384..8d25b646f7c 100644 --- a/Content.Shared/_Scp/Holding/Components/ActiveScpHolderComponent.cs +++ b/Content.Shared/_Scp/Holding/Components/ActiveScpHolderComponent.cs @@ -7,7 +7,7 @@ namespace Content.Shared._Scp.Holding.Components; /// /// Runtime contribution state stored on each active holder. /// -[RegisterComponent, NetworkedComponent, AutoGenerateComponentState(raiseAfterAutoHandleState: true)] +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState(raiseAfterAutoHandleState: true, fieldDeltas: true)] [Access(typeof(SharedScpHoldingSystem))] public sealed partial class ActiveScpHolderComponent : Component { diff --git a/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.Actions.cs b/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.Actions.cs index 01ddb851bf7..71234c55163 100644 --- a/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.Actions.cs +++ b/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.Actions.cs @@ -27,7 +27,7 @@ public bool TryToggleHold(Entity holder, EntityUid target, b if (activeHolder.Target.Value == target) return TryReleaseHold(holder, target); - _popup.PopupClient("scp-hold-already-holding-other", holder); + _popup.PopupClient(Loc.GetString("scp-hold-already-holding-other"), holder); return false; } @@ -65,7 +65,7 @@ public bool CanReleaseHold(Entity holder, EntityUid target, if (activeHolder.Target != target) { if (!quiet) - _popup.PopupClient("scp-hold-already-holding-other", holder); + _popup.PopupClient(Loc.GetString("scp-hold-already-holding-other"), holder); return false; } @@ -235,7 +235,7 @@ public bool TryForceBreakOut(Entity held, bool viaM return true; } - public void SyncHolderState(Entity holder) + private void SyncHolderState(Entity holder) { SyncHolderHandBlocker(holder); } @@ -262,7 +262,7 @@ private bool TryStartFullBreakout(Entity held, bool if (fullHeld.StartedAt == TimeSpan.Zero) { - _popup.PopupClient(Loc.GetString("scp-hold-breakout-too-early", ("seconds", 1)), held); + _popup.PopupClient(Loc.GetString("scp-hold-breakout-not-ready"), held); return false; } @@ -336,7 +336,7 @@ private void ApplyFullBreakoutHolderCooldown(EntityUid holderUid) if (!_holderConfigQuery.TryComp(holderUid, out var hold)) return; - var cooldownEnd = _timing.CurTime + TimeSpan.FromTicks(hold.HoldActionCooldown.Ticks * 2); + var cooldownEnd = _timing.CurTime + hold.HoldActionCooldown * 2; if (hold.HoldAvailableAt != null && hold.HoldAvailableAt.Value >= cooldownEnd) return; diff --git a/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.CursorMove.cs b/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.CursorMove.cs index ac8fbd4a56f..408aef439e8 100644 --- a/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.CursorMove.cs +++ b/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.CursorMove.cs @@ -1,4 +1,3 @@ -using System.Diagnostics.CodeAnalysis; using System.Numerics; using Content.Shared._Scp.Holding.Components; using Robust.Shared.Map; @@ -27,7 +26,7 @@ public bool TryMoveHeldToCursor(EntityUid holderUid, EntityCoordinates cursorCoo if (activeHolder.Target == null) return false; - if (!CanMoveHeldToCursor(holderUid, cursorCoords, out _, out var targetCoords)) + if (!CanMoveHeldToCursor(holderUid, cursorCoords, out var targetCoords)) return false; SetHolderCursorMoveState((holderUid, activeHolder), targetCoords, active: true); @@ -37,11 +36,8 @@ public bool TryMoveHeldToCursor(EntityUid holderUid, EntityCoordinates cursorCoo private bool CanMoveHeldToCursor( EntityUid holderUid, EntityCoordinates cursorCoords, - [NotNullWhen(true)] out Entity? held, - out EntityCoordinates targetCoords, - bool quiet = false) + out EntityCoordinates targetCoords) { - held = null; targetCoords = EntityCoordinates.Invalid; if (!_activeHolderQuery.TryComp(holderUid, out var holder)) @@ -56,9 +52,9 @@ private bool CanMoveHeldToCursor( if (!heldComponent.Holders.Contains(holderUid)) return false; - held = (heldUid, heldComponent); + var held = (heldUid, heldComponent); - if (!TryGetHeldHoldable(held.Value, out var holdable)) + if (!TryGetHeldHoldable(held, out var holdable)) return false; if (!_container.IsInSameOrNoContainer(holderUid, heldUid)) @@ -102,7 +98,7 @@ private bool TryGetHolderCursorDesiredVelocity( if (!holder.Comp.CursorMoveActive && correctionDistance > holdable.SoftDragSettleTolerance) { holder.Comp.CursorMoveActive = true; - Dirty(holder); + DirtyField(holder, holder.Comp, nameof(ActiveScpHolderComponent.CursorMoveActive)); } if (correctionDistance <= holdable.SoftDragSettleTolerance) @@ -110,7 +106,7 @@ private bool TryGetHolderCursorDesiredVelocity( if (holder.Comp.CursorMoveActive) { holder.Comp.CursorMoveActive = false; - Dirty(holder); + DirtyField(holder, holder.Comp, nameof(ActiveScpHolderComponent.CursorMoveActive)); } return true; @@ -291,15 +287,22 @@ private void SetHolderCursorMoveState( EntityCoordinates targetCoordinates, bool active) { - if (holder.Comp.CursorTargetCoordinates == targetCoordinates - && holder.Comp.CursorMoveActive == active) - { + var targetChanged = holder.Comp.CursorTargetCoordinates != targetCoordinates; + var activeChanged = holder.Comp.CursorMoveActive != active; + if (!targetChanged && !activeChanged) return; + + if (targetChanged) + { + holder.Comp.CursorTargetCoordinates = targetCoordinates; + DirtyField(holder, holder.Comp, nameof(ActiveScpHolderComponent.CursorTargetCoordinates)); } - holder.Comp.CursorTargetCoordinates = targetCoordinates; - holder.Comp.CursorMoveActive = active; - Dirty(holder); + if (activeChanged) + { + holder.Comp.CursorMoveActive = active; + DirtyField(holder, holder.Comp, nameof(ActiveScpHolderComponent.CursorMoveActive)); + } } private void ClearHolderCursorMoveState(EntityUid holderUid) @@ -310,15 +313,22 @@ private void ClearHolderCursorMoveState(EntityUid holderUid) private void ClearHolderCursorMoveState(Entity holder) { - if (holder.Comp.CursorTargetCoordinates == EntityCoordinates.Invalid - && !holder.Comp.CursorMoveActive) - { + var targetChanged = holder.Comp.CursorTargetCoordinates != EntityCoordinates.Invalid; + var activeChanged = holder.Comp.CursorMoveActive; + if (!targetChanged && !activeChanged) return; + + if (targetChanged) + { + holder.Comp.CursorTargetCoordinates = EntityCoordinates.Invalid; + DirtyField(holder, holder.Comp, nameof(ActiveScpHolderComponent.CursorTargetCoordinates)); } - holder.Comp.CursorTargetCoordinates = EntityCoordinates.Invalid; - holder.Comp.CursorMoveActive = false; - Dirty(holder); + if (activeChanged) + { + holder.Comp.CursorMoveActive = false; + DirtyField(holder, holder.Comp, nameof(ActiveScpHolderComponent.CursorMoveActive)); + } } private void ClearHeldCursorMoveStates(Entity held) diff --git a/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.Hands.cs b/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.Hands.cs index 18268a48b93..ea3c893903f 100644 --- a/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.Hands.cs +++ b/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.Hands.cs @@ -1,4 +1,5 @@ using Content.Shared._Scp.Holding.Components; +using Content.Shared._Scp.Helpers; using Content.Shared.Hands; using Content.Shared.Hands.Components; using Content.Shared.Hands.EntitySystems; @@ -18,8 +19,7 @@ public abstract partial class SharedScpHoldingSystem [Dependency] private readonly SharedVirtualItemSystem _virtualItem = default!; private readonly List _placeholderIcons = []; - private readonly List> _virtualBlockersToDelete = []; - + private readonly HashSet _holdersSuppressingVirtualItemSync = []; private EntityQuery _handsQuery; private EntityQuery _virtualItemQuery; private EntityQuery _heldHandBlockerQuery; @@ -100,7 +100,7 @@ private void DropHeldItemsForPlaceholders(Entity held) private void DeleteInvalidHeldHandBlockers(Entity held) { - _virtualBlockersToDelete.Clear(); + using var virtualBlockersToDelete = ListPoolEntity.Rent(); foreach (var heldItem in _hands.EnumerateHeld(held)) { @@ -111,10 +111,10 @@ private void DeleteInvalidHeldHandBlockers(Entity held) continue; if (!IsValidHeldHandBlocker(virtualItem)) - _virtualBlockersToDelete.Add((heldItem, virtualItem)); + virtualBlockersToDelete.Value.Add((heldItem, virtualItem)); } - foreach (var virtualItem in _virtualBlockersToDelete) + foreach (var virtualItem in virtualBlockersToDelete.Value) { _virtualItem.DeleteVirtualItem(virtualItem, held); } @@ -131,40 +131,34 @@ private void EnsureHeldHandBlockers(Entity held) while (_hands.TryGetEmptyHand(held, out var emptyHand)) { var holderUid = _placeholderIcons[iconIndex % _placeholderIcons.Count]; - if (!_virtualItem.TrySpawnVirtualItem(holderUid, held, out var virtualItem)) + if (!TryPickupHeldHandBlockerVirtualItem(holderUid, held, emptyHand)) break; - EnsureComp(virtualItem.Value); - EnsureComp(virtualItem.Value); - _hands.DoPickup(held, emptyHand, virtualItem.Value, held.Comp); - iconIndex++; } } private void SyncHolderHandBlocker(Entity holder) { - _virtualBlockersToDelete.Clear(); + using var virtualBlockersToDelete = ListPoolEntity.Rent(); var target = holder.Comp.Target; - var holderActive = holder.Comp.LifeStage <= ComponentLifeStage.Running; var validBlockerCount = 0; var requiredHolderHandCount = 0; - if (holderActive + if (target != null && _activeHoldableQuery.HasComp(target) && TryGetRequiredHolderHandCount(target.Value, out var resolvedRequiredHolderHandCount)) { requiredHolderHandCount = resolvedRequiredHolderHandCount; } - foreach (var heldItem in _hands.EnumerateHeld((EntityUid) holder)) + foreach (var heldItem in _hands.EnumerateHeld(holder.Owner)) { if (!_virtualItemQuery.TryComp(heldItem, out var virtualItem)) continue; var ownedBlocker = _holdHandBlockerQuery.HasComp(heldItem); - var matchesCurrentTarget = holderActive - && target != null + var matchesCurrentTarget = target != null && virtualItem.BlockingEntity == target.Value; if (ownedBlocker && matchesCurrentTarget) @@ -178,15 +172,15 @@ private void SyncHolderHandBlocker(Entity holder) } if (ownedBlocker) - _virtualBlockersToDelete.Add((heldItem, virtualItem)); + virtualBlockersToDelete.Value.Add((heldItem, virtualItem)); } - foreach (var virtualItem in _virtualBlockersToDelete) + foreach (var virtualItem in virtualBlockersToDelete.Value) { RemoveHolderHandBlocker(holder, virtualItem); } - if (!holderActive || target == null) + if (target == null) return; if (!_handsQuery.TryComp(holder, out var hands)) @@ -202,11 +196,8 @@ private void SyncHolderHandBlocker(Entity holder) if (!_hands.TryGetEmptyHand(holderHands, out var emptyHand)) break; - if (!_virtualItem.TrySpawnVirtualItem(target.Value, holder, out var spawnedVirtualItem)) + if (!TryPickupHolderHandBlockerVirtualItem(target.Value, holderHands, emptyHand)) break; - - EnsureComp(spawnedVirtualItem.Value); - _hands.DoPickup(holder, emptyHand, spawnedVirtualItem.Value, hands); validBlockerCount++; } @@ -241,21 +232,69 @@ private int CountOwnedHolderHandBlockers(EntityUid holderUid, EntityUid targetUi return blockerCount; } + private bool TryPickupHeldHandBlockerVirtualItem( + EntityUid blockingEntity, + Entity user, + string handId) + { + if (!_virtualItem.TrySpawnVirtualItem(blockingEntity, user, out var virtualItemUid)) + return false; + + EnsureComp(virtualItemUid.Value); + EnsureComp(virtualItemUid.Value); + _hands.DoPickup(user, handId, virtualItemUid.Value, user.Comp); + + if (_hands.TryGetHeldItem(user, handId, out var heldItem) && heldItem == virtualItemUid.Value) + return true; + + DeleteFailedHandBlockerVirtualItem(virtualItemUid.Value); + return false; + } + + private bool TryPickupHolderHandBlockerVirtualItem( + EntityUid blockingEntity, + Entity user, + string handId) + { + if (!_virtualItem.TrySpawnVirtualItem(blockingEntity, user, out var virtualItemUid)) + return false; + + EnsureComp(virtualItemUid.Value); + _hands.DoPickup(user, handId, virtualItemUid.Value, user.Comp); + + if (_hands.TryGetHeldItem(user, handId, out var heldItem) && heldItem == virtualItemUid.Value) + return true; + + DeleteFailedHandBlockerVirtualItem(virtualItemUid.Value); + return false; + } + + private void DeleteFailedHandBlockerVirtualItem(EntityUid virtualItemUid) + { + QueueDel(virtualItemUid); + } + private void RemoveHolderHandBlocker(EntityUid holderUid, Entity virtualItem) { - if (_handsQuery.TryComp(holderUid, out var hands) && - _hands.IsHolding((holderUid, hands), virtualItem, out var hand)) + var addedSuppression = _holdersSuppressingVirtualItemSync.Add(holderUid); + + if (_handsQuery.TryComp(holderUid, out var hands) + && _hands.IsHolding((holderUid, hands), virtualItem, out var hand)) { _hands.DoDrop((holderUid, hands), hand, doDropInteraction: false, log: false); - return; + } + else + { + _virtualItem.DeleteVirtualItem(virtualItem, holderUid); } - _virtualItem.DeleteVirtualItem(virtualItem, holderUid); + if (addedSuppression) + _holdersSuppressingVirtualItemSync.Remove(holderUid); } private void DeleteHolderHandBlockers(EntityUid holderUid) { - _virtualBlockersToDelete.Clear(); + using var virtualBlockersToDelete = ListPoolEntity.Rent(); foreach (var heldItem in _hands.EnumerateHeld(holderUid)) { @@ -265,10 +304,10 @@ private void DeleteHolderHandBlockers(EntityUid holderUid) if (!_virtualItemQuery.TryComp(heldItem, out var virtualItem)) continue; - _virtualBlockersToDelete.Add((heldItem, virtualItem)); + virtualBlockersToDelete.Value.Add((heldItem, virtualItem)); } - foreach (var virtualItem in _virtualBlockersToDelete) + foreach (var virtualItem in virtualBlockersToDelete.Value) { RemoveHolderHandBlocker(holderUid, virtualItem); } @@ -276,18 +315,18 @@ private void DeleteHolderHandBlockers(EntityUid holderUid) private void DeleteHeldHandBlockers(EntityUid heldUid) { - _virtualBlockersToDelete.Clear(); + using var virtualBlockersToDelete = ListPoolEntity.Rent(); foreach (var heldItem in _hands.EnumerateHeld(heldUid)) { if (_heldHandBlockerQuery.HasComp(heldItem) && _virtualItemQuery.TryComp(heldItem, out var virtualItem)) { - _virtualBlockersToDelete.Add((heldItem, virtualItem)); + virtualBlockersToDelete.Value.Add((heldItem, virtualItem)); } } - foreach (var virtualItem in _virtualBlockersToDelete) + foreach (var virtualItem in virtualBlockersToDelete.Value) { _virtualItem.DeleteVirtualItem(virtualItem, heldUid); } @@ -344,6 +383,12 @@ private void OnHolderVirtualItemDeleted(Entity ent, re if (_timing.ApplyingState) return; + if (_holdersSuppressingVirtualItemSync.Contains(ent)) + return; + + if (TerminatingOrDeleted(ent)) + return; + if (ent.Comp.Target == null || ent.Comp.Target != args.BlockingEntity) return; diff --git a/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.Lifecycle.cs b/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.Lifecycle.cs index 1cdd49fa12c..863a1003241 100644 --- a/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.Lifecycle.cs +++ b/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.Lifecycle.cs @@ -77,7 +77,6 @@ private void OnHolderStartup(Entity ent, ref Component private void OnHolderShutdown(Entity ent, ref ComponentShutdown args) { - ent.Comp.Target = null; DeleteHolderHandBlockers(ent); if (!_timing.ApplyingState) diff --git a/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.State.cs b/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.State.cs index 4c660b5f49f..f4e820b1240 100644 --- a/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.State.cs +++ b/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.State.cs @@ -100,35 +100,25 @@ private void AddHolderContribution(EntityUid holderUid, Entity= 0; i--) - { - if (held.Holders[i] != holderUid) - continue; - - held.Holders.RemoveAt(i); - removed = true; - } - - if (removed) - Dirty(targetUid, held); + if (!holdable.Holders.Remove(holderUid)) + return; - if (_activeHolderQuery.HasComp(holderUid)) - RemComp(holderUid); - else if (_activeHolderSlowdownStateQuery.HasComp(holderUid)) - RemComp(holderUid); + DirtyField(targetUid, holdable, nameof(ActiveScpHoldableComponent.Holders)); + RemComp(holderUid); - if (held.Holders.Count == 0) + var ent = (targetUid, holdable); + if (holdable.Holders.Count == 0) { if (clearIfEmpty) - ClearHoldState((targetUid, held), applyImmunity: false); + ClearHoldState(ent, applyImmunity: false); + return; } - SyncHeldState((targetUid, held)); + SyncHeldState(ent); } protected void SyncHeldState(Entity held) @@ -300,6 +290,6 @@ private void SetHolderTarget(Entity holder, EntityUid? return; holder.Comp.Target = target; - Dirty(holder); + DirtyField(holder, holder.Comp, nameof(ActiveScpHolderComponent.Target)); } } diff --git a/Content.Shared/_Scp/Other/WorldAlert/WorldAlertSystem.cs b/Content.Shared/_Scp/Other/WorldAlert/WorldAlertSystem.cs index ae6dd83406f..fe5b1295e4d 100644 --- a/Content.Shared/_Scp/Other/WorldAlert/WorldAlertSystem.cs +++ b/Content.Shared/_Scp/Other/WorldAlert/WorldAlertSystem.cs @@ -7,11 +7,11 @@ namespace Content.Shared._Scp.Other.WorldAlert; public sealed class WorldAlertSystem : EntitySystem { - private const float DefaultLifetimeSeconds = 1f; - [Dependency] private readonly SharedAudioSystem _audio = default!; [Dependency] private readonly INetManager _net = default!; + private const float DefaultLifetimeSeconds = 1f; + public bool TrySpawnAlert(EntityUid target, WorldAlertSettings settings, EntityUid? soundReceiver = null) { if (settings.Prototype == null) @@ -31,12 +31,11 @@ public bool TrySpawnAlert(EntityUid target, WorldAlertSettings settings, EntityU private void EnsureTimedDespawn(EntityUid uid, TimeSpan? lifetime) { - if (HasComp(uid)) - return; - var despawn = EnsureComp(uid); - despawn.Lifetime = lifetime.HasValue - ? (float) lifetime.Value.TotalSeconds - : DefaultLifetimeSeconds; + + if (lifetime.HasValue) + despawn.Lifetime = (float)lifetime.Value.TotalSeconds; + else if (despawn.Lifetime < DefaultLifetimeSeconds) + despawn.Lifetime = DefaultLifetimeSeconds; } } diff --git a/Content.Shared/_Scp/Scp096/Main/Systems/SharedScp096System.Holding.cs b/Content.Shared/_Scp/Scp096/Main/Systems/SharedScp096System.Holding.cs index b8e236636ff..d0d1d3046d7 100644 --- a/Content.Shared/_Scp/Scp096/Main/Systems/SharedScp096System.Holding.cs +++ b/Content.Shared/_Scp/Scp096/Main/Systems/SharedScp096System.Holding.cs @@ -1,3 +1,4 @@ +using System.Diagnostics; using System.Numerics; using Content.Shared._Scp.Holding; using Content.Shared._Scp.Holding.Components; @@ -88,13 +89,12 @@ private void ApplyHoldBreakoutEffects( private static Vector2 GetHoldBreakoutDirection(Vector2 holderPosition, Vector2 scpPosition, int holderIndex, int holderCount) { + Debug.Assert(holderCount > 0); + var direction = holderPosition - scpPosition; if (direction.LengthSquared() >= 0.001f) return Vector2.Normalize(direction); - if (holderCount <= 0) - return Vector2.UnitX; - var angle = 2f * MathF.PI * holderIndex / holderCount; return new Vector2(MathF.Cos(angle), MathF.Sin(angle)); } diff --git a/Resources/Locale/en-US/_strings/_scp/holding/holding.ftl b/Resources/Locale/en-US/_strings/_scp/holding/holding.ftl index 01238b487ad..db56ac7a2a9 100644 --- a/Resources/Locale/en-US/_strings/_scp/holding/holding.ftl +++ b/Resources/Locale/en-US/_strings/_scp/holding/holding.ftl @@ -7,6 +7,7 @@ scp-hold-target-too-far = You are too far away to hold {THE($target)}. scp-hold-holder-no-free-hand = You need enough free hands to grab {THE($target)}. scp-hold-holder-action-on-cooldown = You can grab again in {$seconds} s. scp-hold-breakout-too-early = You can try to break free in {$seconds} s. +scp-hold-breakout-not-ready = You cannot start breaking free yet. scp-hold-breakout-start = You start trying to break free. scp-hold-breakout-interrupted = Your breakout attempt was interrupted. scp-hold-action-restricted = You cannot do that while being held. diff --git a/Resources/Locale/ru-RU/_strings/_scp/holding/holding.ftl b/Resources/Locale/ru-RU/_strings/_scp/holding/holding.ftl index 6e305544d3e..2b20c3e082b 100644 --- a/Resources/Locale/ru-RU/_strings/_scp/holding/holding.ftl +++ b/Resources/Locale/ru-RU/_strings/_scp/holding/holding.ftl @@ -7,8 +7,10 @@ scp-hold-target-too-far = Вы слишком далеко, чтобы удер scp-hold-holder-no-free-hand = Чтобы схватить {THE($target)}, нужно достаточно свободных рук. scp-hold-holder-action-on-cooldown = Схватить снова можно через {$seconds} с. scp-hold-breakout-too-early = Попытаться вырваться можно через {$seconds} с. +scp-hold-breakout-not-ready = Вы пока не можете начать вырываться. scp-hold-breakout-start = Вы начинаете вырываться. scp-hold-breakout-interrupted = Попытка вырваться была прервана. scp-hold-action-restricted = Вы не можете сделать это, пока вас удерживают. alerts-scp-held-name = Вас удерживают силой -alerts-scp-held-desc = Кто-то физически держит вас. Двигайтесь или нажмите на этот статус-эффект, чтобы попытаться вырваться. В полном удержании сначала нужно выдержать захват. +alerts-scp-held-desc = Кто-то физически держит вас. + OOC: Двигайтесь или нажмите на этот статус-эффект, чтобы попытаться вырваться. В полном удержании сначала нужно выдержать захват. From 903a0ede9aba398b55d012a5cee84438051ad47d Mon Sep 17 00:00:00 2001 From: drdth Date: Tue, 21 Apr 2026 07:16:48 +0300 Subject: [PATCH 25/27] fix: holding scps --- .../Compatibility/PullingSystem.ScpHolding.cs | 10 ++-------- .../_Scp/Holding/Components/ScpHoldableComponent.cs | 7 +++++++ .../Systems/SharedScpHoldingSystem.CursorMove.cs | 13 +++++++++++++ .../SharedScpHoldingSystem.PullCompatibility.cs | 5 +++++ .../_Scp/Mobs/Systems/ScpRestrictionSystem.cs | 7 +++++++ 5 files changed, 34 insertions(+), 8 deletions(-) diff --git a/Content.Shared/_Scp/Holding/Compatibility/PullingSystem.ScpHolding.cs b/Content.Shared/_Scp/Holding/Compatibility/PullingSystem.ScpHolding.cs index 657ac816edc..e1fc8e8db1a 100644 --- a/Content.Shared/_Scp/Holding/Compatibility/PullingSystem.ScpHolding.cs +++ b/Content.Shared/_Scp/Holding/Compatibility/PullingSystem.ScpHolding.cs @@ -10,15 +10,11 @@ public sealed partial class PullingSystem [Dependency] private readonly SharedScpHoldingSystem _scpHolding = default!; private EntityQuery _pullableQuery; - private EntityQuery _scpHolderConfigQuery; - private EntityQuery _scpHoldableQuery; private EntityQuery _scpActiveHolderQuery; private void InitializeScpHolding() { _pullableQuery = GetEntityQuery(); - _scpHolderConfigQuery = GetEntityQuery(); - _scpHoldableQuery = GetEntityQuery(); _scpActiveHolderQuery = GetEntityQuery(); } @@ -35,12 +31,10 @@ private bool TryRedirectPullToScpHold(EntityUid pullerUid, EntityUid pullableUid { success = false; - if (!_scpHolderConfigQuery.TryComp(pullerUid, out var holdComp) || - !_scpHoldableQuery.HasComp(pullableUid)) - { + if (!_scpHolding.CanRedirectPullToScpHold(pullerUid, pullableUid)) return false; - } + var holdComp = Comp(pullerUid); var holder = (pullerUid, holdComp); if (_scpActiveHolderQuery.TryComp(pullerUid, out var activeHolder) && diff --git a/Content.Shared/_Scp/Holding/Components/ScpHoldableComponent.cs b/Content.Shared/_Scp/Holding/Components/ScpHoldableComponent.cs index c4610f9d467..ef34dc24761 100644 --- a/Content.Shared/_Scp/Holding/Components/ScpHoldableComponent.cs +++ b/Content.Shared/_Scp/Holding/Components/ScpHoldableComponent.cs @@ -138,4 +138,11 @@ public sealed partial class ScpHoldableComponent : Component /// [DataField, AutoNetworkedField] public float HolderSprintModifier = 0.7f; + + /// + /// Optional speed modifier applied only while the held target is moved toward the cursor. + /// If not set, is used. + /// + [DataField, AutoNetworkedField] + public float? CursorMoveSpeedModifier; } diff --git a/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.CursorMove.cs b/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.CursorMove.cs index 408aef439e8..9e0692d15e4 100644 --- a/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.CursorMove.cs +++ b/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.CursorMove.cs @@ -127,6 +127,7 @@ private bool TryGetHolderCursorDesiredVelocity( if (awaySpeed > 0f) desiredVelocity += correctionDirection * awaySpeed * holdable.SoftDragAwayVelocityStrength; + desiredVelocity = ApplyCursorMoveSpeedModifier(desiredVelocity, holdable); return true; } @@ -238,6 +239,18 @@ private Vector2 GetDirectCursorCorrectionVelocity( return correctionDirection * correctionSpeed; } + private static Vector2 ApplyCursorMoveSpeedModifier(Vector2 desiredVelocity, ScpHoldableComponent holdable) + { + if (desiredVelocity == Vector2.Zero) + return desiredVelocity; + + var speedModifier = Math.Max(0f, holdable.CursorMoveSpeedModifier ?? holdable.HolderSprintModifier); + if (MathF.Abs(speedModifier - 1f) <= 0.0001f) + return desiredVelocity; + + return desiredVelocity * speedModifier; + } + private bool TryNormalizeHeldCursorMoveTargetCoordinates( EntityUid holderUid, EntityCoordinates cursorCoords, diff --git a/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.PullCompatibility.cs b/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.PullCompatibility.cs index 22254614325..0a7c335a416 100644 --- a/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.PullCompatibility.cs +++ b/Content.Shared/_Scp/Holding/Systems/SharedScpHoldingSystem.PullCompatibility.cs @@ -5,6 +5,11 @@ namespace Content.Shared._Scp.Holding.Systems; public abstract partial class SharedScpHoldingSystem { + public bool CanRedirectPullToScpHold(EntityUid pullerUid, EntityUid pullableUid) + { + return _holderConfigQuery.HasComp(pullerUid) && _holdableQuery.HasComp(pullableUid); + } + private bool CanPassPullAttempt(EntityUid holderUid, EntityUid targetUid) { if (!_actionBlocker.CanInteract(holderUid, targetUid)) diff --git a/Content.Shared/_Scp/Mobs/Systems/ScpRestrictionSystem.cs b/Content.Shared/_Scp/Mobs/Systems/ScpRestrictionSystem.cs index 42fc982470f..c834a2e3f9c 100644 --- a/Content.Shared/_Scp/Mobs/Systems/ScpRestrictionSystem.cs +++ b/Content.Shared/_Scp/Mobs/Systems/ScpRestrictionSystem.cs @@ -1,4 +1,5 @@ using Content.Shared._Scp.Mobs.Components; +using Content.Shared._Scp.Holding.Systems; using Content.Shared._Scp.ScpMask; using Content.Shared._Sunrise.Carrying; using Content.Shared.Actions.Events; @@ -19,6 +20,7 @@ namespace Content.Shared._Scp.Mobs.Systems; public sealed class ScpRestrictionSystem : EntitySystem { [Dependency] private readonly MobStateSystem _mobState = default!; + [Dependency] private readonly SharedScpHoldingSystem _scpHolding = default!; [Dependency] private readonly ScpMaskSystem _scpMask = default!; public override void Initialize() @@ -61,6 +63,11 @@ private static void OnPullAttempt(Entity ent, ref PullA private void OnBeingPulled(Entity ent, ref BeingPulledAttemptEvent args) { + // Fire added start - let SCP hold validation pass for restricted SCP hold targets. + if (_scpHolding.CanRedirectPullToScpHold(args.Puller, ent)) + return; + // Fire added end + var canBePulled = _mobState.IsIncapacitated(ent) || HasComp(ent) || _scpMask.HasScpMask(ent) From 7859510e567f64b437b67887e265eb7ab1c38653 Mon Sep 17 00:00:00 2001 From: drdth Date: Tue, 21 Apr 2026 07:19:22 +0300 Subject: [PATCH 26/27] fix: ai review --- Content.Shared/_Scp/Other/WorldAlert/WorldAlertSystem.cs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/Content.Shared/_Scp/Other/WorldAlert/WorldAlertSystem.cs b/Content.Shared/_Scp/Other/WorldAlert/WorldAlertSystem.cs index fe5b1295e4d..7fa4df51b54 100644 --- a/Content.Shared/_Scp/Other/WorldAlert/WorldAlertSystem.cs +++ b/Content.Shared/_Scp/Other/WorldAlert/WorldAlertSystem.cs @@ -21,10 +21,15 @@ public bool TrySpawnAlert(EntityUid target, WorldAlertSettings settings, EntityU EnsureTimedDespawn(alert, settings.Lifetime); soundReceiver ??= target; - if (settings.DirectSound && _net.IsServer) - _audio.PlayEntity(settings.Sound, target, soundReceiver.Value); + if (settings.DirectSound) + { + if (_net.IsServer) + _audio.PlayEntity(settings.Sound, target, soundReceiver.Value); + } else + { _audio.PlayPredicted(settings.Sound, target, soundReceiver.Value); + } return true; } From 549786a6b7947d8fd6a10d3ad8b791a4e3c37c14 Mon Sep 17 00:00:00 2001 From: drdth Date: Tue, 21 Apr 2026 07:36:49 +0300 Subject: [PATCH 27/27] i am ready to merge --- Resources/Prototypes/Entities/Mobs/Species/base.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/Resources/Prototypes/Entities/Mobs/Species/base.yml b/Resources/Prototypes/Entities/Mobs/Species/base.yml index f5fd8a7565e..cc0398a846f 100644 --- a/Resources/Prototypes/Entities/Mobs/Species/base.yml +++ b/Resources/Prototypes/Entities/Mobs/Species/base.yml @@ -278,7 +278,6 @@ - type: CritHeartbeat # Sunrise-End # Fire start - - type: ScpHolder # TODO: Убрать перед мержем - type: ScpHoldable - type: FieldOfView - type: Blinkable