Merge eureka/aetheria-release-sim-speed: one SimClock steps the zone, presenters draw ahead by Lead

AetherDrive.cs: master deleted the behaviour (2cfdc728); the branch only added a
TurnRate line to it. Deletion kept. Thruster remains the one rotation actuator
and feeds Entity.TurnRate. No sim stepping outside the clock on master
(mines presenter, Pirates AI, lot-product all step through Zone.Update).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Meta Vens 2026-10-09 02:29:37 +02:00
commit f28d4b4b12
10 changed files with 508 additions and 38 deletions

View file

@ -125,8 +125,9 @@ public class Thruster : Behavior, IAnalogBehavior, IPowerConsumer
{
var thrust = Thrust;
Entity.Velocity -= Direction.xz * _input * thrust / Entity.Mass * dt;
Entity.Direction = mul(Entity.Direction,
CultMath.float2x2.Rotate(_input * Torque * thrust * ItemManager.GameplaySettings.TorqueMultiplier / Entity.Mass * dt));
var turnRate = _input * Torque * thrust * ItemManager.GameplaySettings.TorqueMultiplier / Entity.Mass;
Entity.Direction = mul(Entity.Direction, CultMath.float2x2.Rotate(turnRate * dt));
Entity.TurnRate += turnRate;
AddHeat(_input * Evaluate(_data.Heat) * dt);
var vis = _input * Evaluate(_data.Visibility);
if (!Entity.VisibilitySources.ContainsKey(this) || vis > Entity.VisibilitySources[this])

View file

@ -0,0 +1,40 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
using CultMath;
using static CultMath.math;
// Where a presenter draws the latest sim state when the frame falls Lead sim seconds after the last step. Pure reads
// of the simulation for presenters (never called by ServerShared): the sim owns every pose and no presenter stores
// one, a previous state or a velocity of its own.
public static class DrawAhead
{
// A ship in a wormhole animation has its pose scripted by the step, not integrated from Velocity and TurnRate,
// so it is drawn where the step put it: extrapolating a scripted pose draws a streak that resets every step.
public static float3 Position(Entity entity, float lead) =>
entity is Ship { WormholeAnimationInProgress: true }
? entity.Position
: entity.Position + float3(entity.Velocity.x, 0, entity.Velocity.y) * lead;
// The ship's rotation turned about its own up axis by the rate its last step turned, for lead seconds.
public static quaternion Rotation(Ship ship, float lead) =>
ship.WormholeAnimationInProgress ? ship.Rotation : Turned(ship.Rotation, ship.TurnRate * lead);
// q turned about its own up axis by the angle, in the sense Direction turns for a positive TurnRate.
public static quaternion Turned(quaternion q, float angle)
{
var u = float3(q.x, q.y, q.z);
var up = float3(0, 1, 0);
up += 2 * (q.w * cross(u, up) + cross(u, cross(u, up)));
var half = angle * .5f;
var s = sin(half);
var t = new quaternion(up.x * s, up.y * s, up.z * s, cos(half));
// Hamilton product t * q: CultMath carries no quaternion product (a gap in CultMath, not filled here).
return normalize(new quaternion(
t.w * q.x + t.x * q.w + t.y * q.z - t.z * q.y,
t.w * q.y - t.x * q.z + t.y * q.w + t.z * q.x,
t.w * q.z + t.x * q.y - t.y * q.x + t.z * q.w,
t.w * q.w - t.x * q.x - t.y * q.y - t.z * q.z));
}
}

View file

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 007fe4255cd84c26a010151a399f45c2
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -27,6 +27,10 @@ public abstract class Entity
public float3 Position;
public float2 Direction = float2(0,1);
public float2 Velocity;
// Rad per sim second, signed as the step turns Direction: the sum of this step's rotation actuators' applied
// angle over the step. Runtime only, zeroed at the start of Update, so a staged, loaded or wormhole write to
// Direction is never a turn. Read only by presenters that draw ahead of the step.
public float TurnRate;
// Runtime only, never saved; Entity.Update is the one writer of both. Acceleration is the planar velocity change
// across the base update over its delta (thrust, not the drag or gravity a subclass applies before calling it),
@ -1359,6 +1363,7 @@ public abstract class Entity
public virtual void Update(float delta)
{
TurnRate = 0;
if (!_active) return;
var velocityBefore = Velocity;

View file

@ -0,0 +1,68 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
using System;
// The one relation between simulation time and real time. Real seconds go in; whole fixed steps come out, each
// handed to the caller's step action with the same Step. Nothing else in the game converts frame time to sim time,
// and nothing integrates a sim quantity on this clock: presenters read Lead to draw ahead of the latest step
// (DrawAhead) and decide no sim fact.
public sealed class SimClock
{
// The longest real time one frame may feed. A hitch (a load, a breakpoint) is a slow frame, not a burst of
// catch-up steps.
public const float MaxFrameSeconds = .1f;
private readonly double _period;
private double _accumulator;
private bool _advancing;
public SimClock(float step, float stepsPerRealSecond = 60)
{
if (!(step > 0)) throw new ArgumentOutOfRangeException(nameof(step));
if (!(stepsPerRealSecond > 0)) throw new ArgumentOutOfRangeException(nameof(stepsPerRealSecond));
Step = step;
StepsPerRealSecond = stepsPerRealSecond;
_period = 1.0 / stepsPerRealSecond;
}
// Sim seconds per step, fixed for the clock's life.
public float Step { get; }
public float StepsPerRealSecond { get; }
public long Steps { get; private set; }
// Sim seconds since the last step, 0 <= Lead < Step.
public float Lead => (float) (_accumulator * StepsPerRealSecond * Step);
// Forgets the real time paid toward the next step (a new zone starts between steps). A step cannot do this to the
// clock that is running it.
public void Reset()
{
if (_advancing) throw new InvalidOperationException("A step cannot reset the clock that is running it.");
_accumulator = 0;
}
// Feeds real seconds (negative counts as none, one frame counts at most MaxFrameSeconds) and runs the whole
// steps they pay for. Returns how many ran.
public int Advance(float realSeconds, Action<float> step)
{
_accumulator += Math.Min(Math.Max(realSeconds, 0f), MaxFrameSeconds);
var ran = 0;
_advancing = true;
try
{
while (_accumulator >= _period)
{
_accumulator -= _period;
step(Step);
Steps++;
ran++;
}
}
finally { _advancing = false; }
return ran;
}
}

View file

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 7a9e3edac65f4a8e8313bdbfe69ccebd
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: