Skip to content

Commit

Permalink
Fix switches not activating for remote players, improvements to battl…
Browse files Browse the repository at this point in the history
…e scenes
  • Loading branch information
Extremelyd1 committed Jul 9, 2024
1 parent 4ec6aaf commit 152c7d4
Show file tree
Hide file tree
Showing 4 changed files with 138 additions and 37 deletions.
34 changes: 11 additions & 23 deletions HKMP/Fsm/FsmPatcher.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,29 +36,6 @@ private void OnFsmEnable(On.PlayMakerFSM.orig_OnEnable orig, PlayMakerFSM self)
triggerAction.collideTag.Value = "Player";
triggerAction.collideTag.UseVariable = false;
}

// Specific patch for the Battle Control FSM in Fungus2_05 where the Shroomal Ogres are with the Charm Notch
if (self.name.Equals("Battle Scene v2") &&
self.Fsm.Name.Equals("Battle Control") &&
self.gameObject.scene.name.Equals("Fungus2_05")) {
var findBrawler1 = self.GetAction<FindGameObject>("Init", 6);
var findBrawler2 = self.GetAction<FindGameObject>("Init", 8);

// With the way the entity system works, the Mushroom Brawlers might not be found with the existing actions
// We complement these actions by checking if the Brawlers were found and if not, find them another way
self.InsertMethod("Init", 7, () => {
if (findBrawler1.store.Value == null) {
var brawler1 = GameObjectUtil.FindInactiveGameObject("Mushroom Brawler 1");
findBrawler1.store.Value = brawler1;
}
});
self.InsertMethod("Init", 10, () => {
if (findBrawler2.store.Value == null) {
var brawler2 = GameObjectUtil.FindInactiveGameObject("Mushroom Brawler 2");
findBrawler2.store.Value = brawler2;
}
});
}

// 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")) {
Expand All @@ -67,5 +44,16 @@ private void OnFsmEnable(On.PlayMakerFSM.orig_OnEnable orig, PlayMakerFSM self)
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<BoolTest>("Range");
}

