Smooth docking traversal (#10822)
This commit is contained in:
parent
0ebc733b3a
commit
5b42861539
|
|
@ -105,6 +105,11 @@ namespace Content.Client.EscapeMenu.UI.Tabs
|
|||
AddButton(EngineKeyFunctions.MoveRight);
|
||||
AddButton(EngineKeyFunctions.Walk);
|
||||
|
||||
AddHeader("ui-options-header-camera");
|
||||
AddButton(EngineKeyFunctions.CameraRotateLeft);
|
||||
AddButton(EngineKeyFunctions.CameraRotateRight);
|
||||
AddButton(EngineKeyFunctions.CameraReset);
|
||||
|
||||
AddHeader("ui-options-header-interaction-basic");
|
||||
AddButton(EngineKeyFunctions.Use);
|
||||
AddButton(ContentKeyFunctions.UseItemInHand);
|
||||
|
|
|
|||
|
|
@ -1,41 +1,31 @@
|
|||
using System;
|
||||
using Content.Shared.Movement.Components;
|
||||
using Content.Shared.Movement.Systems;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.Physics;
|
||||
using Robust.Client.Player;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Collections;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Client.Eye;
|
||||
|
||||
public sealed class EyeLerpingSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly IEyeManager _eyeManager = default!;
|
||||
[Dependency] private readonly IMapManager _mapManager = default!;
|
||||
[Dependency] private readonly IPlayerManager _playerManager = default!;
|
||||
[Dependency] private readonly IGameTiming _gameTiming = default!;
|
||||
|
||||
// How fast the camera rotates in radians / s
|
||||
private const float CameraRotateSpeed = MathF.PI;
|
||||
|
||||
// Safety override
|
||||
private const float LerpTimeMax = 1.5f;
|
||||
|
||||
// Lerping information for the player's active eye.
|
||||
private readonly EyeLerpInformation _playerActiveEye = new();
|
||||
[Dependency] private readonly SharedMoverController _mover = default!;
|
||||
|
||||
// Eyes other than the primary eye that are currently active.
|
||||
private readonly Dictionary<EntityUid, EyeLerpInformation> _activeEyes = new();
|
||||
private readonly List<EntityUid> _toRemove = new();
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<EyeComponent, ComponentStartup>(OnEyeStartup);
|
||||
SubscribeLocalEvent<EyeComponent, ComponentShutdown>(OnEyeShutdown);
|
||||
|
||||
UpdatesAfter.Add(typeof(TransformSystem));
|
||||
|
|
@ -43,6 +33,27 @@ public sealed class EyeLerpingSystem : EntitySystem
|
|||
UpdatesBefore.Add(typeof(EyeUpdateSystem));
|
||||
}
|
||||
|
||||
private void OnEyeStartup(EntityUid uid, EyeComponent component, ComponentStartup args)
|
||||
{
|
||||
if (component.Eye == null)
|
||||
return;
|
||||
|
||||
// If the eye starts up then don't lerp at all.
|
||||
var xformQuery = GetEntityQuery<TransformComponent>();
|
||||
TryComp<InputMoverComponent>(uid, out var mover);
|
||||
xformQuery.TryGetComponent(uid, out var xform);
|
||||
var lerpInfo = _activeEyes.GetOrNew(uid);
|
||||
lerpInfo.TargetRotation = GetRotation(xformQuery, mover, xform);
|
||||
lerpInfo.LastRotation = lerpInfo.TargetRotation;
|
||||
|
||||
if (xform != null)
|
||||
{
|
||||
lerpInfo.MapId = xform.MapID;
|
||||
}
|
||||
|
||||
component.Eye.Rotation = lerpInfo.TargetRotation;
|
||||
}
|
||||
|
||||
private void OnEyeShutdown(EntityUid uid, EyeComponent component, ComponentShutdown args)
|
||||
{
|
||||
RemoveEye(uid);
|
||||
|
|
@ -50,145 +61,131 @@ public sealed class EyeLerpingSystem : EntitySystem
|
|||
|
||||
public void AddEye(EntityUid uid)
|
||||
{
|
||||
if (!_activeEyes.ContainsKey(uid))
|
||||
{
|
||||
_activeEyes.Add(uid, new());
|
||||
}
|
||||
_activeEyes.TryAdd(uid, new EyeLerpInformation());
|
||||
}
|
||||
|
||||
public void RemoveEye(EntityUid uid)
|
||||
{
|
||||
if (_activeEyes.ContainsKey(uid))
|
||||
_activeEyes.Remove(uid);
|
||||
}
|
||||
|
||||
public override void Update(float frameTime)
|
||||
{
|
||||
base.Update(frameTime);
|
||||
|
||||
if (!_gameTiming.IsFirstTimePredicted)
|
||||
return;
|
||||
|
||||
var moverQuery = GetEntityQuery<InputMoverComponent>();
|
||||
var xformQuery = GetEntityQuery<TransformComponent>();
|
||||
var foundEyes = new ValueList<EntityUid>(1);
|
||||
|
||||
// Set all of our eye rotations to the relevant values.
|
||||
foreach (var (eye, entity) in GetEyes())
|
||||
{
|
||||
_activeEyes.Remove(uid);
|
||||
var lerpInfo = _activeEyes.GetOrNew(entity);
|
||||
foundEyes.Add(entity);
|
||||
moverQuery.TryGetComponent(entity, out var mover);
|
||||
xformQuery.TryGetComponent(entity, out var xform);
|
||||
lerpInfo.LastRotation = eye.Rotation;
|
||||
lerpInfo.TargetRotation = GetRotation(xformQuery, mover, xform);
|
||||
|
||||
if (xform != null)
|
||||
{
|
||||
// If we traverse maps then don't lerp.
|
||||
if (xform.MapID != lerpInfo.MapId)
|
||||
{
|
||||
lerpInfo.LastRotation = lerpInfo.TargetRotation;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var eye in foundEyes)
|
||||
{
|
||||
if (!_activeEyes.ContainsKey(eye))
|
||||
{
|
||||
_activeEyes.Remove(eye);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Angle GetRotation(EntityQuery<TransformComponent> xformQuery, InputMoverComponent? mover = null, TransformComponent? xform = null)
|
||||
{
|
||||
// If we can move then tie our eye to our inputs (these also get lerped so it should be fine).
|
||||
if (mover != null)
|
||||
{
|
||||
return -_mover.GetParentGridAngle(mover);
|
||||
}
|
||||
|
||||
// if not tied to a mover then lock it to map / grid
|
||||
if (xform != null)
|
||||
{
|
||||
var relative = xform.GridUid;
|
||||
relative ??= xform.MapUid;
|
||||
|
||||
if (xformQuery.TryGetComponent(relative, out var relativeXform))
|
||||
{
|
||||
return relativeXform.WorldRotation;
|
||||
}
|
||||
}
|
||||
|
||||
return Angle.Zero;
|
||||
}
|
||||
|
||||
private IEnumerable<(IEye Eye, EntityUid Entity)> GetEyes()
|
||||
{
|
||||
if (_playerManager.LocalPlayer?.ControlledEntity is { } player && !Deleted(player))
|
||||
{
|
||||
yield return (_eyeManager.CurrentEye, player);
|
||||
}
|
||||
|
||||
if (_activeEyes.Count == 0)
|
||||
yield break;
|
||||
|
||||
var eyeQuery = GetEntityQuery<EyeComponent>();
|
||||
|
||||
foreach (var (ent, info) in _activeEyes)
|
||||
{
|
||||
if (!eyeQuery.TryGetComponent(ent, out var eyeComp) ||
|
||||
eyeComp.Eye == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
yield return (eyeComp.Eye, ent);
|
||||
}
|
||||
}
|
||||
|
||||
public override void FrameUpdate(float frameTime)
|
||||
{
|
||||
if (!_gameTiming.IsFirstTimePredicted)
|
||||
return;
|
||||
var tickFraction = (float) _gameTiming.TickFraction / ushort.MaxValue;
|
||||
var lerpMinimum = 0.01;
|
||||
|
||||
// Always do this one.
|
||||
LerpPlayerEye(frameTime);
|
||||
|
||||
foreach (var (entity, info) in _activeEyes)
|
||||
foreach (var (eye, entity) in GetEyes())
|
||||
{
|
||||
LerpEntityEye(entity, info, frameTime);
|
||||
}
|
||||
if (!_activeEyes.TryGetValue(entity, out var lerpInfo))
|
||||
continue;
|
||||
|
||||
if (_toRemove.Count != 0)
|
||||
{
|
||||
foreach (var entity in _toRemove)
|
||||
var shortest = Angle.ShortestDistance(lerpInfo.LastRotation, lerpInfo.TargetRotation);
|
||||
|
||||
if (Math.Abs(shortest.Theta) < lerpMinimum)
|
||||
{
|
||||
RemoveEye(entity);
|
||||
eye.Rotation = lerpInfo.TargetRotation;
|
||||
continue;
|
||||
}
|
||||
|
||||
_toRemove.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
private void LerpPlayerEye(float frameTime)
|
||||
{
|
||||
if (_playerManager.LocalPlayer?.ControlledEntity is not {} mob || Deleted(mob))
|
||||
return;
|
||||
|
||||
// We can't lerp if the mob can't move!
|
||||
if (!TryComp(mob, out InputMoverComponent? mover))
|
||||
return;
|
||||
|
||||
LerpEye(_eyeManager.CurrentEye, frameTime, mover.LastGridAngle, _playerActiveEye);
|
||||
}
|
||||
|
||||
private void LerpEntityEye(EntityUid uid, EyeLerpInformation info, float frameTime)
|
||||
{
|
||||
if (!TryComp(uid, out TransformComponent? transform)
|
||||
|| !TryComp(uid, out EyeComponent? eye)
|
||||
|| eye.Eye == null
|
||||
|| !_mapManager.TryGetGrid(transform.GridUid, out var grid))
|
||||
{
|
||||
_toRemove.Add(uid);
|
||||
return;
|
||||
}
|
||||
|
||||
LerpEye(eye.Eye, frameTime, grid.WorldRotation, info);
|
||||
}
|
||||
|
||||
private void LerpEye(IEye eye, float frameTime, Angle lastAngle, EyeLerpInformation lerpInfo)
|
||||
{
|
||||
|
||||
// Let's not turn the camera into a washing machine when the game starts.
|
||||
if (lerpInfo.LastGridAngle == null)
|
||||
{
|
||||
lerpInfo.LastGridAngle = lastAngle;
|
||||
eye.Rotation = -lastAngle;
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the last lerp grid angle we have is not the same as the last mover grid angle...
|
||||
if (!lerpInfo.LastGridAngle.Value.EqualsApprox(lastAngle))
|
||||
{
|
||||
// And now, we start lerping.
|
||||
lerpInfo.LerpTo = lastAngle;
|
||||
lerpInfo.LastGridAngle = lastAngle;
|
||||
lerpInfo.LerpStartRotation = eye.Rotation;
|
||||
lerpInfo.Accumulator = 0f;
|
||||
}
|
||||
|
||||
if (lerpInfo.LerpTo != null)
|
||||
{
|
||||
lerpInfo.Accumulator += frameTime;
|
||||
|
||||
var lerpRot = -lerpInfo.LerpTo.Value.FlipPositive().Reduced();
|
||||
var startRot = lerpInfo.LerpStartRotation.FlipPositive().Reduced();
|
||||
|
||||
var changeNeeded = Angle.ShortestDistance(startRot, lerpRot);
|
||||
|
||||
if (changeNeeded.EqualsApprox(Angle.Zero))
|
||||
{
|
||||
// Nothing to do here!
|
||||
lerpInfo.Cleanup(eye);
|
||||
return;
|
||||
}
|
||||
|
||||
// Get how much the camera should have moved by now. Make it faster depending on the change needed.
|
||||
var changeRot = (CameraRotateSpeed * Math.Max(1f, Math.Abs(changeNeeded) * 0.75f)) * lerpInfo.Accumulator * Math.Sign(changeNeeded);
|
||||
|
||||
// How close is this from reaching the end?
|
||||
var percentage = (float)Math.Abs(changeRot / changeNeeded);
|
||||
|
||||
eye.Rotation = Angle.Lerp(startRot, lerpRot, percentage);
|
||||
|
||||
// Either we have overshot, or we have taken way too long on this, emergency reset time
|
||||
if (percentage >= 1.0f || lerpInfo.Accumulator >= LerpTimeMax)
|
||||
{
|
||||
lerpInfo.Cleanup(eye);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// This makes it so rotating the camera manually is impossible...
|
||||
// However, it is needed. Why? Because of a funny (hilarious, even) race condition involving
|
||||
// ghosting, this system listening for attached mob changes, and the eye rotation being reset after our
|
||||
// changes back to zero because of an EyeComponent state coming from the server being applied.
|
||||
// At some point we'll need to come up with a solution for that. But for now, I just want to fix this.
|
||||
eye.Rotation = -lastAngle;
|
||||
eye.Rotation = shortest * tickFraction + lerpInfo.LastRotation;
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class EyeLerpInformation
|
||||
{
|
||||
public Angle? LastGridAngle { get; set; }
|
||||
public Angle? LerpTo { get; set; }
|
||||
public Angle LerpStartRotation { get; set; }
|
||||
public float Accumulator { get; set; }
|
||||
public Angle LastRotation;
|
||||
public Angle TargetRotation;
|
||||
|
||||
public void Cleanup(IEye eye)
|
||||
{
|
||||
eye.Rotation = -LerpTo ?? Angle.Zero;
|
||||
LerpStartRotation = eye.Rotation;
|
||||
LerpTo = null;
|
||||
Accumulator = 0;
|
||||
}
|
||||
/// <summary>
|
||||
/// If we go to a new map then don't lerp and snap instantly.
|
||||
/// </summary>
|
||||
public MapId MapId;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ using Content.Shared.Movement.Systems;
|
|||
using Content.Shared.Pulling.Components;
|
||||
using Robust.Client.Player;
|
||||
using Robust.Shared.Physics;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Timing;
|
||||
|
||||
namespace Content.Client.Physics.Controllers
|
||||
|
|
@ -23,7 +22,17 @@ namespace Content.Client.Physics.Controllers
|
|||
if (TryComp<RelayInputMoverComponent>(player, out var relayMover))
|
||||
{
|
||||
if (relayMover.RelayEntity != null)
|
||||
{
|
||||
if (TryComp<InputMoverComponent>(player, out var mover) &&
|
||||
TryComp<InputMoverComponent>(relayMover.RelayEntity, out var relayed))
|
||||
{
|
||||
relayed.RelativeEntity = mover.RelativeEntity;
|
||||
relayed.RelativeRotation = mover.RelativeRotation;
|
||||
relayed.TargetRelativeRotation = mover.RelativeRotation;
|
||||
}
|
||||
|
||||
HandleClientsideMovement(relayMover.RelayEntity.Value, frameTime);
|
||||
}
|
||||
}
|
||||
|
||||
HandleClientsideMovement(player, frameTime);
|
||||
|
|
@ -31,8 +40,10 @@ namespace Content.Client.Physics.Controllers
|
|||
|
||||
private void HandleClientsideMovement(EntityUid player, float frameTime)
|
||||
{
|
||||
var xformQuery = GetEntityQuery<TransformComponent>();
|
||||
|
||||
if (!TryComp(player, out InputMoverComponent? mover) ||
|
||||
!TryComp(player, out TransformComponent? xform))
|
||||
!xformQuery.TryGetComponent(player, out var xform))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
|
@ -47,20 +58,12 @@ namespace Content.Client.Physics.Controllers
|
|||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (TryComp<InputMoverComponent>(xform.ParentUid, out var parentMover))
|
||||
{
|
||||
mover.LastGridAngle = parentMover.LastGridAngle;
|
||||
}
|
||||
}
|
||||
else if (!TryComp(player, out body))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (xform.GridUid != null)
|
||||
mover.LastGridAngle = GetParentGridAngle(xform, mover);
|
||||
|
||||
// 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.
|
||||
|
|
@ -98,7 +101,7 @@ namespace Content.Client.Physics.Controllers
|
|||
}
|
||||
|
||||
// Server-side should just be handled on its own so we'll just do this shizznit
|
||||
HandleMobMovement(mover, body, xformMover, frameTime);
|
||||
HandleMobMovement(mover, body, xformMover, frameTime, xformQuery);
|
||||
}
|
||||
|
||||
protected override bool CanSound()
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ namespace Content.Server.Administration.Commands
|
|||
var coordinates = player.AttachedEntity != null
|
||||
? _entities.GetComponent<TransformComponent>(player.AttachedEntity.Value).Coordinates
|
||||
: EntitySystem.Get<GameTicker>().GetObserverSpawnPoint();
|
||||
var ghost = _entities.SpawnEntity("AdminObserver", coordinates.ToMap(_entities));
|
||||
var ghost = _entities.SpawnEntity("AdminObserver", coordinates);
|
||||
|
||||
if (canReturn)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -121,10 +121,9 @@ namespace Content.Server.GameTicking
|
|||
|
||||
var (entities, gridIds) = _mapLoader.LoadMap(targetMapId, ev.GameMap.MapPath.ToString(), ev.Options);
|
||||
|
||||
var gridUids = gridIds.Select(g => (EntityUid)g).ToList();
|
||||
var gridUids = gridIds.Select(g => g).ToList();
|
||||
RaiseLocalEvent(new PostGameMapLoad(map, targetMapId, entities, gridUids, stationName));
|
||||
|
||||
_spawnPoint = _mapManager.GetGrid(gridIds[0]).ToCoordinates();
|
||||
return (entities, gridUids);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -25,9 +25,6 @@ namespace Content.Server.GameTicking
|
|||
{
|
||||
private const string ObserverPrototypeName = "MobObserver";
|
||||
|
||||
[ViewVariables(VVAccess.ReadWrite), Obsolete("Due for removal when observer spawning is refactored.")] // See also: MindComponent's OnShutdown shitcode
|
||||
private EntityCoordinates _spawnPoint;
|
||||
|
||||
/// <summary>
|
||||
/// How many players have joined the round through normal methods.
|
||||
/// Useful for game rules to look at. Doesn't count observers, people in lobby, etc.
|
||||
|
|
@ -280,24 +277,72 @@ namespace Content.Server.GameTicking
|
|||
#region Spawn Points
|
||||
public EntityCoordinates GetObserverSpawnPoint()
|
||||
{
|
||||
// TODO rename this to TryGetObserverSpawnPoint to make it clear that the result might be invalid. Or at
|
||||
// least try try more fallback values, like randomly spawning them in any available map or just creating a
|
||||
// "we fucked up" map. Its better than dumping them into the void.
|
||||
|
||||
var location = _spawnPoint.IsValid(EntityManager) ? _spawnPoint : EntityCoordinates.Invalid;
|
||||
|
||||
_possiblePositions.Clear();
|
||||
|
||||
foreach (var (point, transform) in EntityManager.EntityQuery<SpawnPointComponent, TransformComponent>(true))
|
||||
{
|
||||
if (point.SpawnType == SpawnPointType.Observer)
|
||||
_possiblePositions.Add(transform.Coordinates);
|
||||
if (point.SpawnType != SpawnPointType.Observer)
|
||||
continue;
|
||||
|
||||
_possiblePositions.Add(transform.Coordinates);
|
||||
}
|
||||
|
||||
var metaQuery = GetEntityQuery<MetaDataComponent>();
|
||||
|
||||
// Fallback to a random grid.
|
||||
if (_possiblePositions.Count == 0)
|
||||
{
|
||||
foreach (var grid in _mapManager.GetAllGrids())
|
||||
{
|
||||
if (!metaQuery.TryGetComponent(grid.GridEntityId, out var meta) ||
|
||||
meta.EntityPaused)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
_possiblePositions.Add(new EntityCoordinates(grid.GridEntityId, Vector2.Zero));
|
||||
}
|
||||
}
|
||||
|
||||
if (_possiblePositions.Count != 0)
|
||||
location = _robustRandom.Pick(_possiblePositions);
|
||||
{
|
||||
// TODO: This is just here for the eye lerping.
|
||||
// Ideally engine would just spawn them on grid directly I guess? Right now grid traversal is handling it during
|
||||
// update which means we need to add a hack somewhere around it.
|
||||
var spawn = _robustRandom.Pick(_possiblePositions);
|
||||
var toMap = spawn.ToMap(EntityManager);
|
||||
|
||||
return location;
|
||||
if (_mapManager.TryFindGridAt(toMap, out var foundGrid))
|
||||
{
|
||||
return new EntityCoordinates(foundGrid.GridEntityId,
|
||||
foundGrid.InvWorldMatrix.Transform(toMap.Position));
|
||||
}
|
||||
|
||||
return spawn;
|
||||
}
|
||||
|
||||
if (_mapManager.MapExists(DefaultMap))
|
||||
{
|
||||
return new EntityCoordinates(_mapManager.GetMapEntityId(DefaultMap), Vector2.Zero);
|
||||
}
|
||||
|
||||
// Just pick a point at this point I guess.
|
||||
foreach (var map in _mapManager.GetAllMapIds())
|
||||
{
|
||||
var mapUid = _mapManager.GetMapEntityId(map);
|
||||
|
||||
if (!metaQuery.TryGetComponent(mapUid, out var meta) ||
|
||||
meta.EntityPaused)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
return new EntityCoordinates(mapUid, Vector2.Zero);
|
||||
}
|
||||
|
||||
// AAAAAAAAAAAAA
|
||||
_sawmill.Error("Found no observer spawn points!");
|
||||
return EntityCoordinates.Invalid;
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
|
|
|
|||
|
|
@ -75,9 +75,7 @@ namespace Content.Server.Medical.CrewMonitoring
|
|||
// the monitor. But in the special case where the monitor IS a player (i.e., admin ghost), we base it off
|
||||
// the players eye rotation. We don't know what that is for sure, but we know their last grid angle, which
|
||||
// should work well enough?
|
||||
if (TryComp(uid, out InputMoverComponent? mover))
|
||||
worldRot = mover.LastGridAngle;
|
||||
else if (_mapManager.TryGetGrid(xform.GridUid, out var grid))
|
||||
if (_mapManager.TryGetGrid(xform.GridUid, out var grid))
|
||||
worldRot = grid.WorldRotation;
|
||||
|
||||
// update all sensors info
|
||||
|
|
|
|||
|
|
@ -28,10 +28,23 @@ namespace Content.Server.Physics.Controllers
|
|||
|
||||
var bodyQuery = GetEntityQuery<PhysicsComponent>();
|
||||
var relayQuery = GetEntityQuery<RelayInputMoverComponent>();
|
||||
var xformQuery = GetEntityQuery<TransformComponent>();
|
||||
var moverQuery = GetEntityQuery<InputMoverComponent>();
|
||||
|
||||
foreach (var (mover, xform) in EntityQuery<InputMoverComponent, TransformComponent>(true))
|
||||
foreach (var mover in EntityQuery<InputMoverComponent>(true))
|
||||
{
|
||||
if (relayQuery.TryGetComponent(mover.Owner, out var relayed) && relayed.RelayEntity != null)
|
||||
{
|
||||
if (moverQuery.TryGetComponent(relayed.RelayEntity, out var relayMover))
|
||||
{
|
||||
relayMover.RelativeEntity = mover.RelativeEntity;
|
||||
relayMover.RelativeRotation = mover.RelativeRotation;
|
||||
relayMover.TargetRelativeRotation = mover.TargetRelativeRotation;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (!xformQuery.TryGetComponent(mover.Owner, out var xform))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
|
@ -46,18 +59,13 @@ namespace Content.Server.Physics.Controllers
|
|||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (TryComp<InputMoverComponent>(xform.ParentUid, out var parentMover))
|
||||
{
|
||||
mover.LastGridAngle = parentMover.LastGridAngle;
|
||||
}
|
||||
}
|
||||
else if (!bodyQuery.TryGetComponent(mover.Owner, out body))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
HandleMobMovement(mover, body, xformMover, frameTime);
|
||||
HandleMobMovement(mover, body, xformMover, frameTime, xformQuery);
|
||||
}
|
||||
|
||||
HandleShuttleMovement(frameTime);
|
||||
|
|
|
|||
|
|
@ -908,6 +908,16 @@ namespace Content.Shared.CCVar
|
|||
* Shuttles
|
||||
*/
|
||||
|
||||
// Look this is technically eye behavior but its main impact is shuttles so I just dumped it here.
|
||||
/// <summary>
|
||||
/// If true then the camera will match the grid / map and is unchangeable.
|
||||
/// - When traversing grids it will snap to 0 degrees rotation.
|
||||
/// False means the player has control over the camera rotation.
|
||||
/// - When traversing grids it will snap to the nearest cardinal which will generally be imperceptible.
|
||||
/// </summary>
|
||||
public static readonly CVarDef<bool> CameraRotationLocked =
|
||||
CVarDef.Create("shuttle.camera_rotation_locked", true, CVar.REPLICATED);
|
||||
|
||||
/// <summary>
|
||||
/// Whether cargo shuttles are enabled.
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -40,8 +40,30 @@ namespace Content.Shared.Movement.Components
|
|||
|
||||
public MoveButtons HeldMoveButtons = MoveButtons.None;
|
||||
|
||||
/// <summary>
|
||||
/// Entity our movement is relative to.
|
||||
/// </summary>
|
||||
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]
|
||||
public Angle LastGridAngle { get; set; } = new(0);
|
||||
public Angle TargetRelativeRotation = Angle.Zero;
|
||||
|
||||
/// <summary>
|
||||
/// The current relative rotation. This will lerp towards the <see cref="TargetRelativeRotation"/>.
|
||||
/// </summary>
|
||||
[ViewVariables] public Angle RelativeRotation;
|
||||
|
||||
/// <summary>
|
||||
/// If we traverse on / off a grid then set a timer to update our relative inputs.
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public float LerpAccumulator;
|
||||
|
||||
public const float LerpTime = 1.0f;
|
||||
|
||||
public bool Sprinting => (HeldMoveButtons & MoveButtons.Walk) == 0x0;
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ using Content.Shared.CCVar;
|
|||
using Content.Shared.Input;
|
||||
using Content.Shared.Movement.Components;
|
||||
using Content.Shared.Movement.Events;
|
||||
using Content.Shared.Shuttles.Components;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.GameStates;
|
||||
using Robust.Shared.Input;
|
||||
using Robust.Shared.Input.Binding;
|
||||
|
|
@ -17,6 +17,8 @@ namespace Content.Shared.Movement.Systems
|
|||
/// </summary>
|
||||
public abstract partial class SharedMoverController
|
||||
{
|
||||
public bool CameraRotationLocked { get; private set; }
|
||||
|
||||
private void InitializeInput()
|
||||
{
|
||||
var moveUpCmdHandler = new MoverDirInputCmdHandler(this, Direction.North);
|
||||
|
|
@ -30,6 +32,9 @@ namespace Content.Shared.Movement.Systems
|
|||
.Bind(EngineKeyFunctions.MoveRight, moveRightCmdHandler)
|
||||
.Bind(EngineKeyFunctions.MoveDown, moveDownCmdHandler)
|
||||
.Bind(EngineKeyFunctions.Walk, new WalkInputCmdHandler(this))
|
||||
.Bind(EngineKeyFunctions.CameraRotateLeft, new CameraRotateInputCmdHandler(this, Direction.West))
|
||||
.Bind(EngineKeyFunctions.CameraRotateRight, new CameraRotateInputCmdHandler(this, Direction.East))
|
||||
.Bind(EngineKeyFunctions.CameraReset, new CameraResetInputCmdHandler(this))
|
||||
// TODO: Relay
|
||||
// Shuttle
|
||||
.Bind(ContentKeyFunctions.ShuttleStrafeUp, new ShuttleInputCmdHandler(this, ShuttleButtons.StrafeUp))
|
||||
|
|
@ -44,6 +49,14 @@ namespace Content.Shared.Movement.Systems
|
|||
SubscribeLocalEvent<InputMoverComponent, ComponentInit>(OnInputInit);
|
||||
SubscribeLocalEvent<InputMoverComponent, ComponentGetState>(OnInputGetState);
|
||||
SubscribeLocalEvent<InputMoverComponent, ComponentHandleState>(OnInputHandleState);
|
||||
SubscribeLocalEvent<InputMoverComponent, EntParentChangedMessage>(OnInputParentChange);
|
||||
|
||||
_configManager.OnValueChanged(CCVars.CameraRotationLocked, SetCameraRotationLocked, true);
|
||||
}
|
||||
|
||||
private void SetCameraRotationLocked(bool obj)
|
||||
{
|
||||
CameraRotationLocked = obj;
|
||||
}
|
||||
|
||||
private void SetMoveInput(InputMoverComponent component, MoveButtons buttons)
|
||||
|
|
@ -55,27 +68,98 @@ namespace Content.Shared.Movement.Systems
|
|||
|
||||
private void OnInputHandleState(EntityUid uid, InputMoverComponent component, ref ComponentHandleState args)
|
||||
{
|
||||
if (args.Current is not InputMoverComponentState state) return;
|
||||
if (args.Current is not InputMoverComponentState state)
|
||||
return;
|
||||
|
||||
component.HeldMoveButtons = state.Buttons;
|
||||
component.LastInputTick = GameTick.Zero;
|
||||
component.LastInputSubTick = 0;
|
||||
component.CanMove = state.CanMove;
|
||||
|
||||
component.RelativeRotation = state.RelativeRotation;
|
||||
component.TargetRelativeRotation = state.TargetRelativeRotation;
|
||||
component.RelativeEntity = state.RelativeEntity;
|
||||
component.LerpAccumulator = state.LerpAccumulator;
|
||||
}
|
||||
|
||||
private void OnInputGetState(EntityUid uid, InputMoverComponent component, ref ComponentGetState args)
|
||||
{
|
||||
args.State = new InputMoverComponentState(component.HeldMoveButtons, component.CanMove);
|
||||
args.State = new InputMoverComponentState(
|
||||
component.HeldMoveButtons,
|
||||
component.CanMove,
|
||||
component.RelativeRotation,
|
||||
component.TargetRelativeRotation,
|
||||
component.RelativeEntity,
|
||||
component.LerpAccumulator);
|
||||
}
|
||||
|
||||
private void ShutdownInput()
|
||||
{
|
||||
CommandBinds.Unregister<SharedMoverController>();
|
||||
_configManager.UnsubValueChanged(CCVars.CameraRotationLocked, SetCameraRotationLocked);
|
||||
}
|
||||
|
||||
public bool DiagonalMovementEnabled => _configManager.GetCVar(CCVars.GameDiagonalMovement);
|
||||
|
||||
protected virtual void HandleShuttleInput(EntityUid uid, ShuttleButtons button, ushort subTick, bool state) {}
|
||||
|
||||
public void RotateCamera(EntityUid uid, Angle angle)
|
||||
{
|
||||
if (CameraRotationLocked || !TryComp<InputMoverComponent>(uid, out var mover))
|
||||
return;
|
||||
|
||||
mover.TargetRelativeRotation += angle;
|
||||
Dirty(mover);
|
||||
}
|
||||
|
||||
public void ResetCamera(EntityUid uid)
|
||||
{
|
||||
if (CameraRotationLocked || !TryComp<InputMoverComponent>(uid, out var mover) || mover.TargetRelativeRotation.Equals(Angle.Zero))
|
||||
return;
|
||||
|
||||
mover.TargetRelativeRotation = Angle.Zero;
|
||||
Dirty(mover);
|
||||
}
|
||||
|
||||
public Angle GetParentGridAngle(InputMoverComponent mover)
|
||||
{
|
||||
var rotation = mover.RelativeRotation;
|
||||
|
||||
if (TryComp<TransformComponent>(mover.RelativeEntity, out var relativeXform))
|
||||
return (relativeXform.WorldRotation + rotation);
|
||||
|
||||
return rotation;
|
||||
}
|
||||
|
||||
private void OnInputParentChange(EntityUid uid, InputMoverComponent component, ref EntParentChangedMessage args)
|
||||
{
|
||||
// If we change our grid / map then delay updating our LastGridAngle.
|
||||
var relative = args.Transform.GridUid;
|
||||
relative ??= args.Transform.MapUid;
|
||||
|
||||
if (component.LifeStage < ComponentLifeStage.Running)
|
||||
{
|
||||
component.RelativeEntity = relative;
|
||||
Dirty(component);
|
||||
return;
|
||||
}
|
||||
|
||||
// If we go on a grid and back off then just reset the accumulator.
|
||||
if (relative == component.RelativeEntity)
|
||||
{
|
||||
if (component.LerpAccumulator != 0f)
|
||||
{
|
||||
component.LerpAccumulator = 0f;
|
||||
Dirty(component);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
component.LerpAccumulator = InputMoverComponent.LerpTime;
|
||||
Dirty(component);
|
||||
}
|
||||
|
||||
private void HandleDirChange(EntityUid entity, Direction dir, ushort subTick, bool state)
|
||||
{
|
||||
// Relayed movement just uses the same keybinds given we're moving the relayed entity
|
||||
|
|
@ -124,9 +208,11 @@ namespace Content.Shared.Movement.Systems
|
|||
{
|
||||
var xform = Transform(uid);
|
||||
|
||||
if (!xform.ParentUid.IsValid()) return;
|
||||
if (!xform.ParentUid.IsValid())
|
||||
return;
|
||||
|
||||
component.LastGridAngle = Transform(xform.ParentUid).WorldRotation;
|
||||
component.RelativeEntity = xform.GridUid ?? xform.MapUid;
|
||||
component.TargetRelativeRotation = Angle.Zero;
|
||||
}
|
||||
|
||||
private void HandleRunChange(EntityUid uid, ushort subTick, bool walking)
|
||||
|
|
@ -300,9 +386,53 @@ namespace Content.Shared.Movement.Systems
|
|||
return (buttons & flag) == flag;
|
||||
}
|
||||
|
||||
private sealed class CameraRotateInputCmdHandler : InputCmdHandler
|
||||
{
|
||||
private readonly SharedMoverController _controller;
|
||||
private readonly Angle _angle;
|
||||
|
||||
public CameraRotateInputCmdHandler(SharedMoverController controller, Direction direction)
|
||||
{
|
||||
_controller = controller;
|
||||
_angle = direction.ToAngle();
|
||||
}
|
||||
|
||||
public override bool HandleCmdMessage(ICommonSession? session, InputCmdMessage message)
|
||||
{
|
||||
if (message is not FullInputCmdMessage full || session?.AttachedEntity == null) return false;
|
||||
|
||||
if (full.State != BoundKeyState.Up)
|
||||
return false;
|
||||
|
||||
_controller.RotateCamera(session.AttachedEntity.Value, _angle);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class CameraResetInputCmdHandler : InputCmdHandler
|
||||
{
|
||||
private readonly SharedMoverController _controller;
|
||||
|
||||
public CameraResetInputCmdHandler(SharedMoverController controller)
|
||||
{
|
||||
_controller = controller;
|
||||
}
|
||||
|
||||
public override bool HandleCmdMessage(ICommonSession? session, InputCmdMessage message)
|
||||
{
|
||||
if (message is not FullInputCmdMessage full || session?.AttachedEntity == null) return false;
|
||||
|
||||
if (full.State != BoundKeyState.Up)
|
||||
return false;
|
||||
|
||||
_controller.ResetCamera(session.AttachedEntity.Value);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class MoverDirInputCmdHandler : InputCmdHandler
|
||||
{
|
||||
private SharedMoverController _controller;
|
||||
private readonly SharedMoverController _controller;
|
||||
private readonly Direction _dir;
|
||||
|
||||
public MoverDirInputCmdHandler(SharedMoverController controller, Direction dir)
|
||||
|
|
@ -344,17 +474,33 @@ namespace Content.Shared.Movement.Systems
|
|||
public MoveButtons Buttons { get; }
|
||||
public readonly bool CanMove;
|
||||
|
||||
public InputMoverComponentState(MoveButtons buttons, bool canMove)
|
||||
/// <summary>
|
||||
/// Our current rotation for movement purposes. This is lerping towards <see cref="TargetRelativeRotation"/>
|
||||
/// </summary>
|
||||
public Angle RelativeRotation;
|
||||
|
||||
/// <summary>
|
||||
/// Target rotation relative to the <see cref="RelativeEntity"/>. Typically 0
|
||||
/// </summary>
|
||||
public Angle TargetRelativeRotation;
|
||||
public EntityUid? RelativeEntity;
|
||||
public float LerpAccumulator = 0f;
|
||||
|
||||
public InputMoverComponentState(MoveButtons buttons, bool canMove, Angle relativeRotation, Angle targetRelativeRotation, EntityUid? relativeEntity, float lerpAccumulator)
|
||||
{
|
||||
Buttons = buttons;
|
||||
CanMove = canMove;
|
||||
RelativeRotation = relativeRotation;
|
||||
TargetRelativeRotation = targetRelativeRotation;
|
||||
RelativeEntity = relativeEntity;
|
||||
LerpAccumulator = lerpAccumulator;
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class ShuttleInputCmdHandler : InputCmdHandler
|
||||
{
|
||||
private SharedMoverController _controller;
|
||||
private ShuttleButtons _button;
|
||||
private readonly SharedMoverController _controller;
|
||||
private readonly ShuttleButtons _button;
|
||||
|
||||
public ShuttleInputCmdHandler(SharedMoverController controller, ShuttleButtons button)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -44,6 +44,8 @@ namespace Content.Shared.Movement.Systems
|
|||
private const float FootstepVolume = 3f;
|
||||
private const float FootstepWalkingAddedVolumeMultiplier = 0f;
|
||||
|
||||
protected ISawmill Sawmill = default!;
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="CCVars.StopSpeed"/>
|
||||
/// </summary>
|
||||
|
|
@ -59,6 +61,7 @@ namespace Content.Shared.Movement.Systems
|
|||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
Sawmill = Logger.GetSawmill("mover");
|
||||
InitializeFootsteps();
|
||||
InitializeInput();
|
||||
InitializeMob();
|
||||
|
|
@ -87,14 +90,6 @@ namespace Content.Shared.Movement.Systems
|
|||
UsedMobMovement.Clear();
|
||||
}
|
||||
|
||||
protected Angle GetParentGridAngle(TransformComponent xform, InputMoverComponent mover)
|
||||
{
|
||||
if (!_mapManager.TryGetGrid(xform.GridUid, out var grid))
|
||||
return mover.LastGridAngle;
|
||||
|
||||
return grid.WorldRotation;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Movement while considering actionblockers, weightlessness, etc.
|
||||
/// </summary>
|
||||
|
|
@ -102,7 +97,8 @@ namespace Content.Shared.Movement.Systems
|
|||
InputMoverComponent mover,
|
||||
PhysicsComponent physicsComponent,
|
||||
TransformComponent xform,
|
||||
float frameTime)
|
||||
float frameTime,
|
||||
EntityQuery<TransformComponent> xformQuery)
|
||||
{
|
||||
DebugTools.Assert(!UsedMobMovement.ContainsKey(mover.Owner));
|
||||
|
||||
|
|
@ -134,12 +130,6 @@ namespace Content.Shared.Movement.Systems
|
|||
if (!touching && TryComp<MobMoverComponent>(xform.Owner, out var mobMover))
|
||||
touching |= IsAroundCollider(PhysicsSystem, xform, mobMover, physicsComponent);
|
||||
}
|
||||
|
||||
if (!touching)
|
||||
{
|
||||
if (xform.GridUid != null)
|
||||
mover.LastGridAngle = GetParentGridAngle(xform, mover);
|
||||
}
|
||||
}
|
||||
|
||||
// Regular movement.
|
||||
|
|
@ -152,7 +142,92 @@ namespace Content.Shared.Movement.Systems
|
|||
|
||||
var total = walkDir * walkSpeed + sprintDir * sprintSpeed;
|
||||
|
||||
var parentRotation = GetParentGridAngle(xform, mover);
|
||||
// Update relative movement
|
||||
if (mover.LerpAccumulator > 0f)
|
||||
{
|
||||
Dirty(mover);
|
||||
mover.LerpAccumulator -= frameTime;
|
||||
|
||||
if (mover.LerpAccumulator <= 0f)
|
||||
{
|
||||
mover.LerpAccumulator = 0f;
|
||||
var relative = xform.GridUid;
|
||||
relative ??= xform.MapUid;
|
||||
|
||||
// So essentially what we want:
|
||||
// 1. If we go from grid to map then preserve our rotation and continue as usual
|
||||
// 2. If we go from grid -> grid then (after lerp time) snap to nearest cardinal (probably imperceptible)
|
||||
// 3. If we go from map -> grid then (after lerp time) snap to nearest cardinal
|
||||
|
||||
if (!mover.RelativeEntity.Equals(relative))
|
||||
{
|
||||
// Okay need to get our old relative rotation with respect to our new relative rotation
|
||||
// e.g. if we were right side up on our current grid need to get what that is on our new grid.
|
||||
var currentRotation = Angle.Zero;
|
||||
var targetRotation = Angle.Zero;
|
||||
|
||||
// Get our current relative rotation
|
||||
if (xformQuery.TryGetComponent(mover.RelativeEntity, out var oldRelativeXform))
|
||||
{
|
||||
currentRotation = oldRelativeXform.WorldRotation + mover.RelativeRotation;
|
||||
}
|
||||
|
||||
if (xformQuery.TryGetComponent(relative, out var relativeXform))
|
||||
{
|
||||
// This is our current rotation relative to our new parent.
|
||||
mover.RelativeRotation = (currentRotation - relativeXform.WorldRotation).FlipPositive();
|
||||
}
|
||||
|
||||
// If we went from grid -> map we'll preserve our worldrotation
|
||||
if (relative != null && _mapManager.IsMap(relative.Value))
|
||||
{
|
||||
targetRotation = currentRotation.FlipPositive().Reduced();
|
||||
}
|
||||
// If we went from grid -> grid OR grid -> map then snap the target to cardinal and lerp there.
|
||||
// OR just rotate to zero (depending on cvar)
|
||||
else if (relative != null && _mapManager.IsGrid(relative.Value))
|
||||
{
|
||||
if (CameraRotationLocked)
|
||||
targetRotation = Angle.Zero;
|
||||
else
|
||||
targetRotation = mover.RelativeRotation.GetCardinalDir().ToAngle().Reduced();
|
||||
}
|
||||
|
||||
mover.RelativeEntity = relative;
|
||||
mover.TargetRelativeRotation = targetRotation;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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.005))
|
||||
{
|
||||
var adjustment = angleDiff * 5f * frameTime;
|
||||
var minAdjustment = 0.005 * 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;
|
||||
Dirty(mover);
|
||||
}
|
||||
else if (!angleDiff.Equals(Angle.Zero))
|
||||
{
|
||||
mover.RelativeRotation = mover.TargetRelativeRotation;
|
||||
Dirty(mover);
|
||||
}
|
||||
|
||||
var parentRotation = GetParentGridAngle(mover);
|
||||
var worldTotal = _relativeMovement ? parentRotation.RotateVec(total) : total;
|
||||
|
||||
DebugTools.Assert(MathHelper.CloseToPercent(total.Length, worldTotal.Length));
|
||||
|
|
@ -190,17 +265,11 @@ namespace Content.Shared.Movement.Systems
|
|||
var minimumFrictionSpeed = moveSpeedComponent?.MinimumFrictionSpeed ?? MovementSpeedModifierComponent.DefaultMinimumFrictionSpeed;
|
||||
Friction(minimumFrictionSpeed, frameTime, friction, ref velocity);
|
||||
|
||||
if (xform.GridUid != EntityUid.Invalid)
|
||||
mover.LastGridAngle = parentRotation;
|
||||
|
||||
if (worldTotal != Vector2.Zero)
|
||||
{
|
||||
// This should have its event run during island solver soooo
|
||||
xform.DeferUpdates = true;
|
||||
|
||||
xform.LocalRotation = xform.GridUid != null
|
||||
? total.ToWorldAngle()
|
||||
: worldTotal.ToWorldAngle();
|
||||
xform.WorldRotation = worldTotal.ToWorldAngle();
|
||||
xform.DeferUpdates = false;
|
||||
|
||||
if (!weightless && TryComp<MobMoverComponent>(mover.Owner, out var mobMover) &&
|
||||
|
|
|
|||
|
|
@ -66,6 +66,7 @@ ui-options-bind-reset = Reset
|
|||
ui-options-key-prompt = Press a key...
|
||||
|
||||
ui-options-header-movement = Movement
|
||||
ui-options-header-camera = Camera
|
||||
ui-options-header-interaction-basic = Basic Interaction
|
||||
ui-options-header-interaction-adv = Advanced Interaction
|
||||
ui-options-header-ui = User Interface
|
||||
|
|
@ -84,6 +85,10 @@ ui-options-function-move-down = Move Down
|
|||
ui-options-function-move-right = Move Right
|
||||
ui-options-function-walk = Walk
|
||||
|
||||
ui-options-function-camera-rotate-left = Rotate left
|
||||
ui-options-function-camera-rotate-right = Rotate right
|
||||
ui-options-function-camera-reset = Reset
|
||||
|
||||
ui-options-function-use = Use
|
||||
ui-options-function-wide-attack = Wide attack
|
||||
ui-options-function-activate-item-in-hand = Activate item in hand
|
||||
|
|
|
|||
|
|
@ -61,13 +61,17 @@ binds:
|
|||
- function: ShuttleBrake
|
||||
type: State
|
||||
key: Space
|
||||
|
||||
# Camera
|
||||
- function: CameraRotateLeft
|
||||
type: State
|
||||
key: NumpadNum7
|
||||
- function: CameraRotateRight
|
||||
type: State
|
||||
key: NumpadNum9
|
||||
- function: CameraReset
|
||||
type: State
|
||||
key: NumpadNum8
|
||||
# Misc
|
||||
- function: ShowEscapeMenu
|
||||
type: State
|
||||
key: Escape
|
||||
|
|
|
|||
Loading…
Reference in New Issue