File size: 2,161 Bytes
3ce1a40
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
namespace Repoverse.Core.Simulation;

/// <summary>
/// Fixed-step simulation time, decoupled from render frames (§31). The host calls
/// <see cref="Advance"/> with real frame time; the clock runs zero or more fixed ticks
/// scaled by <see cref="Speed"/>, and fires scheduled actions in tick order.
/// </summary>
public sealed class SimulationClock(double ticksPerSecond = WorldConstants.SimulationTicksPerSecond, int maxTicksPerAdvance = 16)
{
    private readonly PriorityQueue<(long Tick, long Seq, Action Action), (long, long)> _scheduled = new();
    private double _accumulator;
    private long _seq;

    public long Tick { get; private set; }
    public double Speed { get; set; } = 1.0;
    public bool Paused { get; set; }
    public TimeSpan TickLength => TimeSpan.FromSeconds(1 / ticksPerSecond);
    public TimeSpan SimulatedTime => TickLength * Tick;

    /// <summary>Raised once per fixed tick, after that tick's scheduled actions.</summary>
    public event Action<long>? Ticked;

    public void Schedule(long ticksFromNow, Action action)
    {
        ArgumentOutOfRangeException.ThrowIfNegative(ticksFromNow);
        var at = Tick + ticksFromNow;
        _scheduled.Enqueue((at, _seq, action), (at, _seq++));
    }

    /// <returns>Number of ticks run.</returns>
    public int Advance(TimeSpan realElapsed)
    {
        if (Paused || Speed <= 0) return 0;
        _accumulator += realElapsed.TotalSeconds * Speed * ticksPerSecond;
        int ran = 0;
        while (_accumulator >= 1 && ran < maxTicksPerAdvance)
        {
            _accumulator -= 1;
            Step();
            ran++;
        }
        // Spiral-of-death guard: drop backlog rather than stall the frame.
        if (ran == maxTicksPerAdvance) _accumulator = Math.Min(_accumulator, 1);
        return ran;
    }

    /// <summary>Runs exactly one tick regardless of pause; used for deterministic stepping.</summary>
    public void Step()
    {
        Tick++;
        while (_scheduled.TryPeek(out var item, out _) && item.Tick <= Tick)
        {
            _scheduled.Dequeue();
            item.Action();
        }
        Ticked?.Invoke(Tick);
    }
}