if (self.GetState("Check If Nail") != null) {
self.RemoveFirstAction<BoolTest>("Check If Nail");
}
}
}
}
78 changes: 64 additions & 14 deletions HKMP/Game/Client/Entity/Entity.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
using Hkmp.Util;
using HutongGames.PlayMaker;
using Modding;
using On.HutongGames.PlayMaker.Actions;
using UnityEngine;
using Logger = Hkmp.Logging.Logger;
using Vector2 = Hkmp.Math.Vector2;
Expand Down Expand Up @@ -79,13 +80,18 @@ internal class Entity {
/// <summary>
/// Whether the unity game object for the host entity was originally active.
/// </summary>
private readonly bool _originalIsActive;
private bool _originalIsActive;

/// <summary>
/// Whether the entity is controlled, i.e. in control by updates from the server.
/// </summary>
private bool _isControlled;

/// <summary>
/// Whether the scene host is determined, or alternatively whether the entity has been determined to be a host or client entity.
/// </summary>
private bool _isSceneHostDetermined;

/// <summary>
/// The last position of the entity.
/// </summary>
Expand Down Expand Up @@ -191,6 +197,10 @@ params EntityComponentType[] types

// Always disallow the client object from being recycled, because it will simply be destroyed
On.ObjectPool.Recycle_GameObject += ObjectPoolOnRecycleGameObject;

// Register a hook for the ActivateGameObject action to update the active state of the host game object
// before scene host is determined
ActivateGameObject.DoActivateGameObject += OnDoActivateGameObject;

_fsms = new HostClientPair<List<PlayMakerFSM>> {
Host = Object.Host.GetComponents<PlayMakerFSM>().ToList(),
Expand Down Expand Up @@ -654,12 +664,12 @@ private void OnUpdate() {
}

// Define a method that allows generalization of checking for changes in all FSM variables
void CondAddData<VarType, BaseType, DataType>(
VarType[] fsmVars,
BaseType[] snapshotArray,
Func<VarType, BaseType> fsmVarValue,
void CondAddData<TVar, TBase, TData>(
TVar[] fsmVars,
TBase[] snapshotArray,
Func<TVar, TBase> fsmVarValue,
EntityHostFsmData.Type type,
Dictionary<byte, DataType> dataDict
Dictionary<byte, TData> dataDict
) {
for (byte i = 0; i < fsmVars.Length; i++) {
var fsmVar = fsmVars[i];
Expand All @@ -680,11 +690,11 @@ Dictionary<byte, DataType> dataDict
// Since there is a mismatch between our Hkmp.Math.Vector2 and Unity's Vector2
// But our types have explicit converters, so casting is possible
if (value is UnityEngine.Vector2 vec2) {
dataDict[i] = (DataType) (object) (Vector2) vec2;
dataDict[i] = (TData) (object) (Vector2) vec2;
} else if (value is Vector3 vec3) {
dataDict[i] = (DataType) (object) (Hkmp.Math.Vector3) vec3;
dataDict[i] = (TData) (object) (Hkmp.Math.Vector3) vec3;
} else {
dataDict[i] = (DataType) (object) value;
dataDict[i] = (TData) (object) value;
}
}
}
Expand Down Expand Up @@ -803,6 +813,31 @@ private void ObjectPoolOnRecycleGameObject(On.ObjectPool.orig_Recycle_GameObject

orig(obj);
}

/// <summary>
/// Callback method for when the 'active' of the host game object is changed. Used to update whether the host
/// game object should return to what active state after the scene host is determined.
/// </summary>
private void OnDoActivateGameObject(ActivateGameObject.orig_DoActivateGameObject orig, HutongGames.PlayMaker.Actions.ActivateGameObject self) {
// If the game object in the action is not our host game object, we skip it
if (self.Fsm.GetOwnerDefaultTarget(self.gameObject) != Object.Host) {
orig(self);
return;
}

// If the host client is determined already we skip (although this hook should have been deregistered
if (_isSceneHostDetermined) {
orig(self);
return;
}

Logger.Debug($"Entity '{Object.Host.name}' tried changing active of host object, while host is not determined yet, updating original active to: {self.activate.Value}");

// Update the original active value to whatever this action will set
// Also, we do not let this action execute any further since we do not want it to modify our host object
// before the scene host is determined
_originalIsActive = self.activate.Value;
}

/// <summary>
/// Initializes the entity when the client user is the scene host.
Expand All @@ -820,11 +855,26 @@ public void InitializeHost() {
_netClient.UpdateManager.UpdateEntityIsActive(Id, _lastIsActive);

_isControlled = false;
_isSceneHostDetermined = true;

foreach (var component in _components.Values) {
component.IsControlled = false;
component.InitializeHost();
}

// Deregister the hook for updating the active value of the host object
ActivateGameObject.DoActivateGameObject -= OnDoActivateGameObject;
}

/// <summary>
/// Initializes the entity when the client user is a scene client. Only sets a variable to indicate the scene
/// host has been determined.
/// </summary>
public void InitializeClient() {
_isSceneHostDetermined = true;

// Deregister the hook for updating the active value of the host object
ActivateGameObject.DoActivateGameObject -= OnDoActivateGameObject;
}

/// <summary>
Expand Down Expand Up @@ -1232,16 +1282,16 @@ public void UpdateHostFsmData(Dictionary<byte, EntityHostFsmData> hostFsmData) {

var fsms = new[] { hostFsm, _fsms.Client[fsmIndex] };

void CondUpdateVars<FsmType, BaseType>(
void CondUpdateVars<TFsm, TBase>(
EntityHostFsmData.Type type,
Dictionary<byte, BaseType> dataDict,
FsmType[] fsmVarArray,
Action<byte, FsmType, BaseType> setValueAction
Dictionary<byte, TBase> dataDict,
TFsm[] fsmVarArray,
Action<byte, TFsm, TBase> setValueAction
) {
if (data.Types.Contains(type)) {
foreach (var pair in dataDict) {
if (fsmVarArray.Length <= pair.Key) {
Logger.Warn($"Tried to update host FSM var ({typeof(BaseType)}) for unknown index: {pair.Key}");
Logger.Warn($"Tried to update host FSM var ({typeof(TBase)}) for unknown index: {pair.Key}");
} else {
setValueAction.Invoke(pair.Key, fsmVarArray[pair.Key], pair.Value);
}
Expand Down
43 changes: 43 additions & 0 deletions HKMP/Game/Client/Entity/EntityManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
using Modding;
using UnityEngine;
using UnityEngine.SceneManagement;
using FindGameObject = On.HutongGames.PlayMaker.Actions.FindGameObject;
using Logger = Hkmp.Logging.Logger;
using Object = UnityEngine.Object;

Expand Down Expand Up @@ -56,6 +57,8 @@ public EntityManager(NetClient netClient) {
EntityFsmActions.EntitySpawnEvent += OnGameObjectSpawned;
UnityEngine.SceneManagement.SceneManager.sceneLoaded += OnSceneLoaded;
UnityEngine.SceneManagement.SceneManager.activeSceneChanged += OnSceneChanged;

FindGameObject.Find += OnFindGameObject;
}

/// <summary>
Expand All @@ -82,6 +85,10 @@ public void InitializeSceneClient() {
Logger.Info("We are scene client, taking control of all registered entities");

IsSceneHost = false;

foreach (var entity in _entities.Values) {
entity.InitializeClient();
}

IsSceneHostDetermined = true;

Expand Down Expand Up @@ -454,4 +461,40 @@ private void FindEntitiesInScene(Scene scene, bool lateLoad) {
}.Process();
}
}

/// <summary>
/// Callback method for when the find method of FindGameObject is called. This is to look for objects that are
/// normally not found by the action due to our entity system making certain objects inactive. If we notice that
/// the find failed, but the name to look for was one of our host objects in the entity system, we set that object
/// instead.
/// </summary>
private void OnFindGameObject(FindGameObject.orig_Find orig, HutongGames.PlayMaker.Actions.FindGameObject self) {
orig(self);

// If the object was found after the method executed, we skip
if (self.store.Value != null) {
return;
}

Logger.Debug($"OnFindGameObject, find failed: looking for '{self.objectName.Value}'");

// If the object to find is tagged we skip, since this doesn't happen in our case
if (self.withTag.Value != "Untagged") {
return;
}

// Check if the name we are looking for is one of our registered entity's host objects
foreach (var entity in _entities.Values) {
var obj = entity.Object.Host;
if (obj.name == self.objectName.Value) {
// The host object of the entity matches the name the action was looking for, so we set the variable
self.store.Value = obj;

Logger.Debug($" Name matches host object of entity: ({entity.Id}, {entity.Type})");
return;
}
}

Logger.Debug(" Name did not match any entity");
}
}
20 changes: 20 additions & 0 deletions HKMP/Util/FsmUtilExt.cs
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,26 @@ public static void RemoveAction(this PlayMakerFSM fsm, string stateName, int ind

state.Actions = actions;
}

/// <summary>
/// Removes the first action in the given state of the given type from the FSM.
/// </summary>
/// <param name="fsm">The FSM.</param>
/// <param name="stateName">The name of the state with the action to remove.</param>
/// <typeparam name="T">The type of the action to remove.</typeparam>
public static void RemoveFirstAction<T>(this PlayMakerFSM fsm, string stateName) {
var state = fsm.GetState(stateName);

var skipped = false;
state.Actions = state.Actions.Where(a => {
if (!skipped && a.GetType() != typeof(T)) {
skipped = true;
return false;
}

return true;
}).ToArray();
}
}

/// <summary>
Expand Down

0 comments on commit 152c7d4

Please sign in to comment.