--- /dev/null
+using Content.Shared.Movement.Components;
+using Content.Shared.Movement.Events;
+using Content.Shared.Movement.Systems;
+using Robust.Client.GameObjects;
+using Robust.Shared.Timing;
+
+namespace Content.Client.Movement.Systems;
+
+/// <summary>
+/// Handles setting sprite states based on whether an entity has movement input.
+/// </summary>
+public sealed class SpriteMovementSystem : EntitySystem
+{
+ [Dependency] private readonly IGameTiming _timing = default!;
+
+ private EntityQuery<SpriteComponent> _spriteQuery;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+ SubscribeLocalEvent<SpriteMovementComponent, MoveInputEvent>(OnSpriteMoveInput);
+ _spriteQuery = GetEntityQuery<SpriteComponent>();
+ }
+
+ private void OnSpriteMoveInput(EntityUid uid, SpriteMovementComponent component, ref MoveInputEvent args)
+ {
+ if (!_timing.IsFirstTimePredicted)
+ return;
+
+ var oldMoving = SharedMoverController.GetNormalizedMovement(args.OldMovement) != MoveButtons.None;
+ var moving = SharedMoverController.GetNormalizedMovement(args.Component.HeldMoveButtons) != MoveButtons.None;
+
+ if (oldMoving == moving || !_spriteQuery.TryGetComponent(uid, out var sprite))
+ return;
+
+ if (moving)
+ {
+ foreach (var (layer, state) in component.MovementLayers)
+ {
+ sprite.LayerSetData(layer, state);
+ }
+ }
+ else
+ {
+ foreach (var (layer, state) in component.NoMovementLayers)
+ {
+ sprite.LayerSetData(layer, state);
+ }
+ }
+ }
+}
using System.Numerics;
using Content.Shared.Movement.Systems;
using Robust.Shared.GameStates;
+using Robust.Shared.Serialization;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
using Robust.Shared.Timing;
namespace Content.Shared.Movement.Components
{
- [RegisterComponent, NetworkedComponent, AutoGenerateComponentState(true)]
+ [RegisterComponent, NetworkedComponent]
public sealed partial class InputMoverComponent : Component
{
// This class has to be able to handle server TPS being lower than client FPS.
public Vector2 CurTickWalkMovement;
public Vector2 CurTickSprintMovement;
- [AutoNetworkedField]
public MoveButtons HeldMoveButtons = MoveButtons.None;
/// <summary>
/// Entity our movement is relative to.
/// </summary>
- [AutoNetworkedField]
public EntityUid? RelativeEntity;
/// <summary>
/// Although our movement might be relative to a particular entity we may have an additional relative rotation
/// e.g. if we've snapped to a different cardinal direction
/// </summary>
- [ViewVariables, AutoNetworkedField]
+ [ViewVariables]
public Angle TargetRelativeRotation = Angle.Zero;
/// <summary>
/// The current relative rotation. This will lerp towards the <see cref="TargetRelativeRotation"/>.
/// </summary>
- [ViewVariables, AutoNetworkedField]
+ [ViewVariables]
public Angle RelativeRotation;
/// <summary>
/// If we traverse on / off a grid then set a timer to update our relative inputs.
/// </summary>
- [DataField(customTypeSerializer: typeof(TimeOffsetSerializer)), AutoNetworkedField]
+ [DataField(customTypeSerializer: typeof(TimeOffsetSerializer))]
[ViewVariables(VVAccess.ReadWrite)]
public TimeSpan LerpTarget;
public bool Sprinting => (HeldMoveButtons & MoveButtons.Walk) == 0x0;
- [ViewVariables(VVAccess.ReadWrite), AutoNetworkedField]
- public bool CanMove { get; set; } = true;
+ [ViewVariables(VVAccess.ReadWrite)]
+ public bool CanMove = true;
+ }
+
+ [Serializable, NetSerializable]
+ public sealed class InputMoverComponentState : ComponentState
+ {
+ public MoveButtons HeldMoveButtons;
+ public NetEntity? RelativeEntity;
+ public Angle TargetRelativeRotation;
+ public Angle RelativeRotation;
+ public TimeSpan LerpTarget;
+ public bool CanMove;
}
}
using Content.Shared.Input;
using Content.Shared.Movement.Components;
using Content.Shared.Movement.Events;
+using Robust.Shared.GameStates;
using Robust.Shared.Input;
using Robust.Shared.Input.Binding;
using Robust.Shared.Player;
.Register<SharedMoverController>();
SubscribeLocalEvent<InputMoverComponent, ComponentInit>(OnInputInit);
- SubscribeLocalEvent<InputMoverComponent, AfterAutoHandleStateEvent>(OnInputHandleState);
+ SubscribeLocalEvent<InputMoverComponent, ComponentGetState>(OnMoverGetState);
+ SubscribeLocalEvent<InputMoverComponent, ComponentHandleState>(OnMoverHandleState);
SubscribeLocalEvent<InputMoverComponent, EntParentChangedMessage>(OnInputParentChange);
SubscribeLocalEvent<AutoOrientComponent, EntParentChangedMessage>(OnAutoParentChange);
CameraRotationLocked = obj;
}
+ /// <summary>
+ /// Gets the buttons held with opposites cancelled out.
+ /// </summary>
+ public static MoveButtons GetNormalizedMovement(MoveButtons buttons)
+ {
+ var oldMovement = buttons;
+
+ if ((oldMovement & (MoveButtons.Left | MoveButtons.Right)) == (MoveButtons.Left | MoveButtons.Right))
+ {
+ oldMovement &= ~MoveButtons.Left;
+ oldMovement &= ~MoveButtons.Right;
+ }
+
+ if ((oldMovement & (MoveButtons.Up | MoveButtons.Down)) == (MoveButtons.Up | MoveButtons.Down))
+ {
+ oldMovement &= ~MoveButtons.Up;
+ oldMovement &= ~MoveButtons.Down;
+ }
+
+ return oldMovement;
+ }
+
protected void SetMoveInput(InputMoverComponent component, MoveButtons buttons)
{
if (component.HeldMoveButtons == buttons)
return;
+ // Relay the fact we had any movement event.
+ // TODO: Ideally we'd do these in a tick instead of out of sim.
+ var moveEvent = new MoveInputEvent(component.Owner, component, component.HeldMoveButtons);
component.HeldMoveButtons = buttons;
- Dirty(component);
+ RaiseLocalEvent(component.Owner, ref moveEvent);
+ Dirty(component.Owner, component);
}
- private void OnInputHandleState(EntityUid uid, InputMoverComponent component, ref AfterAutoHandleStateEvent args)
+ private void OnMoverHandleState(EntityUid uid, InputMoverComponent component, ComponentHandleState args)
{
+ if (args.Current is not InputMoverComponentState state)
+ return;
+
+ // Handle state
+ component.LerpTarget = state.LerpTarget;
+ component.RelativeRotation = state.RelativeRotation;
+ component.TargetRelativeRotation = state.TargetRelativeRotation;
+ component.CanMove = state.CanMove;
+ component.RelativeEntity = EnsureEntity<InputMoverComponent>(state.RelativeEntity, uid);
+
+ // Reset
component.LastInputTick = GameTick.Zero;
component.LastInputSubTick = 0;
+
+ if (component.HeldMoveButtons != state.HeldMoveButtons)
+ {
+ var moveEvent = new MoveInputEvent(uid, component, component.HeldMoveButtons);
+ component.HeldMoveButtons = state.HeldMoveButtons;
+ RaiseLocalEvent(uid, ref moveEvent);
+ }
+ }
+
+ private void OnMoverGetState(EntityUid uid, InputMoverComponent component, ref ComponentGetState args)
+ {
+ args.State = new InputMoverComponentState()
+ {
+ CanMove = component.CanMove,
+ RelativeEntity = GetNetEntity(component.RelativeEntity),
+ LerpTarget = component.LerpTarget,
+ HeldMoveButtons = component.HeldMoveButtons,
+ RelativeRotation = component.RelativeRotation,
+ TargetRelativeRotation = component.TargetRelativeRotation,
+ };
}
private void ShutdownInput()
if (!MoverQuery.TryGetComponent(entity, out var moverComp))
return;
- // Relay the fact we had any movement event.
- // TODO: Ideally we'd do these in a tick instead of out of sim.
- var moveEvent = new MoveInputEvent(entity);
- RaiseLocalEvent(entity, ref moveEvent);
-
// For stuff like "Moving out of locker" or the likes
// We'll relay a movement input to the parent.
if (_container.IsEntityInContainer(entity) &&
RaiseLocalEvent(xform.ParentUid, ref relayMoveEvent);
}
- SetVelocityDirection(moverComp, dir, subTick, state);
+ SetVelocityDirection(entity, moverComp, dir, subTick, state);
}
private void OnInputInit(EntityUid uid, InputMoverComponent component, ComponentInit args)
if (moverComp == null) return;
- SetSprinting(moverComp, subTick, walking);
+ SetSprinting(uid, moverComp, subTick, walking);
}
public (Vector2 Walking, Vector2 Sprinting) GetVelocityInput(InputMoverComponent mover)
/// composed into a single direction vector, <see cref="VelocityDir"/>. Enabling
/// opposite directions will cancel each other out, resulting in no direction.
/// </summary>
- public void SetVelocityDirection(InputMoverComponent component, Direction direction, ushort subTick, bool enabled)
+ public void SetVelocityDirection(EntityUid entity, InputMoverComponent component, Direction direction, ushort subTick, bool enabled)
{
// Logger.Info($"[{_gameTiming.CurTick}/{subTick}] {direction}: {enabled}");
_ => throw new ArgumentException(nameof(direction))
};
- SetMoveInput(component, subTick, enabled, bit);
+ SetMoveInput(entity, component, subTick, enabled, bit);
}
- private void SetMoveInput(InputMoverComponent component, ushort subTick, bool enabled, MoveButtons bit)
+ private void SetMoveInput(EntityUid entity, InputMoverComponent component, ushort subTick, bool enabled, MoveButtons bit)
{
// Modifies held state of a movement button at a certain sub tick and updates current tick movement vectors.
ResetSubtick(component);
component.LastInputSubTick = 0;
}
- public void SetSprinting(InputMoverComponent component, ushort subTick, bool walking)
+ public void SetSprinting(EntityUid entity, InputMoverComponent component, ushort subTick, bool walking)
{
// Logger.Info($"[{_gameTiming.CurTick}/{subTick}] Sprint: {enabled}");
- SetMoveInput(component, subTick, walking, MoveButtons.Walk);
+ SetMoveInput(entity, component, subTick, walking, MoveButtons.Walk);
}
/// <summary>