qstbak/LLib/LLib.Movement.Unstuck/RecoveryLadder.cs
2026-08-17 20:29:32 +10:00

103 lines
2.5 KiB
C#

using System.Collections.Generic;
using System.Linq;
namespace LLib.Movement.Unstuck;
public sealed class RecoveryLadder
{
private readonly IRecoveryRung[] _rungs;
private readonly long[] _lastRunMs;
private readonly bool[] _hasRun;
private readonly List<string> _rungsFired;
private bool _hasRunAny;
private long _lastRunAnyMs;
private long _firstStallMs;
private long _lastStallMs;
public int Attempts { get; private set; }
public bool IsExhausted { get; private set; }
private int Cap => _003Coptions_003EP.Mode switch
{
RecoveryMode.Aggressive => _003Coptions_003EP.AggressiveCap,
RecoveryMode.Passive => _003Coptions_003EP.PassiveCap,
_ => _003Coptions_003EP.ConservativeCap,
};
public RecoveryLadder(RecoveryLadderOptions options, IReadOnlyList<IRecoveryRung> rungs)
{
_003Coptions_003EP = options;
_rungs = rungs.ToArray();
_lastRunMs = new long[rungs.Count];
_hasRun = new bool[rungs.Count];
_rungsFired = new List<string>();
base._002Ector();
}
public RecoveryDecision OnStall(long nowMs)
{
if (IsExhausted)
{
return new RecoveryDecision(RecoveryDecisionKind.None, null, null);
}
if (Attempts >= Cap)
{
IsExhausted = true;
return new RecoveryDecision(RecoveryDecisionKind.Exhausted, null, new RecoveryExhaustion(Attempts, _rungsFired.ToArray(), _firstStallMs, _lastStallMs));
}
if (_hasRunAny && nowMs - _lastRunAnyMs < _003Coptions_003EP.GlobalCooldownMs)
{
return new RecoveryDecision(RecoveryDecisionKind.None, null, null);
}
if (_003Coptions_003EP.Mode == RecoveryMode.Passive)
{
Consume(nowMs);
return new RecoveryDecision(RecoveryDecisionKind.Observe, null, null);
}
for (int i = 0; i < _rungs.Length; i++)
{
IRecoveryRung recoveryRung = _rungs[i];
if ((!_hasRun[i] || nowMs - _lastRunMs[i] >= recoveryRung.CooldownMs) && recoveryRung.CanRun(Attempts + 1, nowMs))
{
Consume(nowMs);
_rungsFired.Add(recoveryRung.Name);
_hasRun[i] = true;
_lastRunMs[i] = nowMs;
_hasRunAny = true;
_lastRunAnyMs = nowMs;
return new RecoveryDecision(RecoveryDecisionKind.Run, recoveryRung, null);
}
}
return new RecoveryDecision(RecoveryDecisionKind.None, null, null);
}
public void Reset()
{
Attempts = 0;
IsExhausted = false;
_rungsFired.Clear();
_hasRunAny = false;
for (int i = 0; i < _hasRun.Length; i++)
{
_hasRun[i] = false;
}
}
private void Consume(long nowMs)
{
if (Attempts == 0)
{
_firstStallMs = nowMs;
}
Attempts++;
_lastStallMs = nowMs;
}
}