using System; using System.Collections.Generic; using Content.Server.Inventory.Components; using Content.Server.Items; using Content.Server.Movement.Components; using Content.Server.Shuttles.Components; using Content.Server.Shuttles.EntitySystems; using Content.Shared.CCVar; using Content.Shared.Inventory; using Content.Shared.Maps; using Content.Shared.Movement; using Content.Shared.Movement.Components; using Content.Shared.Shuttles; using Content.Shared.Shuttles.Components; using Content.Shared.Tag; using Robust.Shared.Audio; using Robust.Shared.Configuration; using Robust.Shared.GameObjects; using Robust.Shared.IoC; using Robust.Shared.Log; using Robust.Shared.Map; using Robust.Shared.Maths; using Robust.Shared.Player; using Robust.Shared.Utility; namespace Content.Server.Physics.Controllers { public class MoverController : SharedMoverController { [Dependency] private readonly ITileDefinitionManager _tileDefinitionManager = default!; [Dependency] private readonly IMapManager _mapManager = default!; private const float StepSoundMoveDistanceRunning = 2; private const float StepSoundMoveDistanceWalking = 1.5f; private float _shuttleDockSpeedCap; private HashSet _excludedMobs = new(); private Dictionary> _shuttlePilots = new(); public override void Initialize() { base.Initialize(); var configManager = IoCManager.Resolve(); configManager.OnValueChanged(CCVars.ShuttleDockSpeedCap, value => _shuttleDockSpeedCap = value, true); } public override void UpdateBeforeSolve(bool prediction, float frameTime) { base.UpdateBeforeSolve(prediction, frameTime); _excludedMobs.Clear(); foreach (var (mobMover, mover, physics) in EntityManager.EntityQuery()) { _excludedMobs.Add(mover.Owner.Uid); HandleMobMovement(mover, physics, mobMover); } HandleShuttleMovement(frameTime); foreach (var (mover, physics) in EntityManager.EntityQuery(true)) { if (_excludedMobs.Contains(mover.Owner.Uid)) continue; HandleKinematicMovement(mover, physics); } } private void HandleShuttleMovement(float frameTime) { var newPilots = new Dictionary>(); // We just mark off their movement and the shuttle itself does its own movement foreach (var (pilot, mover, xform) in EntityManager.EntityQuery()) { if (pilot.Console == null) continue; _excludedMobs.Add(mover.Owner.Uid); var gridId = xform.GridID; if (!_mapManager.TryGetGrid(gridId, out var grid) || !EntityManager.TryGetComponent(grid.GridEntityId, out ShuttleComponent? shuttleComponent)) continue; if (!newPilots.TryGetValue(shuttleComponent, out var pilots)) { pilots = new List<(PilotComponent, IMoverComponent)>(); newPilots[shuttleComponent] = pilots; } pilots.Add((pilot, mover)); } var shuttleSystem = EntitySystem.Get(); var thrusterSystem = EntitySystem.Get(); // Reset inputs for non-piloted shuttles. foreach (var (shuttle, _) in _shuttlePilots) { if (newPilots.ContainsKey(shuttle)) continue; thrusterSystem.DisableLinearThrusters(shuttle); } _shuttlePilots = newPilots; // Collate all of the linear / angular velocites for a shuttle // then do the movement input once for it. foreach (var (shuttle, pilots) in _shuttlePilots) { if (shuttle.Paused || !EntityManager.TryGetComponent(shuttle.OwnerUid, out PhysicsComponent? body)) continue; // Collate movement linear and angular inputs together var linearInput = Vector2.Zero; var angularInput = 0f; switch (shuttle.Mode) { case ShuttleMode.Cruise: foreach (var (pilot, mover) in pilots) { var console = pilot.Console; if (console == null) { DebugTools.Assert(false); continue; } var sprint = mover.VelocityDir.sprinting; if (sprint.Equals(Vector2.Zero)) continue; var offsetRotation = EntityManager.GetComponent(console.OwnerUid).LocalRotation; linearInput += offsetRotation.RotateVec(new Vector2(0f, sprint.Y)); angularInput += sprint.X; } break; case ShuttleMode.Docking: // No angular input possible foreach (var (pilot, mover) in pilots) { var console = pilot.Console; if (console == null) { DebugTools.Assert(false); continue; } var sprint = mover.VelocityDir.sprinting; if (sprint.Equals(Vector2.Zero)) continue; var offsetRotation = EntityManager.GetComponent(console.OwnerUid).LocalRotation; sprint = offsetRotation.RotateVec(sprint); linearInput += sprint; } break; default: throw new ArgumentOutOfRangeException(); } var count = pilots.Count; linearInput /= count; angularInput /= count; // Handle shuttle movement if (linearInput.Length.Equals(0f)) { thrusterSystem.DisableLinearThrusters(shuttle); body.LinearDamping = shuttleSystem.ShuttleIdleLinearDamping; } else { body.LinearDamping = shuttleSystem.ShuttleMovingLinearDamping; var angle = linearInput.ToWorldAngle(); var linearDir = angle.GetDir(); var dockFlag = linearDir.AsFlag(); var shuttleNorth = EntityManager.GetComponent(body.OwnerUid).WorldRotation.ToWorldVec(); // Won't just do cardinal directions. foreach (DirectionFlag dir in Enum.GetValues(typeof(DirectionFlag))) { // Brain no worky but I just want cardinals switch (dir) { case DirectionFlag.South: case DirectionFlag.East: case DirectionFlag.North: case DirectionFlag.West: break; default: continue; } if ((dir & dockFlag) == 0x0) { thrusterSystem.DisableLinearThrustDirection(shuttle, dir); continue; } float length; switch (dir) { case DirectionFlag.North: length = linearInput.Y; break; case DirectionFlag.South: length = -linearInput.Y; break; case DirectionFlag.East: length = linearInput.X; break; case DirectionFlag.West: length = -linearInput.X; break; default: throw new ArgumentOutOfRangeException(); } thrusterSystem.EnableLinearThrustDirection(shuttle, dir); var index = (int) Math.Log2((int) dir); var speed = shuttle.LinearThrusterImpulse[index] * length; if (body.LinearVelocity.LengthSquared < 0.5f) { speed *= 5f; } body.ApplyLinearImpulse( angle.RotateVec(shuttleNorth) * speed * frameTime); } } if (MathHelper.CloseTo(angularInput, 0f)) { thrusterSystem.SetAngularThrust(shuttle, false); body.AngularDamping = shuttleSystem.ShuttleIdleAngularDamping; } else { body.AngularDamping = shuttleSystem.ShuttleMovingAngularDamping; var angularSpeed = shuttle.AngularThrust; if (body.AngularVelocity < 0.5f) { angularSpeed *= 5f; } // Scale rotation by mass just to make rotating larger things a bit more bearable. body.ApplyAngularImpulse( -angularInput * angularSpeed * frameTime * body.Mass / 100f); thrusterSystem.SetAngularThrust(shuttle, true); } } } protected override void HandleFootsteps(IMoverComponent mover, IMobMoverComponent mobMover) { if (!mover.Owner.HasTag("FootstepSound")) return; var transform = mover.Owner.Transform; var coordinates = transform.Coordinates; var gridId = coordinates.GetGridId(EntityManager); var distanceNeeded = mover.Sprinting ? StepSoundMoveDistanceRunning : StepSoundMoveDistanceWalking; // Handle footsteps. if (_mapManager.GridExists(gridId)) { // 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; } DebugTools.Assert(gridId != GridId.Invalid); mobMover.LastPosition = coordinates; if (mobMover.StepSoundDistance < distanceNeeded) return; mobMover.StepSoundDistance -= distanceNeeded; if (mover.Owner.TryGetComponent(out var inventory) && inventory.TryGetSlotItem(EquipmentSlotDefines.Slots.SHOES, out var item) && item.Owner.TryGetComponent(out var modifier)) { modifier.PlayFootstep(); } else { PlayFootstepSound(mover.Owner, gridId, coordinates, mover.Sprinting); } } private void PlayFootstepSound(IEntity mover, GridId gridId, EntityCoordinates coordinates, bool sprinting) { var grid = _mapManager.GetGrid(gridId); var tile = grid.GetTileRef(coordinates); if (tile.IsSpace(_tileDefinitionManager)) return; // If the coordinates have a FootstepModifier component // i.e. component that emit sound on footsteps emit that sound string? soundToPlay = null; foreach (var maybeFootstep in grid.GetAnchoredEntities(tile.GridIndices)) { if (EntityManager.TryGetComponent(maybeFootstep, out FootstepModifierComponent? footstep)) { soundToPlay = footstep.SoundCollection.GetSound(); break; } } // if there is no FootstepModifierComponent, determine sound based on tiles if (soundToPlay == null) { // Walking on a tile. var def = (ContentTileDefinition) _tileDefinitionManager[tile.Tile.TypeId]; soundToPlay = def.FootstepSounds?.GetSound(); if (string.IsNullOrEmpty(soundToPlay)) return; } if (string.IsNullOrWhiteSpace(soundToPlay)) { Logger.ErrorS("sound", $"Unable to find sound in {nameof(PlayFootstepSound)}"); return; } SoundSystem.Play( Filter.Pvs(coordinates), soundToPlay, mover.Transform.Coordinates, sprinting ? AudioParams.Default.WithVolume(0.75f) : null); } } }