Skip to content

Commit 64f7ea1

Browse files
committed
Migrate Harmony patches to MonoMod hooks
1 parent 30337db commit 64f7ea1

54 files changed

Lines changed: 1257 additions & 995 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
using System;
2+
using MonoMod.RuntimeDetour;
3+
using Zenject;
4+
5+
namespace SongCore.Hooks
6+
{
7+
/// <summary>
8+
/// Allows negative note jump speed, which is otherwise reset to the default NJS value.
9+
/// When a negative NJS is used, notes will come from behind the player and spin toward them.
10+
/// </summary>
11+
/// <example>
12+
/// https://beatsaver.com/maps/6cd
13+
/// </example>
14+
internal class AllowNegativeNoteJumpSpeedHook : IInitializable, IDisposable
15+
{
16+
private Hook _negativeNoteJumpSpeedHook = null!;
17+
18+
public void Initialize()
19+
{
20+
_negativeNoteJumpSpeedHook = new Hook(typeof(BeatmapDifficultyMethods).GetMethod(nameof(BeatmapDifficultyMethods.NoteJumpMovementSpeed))!, AllowNegativeJumpSpeed, true);
21+
}
22+
23+
public void Dispose()
24+
{
25+
_negativeNoteJumpSpeedHook.Dispose();
26+
}
27+
28+
private float AllowNegativeJumpSpeed(Func<BeatmapDifficulty, float, bool, float> original, BeatmapDifficulty difficulty, float noteJumpMovementSpeed, bool fastNotes)
29+
{
30+
return noteJumpMovementSpeed <= -VariableMovementDataProvider.kMinNoteJumpMovementSpeed ? noteJumpMovementSpeed : original(difficulty, noteJumpMovementSpeed, fastNotes);
31+
}
32+
}
33+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
using System;
2+
using System.Reflection;
3+
using Mono.Cecil.Cil;
4+
using MonoMod.Cil;
5+
using MonoMod.RuntimeDetour;
6+
using UnityEngine;
7+
using Zenject;
8+
9+
namespace SongCore.Hooks
10+
{
11+
/// <summary>
12+
/// By default, the provider uses the highest note jump speed value, capping it at <see cref="VariableMovementDataProvider.kMinNoteJumpMovementSpeed"/>.
13+
/// This allows it to also use the lowest NJS value when the initial one is negative, capping it at -<see cref="VariableMovementDataProvider.kMinNoteJumpMovementSpeed"/>.
14+
/// </summary>
15+
internal class AllowNegativeVariableNoteJumpSpeedHook : IInitializable, IDisposable
16+
{
17+
private readonly VariableMovementDataProvider _variableMovementDataProvider;
18+
19+
private ILHook _variableMovementDataProviderHook = null!;
20+
21+
private AllowNegativeVariableNoteJumpSpeedHook(VariableMovementDataProvider variableMovementDataProvider)
22+
{
23+
_variableMovementDataProvider = variableMovementDataProvider;
24+
}
25+
26+
public void Initialize()
27+
{
28+
_variableMovementDataProviderHook = new ILHook(typeof(VariableMovementDataProvider).GetMethod(nameof(VariableMovementDataProvider.ManualUpdate), BindingFlags.Instance | BindingFlags.NonPublic)!, ctx =>
29+
{
30+
var cursor = new ILCursor(ctx);
31+
cursor.RemoveRange(10);
32+
cursor.Emit(OpCodes.Ldarg_0);
33+
cursor.Emit(OpCodes.Ldarg_1);
34+
cursor.EmitDelegate(GetNoteJumpMovementSpeed);
35+
}, true);
36+
}
37+
38+
public void Dispose()
39+
{
40+
_variableMovementDataProviderHook.Dispose();
41+
}
42+
43+
private float GetNoteJumpMovementSpeed(float songTime)
44+
{
45+
var noteJumpSpeed = _variableMovementDataProvider._initNoteJumpMovementSpeed + _variableMovementDataProvider._relativeNoteJumpSpeedInterpolation.GetValue(songTime);
46+
return _variableMovementDataProvider._initNoteJumpMovementSpeed > 0
47+
? Mathf.Max(noteJumpSpeed, VariableMovementDataProvider.kMinNoteJumpMovementSpeed)
48+
: Mathf.Min(noteJumpSpeed, -VariableMovementDataProvider.kMinNoteJumpMovementSpeed);
49+
}
50+
}
51+
}

source/SongCore/Patches/BeatmapLevelCache/BeatmapDataCachePatches.cs renamed to source/SongCore/Hooks/BeatmapLevelCache/BeatmapDataCacheHooks.cs

Lines changed: 64 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,19 @@
22
using System.Collections.Generic;
33
using System.IO;
44
using System.Linq;
5+
using System.Reflection;
56
using System.Threading;
67
using System.Threading.Tasks;
78
using IPA.Utilities;
89
using ModestTree;
9-
using SiraUtil.Affinity;
10+
using MonoMod.RuntimeDetour;
1011
using SongCore.Utilities;
12+
using Zenject;
1113

12-
namespace SongCore.Patches.BeatmapLevelCache
14+
namespace SongCore.Hooks.BeatmapLevelCache
1315
{
1416
/// <summary>
15-
/// These patches implement a way to cache beatmap data when selecting levels for later use by the game or mods.
17+
/// This implements a way to cache beatmap data when selecting levels for later use by the game or mods.
1618
/// The execution flow is as follows:
1719
/// <list>
1820
/// <item><see cref="LevelCollectionTableView.didSelectLevelEvent"/> fires.</item>
@@ -29,25 +31,48 @@ namespace SongCore.Patches.BeatmapLevelCache
2931
/// </list>
3032
/// Ultimately, it ensures the cache is ready and prevents race conditions.
3133
/// </summary>
32-
internal class BeatmapDataCachePatches : IAffinity
34+
internal class BeatmapDataCacheHooks : IInitializable, IDisposable
3335
{
3436
private readonly CustomLevelLoader _customLevelLoader;
3537
private readonly BeatmapLevelsModel _beatmapLevelsModel;
3638
private readonly BeatmapLevelsEntitlementModel _beatmapLevelsEntitlementModel;
3739
private readonly BeatmapDataLoader _beatmapDataLoader;
3840
private readonly BeatmapLevelCache _beatmapLevelCache;
39-
private readonly EventProxyPatches _eventProxyPatches;
41+
private readonly EventProxyHooks _eventProxyHooks;
4042

43+
private Hook _readAllTextFromPathAsyncHook = null!;
44+
private Hook _replaceDidSelectLevelEventHook = null!;
45+
private Hook _replaceDidChangeContentEventHook = null!;
46+
private Hook _createbeatmapKeyHook = null!;
47+
private Hook _loadBeatmapDataAsyncHook = null!;
4148
private CancellationToken _cancellationToken;
4249

43-
private BeatmapDataCachePatches(CustomLevelLoader customLevelLoader, BeatmapLevelsModel beatmapLevelsModel, BeatmapLevelsEntitlementModel beatmapLevelsEntitlementModel, BeatmapDataLoader beatmapDataLoader, BeatmapLevelCache beatmapLevelCache, EventProxyPatches eventProxyPatches)
50+
private BeatmapDataCacheHooks(CustomLevelLoader customLevelLoader, BeatmapLevelsModel beatmapLevelsModel, BeatmapLevelsEntitlementModel beatmapLevelsEntitlementModel, BeatmapDataLoader beatmapDataLoader, BeatmapLevelCache beatmapLevelCache, EventProxyHooks eventProxyHooks)
4451
{
4552
_customLevelLoader = customLevelLoader;
4653
_beatmapLevelsModel = beatmapLevelsModel;
4754
_beatmapLevelsEntitlementModel = beatmapLevelsEntitlementModel;
4855
_beatmapDataLoader = beatmapDataLoader;
4956
_beatmapLevelCache = beatmapLevelCache;
50-
_eventProxyPatches = eventProxyPatches;
57+
_eventProxyHooks = eventProxyHooks;
58+
}
59+
60+
public void Initialize()
61+
{
62+
_readAllTextFromPathAsyncHook = new Hook(typeof(BeatmapLevelDataUtils).GetMethod(nameof(BeatmapLevelDataUtils.ReadAllTextFromPathAsync))!, LogReadAllTextFromPathAsync, true);
63+
_replaceDidSelectLevelEventHook = new Hook(typeof(LevelCollectionViewController).GetMethod(nameof(LevelCollectionViewController.DidActivate), BindingFlags.Instance | BindingFlags.NonPublic)!, ReplaceDidSelectLevelEvent, true);
64+
_replaceDidChangeContentEventHook = new Hook(typeof(StandardLevelDetailViewController).GetMethod(nameof(StandardLevelDetailViewController.DidActivate), BindingFlags.Instance | BindingFlags.NonPublic)!, ReplaceDidChangeContentEvent, true);
65+
_createbeatmapKeyHook = new Hook(typeof(StandardLevelDetailView).GetMethod(nameof(StandardLevelDetailView.CreateBeatmapKey), BindingFlags.Instance | BindingFlags.NonPublic)!, SetBeatmapLevelCacheBeatmapKey, true);
66+
_loadBeatmapDataAsyncHook = new Hook(typeof(BeatmapDataLoader).GetMethod(nameof(BeatmapDataLoader.LoadBeatmapDataAsync))!, LoadBeatmapDataWithCacheAsync, true);
67+
}
68+
69+
public void Dispose()
70+
{
71+
_readAllTextFromPathAsyncHook.Dispose();
72+
_replaceDidSelectLevelEventHook.Dispose();
73+
_replaceDidChangeContentEventHook.Dispose();
74+
_createbeatmapKeyHook.Dispose();
75+
_loadBeatmapDataAsyncHook.Dispose();
5176
}
5277

5378
private async Task<IBeatmapLevelData?> InitializeBeatmapLevelCacheAsync(BeatmapLevel beatmapLevel, CancellationToken cancellationToken)
@@ -130,7 +155,7 @@ private async void HandleDidSelectLevel(LevelCollectionTableView levelCollection
130155
_beatmapLevelCache.Init(beatmapLevel, InitializeBeatmapLevelCacheAsync);
131156
_cancellationToken = _beatmapLevelCache.CancellationTokenSource!.Token;
132157

133-
await InvokeEventAsync(levelCollectionTableView, beatmapLevel, _eventProxyPatches.LevelCollectionTableViewDidSelectLevelDelegate!, _cancellationToken);
158+
await InvokeEventAsync(levelCollectionTableView, beatmapLevel, _eventProxyHooks.LevelCollectionTableViewDidSelectLevelDelegate!, _cancellationToken);
134159
}
135160

136161
private async void HandleDidSelectLevel(LevelCollectionViewController levelCollectionViewController, BeatmapLevel beatmapLevel)
@@ -139,7 +164,7 @@ private async void HandleDidSelectLevel(LevelCollectionViewController levelColle
139164

140165
Assert.That(beatmapLevel == _beatmapLevelCache.BeatmapLevel);
141166

142-
await InvokeEventAsync(levelCollectionViewController, beatmapLevel, _eventProxyPatches.LevelCollectionViewControllerDidSelectLevelDelegate!, _cancellationToken);
167+
await InvokeEventAsync(levelCollectionViewController, beatmapLevel, _eventProxyHooks.LevelCollectionViewControllerDidSelectLevelDelegate!, _cancellationToken);
143168
}
144169

145170
private async void HandleDidChangeContent(StandardLevelDetailViewController? standardLevelDetailViewController, StandardLevelDetailViewController.ContentType contentType)
@@ -151,46 +176,54 @@ private async void HandleDidChangeContent(StandardLevelDetailViewController? sta
151176
Assert.That(standardLevelDetailViewController.beatmapLevel == _beatmapLevelCache.BeatmapLevel);
152177
}
153178

154-
await InvokeEventAsync(standardLevelDetailViewController, contentType, _eventProxyPatches.StandardLevelDetailViewControllerDidChangeContentDelegate!, _cancellationToken);
179+
await InvokeEventAsync(standardLevelDetailViewController, contentType, _eventProxyHooks.StandardLevelDetailViewControllerDidChangeContentDelegate!, _cancellationToken);
155180
}
156181

157-
[AffinityPatch(typeof(BeatmapLevelDataUtils), nameof(BeatmapLevelDataUtils.ReadAllTextFromPathAsync))]
158-
private void LogReadAllTextFromPathAsync(string path)
182+
private Task<string?> LogReadAllTextFromPathAsync(Func<string, CancellationToken, Task<string?>> original, string path, CancellationToken cancellationToken)
159183
{
160184
Plugin.Log.Debug($"ReadAllTextFromPathAsync {_beatmapLevelCache.BeatmapKey.ToString()} {path}");
185+
return original(path, cancellationToken);
161186
}
162187

163-
[AffinityPatch(typeof(LevelCollectionViewController), nameof(LevelCollectionViewController.DidActivate))]
164-
[AffinityPrefix]
165-
private void ReplaceDidSelectLevelEvent(LevelCollectionViewController __instance, bool firstActivation)
188+
private void ReplaceDidSelectLevelEvent(Action<LevelCollectionViewController, bool, bool, bool> original, LevelCollectionViewController instance, bool firstActivation, bool addedToHierarchy, bool screenSystemEnabling)
166189
{
167190
if (!firstActivation)
168191
{
192+
original(instance, firstActivation, addedToHierarchy, screenSystemEnabling);
169193
return;
170194
}
171195

172-
ref var didSelectLevelViewControllerEvent = ref Accessors.ViewControllerDidSelectLevelEventAccessor(ref __instance);
196+
ref var didSelectLevelViewControllerEvent = ref Accessors.ViewControllerDidSelectLevelEventAccessor(ref instance);
173197
didSelectLevelViewControllerEvent = HandleDidSelectLevel;
174198

175-
ref var didSelectLevelViewEvent = ref Accessors.TableViewDidSelectLevelEventAccessor(ref __instance._levelCollectionTableView);
199+
ref var didSelectLevelViewEvent = ref Accessors.TableViewDidSelectLevelEventAccessor(ref instance._levelCollectionTableView);
176200
didSelectLevelViewEvent = HandleDidSelectLevel;
201+
202+
original(instance, firstActivation, addedToHierarchy, screenSystemEnabling);
177203
}
178204

179-
[AffinityPatch(typeof(StandardLevelDetailViewController), nameof(StandardLevelDetailViewController.DidActivate))]
180-
[AffinityPrefix]
181-
private void ReplaceDidChangeContentEvent(StandardLevelDetailViewController __instance, bool firstActivation)
205+
private void ReplaceDidChangeContentEvent(Action<StandardLevelDetailViewController, bool, bool, bool> original, StandardLevelDetailViewController instance, bool firstActivation, bool addedToHierarchy, bool screenSystemEnabling)
182206
{
183207
if (!firstActivation)
184208
{
209+
original(instance, firstActivation, addedToHierarchy, screenSystemEnabling);
185210
return;
186211
}
187212

188-
ref var didChangeContentEvent = ref Accessors.DidChangeContentEventAccessor(ref __instance);
213+
ref var didChangeContentEvent = ref Accessors.DidChangeContentEventAccessor(ref instance);
189214
didChangeContentEvent = HandleDidChangeContent;
215+
216+
original(instance, firstActivation, addedToHierarchy, screenSystemEnabling);
217+
}
218+
219+
private BeatmapKey SetBeatmapLevelCacheBeatmapKey(Func<StandardLevelDetailView, BeatmapKey> original, StandardLevelDetailView instance)
220+
{
221+
var result = original(instance);
222+
SetBeatmapLevelCacheBeatmapKeyAsync(result);
223+
return result;
190224
}
191225

192-
[AffinityPatch(typeof(StandardLevelDetailView), nameof(StandardLevelDetailView.CreateBeatmapKey))]
193-
private async void SetBeatmapLevelCacheBeatmapKeyAsync(BeatmapKey __result)
226+
private async void SetBeatmapLevelCacheBeatmapKeyAsync(BeatmapKey beatmapKey)
194227
{
195228
Plugin.Log.Debug("Attempting to set beatmap level cache beatmap key");
196229

@@ -206,44 +239,39 @@ private async void SetBeatmapLevelCacheBeatmapKeyAsync(BeatmapKey __result)
206239

207240
Assert.That(beatmapLevelData == _beatmapLevelCache.BeatmapLevelData);
208241

209-
if (!_beatmapLevelCache.DifficultyMatches(__result))
242+
if (!_beatmapLevelCache.DifficultyMatches(beatmapKey))
210243
{
211-
Plugin.Log.Debug($"Setting beatmap level cache beatmap key to {__result}");
244+
Plugin.Log.Debug($"Setting beatmap level cache beatmap key to {beatmapKey}");
212245

213246
_beatmapLevelCache.InvalidateDifficulty();
214-
_beatmapLevelCache.BeatmapKey = __result;
247+
_beatmapLevelCache.BeatmapKey = beatmapKey;
215248
}
216249

217250
tcs.TrySetResult(true);
218251
}
219252

220-
[AffinityPatch(typeof(BeatmapDataLoader), nameof(BeatmapDataLoader.LoadBeatmapDataAsync))]
221-
[AffinityPrefix]
222-
private bool LoadBeatmapDataWithCacheAsync(ref Task<IReadonlyBeatmapData?> __result, IBeatmapLevelData beatmapLevelData, BeatmapKey beatmapKey, float startBpm, bool loadingForDesignatedEnvironment, IEnvironmentInfo? targetEnvironmentInfo, IEnvironmentInfo? originalEnvironmentInfo, BeatmapLevelDataVersion beatmapLevelDataVersion, GameplayModifiers? gameplayModifiers, PlayerSpecificSettings? playerSpecificSettings, bool enableBeatmapDataCaching)
253+
private Task<IReadonlyBeatmapData?> LoadBeatmapDataWithCacheAsync(Func<BeatmapDataLoader, IBeatmapLevelData, BeatmapKey, float, bool, IEnvironmentInfo?, IEnvironmentInfo?, BeatmapLevelDataVersion, GameplayModifiers?, PlayerSpecificSettings?, bool, Task<IReadonlyBeatmapData?>> original, BeatmapDataLoader instance, IBeatmapLevelData beatmapLevelData, BeatmapKey beatmapKey, float startBpm, bool loadingForDesignatedEnvironment, IEnvironmentInfo? targetEnvironmentInfo, IEnvironmentInfo? originalEnvironmentInfo, BeatmapLevelDataVersion beatmapLevelDataVersion, GameplayModifiers? gameplayModifiers, PlayerSpecificSettings? playerSpecificSettings, bool enableBeatmapDataCaching)
223254
{
224-
Assert.That(UnityGame.OnMainThread);
255+
Assert.That(UnityGame.OnMainThread, "This method must be called on the main thread.");
225256

226257
var request = new BeatmapDataRequest(beatmapLevelData, beatmapKey, startBpm, loadingForDesignatedEnvironment, targetEnvironmentInfo, originalEnvironmentInfo, beatmapLevelDataVersion, gameplayModifiers, playerSpecificSettings, enableBeatmapDataCaching);
227258

228259
if (!_beatmapLevelCache.LevelMatches(beatmapLevelData))
229260
{
230261
Plugin.Log.Debug("Level data changed, returning original method");
231-
return true;
262+
return original(instance, beatmapLevelData, beatmapKey, startBpm, loadingForDesignatedEnvironment, targetEnvironmentInfo, originalEnvironmentInfo, beatmapLevelDataVersion, gameplayModifiers, playerSpecificSettings, enableBeatmapDataCaching);
232263
}
233264

234265
if (_beatmapLevelCache.BeatmapDataRequest?.Equals(request) == true)
235266
{
236267
Plugin.Log.Debug("Returning stored beatmap data task");
237-
__result = _beatmapLevelCache.BeatmapDataLoadingTask!;
238-
return false;
268+
return _beatmapLevelCache.BeatmapDataLoadingTask!;
239269
}
240270

241271
Plugin.Log.Debug("Starting new beatmap data request");
242272

243273
_beatmapLevelCache.BeatmapDataRequest = request;
244-
__result = _beatmapLevelCache.BeatmapDataLoadingTask = request.Start(_beatmapDataLoader);
245-
246-
return false;
274+
return _beatmapLevelCache.BeatmapDataLoadingTask = request.Start(original, _beatmapDataLoader);
247275
}
248276
}
249277
}

