using Content.Shared.Inventory; using Content.Shared.Movement.Components; using Content.Shared.Movement.Systems; using Content.Shared.Pulling.Components; using Robust.Client.GameObjects; using Robust.Client.Player; using Robust.Shared.Containers; using Robust.Shared.Physics; using Robust.Shared.Physics.Components; using Robust.Shared.Timing; using Robust.Shared.Utility; namespace Content.Client.Physics.Controllers { public sealed class MoverController : SharedMoverController { [Dependency] private readonly IGameTiming _timing = default!; [Dependency] private readonly IPlayerManager _playerManager = default!; public override void Initialize() { base.Initialize(); SubscribeLocalEvent(OnRelayPlayerAttached); SubscribeLocalEvent(OnRelayPlayerDetached); SubscribeLocalEvent(OnPlayerAttached); SubscribeLocalEvent(OnPlayerDetached); } private void OnRelayPlayerAttached(EntityUid uid, RelayInputMoverComponent component, PlayerAttachedEvent args) { if (TryComp(component.RelayEntity, out var inputMover)) SetMoveInput(inputMover, MoveButtons.None); } private void OnRelayPlayerDetached(EntityUid uid, RelayInputMoverComponent component, PlayerDetachedEvent args) { if (TryComp(component.RelayEntity, out var inputMover)) SetMoveInput(inputMover, MoveButtons.None); } private void OnPlayerAttached(EntityUid uid, InputMoverComponent component, PlayerAttachedEvent args) { SetMoveInput(component, MoveButtons.None); } private void OnPlayerDetached(EntityUid uid, InputMoverComponent component, PlayerDetachedEvent args) { SetMoveInput(component, MoveButtons.None); } public override void UpdateBeforeSolve(bool prediction, float frameTime) { base.UpdateBeforeSolve(prediction, frameTime); if (_playerManager.LocalPlayer?.ControlledEntity is not {Valid: true} player) return; if (TryComp(player, out var relayMover)) { if (relayMover.RelayEntity != null) { if (TryComp(player, out var mover) && TryComp(relayMover.RelayEntity, out var relayed)) { relayed.CanMove = mover.CanMove; relayed.RelativeEntity = mover.RelativeEntity; relayed.RelativeRotation = mover.RelativeRotation; relayed.TargetRelativeRotation = mover.RelativeRotation; } HandleClientsideMovement(relayMover.RelayEntity.Value, frameTime); } } HandleClientsideMovement(player, frameTime); } private void HandleClientsideMovement(EntityUid player, float frameTime) { var xformQuery = GetEntityQuery(); if (!TryComp(player, out InputMoverComponent? mover) || !xformQuery.TryGetComponent(player, out var xform)) { return; } PhysicsComponent? body = null; TransformComponent? xformMover = xform; if (mover.ToParent && HasComp(xform.ParentUid)) { if (!TryComp(xform.ParentUid, out body) || !TryComp(xform.ParentUid, out xformMover)) { return; } } else if (!TryComp(player, out body)) { return; } // Essentially we only want to set our mob to predicted so every other entity we just interpolate // (i.e. only see what the server has sent us). // The exception to this is joints. body.Predict = true; // We set joints to predicted given these can affect how our mob moves. // I would only recommend disabling this if you make pulling not use joints anymore (someday maybe?) if (TryComp(player, out JointComponent? jointComponent)) { foreach (var joint in jointComponent.GetJoints.Values) { if (TryComp(joint.BodyAUid, out PhysicsComponent? physics)) physics.Predict = true; if (TryComp(joint.BodyBUid, out physics)) physics.Predict = true; } } // If we're being pulled then we won't predict anything and will receive server lerps so it looks way smoother. if (TryComp(player, out SharedPullableComponent? pullableComp)) { if (pullableComp.Puller is {Valid: true} puller && TryComp(puller, out var pullerBody)) { pullerBody.Predict = false; body.Predict = false; if (TryComp(player, out var playerPuller) && playerPuller.Pulling != null && TryComp(playerPuller.Pulling, out var pulledBody)) { pulledBody.Predict = false; } } } var mobQuery = GetEntityQuery(); var inventoryQuery = GetEntityQuery(); var containerQuery = GetEntityQuery(); var footQuery = GetEntityQuery(); DebugTools.Assert(!UsedMobMovement.ContainsKey(mover.Owner)); // Server-side should just be handled on its own so we'll just do this shizznit HandleMobMovement(mover, body, xformMover, frameTime, xformQuery, mobQuery, inventoryQuery, containerQuery, footQuery, out var dirtyMover, out var linearVelocity, out var sound, out var audio); MetaDataComponent? metadata = null; if (dirtyMover) { Dirty(mover, metadata); } if (linearVelocity != null) { PhysicsSystem.SetLinearVelocity(body, linearVelocity.Value, false); PhysicsSystem.SetAngularVelocity(body, 0f, false); Dirty(body, metadata); } Audio.PlayPredicted(sound, mover.Owner, mover.Owner, audio); } protected override bool CanSound() { return _timing.IsFirstTimePredicted && _timing.InSimulation; } } }