Skip to content

Commit

Permalink
Improve enter scene detection, fix backwards walking players
Browse files Browse the repository at this point in the history
  • Loading branch information
Extremelyd1 committed Jul 30, 2024
1 parent 04217f6 commit 18c1efd
Show file tree
Hide file tree
Showing 3 changed files with 200 additions and 39 deletions.
75 changes: 36 additions & 39 deletions HKMP/Game/Client/ClientManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -155,11 +155,6 @@ public string Username {
/// </summary>
private Vector3 _lastScale;

/// <summary>
/// Whether the scene has just changed and we are in a scene change.
/// </summary>
private bool _sceneChanged;

/// <summary>
/// Whether we have already determined whether we are scene host or not for the entity system.
/// </summary>
Expand Down Expand Up @@ -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();

Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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;
Expand All @@ -954,6 +921,36 @@ private void OnPlayerUpdate(On.HeroController.orig_Update orig, HeroController s
}
}

/// <summary>
/// Callback method for the local player enters a scene. Used to network to the server that a scene is entered.
/// </summary>
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
);
}

/// <summary>
/// Callback method for when a chat message is received.
/// </summary>
Expand Down
160 changes: 160 additions & 0 deletions HKMP/Game/Client/CustomHooks.cs
Original file line number Diff line number Diff line change
@@ -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
/// <summary>
/// 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.
/// </summary>
public class CustomHooks {
/// <summary>
/// The binding flags for obtaining certain types for hooking.
/// </summary>
private const BindingFlags BindingFlags = System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance;

/// <summary>
/// The instruction match set for matching the instructions below. This is the call to HeroInPosition.Invoke.
/// </summary>
// 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<Instruction, bool>[] HeroInPositionInstructions = [
i => i.MatchLdfld(typeof(HeroController), "heroInPosition"),
i => i.MatchLdcI4(out _),
i => i.MatchCallvirt(typeof(HeroController.HeroInPosition), "Invoke")
];

/// <summary>
/// IL Hook instance for the HeroController EnterScene hook.
/// </summary>
private ILHook _heroControllerEnterSceneIlHook;
/// <summary>
/// IL Hook instance for the HeroController Respawn hook.
/// </summary>
private ILHook _heroControllerRespawnIlHook;

/// <summary>
/// Event for when the player object is done being transformed (changed position, scale) after entering a scene.
/// </summary>
public event Action AfterEnterSceneHeroTransformed;

/// <summary>
/// Initialize the class by registering the IL hooks.
/// </summary>
public void Initialize() {
IL.HeroController.Start += HeroControllerOnStart;
IL.HeroController.EnterSceneDreamGate += HeroControllerOnEnterSceneDreamGate;

var type = typeof(HeroController).GetNestedType("<EnterScene>d__469", BindingFlags);
_heroControllerEnterSceneIlHook = new ILHook(type.GetMethod("MoveNext", BindingFlags), HeroControllerOnEnterScene);

type = typeof(HeroController).GetNestedType("<Respawn>d__473", BindingFlags);
_heroControllerRespawnIlHook = new ILHook(type.GetMethod("MoveNext", BindingFlags), HeroControllerOnRespawn);
}

/// <summary>
/// IL Hook for the HeroController Start method. Calls an event within the method.
/// </summary>
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}");
}
}

/// <summary>
/// IL Hook for the HeroController EnterSceneDreamGate method. Calls an event within the method.
/// </summary>
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}");
}
}

/// <summary>
/// IL Hook for the HeroController EnterScene method. Calls an event multiple times within the method.
/// </summary>
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<Instruction, bool>[] 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}");
}
}

/// <summary>
/// IL Hook for the HeroController Respawn method. Calls an event multiple times within the method.
/// </summary>
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}");
}
}

/// <summary>
/// Emit the delegate for calling the <see cref="AfterEnterSceneHeroTransformed"/> event after the
/// 'HeroInPosition' instructions.
/// </summary>
/// <param name="c">The IL cursor on which to match the instructions and emit the delegate.</param>
private void EmitAfterEnterSceneEventHeroInPosition(ILCursor c) {
c.GotoNext(
MoveType.After,
HeroInPositionInstructions
);

c.EmitDelegate(() => { AfterEnterSceneHeroTransformed?.Invoke(); });
}
}
4 changes: 4 additions & 0 deletions HKMP/Game/Client/Save/SaveManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,10 @@ private void OnUpdatePlayerData() {
}

var gm = global::GameManager.instance;
if (gm == null) {
return;
}

if (gm.gameState == GameState.MAIN_MENU) {
return;
}
Expand Down

0 comments on commit 18c1efd

Please sign in to comment.