using System.Diagnostics.CodeAnalysis; using System.Numerics; using Content.Shared.Bed.Sleep; using Content.Shared.CCVar; using Content.Shared.Friction; using Content.Shared.Gravity; using Content.Shared.Inventory; using Content.Shared.Maps; using Content.Shared.Mobs.Systems; using Content.Shared.Movement.Components; using Content.Shared.Movement.Events; using Content.Shared.Tag; using Robust.Shared.Audio; using Robust.Shared.Audio.Systems; using Robust.Shared.Configuration; using Robust.Shared.Containers; using Robust.Shared.Map; using Robust.Shared.Map.Components; using Robust.Shared.Physics; using Robust.Shared.Physics.Components; using Robust.Shared.Physics.Controllers; using Robust.Shared.Physics.Systems; using Robust.Shared.Timing; using Robust.Shared.Utility; using PullableComponent = Content.Shared.Movement.Pulling.Components.PullableComponent; namespace Content.Shared.Movement.Systems; /// /// Handles player and NPC mob movement. /// NPCs are handled server-side only. /// public abstract partial class SharedMoverController : VirtualController { [Dependency] private readonly IConfigurationManager _configManager = default!; [Dependency] protected readonly IGameTiming Timing = default!; [Dependency] private readonly IMapManager _mapManager = default!; [Dependency] private readonly ITileDefinitionManager _tileDefinitionManager = default!; [Dependency] private readonly EntityLookupSystem _lookup = default!; [Dependency] private readonly InventorySystem _inventory = default!; [Dependency] private readonly MobStateSystem _mobState = default!; [Dependency] private readonly SharedAudioSystem _audio = default!; [Dependency] private readonly SharedContainerSystem _container = default!; [Dependency] private readonly SharedMapSystem _mapSystem = default!; [Dependency] private readonly SharedGravitySystem _gravity = default!; [Dependency] protected readonly SharedPhysicsSystem Physics = default!; [Dependency] private readonly SharedTransformSystem _transform = default!; [Dependency] private readonly TagSystem _tags = default!; protected EntityQuery MoverQuery; protected EntityQuery MobMoverQuery; protected EntityQuery RelayTargetQuery; protected EntityQuery ModifierQuery; protected EntityQuery PhysicsQuery; protected EntityQuery RelayQuery; protected EntityQuery PullableQuery; protected EntityQuery XformQuery; protected EntityQuery CanMoveInAirQuery; protected EntityQuery NoRotateQuery; protected EntityQuery FootstepModifierQuery; protected EntityQuery MapGridQuery; /// /// /// private float _stopSpeed; private bool _relativeMovement; /// /// Cache the mob movement calculation to re-use elsewhere. /// public Dictionary UsedMobMovement = new(); public override void Initialize() { base.Initialize(); MoverQuery = GetEntityQuery(); MobMoverQuery = GetEntityQuery(); ModifierQuery = GetEntityQuery(); RelayTargetQuery = GetEntityQuery(); PhysicsQuery = GetEntityQuery(); RelayQuery = GetEntityQuery(); PullableQuery = GetEntityQuery(); XformQuery = GetEntityQuery(); NoRotateQuery = GetEntityQuery(); CanMoveInAirQuery = GetEntityQuery(); FootstepModifierQuery = GetEntityQuery(); MapGridQuery = GetEntityQuery(); InitializeInput(); InitializeRelay(); Subs.CVar(_configManager, CCVars.RelativeMovement, value => _relativeMovement = value, true); Subs.CVar(_configManager, CCVars.StopSpeed, value => _stopSpeed = value, true); UpdatesBefore.Add(typeof(TileFrictionController)); } public override void Shutdown() { base.Shutdown(); ShutdownInput(); } public override void UpdateAfterSolve(bool prediction, float frameTime) { base.UpdateAfterSolve(prediction, frameTime); UsedMobMovement.Clear(); } /// /// Movement while considering actionblockers, weightlessness, etc. /// protected void HandleMobMovement( EntityUid uid, InputMoverComponent mover, EntityUid physicsUid, PhysicsComponent physicsComponent, TransformComponent xform, float frameTime) { var canMove = mover.CanMove; if (RelayTargetQuery.TryGetComponent(uid, out var relayTarget)) { if (_mobState.IsIncapacitated(relayTarget.Source) || TryComp(relayTarget.Source, out _) || !MoverQuery.TryGetComponent(relayTarget.Source, out var relayedMover)) { canMove = false; } else { mover.RelativeEntity = relayedMover.RelativeEntity; mover.RelativeRotation = relayedMover.RelativeRotation; mover.TargetRelativeRotation = relayedMover.TargetRelativeRotation; } } // Update relative movement if (mover.LerpTarget < Timing.CurTime) { if (TryUpdateRelative(mover, xform)) { Dirty(uid, mover); } } LerpRotation(uid, mover, frameTime); if (!canMove || physicsComponent.BodyStatus != BodyStatus.OnGround && !CanMoveInAirQuery.HasComponent(uid) || PullableQuery.TryGetComponent(uid, out var pullable) && pullable.BeingPulled) { UsedMobMovement[uid] = false; return; } UsedMobMovement[uid] = true; // Specifically don't use mover.Owner because that may be different to the actual physics body being moved. var weightless = _gravity.IsWeightless(physicsUid, physicsComponent, xform); var (walkDir, sprintDir) = GetVelocityInput(mover); var touching = false; // Handle wall-pushes. if (weightless) { if (xform.GridUid != null) touching = true; if (!touching) { var ev = new CanWeightlessMoveEvent(uid); RaiseLocalEvent(uid, ref ev, true); // No gravity: is our entity touching anything? touching = ev.CanMove; if (!touching && TryComp(uid, out var mobMover)) touching |= IsAroundCollider(PhysicsSystem, xform, mobMover, physicsUid, physicsComponent); } } // Get current tile def for things like speed/friction mods ContentTileDefinition? tileDef = null; // Don't bother getting the tiledef here if we're weightless or in-air // since no tile-based modifiers should be applying in that situation if (MapGridQuery.TryComp(xform.GridUid, out var gridComp) && _mapSystem.TryGetTileRef(xform.GridUid.Value, gridComp, xform.Coordinates, out var tile) && !(weightless || physicsComponent.BodyStatus == BodyStatus.InAir)) { tileDef = (ContentTileDefinition) _tileDefinitionManager[tile.Tile.TypeId]; } // Regular movement. // Target velocity. // This is relative to the map / grid we're on. var moveSpeedComponent = ModifierQuery.CompOrNull(uid); var walkSpeed = moveSpeedComponent?.CurrentWalkSpeed ?? MovementSpeedModifierComponent.DefaultBaseWalkSpeed; var sprintSpeed = moveSpeedComponent?.CurrentSprintSpeed ?? MovementSpeedModifierComponent.DefaultBaseSprintSpeed; var total = walkDir * walkSpeed + sprintDir * sprintSpeed; var parentRotation = GetParentGridAngle(mover); var wishDir = _relativeMovement ? parentRotation.RotateVec(total) : total; DebugTools.Assert(MathHelper.CloseToPercent(total.Length(), wishDir.Length())); float friction; float weightlessModifier; float accel; var velocity = physicsComponent.LinearVelocity; // Whether we use weightless friction or not. if (weightless) { if (gridComp == null && !MapGridQuery.HasComp(xform.GridUid)) friction = moveSpeedComponent?.OffGridFriction ?? MovementSpeedModifierComponent.DefaultOffGridFriction; else if (wishDir != Vector2.Zero && touching) friction = moveSpeedComponent?.WeightlessFriction ?? MovementSpeedModifierComponent.DefaultWeightlessFriction; else friction = moveSpeedComponent?.WeightlessFrictionNoInput ?? MovementSpeedModifierComponent.DefaultWeightlessFrictionNoInput; weightlessModifier = moveSpeedComponent?.WeightlessModifier ?? MovementSpeedModifierComponent.DefaultWeightlessModifier; accel = moveSpeedComponent?.WeightlessAcceleration ?? MovementSpeedModifierComponent.DefaultWeightlessAcceleration; } else { if (wishDir != Vector2.Zero || moveSpeedComponent?.FrictionNoInput == null) { friction = tileDef?.MobFriction ?? moveSpeedComponent?.Friction ?? MovementSpeedModifierComponent.DefaultFriction; } else { friction = tileDef?.MobFrictionNoInput ?? moveSpeedComponent.FrictionNoInput ?? MovementSpeedModifierComponent.DefaultFrictionNoInput; } weightlessModifier = 1f; accel = tileDef?.MobAcceleration ?? moveSpeedComponent?.Acceleration ?? MovementSpeedModifierComponent.DefaultAcceleration; } var minimumFrictionSpeed = moveSpeedComponent?.MinimumFrictionSpeed ?? MovementSpeedModifierComponent.DefaultMinimumFrictionSpeed; Friction(minimumFrictionSpeed, frameTime, friction, ref velocity); wishDir *= weightlessModifier; if (!weightless || touching) Accelerate(ref velocity, in wishDir, accel, frameTime); SetWishDir((uid, mover), wishDir); PhysicsSystem.SetLinearVelocity(physicsUid, velocity, body: physicsComponent); // Ensures that players do not spiiiiiiin PhysicsSystem.SetAngularVelocity(physicsUid, 0, body: physicsComponent); // Handle footsteps at the end if (total != Vector2.Zero) { if (!NoRotateQuery.HasComponent(uid)) { // TODO apparently this results in a duplicate move event because "This should have its event run during // island solver"??. So maybe SetRotation needs an argument to avoid raising an event? var worldRot = _transform.GetWorldRotation(xform); _transform.SetLocalRotation(xform, xform.LocalRotation + wishDir.ToWorldAngle() - worldRot); } if (!weightless && MobMoverQuery.TryGetComponent(uid, out var mobMover) && TryGetSound(weightless, uid, mover, mobMover, xform, out var sound, tileDef: tileDef)) { var soundModifier = mover.Sprinting ? 3.5f : 1.5f; var audioParams = sound.Params .WithVolume(sound.Params.Volume + soundModifier) .WithVariation(sound.Params.Variation ?? mobMover.FootstepVariation); // If we're a relay target then predict the sound for all relays. if (relayTarget != null) { _audio.PlayPredicted(sound, uid, relayTarget.Source, audioParams); } else { _audio.PlayPredicted(sound, uid, uid, audioParams); } } } } public Vector2 GetWishDir(Entity mover) { if (!MoverQuery.Resolve(mover.Owner, ref mover.Comp, false)) return Vector2.Zero; return mover.Comp.WishDir; } public void SetWishDir(Entity mover, Vector2 wishDir) { if (mover.Comp.WishDir.Equals(wishDir)) return; mover.Comp.WishDir = wishDir; Dirty(mover); } public void LerpRotation(EntityUid uid, InputMoverComponent mover, float frameTime) { var angleDiff = Angle.ShortestDistance(mover.RelativeRotation, mover.TargetRelativeRotation); // if we've just traversed then lerp to our target rotation. if (!angleDiff.EqualsApprox(Angle.Zero, 0.001)) { var adjustment = angleDiff * 5f * frameTime; var minAdjustment = 0.01 * frameTime; if (angleDiff < 0) { adjustment = Math.Min(adjustment, -minAdjustment); adjustment = Math.Clamp(adjustment, angleDiff, -angleDiff); } else { adjustment = Math.Max(adjustment, minAdjustment); adjustment = Math.Clamp(adjustment, -angleDiff, angleDiff); } mover.RelativeRotation += adjustment; mover.RelativeRotation.FlipPositive(); Dirty(uid, mover); } else if (!angleDiff.Equals(Angle.Zero)) { mover.TargetRelativeRotation.FlipPositive(); mover.RelativeRotation = mover.TargetRelativeRotation; Dirty(uid, mover); } } public void Friction(float minimumFrictionSpeed, float frameTime, float friction, ref Vector2 velocity) { var speed = velocity.Length(); if (speed < minimumFrictionSpeed) return; var drop = 0f; var control = MathF.Max(_stopSpeed, speed); drop += control * friction * frameTime; var newSpeed = MathF.Max(0f, speed - drop); if (newSpeed.Equals(speed)) return; newSpeed /= speed; velocity *= newSpeed; } /// /// Adjusts the current velocity to the target velocity based on the specified acceleration. /// public static void Accelerate(ref Vector2 currentVelocity, in Vector2 velocity, float accel, float frameTime) { var wishDir = velocity != Vector2.Zero ? velocity.Normalized() : Vector2.Zero; var wishSpeed = velocity.Length(); var currentSpeed = Vector2.Dot(currentVelocity, wishDir); var addSpeed = wishSpeed - currentSpeed; if (addSpeed <= 0f) return; var accelSpeed = accel * frameTime * wishSpeed; accelSpeed = MathF.Min(accelSpeed, addSpeed); currentVelocity += wishDir * accelSpeed; } public bool UseMobMovement(EntityUid uid) { return UsedMobMovement.TryGetValue(uid, out var used) && used; } /// /// Used for weightlessness to determine if we are near a wall. /// private bool IsAroundCollider(SharedPhysicsSystem broadPhaseSystem, TransformComponent transform, MobMoverComponent mover, EntityUid physicsUid, PhysicsComponent collider) { var enlargedAABB = _lookup.GetWorldAABB(physicsUid, transform).Enlarged(mover.GrabRangeVV); foreach (var otherCollider in broadPhaseSystem.GetCollidingEntities(transform.MapID, enlargedAABB)) { if (otherCollider == collider) continue; // Don't try to push off of yourself! // 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(otherCollider.Owner, out PullableComponent? pullable) && pullable.BeingPulled)) { continue; } return true; } return false; } protected abstract bool CanSound(); private bool TryGetSound( bool weightless, EntityUid uid, InputMoverComponent mover, MobMoverComponent mobMover, TransformComponent xform, [NotNullWhen(true)] out SoundSpecifier? sound, ContentTileDefinition? tileDef = null) { sound = null; if (!CanSound() || !_tags.HasTag(uid, "FootstepSound")) return false; var coordinates = xform.Coordinates; var distanceNeeded = mover.Sprinting ? mobMover.StepSoundMoveDistanceRunning : mobMover.StepSoundMoveDistanceWalking; // Handle footsteps. if (!weightless) { // Can happen when teleporting between grids. if (!coordinates.TryDistance(EntityManager, mobMover.LastPosition, out var distance) || distance > distanceNeeded) { mobMover.StepSoundDistance = distanceNeeded; } else { mobMover.StepSoundDistance += distance; } } else { // In space no one can hear you squeak return false; } mobMover.LastPosition = coordinates; if (mobMover.StepSoundDistance < distanceNeeded) return false; mobMover.StepSoundDistance -= distanceNeeded; if (FootstepModifierQuery.TryComp(uid, out var moverModifier)) { sound = moverModifier.FootstepSoundCollection; return sound != null; } if (_inventory.TryGetSlotEntity(uid, "shoes", out var shoes) && FootstepModifierQuery.TryComp(shoes, out var modifier)) { sound = modifier.FootstepSoundCollection; return sound != null; } return TryGetFootstepSound(uid, xform, shoes != null, out sound, tileDef: tileDef); } private bool TryGetFootstepSound( EntityUid uid, TransformComponent xform, bool haveShoes, [NotNullWhen(true)] out SoundSpecifier? sound, ContentTileDefinition? tileDef = null) { sound = null; // Fallback to the map? if (!MapGridQuery.TryComp(xform.GridUid, out var grid)) { if (FootstepModifierQuery.TryComp(xform.MapUid, out var modifier)) { sound = modifier.FootstepSoundCollection; } return sound != null; } var position = grid.LocalToTile(xform.Coordinates); var soundEv = new GetFootstepSoundEvent(uid); // If the coordinates have a FootstepModifier component // i.e. component that emit sound on footsteps emit that sound var anchored = grid.GetAnchoredEntitiesEnumerator(position); while (anchored.MoveNext(out var maybeFootstep)) { RaiseLocalEvent(maybeFootstep.Value, ref soundEv); if (soundEv.Sound != null) { sound = soundEv.Sound; return true; } if (FootstepModifierQuery.TryComp(maybeFootstep, out var footstep)) { sound = footstep.FootstepSoundCollection; return sound != null; } } // Walking on a tile. // Tile def might have been passed in already from previous methods, so use that // if we have it if (tileDef == null && grid.TryGetTileRef(position, out var tileRef)) { tileDef = (ContentTileDefinition) _tileDefinitionManager[tileRef.Tile.TypeId]; } if (tileDef == null) return false; sound = haveShoes ? tileDef.FootstepSounds : tileDef.BarestepSounds; return sound != null; } }