Skip to content

Commit

Permalink
feat: hooks
Browse files Browse the repository at this point in the history
  • Loading branch information
Clazex committed Jun 13, 2022
1 parent e67c0d7 commit fb2cb03
Show file tree
Hide file tree
Showing 3 changed files with 209 additions and 0 deletions.
1 change: 1 addition & 0 deletions Osmi/Imports.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,6 @@
global using UnityEngine;

global using Lang = Language.Language;
global using ReflectionHelper = Modding.ReflectionHelper;
global using Logger = Osmi.Utils.Logger;
global using UObject = UnityEngine.Object;
3 changes: 3 additions & 0 deletions Osmi/Osmi.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,8 @@ public sealed class Osmi : Mod {
public Osmi() =>
Instance = this;

public override void Initialize() =>
_ = Ref.GM.StartCoroutine(OsmiHooks.CheckGameInitialized());

public static bool IsHere() => true;
}
205 changes: 205 additions & 0 deletions Osmi/OsmiHooks.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
using Mono.Cecil.Cil;

using MonoMod.Cil;
using MonoMod.RuntimeDetour;
using MonoMod.Utils;

using UnityEngine.SceneManagement;

namespace Osmi;

[PublicAPI]
public static class OsmiHooks {
/// <summary>
/// Called when entering main menu for the first time <br />
/// Hooking new methods after that will result in immediate invocation
/// </summary>
public static event Action GameInitializedHook {
add {
if (gameInitializedHookCalled) {
value?.Invoke();
return;
}

InternalGameInitializedHook += value;
}
remove => InternalGameInitializedHook -= value;
}
private static bool gameInitializedHookCalled = false;
private static event Action InternalGameInitializedHook = null!;

internal static IEnumerator CheckGameInitialized() {
yield return new WaitUntil(() => GameObject.Find("LogoTitle") != null);

gameInitializedHookCalled = true;
if (InternalGameInitializedHook == null) {
yield break;
}

foreach (Action a in InternalGameInitializedHook.GetInvocationList()) {
try {
a.Invoke();
} catch (Exception e) {
Logger.LogError(e.ToString());
}
}

InternalGameInitializedHook = null!;
}


/// <summary>
/// Equivalent to <see cref="UIManager.EditMenus"/>,
/// but ensure to be called after MAPI finished building menus
/// </summary>
public static event Action MenuBuildHook = null!;

private static void OnMenuBuild() {
Logger.LogFine($"{nameof(OnMenuBuild)} Invoked");

if (MenuBuildHook == null) {
return;
}

foreach (Action a in MenuBuildHook.GetInvocationList()) {
try {
a.Invoke();
} catch (Exception e) {
Logger.LogError(e.ToString());
}
}
}


/// <summary>
/// Equivalent to
/// <see cref="UnityEngine.SceneManagement.SceneManager.activeSceneChanged"/>,
/// but catch all exceptions
/// </summary>
public static event Action<Scene, Scene> SceneChangeHook = null!;

private static void OnSceneChange(Scene prev, Scene next) {
Logger.LogFine($"{nameof(OnSceneChange)} Invoked");

if (SceneChangeHook == null) {
return;
}

foreach (Action<Scene, Scene> a in SceneChangeHook.GetInvocationList()) {
try {
a.Invoke(prev, next);
} catch (Exception e) {
Logger.LogError(e.ToString());
}
}
}


/// <param name="invincible">Invincibility state passed by vanilla code or last hooked method</param>
/// <returns>New invincibility state</returns>
public delegate bool HitEnemyHandler(HealthManager hm, HitInstance hit, bool invincible);

/// <summary>
/// Called when deciding whether an enemy is invincible to a hit or not
/// </summary>
public static event HitEnemyHandler HitEnemyHook = null!;

private static bool OnHitEnemy(bool invincible, HealthManager hm, HitInstance hitInstance) {
Logger.LogFine($"{nameof(OnHitEnemy)} Invoked");

if (HitEnemyHook == null) {
return invincible;
}

foreach (HitEnemyHandler a in HitEnemyHook.GetInvocationList()) {
try {
invincible = a.Invoke(hm, hitInstance, invincible);
} catch (Exception e) {
Logger.LogError(e.ToString());
}
}

return invincible;
}


/// <summary>
/// Called when game is about to pause <br />
/// <see cref="Time.timeScale"/> is still 1f when called
/// </summary>
public static event Action GamePauseHook = null!;

private static void OnGamePause() {
Logger.LogFine($"{nameof(OnGamePause)} Invoked");

if (GamePauseHook == null) {
return;
}

foreach (Action a in GamePauseHook.GetInvocationList()) {
try {
a.Invoke();
} catch (Exception e) {
Logger.LogError(e.ToString());
}
}
}

/// <summary>
/// Called when game is about to unpause <br />
/// <see cref="Time.timeScale"/> is still 0f when called
/// </summary>
public static event Action GameUnpauseHook = null!;

private static void OnGameUnpause() {
Logger.LogFine($"{nameof(OnGameUnpause)} Invoked");

if (GameUnpauseHook == null) {
return;
}

foreach (Action a in GameUnpauseHook.GetInvocationList()) {
try {
a.Invoke();
} catch (Exception e) {
Logger.LogError(e.ToString());
}
}
}


static OsmiHooks() {
GameInitializedHook += () => UIManager.EditMenus += OnMenuBuild;

UnityEngine.SceneManagement.SceneManager.activeSceneChanged += OnSceneChange;

IL.HealthManager.Hit += il => new ILCursor(il)
.Goto(0)
.GotoNext(i => i.MatchCallvirt<HealthManager>(
nameof(HealthManager.IsBlockingByDirection)
))
.Emit(OpCodes.Ldarg_0) // this
.Emit(OpCodes.Ldarg_1) // hitInstance
.EmitDelegate(OnHitEnemy);

_ = new ILHook(
ReflectionHelper
.GetMethodInfo(typeof(GameManager), nameof(GameManager.PauseGameToggle))
.GetStateMachineTarget(),
il => new ILCursor(il)
.Goto(0)
.GotoNext(
MoveType.Before,
i => i.MatchLdcR4(0f),
i => i.MatchCallvirt(typeof(GameManager), "SetTimeScale")
)
.Emit(OpCodes.Call, ((Delegate) OnGamePause).Method)
.GotoNext(
MoveType.Before,
i => i.MatchLdcR4(1f),
i => i.MatchCallvirt(typeof(GameManager), "SetTimeScale")
)
.Emit(OpCodes.Call, ((Delegate) OnGameUnpause).Method)
);
}
}

0 comments on commit fb2cb03

Please sign in to comment.