diff --git a/DriftCorrector.cs b/DriftCorrector.cs new file mode 100644 index 0000000..68cc43d --- /dev/null +++ b/DriftCorrector.cs @@ -0,0 +1,151 @@ +using Il2CppFormulaBase; +using MelonLoader; +using UnityEngine; + +namespace Cinema +{ + internal static class DriftCorrector + { + // higher == more precise, but may have lower performance + private const float CorrectionPrecision = 1f; + private const int ForceSetThreshold = 20; + private const float DisableCoefficient = 1.1f; + private static StageBattleComponent _battleComponent; + private static bool _isInit; + private static readonly float[] DeltaSamples = new float[50]; + + private static int _deltaSampleIndex; + private static SpeedState _currentSpeed = SpeedState.Normal; + + /// + /// In case another mod wants to change the playback speed (such as PracticeMod), + ///
+ /// we store the original playback speed and use it in our calculations. + ///
+ private static float _originalPlaybackSpeed = 1f; + + private static double _lastUpdateTime; + + internal static void Init() + { + if (_isInit) return; + InitAverageDelta(); + _isInit = true; + } + + internal static void Stop() + { + var del = LateUpdate; + if (MelonEvents.OnLateUpdate.CheckIfSubscribed(del.Method)) + { + MelonEvents.OnLateUpdate.Unsubscribe(LateUpdate); + } + + if (Main.Player is not null) Main.Player.playbackSpeed = _originalPlaybackSpeed; + } + + internal static void Run() + { + _originalPlaybackSpeed = Main.Player.playbackSpeed; + _lastUpdateTime = 0; + _battleComponent = StageBattleComponent.instance; + _currentSpeed = SpeedState.Normal; + MelonEvents.OnLateUpdate.Subscribe(LateUpdate); + } + + private static void CalculateAverageDelta() + { + DeltaSamples[_deltaSampleIndex++] = Time.deltaTime; + if (_deltaSampleIndex == DeltaSamples.Length) _deltaSampleIndex = 0; + } + + private static void InitAverageDelta() + { + CalculateAverageDelta(); + for (var i = 1; i < DeltaSamples.Length; i++) DeltaSamples[i] = DeltaSamples[0]; + } + + /// + /// Calculates playback speed using the given parameters + /// + private static float CalculateSpeed(float deltaTime, float drift) + { + var averageScaled = deltaTime / 10; + var relativeDrift = Math.Abs(drift) + 1; + var speed = Math.Min((1 + averageScaled) * relativeDrift, 1.05f); + if (drift > 0) speed = 2 - speed; + ; + return speed * _originalPlaybackSpeed; + } + + private static void LateUpdate() + { + if (Main.Player is null || Math.Abs(Main.Player.time - _lastUpdateTime) < 0.000001) return; + if (Main.Player.time >= Main.Player.length) + { + Stop(); + return; + } + + CalculateAverageDelta(); + _lastUpdateTime = Main.Player.time; + + var averageDelta = DeltaSamples.Average(); + var drift = (float)(_lastUpdateTime - _battleComponent.timeFromMusicStart); + if (Math.Abs(drift) > averageDelta * ForceSetThreshold) + { + // If the game freezes, (e.g. extreme drift) instantly set the player's time, + // instead of slightly speeding up, like when we correct small drifts. + // This correction may cause a few Update loops worth of drift, + // but is preferable to a multiple second drift. + var correct = _battleComponent.timeFromMusicStart + averageDelta * ForceSetThreshold; + Main.Player.time = correct; + return; + } + + var maxDifference = averageDelta / CorrectionPrecision; + var disableThreshold = maxDifference / DisableCoefficient; + switch (_currentSpeed) + { + case SpeedState.Slow: + if (drift > disableThreshold) return; + break; + case SpeedState.Fast: + if (drift < -disableThreshold) return; + break; + case SpeedState.Normal: + break; + default: + throw new ArgumentOutOfRangeException(); + } + + if (drift < -maxDifference) + { + _currentSpeed = SpeedState.Fast; + var speed = CalculateSpeed(averageDelta, drift); + Main.Player.playbackSpeed = speed; + return; + } + + if (drift > maxDifference) + { + _currentSpeed = SpeedState.Slow; + var speed = CalculateSpeed(averageDelta, drift); + Main.Player.playbackSpeed = speed; + return; + } + + if (_currentSpeed is SpeedState.Normal) return; + + Main.Player.playbackSpeed = _originalPlaybackSpeed; + _currentSpeed = SpeedState.Normal; + } + + private enum SpeedState + { + Slow, + Normal, + Fast + } + } +} \ No newline at end of file diff --git a/Main.cs b/Main.cs index bd960f7..deabb9c 100644 --- a/Main.cs +++ b/Main.cs @@ -13,9 +13,15 @@ public class Main : MelonMod internal static VideoPlayer Player; internal static bool Christmas; + public override void OnSceneWasLoaded(int buildIndex, string sceneName) + { + DriftCorrector.Stop(); + } + public override void OnInitializeMelon() { base.OnInitializeMelon(); + DriftCorrector.Init(); // Clean up old file from v1.1.x and below if (File.Exists(Application.persistentDataPath + "/cinema.mp4")) @@ -56,6 +62,8 @@ internal static void InitCamera(float opacity) Player.audioOutputMode = VideoAudioOutputMode.None; Player.aspectRatio = VideoAspectRatio.FitOutside; Player.url = Application.dataPath + "/cinema.mp4"; + Player.skipOnDrop = true; + Player.Prepare(); } internal static void HideSceneElements() @@ -65,8 +73,6 @@ internal static void HideSceneElements() : $"scene_0{SceneChangeController.curScene}"; if (Christmas && sceneName == "scene_05") sceneName += "_christmas"; - MelonLogger.Msg("SCENE TO HIDE: " + sceneName); - var sceneObject = GameObject.Find("SceneObjectController").transform.Find(sceneName); if (sceneObject == null) return; diff --git a/Patches/GameStartPatch.cs b/Patches/GameStartPatch.cs index 7120bc0..276be9d 100644 --- a/Patches/GameStartPatch.cs +++ b/Patches/GameStartPatch.cs @@ -12,6 +12,7 @@ private static void Postfix() Main.GameStarted = true; Main.Player.Play(); + DriftCorrector.Run(); } } } \ No newline at end of file diff --git a/Patches/RestartPatch.cs b/Patches/RestartPatch.cs index e13288f..229dfdc 100644 --- a/Patches/RestartPatch.cs +++ b/Patches/RestartPatch.cs @@ -11,6 +11,7 @@ private static void Postfix() if (Main.Player == null) return; Main.GameStarted = false; + DriftCorrector.Stop(); } } } \ No newline at end of file