diff --git a/.editorconfig b/.editorconfig
index 60fa5122..9bf17a4b 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -62,11 +62,11 @@ resharper_web_config_module_not_resolved_highlighting = warning
resharper_web_config_type_not_resolved_highlighting = warning
resharper_web_config_wrong_module_highlighting = warning
-[{*.har,*.inputactions,*.jsb2,*.jsb3,*.json,*.yml,.babelrc,.eslintrc,.stylelintrc,bowerrc,jest.config}]
+[{*.har,*.inputactions,*.jsb2,*.jsb3,*.yml,.babelrc,.eslintrc,.stylelintrc,bowerrc,jest.config}]
indent_style = space
indent_size = 2
-[*.{appxmanifest,asax,ascx,aspx,axaml,build,c,c++,cc,cginc,compute,cp,cpp,cs,cshtml,cu,cuh,cxx,dtd,fs,fsi,fsscript,fsx,fx,fxh,h,hh,hlsl,hlsli,hlslinc,hpp,hxx,inc,inl,ino,ipp,master,ml,mli,mpp,mq4,mq5,mqh,nuspec,paml,razor,resw,resx,shader,skin,tpp,usf,ush,vb,xaml,xamlx,xoml,xsd}]
+[*.{appxmanifest,asax,ascx,aspx,axaml,build,c,c++,cc,cginc,compute,cp,cpp,cs,cshtml,cu,cuh,cxx,dtd,fs,fsi,fsscript,fsx,fx,fxh,h,hh,hlsl,hlsli,hlslinc,hpp,hxx,inc,inl,ino,ipp,json,master,ml,mli,mpp,mq4,mq5,mqh,nuspec,paml,razor,resw,resx,shader,skin,tpp,usf,ush,vb,xaml,xamlx,xoml,xsd}]
indent_style = space
indent_size = 4
tab_width = 4
diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml
index db22e786..6300b636 100644
--- a/.github/workflows/dotnet.yml
+++ b/.github/workflows/dotnet.yml
@@ -26,7 +26,7 @@ jobs:
ref: ${{ github.event.pull_request.head.sha }}
- name: Download dependencies
- run: wget https://files.catbox.moe/r5p6rr.gpg -O deps.zip.gpg
+ run: wget https://files.catbox.moe/t156ho.gpg -O deps.zip.gpg
- name: Decrypt dependencies
run: gpg --quiet --batch --yes --decrypt --passphrase="${{ secrets.DEPENDENCIES_ZIP_PASSPHRASE }}" --output deps.zip deps.zip.gpg
@@ -44,7 +44,7 @@ jobs:
- name: Setup .NET
uses: actions/setup-dotnet@v5
with:
- dotnet-version: 6.0.x
+ dotnet-version: 8.0.x
- name: Restore dependencies
run: dotnet restore ${{ github.workspace }}
@@ -60,6 +60,7 @@ jobs:
${{ github.workspace }}/HKMP/bin/Release/net472/HKMP.dll
${{ github.workspace }}/HKMP/bin/Release/net472/HKMP.xml
${{ github.workspace }}/HKMP/bin/Release/net472/HKMP.pdb
+ ${{ github.workspace }}/HKMP/bin/Release/net472/BouncyCastle.Cryptography.dll
- name: Upload HKMPServer artifact
uses: actions/upload-artifact@v4
@@ -68,3 +69,7 @@ jobs:
path: |
${{ github.workspace }}/HKMPServer/bin/Release/net472/HKMPServer.exe
${{ github.workspace }}/HKMPServer/bin/Release/net472/HKMPServer.pdb
+ ${{ github.workspace }}/HKMPServer/bin/Release/net472/HKMP.dll
+ ${{ github.workspace }}/HKMPServer/bin/Release/net472/HKMP.pdb
+ ${{ github.workspace }}/HKMPServer/bin/Release/net472/Newtonsoft.Json.dll
+ ${{ github.workspace }}/HKMPServer/bin/Release/net472/BouncyCastle.Cryptography.dll
diff --git a/HKMP/Animation/AnimationEffect.cs b/HKMP/Animation/AnimationEffect.cs
index c430c28c..2fb04664 100644
--- a/HKMP/Animation/AnimationEffect.cs
+++ b/HKMP/Animation/AnimationEffect.cs
@@ -26,21 +26,19 @@ public void SetServerSettings(ServerSettings serverSettings) {
}
///
- /// Locate the damages_enemy FSM and change the attack type to generic. This will avoid the local
- /// player taking knock back from remote players hitting shields etc.
+ /// Locate the damages_enemy FSM and change the attack direction to the given direciton. This will ensure that
+ /// enemies are getting knocked back in the correct direction from remote player's attacks.
///
/// The target GameObject to change.
- protected static void ChangeAttackTypeOfFsm(GameObject targetObject) {
+ /// The direction in float that the damage is coming from.
+ protected static void ChangeAttackDirection(GameObject targetObject, float direction) {
var damageFsm = targetObject.LocateMyFSM("damages_enemy");
if (damageFsm == null) {
return;
}
-
- var takeDamage = damageFsm.GetFirstAction("Send Event");
- takeDamage.AttackType.Value = (int) AttackTypes.Generic;
- takeDamage = damageFsm.GetFirstAction("Parent");
- takeDamage.AttackType.Value = (int) AttackTypes.Generic;
- takeDamage = damageFsm.GetFirstAction("Grandparent");
- takeDamage.AttackType.Value = (int) AttackTypes.Generic;
+
+ // Find the variable that controls the slash direction for damaging enemies
+ var directionVar = damageFsm.FsmVariables.GetFsmFloat("direction");
+ directionVar.Value = direction;
}
}
diff --git a/HKMP/Animation/AnimationManager.cs b/HKMP/Animation/AnimationManager.cs
index 105968b7..37949d58 100644
--- a/HKMP/Animation/AnimationManager.cs
+++ b/HKMP/Animation/AnimationManager.cs
@@ -10,7 +10,6 @@
using Hkmp.Game.Client;
using Hkmp.Game.Settings;
using Hkmp.Networking.Client;
-using Hkmp.Networking.Packet;
using Hkmp.Networking.Packet.Data;
using Hkmp.Util;
using HutongGames.PlayMaker.Actions;
@@ -18,6 +17,8 @@
using UnityEngine;
using UnityEngine.SceneManagement;
using Logger = Hkmp.Logging.Logger;
+using Object = UnityEngine.Object;
+using Random = UnityEngine.Random;
namespace Hkmp.Animation;
@@ -33,14 +34,14 @@ internal class AnimationManager {
///
/// Animations that are allowed to loop, because they need to transmit the effect.
///
- private static readonly string[] AllowedLoopAnimations = { "Focus Get", "Run" };
+ private static readonly string[] AllowedLoopAnimations = ["Focus Get", "Run"];
///
/// Clip names of animations that are handled by the animation controller.
///
- private static readonly string[] AnimationControllerClipNames = {
+ private static readonly string[] AnimationControllerClipNames = [
"Airborne"
- };
+ ];
///
/// The animation effect for cancelling the Crystal Dash Charge. Stored since it needs to be called
@@ -406,20 +407,29 @@ internal class AnimationManager {
public AnimationManager(
NetClient netClient,
- PlayerManager playerManager,
- PacketManager packetManager,
- ServerSettings serverSettings
+ PlayerManager playerManager
) {
_netClient = netClient;
_playerManager = playerManager;
_chargedEffectStopwatch = new Stopwatch();
_chargedEndEffectStopwatch = new Stopwatch();
+ }
- // Register packet handler
- packetManager.RegisterClientPacketHandler(ClientPacketId.PlayerDeath,
- OnPlayerDeath);
+ ///
+ /// Initialize the animation manager by registering packet handlers and initializing animation effects.
+ ///
+ public void Initialize(ServerSettings serverSettings) {
+ // Set the server settings for all animation effects
+ foreach (var effect in AnimationEffects.Values) {
+ effect.SetServerSettings(serverSettings);
+ }
+ }
+ ///
+ /// Register the game hooks for the animation manager.
+ ///
+ public void RegisterHooks() {
// Register scene change, which is where we update the animation event handler
UnityEngine.SceneManagement.SceneManager.activeSceneChanged += OnSceneChange;
@@ -443,18 +453,39 @@ ServerSettings serverSettings
On.GameManager.HazardRespawn += GameManagerOnHazardRespawn;
// Register when the HeroController starts, so we can register dung trail events
- On.HeroController.Start += HeroControllerOnStart;
+ CustomHooks.HeroControllerStartAction += HeroControllerOnStart;
// Relinquish Control cancels a lot of effects, so we need to broadcast the end of these effects
On.HeroController.RelinquishControl += HeroControllerOnRelinquishControl;
// Register when the player dies to send the animation
ModHooks.BeforePlayerDeadHook += OnDeath;
+ }
- // Set the server settings for all animation effects
- foreach (var effect in AnimationEffects.Values) {
- effect.SetServerSettings(serverSettings);
- }
+ ///
+ /// Deregister the game hooks for the animation manager.
+ ///
+ public void DeregisterHooks() {
+ UnityEngine.SceneManagement.SceneManager.activeSceneChanged -= OnSceneChange;
+
+ On.HeroAnimationController.Play -= HeroAnimationControllerOnPlay;
+ On.HeroAnimationController.PlayFromFrame -= HeroAnimationControllerOnPlayFromFrame;
+
+ On.tk2dSpriteAnimator.WarpClipToLocalTime -= Tk2dSpriteAnimatorOnWarpClipToLocalTime;
+ On.tk2dSpriteAnimator.ProcessEvents -= Tk2dSpriteAnimatorOnProcessEvents;
+
+ On.HeroController.CancelDash -= HeroControllerOnCancelDash;
+
+ ModHooks.HeroUpdateHook -= OnHeroUpdateHook;
+
+ On.HeroController.DieFromHazard -= HeroControllerOnDieFromHazard;
+ On.GameManager.HazardRespawn -= GameManagerOnHazardRespawn;
+
+ CustomHooks.HeroControllerStartAction -= HeroControllerOnStart;
+
+ On.HeroController.RelinquishControl -= HeroControllerOnRelinquishControl;
+
+ ModHooks.BeforePlayerDeadHook -= OnDeath;
}
///
@@ -470,15 +501,13 @@ public void OnPlayerAnimationUpdate(ushort id, int clipId, int frame, bool[] eff
var animationClip = (AnimationClip) clipId;
- if (AnimationEffects.ContainsKey(animationClip)) {
+ if (AnimationEffects.TryGetValue(animationClip, out var animationEffect)) {
var playerObject = _playerManager.GetPlayerObject(id);
- if (playerObject == null) {
+ if (!playerObject) {
// Logger.Get().Warn(this, $"Tried to play animation effect {clipName} with ID: {id}, but player object doesn't exist");
return;
}
- var animationEffect = AnimationEffects[animationClip];
-
// Check if the animation effect is a DamageAnimationEffect and if so,
// set whether it should deal damage based on player teams
if (animationEffect is DamageAnimationEffect damageAnimationEffect) {
@@ -507,7 +536,7 @@ public void OnPlayerAnimationUpdate(ushort id, int clipId, int frame, bool[] eff
/// The frame that the animation should play from.
public void UpdatePlayerAnimation(ushort id, int clipId, int frame) {
var playerObject = _playerManager.GetPlayerObject(id);
- if (playerObject == null) {
+ if (!playerObject) {
// Logger.Get().Warn(this, $"Tried to update animation, but there was not matching player object for ID {id}");
return;
}
@@ -615,8 +644,8 @@ private void OnAnimationEvent(tk2dSpriteAnimationClip clip) {
var animationClip = ClipEnumNames[clip.name];
// Check whether there is an effect that adds info to this packet
- if (AnimationEffects.ContainsKey(animationClip)) {
- var effectInfo = AnimationEffects[animationClip].GetEffectInfo();
+ if (AnimationEffects.TryGetValue(animationClip, out var effect)) {
+ var effectInfo = effect.GetEffectInfo();
_netClient.UpdateManager.UpdatePlayerAnimation(animationClip, 0, effectInfo);
} else {
@@ -783,7 +812,7 @@ float time
orig(self, clip, time);
var localPlayer = HeroController.instance;
- if (localPlayer == null) {
+ if (!localPlayer) {
return;
}
@@ -820,7 +849,7 @@ int direction
orig(self, start, last, direction);
var localPlayer = HeroController.instance;
- if (localPlayer == null) {
+ if (!localPlayer) {
return;
}
@@ -862,10 +891,10 @@ private IEnumerator HeroControllerOnDieFromHazard(On.HeroController.orig_DieFrom
return orig(self, hazardType, angle);
}
- _netClient.UpdateManager.UpdatePlayerAnimation(AnimationClip.HazardDeath, 0, new[] {
+ _netClient.UpdateManager.UpdatePlayerAnimation(AnimationClip.HazardDeath, 0, [
hazardType.Equals(HazardType.SPIKES),
hazardType.Equals(HazardType.ACID)
- });
+ ]);
// Execute the original method and return its value
return orig(self, hazardType, angle);
@@ -891,7 +920,7 @@ private void GameManagerOnHazardRespawn(On.GameManager.orig_HazardRespawn orig,
/// Callback method for when a player death is received.
///
/// The generic client data for this event.
- private void OnPlayerDeath(GenericClientData data) {
+ public void OnPlayerDeath(GenericClientData data) {
// And play the death animation for the ID in the packet
MonoBehaviourUtil.Instance.StartCoroutine(PlayDeathAnimation(data.Id));
}
@@ -1004,12 +1033,7 @@ private IEnumerator PlayDeathAnimation(ushort id) {
///
/// Callback method on the HeroController#Start method.
///
- /// The original method.
- /// The HeroController instance.
- private void HeroControllerOnStart(On.HeroController.orig_Start orig, HeroController self) {
- // Execute original method
- orig(self);
-
+ private void HeroControllerOnStart() {
SetDescendingDarkLandEffectDelay();
RegisterDefenderCrestEffects();
}
@@ -1061,12 +1085,12 @@ private void SetDescendingDarkLandEffectDelay() {
///
private void RegisterDefenderCrestEffects() {
var charmEffects = HeroController.instance.gameObject.FindGameObjectInChildren("Charm Effects");
- if (charmEffects == null) {
+ if (!charmEffects) {
return;
}
var dungObject = charmEffects.FindGameObjectInChildren("Dung");
- if (dungObject == null) {
+ if (!dungObject) {
return;
}
diff --git a/HKMP/Animation/Effects/CycloneSlash.cs b/HKMP/Animation/Effects/CycloneSlash.cs
index ca386c16..1765ee1e 100644
--- a/HKMP/Animation/Effects/CycloneSlash.cs
+++ b/HKMP/Animation/Effects/CycloneSlash.cs
@@ -47,13 +47,13 @@ public override void Play(GameObject playerObject, bool[] effectInfo) {
cycloneObj,
playerAttacks.transform
);
- cycloneSlash.layer = 22;
+ cycloneSlash.layer = 17;
var hitLComponent = cycloneSlash.FindGameObjectInChildren("Hit L");
- ChangeAttackTypeOfFsm(hitLComponent);
+ ChangeAttackDirection(hitLComponent, 0f);
var hitRComponent = cycloneSlash.FindGameObjectInChildren("Hit R");
- ChangeAttackTypeOfFsm(hitRComponent);
+ ChangeAttackDirection(hitRComponent, 180f);
cycloneSlash.SetActive(true);
diff --git a/HKMP/Animation/Effects/DashBase.cs b/HKMP/Animation/Effects/DashBase.cs
index 4b4ef1bf..ef7e1ea6 100644
--- a/HKMP/Animation/Effects/DashBase.cs
+++ b/HKMP/Animation/Effects/DashBase.cs
@@ -124,20 +124,38 @@ protected void Play(GameObject playerObject, bool[] effectInfo, bool shadowDash,
// Lastly, disable the player collider, since we are in a shadow dash
// We only do this, if we don't have sharp shadow
playerObject.GetComponent().enabled = false;
- } else if (
- !ServerSettings.IsBodyDamageEnabled &&
- ServerSettings.IsPvpEnabled &&
- ShouldDoDamage &&
- damage != 0
- ) {
- // If body damage is disabled, but PvP is enabled and we are performing a sharp shadow dash
- // we need to enable the DamageHero component and move the player object to the correct layer
- // to allow the local player to collide with it
- playerObject.layer = 11;
-
- var damageHero = playerObject.GetComponent();
- damageHero.enabled = true;
- damageHero.damageDealt = damage;
+ } else {
+ var localPlayerAttacks = HeroController.instance.gameObject.FindGameObjectInChildren("Attacks");
+ var playerAttacks = playerObject.FindGameObjectInChildren("Attacks");
+
+ var sharpShadowPrefab = localPlayerAttacks.FindGameObjectInChildren("Sharp Shadow");
+
+ var sharpShadowObject = Object.Instantiate(
+ sharpShadowPrefab,
+ playerAttacks.transform
+ );
+ sharpShadowObject.name = "Sharp Shadow";
+ sharpShadowObject.SetActive(true);
+ sharpShadowObject.layer = 17;
+
+ if (
+ !ServerSettings.IsBodyDamageEnabled &&
+ ServerSettings.IsPvpEnabled &&
+ ShouldDoDamage &&
+ damage != 0
+ ) {
+ // If body damage is disabled, but PvP is enabled and we are performing a sharp shadow dash
+ // we need to enable the DamageHero component and move the player object to the correct layer
+ // to allow the local player to collide with it
+ playerObject.layer = 11;
+
+ var damageHero = playerObject.GetComponent();
+ damageHero.enabled = true;
+ damageHero.damageDealt = damage;
+ }
+
+ // As a failsafe, we remove the sharp shadow object again on a timer
+ Object.Destroy(sharpShadowObject, 1f);
}
} else {
// Instantiate the dash burst relative to the player effects
diff --git a/HKMP/Animation/Effects/DashEnd.cs b/HKMP/Animation/Effects/DashEnd.cs
index 83df0388..e2dfc1a1 100644
--- a/HKMP/Animation/Effects/DashEnd.cs
+++ b/HKMP/Animation/Effects/DashEnd.cs
@@ -22,24 +22,30 @@ public override void Play(GameObject playerObject, bool[] effectInfo) {
}
var playerEffects = playerObject.FindGameObjectInChildren("Effects");
- if (playerEffects == null) {
- return;
- }
-
- var dashParticles = playerEffects.FindGameObjectInChildren("Dash Particles");
- if (dashParticles != null) {
+ if (playerEffects != null) {
+ var dashParticles = playerEffects.FindGameObjectInChildren("Dash Particles");
+ if (dashParticles != null) {
#pragma warning disable 0618
- // Disable emission
- dashParticles.GetComponent().enableEmission = false;
+ // Disable emission
+ dashParticles.GetComponent().enableEmission = false;
#pragma warning restore 0618
- }
+ }
- var shadowDashParticles = playerEffects.FindGameObjectInChildren("Shadow Dash Particles");
- if (shadowDashParticles != null) {
+ var shadowDashParticles = playerEffects.FindGameObjectInChildren("Shadow Dash Particles");
+ if (shadowDashParticles != null) {
#pragma warning disable 0618
- // Disable emission
- shadowDashParticles.GetComponent().enableEmission = false;
+ // Disable emission
+ shadowDashParticles.GetComponent().enableEmission = false;
#pragma warning restore 0618
+ }
+ }
+
+ var playerAttacks = playerObject.FindGameObjectInChildren("Attacks");
+ if (playerAttacks != null) {
+ var sharpShadow = playerAttacks.FindGameObjectInChildren("Sharp Shadow");
+ if (sharpShadow != null) {
+ Object.Destroy(sharpShadow);
+ }
}
}
diff --git a/HKMP/Animation/Effects/DashSlash.cs b/HKMP/Animation/Effects/DashSlash.cs
index 5e644f77..c1526913 100644
--- a/HKMP/Animation/Effects/DashSlash.cs
+++ b/HKMP/Animation/Effects/DashSlash.cs
@@ -35,21 +35,36 @@ public override void Play(GameObject playerObject, bool[] effectInfo) {
dashSlashObject,
playerObject.transform.parent
);
+ dashSlash.layer = 17;
// Since we anchor the dash slash on the player container instead of the player object
// (to prevent it from flipping when the knight turns around) we need to adjust the scale based
// on which direction the knight is facing
+ var localScale = playerObject.transform.localScale;
+ var playerScaleX = localScale.x;
var dashSlashTransform = dashSlash.transform;
var dashSlashScale = dashSlashTransform.localScale;
dashSlashTransform.localScale = new Vector3(
- dashSlashScale.x * playerObject.transform.localScale.x,
+ dashSlashScale.x * playerScaleX,
dashSlashScale.y,
dashSlashScale.z
);
- dashSlash.layer = 22;
-
- ChangeAttackTypeOfFsm(dashSlash);
+ // Check which direction the knight is facing for the damages_enemy FSM
+ var facingRight = playerScaleX > 0;
+ ChangeAttackDirection(dashSlash, facingRight ? 180f : 0f);
+
+ var controlColliderFsm = dashSlash.LocateMyFSM("Control Collider");
+ // If the player is not facing right, the local position set by the FSM is not right given that we are spawning
+ // the dash slash on the player container instead of on the player object
+ if (!facingRight) {
+ var startPosVar = controlColliderFsm.FsmVariables.GetFsmVector3("Start Pos");
+ startPosVar.Value = new Vector3(
+ startPosVar.Value.x * -1,
+ startPosVar.Value.y,
+ startPosVar.Value.z
+ );
+ }
dashSlash.SetActive(true);
@@ -58,38 +73,40 @@ public override void Play(GameObject playerObject, bool[] effectInfo) {
// Set the newly instantiate collider to state Init, to reset it
// in case the local player was already performing it
- dashSlash.LocateMyFSM("Control Collider").SetState("Init");
+ if (controlColliderFsm.ActiveStateName != "Init") {
+ controlColliderFsm.SetState("Init");
+ }
var damage = ServerSettings.DashSlashDamage;
if (ServerSettings.IsPvpEnabled && ShouldDoDamage) {
- // Somehow adding a DamageHero component or the parry FSM simply to the dash slash object doesn't work,
- // so we create a separate object for it
- var dashSlashCollider = Object.Instantiate(
- new GameObject(
- "DashSlashCollider",
- typeof(PolygonCollider2D)
- ),
- dashSlash.transform
- );
- dashSlashCollider.SetActive(true);
- dashSlashCollider.layer = 22;
+ // Since the dash slash should deal damage to other players, we create a separate object for that purpose
+ var pvpCollider = new GameObject("PvP Collider", typeof(PolygonCollider2D));
+
+ var transform = pvpCollider.transform;
+ transform.SetParent(dashSlash.transform);
+ transform.localPosition = new Vector3(0, 0, 0);
+ transform.localScale = new Vector3(1, 1, 0);
+
+ pvpCollider.SetActive(true);
+ pvpCollider.layer = 22;
// Copy over the polygon collider points
- dashSlashCollider.GetComponent().points =
+ pvpCollider.GetComponent().points =
dashSlash.GetComponent().points;
if (ServerSettings.AllowParries) {
- AddParryFsm(dashSlashCollider);
+ AddParryFsm(pvpCollider);
}
if (damage != 0) {
- dashSlashCollider.AddComponent().damageDealt = damage;
+ pvpCollider.AddComponent().damageDealt = damage;
}
}
// Get the animator, figure out the duration of the animation and destroy the object accordingly afterwards
var dashSlashAnimator = dashSlash.GetComponent();
- var dashSlashAnimationDuration = dashSlashAnimator.DefaultClip.frames.Length / dashSlashAnimator.ClipFps;
+ var defaultClip = dashSlashAnimator.DefaultClip;
+ var dashSlashAnimationDuration = defaultClip.frames.Length / defaultClip.fps;
Object.Destroy(dashSlash, dashSlashAnimationDuration);
}
diff --git a/HKMP/Animation/Effects/DescendingDarkLand.cs b/HKMP/Animation/Effects/DescendingDarkLand.cs
index cfc26a9c..c77274f9 100644
--- a/HKMP/Animation/Effects/DescendingDarkLand.cs
+++ b/HKMP/Animation/Effects/DescendingDarkLand.cs
@@ -54,14 +54,19 @@ private IEnumerator PlayEffectInCoroutine(GameObject playerObject) {
playerSpells.transform
);
quakeSlam.SetActive(true);
- quakeSlam.layer = 22;
+ quakeSlam.layer = 9;
+ var hitL = quakeSlam.FindGameObjectInChildren("Hit L");
+ hitL.layer = 17;
+ var hitR = quakeSlam.FindGameObjectInChildren("Hit R");
+ hitR.layer = 17;
+
// If PvP is enabled add a DamageHero component to both hitbox sides
var damage = ServerSettings.DescendingDarkDamage;
if (ServerSettings.IsPvpEnabled && ShouldDoDamage && damage != 0) {
- quakeSlam.FindGameObjectInChildren("Hit L").AddComponent().damageDealt = damage;
- quakeSlam.FindGameObjectInChildren("Hit R").AddComponent().damageDealt = damage;
+ hitL.AddComponent().damageDealt = damage;
+ hitR.AddComponent().damageDealt = damage;
}
// The FSM has a Wait action of 0.75 as a fallback for when the animationTrigger is not called.
@@ -85,14 +90,16 @@ private IEnumerator PlayEffectInCoroutine(GameObject playerObject) {
playerSpells.transform
);
qMega.SetActive(true);
+ qMega.layer = 9;
+
// Play the Q Mega animation from the first frame
qMega.GetComponent().PlayFromFrame(0);
// Enable the correct layer
var qMegaHitL = qMega.FindGameObjectInChildren("Hit L");
- qMegaHitL.layer = 22;
+ qMegaHitL.layer = 17;
var qMegaHitR = qMega.FindGameObjectInChildren("Hit R");
- qMegaHitR.layer = 22;
+ qMegaHitR.layer = 17;
if (ServerSettings.IsPvpEnabled && ShouldDoDamage && damage != 0) {
qMegaHitL.AddComponent().damageDealt = damage;
diff --git a/HKMP/Animation/Effects/DesolateDiveLand.cs b/HKMP/Animation/Effects/DesolateDiveLand.cs
index dda27a92..7a1bd061 100644
--- a/HKMP/Animation/Effects/DesolateDiveLand.cs
+++ b/HKMP/Animation/Effects/DesolateDiveLand.cs
@@ -53,14 +53,19 @@ private IEnumerator PlayEffectInCoroutine(GameObject playerObject) {
playerSpells.transform
);
quakeSlam.SetActive(true);
- quakeSlam.layer = 22;
+ quakeSlam.layer = 9;
+
+ var hitL = quakeSlam.FindGameObjectInChildren("Hit L");
+ hitL.layer = 17;
+ var hitR = quakeSlam.FindGameObjectInChildren("Hit R");
+ hitR.layer = 17;
// If PvP is enabled add a DamageHero component to both hitbox sides
var damage = ServerSettings.DesolateDiveDamage;
if (ServerSettings.IsPvpEnabled && ShouldDoDamage && damage != 0) {
- quakeSlam.FindGameObjectInChildren("Hit L").AddComponent().damageDealt = damage;
- quakeSlam.FindGameObjectInChildren("Hit R").AddComponent().damageDealt = damage;
+ hitL.AddComponent().damageDealt = damage;
+ hitR.AddComponent().damageDealt = damage;
}
// Obtain the Q1 Pillar prefab and instantiate it relative to the player object
diff --git a/HKMP/Animation/Effects/FireballBase.cs b/HKMP/Animation/Effects/FireballBase.cs
index 08dc9669..a4b4f0b4 100644
--- a/HKMP/Animation/Effects/FireballBase.cs
+++ b/HKMP/Animation/Effects/FireballBase.cs
@@ -125,7 +125,7 @@ int damage
// Make sure the object is scaled according to which direction the player is facing
dungFluke.transform.rotation = Quaternion.Euler(0, 0, 26 * -localScale.x);
- dungFluke.layer = 22;
+ dungFluke.layer = 17;
var shamanStoneModifier = hasShamanStoneCharm ? 1.1f : 1.0f;
@@ -164,7 +164,7 @@ int damage
Quaternion.identity
);
fireball.SetActive(true);
- fireball.layer = 22;
+ fireball.layer = 17;
// We add a fireball component that deals with spawning the moving fireball
var fireballComponent = fireball.AddComponent();
@@ -309,9 +309,7 @@ private IEnumerator StartDungFluke(GameObject dungFluke) {
);
dungCloud.SetActive(true);
- dungCloud.layer = 22;
-
- Object.Destroy(dungCloud.GetComponent());
+ dungCloud.layer = 17;
// Get the control FSM and the audio clip corresponding to the explosion of the dungFluke
// We need it later
diff --git a/HKMP/Animation/Effects/FocusBurst.cs b/HKMP/Animation/Effects/FocusBurst.cs
index 951d7120..3ece1eca 100644
--- a/HKMP/Animation/Effects/FocusBurst.cs
+++ b/HKMP/Animation/Effects/FocusBurst.cs
@@ -74,7 +74,7 @@ public override void Play(GameObject playerObject, bool[] effectInfo) {
Quaternion.identity
);
cloud.SetActive(true);
- cloud.layer = 22;
+ cloud.layer = 17;
// Destroy the FSM so it doesn't use local player variables
Object.Destroy(cloud.LocateMyFSM("Control"));
diff --git a/HKMP/Animation/Effects/GreatSlash.cs b/HKMP/Animation/Effects/GreatSlash.cs
index 7a8f2efe..83259bca 100644
--- a/HKMP/Animation/Effects/GreatSlash.cs
+++ b/HKMP/Animation/Effects/GreatSlash.cs
@@ -36,9 +36,11 @@ public override void Play(GameObject playerObject, bool[] effectInfo) {
greatSlashObject,
playerAttacks.transform
);
- greatSlash.layer = 22;
-
- ChangeAttackTypeOfFsm(greatSlash);
+ greatSlash.layer = 17;
+
+ // Check which direction the knight is facing for the damages_enemy FSM
+ var facingRight = playerObject.transform.localScale.x > 0;
+ ChangeAttackDirection(greatSlash, facingRight ? 180f : 0f);
greatSlash.SetActive(true);
@@ -48,12 +50,26 @@ public override void Play(GameObject playerObject, bool[] effectInfo) {
var damage = ServerSettings.GreatSlashDamage;
if (ServerSettings.IsPvpEnabled && ShouldDoDamage) {
+ // Since the great slash should deal damage to other players, we create a separate object for that purpose
+ var pvpCollider = new GameObject("PvP Collider", typeof(PolygonCollider2D));
+
+ var transform = pvpCollider.transform;
+ transform.SetParent(greatSlash.transform);
+ transform.localPosition = new Vector3(0, 0, 0);
+ transform.localScale = new Vector3(1, 1, 0);
+
+ pvpCollider.SetActive(true);
+ pvpCollider.layer = 22;
+
+ pvpCollider.GetComponent().points =
+ greatSlash.GetComponent().points;
+
if (ServerSettings.AllowParries) {
- AddParryFsm(greatSlash);
+ AddParryFsm(pvpCollider);
}
if (damage != 0) {
- greatSlash.AddComponent().damageDealt = damage;
+ pvpCollider.AddComponent().damageDealt = damage;
}
}
diff --git a/HKMP/Animation/Effects/QuakeDownBase.cs b/HKMP/Animation/Effects/QuakeDownBase.cs
index 5e830332..6fb53be7 100644
--- a/HKMP/Animation/Effects/QuakeDownBase.cs
+++ b/HKMP/Animation/Effects/QuakeDownBase.cs
@@ -48,6 +48,7 @@ protected void Play(GameObject playerObject, bool[] effectInfo, string qTrailPre
localPlayerSpells.FindGameObjectInChildren(qTrailPrefabName),
playerSpells.transform
);
+ qTrail.layer = 17;
qTrail.SetActive(true);
// Assign a name so we reference it later, when we need to delete it
qTrail.name = qTrailPrefabName;
diff --git a/HKMP/Animation/Effects/ScreamBase.cs b/HKMP/Animation/Effects/ScreamBase.cs
index f6975101..6a83f752 100644
--- a/HKMP/Animation/Effects/ScreamBase.cs
+++ b/HKMP/Animation/Effects/ScreamBase.cs
@@ -1,5 +1,4 @@
using System.Collections;
-using System.Collections.Generic;
using Hkmp.Util;
using HutongGames.PlayMaker.Actions;
using UnityEngine;
@@ -46,56 +45,28 @@ protected IEnumerator Play(GameObject playerObject, string screamClipName, strin
playerSpells.transform
);
screamHeads.SetActive(true);
+ screamHeads.layer = 9;
// We don't want to deactivate this when the local player is being hit
Object.Destroy(screamHeads.LocateMyFSM("Deactivate on Hit"));
// For each (L, R and U) of the scream objects, we need to do a few things
var objectNames = new[] { "Hit L", "Hit R", "Hit U" };
- // Also store a few objects that we need to destroy later
- var objectsToDestroy = new List();
foreach (var objectName in objectNames) {
var screamHitObject = screamHeads.FindGameObjectInChildren(objectName);
- Object.Destroy(screamHitObject.LocateMyFSM("damages_enemy"));
- var screamHitDamager = Object.Instantiate(
- new GameObject(objectName),
- screamHitObject.transform
- );
- screamHitDamager.layer = 22;
-
- // Add the object to the list to destroy it later
- objectsToDestroy.Add(screamHitDamager);
-
- // Create a new polygon collider
- var screamHitDamagerPoly = screamHitDamager.AddComponent();
- screamHitDamagerPoly.isTrigger = true;
-
- // Obtain the original polygon collider
- var screamHitPoly = screamHitObject.GetComponent();
-
- // Copy over the polygon collider points
- screamHitDamagerPoly.points = screamHitPoly.points;
-
- // If PvP is enabled, add a DamageHero component to the damager objects
+ // If PvP is enabled, add a DamageHero component to the scream hit objects
if (ServerSettings.IsPvpEnabled && ShouldDoDamage && damage != 0) {
- screamHitDamager.AddComponent().damageDealt = damage;
+ screamHitObject.AddComponent().damageDealt = damage;
}
-
- // Delete the original polygon collider, we don't need it anymore
- Object.Destroy(screamHitPoly);
}
// Wait for the duration of the scream animation
- var duration = playerObject.GetComponent().GetClipByName("Scream 2 Get")
- .Duration;
+ var duration = playerObject.GetComponent().GetClipByName("Scream 2 Get").Duration;
yield return new WaitForSeconds(duration);
// Then destroy the leftover objects
Object.Destroy(screamHeads);
- foreach (var gameObject in objectsToDestroy) {
- Object.Destroy(gameObject);
- }
}
///
diff --git a/HKMP/Animation/Effects/SlashBase.cs b/HKMP/Animation/Effects/SlashBase.cs
index 334ed540..65de041a 100644
--- a/HKMP/Animation/Effects/SlashBase.cs
+++ b/HKMP/Animation/Effects/SlashBase.cs
@@ -60,8 +60,21 @@ protected void Play(GameObject playerObject, bool[] effectInfo, GameObject prefa
// Instantiate the slash gameObject from the given prefab
// and use the attack gameObject as transform reference
var slash = Object.Instantiate(prefab, playerAttacks.transform);
- slash.layer = 22;
+ slash.layer = 17;
+ float direction;
+ if (type is SlashType.Wall or SlashType.Normal or SlashType.Alt) {
+ // For wall, normal and alt slash, we need to check the direction the knight is facing
+ var facingRight = playerObject.transform.localScale.x > 0;
+ direction = facingRight ? 180f : 0f;
+ } else if (type is SlashType.Up) {
+ direction = 90f;
+ } else {
+ direction = 270f;
+ }
+
+ ChangeAttackDirection(slash, direction);
+
// Set the base scale of the slash based on the slash type, this prevents remote nail slashes to occur
// larger than they should be if they are based on the prefab from Long Nail/Mark of Pride/both slash
var baseScale = _baseScales[type];
@@ -146,55 +159,40 @@ protected void Play(GameObject playerObject, bool[] effectInfo, GameObject prefa
slash.GetComponent().enabled = true;
+ // Enable both the polygon collider of the slash and of its child object
var polygonCollider = slash.GetComponent();
-
polygonCollider.enabled = true;
-
- // Instantiate additional game object that can interact with enemies so remote enemies can be hit
- GameObject enemySlash;
- {
- enemySlash = Object.Instantiate(prefab, playerAttacks.transform);
- enemySlash.layer = 17;
- enemySlash.name = "Enemy Slash";
- enemySlash.transform.localScale = slash.transform.localScale;
-
- var typesToRemove = new[] {
- typeof(MeshFilter), typeof(MeshRenderer), typeof(tk2dSprite), typeof(tk2dSpriteAnimator),
- typeof(NailSlash),
- typeof(AudioSource)
- };
- foreach (var typeToRemove in typesToRemove) {
- Object.Destroy(enemySlash.GetComponent(typeToRemove));
- }
-
- for (var i = 0; i < enemySlash.transform.childCount; i++) {
- Object.Destroy(enemySlash.transform.GetChild(i));
- }
-
- polygonCollider = enemySlash.GetComponent();
- polygonCollider.enabled = true;
-
- var damagesEnemyFsm = slash.LocateMyFSM("damages_enemy");
- Object.Destroy(damagesEnemyFsm);
-
- ChangeAttackTypeOfFsm(enemySlash);
- }
+
+ var clashTink = slash.transform.Find("Clash Tink").GetComponent();
+ clashTink.enabled = true;
var damage = ServerSettings.NailDamage;
if (ServerSettings.IsPvpEnabled && ShouldDoDamage) {
+ // Since the slash should deal damage to other players, we create a separate object for that purpose
+ var pvpCollider = new GameObject("PvP Collider", typeof(PolygonCollider2D));
+
+ var transform = pvpCollider.transform;
+ transform.SetParent(slash.transform);
+ transform.localPosition = new Vector3(0, 0, 0);
+ transform.localScale = new Vector3(1, 1, 0);
+
+ pvpCollider.SetActive(true);
+ pvpCollider.layer = 22;
+
+ pvpCollider.GetComponent().points = polygonCollider.points;
+
if (ServerSettings.AllowParries) {
- AddParryFsm(slash);
+ AddParryFsm(pvpCollider);
}
if (damage != 0) {
- slash.AddComponent().damageDealt = damage;
+ pvpCollider.AddComponent().damageDealt = damage;
}
}
// After the animation is finished, we can destroy the slash object
var animationDuration = slashAnimator.CurrentClip.Duration;
Object.Destroy(slash, animationDuration);
- Object.Destroy(enemySlash, animationDuration);
if (!hasGrubberflyElegyCharm
|| isOnOneHealth && !hasFuryCharm
diff --git a/HKMP/Api/Addon/AddonLoader.cs b/HKMP/Api/Addon/AddonLoader.cs
index 7e4b951a..396ebb70 100644
--- a/HKMP/Api/Addon/AddonLoader.cs
+++ b/HKMP/Api/Addon/AddonLoader.cs
@@ -16,6 +16,16 @@ internal abstract class AddonLoader {
///
private const string AssemblyFilePattern = "*.dll";
+ ///
+ /// List of file names (including extension) of files that should be skipped when trying to load addons.
+ /// These are dependencies of either HKMP or HKMPServer.
+ ///
+ private static readonly string[] ExcludedFileNames = [
+ "HKMP.dll",
+ "Newtonsoft.Json.dll",
+ "BouncyCastle.Cryptography.dll"
+ ];
+
///
/// The directory in which to look for assembly files.
///
@@ -54,6 +64,11 @@ protected List LoadAddons() {
var assemblyPaths = GetAssemblyPaths();
foreach (var assemblyPath in assemblyPaths) {
+ if (ExcludedFileNames.Contains(Path.GetFileName(assemblyPath))) {
+ Logger.Debug($"Skipping loading assembly at: {assemblyPath}");
+ continue;
+ }
+
Logger.Info($"Trying to load assembly at: {assemblyPath}");
Assembly assembly;
diff --git a/HKMP/Api/Client/IClientManager.cs b/HKMP/Api/Client/IClientManager.cs
index 5b8e1e01..cb8c5f2e 100644
--- a/HKMP/Api/Client/IClientManager.cs
+++ b/HKMP/Api/Client/IClientManager.cs
@@ -57,12 +57,14 @@ public interface IClientManager {
/// Changes the team of the local player.
///
/// The team value.
+ [Obsolete("ChangeTeam is deprecated. Team changes are handled by the IServerManager.")]
void ChangeTeam(Team team);
///
/// Changes the skin of the local player.
///
/// The ID of the skin.
+ [Obsolete("ChangeSkin is deprecated. Skin changes are handled by the IServerManager.")]
void ChangeSkin(byte skinId);
///
diff --git a/HKMP/Api/Client/IPlayerMapEntry.cs b/HKMP/Api/Client/IPlayerMapEntry.cs
index 3d92ca8a..159f992b 100644
--- a/HKMP/Api/Client/IPlayerMapEntry.cs
+++ b/HKMP/Api/Client/IPlayerMapEntry.cs
@@ -1,4 +1,4 @@
-using Vector2 = Hkmp.Math.Vector2;
+using Hkmp.Math;
namespace Hkmp.Api.Client;
diff --git a/HKMP/Api/Client/IUiManager.cs b/HKMP/Api/Client/IUiManager.cs
index c4a25fd3..bf285f2d 100644
--- a/HKMP/Api/Client/IUiManager.cs
+++ b/HKMP/Api/Client/IUiManager.cs
@@ -1,3 +1,4 @@
+using System;
namespace Hkmp.Api.Client;
@@ -13,11 +14,13 @@ public interface IUiManager {
///
/// Disables the ability for the user to select a team.
///
+ [Obsolete("DisableTeamSelection is deprecated. There is no UI anymore for changing team. Changing teams is handled by the IServerManager.")]
void DisableTeamSelection();
///
/// Enables the ability for the user to select a team if it was disabled.
///
+ [Obsolete("EnableTeamSelection is deprecated. There is no UI anymore for changing team. Changing teams is handled by the IServerManager.")]
void EnableTeamSelection();
///
diff --git a/HKMP/Api/Client/Networking/ClientAddonNetworkReceiver.cs b/HKMP/Api/Client/Networking/ClientAddonNetworkReceiver.cs
index ea2a5a54..6536ba14 100644
--- a/HKMP/Api/Client/Networking/ClientAddonNetworkReceiver.cs
+++ b/HKMP/Api/Client/Networking/ClientAddonNetworkReceiver.cs
@@ -2,6 +2,7 @@
using System.Collections.Generic;
using Hkmp.Collection;
using Hkmp.Networking.Packet;
+using Hkmp.Networking.Packet.Update;
namespace Hkmp.Api.Client.Networking;
@@ -60,7 +61,7 @@ public void CommitPacketHandlers() {
);
foreach (var idHandlerPair in PacketHandlers) {
- PacketManager.RegisterClientAddonPacketHandler(
+ PacketManager.RegisterClientAddonUpdatePacketHandler(
ClientAddon.Id.Value,
idHandlerPair.Key,
idHandlerPair.Value
@@ -104,7 +105,7 @@ public void RegisterPacketHandler(TPacketId packetId, Action handler) {
PacketHandlers[idValue] = ClientPacketHandler;
if (ClientAddon.Id.HasValue) {
- PacketManager.RegisterClientAddonPacketHandler(
+ PacketManager.RegisterClientAddonUpdatePacketHandler(
ClientAddon.Id.Value,
idValue,
ClientPacketHandler
@@ -130,7 +131,7 @@ GenericClientPacketHandler handler
PacketHandlers[idValue] = ClientPacketHandler;
if (ClientAddon.Id.HasValue) {
- PacketManager.RegisterClientAddonPacketHandler(
+ PacketManager.RegisterClientAddonUpdatePacketHandler(
ClientAddon.Id.Value,
idValue,
ClientPacketHandler
@@ -152,7 +153,7 @@ public void DeregisterPacketHandler(TPacketId packetId) {
PacketHandlers.Remove(idValue);
if (ClientAddon.Id.HasValue) {
- PacketManager.DeregisterClientAddonPacketHandler(ClientAddon.Id.Value, idValue);
+ PacketManager.DeregisterClientAddonUpdatePacketHandler(ClientAddon.Id.Value, idValue);
}
}
diff --git a/HKMP/Api/Server/Networking/ServerAddonNetworkReceiver.cs b/HKMP/Api/Server/Networking/ServerAddonNetworkReceiver.cs
index 8a652b35..2dfc184d 100644
--- a/HKMP/Api/Server/Networking/ServerAddonNetworkReceiver.cs
+++ b/HKMP/Api/Server/Networking/ServerAddonNetworkReceiver.cs
@@ -51,7 +51,7 @@ public void RegisterPacketHandler(TPacketId packetId, Action handler) {
throw new InvalidOperationException(NoAddonIdMsg);
}
- _packetManager.RegisterServerAddonPacketHandler(
+ _packetManager.RegisterServerAddonUpdatePacketHandler(
_serverAddon.Id.Value,
idValue,
(id, _) => handler(id)
@@ -70,7 +70,7 @@ public void RegisterPacketHandler(TPacketId packetId,
throw new InvalidOperationException(NoAddonIdMsg);
}
- _packetManager.RegisterServerAddonPacketHandler(
+ _packetManager.RegisterServerAddonUpdatePacketHandler(
_serverAddon.Id.Value,
idValue,
(id, iPacketData) => handler(id, (TPacketData) iPacketData)
@@ -88,7 +88,7 @@ public void DeregisterPacketHandler(TPacketId packetId) {
throw new InvalidOperationException(NoAddonIdMsg);
}
- _packetManager.DeregisterServerAddonPacketHandler(_serverAddon.Id.Value, idValue);
+ _packetManager.DeregisterServerAddonUpdatePacketHandler(_serverAddon.Id.Value, idValue);
}
///
diff --git a/HKMP/Collection/BiLookup.cs b/HKMP/Collection/BiLookup.cs
index 2949ea20..a312da5f 100644
--- a/HKMP/Collection/BiLookup.cs
+++ b/HKMP/Collection/BiLookup.cs
@@ -135,6 +135,14 @@ public bool ContainsSecond(TSecond index) {
return _inverse.ContainsKey(index);
}
+ ///
+ /// Removes all values from the BiLookup.
+ ///
+ public void Clear() {
+ _normal.Clear();
+ _inverse.Clear();
+ }
+
///
public IEnumerator> GetEnumerator() {
return _normal.GetEnumerator();
diff --git a/HKMP/Fsm/FsmPatcher.cs b/HKMP/Fsm/FsmPatcher.cs
index fc6f819b..c1250676 100644
--- a/HKMP/Fsm/FsmPatcher.cs
+++ b/HKMP/Fsm/FsmPatcher.cs
@@ -15,6 +15,13 @@ public void RegisterHooks() {
On.PlayMakerFSM.OnEnable += OnFsmEnable;
}
+ ///
+ /// Deregisters the hooks necessary to patch.
+ ///
+ public void DeregisterHooks() {
+ On.PlayMakerFSM.OnEnable -= OnFsmEnable;
+ }
+
///
/// Callback method for the PlayMakerFSM#OnEnable hook.
///
@@ -36,5 +43,124 @@ private void OnFsmEnable(On.PlayMakerFSM.orig_OnEnable orig, PlayMakerFSM self)
triggerAction.collideTag.Value = "Player";
triggerAction.collideTag.UseVariable = false;
}
+
+ // Patch the break floor FSM to make sure the Hero Range is not checked so remote players can break the floor
+ if (self.Fsm.Name.Equals("break_floor")) {
+ var boolTestAction = self.GetAction("Check If Nail", 0);
+ if (boolTestAction != null) {
+ self.RemoveAction("Check If Nail", 0);
+ }
+ }
+
+ // Patch Switch Control FSMs to ignore the range requirements to allow remote players from hitting them
+ if (self.Fsm.Name.Equals("Switch Control")) {
+ if (self.GetState("Range") != null) {
+ self.RemoveFirstAction("Range");
+ }
+
+ if (self.GetState("Check If Nail") != null) {
+ self.RemoveFirstAction("Check If Nail");
+ }
+ }
+
+ // Patch the Mantis Throne Main to not rely on animation events from the local player in case another
+ // player challenges the boss
+ if (self.name.Equals("Mantis Lord Throne 2") && self.Fsm.Name.Equals("Mantis Throne Main")) {
+ // Get the animation action for the animation clip that is played
+ var animationAction = self.GetFirstAction("End Challenge");
+
+ // Get the game object for the animation and check if it is not null
+ var go = self.Fsm.GetOwnerDefaultTarget(animationAction.gameObject);
+ if (go == null) {
+ return;
+ }
+
+ // Get the animator from the game object, the clip from the action and its length
+ var animator = go.GetComponent();
+ var clip = animator.GetClipByName(animationAction.clipName.Value);
+ var length = clip.Duration;
+
+ // Get the original watch animation action for the FSM event it sends
+ var watchAnimationAction = self.GetFirstAction("End Challenge");
+ // If the action was already removed before (maybe because the OnEnable triggers multiple times)
+ // we don't have to do anything anymore
+ if (watchAnimationAction == null) {
+ return;
+ }
+
+ // Insert a wait action that takes exactly the duration of the animation and sends the original event
+ // when it finishes
+ self.InsertAction("End Challenge", new Wait {
+ time = length,
+ finishEvent = watchAnimationAction.animationCompleteEvent
+ }, 2);
+
+ // Remove the original watch animation action
+ self.RemoveFirstAction("End Challenge");
+ }
+
+ // Patch the Toll Machine FSM to set the 'activated' bool earlier in the FSM so that it synchronises better
+ if (self.name.StartsWith("Toll Gate Machine") && self.Fsm.Name.Equals("Toll Machine")) {
+ var setBoolAction = self.GetFirstAction("Open Gates");
+ if (setBoolAction == null) {
+ return;
+ }
+
+ self.InsertAction("Box Disappear Anim", setBoolAction, 0);
+ self.RemoveFirstAction("Open Gates");
+ }
+
+ // Patch the tutorial collapser FSMs to set the 'activated' bool earlier in the FSM so that it synchronises better
+ if (self.name == "Collapser Tute 01" && self.Fsm.Name.Equals("collapse tute")) {
+ var setBoolAction = self.GetFirstAction("Break");
+ if (setBoolAction == null) {
+ return;
+ }
+
+ self.InsertAction("Crumble", setBoolAction, 0);
+ self.RemoveFirstAction("Break");
+ }
+
+ // Patch the collapser FSMs to set the 'activated' bool earlier in the FSM so that it synchronises better
+ if (self.name.StartsWith("Collapser Small") && self.Fsm.Name.Equals("collapse small")) {
+ var setBoolAction = self.GetFirstAction("Break");
+ if (setBoolAction == null) {
+ return;
+ }
+
+ self.InsertAction("Split", setBoolAction, 0);
+ self.RemoveFirstAction("Break");
+ }
+
+ // Patch the 'Conversation Control' FSM of the Flower Quest end to set the player data bool earlier in the
+ // FSM so that it synchronises better
+ if (self.name.StartsWith("Inspect Region") && self.Fsm.Name.Equals("Conversation Control")) {
+ var setPdBoolAction = self.GetFirstAction("Flowers");
+ if (setPdBoolAction == null) {
+ return;
+ }
+
+ self.InsertAction("Glow", setPdBoolAction, 0);
+ self.RemoveFirstAction("Flowers");
+ }
+
+ // Patch the 'Chest Control' FSM of the Geo chests object to ensure that they can be opened by remote players
+ // by removing the range check on it
+ if (self.name.StartsWith("Chest") && self.Fsm.Name.Equals("Chest Control")) {
+ var boolTestAction = self.GetFirstAction("Range?");
+ if (boolTestAction == null) {
+ Logger.Warn("Could not patch 'Chest Control' of 'Chest' object, action is missing");
+ return;
+ }
+
+ boolTestAction.isFalse = boolTestAction.isTrue;
+ }
+
+ // Patch the 'Ascend' FSM of the Abyss Pit to give a bit more delay before it starts rising if certain triggers
+ // have been hit. Otherwise, other players have no time to react.
+ if (self.name == "Abyss Pit" && self.Fsm.Name == "Ascend") {
+ var iTweenAction = self.GetFirstAction("Ascend");
+ iTweenAction.delay.Value = 2f;
+ }
}
}
diff --git a/HKMP/Fsm/PositionInterpolation.cs b/HKMP/Fsm/PositionInterpolation.cs
index e924acc5..07a58341 100644
--- a/HKMP/Fsm/PositionInterpolation.cs
+++ b/HKMP/Fsm/PositionInterpolation.cs
@@ -38,10 +38,10 @@ public void Start() {
/// The new position as Vector3.
public void SetNewPosition(Vector3 newPosition) {
#if no_interpolation
- transform.position = newPosition;
+ transform.localPosition = newPosition;
#else
if (_firstUpdate) {
- transform.position = newPosition;
+ transform.localPosition = newPosition;
_firstUpdate = false;
return;
@@ -62,15 +62,15 @@ public void SetNewPosition(Vector3 newPosition) {
/// An enumerator for this coroutine.
private IEnumerator LerpPosition(Vector3 targetPosition, float duration) {
var time = 0f;
- var startPosition = transform.position;
+ var startPosition = transform.localPosition;
while (time < duration) {
- transform.position = Vector3.Lerp(startPosition, targetPosition, time / duration);
+ transform.localPosition = Vector3.Lerp(startPosition, targetPosition, time / duration);
time += Time.deltaTime;
yield return null;
}
- transform.position = targetPosition;
+ transform.localPosition = targetPosition;
#endif
}
}
diff --git a/HKMP/Game/Client/ClientManager.cs b/HKMP/Game/Client/ClientManager.cs
index c498aeb9..c4e1b259 100644
--- a/HKMP/Game/Client/ClientManager.cs
+++ b/HKMP/Game/Client/ClientManager.cs
@@ -6,12 +6,14 @@
using Hkmp.Eventing;
using Hkmp.Fsm;
using Hkmp.Game.Client.Entity;
+using Hkmp.Game.Client.Save;
using Hkmp.Game.Command.Client;
using Hkmp.Game.Server;
using Hkmp.Game.Settings;
using Hkmp.Networking.Client;
using Hkmp.Networking.Packet;
using Hkmp.Networking.Packet.Data;
+using Hkmp.Networking.Packet.Update;
using Hkmp.Ui;
using Hkmp.Util;
using Modding;
@@ -35,9 +37,9 @@ internal class ClientManager : IClientManager {
private readonly NetClient _netClient;
///
- /// The server manager instance.
+ /// The packet manager instance for registering packet handler when we connect to a server.
///
- private readonly ServerManager _serverManager;
+ private readonly PacketManager _packetManager;
///
/// The UI manager instance.
@@ -79,6 +81,21 @@ internal class ClientManager : IClientManager {
///
private readonly PauseManager _pauseManager;
+ ///
+ /// The save manager instance.
+ ///
+ private readonly SaveManager _saveManager;
+
+ ///
+ /// The game patcher instance.
+ ///
+ private readonly GamePatcher _gamePatcher;
+
+ ///
+ /// The FSM patcher instance.
+ ///
+ private readonly FsmPatcher _fsmPatcher;
+
///
/// The client addon manager instance.
///
@@ -94,6 +111,58 @@ internal class ClientManager : IClientManager {
///
private readonly Dictionary _playerData;
+ ///
+ /// Whether we are automatically connected to an in-game hosted server.
+ /// This is used to determine whether to apply save data from the server to the client and warp them to a bench.
+ ///
+ private bool _autoConnect;
+
+ ///
+ /// The username that was last used to connect with.
+ ///
+ private string _username;
+
+ ///
+ /// Keeps track of the last updated location of the local player object.
+ ///
+ private Vector3 _lastPosition;
+
+ ///
+ /// Keeps track of the last updated scale of the local player object.
+ ///
+ private Vector3 _lastScale;
+
+ ///
+ /// The last scene that the player was in, to check whether we should be sending that we left a certain scene.
+ ///
+ private string _lastScene;
+
+ ///
+ /// Whether full synchronisation is enabled for the server we are connected to.
+ ///
+ private bool _fullSynchronisation;
+
+ ///
+ /// Whether we have already determined whether we are scene host or not for the entity system.
+ ///
+ private bool _sceneHostDetermined;
+
+ ///
+ /// Event for when the server settings change after being received from the server.
+ /// The parameter for the action is a copy of the last received server settings.
+ ///
+ public event Action ServerSettingsChangedEvent;
+
+ ///
+ /// Event for when the player's team changes after being received from the server.
+ ///
+ public event Action TeamChangedEvent;
+
+ ///
+ /// Event for when the player's skin changes after being received from the server.
+ ///
+ public event Action SkinChangedEvent;
+
#endregion
#region IClientManager properties
@@ -141,152 +210,266 @@ public string Username {
#endregion
- ///
- /// The username that was last used to connect with.
- ///
- private string _username;
-
- ///
- /// Keeps track of the last updated location of the local player object.
- ///
- private Vector3 _lastPosition;
-
- ///
- /// Keeps track of the last updated scale of the local player object.
- ///
- 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.
- ///
- private bool _sceneHostDetermined;
-
public ClientManager(
NetClient netClient,
- ServerManager serverManager,
PacketManager packetManager,
UiManager uiManager,
ServerSettings serverSettings,
ModSettings modSettings
) {
_netClient = netClient;
- _serverManager = serverManager;
+ _packetManager = packetManager;
_uiManager = uiManager;
_serverSettings = serverSettings;
_modSettings = modSettings;
_playerData = new Dictionary();
- _playerManager = new PlayerManager(packetManager, serverSettings, _playerData);
- _animationManager = new AnimationManager(netClient, _playerManager, packetManager, serverSettings);
+ _playerManager = new PlayerManager(serverSettings, _playerData);
+ _animationManager = new AnimationManager(netClient, _playerManager);
_mapManager = new MapManager(netClient, serverSettings);
_entityManager = new EntityManager(netClient);
+
+ _saveManager = new SaveManager(netClient, _entityManager);
_pauseManager = new PauseManager(netClient);
- _pauseManager.RegisterHooks();
-
- new FsmPatcher().RegisterHooks();
+ _gamePatcher = new GamePatcher(netClient);
+ _fsmPatcher = new FsmPatcher();
_commandManager = new ClientCommandManager();
var eventAggregator = new EventAggregator();
var clientApi = new ClientApi(this, _commandManager, uiManager, netClient, eventAggregator);
_addonManager = new ClientAddonManager(clientApi, _modSettings);
+ }
+
+ #region Internal client-manager methods
+
+ ///
+ /// Initialize the client manager by initializing other classes and setting, hooking, or otherwise handling things
+ /// that only need to be done once.
+ ///
+ public void Initialize(ServerManager serverManager) {
+ _playerManager.Initialize();
+ _animationManager.Initialize(_serverSettings);
+ _mapManager.Initialize();
+
+ _entityManager.Initialize();
+
+ _saveManager.Initialize();
+
+ CustomHooks.Initialize();
RegisterCommands();
-
+
ModHooks.FinishedLoadingModsHook += _addonManager.LoadAddons;
-
+
// Check if there is a valid authentication key and if not, generate a new one
- if (!AuthUtil.IsValidAuthKey(modSettings.AuthKey)) {
- modSettings.AuthKey = AuthUtil.GenerateAuthKey();
+ if (!AuthUtil.IsValidAuthKey(_modSettings.AuthKey)) {
+ _modSettings.AuthKey = AuthUtil.GenerateAuthKey();
}
-
+
// Then authorize the key on the locally hosted server
- serverManager.AuthorizeKey(modSettings.AuthKey);
-
- // Register packet handlers
- packetManager.RegisterClientPacketHandler(ClientPacketId.HelloClient, OnHelloClient);
- packetManager.RegisterClientPacketHandler(ClientPacketId.ServerClientDisconnect,
- OnDisconnect);
- packetManager.RegisterClientPacketHandler(ClientPacketId.PlayerConnect, OnPlayerConnect);
- packetManager.RegisterClientPacketHandler(ClientPacketId.PlayerDisconnect,
- OnPlayerDisconnect);
- packetManager.RegisterClientPacketHandler(ClientPacketId.PlayerEnterScene,
- OnPlayerEnterScene);
- packetManager.RegisterClientPacketHandler(ClientPacketId.PlayerAlreadyInScene,
- OnPlayerAlreadyInScene);
- packetManager.RegisterClientPacketHandler(ClientPacketId.PlayerLeaveScene,
- OnPlayerLeaveScene);
- packetManager.RegisterClientPacketHandler(ClientPacketId.PlayerUpdate, OnPlayerUpdate);
- packetManager.RegisterClientPacketHandler(ClientPacketId.PlayerMapUpdate,
- OnPlayerMapUpdate);
- packetManager.RegisterClientPacketHandler(ClientPacketId.EntityUpdate, OnEntityUpdate);
- packetManager.RegisterClientPacketHandler(ClientPacketId.ServerSettingsUpdated,
- OnServerSettingsUpdated);
- packetManager.RegisterClientPacketHandler(ClientPacketId.ChatMessage, OnChatMessage);
+ serverManager.AuthorizeKey(_modSettings.AuthKey);
// Register handlers for events from UI
- uiManager.ConnectInterface.ConnectButtonPressed += Connect;
- uiManager.ConnectInterface.DisconnectButtonPressed += () => Disconnect();
- uiManager.SettingsInterface.OnTeamRadioButtonChange += InternalChangeTeam;
- uiManager.SettingsInterface.OnSkinIdChange += InternalChangeSkin;
+ _uiManager.RequestClientConnectEvent += (address, port, username, autoConnect) => {
+ _autoConnect = autoConnect;
+ Connect(address, port, username);
+ };
+ _uiManager.RequestClientDisconnectEvent += Disconnect;
+ _uiManager.RequestServerStartHostEvent += _ => {
+ _saveManager.IsHostingServer = true;
+ };
+ _uiManager.RequestServerStopHostEvent += () => {
+ _saveManager.IsHostingServer = false;
+ };
UiManager.InternalChatBox.ChatInputEvent += OnChatInput;
- netClient.ConnectEvent += _ => uiManager.OnSuccessfulConnect();
- netClient.ConnectFailedEvent += OnConnectFailed;
-
- // Register the Hero Controller Start, which is when the local player spawns
- On.HeroController.Start += (orig, self) => {
- // Execute the original method
- orig(self);
- // If we are connect to a server, add a username to the player object
- if (netClient.IsConnected) {
- _playerManager.AddNameToPlayer(
- HeroController.instance.gameObject,
- _username,
- _playerManager.LocalPlayerTeam
- );
- }
- };
-
- // Register handlers for scene change and player update
- UnityEngine.SceneManagement.SceneManager.activeSceneChanged += OnSceneChange;
- On.HeroController.Update += OnPlayerUpdate;
+ _netClient.ConnectEvent += _ => _uiManager.OnSuccessfulConnect();
+ _netClient.ConnectFailedEvent += OnConnectFailed;
// Register client connect and timeout handler
- netClient.ConnectEvent += OnClientConnect;
- netClient.TimeoutEvent += OnTimeout;
+ _netClient.ConnectEvent += OnClientConnect;
+ _netClient.TimeoutEvent += OnTimeout;
+ }
+ ///
+ /// Register the hooks of the client manager and the internal instances.
+ ///
+ private void RegisterHooks() {
+ // Have internal components register their hooks
+ _playerManager.RegisterHooks();
+ _animationManager.RegisterHooks();
+ _mapManager.RegisterHooks();
+ _pauseManager.RegisterHooks();
+ _gamePatcher.RegisterHooks();
+ _fsmPatcher.RegisterHooks();
+
+ if (_fullSynchronisation) {
+ _entityManager.RegisterHooks();
+ _saveManager.RegisterHooks();
+ }
+
+ // Register handlers for various things
+ UnityEngine.SceneManagement.SceneManager.activeSceneChanged += OnSceneChange;
+ CustomHooks.HeroControllerStartAction += OnHeroControllerStart;
+ On.HeroController.Update += OnPlayerUpdate;
+
+ CustomHooks.AfterEnterSceneHeroTransformed += OnEnterScene;
+
// Register application quit handler
ModHooks.ApplicationQuitHook += OnApplicationQuit;
}
- #region Internal client-manager methods
+ ///
+ /// Deregister the hooks of the client manager and the internal instances.
+ ///
+ private void DeregisterHooks() {
+ // Have internal components deregister their hooks
+ _playerManager.DeregisterHooks();
+ _animationManager.DeregisterHooks();
+ _mapManager.DeregisterHooks();
+ _pauseManager.DeregisterHooks();
+ _gamePatcher.DeregisterHooks();
+ _fsmPatcher.DeregisterHooks();
+
+ if (_fullSynchronisation) {
+ _entityManager.DeregisterHooks();
+ _saveManager.DeregisterHooks();
+ }
+
+ // Deregister handlers for various things
+ UnityEngine.SceneManagement.SceneManager.activeSceneChanged -= OnSceneChange;
+ CustomHooks.HeroControllerStartAction -= OnHeroControllerStart;
+ On.HeroController.Update -= OnPlayerUpdate;
+
+ CustomHooks.AfterEnterSceneHeroTransformed -= OnEnterScene;
+
+ // Deregister application quit handler
+ ModHooks.ApplicationQuitHook -= OnApplicationQuit;
+ }
///
/// Register the default client commands.
///
private void RegisterCommands() {
- _commandManager.RegisterCommand(new ConnectCommand(this));
- _commandManager.RegisterCommand(new HostCommand(_serverManager));
_commandManager.RegisterCommand(new AddonCommand(_addonManager, _netClient));
}
+ ///
+ /// Register the packet handlers.
+ ///
+ private void RegisterPacketHandlers() {
+ _packetManager.RegisterClientUpdatePacketHandler(
+ ClientUpdatePacketId.ServerClientDisconnect,
+ OnDisconnect
+ );
+ _packetManager.RegisterClientUpdatePacketHandler(
+ ClientUpdatePacketId.PlayerConnect,
+ OnPlayerConnect
+ );
+ _packetManager.RegisterClientUpdatePacketHandler(
+ ClientUpdatePacketId.PlayerDisconnect,
+ OnPlayerDisconnect
+ );
+ _packetManager.RegisterClientUpdatePacketHandler(
+ ClientUpdatePacketId.PlayerEnterScene,
+ OnPlayerEnterScene
+ );
+ _packetManager.RegisterClientUpdatePacketHandler(
+ ClientUpdatePacketId.PlayerAlreadyInScene,
+ OnPlayerAlreadyInScene
+ );
+ _packetManager.RegisterClientUpdatePacketHandler(
+ ClientUpdatePacketId.PlayerLeaveScene,
+ OnPlayerLeaveScene
+ );
+ _packetManager.RegisterClientUpdatePacketHandler(
+ ClientUpdatePacketId.PlayerUpdate,
+ OnPlayerUpdate
+ );
+ _packetManager.RegisterClientUpdatePacketHandler(
+ ClientUpdatePacketId.PlayerMapUpdate,
+ OnPlayerMapUpdate
+ );
+ _packetManager.RegisterClientUpdatePacketHandler(
+ ClientUpdatePacketId.ServerSettingsUpdated,
+ OnServerSettingsUpdated
+ );
+ _packetManager.RegisterClientUpdatePacketHandler(
+ ClientUpdatePacketId.ChatMessage,
+ OnChatMessage
+ );
+ _packetManager.RegisterClientUpdatePacketHandler(
+ ClientUpdatePacketId.PlayerSetting,
+ OnPlayerSettingUpdate
+ );
+ _packetManager.RegisterClientUpdatePacketHandler(
+ ClientUpdatePacketId.PlayerDeath,
+ OnPlayerDeath
+ );
+
+ // Register packet handlers related to full synchronisation
+ if (_fullSynchronisation) {
+ _packetManager.RegisterClientUpdatePacketHandler(
+ ClientUpdatePacketId.EntitySpawn,
+ OnEntitySpawn
+ );
+ _packetManager.RegisterClientUpdatePacketHandler(
+ ClientUpdatePacketId.EntityUpdate,
+ OnEntityUpdate
+ );
+ _packetManager.RegisterClientUpdatePacketHandler(
+ ClientUpdatePacketId.ReliableEntityUpdate,
+ OnReliableEntityUpdate
+ );
+ _packetManager.RegisterClientUpdatePacketHandler(
+ ClientUpdatePacketId.SceneHostTransfer,
+ OnSceneHostTransfer
+ );
+ _packetManager.RegisterClientUpdatePacketHandler(
+ ClientUpdatePacketId.SaveUpdate,
+ OnSaveUpdate
+ );
+ }
+ }
+
+ ///
+ /// De-register packet handlers.
+ ///
+ private void DeregisterPacketHandlers() {
+ _packetManager.DeregisterClientUpdatePacketHandler(ClientUpdatePacketId.ServerClientDisconnect);
+ _packetManager.DeregisterClientUpdatePacketHandler(ClientUpdatePacketId.PlayerConnect);
+ _packetManager.DeregisterClientUpdatePacketHandler(ClientUpdatePacketId.PlayerDisconnect);
+ _packetManager.DeregisterClientUpdatePacketHandler(ClientUpdatePacketId.PlayerEnterScene);
+ _packetManager.DeregisterClientUpdatePacketHandler(ClientUpdatePacketId.PlayerAlreadyInScene);
+ _packetManager.DeregisterClientUpdatePacketHandler(ClientUpdatePacketId.PlayerLeaveScene);
+ _packetManager.DeregisterClientUpdatePacketHandler(ClientUpdatePacketId.PlayerUpdate);
+ _packetManager.DeregisterClientUpdatePacketHandler(ClientUpdatePacketId.PlayerMapUpdate);
+ _packetManager.DeregisterClientUpdatePacketHandler(ClientUpdatePacketId.ServerSettingsUpdated);
+ _packetManager.DeregisterClientUpdatePacketHandler(ClientUpdatePacketId.ChatMessage);
+ _packetManager.DeregisterClientUpdatePacketHandler(ClientUpdatePacketId.PlayerSetting);
+ _packetManager.DeregisterClientUpdatePacketHandler(ClientUpdatePacketId.PlayerDeath);
+
+ if (_fullSynchronisation) {
+ _packetManager.DeregisterClientUpdatePacketHandler(ClientUpdatePacketId.EntitySpawn);
+ _packetManager.DeregisterClientUpdatePacketHandler(ClientUpdatePacketId.EntityUpdate);
+ _packetManager.DeregisterClientUpdatePacketHandler(ClientUpdatePacketId.ReliableEntityUpdate);
+ _packetManager.DeregisterClientUpdatePacketHandler(ClientUpdatePacketId.SceneHostTransfer);
+ _packetManager.DeregisterClientUpdatePacketHandler(ClientUpdatePacketId.SaveUpdate);
+ }
+ }
+
///
/// Connect the client to the server with the given address, port and username.
///
/// The address of the server.
/// The port of the server.
/// The username of the client.
- public void Connect(string address, int port, string username) {
+ private void Connect(string address, int port, string username) {
Logger.Info($"Connecting client to server: {address}:{port} as {username}");
// Stop existing client
@@ -323,6 +506,10 @@ public void Disconnect() {
/// Internal logic for disconnecting from the server.
///
private void InternalDisconnect() {
+ Logger.Info("Disconnecting from server");
+
+ _autoConnect = false;
+
_netClient.Disconnect();
// Let the player manager know we disconnected
@@ -339,6 +526,10 @@ private void InternalDisconnect() {
if (UIManager.instance.uiState.Equals(UIState.PAUSED)) {
_pauseManager.SetTimeScale(0);
}
+
+ // Deregister the hooks and handlers
+ DeregisterHooks();
+ DeregisterPacketHandlers();
try {
DisconnectEvent?.Invoke();
@@ -352,19 +543,20 @@ private void InternalDisconnect() {
/// Callback method for when the connection to the server fails with a given result.
///
/// The result of the failed connection.
- private void OnConnectFailed(ConnectFailedResult result) {
+ private void OnConnectFailed(ConnectionFailedResult result) {
_uiManager.OnFailedConnect(result);
- if (result.Type == ConnectFailedResult.FailType.InvalidAddons) {
+ if (result.Reason == ConnectionFailedReason.InvalidAddons) {
// Inform the user of the correct addons that the server needs
UiManager.InternalChatBox.AddMessage("Server requires the following addons:");
// Keep track of addons that the client has that the server does not, by removing all addons
// that the server reports to have
var clientAddonData = _addonManager.GetNetworkedAddonData();
+ var serverAddonData = ((ConnectionInvalidAddonsResult) result).AddonData;
// First check for each of the addons that the server has, whether the client has them or not
- foreach (var addonData in result.AddonData) {
+ foreach (var addonData in serverAddonData) {
var addonName = addonData.Identifier;
var addonVersion = addonData.Version;
var message = $" {addonName} v{addonVersion}";
@@ -412,106 +604,65 @@ private void OnChatInput(string message) {
_netClient.UpdateManager.SetChatMessage(message);
}
- ///
- /// Internal method for changing the local player team.
- ///
- /// The new team.
- private void InternalChangeTeam(Team team) {
- if (!_netClient.IsConnected) {
- return;
- }
-
- if (!_serverSettings.TeamsEnabled) {
- Logger.Debug("Team are not enabled by server");
- return;
- }
-
- if (_playerManager.LocalPlayerTeam == team) {
- return;
- }
-
- _playerManager.OnLocalPlayerTeamUpdate(team);
-
- _netClient.UpdateManager.SetTeamUpdate(team);
-
- UiManager.InternalChatBox.AddMessage($"You are now in Team {team}");
- }
-
- ///
- /// Internal method for changing the local player skin.
- ///
- /// The ID of the new skin.
- private void InternalChangeSkin(byte skinId) {
- if (!_netClient.IsConnected) {
- return;
- }
-
- if (!_serverSettings.AllowSkins) {
- Logger.Debug("User changed skin ID, but skins are not allowed by server");
- return;
- }
-
- Logger.Debug($"Changed local player skin to ID: {skinId}");
-
- // Let the player manager handle the skin updating and send the change to the server
- _playerManager.UpdateLocalPlayerSkin(skinId);
- _netClient.UpdateManager.SetSkinUpdate(skinId);
- }
-
///
/// Callback method for when the net client establishes a connection with a server.
///
- /// The login response received from the server.
- private void OnClientConnect(LoginResponse loginResponse) {
- // First relay the addon order from the login response to the addon manager
- _addonManager.UpdateNetworkedAddonOrder(loginResponse.AddonOrder);
-
- // We should only be able to connect during a gameplay scene,
- // which is when the player is spawned already, so we can add the username
- _playerManager.AddNameToPlayer(HeroController.instance.gameObject, _username,
- _playerManager.LocalPlayerTeam);
-
- Logger.Info("Client is connected, sending Hello packet");
+ /// The server info received from the server.
+ private void OnClientConnect(ServerInfo serverInfo) {
+ Logger.Info("Received server info from server");
+
+ // Update the locally stored server settings
+ _serverSettings.SetAllProperties(serverInfo.ServerSettingsUpdate.ServerSettings);
+ // Call the event that the settings were updated
+ ServerSettingsChangedEvent?.Invoke(serverInfo.ServerSettingsUpdate.ServerSettings);
+
+ // Note whether full synchronisation is enabled
+ _fullSynchronisation = serverInfo.FullSynchronisation;
+
+ // Register hooks and packet handlers before we load into the game
+ RegisterHooks();
+ RegisterPacketHandlers();
+
+ // Relay the addon order from the server info to the addon manager
+ _addonManager.UpdateNetworkedAddonOrder(serverInfo.AddonOrder);
+
+ if (!_autoConnect) {
+ if (_fullSynchronisation) {
+ // This was not an auto-connect and full synchronisation is enabled, so we set save data.
+ // Otherwise, with hosting we already have the save data, or with no full synchronisation, we don't
+ // need it.
+ _saveManager.SetSaveWithData(serverInfo.CurrentSave);
+ _uiManager.EnterGameFromMultiplayerMenu(serverInfo.CurrentSave.NewForPlayer);
+ } else {
+ // This was not an auto-connect and full synchronisation is disabled, so we need to prompt the user
+ // with a local save file that they want to use
+ _uiManager.OpenSaveSlotSelection(saveSelected => {
+ // If this callback executes, but we have not selected a save (by pressing the back button on the
+ // save selection screen, we need to disconnect from the server again, because we are not entering
+ // the world
+ if (saveSelected) {
+ return;
+ }
- // If we are in a non-gameplay scene, we transmit that we are not active yet
- var currentSceneName = SceneUtil.GetCurrentSceneName();
- if (SceneUtil.IsNonGameplayScene(currentSceneName)) {
- Logger.Error(
- $"Client connected during a non-gameplay scene named {currentSceneName}, this should never happen!");
- return;
+ Disconnect();
+ });
+ }
}
- var transform = HeroController.instance.transform;
- var position = transform.position;
-
- Logger.Info("Sending Hello packet");
-
- _netClient.UpdateManager.SetHelloServerData(
- SceneUtil.GetCurrentSceneName(),
- new Vector2(position.x, position.y),
- transform.localScale.x > 0,
- (ushort) AnimationManager.GetCurrentAnimationClip()
- );
-
- // Since we are probably in the pause menu when we connect, set the timescale so the game
- // is running while paused
- _pauseManager.SetTimeScale(1.0f);
-
- UiManager.InternalChatBox.AddMessage("You are connected to the server");
- }
-
- ///
- /// Callback method for when we receive the HelloClient data.
- ///
- /// The HelloClient packet data.
- private void OnHelloClient(HelloClient helloClient) {
- Logger.Info("Received HelloClient from server");
-
// Fill the player data dictionary with the info from the packet
- foreach (var (id, username) in helloClient.ClientInfo) {
+ foreach (var (id, username) in serverInfo.PlayerInfo) {
_playerData[id] = new ClientPlayerData(id, username);
}
+ // Add the username to the player if we are in-game already
+ if (HeroController.instance && HeroController.instance.gameObject) {
+ _playerManager.AddNameToPlayer(
+ HeroController.instance.gameObject,
+ _username,
+ _playerManager.LocalPlayerTeam
+ );
+ }
+
try {
ConnectEvent?.Invoke();
} catch (Exception e) {
@@ -519,6 +670,21 @@ private void OnHelloClient(HelloClient helloClient) {
$"Exception thrown while invoking Connect event:\n{e}");
}
}
+
+ ///
+ /// Callback method for when the HeroController is started so we can add the username to the player object.
+ ///
+ private void OnHeroControllerStart() {
+ Logger.Debug($"OnHeroControllerStart called, netclient connected: {_netClient.IsConnected}");
+
+ if (_netClient.IsConnected) {
+ _playerManager.AddNameToPlayer(
+ HeroController.instance.gameObject,
+ _username,
+ _playerManager.LocalPlayerTeam
+ );
+ }
+ }
///
/// Callback method for when we receive a server disconnect.
@@ -533,6 +699,8 @@ private void OnDisconnect(ServerClientDisconnect disconnect) {
} else if (disconnect.Reason == DisconnectReason.Shutdown) {
UiManager.InternalChatBox.AddMessage("You are disconnected from the server (server is shutting down)");
}
+
+ _uiManager.ReturnToMainMenuFromGame();
// Disconnect without sending the server that we disconnect, because the server knows that already
InternalDisconnect();
@@ -606,17 +774,35 @@ private void OnPlayerAlreadyInScene(ClientPlayerAlreadyInScene alreadyInScene) {
OnPlayerEnterScene(playerEnterScene);
}
- if (alreadyInScene.SceneHost) {
- // Notify the entity manager that we are scene host
- _entityManager.OnBecomeSceneHost();
- } else {
- // Notify the entity manager that we are scene client (non-host)
- _entityManager.OnBecomeSceneClient();
- }
+ if (_fullSynchronisation) {
+ if (alreadyInScene.SceneHost) {
+ // Notify the entity manager that we are scene host
+ _entityManager.InitializeSceneHost();
+ } else {
+ // Notify the entity manager that we are scene client (non-host)
+ _entityManager.InitializeSceneClient();
+ }
+
+ foreach (var entitySpawn in alreadyInScene.EntitySpawnList) {
+ Logger.Info(
+ $"Updating already in scene spawned entity with ID: {entitySpawn.Id}, types: {entitySpawn.SpawningType}, {entitySpawn.SpawnedType}");
+ _entityManager.SpawnEntity(entitySpawn.Id, entitySpawn.SpawningType, entitySpawn.SpawnedType);
+ }
- // Whether there were players in the scene or not, we have now determined whether
- // we are the scene host
- _sceneHostDetermined = true;
+ foreach (var entityUpdate in alreadyInScene.EntityUpdateList) {
+ Logger.Info($"Updating already in scene entity with ID: {entityUpdate.Id}");
+ _entityManager.HandleEntityUpdate(entityUpdate, true);
+ }
+
+ foreach (var entityUpdate in alreadyInScene.ReliableEntityUpdateList) {
+ Logger.Info($"Updating already in scene reliable entity data with ID: {entityUpdate.Id}");
+ _entityManager.HandleReliableEntityUpdate(entityUpdate, true);
+ }
+
+ // Whether there were players in the scene or not, we have now determined whether
+ // we are the scene host
+ _sceneHostDetermined = true;
+ }
}
///
@@ -657,8 +843,8 @@ private void OnPlayerEnterScene(ClientPlayerEnterScene enterSceneData) {
///
/// Callback method for when a player leaves our scene.
///
- /// The generic client packet data.
- private void OnPlayerLeaveScene(GenericClientData data) {
+ /// The client player leave scene packet data.
+ private void OnPlayerLeaveScene(ClientPlayerLeaveScene data) {
var id = data.Id;
Logger.Info($"Player {id} left scene");
@@ -668,6 +854,11 @@ private void OnPlayerLeaveScene(GenericClientData data) {
return;
}
+ if (UnityEngine.SceneManagement.SceneManager.GetActiveScene().name != data.SceneName) {
+ Logger.Info($"Player is leaving other scene than we are currently in ({data.SceneName}), ignoring");
+ return;
+ }
+
// Recycle corresponding player
_playerManager.RecyclePlayer(id);
@@ -723,38 +914,71 @@ private void OnPlayerUpdate(PlayerUpdate playerUpdate) {
private void OnPlayerMapUpdate(PlayerMapUpdate playerMapUpdate) {
_mapManager.UpdatePlayerHasIcon(playerMapUpdate.Id, playerMapUpdate.HasIcon);
}
+
+ ///
+ /// Callback method for when an entity spawn is received.
+ ///
+ /// The EntitySpawn packet data.
+ private void OnEntitySpawn(EntitySpawn entitySpawn) {
+ if (!_fullSynchronisation) {
+ return;
+ }
+
+ _entityManager.SpawnEntity(entitySpawn.Id, entitySpawn.SpawningType, entitySpawn.SpawnedType);
+ }
///
/// Callback method for when an entity update is received.
///
/// The EntityUpdate packet data.
private void OnEntityUpdate(EntityUpdate entityUpdate) {
+ if (!_fullSynchronisation) {
+ return;
+ }
+
// We only propagate entity updates to the entity manager if we have determined the scene host
if (!_sceneHostDetermined) {
return;
}
- if (entityUpdate.UpdateTypes.Contains(EntityUpdateType.Position)) {
- _entityManager.UpdateEntityPosition((EntityType) entityUpdate.EntityType, entityUpdate.Id,
- entityUpdate.Position);
+ _entityManager.HandleEntityUpdate(entityUpdate);
+ }
+
+ ///
+ /// Callback method for when a reliable entity update is received.
+ ///
+ /// The ReliableEntityUpdate packet data.
+ private void OnReliableEntityUpdate(ReliableEntityUpdate entityUpdate) {
+ if (!_fullSynchronisation) {
+ return;
}
- if (entityUpdate.UpdateTypes.Contains(EntityUpdateType.State)) {
- List variables;
+ // We only propagate entity updates to the entity manager if we have determined the scene host
+ if (!_sceneHostDetermined) {
+ return;
+ }
- if (entityUpdate.UpdateTypes.Contains(EntityUpdateType.Variables)) {
- variables = entityUpdate.Variables;
- } else {
- variables = new List();
- }
+ _entityManager.HandleReliableEntityUpdate(entityUpdate);
+ }
+
+ ///
+ /// Callback method for when a host transfer is received.
+ ///
+ /// The HostTransfer packet data.
+ private void OnSceneHostTransfer(HostTransfer hostTransfer) {
+ if (!_fullSynchronisation) {
+ return;
+ }
- _entityManager.UpdateEntityState(
- (EntityType) entityUpdate.EntityType,
- entityUpdate.Id,
- entityUpdate.State,
- variables
- );
+ Logger.Info($"Received scene host transfer for scene: {hostTransfer.SceneName}");
+
+ var currentScene = UnityEngine.SceneManagement.SceneManager.GetActiveScene().name;
+ if (currentScene != hostTransfer.SceneName) {
+ Logger.Info($" Current scene ({currentScene}) does not match scene for host transfer, ignoring");
+ return;
}
+
+ _entityManager.BecomeSceneHost();
}
///
@@ -770,33 +994,35 @@ private void OnServerSettingsUpdated(ServerSettingsUpdate update) {
var teamsChanged = false;
var allowSkinsChanged = false;
+ var newServerSettings = update.ServerSettings;
+
// Check whether the PvP state changed
- if (_serverSettings.IsPvpEnabled != update.ServerSettings.IsPvpEnabled) {
+ if (_serverSettings.IsPvpEnabled != newServerSettings.IsPvpEnabled) {
pvpChanged = true;
- var message = $"PvP is now {(update.ServerSettings.IsPvpEnabled ? "enabled" : "disabled")}";
+ var message = $"PvP is now {(newServerSettings.IsPvpEnabled ? "enabled" : "disabled")}";
UiManager.InternalChatBox.AddMessage(message);
Logger.Info(message);
}
// Check whether the body damage state changed
- if (_serverSettings.IsBodyDamageEnabled != update.ServerSettings.IsBodyDamageEnabled) {
+ if (_serverSettings.IsBodyDamageEnabled != newServerSettings.IsBodyDamageEnabled) {
bodyDamageChanged = true;
var message =
- $"Body damage is now {(update.ServerSettings.IsBodyDamageEnabled ? "enabled" : "disabled")}";
+ $"Body damage is now {(newServerSettings.IsBodyDamageEnabled ? "enabled" : "disabled")}";
UiManager.InternalChatBox.AddMessage(message);
Logger.Info(message);
}
// Check whether the always show map icons state changed
- if (_serverSettings.AlwaysShowMapIcons != update.ServerSettings.AlwaysShowMapIcons) {
+ if (_serverSettings.AlwaysShowMapIcons != newServerSettings.AlwaysShowMapIcons) {
alwaysShowMapChanged = true;
var message =
- $"Map icons are now{(update.ServerSettings.AlwaysShowMapIcons ? "" : " not")} always visible";
+ $"Map icons are now{(newServerSettings.AlwaysShowMapIcons ? "" : " not")} always visible";
UiManager.InternalChatBox.AddMessage(message);
Logger.Info(message);
@@ -804,48 +1030,50 @@ private void OnServerSettingsUpdated(ServerSettingsUpdate update) {
// Check whether the wayward compass broadcast state changed
if (_serverSettings.OnlyBroadcastMapIconWithWaywardCompass !=
- update.ServerSettings.OnlyBroadcastMapIconWithWaywardCompass) {
+ newServerSettings.OnlyBroadcastMapIconWithWaywardCompass) {
onlyCompassChanged = true;
var message =
- $"Map icons are {(update.ServerSettings.OnlyBroadcastMapIconWithWaywardCompass ? "now only" : "not")} broadcast when wearing the Wayward Compass charm";
+ $"Map icons are {(newServerSettings.OnlyBroadcastMapIconWithWaywardCompass ? "now only" : "not")} broadcast when wearing the Wayward Compass charm";
UiManager.InternalChatBox.AddMessage(message);
Logger.Info(message);
}
// Check whether the display names setting changed
- if (_serverSettings.DisplayNames != update.ServerSettings.DisplayNames) {
+ if (_serverSettings.DisplayNames != newServerSettings.DisplayNames) {
displayNamesChanged = true;
- var message = $"Names are {(update.ServerSettings.DisplayNames ? "now" : "no longer")} displayed";
+ var message = $"Names are {(newServerSettings.DisplayNames ? "now" : "no longer")} displayed";
UiManager.InternalChatBox.AddMessage(message);
Logger.Info(message);
}
// Check whether the teams enabled setting changed
- if (_serverSettings.TeamsEnabled != update.ServerSettings.TeamsEnabled) {
+ if (_serverSettings.TeamsEnabled != newServerSettings.TeamsEnabled) {
teamsChanged = true;
- var message = $"Teams are {(update.ServerSettings.TeamsEnabled ? "now" : "no longer")} enabled";
+ var message = $"Teams are {(newServerSettings.TeamsEnabled ? "now" : "no longer")} enabled";
UiManager.InternalChatBox.AddMessage(message);
Logger.Info(message);
}
// Check whether allow skins setting changed
- if (_serverSettings.AllowSkins != update.ServerSettings.AllowSkins) {
+ if (_serverSettings.AllowSkins != newServerSettings.AllowSkins) {
allowSkinsChanged = true;
- var message = $"Skins are {(update.ServerSettings.AllowSkins ? "now" : "no longer")} enabled";
+ var message = $"Skins are {(newServerSettings.AllowSkins ? "now" : "no longer")} enabled";
UiManager.InternalChatBox.AddMessage(message);
Logger.Info(message);
}
// Update the settings so callbacks can read updated values
- _serverSettings.SetAllProperties(update.ServerSettings);
+ _serverSettings.SetAllProperties(newServerSettings);
+ // Call the event that the settings were updated
+ ServerSettingsChangedEvent?.Invoke(newServerSettings);
// Only update the player manager if the either PvP or body damage have been changed
if (pvpChanged || bodyDamageChanged || displayNamesChanged) {
@@ -860,17 +1088,22 @@ private void OnServerSettingsUpdated(ServerSettingsUpdate update) {
// If the teams setting changed, we invoke the registered event handler if they exist
if (teamsChanged) {
- // If the team setting was disabled, we reset all teams
+ // If the team setting was disabled, we reset all teams and call the event
if (!_serverSettings.TeamsEnabled) {
_playerManager.ResetAllTeams();
+
+ TeamChangedEvent?.Invoke(Team.None);
}
- _uiManager.OnTeamSettingChange();
+ // _uiManager.OnTeamSettingChange();
}
- // If the allow skins setting changed and it is no longer allowed, we reset all existing skins
+ // If the allow skins setting changed, and it is no longer allowed, we reset all existing skins and call the
+ // event
if (allowSkinsChanged && !_serverSettings.AllowSkins) {
_playerManager.ResetAllPlayerSkins();
+
+ SkinChangedEvent?.Invoke(0);
}
}
@@ -881,6 +1114,7 @@ private void OnServerSettingsUpdated(ServerSettingsUpdate update) {
/// The new scene instance.
private void OnSceneChange(Scene oldScene, Scene newScene) {
Logger.Info($"Scene changed from {oldScene.name} to {newScene.name}");
+ Logger.Debug($" Current scene: {UnityEngine.SceneManagement.SceneManager.GetActiveScene().name}");
// Always recycle existing players, because we changed scenes
_playerManager.RecycleAllPlayers();
@@ -895,17 +1129,14 @@ private void OnSceneChange(Scene oldScene, Scene newScene) {
return;
}
- _sceneChanged = true;
-
// Reset the status of whether we determined the scene host or not
_sceneHostDetermined = false;
- // Ignore scene changes from and to non-gameplay scenes
- if (SceneUtil.IsNonGameplayScene(oldScene.name) && SceneUtil.IsNonGameplayScene(newScene.name)) {
- return;
+ // If the old scene is a gameplay scene, we need to notify the server that we left
+ if (!SceneUtil.IsNonGameplayScene(oldScene.name) && oldScene.name == _lastScene) {
+ Logger.Debug("Left scene, sending to server");
+ _netClient.UpdateManager.SetLeftScene(oldScene.name);
}
-
- _netClient.UpdateManager.SetLeftScene();
}
///
@@ -936,39 +1167,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;
@@ -981,6 +1180,42 @@ 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() {
+ var sceneName = UnityEngine.SceneManagement.SceneManager.GetActiveScene().name;
+
+ Logger.Debug($"OnEnterScene, scene: {sceneName}");
+
+ _lastScene = sceneName;
+
+ // 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) {
+ 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.
///
@@ -989,6 +1224,52 @@ private void OnChatMessage(ChatMessage chatMessage) {
UiManager.InternalChatBox.AddMessage(chatMessage.Message);
}
+ ///
+ /// Callback method for when a player setting update is received.
+ ///
+ /// The packet data.
+ private void OnPlayerSettingUpdate(ClientPlayerSettingUpdate settingUpdate) {
+ if (settingUpdate.UpdateTypes.Contains(PlayerSettingUpdateType.Team)) {
+ if (settingUpdate.Self) {
+ _playerManager.OnPlayerTeamUpdate(true, settingUpdate.Team);
+
+ TeamChangedEvent?.Invoke(settingUpdate.Team);
+ } else {
+ _playerManager.OnPlayerTeamUpdate(false, settingUpdate.Team, settingUpdate.Id);
+ }
+ }
+
+ if (settingUpdate.UpdateTypes.Contains(PlayerSettingUpdateType.Skin)) {
+ if (settingUpdate.Self) {
+ _playerManager.OnPlayerSkinUpdate(true, settingUpdate.SkinId);
+
+ SkinChangedEvent?.Invoke(settingUpdate.SkinId);
+ } else {
+ _playerManager.OnPlayerSkinUpdate(false, settingUpdate.SkinId, settingUpdate.Id);
+ }
+ }
+ }
+
+ ///
+ /// Callback method for when a player death packet is received.
+ ///
+ /// The GenericClientData packet data.
+ private void OnPlayerDeath(GenericClientData deathData) {
+ _animationManager.OnPlayerDeath(deathData);
+ }
+
+ ///
+ /// Callback method for when a save update is received.
+ ///
+ /// The SaveUpdate packet data.
+ private void OnSaveUpdate(SaveUpdate saveUpdate) {
+ if (!_fullSynchronisation) {
+ return;
+ }
+
+ _saveManager.UpdateSaveWithData(saveUpdate);
+ }
+
///
/// Callback method for when the net client is timed out.
///
@@ -997,7 +1278,10 @@ private void OnTimeout() {
return;
}
- Logger.Info("Connection to server timed out, disconnecting");
+ Logger.Info("Connection to server timed out, moving to main menu");
+
+ _uiManager.ReturnToMainMenuFromGame();
+
UiManager.InternalChatBox.AddMessage("You are disconnected from the server (server timed out)");
Disconnect();
@@ -1036,20 +1320,10 @@ public bool TryGetPlayer(ushort id, out IClientPlayer player) {
///
public void ChangeTeam(Team team) {
- if (!_netClient.IsConnected) {
- throw new InvalidOperationException("Client is not connected, cannot change team");
- }
-
- InternalChangeTeam(team);
}
///
public void ChangeSkin(byte skinId) {
- if (!_netClient.IsConnected) {
- throw new InvalidOperationException("Client is not connected, cannot change skin");
- }
-
- InternalChangeSkin(skinId);
}
#endregion
diff --git a/HKMP/Game/Client/CustomHooks.cs b/HKMP/Game/Client/CustomHooks.cs
new file mode 100644
index 00000000..282c6d28
--- /dev/null
+++ b/HKMP/Game/Client/CustomHooks.cs
@@ -0,0 +1,262 @@
+using System;
+using System.Reflection;
+using Hkmp.Logging;
+using HutongGames.PlayMaker;
+using HutongGames.PlayMaker.Actions;
+using Mono.Cecil.Cil;
+using MonoMod.Cil;
+using MonoMod.RuntimeDetour;
+using UnityEngine.Audio;
+
+namespace Hkmp.Game.Client;
+
+// TODO: create method for de-registering the hooks
+///
+/// Static 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 static 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 static ILHook _heroControllerEnterSceneIlHook;
+ ///
+ /// IL Hook instance for the HeroController Respawn hook.
+ ///
+ private static ILHook _heroControllerRespawnIlHook;
+
+ ///
+ /// Event for when the player object is done being transformed (changed position, scale) after entering a scene.
+ ///
+ public static event Action AfterEnterSceneHeroTransformed;
+
+ ///
+ /// Event for when the AudioManager.ApplyMusicCue method is called from the ApplyMusicCue FSM action.
+ ///
+ public static event Action ApplyMusicCueFromFsmAction;
+
+ ///
+ /// Event for when the AudioMixerSnapshot.TransitionTo method is called from the TransitionToAudioSnapshot FSM
+ /// action.
+ ///
+ public static event Action TransitionToAudioSnapshotFromFsmAction;
+
+ ///
+ /// Internal event for .
+ ///
+ private static event Action HeroControllerStartActionInternal;
+
+ ///
+ /// Event that executes when the HeroController starts or executes its subscriber immediately if the HeroContoller
+ /// is already active.
+ ///
+ public static event Action HeroControllerStartAction {
+ add {
+ if (HeroController.UnsafeInstance) {
+ value.Invoke();
+ }
+
+ HeroControllerStartActionInternal += value;
+ }
+
+ remove => HeroControllerStartActionInternal -= value;
+ }
+
+ ///
+ /// Initialize the class by registering the IL/On hooks.
+ ///
+ public static 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.HutongGames.PlayMaker.Actions.ApplyMusicCue.OnEnter += ApplyMusicCueOnEnter;
+ IL.HutongGames.PlayMaker.Actions.TransitionToAudioSnapshot.OnEnter += TransitionToAudioSnapshotOnEnter;
+
+ On.HeroController.Start += HeroControllerOnStart;
+ }
+
+ ///
+ /// IL Hook for the HeroController Start method. Calls an event within the method.
+ ///
+ private static 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 static 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 static 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 static 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 static void EmitAfterEnterSceneEventHeroInPosition(ILCursor c) {
+ c.GotoNext(
+ MoveType.After,
+ HeroInPositionInstructions
+ );
+
+ c.EmitDelegate(() => { AfterEnterSceneHeroTransformed?.Invoke(); });
+ }
+
+ ///
+ /// IL Hook for the ApplyMusicCue OnEnter method. Calls an event in the method after the ApplyMusicCue call is
+ /// made.
+ ///
+ private static void ApplyMusicCueOnEnter(ILContext il) {
+ try {
+ // Create a cursor for this context
+ var c = new ILCursor(il);
+
+ // IL_005d: ldc.i4.0
+ // IL_005e: callvirt instance void AudioManager::ApplyMusicCue(class MusicCue, float32, float32, bool)
+ c.GotoNext(
+ MoveType.After,
+ i => i.MatchLdcI4(0),
+ i => i.MatchCallvirt(typeof(AudioManager), "ApplyMusicCue")
+ );
+
+ // Put the instance of the ApplyMusicCue class onto the stack
+ c.Emit(OpCodes.Ldarg_0);
+
+ // Emit a delegate for firing the event with the ApplyMusicCue instance
+ c.EmitDelegate>(action => { ApplyMusicCueFromFsmAction?.Invoke(action); });
+ } catch (Exception e) {
+ Logger.Error($"Could not change ApplyMusicCueOnEnter IL: \n{e}");
+ }
+ }
+
+ ///
+ /// IL Hook for the TransitionToAudioSnapshot OnEnter method. Calls an event in the method after the TransitionTo
+ /// call is made.
+ ///
+ private static void TransitionToAudioSnapshotOnEnter(ILContext il) {
+ try {
+ // Create a cursor for this context
+ var c = new ILCursor(il);
+
+ // IL_0021: callvirt instance float32 [PlayMaker]HutongGames.PlayMaker.FsmFloat::get_Value()
+ // IL_0026: callvirt instance void [UnityEngine.AudioModule]UnityEngine.Audio.AudioMixerSnapshot::TransitionTo(float32)
+ c.GotoNext(
+ MoveType.After,
+ i => i.MatchCallvirt(typeof(FsmFloat), "get_Value"),
+ i => i.MatchCallvirt(typeof(AudioMixerSnapshot), "TransitionTo")
+ );
+
+ // Put the instance of the TransitionToAudioSnapshot class onto the stack
+ c.Emit(OpCodes.Ldarg_0);
+
+ // Emit a delegate for firing the event with the TransitionToAudioSnapshot instance
+ c.EmitDelegate>(action => { TransitionToAudioSnapshotFromFsmAction?.Invoke(action); });
+ } catch (Exception e) {
+ Logger.Error($"Could not change TransitionToAudioSnapshotOnEnter IL: \n{e}");
+ }
+ }
+
+ ///
+ /// On hook for when the HeroController starts, so we can invoke our custom event.
+ ///
+ private static void HeroControllerOnStart(On.HeroController.orig_Start orig, HeroController self) {
+ orig(self);
+ HeroControllerStartActionInternal?.Invoke();
+ }
+}
diff --git a/HKMP/Game/Client/Entity/Action/ActionRegistry.cs b/HKMP/Game/Client/Entity/Action/ActionRegistry.cs
new file mode 100644
index 00000000..27cb3623
--- /dev/null
+++ b/HKMP/Game/Client/Entity/Action/ActionRegistry.cs
@@ -0,0 +1,84 @@
+using System.Collections.Generic;
+using System.Linq;
+using Hkmp.Util;
+using HutongGames.PlayMaker;
+using Newtonsoft.Json;
+using Logger = Hkmp.Logging.Logger;
+
+namespace Hkmp.Game.Client.Entity.Action;
+
+///
+/// Static class that manages loading and storing of action data. Specifically, which actions execute every frame.
+///
+internal static class ActionRegistry {
+ ///
+ /// The file path of the embedded resource file for the action registry.
+ ///
+ private const string ActionRegistryFilePath = "Hkmp.Resource.action-registry.json";
+
+ ///
+ /// List of all entity registry entries that are loaded from the embedded file.
+ ///
+ private static List Entries { get; }
+
+ static ActionRegistry() {
+ Entries = FileUtil.LoadObjectFromEmbeddedJson>(ActionRegistryFilePath);
+ if (Entries == null) {
+ Logger.Warn("Could not load action registry");
+ }
+ }
+
+ ///
+ /// Checks whether the given FSM action is an action that executes continuously. This is the case for actions
+ /// that have the "everyFrame" field and the value for this field is true or in several other specific cases
+ /// (such as actions related to collisions).
+ ///
+ /// The FSM action to check.
+ /// true if the action executes continuously; otherwise false.
+ public static bool IsActionContinuous(FsmStateAction action) {
+ var entry = Entries.FirstOrDefault(entry => entry.Type == action.GetType().Name);
+ if (entry == null) {
+ return false;
+ }
+
+ if (entry.UpdateField == null) {
+ return true;
+ }
+
+ var type = action.GetType();
+ var fieldInfo = type.GetField(entry.UpdateField);
+ if (fieldInfo == null) {
+ Logger.Warn($"Could not find field on FSM state action class: {type}, {entry.UpdateField}");
+ return false;
+ }
+
+ var value = fieldInfo.GetValue(action);
+ if (value is bool boolValue) {
+ return boolValue;
+ }
+
+ if (value is FsmInt fsmIntValue) {
+ return fsmIntValue.Value > 0;
+ }
+
+ Logger.Warn($"Could not find type of the field on FSM state action class: {type}, {entry.UpdateField}");
+ return false;
+ }
+}
+
+///
+/// Class representing a single entry in the action registry that contains the relevant data for an action.
+///
+internal class ActionRegistryEntry {
+ ///
+ /// The type of the action.
+ ///
+ [JsonProperty("type")]
+ public string Type { get; set; }
+
+ ///
+ /// The name of the field that controls whether the actions is checked every frame.
+ ///
+ [JsonProperty("update_field")]
+ public string UpdateField { get; set; }
+}
diff --git a/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs b/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs
new file mode 100644
index 00000000..0b78dde0
--- /dev/null
+++ b/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs
@@ -0,0 +1,3240 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Reflection;
+using Hkmp.Networking.Packet.Data;
+using Hkmp.Util;
+using HutongGames.PlayMaker;
+using HutongGames.PlayMaker.Actions;
+using Modding;
+using Mono.Cecil.Cil;
+using MonoMod.Cil;
+using UnityEngine;
+using Logger = Hkmp.Logging.Logger;
+using Object = UnityEngine.Object;
+using Random = UnityEngine.Random;
+
+// ReSharper disable UnusedMember.Local
+// ReSharper disable UnusedParameter.Local
+
+namespace Hkmp.Game.Client.Entity.Action;
+
+///
+/// Static class containing method that transform FSM actions into network-able data and applying networked data
+/// into the FSM actions implementations.
+///
+internal static class EntityFsmActions {
+ ///
+ /// The prefix of a method name that transforms an FSM action into network-able data.
+ ///
+ private const string GetMethodNamePrefix = "Get";
+ ///
+ /// The prefix of a method name that applies network data into an FSM action.
+ ///
+ private const string ApplyMethodNamePrefix = "Apply";
+
+ ///
+ /// Binding flags for accessing the private static methods in this class.
+ ///
+ private const BindingFlags StaticNonPublicFlags = BindingFlags.Static | BindingFlags.NonPublic;
+
+ ///
+ /// Set containing types of actions that are supported for transformation by a method in this class.
+ ///
+ public static readonly HashSet SupportedActionTypes = new();
+
+ ///
+ /// Event that is called when an entity is spawned from an object.
+ ///
+ public static event Func EntitySpawnEvent;
+
+ ///
+ /// Dictionary mapping a type of an FSM action to the corresponding method info of the "get" method in this class.
+ ///
+ private static readonly Dictionary TypeGetMethodInfos = new();
+ ///
+ /// Dictionary mapping a type of an FSM action to the corresponding method info of the "apply" method in this class.
+ ///
+ private static readonly Dictionary TypeApplyMethodInfos = new();
+
+ ///
+ /// Dictionary containing queues of objects for a FSM action that has been executed on a host entity.
+ /// Used to log the results of random calls to network to clients.
+ ///
+ private static readonly Dictionary> RandomActionValues = new();
+
+ ///
+ /// List of actions that are executing while in a state and need to be stopped again when the state is exited.
+ ///
+ private static readonly List ActionsInState = new();
+
+ ///
+ /// Static constructor that initializes the set and dictionaries by checking all methods in the class.
+ ///
+ ///
+ static EntityFsmActions() {
+ var methodInfos = typeof(EntityFsmActions).GetMethods(StaticNonPublicFlags);
+
+ foreach (var methodInfo in methodInfos) {
+ var parameterInfos = methodInfo.GetParameters();
+ if (parameterInfos.Length != 2) {
+ // Can't be a method that gets or applies entity network data
+ continue;
+ }
+
+ // Filter out the base methods
+ var parameterType = parameterInfos[1].ParameterType;
+ if (parameterType.IsAbstract || !parameterType.IsSubclassOf(typeof(FsmStateAction))) {
+ continue;
+ }
+
+ SupportedActionTypes.Add(parameterType);
+
+ if (methodInfo.Name.StartsWith(GetMethodNamePrefix)) {
+ TypeGetMethodInfos.Add(parameterType, methodInfo);
+ } else if (methodInfo.Name.StartsWith(ApplyMethodNamePrefix)) {
+ TypeApplyMethodInfos.Add(parameterType, methodInfo);
+ } else {
+ throw new Exception("Method was defined that does not adhere to the method naming");
+ }
+ }
+
+ // Register the IL hooks for modifying FSM action methods
+ IL.HutongGames.PlayMaker.Actions.FlingObjectsFromGlobalPool.OnEnter += FlingObjectsFromGlobalPoolOnEnter;
+ IL.HutongGames.PlayMaker.Actions.FlingObjectsFromGlobalPoolVel.OnEnter += FlingObjectsFromGlobalPoolVelOnEnter;
+ IL.HutongGames.PlayMaker.Actions.FlingObjectsFromGlobalPoolTime.OnUpdate += FlingObjectsFromGlobalPoolTimeOnUpdate;
+ IL.HutongGames.PlayMaker.Actions.GetRandomChild.DoGetRandomChild += GetRandomChildOnDoGetRandomChild;
+
+ // Register IL hooks for the OnEnter method of certain classes. These OnEnter methods do not
+ // have a method body and thus no IL instructions (apart from ret). Hooking this in the FsmActionHooks class
+ // will not work, so we emit a NOP instruction to the body to make it hookable
+ void EmitNop(ILContext il) => new ILCursor(il).Emit(OpCodes.Nop);
+
+ IL.HutongGames.PlayMaker.Actions.FlingObjectsFromGlobalPoolTime.OnEnter += EmitNop;
+ IL.SpawnBloodTime.OnEnter += EmitNop;
+ }
+
+ ///
+ /// Gets network-able data from the given action and puts it in the given instance.
+ ///
+ /// The instance to put the data into.
+ /// The action to transform.
+ /// Whether from this action network-able data was made.
+ /// Thrown if there is no suitable method for the action and thus
+ /// no network data is written.
+ public static bool GetNetworkDataFromAction(EntityNetworkData data, FsmStateAction action) {
+ var actionType = action.GetType();
+ if (!TypeGetMethodInfos.TryGetValue(actionType, out var methodInfo)) {
+ throw new InvalidOperationException(
+ $"Given action type: {action.GetType()} does not have an associated method to get");
+ }
+
+ var returnObject = methodInfo.Invoke(
+ null,
+ StaticNonPublicFlags,
+ null,
+ new object[] { data, action },
+ null!
+ );
+
+ // Return whether the return object is a bool and has the value 'true'
+ return returnObject is true;
+ }
+
+ ///
+ /// Reads networked data from the given instance and mimics the execution of the given FSM action.
+ ///
+ /// The instance from which to get the data.
+ /// The FSM action to mimic execution for.
+ /// Thrown if there is no suitable method for the action and thus
+ /// no FSM action will be mimicked.
+ public static void ApplyNetworkDataFromAction(EntityNetworkData data, FsmStateAction action) {
+ var actionType = action.GetType();
+ if (!TypeApplyMethodInfos.TryGetValue(actionType, out var methodInfo)) {
+ throw new InvalidOperationException(
+ $"Given action type: {action.GetType()} does not have an associated method to apply");
+ }
+
+ try {
+ methodInfo.Invoke(
+ null,
+ StaticNonPublicFlags,
+ null,
+ new object[] { data, action },
+ null!
+ );
+ } catch (Exception e) {
+ Logger.Warn($"Apply method threw exception: {e.GetType()}, {e.Message}, {e.StackTrace}");
+
+ e = e.InnerException;
+ while (e != null) {
+ Logger.Warn($" Inner exception: {e.GetType()}, {e.Message}, {e.StackTrace}");
+
+ e = e.InnerException;
+ }
+ }
+ }
+
+ ///
+ /// Checks whether the given game object is in the entity registry and can thus be registered as an entity in
+ /// the system.
+ ///
+ /// The game object to check for.
+ /// true if the given game object is in the entity registry; otherwise false.
+ private static bool IsObjectInRegistry(GameObject gameObject) {
+ return EntityRegistry.TryGetEntry(gameObject, out _);
+ }
+
+ ///
+ /// Method to call the spawn event externally. TODO: refactor this into something more appropriate
+ ///
+ /// The spawn details for the event.
+ /// Whether an entity was registered from this spawn.
+ public static bool CallEntitySpawnEvent(EntitySpawnDetails details) {
+ return EntitySpawnEvent != null && EntitySpawnEvent.Invoke(details);
+ }
+
+ ///
+ /// Emit intercept instruction on the next Unity Random Range() call for the given IL cursor.
+ ///
+ /// The cursor for the IL context of the method.
+ /// The return type of the random call.
+ /// The type of the FSM state action in which the random call occurs.
+ private static void EmitRandomInterceptInstructions(ILCursor c) where TObject : FsmStateAction {
+ // Goto the next call instruction for Random.Range()
+ c.GotoNext(i => i.MatchCall(typeof(Random), "Range"));
+
+ // Move the cursor after the call instruction
+ c.Index++;
+
+ // Push the current instance of the class onto the stack
+ c.Emit(OpCodes.Ldarg_0);
+
+ // Emit a delegate that pops the current random value off the stack and puts it back after some processing
+ c.EmitDelegate>((value, instance) => {
+ // We need to check whether the game object that is being spawned with this action is not an object
+ // managed by the system. Because if so, we do not store the random values because the action for it
+ // is not being networked. Only the game object spawn is networked with an EntitySpawn packet directly.
+ var fsmGameObject = ReflectionHelper.GetField(instance, "gameObject");
+ if (fsmGameObject != null && fsmGameObject.Value != null && IsObjectInRegistry(fsmGameObject.Value)) {
+ return value;
+ }
+
+ if (!RandomActionValues.TryGetValue(instance, out var queue)) {
+ queue = new Queue