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