using System;
using System.Diagnostics;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using MelonLoader;
using MelonLoader.Preferences;
using PhaseSteer;
using UnityEngine;
[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)]
[assembly: MelonInfo(typeof(Core), "PhaseSteer", "1.1.0", "you", null)]
[assembly: TargetFramework(".NETCoreApp,Version=v6.0", FrameworkDisplayName = "")]
[assembly: AssemblyVersion("0.0.0.0")]
namespace PhaseSteer;
public class Core : MelonMod
{
private MelonPreferences_Entry<bool> _prefEnabled;
private MelonPreferences_Entry<float> _prefKp;
private MelonPreferences_Entry<float> _prefMaxPercent;
private MelonPreferences_Entry<float> _prefTarget;
private MelonPreferences_Entry<int> _prefToggleKey;
private MelonPreferences_Entry<int> _prefSummarySeconds;
private readonly PhaseSteerController _ctl = new PhaseSteerController();
private bool _steerOn = true;
private bool _prevKey;
private int _stepsThisFrame;
private PhaseSteerController.Phase _lastMode;
private long _onFrames;
private long _onAnom;
private long _offFrames;
private long _offAnom;
private double _phaseSum;
private double _phaseMin = 9.0;
private double _phaseMax = -9.0;
private double _dtMin = double.MaxValue;
private double _dtMax;
private long _phaseN;
private long _anomGlitch;
private long _anomEdge;
private long _anomOther;
private long _late;
private long _stalls;
private long _paceFrames;
private bool _prevGlitchy;
private double _prevPhase = 0.5;
private float _nextSummary;
[DllImport("user32.dll")]
private static extern short GetAsyncKeyState(int vKey);
public override void OnInitializeMelon()
{
MelonPreferences_Category val = MelonPreferences.CreateCategory("PhaseSteer");
_prefEnabled = val.CreateEntry<bool>("Enabled", true, (string)null, (string)null, false, false, (ValueValidator)null, (string)null);
_prefKp = val.CreateEntry<float>("Kp", 0.01f, (string)null, (string)null, false, false, (ValueValidator)null, (string)null);
_prefMaxPercent = val.CreateEntry<float>("MaxAdjustPercent", 1f, (string)null, (string)null, false, false, (ValueValidator)null, (string)null);
_prefTarget = val.CreateEntry<float>("TargetPhase", 0.3f, (string)null, (string)null, false, false, (ValueValidator)null, (string)null);
_prefToggleKey = val.CreateEntry<int>("ToggleKey", 121, (string)null, (string)null, false, false, (ValueValidator)null, (string)null);
_prefSummarySeconds = val.CreateEntry<int>("SummarySeconds", 60, (string)null, (string)null, false, false, (ValueValidator)null, (string)null);
_steerOn = _prefEnabled.Value;
_ctl.Kp = Math.Max(0.0, _prefKp.Value);
_ctl.MaxAdjust = Math.Min(0.05, Math.Max(0.0, (double)_prefMaxPercent.Value / 100.0));
_ctl.TargetPhase = Math.Min(0.9, Math.Max(0.1, _prefTarget.Value));
((MelonBase)this).LoggerInstance.Msg("PhaseSteer loaded. Steering " + (_steerOn ? "ON" : "OFF") + " (Kp=" + _ctl.Kp + ", max +/-" + _ctl.MaxAdjust * 100.0 + "%, hotkey vk=" + _prefToggleKey.Value + ").");
_nextSummary = Time.realtimeSinceStartup + (float)Math.Max(10, _prefSummarySeconds.Value);
}
public override void OnFixedUpdate()
{
_stepsThisFrame++;
}
public override void OnLateUpdate()
{
PollHotkey();
PhaseSteerController.FrameInput f = new PhaseSteerController.FrameInput
{
Time = Time.timeAsDouble,
FixedTime = Time.fixedTimeAsDouble,
FixedDt = Time.fixedDeltaTime,
DeltaTime = Time.deltaTime,
UnscaledDt = Time.unscaledDeltaTime,
TimeScale = Time.timeScale
};
if (_ctl.Step(in f, _steerOn, out var newFixedDt))
{
Time.fixedDeltaTime = (float)newFixedDt;
}
if (_ctl.Mode != _lastMode)
{
if (_ctl.Mode == PhaseSteerController.Phase.Steering)
{
((MelonBase)this).LoggerInstance.Msg("Steering engaged (physics steps per frame = " + _ctl.Ratio + ", base step " + (_ctl.BaseDt * 1000.0).ToString("F4") + " ms).");
}
else if (_ctl.Mode == PhaseSteerController.Phase.Disabled)
{
((MelonBase)this).LoggerInstance.Warning("Steering disabled for safety: " + _ctl.DisabledReason);
}
else if (_lastMode == PhaseSteerController.Phase.Steering)
{
((MelonBase)this).LoggerInstance.Msg("Steering paused (physics step and frame period no longer match).");
}
_lastMode = _ctl.Mode;
}
if (_ctl.FrameEma > 0.0 && f.DeltaTime > 0.0 && f.TimeScale > 0.0)
{
double num = f.UnscaledDt / _ctl.FrameEma;
if (num > 1.15)
{
_late++;
}
if (num > 2.0)
{
_stalls++;
}
_paceFrames++;
}
if (_ctl.Mode == PhaseSteerController.Phase.Steering && _ctl.FrameEma > 0.0 && f.DeltaTime > 0.0 && f.TimeScale > 0.0 && f.UnscaledDt > 0.5 * _ctl.FrameEma && f.UnscaledDt < 1.5 * _ctl.FrameEma)
{
bool flag = _stepsThisFrame != _ctl.Ratio;
bool flag2 = Math.Abs(f.UnscaledDt / _ctl.FrameEma - 1.0) > 0.12;
if (_steerOn)
{
_onFrames++;
if (flag)
{
_onAnom++;
double num2 = Math.Min(_prevPhase, 1.0 - _prevPhase);
if (flag2 || _prevGlitchy)
{
_anomGlitch++;
}
else if (num2 < 0.15)
{
_anomEdge++;
}
else
{
_anomOther++;
}
}
}
else
{
_offFrames++;
if (flag)
{
_offAnom++;
}
}
_phaseSum += _ctl.LastPhase;
if (_ctl.LastPhase < _phaseMin)
{
_phaseMin = _ctl.LastPhase;
}
if (_ctl.LastPhase > _phaseMax)
{
_phaseMax = _ctl.LastPhase;
}
_phaseN++;
double fixedDt = f.FixedDt;
if (fixedDt < _dtMin)
{
_dtMin = fixedDt;
}
if (fixedDt > _dtMax)
{
_dtMax = fixedDt;
}
_prevGlitchy = flag2;
_prevPhase = _ctl.LastPhase;
}
_stepsThisFrame = 0;
if (Time.realtimeSinceStartup >= _nextSummary)
{
WriteSummary();
_nextSummary = Time.realtimeSinceStartup + (float)Math.Max(10, _prefSummarySeconds.Value);
}
}
private void PollHotkey()
{
int value = _prefToggleKey.Value;
if (value <= 0)
{
return;
}
bool flag = (GetAsyncKeyState(value) & 0x8000) != 0;
if (flag && !_prevKey)
{
_steerOn = !_steerOn;
if (!_steerOn && _ctl.Release(Time.fixedDeltaTime, out var restoreDt))
{
Time.fixedDeltaTime = (float)restoreDt;
}
((MelonBase)this).LoggerInstance.Msg("Steering " + (_steerOn ? "ON" : "OFF") + " (hotkey).");
}
_prevKey = flag;
}
private void WriteSummary()
{
string text = ((_onFrames > 0) ? ((1000.0 * (double)_onAnom / (double)_onFrames).ToString("F2") + "/1000 over " + _onFrames + " frames") : "no data");
string text2 = ((_offFrames > 0) ? ((1000.0 * (double)_offAnom / (double)_offFrames).ToString("F2") + "/1000 over " + _offFrames + " frames") : "no data");
string text3 = ((_phaseN > 0) ? ("phase mean " + (_phaseSum / (double)_phaseN).ToString("F2") + " [" + _phaseMin.ToString("F2") + ".." + _phaseMax.ToString("F2") + "]") : "phase n/a");
string text4 = ((_dtMax > 0.0) ? ("step " + (_dtMin * 1000.0).ToString("F4") + ".." + (_dtMax * 1000.0).ToString("F4") + " ms") : "step n/a");
((MelonBase)this).LoggerInstance.Msg("[summary] mode=" + _ctl.Mode.ToString() + " | steering ON: " + text + " | OFF: " + text2 + " | " + text3 + " | " + text4 + " | ON anomalies by cause: late/early frame " + _anomGlitch + ", near boundary " + _anomEdge + ", other " + _anomOther + " | pacing: late frames (>15%) " + _late + ", stalls (>2x) " + _stalls + " of " + _paceFrames + " frames");
_onFrames = (_onAnom = (_offFrames = (_offAnom = 0L)));
_anomGlitch = (_anomEdge = (_anomOther = (_late = (_stalls = (_paceFrames = 0L)))));
_phaseSum = 0.0;
_phaseN = 0L;
_phaseMin = 9.0;
_phaseMax = -9.0;
_dtMin = double.MaxValue;
_dtMax = 0.0;
}
}
internal sealed class PhaseSteerController
{
public struct FrameInput
{
public double Time;
public double FixedTime;
public double FixedDt;
public double DeltaTime;
public double UnscaledDt;
public double TimeScale;
}
public enum Phase
{
Idle,
Calibrating,
Steering,
Disabled
}
public double Kp = 0.01;
public double MaxAdjust = 0.01;
public double TargetPhase = 0.3;
public double RatioTolerance = 0.03;
public int MaxRatio = 4;
public int EngageAfterFrames = 120;
public int CalibrationFrames = 600;
private bool _haveBase;
private double _lastWritten;
private int _outliers;
private int _stable;
private int _calibCount;
private readonly int[] _inRange = new int[3];
private static readonly double[] Offsets = new double[3] { 0.0, 1.0, -1.0 };
private double _phaseOffset;
private bool _calibrated;
private int _monitorCount;
private int _monitorBad;
public Phase Mode { get; private set; }
public string DisabledReason { get; private set; }
public double BaseDt { get; private set; }
public double LastPhase { get; private set; }
public double LastAdjust { get; private set; }
public int Ratio { get; private set; } = 1;
public double FrameEma { get; private set; }
public bool Step(in FrameInput f, bool apply, out double newFixedDt)
{
newFixedDt = f.FixedDt;
if (Mode == Phase.Disabled)
{
return false;
}
if (!(f.FixedDt > 1E-05) || !(f.DeltaTime > 0.0) || !(f.UnscaledDt > 0.0) || !(f.TimeScale > 0.0))
{
return false;
}
if (!_haveBase || Math.Abs(f.FixedDt - _lastWritten) > 1E-09 + 1E-06 * _lastWritten)
{
BaseDt = f.FixedDt;
_lastWritten = f.FixedDt;
_haveBase = true;
}
UpdateFrameEma(f.UnscaledDt);
if (FrameEma <= 0.0)
{
return false;
}
double num = FrameEma * f.TimeScale / BaseDt;
double num2 = Math.Round(num);
if (!(num2 >= 1.0) || !(num2 <= (double)MaxRatio) || !(Math.Abs(num - num2) <= RatioTolerance))
{
_stable = 0;
bool num3 = Mode != Phase.Idle;
Mode = Phase.Idle;
_calibCount = 0;
Array.Clear(_inRange, 0, _inRange.Length);
if (num3 && apply && Math.Abs(f.FixedDt - BaseDt) > 1E-12)
{
_lastWritten = BaseDt;
newFixedDt = BaseDt;
return true;
}
return false;
}
Ratio = (int)num2;
if (_stable < int.MaxValue)
{
_stable++;
}
if (_stable < EngageAfterFrames)
{
return false;
}
double num4 = (f.Time - f.FixedTime) / f.FixedDt;
if (Mode == Phase.Idle)
{
if (_calibrated)
{
Mode = Phase.Steering;
_monitorCount = 0;
_monitorBad = 0;
}
else
{
Mode = Phase.Calibrating;
_calibCount = 0;
Array.Clear(_inRange, 0, _inRange.Length);
}
}
if (Mode == Phase.Calibrating)
{
_calibCount++;
for (int i = 0; i < Offsets.Length; i++)
{
double num5 = num4 + Offsets[i];
if (num5 >= -0.02 && num5 <= 1.02)
{
_inRange[i]++;
}
}
if (_calibCount >= CalibrationFrames)
{
int num6 = 0;
for (int j = 1; j < Offsets.Length; j++)
{
if (_inRange[j] > _inRange[num6])
{
num6 = j;
}
}
double num7 = (double)_inRange[num6] / (double)_calibCount;
if (num7 >= 0.97)
{
_phaseOffset = Offsets[num6];
_calibrated = true;
Mode = Phase.Steering;
_monitorCount = 0;
_monitorBad = 0;
}
else
{
Disable("phase model does not match this game (only " + (int)(num7 * 100.0) + "% of frames had phase in [0,1))");
}
}
return false;
}
num4 = (LastPhase = num4 + _phaseOffset);
_monitorCount++;
if (num4 < -0.05 || num4 > 1.05)
{
_monitorBad++;
}
if (_monitorCount >= 600)
{
if ((double)_monitorBad > 0.2 * (double)_monitorCount)
{
Disable("phase left the expected range too often (" + _monitorBad + " of " + _monitorCount + " frames)");
if (apply && Math.Abs(f.FixedDt - BaseDt) > 1E-12)
{
_lastWritten = BaseDt;
newFixedDt = BaseDt;
return true;
}
return false;
}
_monitorCount = 0;
_monitorBad = 0;
}
if (num4 < -0.5 || num4 > 1.5)
{
return false;
}
if (!apply)
{
LastAdjust = 0.0;
return false;
}
double num9 = Kp * (num4 - TargetPhase);
if (num9 > MaxAdjust)
{
num9 = MaxAdjust;
}
else if (num9 < 0.0 - MaxAdjust)
{
num9 = 0.0 - MaxAdjust;
}
LastAdjust = num9;
float num10 = (float)(BaseDt * (1.0 + num9));
_lastWritten = num10;
newFixedDt = num10;
return true;
}
public bool Release(double currentFixedDt, out double restoreDt)
{
restoreDt = currentFixedDt;
if (_haveBase && Math.Abs(currentFixedDt - BaseDt) > 1E-12)
{
_lastWritten = BaseDt;
restoreDt = BaseDt;
LastAdjust = 0.0;
return true;
}
return false;
}
private void Disable(string reason)
{
Mode = Phase.Disabled;
DisabledReason = reason;
}
private void UpdateFrameEma(double unscaledDt)
{
if (FrameEma <= 0.0)
{
FrameEma = unscaledDt;
_outliers = 0;
}
else if (unscaledDt > 0.85 * FrameEma && unscaledDt < 1.15 * FrameEma)
{
FrameEma += 0.02 * (unscaledDt - FrameEma);
_outliers = 0;
}
else if (++_outliers >= 30)
{
if (unscaledDt >= 1.0 / 360.0 && unscaledDt <= 1.0 / 24.0)
{
FrameEma = unscaledDt;
}
_outliers = 0;
}
}
}