From 18c1efda42ef5115d65ee1de77e352baf86bfa11 Mon Sep 17 00:00:00 2001
From: Extremelyd1 <10898310+Extremelyd1@users.noreply.github.com>
Date: Tue, 30 Jul 2024 23:13:56 +0200
Subject: [PATCH] Improve enter scene detection, fix backwards walking players
---
HKMP/Game/Client/ClientManager.cs | 75 ++++++-------
HKMP/Game/Client/CustomHooks.cs | 160 +++++++++++++++++++++++++++
HKMP/Game/Client/Save/SaveManager.cs | 4 +
3 files changed, 200 insertions(+), 39 deletions(-)
create mode 100644 HKMP/Game/Client/CustomHooks.cs
diff --git a/HKMP/Game/Client/ClientManager.cs b/HKMP/Game/Client/ClientManager.cs
index 28fe1be..ce5d5f4 100644
--- a/HKMP/Game/Client/ClientManager.cs
+++ b/HKMP/Game/Client/ClientManager.cs
@@ -155,11 +155,6 @@ public string Username {
///
private Vector3 _lastScale;
- ///
- /// Whether the scene has just changed and we are in a scene change.
- ///
- private bool _sceneChanged;
-
///
/// Whether we have already determined whether we are scene host or not for the entity system.
///
@@ -193,6 +188,9 @@ ModSettings modSettings
new GamePatcher(netClient).RegisterHooks();
new FsmPatcher().RegisterHooks();
+ var customHooks = new CustomHooks();
+ customHooks.Initialize();
+
_commandManager = new ClientCommandManager();
var eventAggregator = new EventAggregator();
@@ -259,6 +257,8 @@ ModSettings modSettings
On.HeroController.Start += OnHeroControllerStart;
On.HeroController.Update += OnPlayerUpdate;
+ customHooks.AfterEnterSceneHeroTransformed += OnEnterScene;
+
// Register client connect and timeout handler
netClient.ConnectEvent += OnClientConnect;
netClient.TimeoutEvent += OnTimeout;
@@ -871,8 +871,6 @@ private void OnSceneChange(Scene oldScene, Scene newScene) {
return;
}
- _sceneChanged = true;
-
// Reset the status of whether we determined the scene host or not
_sceneHostDetermined = false;
@@ -910,38 +908,7 @@ private void OnPlayerUpdate(On.HeroController.orig_Update orig, HeroController s
// Update the last position, since it changed
_lastPosition = newPosition;
- if (_sceneChanged) {
- _sceneChanged = false;
-
- // Set some default values for the packet variables in case we don't have a HeroController instance
- // This might happen when we are in a non-gameplay scene without the knight
- var position = Vector2.Zero;
- var scale = Vector3.zero;
- ushort animationClipId = 0;
-
- // If we do have a HeroController instance, use its values
- if (HeroController.instance != null) {
- var transform = HeroController.instance.transform;
- var transformPos = transform.position;
-
- position = new Vector2(transformPos.x, transformPos.y);
- scale = transform.localScale;
- animationClipId = (ushort) AnimationManager.GetCurrentAnimationClip();
- }
-
- Logger.Debug("Sending EnterScene packet");
-
- _netClient.UpdateManager.SetEnterSceneData(
- SceneUtil.GetCurrentSceneName(),
- position,
- scale.x > 0,
- animationClipId
- );
- } else {
- // If this was not the first position update after a scene change,
- // we can simply send a position update packet
- _netClient.UpdateManager.UpdatePlayerPosition(new Vector2(newPosition.x, newPosition.y));
- }
+ _netClient.UpdateManager.UpdatePlayerPosition(new Vector2(newPosition.x, newPosition.y));
}
var newScale = heroTransform.localScale;
@@ -954,6 +921,36 @@ private void OnPlayerUpdate(On.HeroController.orig_Update orig, HeroController s
}
}
+ ///
+ /// Callback method for the local player enters a scene. Used to network to the server that a scene is entered.
+ ///
+ private void OnEnterScene() {
+ // Set some default values for the packet variables in case we don't have a HeroController instance
+ // This might happen when we are in a non-gameplay scene without the knight
+ var position = Vector2.Zero;
+ var scale = Vector3.zero;
+ ushort animationClipId = 0;
+
+ // If we do have a HeroController instance, use its values
+ if (HeroController.instance != null) {
+ var transform = HeroController.instance.transform;
+ var transformPos = transform.position;
+
+ position = new Vector2(transformPos.x, transformPos.y);
+ scale = transform.localScale;
+ animationClipId = (ushort) AnimationManager.GetCurrentAnimationClip();
+ }
+
+ Logger.Debug($"Sending EnterScene packet");
+
+ _netClient.UpdateManager.SetEnterSceneData(
+ SceneUtil.GetCurrentSceneName(),
+ position,
+ scale.x > 0,
+ animationClipId
+ );
+ }
+
///
/// Callback method for when a chat message is received.
///
diff --git a/HKMP/Game/Client/CustomHooks.cs b/HKMP/Game/Client/CustomHooks.cs
new file mode 100644
index 0000000..3bb6855
--- /dev/null
+++ b/HKMP/Game/Client/CustomHooks.cs
@@ -0,0 +1,160 @@
+using System;
+using System.Reflection;
+using Hkmp.Logging;
+using Mono.Cecil.Cil;
+using MonoMod.Cil;
+using MonoMod.RuntimeDetour;
+
+namespace Hkmp.Game.Client;
+
+// TODO: create method for de-registering the hooks
+///
+/// Class that manages and exposes custom hooks that are not possible with On hooks or ModHooks. Uses IL modification
+/// to embed event calls in certain methods.
+///
+public class CustomHooks {
+ ///
+ /// The binding flags for obtaining certain types for hooking.
+ ///
+ private const BindingFlags BindingFlags = System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance;
+
+ ///
+ /// The instruction match set for matching the instructions below. This is the call to HeroInPosition.Invoke.
+ ///
+ // IL_01ae: ldloc.1 // V_1
+ // IL_01af: ldfld class HeroController/HeroInPosition HeroController::heroInPosition
+ // IL_01b4: ldc.i4.0
+ // IL_01b5: callvirt instance void HeroController/HeroInPosition::Invoke(bool)
+ private static readonly Func[] HeroInPositionInstructions = [
+ i => i.MatchLdfld(typeof(HeroController), "heroInPosition"),
+ i => i.MatchLdcI4(out _),
+ i => i.MatchCallvirt(typeof(HeroController.HeroInPosition), "Invoke")
+ ];
+
+ ///
+ /// IL Hook instance for the HeroController EnterScene hook.
+ ///
+ private ILHook _heroControllerEnterSceneIlHook;
+ ///
+ /// IL Hook instance for the HeroController Respawn hook.
+ ///
+ private ILHook _heroControllerRespawnIlHook;
+
+ ///
+ /// Event for when the player object is done being transformed (changed position, scale) after entering a scene.
+ ///
+ public event Action AfterEnterSceneHeroTransformed;
+
+ ///
+ /// Initialize the class by registering the IL hooks.
+ ///
+ public void Initialize() {
+ IL.HeroController.Start += HeroControllerOnStart;
+ IL.HeroController.EnterSceneDreamGate += HeroControllerOnEnterSceneDreamGate;
+
+ var type = typeof(HeroController).GetNestedType("d__469", BindingFlags);
+ _heroControllerEnterSceneIlHook = new ILHook(type.GetMethod("MoveNext", BindingFlags), HeroControllerOnEnterScene);
+
+ type = typeof(HeroController).GetNestedType("d__473", BindingFlags);
+ _heroControllerRespawnIlHook = new ILHook(type.GetMethod("MoveNext", BindingFlags), HeroControllerOnRespawn);
+ }
+
+ ///
+ /// IL Hook for the HeroController Start method. Calls an event within the method.
+ ///
+ private void HeroControllerOnStart(ILContext il) {
+ try {
+ // Create a cursor for this context
+ var c = new ILCursor(il);
+
+ EmitAfterEnterSceneEventHeroInPosition(c);
+ } catch (Exception e) {
+ Logger.Error($"Could not change HeroControllerOnStart IL: \n{e}");
+ }
+ }
+
+ ///
+ /// IL Hook for the HeroController EnterSceneDreamGate method. Calls an event within the method.
+ ///
+ private void HeroControllerOnEnterSceneDreamGate(ILContext il) {
+ try {
+ // Create a cursor for this context
+ var c = new ILCursor(il);
+
+ EmitAfterEnterSceneEventHeroInPosition(c);
+ } catch (Exception e) {
+ Logger.Error($"Could not change HeroControllerOnEnterSceneDreamGate IL: \n{e}");
+ }
+ }
+
+ ///
+ /// IL Hook for the HeroController EnterScene method. Calls an event multiple times within the method.
+ ///
+ private void HeroControllerOnEnterScene(ILContext il) {
+ try {
+ // Create a cursor for this context
+ var c = new ILCursor(il);
+
+ for (var i = 0; i < 2; i++) {
+ EmitAfterEnterSceneEventHeroInPosition(c);
+ }
+
+ // IL_0634: ldloc.1 // V_1
+ // IL_0635: callvirt instance void HeroController::FaceRight()
+ Func[] faceDirectionInstructions = [
+ i => i.MatchLdloc(1),
+ i =>
+ i.MatchCallvirt(typeof(HeroController), "FaceRight") ||
+ i.MatchCallvirt(typeof(HeroController), "FaceLeft")
+ ];
+
+ for (var i = 0; i < 2; i++) {
+ c.GotoNext(
+ MoveType.After,
+ HeroInPositionInstructions
+ );
+
+ c.GotoNext(
+ MoveType.After,
+ faceDirectionInstructions
+ );
+
+ c.EmitDelegate(() => { AfterEnterSceneHeroTransformed?.Invoke(); });
+ }
+
+ EmitAfterEnterSceneEventHeroInPosition(c);
+ } catch (Exception e) {
+ Logger.Error($"Could not change HeroController#EnterScene IL: \n{e}");
+ }
+ }
+
+ ///
+ /// IL Hook for the HeroController Respawn method. Calls an event multiple times within the method.
+ ///
+ private void HeroControllerOnRespawn(ILContext il) {
+ try {
+ // Create a cursor for this context
+ var c = new ILCursor(il);
+
+ for (var i = 0; i < 2; i++) {
+ EmitAfterEnterSceneEventHeroInPosition(c);
+ }
+ } catch (Exception e) {
+ Logger.Error($"Could not change HeroControllerOnRespawn IL: \n{e}");
+ }
+ }
+
+ ///
+ /// Emit the delegate for calling the event after the
+ /// 'HeroInPosition' instructions.
+ ///
+ /// The IL cursor on which to match the instructions and emit the delegate.
+ private void EmitAfterEnterSceneEventHeroInPosition(ILCursor c) {
+ c.GotoNext(
+ MoveType.After,
+ HeroInPositionInstructions
+ );
+
+ c.EmitDelegate(() => { AfterEnterSceneHeroTransformed?.Invoke(); });
+ }
+}
diff --git a/HKMP/Game/Client/Save/SaveManager.cs b/HKMP/Game/Client/Save/SaveManager.cs
index c21b78c..96e2ceb 100644
--- a/HKMP/Game/Client/Save/SaveManager.cs
+++ b/HKMP/Game/Client/Save/SaveManager.cs
@@ -156,6 +156,10 @@ private void OnUpdatePlayerData() {
}
var gm = global::GameManager.instance;
+ if (gm == null) {
+ return;
+ }
+
if (gm.gameState == GameState.MAIN_MENU) {
return;
}