From 79f58a0314031b852d616a7f0719371ded6bcf8f Mon Sep 17 00:00:00 2001 From: Perry Fraser Date: Sat, 20 Dec 2025 14:24:04 -0500 Subject: [PATCH] Don't process paused MoverControllers (#39444) * refactor: make MoverController use more queries * perf: don't process paused MoverControllers * perf: track active input movers via events * Revert "place stored changeling identities next to each other (#39452)" This reverts commit 9b5d2ff11b8f19fafbf97d6ceab238028ca6dfeb. * perf: keep around the seen movers hashset * fix: don't reintroduce wild wild west ordering * style: use virtual method Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com> * docs: better ActiveInputMoverComponent motiviation Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com> * fix: pass through known comp * fix: properly order relay movers for real * perf: use proxy Transform() and inline it Actually this might be a slight performance improvement since it avoids the dictionary lookup until the case that its body status is on ground. * style: switch an event handler to Entity * fix: just-in-case track for relay loops * merg conflix * borger * whitespace moment * whoops * empty --------- Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com> Co-authored-by: Princess Cheeseballs <66055347+Pronana@users.noreply.github.com> Co-authored-by: ArtisticRoomba <145879011+ArtisticRoomba@users.noreply.github.com> --- .../Physics/Controllers/MoverController.cs | 208 +++++++++++++----- .../ActionBlocker/ActionBlockerSystem.cs | 10 +- .../Systems/SharedChangelingIdentitySystem.cs | 7 +- .../Components/ActiveInputMoverComponent.cs | 27 +++ .../Systems/SharedMoverController.Relay.cs | 6 +- .../Movement/Systems/SharedMoverController.cs | 21 +- 6 files changed, 210 insertions(+), 69 deletions(-) create mode 100644 Content.Shared/Movement/Components/ActiveInputMoverComponent.cs diff --git a/Content.Server/Physics/Controllers/MoverController.cs b/Content.Server/Physics/Controllers/MoverController.cs index 5c87de1863..f933c72103 100644 --- a/Content.Server/Physics/Controllers/MoverController.cs +++ b/Content.Server/Physics/Controllers/MoverController.cs @@ -1,18 +1,16 @@ +using System.Diagnostics; using System.Numerics; using System.Runtime.CompilerServices; using Content.Server.Shuttles.Components; using Content.Server.Shuttles.Systems; -using Content.Shared.Friction; using Content.Shared.Movement.Components; using Content.Shared.Movement.Systems; using Content.Shared.Shuttles.Components; using Content.Shared.Shuttles.Systems; using Prometheus; -using Robust.Shared.Physics.Components; using Robust.Shared.Player; -using DroneConsoleComponent = Content.Server.Shuttles.DroneConsoleComponent; using DependencyAttribute = Robust.Shared.IoC.DependencyAttribute; -using Robust.Shared.Map.Components; +using DroneConsoleComponent = Content.Server.Shuttles.DroneConsoleComponent; namespace Content.Server.Physics.Controllers; @@ -20,20 +18,111 @@ public sealed class MoverController : SharedMoverController { private static readonly Gauge ActiveMoverGauge = Metrics.CreateGauge( "physics_active_mover_count", - "Active amount of InputMovers being processed by MoverController"); + "Amount of ActiveInputMovers being processed by MoverController"); [Dependency] private readonly ThrusterSystem _thruster = default!; - [Dependency] private readonly SharedTransformSystem _xformSystem = default!; private Dictionary)> _shuttlePilots = new(); + private EntityQuery _activeQuery; + private EntityQuery _droneQuery; + private EntityQuery _shuttleQuery; + + // Not needed for persistence; just used to save an alloc + private readonly HashSet _seenMovers = []; + private readonly HashSet _seenRelayMovers = []; + private readonly List> _moversToUpdate = []; + public override void Initialize() { base.Initialize(); + + SubscribeLocalEvent(OnEntityPaused); + SubscribeLocalEvent(OnEntityUnpaused); + SubscribeLocalEvent(OnRelayPlayerAttached); SubscribeLocalEvent(OnRelayPlayerDetached); SubscribeLocalEvent(OnPlayerAttached); SubscribeLocalEvent(OnPlayerDetached); + + _activeQuery = GetEntityQuery(); + _droneQuery = GetEntityQuery(); + _shuttleQuery = GetEntityQuery(); + } + + private void OnEntityPaused(Entity ent, ref EntityPausedEvent args) + { + // Become unactive [sic] if we don't have PhysicsComp.IgnorePaused + if (PhysicsQuery.TryComp(ent, out var phys) && phys.IgnorePaused) + return; + RemCompDeferred(ent); + } + + private void OnEntityUnpaused(Entity ent, ref EntityUnpausedEvent args) + { + UpdateMoverStatus((ent, ent.Comp)); + } + + protected override void OnMoverStartup(Entity ent, ref ComponentStartup args) + { + base.OnMoverStartup(ent, ref args); + UpdateMoverStatus((ent, ent.Comp)); + } + + protected override void OnTargetRelayShutdown(Entity ent, ref ComponentShutdown args) + { + base.OnTargetRelayShutdown(ent, ref args); + UpdateMoverStatus((ent, null, ent.Comp)); + } + + protected override void UpdateMoverStatus(Entity ent) + { + // Track that we aren't in a loop of movement relayers + _seenMovers.Clear(); + while (true) + { + if (!MoverQuery.Resolve(ent, ref ent.Comp1, logMissing: false)) + { + RemCompDeferred(ent); + break; + } + + var meta = MetaData(ent); + if (Terminating(ent, meta)) + break; + + ActiveInputMoverComponent? activeMover = null; + if (!meta.EntityPaused + || PhysicsQuery.TryComp(ent, out var phys) && phys.IgnorePaused) + activeMover = EnsureComp(ent); + + // If we're a relay target, make sure our drivers are InputMovers + if (RelayTargetQuery.Resolve(ent, ref ent.Comp2, logMissing: false) + // In case we're called from ComponentShutdown: + && ent.Comp2.LifeStage <= ComponentLifeStage.Running + && Exists(ent.Comp2.Source) + && !_seenMovers.Contains(ent.Comp2.Source)) + { + if (ent.Comp2.Source == ent.Owner) + { + Log.Error($"Entity {ToPrettyString(ent)} is attempting to relay movement to itself!"); + break; + } + + if (activeMover is not null) + activeMover.RelayedFrom = ent.Comp2.Source; + + ent = ent.Comp2.Source; + _seenMovers.Add(ent); + continue; + } + + // No longer a well-defined relay target + if (activeMover is not null) + activeMover.RelayedFrom = null; + + break; + } } private void OnRelayPlayerAttached(Entity entity, ref PlayerAttachedEvent args) @@ -63,48 +152,70 @@ public sealed class MoverController : SharedMoverController return true; } - private HashSet _moverAdded = new(); - private List> _movers = new(); - - private void InsertMover(Entity source) + public override void UpdateBeforeSolve(bool prediction, float frameTime) { - if (TryComp(source, out MovementRelayTargetComponent? relay)) - { - if (TryComp(relay.Source, out InputMoverComponent? relayMover)) - { - InsertMover((relay.Source, relayMover)); - } - } + base.UpdateBeforeSolve(prediction, frameTime); - // Already added - if (!_moverAdded.Add(source.Owner)) - return; + // We use _seenMovers here as well as in UpdateMoverStatus—this means we + // cannot have any events get fired while we use it in this while loop. + _seenMovers.Clear(); + _moversToUpdate.Clear(); - _movers.Add(source); - } + // Don't use EntityQueryEnumerator because admin ghosts have to move on + // paused maps. Pausing movers is handled via ActiveInputMoverComponent. + var inputQueryEnumerator = AllEntityQuery(); + while (inputQueryEnumerator.MoveNext(out var uid, out var activeComp, out var moverComp)) + { + _seenRelayMovers.Clear(); // O(1) if already empty + QueueRelaySources(activeComp.RelayedFrom); - public override void UpdateBeforeSolve(bool prediction, float frameTime) - { - base.UpdateBeforeSolve(prediction, frameTime); + // If it's already inserted, that's fine—that means it'll still be + // handled before its child movers + AddMover((uid, moverComp)); + } - _moverAdded.Clear(); - _movers.Clear(); - var inputQueryEnumerator = AllEntityQuery(); + ActiveMoverGauge.Set(_moversToUpdate.Count); - // Need to order mob movement so that movers don't run before their relays. - while (inputQueryEnumerator.MoveNext(out var uid, out var mover)) + foreach (var ent in _moversToUpdate) { - InsertMover((uid, mover)); + HandleMobMovement(ent, frameTime); } - foreach (var mover in _movers) + HandleShuttleMovement(frameTime); + return; + + // When we insert a chain of relay sources we have to flip its ordering + // It's going to be extremely uncommon for a relay chain to be more than + // one entity so we just recurse as needed. + void QueueRelaySources(EntityUid? next) { - HandleMobMovement(mover, frameTime); + // We only care if it's still a mover + if (!_activeQuery.TryComp(next, out var nextActive) + || !MoverQuery.TryComp(next, out var nextMover) + || !_seenRelayMovers.Add(next.Value)) + return; + + Debug.Assert(next.Value != nextActive.RelayedFrom); + + // While it is (as of writing) currently true that this recursion + // should always terminate due to RelayedFrom always being written + // in a way that tracks if it's made a loop, we still take the extra + // memory (and small time cost) of making sure via _seenRelayMovers. + QueueRelaySources(nextActive.RelayedFrom); + AddMover((next.Value, nextMover)); } - ActiveMoverGauge.Set(_movers.Count); + // Track inserts so we have ~ O(1) inserts without duplicates. Hopefully + // it doesn't matter that both _seenMovers and _moversToUpdate are never + // trimmed? They should be pretty memory light anyway, and in general + // it'll be rare for there to be a decrease in movers. + void AddMover(Entity entity) + { + if (!_seenMovers.Add(entity)) + return; - HandleShuttleMovement(frameTime); + _moversToUpdate.Add(entity); + } } public (Vector2 Strafe, float Rotation, float Brakes) GetPilotVelocityInput(PilotComponent component) @@ -152,7 +263,7 @@ public sealed class MoverController : SharedMoverController protected override void HandleShuttleInput(EntityUid uid, ShuttleButtons button, ushort subTick, bool state) { - if (!TryComp(uid, out var pilot) || pilot.Console == null) + if (!PilotQuery.TryComp(uid, out var pilot) || pilot.Console == null) return; ResetSubtick(pilot); @@ -263,27 +374,25 @@ public sealed class MoverController : SharedMoverController // We just mark off their movement and the shuttle itself does its own movement var activePilotQuery = EntityQueryEnumerator(); - var shuttleQuery = GetEntityQuery(); while (activePilotQuery.MoveNext(out var uid, out var pilot, out var mover)) { var consoleEnt = pilot.Console; // TODO: This is terrible. Just make a new mover and also make it remote piloting + device networks - if (TryComp(consoleEnt, out var cargoConsole)) - { + if (_droneQuery.TryComp(consoleEnt, out var cargoConsole)) consoleEnt = cargoConsole.Entity; - } - if (!TryComp(consoleEnt, out TransformComponent? xform)) continue; + if (!XformQuery.TryComp(consoleEnt, out var xform)) + continue; var gridId = xform.GridUid; // This tries to see if the grid is a shuttle and if the console should work. - if (!TryComp(gridId, out var _) || - !shuttleQuery.TryGetComponent(gridId, out var shuttleComponent) || + if (!MapGridQuery.HasComp(gridId) || + !_shuttleQuery.TryGetComponent(gridId, out var shuttleComponent) || !shuttleComponent.Enabled) continue; - if (!newPilots.TryGetValue(gridId!.Value, out var pilots)) + if (!newPilots.TryGetValue(gridId.Value, out var pilots)) { pilots = (shuttleComponent, new List<(EntityUid, PilotComponent, InputMoverComponent, TransformComponent)>()); newPilots[gridId.Value] = pilots; @@ -305,13 +414,12 @@ public sealed class MoverController : SharedMoverController // Collate all of the linear / angular velocites for a shuttle // then do the movement input once for it. - var xformQuery = GetEntityQuery(); foreach (var (shuttleUid, (shuttle, pilots)) in _shuttlePilots) { - if (Paused(shuttleUid) || CanPilot(shuttleUid) || !TryComp(shuttleUid, out var body)) + if (Paused(shuttleUid) || CanPilot(shuttleUid) || !PhysicsQuery.TryComp(shuttleUid, out var body)) continue; - var shuttleNorthAngle = _xformSystem.GetWorldRotation(shuttleUid, xformQuery); + var shuttleNorthAngle = TransformSystem.GetWorldRotation(shuttleUid, XformQuery); // Collate movement linear and angular inputs together var linearInput = Vector2.Zero; @@ -321,7 +429,7 @@ public sealed class MoverController : SharedMoverController var brakeCount = 0; var angularCount = 0; - foreach (var (pilotUid, pilot, _, consoleXform) in pilots) + foreach (var (_, pilot, _, consoleXform) in pilots) { var (strafe, rotation, brakes) = GetPilotVelocityInput(pilot); @@ -571,9 +679,9 @@ public sealed class MoverController : SharedMoverController private bool CanPilot(EntityUid shuttleUid) { - return TryComp(shuttleUid, out var ftl) + return FTLQuery.TryComp(shuttleUid, out var ftl) && (ftl.State & (FTLState.Starting | FTLState.Travelling | FTLState.Arriving)) != 0x0 - || HasComp(shuttleUid); + || PreventPilotQuery.HasComp(shuttleUid); } } diff --git a/Content.Shared/ActionBlocker/ActionBlockerSystem.cs b/Content.Shared/ActionBlocker/ActionBlockerSystem.cs index c256872cc7..485dc89580 100644 --- a/Content.Shared/ActionBlocker/ActionBlockerSystem.cs +++ b/Content.Shared/ActionBlocker/ActionBlockerSystem.cs @@ -30,15 +30,11 @@ namespace Content.Shared.ActionBlocker base.Initialize(); _complexInteractionQuery = GetEntityQuery(); - - SubscribeLocalEvent(OnMoverStartup); - } - - private void OnMoverStartup(EntityUid uid, InputMoverComponent component, ComponentStartup args) - { - UpdateCanMove(uid, component); } + // These two methods should probably both live in SharedMoverController + // but they're called in a million places and I'm not doing that + // refactor right now. public bool CanMove(EntityUid uid, InputMoverComponent? component = null) { return Resolve(uid, ref component, false) && component.CanMove; diff --git a/Content.Shared/Changeling/Systems/SharedChangelingIdentitySystem.cs b/Content.Shared/Changeling/Systems/SharedChangelingIdentitySystem.cs index 830aed6ab6..57439d0a0b 100644 --- a/Content.Shared/Changeling/Systems/SharedChangelingIdentitySystem.cs +++ b/Content.Shared/Changeling/Systems/SharedChangelingIdentitySystem.cs @@ -23,7 +23,6 @@ public abstract class SharedChangelingIdentitySystem : EntitySystem [Dependency] private readonly SharedPvsOverrideSystem _pvsOverrideSystem = default!; public MapId? PausedMapId; - private int _numberOfStoredIdentities = 0; // TODO: remove this public override void Initialize() { @@ -99,11 +98,7 @@ public abstract class SharedChangelingIdentitySystem : EntitySystem return null; EnsurePausedMap(); - // TODO: Setting the spawn location is a shitty bandaid to prevent admins from crashing our servers. - // Movercontrollers and mob collisions are currently being calculated even for paused entities. - // Spawning all of them in the same spot causes severe performance problems. - // Cryopods and Polymorph have the same problem. - var clone = Spawn(speciesPrototype.Prototype, new MapCoordinates(new Vector2(2 * _numberOfStoredIdentities++, 0), PausedMapId!.Value)); + var clone = Spawn(speciesPrototype.Prototype, new MapCoordinates(Vector2.Zero, PausedMapId!.Value)); var storedIdentity = EnsureComp(clone); storedIdentity.OriginalEntity = target; // TODO: network this once we have WeakEntityReference or the autonetworking source gen is fixed diff --git a/Content.Shared/Movement/Components/ActiveInputMoverComponent.cs b/Content.Shared/Movement/Components/ActiveInputMoverComponent.cs new file mode 100644 index 0000000000..24a0d9eacd --- /dev/null +++ b/Content.Shared/Movement/Components/ActiveInputMoverComponent.cs @@ -0,0 +1,27 @@ +using Content.Shared.Movement.Systems; + +namespace Content.Shared.Movement.Components; + +/// +/// Marker component for entities that are being processed by MoverController. +/// +/// +/// The idea here is to keep track via event subscriptions which mover +/// controllers actually need to be processed. Instead of having this be a +/// boolean field on the , we instead track it +/// as a separate component which is much faster to query all at once. +/// +/// +/// +[RegisterComponent, Access(typeof(SharedMoverController))] +public sealed partial class ActiveInputMoverComponent : Component +{ + /// + /// Cached version of . + /// + /// + /// This must not form a loop of EntityUids. + /// + [DataField, ViewVariables] + public EntityUid? RelayedFrom; +}; diff --git a/Content.Shared/Movement/Systems/SharedMoverController.Relay.cs b/Content.Shared/Movement/Systems/SharedMoverController.Relay.cs index 79c593cbe7..09cf8eb706 100644 --- a/Content.Shared/Movement/Systems/SharedMoverController.Relay.cs +++ b/Content.Shared/Movement/Systems/SharedMoverController.Relay.cs @@ -1,4 +1,3 @@ -using Content.Shared.ActionBlocker; using Content.Shared.Movement.Components; namespace Content.Shared.Movement.Systems; @@ -61,6 +60,7 @@ public abstract partial class SharedMoverController Dirty(uid, component); Dirty(relayEntity, targetComp); _blocker.UpdateCanMove(uid); + UpdateMoverStatus((relayEntity, null, targetComp)); } private void OnRelayShutdown(Entity entity, ref ComponentShutdown args) @@ -80,7 +80,7 @@ public abstract partial class SharedMoverController _blocker.UpdateCanMove(entity.Owner); } - private void OnTargetRelayShutdown(Entity entity, ref ComponentShutdown args) + protected virtual void OnTargetRelayShutdown(Entity entity, ref ComponentShutdown args) { PhysicsSystem.UpdateIsPredicted(entity.Owner); PhysicsSystem.UpdateIsPredicted(entity.Comp.Source); @@ -91,4 +91,6 @@ public abstract partial class SharedMoverController if (TryComp(entity.Comp.Source, out RelayInputMoverComponent? relay) && relay.LifeStage <= ComponentLifeStage.Running) RemComp(entity.Comp.Source, relay); } + + protected virtual void UpdateMoverStatus(Entity ent) { } } diff --git a/Content.Shared/Movement/Systems/SharedMoverController.cs b/Content.Shared/Movement/Systems/SharedMoverController.cs index 5d24b16621..ff034a9c07 100644 --- a/Content.Shared/Movement/Systems/SharedMoverController.cs +++ b/Content.Shared/Movement/Systems/SharedMoverController.cs @@ -9,6 +9,7 @@ using Content.Shared.Maps; using Content.Shared.Mobs.Systems; using Content.Shared.Movement.Components; using Content.Shared.Movement.Events; +using Content.Shared.Shuttles.Components; using Content.Shared.Tag; using Robust.Shared.Audio; using Robust.Shared.Audio.Systems; @@ -48,6 +49,7 @@ public abstract partial class SharedMoverController : VirtualController protected EntityQuery CanMoveInAirQuery; protected EntityQuery FootstepModifierQuery; + protected EntityQuery FTLQuery; protected EntityQuery MoverQuery; protected EntityQuery MapQuery; protected EntityQuery MapGridQuery; @@ -56,6 +58,8 @@ public abstract partial class SharedMoverController : VirtualController protected EntityQuery ModifierQuery; protected EntityQuery NoRotateQuery; protected EntityQuery PhysicsQuery; + protected EntityQuery PilotQuery; + protected EntityQuery PreventPilotQuery; protected EntityQuery RelayQuery; protected EntityQuery PullableQuery; protected EntityQuery XformQuery; @@ -92,8 +96,12 @@ public abstract partial class SharedMoverController : VirtualController FootstepModifierQuery = GetEntityQuery(); MapGridQuery = GetEntityQuery(); MapQuery = GetEntityQuery(); + FTLQuery = GetEntityQuery(); + PilotQuery = GetEntityQuery(); + PreventPilotQuery = GetEntityQuery(); SubscribeLocalEvent(OnTileFriction); + SubscribeLocalEvent(OnMoverStartup); InitializeInput(); InitializeRelay(); @@ -103,6 +111,11 @@ public abstract partial class SharedMoverController : VirtualController Subs.CVar(_configManager, CCVars.OffgridFriction, value => _offGridDamping = value, true); } + protected virtual void OnMoverStartup(Entity ent, ref ComponentStartup args) + { + _blocker.UpdateCanMove(ent, ent.Comp); + } + public override void Shutdown() { base.Shutdown(); @@ -469,9 +482,9 @@ public abstract partial class SharedMoverController : VirtualController // Only allow pushing off of anchored things that have collision. if (otherCollider.BodyType != BodyType.Static || !otherCollider.CanCollide || - ((collider.CollisionMask & otherCollider.CollisionLayer) == 0 && - (otherCollider.CollisionMask & collider.CollisionLayer) == 0) || - (TryComp(otherEntity, out PullableComponent? pullable) && pullable.BeingPulled)) + (collider.CollisionMask & otherCollider.CollisionLayer) == 0 && + (otherCollider.CollisionMask & collider.CollisionLayer) == 0 || + PullableQuery.TryComp(otherEntity, out var pullable) && pullable.BeingPulled) { continue; } @@ -621,7 +634,7 @@ public abstract partial class SharedMoverController : VirtualController private void OnTileFriction(Entity ent, ref TileFrictionEvent args) { - if (!TryComp(ent, out var physicsComponent) || !XformQuery.TryComp(ent, out var xform)) + if (!PhysicsQuery.TryComp(ent, out var physicsComponent)) return; if (physicsComponent.BodyStatus != BodyStatus.OnGround || _gravity.IsWeightless(ent.Owner)) -- 2.52.0