source/SongCore/Patches/BeatmapLevelCache/BeatmapDataRequest.cs renamed to source/SongCore/Hooks/BeatmapLevelCache/BeatmapDataRequest.cs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,18 @@
1-
using System.Threading.Tasks;
1+
using System;
2+
using System.Threading.Tasks;
23
using BeatSaber.Destinations;
34
using ModestTree;
45
using UnityEngine.SceneManagement;
56

6-
namespace SongCore.Patches.BeatmapLevelCache
7+
namespace SongCore.Hooks.BeatmapLevelCache
78
{
89
internal record BeatmapDataRequest(IBeatmapLevelData BeatmapLevelData, BeatmapKey BeatmapKey, float StartBpm, bool LoadingForDesignatedEnvironment, IEnvironmentInfo? TargetEnvironmentInfo, IEnvironmentInfo? OriginalEnvironmentInfo, BeatmapLevelDataVersion BeatmapLevelDataVersion, GameplayModifiers? GameplayModifiers, PlayerSpecificSettings? PlayerSpecificSettings, bool EnableBeatmapDataCaching)
910
{
10-
public Task<IReadonlyBeatmapData?> Start(BeatmapDataLoader beatmapDataLoader)
11+
public Task<IReadonlyBeatmapData?> Start(Func<BeatmapDataLoader, IBeatmapLevelData, BeatmapKey, float, bool, IEnvironmentInfo?, IEnvironmentInfo?, BeatmapLevelDataVersion, GameplayModifiers?, PlayerSpecificSettings?, bool, Task<IReadonlyBeatmapData?>> original, BeatmapDataLoader beatmapDataLoader)
1112
{
1213
Assert.That(SceneManager.GetActiveScene().name != SceneNames.kGameCoreSceneName, "Beatmap data should not be loaded in the game scene, as garbage collection is disabled.");
1314

14-
return ReversePatches.BeatmapDataLoader.LoadBeatmapDataAsync(beatmapDataLoader, BeatmapLevelData, BeatmapKey, StartBpm, LoadingForDesignatedEnvironment, TargetEnvironmentInfo, OriginalEnvironmentInfo, BeatmapLevelDataVersion, GameplayModifiers, PlayerSpecificSettings, EnableBeatmapDataCaching);
15+
return original(beatmapDataLoader, BeatmapLevelData, BeatmapKey, StartBpm, LoadingForDesignatedEnvironment, TargetEnvironmentInfo, OriginalEnvironmentInfo, BeatmapLevelDataVersion, GameplayModifiers, PlayerSpecificSettings, EnableBeatmapDataCaching);
1516
}
1617
}
1718
}

source/SongCore/Patches/BeatmapLevelCache/BeatmapDataType.cs renamed to source/SongCore/Hooks/BeatmapLevelCache/BeatmapDataType.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
namespace SongCore.Patches.BeatmapLevelCache
1+
namespace SongCore.Hooks.BeatmapLevelCache
22
{
33
internal enum BeatmapDataType
44
{

0 commit comments

Comments
 (0)