diff --git a/Assets/Scripts/ServerShared/Entity.cs b/Assets/Scripts/ServerShared/Entity.cs index 90fb6e0b5..87171d80e 100644 --- a/Assets/Scripts/ServerShared/Entity.cs +++ b/Assets/Scripts/ServerShared/Entity.cs @@ -1373,6 +1373,10 @@ public abstract class Entity yield return b; } + // The phase between the items' performance update and the power bus: what an entity commands its items to do this + // tick, read from the same liveness and stats Execute then acts on. Ship's allocation lives here. + protected virtual void UpdateThrottles() { } + public virtual void Update(float delta) { TurnRate = 0; @@ -1402,6 +1406,7 @@ public abstract class Entity UpdateTemperature(delta); foreach (var item in _orderedEquipment) item.UpdatePerformance(); + UpdateThrottles(); if (_active) { diff --git a/Assets/Scripts/ServerShared/Ship.cs b/Assets/Scripts/ServerShared/Ship.cs index 7daae5c9c..a7339c4e1 100644 --- a/Assets/Scripts/ServerShared/Ship.cs +++ b/Assets/Scripts/ServerShared/Ship.cs @@ -98,19 +98,21 @@ public class Ship : Entity #endregion + // Called by Entity.Update once the items' performance is current and before the power bus is stepped and the items + // execute: liveness, thrust and the throttles they decide are one phase, the one Execute acts on. The lock is a + // bound on the allocator's solve; intent reaches the allocator here and nowhere else. + protected override void UpdateThrottles() + { + if (_exitingWormhole || _enteringWormhole) return; + RecalculateEnvelope(); + for (var i = 0; i < _allThrusters.Length; i++) + _columns[i] = _allThrusters[i].Column(_allThrusters[i].NominalThrust); + _allocator.Allocate(_columns, MovementDirection, Turn, _throttles, ThrottleLocked); + for (var i = 0; i < _allThrusters.Length; i++) _allThrusters[i].Axis = _throttles[i]; + } + public override void Update(float delta) { - if (_active && !_exitingWormhole && !_enteringWormhole) - { - RecalculateEnvelope(); - // The lock replaces intent here, once; player and agent intent both pass through this one read. - var move = ThrottleLocked ? float2(0, 1) : MovementDirection; - for (var i = 0; i < _allThrusters.Length; i++) - _columns[i] = _allThrusters[i].Column(_allThrusters[i].NominalThrust); - _allocator.Allocate(_columns, move, Turn, _throttles); - for (var i = 0; i < _allThrusters.Length; i++) _allThrusters[i].Axis = _throttles[i]; - } - var velocityMagnitude = length(Velocity); if(velocityMagnitude > .01f) Velocity = normalize(Velocity) * decay(velocityMagnitude, HullData.Drag, delta); diff --git a/Assets/Scripts/ServerShared/ThrustAllocator.cs b/Assets/Scripts/ServerShared/ThrustAllocator.cs index 828edf993..148b98d29 100644 --- a/Assets/Scripts/ServerShared/ThrustAllocator.cs +++ b/Assets/Scripts/ServerShared/ThrustAllocator.cs @@ -31,11 +31,18 @@ public sealed class ThrustAllocator int _columns = -1; float[] _a, _b, _lo, _hi, _x, _workspace; - public void Allocate(ReadOnlySpan columns, float2 move, float turn, Span throttle) + /// + /// Solves the throttles for the intent. is the throttle lock: every thruster whose + /// column pushes forward is bounded below at full, the stick is ignored (forward is the only translation asked, so + /// nothing fires against the floored drives), and the turn is served by what remains. + /// + public void Allocate(ReadOnlySpan columns, float2 move, float turn, Span throttle, bool forwardFloor = false) { var n = columns.Length; if (n != _columns) Resize(n); for (var i = 0; i < n; i++) throttle[i] = 0; + if (forwardFloor) move = new float2(0, 1); + for (var i = 0; i < n; i++) _lo[i] = forwardFloor && columns[i].y > 0 ? 1f : 0f; // Nothing asked, nothing fired: that is a fact about the intent, decided before the solve. The solver's warm // start is left as the last solved point, which is where the next non-zero call resumes.