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; }