using System.Diagnostics; namespace AcDream.Core.Net.Transport; /// /// The transport's monotonic time authority: an injectable timestamp source /// (production default ) driving retail's /// 0.5-second interval counter. is the value retail /// writes into ProtoHeader::interval_ — our /// — on rebuilt resend headers. /// /// /// Retail oracle: ClientFlowQueue::IncrementLocalInterval @ 0x00547F10 /// (named-retail pseudo-C :338389) — the tail is /// CurLocalInterval_.intervalID_ += elapsedIntervals, advanced by the /// caller once per elapsed 0.5-s slice. Only the interval counter is in /// N1 scope: the same function's every-6-intervals TimeSync/EchoRequest /// (~3 s) and every-0xDC-intervals CICMD keepalive are campaign §5 /// deferrals (TS-58) — standalone-unsafe against ACE's watermark hole. /// /// /// /// One clock owns every transport gate (campaign §5 AP-126 — retail's /// cur/local clock split is immaterial to the gates we port). Single-threaded /// like the rest of the transport: runs only from the /// session sweep. /// /// internal sealed class TransportClock { private readonly Func _timestampSource; private readonly long _ticksPerInterval; private long _intervalBaseTimestamp; /// Timestamp ticks per second of the injected source. public long Frequency { get; } /// /// Retail's 0.5-s interval counter (CurLocalInterval_.intervalID_). /// Starts at 1; wraps with natural ushort arithmetic. /// public ushort IntervalId { get; private set; } public TransportClock( Func? timestampSource = null, long? frequency = null) { _timestampSource = timestampSource ?? Stopwatch.GetTimestamp; Frequency = frequency ?? Stopwatch.Frequency; if (Frequency < 2) { throw new ArgumentOutOfRangeException( nameof(frequency), "the interval clock needs at least 2 ticks per second"); } _ticksPerInterval = Frequency / 2; _intervalBaseTimestamp = _timestampSource(); IntervalId = 1; } /// Current raw timestamp from the injected source. public long GetTimestamp() => _timestampSource(); /// /// Advance by however many whole 0.5-s /// intervals have elapsed since the last update. Called once per sweep. /// public void Update() { long elapsed = _timestampSource() - _intervalBaseTimestamp; if (elapsed < _ticksPerInterval) return; long steps = elapsed / _ticksPerInterval; IntervalId = unchecked((ushort)(IntervalId + steps)); _intervalBaseTimestamp += steps * _ticksPerInterval; } }