Skip to content

Commit 3be85de

Browse files
authored
Implement drift correction (#3)
Makes cinema mod automatically correct drift, making the background stay in sync and letting charters to get a bit more creative
1 parent 345b8d6 commit 3be85de

File tree

4 files changed

+161
-2
lines changed

4 files changed

+161
-2
lines changed

DriftCorrector.cs

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
using Il2CppFormulaBase;
2+
using MelonLoader;
3+
using UnityEngine;
4+
5+
namespace Cinema
6+
{
7+
internal static class DriftCorrector
8+
{
9+
// higher == more precise, but may have lower performance
10+
private const float CorrectionPrecision = 1f;
11+
private const int ForceSetThreshold = 20;
12+
private const float DisableCoefficient = 1.1f;
13+
private static StageBattleComponent _battleComponent;
14+
private static bool _isInit;
15+
private static readonly float[] DeltaSamples = new float[50];
16+
17+
private static int _deltaSampleIndex;
18+
private static SpeedState _currentSpeed = SpeedState.Normal;
19+
20+
/// <summary>
21+
/// In case another mod wants to change the playback speed (such as PracticeMod),
22+
/// <br />
23+
/// we store the original playback speed and use it in our calculations.
24+
/// </summary>
25+
private static float _originalPlaybackSpeed = 1f;
26+
27+
private static double _lastUpdateTime;
28+
29+
internal static void Init()
30+
{
31+
if (_isInit) return;
32+
InitAverageDelta();
33+
_isInit = true;
34+
}
35+
36+
internal static void Stop()
37+
{
38+
var del = LateUpdate;
39+
if (MelonEvents.OnLateUpdate.CheckIfSubscribed(del.Method))
40+
{
41+
MelonEvents.OnLateUpdate.Unsubscribe(LateUpdate);
42+
}
43+
44+
if (Main.Player is not null) Main.Player.playbackSpeed = _originalPlaybackSpeed;
45+
}
46+
47+
internal static void Run()
48+
{
49+
_originalPlaybackSpeed = Main.Player.playbackSpeed;
50+
_lastUpdateTime = 0;
51+
_battleComponent = StageBattleComponent.instance;
52+
_currentSpeed = SpeedState.Normal;
53+
MelonEvents.OnLateUpdate.Subscribe(LateUpdate);
54+
}
55+
56+
private static void CalculateAverageDelta()
57+
{
58+
DeltaSamples[_deltaSampleIndex++] = Time.deltaTime;
59+
if (_deltaSampleIndex == DeltaSamples.Length) _deltaSampleIndex = 0;
60+
}
61+
62+
private static void InitAverageDelta()
63+
{
64+
CalculateAverageDelta();
65+
for (var i = 1; i < DeltaSamples.Length; i++) DeltaSamples[i] = DeltaSamples[0];
66+
}
67+
68+
/// <summary>
69+
/// Calculates playback speed using the given parameters
70+
/// </summary>
71+
private static float CalculateSpeed(float deltaTime, float drift)
72+
{
73+
var averageScaled = deltaTime / 10;
74+
var relativeDrift = Math.Abs(drift) + 1;
75+
var speed = Math.Min((1 + averageScaled) * relativeDrift, 1.05f);
76+
if (drift > 0) speed = 2 - speed;
77+
;
78+
return speed * _originalPlaybackSpeed;
79+
}
80+
81+
private static void LateUpdate()
82+
{
83+
if (Main.Player is null || Math.Abs(Main.Player.time - _lastUpdateTime) < 0.000001) return;
84+
if (Main.Player.time >= Main.Player.length)
85+
{
86+
Stop();
87+
return;
88+
}
89+
90+
CalculateAverageDelta();
91+
_lastUpdateTime = Main.Player.time;
92+
93+
var averageDelta = DeltaSamples.Average();
94+
var drift = (float)(_lastUpdateTime - _battleComponent.timeFromMusicStart);
95+
if (Math.Abs(drift) > averageDelta * ForceSetThreshold)
96+
{
97+
// If the game freezes, (e.g. extreme drift) instantly set the player's time,
98+
// instead of slightly speeding up, like when we correct small drifts.
99+
// This correction may cause a few Update loops worth of drift,
100+
// but is preferable to a multiple second drift.
101+
var correct = _battleComponent.timeFromMusicStart + averageDelta * ForceSetThreshold;
102+
Main.Player.time = correct;
103+
return;
104+
}
105+
106+
var maxDifference = averageDelta / CorrectionPrecision;
107+
var disableThreshold = maxDifference / DisableCoefficient;
108+
switch (_currentSpeed)
109+
{
110+
case SpeedState.Slow:
111+
if (drift > disableThreshold) return;
112+
break;
113+
case SpeedState.Fast:
114+
if (drift < -disableThreshold) return;
115+
break;
116+
case SpeedState.Normal:
117+
break;
118+
default:
119+
throw new ArgumentOutOfRangeException();
120+
}
121+
122+
if (drift < -maxDifference)
123+
{
124+
_currentSpeed = SpeedState.Fast;
125+
var speed = CalculateSpeed(averageDelta, drift);
126+
Main.Player.playbackSpeed = speed;
127+
return;
128+
}
129+
130+
if (drift > maxDifference)
131+
{
132+
_currentSpeed = SpeedState.Slow;
133+
var speed = CalculateSpeed(averageDelta, drift);
134+
Main.Player.playbackSpeed = speed;
135+
return;
136+
}
137+
138+
if (_currentSpeed is SpeedState.Normal) return;
139+
140+
Main.Player.playbackSpeed = _originalPlaybackSpeed;
141+
_currentSpeed = SpeedState.Normal;
142+
}
143+
144+
private enum SpeedState
145+
{
146+
Slow,
147+
Normal,
148+
Fast
149+
}
150+
}
151+
}

Main.cs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,15 @@ public class Main : MelonMod
1313
internal static VideoPlayer Player;
1414
internal static bool Christmas;
1515

16+
public override void OnSceneWasLoaded(int buildIndex, string sceneName)
17+
{
18+
DriftCorrector.Stop();
19+
}
20+
1621
public override void OnInitializeMelon()
1722
{
1823
base.OnInitializeMelon();
24+
DriftCorrector.Init();
1925

2026
// Clean up old file from v1.1.x and below
2127
if (File.Exists(Application.persistentDataPath + "/cinema.mp4"))
@@ -56,6 +62,8 @@ internal static void InitCamera(float opacity)
5662
Player.audioOutputMode = VideoAudioOutputMode.None;
5763
Player.aspectRatio = VideoAspectRatio.FitOutside;
5864
Player.url = Application.dataPath + "/cinema.mp4";
65+
Player.skipOnDrop = true;
66+
Player.Prepare();
5967
}
6068

6169
internal static void HideSceneElements()
@@ -65,8 +73,6 @@ internal static void HideSceneElements()
6573
: $"scene_0{SceneChangeController.curScene}";
6674
if (Christmas && sceneName == "scene_05") sceneName += "_christmas";
6775

68-
MelonLogger.Msg("SCENE TO HIDE: " + sceneName);
69-
7076
var sceneObject = GameObject.Find("SceneObjectController").transform.Find(sceneName);
7177
if (sceneObject == null) return;
7278

Patches/GameStartPatch.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ private static void Postfix()
1212

1313
Main.GameStarted = true;
1414
Main.Player.Play();
15+
DriftCorrector.Run();
1516
}
1617
}
1718
}

Patches/RestartPatch.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ private static void Postfix()
1111
if (Main.Player == null) return;
1212

1313
Main.GameStarted = false;
14+
DriftCorrector.Stop();
1415
}
1516
}
1617
}

0 commit comments

Comments
 (0)