From 8844c25c5a95968a7d60b39c65471b24fc1a997e Mon Sep 17 00:00:00 2001 From: Extremelyd1 Date: Thu, 9 Jun 2022 00:07:29 +0200 Subject: [PATCH 001/216] Initial divergent approach to entity system --- HKMP/Game/Client/ClientManager.cs | 32 +- HKMP/Game/Client/Entity/Entity.cs | 330 ++++++++++-------- HKMP/Game/Client/Entity/EntityManager.cs | 166 +++++---- HKMP/Game/Client/Entity/EntityType.cs | 5 - HKMP/Game/Client/Entity/FalseKnight.cs | 246 ------------- HKMP/Game/Client/Entity/IEntity.cs | 19 - HKMP/Game/Server/ServerManager.cs | 32 +- HKMP/HKMP.csproj | 16 +- HKMP/Networking/Client/ClientUpdateManager.cs | 55 +-- HKMP/Networking/Packet/Data/EntityUpdate.cs | 69 ++-- HKMP/Networking/Server/ServerUpdateManager.cs | 53 +-- 11 files changed, 402 insertions(+), 621 deletions(-) delete mode 100644 HKMP/Game/Client/Entity/EntityType.cs delete mode 100644 HKMP/Game/Client/Entity/FalseKnight.cs delete mode 100644 HKMP/Game/Client/Entity/IEntity.cs diff --git a/HKMP/Game/Client/ClientManager.cs b/HKMP/Game/Client/ClientManager.cs index 52912165..3e3ed502 100644 --- a/HKMP/Game/Client/ClientManager.cs +++ b/HKMP/Game/Client/ClientManager.cs @@ -543,10 +543,10 @@ private void OnPlayerAlreadyInScene(ClientPlayerAlreadyInScene alreadyInScene) { if (alreadyInScene.SceneHost) { // Notify the entity manager that we are scene host - _entityManager.OnBecomeSceneHost(); + _entityManager.InitializeSceneHost(); } else { // Notify the entity manager that we are scene client (non-host) - _entityManager.OnBecomeSceneClient(); + _entityManager.InitializeSceneClient(); } // Whether there were players in the scene or not, we have now determined whether @@ -662,25 +662,19 @@ private void OnEntityUpdate(EntityUpdate entityUpdate) { } if (entityUpdate.UpdateTypes.Contains(EntityUpdateType.Position)) { - _entityManager.UpdateEntityPosition((EntityType)entityUpdate.EntityType, entityUpdate.Id, - entityUpdate.Position); + _entityManager.UpdateEntityPosition(entityUpdate.Id, entityUpdate.Position); + } + + if (entityUpdate.UpdateTypes.Contains(EntityUpdateType.Scale)) { + _entityManager.UpdateEntityScale(entityUpdate.Id, entityUpdate.Scale); + } + + if (entityUpdate.UpdateTypes.Contains(EntityUpdateType.Animation)) { + _entityManager.UpdateEntityAnimation(entityUpdate.Id, entityUpdate.AnimationId); } - if (entityUpdate.UpdateTypes.Contains(EntityUpdateType.State)) { - List variables; - - if (entityUpdate.UpdateTypes.Contains(EntityUpdateType.Variables)) { - variables = entityUpdate.Variables; - } else { - variables = new List(); - } - - _entityManager.UpdateEntityState( - (EntityType)entityUpdate.EntityType, - entityUpdate.Id, - entityUpdate.State, - variables - ); + if (entityUpdate.UpdateTypes.Contains(EntityUpdateType.Raw)) { + _entityManager.UpdateEntityData(entityUpdate.Id, entityUpdate.RawData); } } diff --git a/HKMP/Game/Client/Entity/Entity.cs b/HKMP/Game/Client/Entity/Entity.cs index dfbe78bc..07fe96eb 100644 --- a/HKMP/Game/Client/Entity/Entity.cs +++ b/HKMP/Game/Client/Entity/Entity.cs @@ -1,244 +1,276 @@ using System; using System.Collections.Generic; -using System.Linq; +using Hkmp.Collection; using Hkmp.Fsm; using Hkmp.Networking.Client; using Hkmp.Util; -using HutongGames.PlayMaker; using UnityEngine; using Vector2 = Hkmp.Math.Vector2; namespace Hkmp.Game.Client.Entity { - internal abstract class Entity : IEntity { + internal class Entity { private readonly NetClient _netClient; - private readonly EntityType _entityType; private readonly byte _entityId; - private readonly Queue _stateVariableUpdates; - private bool _inUpdateState; + private readonly GameObject _gameObject; - protected readonly GameObject GameObject; + private readonly tk2dSpriteAnimator _animator; + private readonly BiLookup _animationClipNameIds; - public bool IsControlled { get; private set; } - public bool AllowEventSending { get; set; } + private readonly PlayMakerFSM[] _fsms; - // Dictionary containing per state name an array of transitions that the state normally has - // This is used to revert nulling out the transitions to prevent it from continuing - private readonly Dictionary _stateTransitions; + private readonly Climber _climber; - protected PlayMakerFSM Fsm; + private bool _isControlled; - protected Entity( + private Vector3 _lastPosition; + private Vector3 _lastScale; + + private Vector3 _lastRotation; + + public Entity( NetClient netClient, - EntityType entityType, byte entityId, GameObject gameObject ) { _netClient = netClient; - _entityType = entityType; _entityId = entityId; - GameObject = gameObject; - - _stateVariableUpdates = new Queue(); + _gameObject = gameObject; - _stateTransitions = new Dictionary(); + _isControlled = true; // Add a position interpolation component to the enemy so we can smooth out position updates - GameObject.AddComponent(); + _gameObject.AddComponent(); // Register an update event to send position updates MonoBehaviourUtil.Instance.OnUpdateEvent += OnUpdate; - } - private void OnUpdate() { - // We don't send updates when this FSM is controlled or when we are not allowed to send events yet - if (IsControlled || !AllowEventSending) { - return; - } + _animator = _gameObject.GetComponent(); + if (_animator != null) { + _animationClipNameIds = new BiLookup(); - var transformPos = GameObject.transform.position; + var index = 0; + foreach (var animationClip in _animator.Library.clips) { + _animationClipNameIds.Add(animationClip.name, (byte)index++); - _netClient.UpdateManager.UpdateEntityPosition( - _entityType, - _entityId, - new Vector2(transformPos.x, transformPos.y) - ); - } + if (index > byte.MaxValue) { + Logger.Get().Error(this, + $"Too many animation clips to fit in a byte for entity: {_gameObject.name}"); + break; + } + } - public void TakeControl() { - if (IsControlled) { - return; + On.tk2dSpriteAnimator.Play_string += OnAnimationPlayed; + On.tk2dSpriteAnimator.Play_tk2dSpriteAnimationClip += OnAnimationPlayed; } - IsControlled = true; + _climber = _gameObject.GetComponent(); + if (_climber != null) { + MonoBehaviourUtil.Instance.OnUpdateEvent += OnUpdateRotation; + + _climber.enabled = false; + } - InternalTakeControl(); + _fsms = _gameObject.GetComponents(); + foreach (var fsm in _fsms) { + fsm.enabled = false; + } } - protected abstract void InternalTakeControl(); + private void OnUpdate() { + // We don't send updates when this entity is controlled + if (_isControlled) { + return; + } - public void ReleaseControl() { - if (!IsControlled) { + if (_gameObject == null) { return; } - IsControlled = false; + var transform = _gameObject.transform; - InternalReleaseControl(); - } + var newPosition = transform.position; + if (newPosition != _lastPosition) { + _lastPosition = newPosition; - protected abstract void InternalReleaseControl(); + _netClient.UpdateManager.UpdateEntityPosition( + _entityId, + new Vector2(newPosition.x, newPosition.y) + ); + } - public void UpdatePosition(Vector2 position) { - var unityPos = new Vector3(position.X, position.Y); + var newScale = transform.localScale; + if (newScale != _lastScale) { + _lastScale = newScale; - GameObject.GetComponent().SetNewPosition(unityPos); + _netClient.UpdateManager.UpdateEntityScale( + _entityId, + newScale.x > 0 + ); + } } - public void UpdateState(byte state, List variables) { - if (IsInterruptingState(state)) { - - Logger.Get().Info(this, "Received update is interrupting state, starting update"); - - _inUpdateState = true; - - // Since we interrupt everything that was going on, we can clear the existing queue - _stateVariableUpdates.Clear(); - - StartQueuedUpdate(state, variables); + private void OnAnimationPlayed( + On.tk2dSpriteAnimator.orig_Play_string orig, + tk2dSpriteAnimator self, + string clipName + ) { + orig(self, clipName); + if (self != _animator) { return; } - if (!_inUpdateState) { - Logger.Get().Info(this, "Queue is empty, starting new update"); - - _inUpdateState = true; + HandlePlayedAnimation(clipName); + } - // If we are not currently updating the state, we can queue it immediately - StartQueuedUpdate(state, variables); + private void OnAnimationPlayed( + On.tk2dSpriteAnimator.orig_Play_tk2dSpriteAnimationClip orig, + tk2dSpriteAnimator self, + tk2dSpriteAnimationClip clip + ) { + orig(self, clip); + if (self != _animator) { return; } - Logger.Get().Info(this, "Queue is non-empty, queueing new update"); - - // There is already an update running, so we queue this one - _stateVariableUpdates.Enqueue(new StateVariableUpdate { - State = state, - Variables = variables - }); + HandlePlayedAnimation(clip.name); } - /** - * Called when the previous state update is done. - * Usually called on specific points in the entity's FSM. - */ - protected void StateUpdateDone() { - // If the queue is empty when we are done, we reset the boolean - // so that a new state update can be started immediately - if (_stateVariableUpdates.Count == 0) { - Logger.Get().Info(this, "Queue is empty"); - _inUpdateState = false; + // TODO: mark animations that loop differently to the server so it knows to repeat the animation for players + // that newly enter a scene + private void HandlePlayedAnimation(string clipName) { + if (_isControlled) { return; } - Logger.Get().Info(this, "Queue is non-empty, starting next"); + if (!_animationClipNameIds.TryGetValue(clipName, out var animationId)) { + Logger.Get().Warn(this, $"Entity '{_gameObject.name}' played unknown animation: {clipName}"); + return; + } - // Get the next queued update and start it - var stateVariableUpdate = _stateVariableUpdates.Dequeue(); - StartQueuedUpdate(stateVariableUpdate.State, stateVariableUpdate.Variables); + // Logger.Get().Info(this, $"Entity '{_gameObject.name}' sends animation: {clipName}, {animationId}"); + _netClient.UpdateManager.UpdateEntityAnimation( + _entityId, + animationId + ); } - /** - * Start a (previously queued) update with given state index and variable list. - */ - protected abstract void StartQueuedUpdate(byte state, List variable); + private void OnUpdateRotation() { + if (_isControlled) { + return; + } - /** - * Whether the given state index represents a state that should interrupt - * other updating states. - */ - protected abstract bool IsInterruptingState(byte state); + if (_gameObject == null) { + return; + } - public void Destroy() { - AllowEventSending = false; + var transform = _gameObject.transform; - MonoBehaviourUtil.Instance.OnUpdateEvent -= OnUpdate; - } + var newRotation = transform.rotation.eulerAngles; + if (newRotation != _lastRotation) { + _lastRotation = newRotation; - protected void SendStateUpdate(byte state) { - _netClient.UpdateManager.UpdateEntityState(_entityType, _entityId, state); - } + var data = new List { (byte)DataType.Rotation }; + data.AddRange(BitConverter.GetBytes(newRotation.z)); - protected void SendStateUpdate(byte state, List variables) { - _netClient.UpdateManager.UpdateEntityStateAndVariables(_entityType, _entityId, state, variables); + _netClient.UpdateManager.AddEntityData( + _entityId, + data + ); + } } - protected void RemoveOutgoingTransitions(string stateName) { - _stateTransitions[stateName] = Fsm.GetState(stateName).Transitions; + public void InitializeHost() { + if (_climber != null) { + _climber.enabled = true; + } - foreach (var transition in _stateTransitions[stateName]) { - Logger.Get().Info(this, $"Removing transition in state: {stateName}, to: {transition.ToState}"); + foreach (var fsm in _fsms) { + fsm.enabled = true; } - Fsm.GetState(stateName).Transitions = new FsmTransition[0]; + _isControlled = false; } - protected void RemoveOutgoingTransition(string stateName, string toState) { - // Get the current array of transitions - var originalTransitions = Fsm.GetState(stateName).Transitions; - - // We don't want to overwrite the originally stored transitions, - // so we only store it if the key doesn't exist yet - if (!_stateTransitions.TryGetValue(stateName, out _)) { - _stateTransitions[stateName] = originalTransitions; - } + // TODO: parameters should be all FSM details to kickstart all FSMs of the game object + public void MakeHost() { + } - // Try to find the transition that has a destination state with the given name - var newTransitions = originalTransitions.ToList(); - foreach (var transition in originalTransitions) { - if (transition.ToState.Equals(toState)) { - newTransitions.Remove(transition); - break; - } - } + public void UpdatePosition(Vector2 position) { + var unityPos = new Vector3(position.X, position.Y); - Fsm.GetState(stateName).Transitions = newTransitions.ToArray(); + _gameObject.GetComponent().SetNewPosition(unityPos); } - protected Action CreateStateUpdateMethod(Action action) { - return () => { - if (IsControlled || !AllowEventSending) { - return; - } - - action.Invoke(); - }; + public void UpdateScale(bool scale) { + var transform = _gameObject.transform; + var localScale = transform.localScale; + var currentScaleX = localScale.x; + + if (currentScaleX > 0 != scale) { + transform.localScale = new Vector3( + currentScaleX * -1, + localScale.y, + localScale.z + ); + } } - protected void RestoreAllOutgoingTransitions() { - foreach (var stateTransitionPair in _stateTransitions) { - Fsm.GetState(stateTransitionPair.Key).Transitions = stateTransitionPair.Value; + public void UpdateAnimation(byte animationId) { + if (_animator == null) { + Logger.Get().Warn(this, + $"Entity '{_gameObject.name}' received animation while animator does not exist"); + return; } - _stateTransitions.Clear(); + if (!_animationClipNameIds.TryGetValue(animationId, out var clipName)) { + Logger.Get().Warn(this, $"Entity '{_gameObject.name}' received unknown animation ID: {animationId}"); + return; + } + + // Logger.Get().Info(this, $"Entity '{_gameObject.name}' received animation: {animationId}, {clipName}"); + _animator.Play(clipName); } - protected void RestoreOutgoingTransitions(string stateName) { - if (!_stateTransitions.TryGetValue(stateName, out var transitions)) { - Logger.Get().Warn(this, - $"Tried to restore transitions for state named: {stateName}, but they are not stored"); + public void UpdateData(List dataList) { + var data = dataList.ToArray(); + + if (data.Length == 0) { return; } - Fsm.GetState(stateName).Transitions = transitions; - _stateTransitions.Remove(stateName); + var i = 0; + while (i < data.Length) { + var dataType = (DataType)data[i++]; + + if (dataType == DataType.Rotation) { + var rotation = BitConverter.ToSingle(data, i); + i += 4; + + var transform = _gameObject.transform; + var eulerAngles = transform.eulerAngles; + transform.eulerAngles = new Vector3( + eulerAngles.x, + eulerAngles.y, + rotation + ); + } else { + break; + } + } + } + + public void Destroy() { + MonoBehaviourUtil.Instance.OnUpdateEvent -= OnUpdate; + On.tk2dSpriteAnimator.Play_string -= OnAnimationPlayed; + On.tk2dSpriteAnimator.Play_tk2dSpriteAnimationClip -= OnAnimationPlayed; + MonoBehaviourUtil.Instance.OnUpdateEvent -= OnUpdateRotation; } - private class StateVariableUpdate { - public byte State { get; set; } - public List Variables { get; set; } + private enum DataType : byte { + Rotation = 0, } } } \ No newline at end of file diff --git a/HKMP/Game/Client/Entity/EntityManager.cs b/HKMP/Game/Client/Entity/EntityManager.cs index 6051a67b..2e1465ca 100644 --- a/HKMP/Game/Client/Entity/EntityManager.cs +++ b/HKMP/Game/Client/Entity/EntityManager.cs @@ -6,141 +6,137 @@ namespace Hkmp.Game.Client.Entity { internal class EntityManager { + private readonly List _validEntityFsms = new() { + "Crawler" + }; + private readonly NetClient _netClient; - private readonly Dictionary<(EntityType, byte), IEntity> _entities; + private readonly Dictionary _entities; private bool _isSceneHost; + private byte _lastId; + public EntityManager(NetClient netClient) { _netClient = netClient; - _entities = new Dictionary<(EntityType, byte), IEntity>(); + _entities = new Dictionary(); - // ModHooks.Instance.OnEnableEnemyHook += OnEnableEnemyHook; + _lastId = 0; UnityEngine.SceneManagement.SceneManager.activeSceneChanged += OnSceneChanged; } - public void OnBecomeSceneHost() { + public void InitializeSceneHost() { Logger.Get().Info(this, "Releasing control of all registered entities"); _isSceneHost = true; foreach (var entity in _entities.Values) { - if (entity.IsControlled) { - entity.ReleaseControl(); - } - - entity.AllowEventSending = true; + entity.InitializeHost(); } } - public void OnBecomeSceneClient() { + public void InitializeSceneClient() { Logger.Get().Info(this, "Taking control of all registered entities"); _isSceneHost = false; - - foreach (var entity in _entities.Values) { - if (!entity.IsControlled) { - entity.TakeControl(); - } - - entity.AllowEventSending = false; - } } - private void OnSceneChanged(Scene oldScene, Scene newScene) { - Logger.Get().Info(this, "Clearing all registered entities"); + public void UpdateEntityPosition(byte entityId, Vector2 position) { + if (_isSceneHost) { + return; + } - foreach (var entity in _entities.Values) { - entity.Destroy(); + if (!_entities.TryGetValue(entityId, out var entity)) { + return; } - _entities.Clear(); + entity.UpdatePosition(position); } - private bool OnEnableEnemyHook(GameObject enemy, bool isDead) { - var enemyName = enemy.name; - - IEntity entity = null; - - if (enemyName.StartsWith("False Knight New")) { - var trimmedName = enemyName.Replace("False Knight New", "").Trim(); - - byte enemyId; - if (trimmedName.Length == 0) { - enemyId = 0; - } else { - if (!byte.TryParse(trimmedName, out enemyId)) { - Logger.Get().Warn(this, $"Could not parse enemy index as byte ({enemyName})"); - - return isDead; - } - } - - Logger.Get().Info(this, $"Registering enabled enemy, name: {enemyName}, id: {enemyId}"); - - entity = new FalseKnight(_netClient, enemyId, enemy); - - _entities[(EntityType.FalseKnight, enemyId)] = entity; + public void UpdateEntityScale(byte entityId, bool scale) { + if (_isSceneHost) { + return; } - if (entity == null) { - return isDead; + if (!_entities.TryGetValue(entityId, out var entity)) { + return; } - if (_isSceneHost) { - Logger.Get().Info(this, "Releasing control of registered enemy"); - - if (entity.IsControlled) { - entity.ReleaseControl(); - } - - entity.AllowEventSending = true; - } else { - Logger.Get().Info(this, "Taking control of registered enemy"); + entity.UpdateScale(scale); + } - if (!entity.IsControlled) { - entity.TakeControl(); - } + public void UpdateEntityAnimation(byte entityId, byte animationId) { + if (_isSceneHost) { + return; + } - entity.AllowEventSending = false; + if (!_entities.TryGetValue(entityId, out var entity)) { + return; } - return isDead; + entity.UpdateAnimation(animationId); } - public void UpdateEntityPosition(EntityType entityType, byte id, Vector2 position) { - if (!_entities.TryGetValue((entityType, id), out var entity)) { - Logger.Get().Info(this, - $"Tried to update entity position for (type, ID) = ({entityType}, {id}), but there was no entry"); + public void UpdateEntityData(byte entityId, List data) { + if (_isSceneHost) { return; } - // Check whether the entity is already controlled, and if not - // take control of it - if (!entity.IsControlled) { - entity.TakeControl(); + if (!_entities.TryGetValue(entityId, out var entity)) { + return; } - entity.UpdatePosition(position); + entity.UpdateData(data); } + + // TODO: methods for transferring scene host to this client - public void UpdateEntityState(EntityType entityType, byte id, byte stateIndex, List variables) { - if (!_entities.TryGetValue((entityType, id), out var entity)) { - Logger.Get().Info(this, - $"Tried to update entity state for (type, ID) = ({entityType}, {id}), but there was no entry"); - return; - } + private void OnSceneChanged(Scene oldScene, Scene newScene) { + Logger.Get().Info(this, "Clearing all registered entities"); - // Check whether the entity is already controlled, and if not - // take control of it - if (!entity.IsControlled) { - entity.TakeControl(); + foreach (var entity in _entities.Values) { + entity.Destroy(); } - // Simply update the state with this new index - entity.UpdateState(stateIndex, variables); + _entities.Clear(); + + _lastId = 0; + + if (!_netClient.IsConnected) { + return; + } + + // Find all PlayMakerFSM components + foreach (var fsm in Object.FindObjectsOfType()) { + // Logger.Get().Info(this, $"Found FSM: {fsm.Fsm.Name}, {fsm.gameObject.name}"); + + if (_validEntityFsms.Contains(fsm.Fsm.Name)) { + Logger.Get().Info(this, $"Registering entity '{fsm.gameObject.name}' with ID '{_lastId}'"); + + _entities[_lastId] = new Entity( + _netClient, + _lastId, + fsm.gameObject + ); + + _lastId++; + } + } + + // Find all Climber components + foreach (var climber in Object.FindObjectsOfType()) { + Logger.Get().Info(this, $"Registering entity '{climber.name}' with ID '{_lastId}'"); + + _entities[_lastId] = new Entity( + _netClient, + _lastId, + climber.gameObject + ); + + _lastId++; + } } } } \ No newline at end of file diff --git a/HKMP/Game/Client/Entity/EntityType.cs b/HKMP/Game/Client/Entity/EntityType.cs deleted file mode 100644 index 9bfb03d0..00000000 --- a/HKMP/Game/Client/Entity/EntityType.cs +++ /dev/null @@ -1,5 +0,0 @@ -namespace Hkmp.Game.Client.Entity { - internal enum EntityType { - FalseKnight = 1, - } -} \ No newline at end of file diff --git a/HKMP/Game/Client/Entity/FalseKnight.cs b/HKMP/Game/Client/Entity/FalseKnight.cs deleted file mode 100644 index 909124cd..00000000 --- a/HKMP/Game/Client/Entity/FalseKnight.cs +++ /dev/null @@ -1,246 +0,0 @@ -using System; -using System.Collections.Generic; -using Hkmp.Networking.Client; -using Hkmp.Util; -using UnityEngine; - -namespace Hkmp.Game.Client.Entity { - internal class FalseKnight : Entity { - private static readonly Dictionary SimpleEventStates = new Dictionary { - {State.Fall, "Start Fall"}, - {State.TurnR, "Turn R"}, - {State.TurnL, "Turn L"}, - {State.Run, "Run Antic"}, - {State.JumpAttackRight, "JA Right"}, - {State.JumpAttackLeft, "JA Left"}, - {State.StunTurnL, "Stun Turn L"}, - {State.StunTurnR, "Stun Turn R"}, - {State.StunStart, "Stun Start"}, - {State.OpenUp, "Open Uuup"}, - {State.Hit, "Hit"}, - {State.StunFail, "Stun Fail"}, - {State.Recover, "Recover"}, - {State.ToPhase2, "To Phase 2"}, - {State.ToPhase3, "To Phase 3"}, - {State.JumpAttack2, "JA Antic 2"}, - {State.Hit2, "Hit 2"}, - {State.Death, "Death Anim Start"} - }; - - private static readonly string[] StateUpdateResetNames = { - // After the initial fall, the FSM will end up here - "First Idle", - // Most sequences end back up in the Idle state - "Idle", - // The run sequence splits in jumping left or right - "JA Check Hero Pos", - // The slam sequences (jump and non-jump) end in "Voice? 2" - "Voice? 2", - // The move choice state has a lot of outputs, and some states end up here - "Move Choice", - // Part of the stun/stagger sequence, might be transitioned to - // after a global Stun event - "Check Direction", - // The end of the tumble through the air after a stun - "Roll End", - // When False Knight's armor is opened and the is popped out - "Opened", - // After the ground slamming rage is over - "Rage Check", - // When False Knight's falls through the floor and its armor opens up - "Opened 2" - }; - - private static readonly List InterruptingStates = new List { - State.StunTurnL, - State.StunTurnR, - State.StunStart, - State.Recover - }; - - private bool _isInitialized; - - public FalseKnight( - NetClient netClient, - byte entityId, - GameObject gameObject - ) : base( - netClient, - EntityType.FalseKnight, - entityId, - gameObject - ) { - Fsm = gameObject.LocateMyFSM("FalseyControl"); - - CreateEvents(); - } - - private void CreateEvents() { - // - // Insert methods for sending updates over network for reached states - // - foreach (var stateNamePair in SimpleEventStates) { - Fsm.InsertMethod(stateNamePair.Value, 0, CreateStateUpdateMethod(() => { - Logger.Get().Info(this, $"Sending {stateNamePair.Key} state"); - SendStateUpdate((byte) stateNamePair.Key); - })); - } - - Fsm.InsertMethod("Jump Antic", 0, CreateStateUpdateMethod(() => { - var variables = new List(); - - // Get the Jump X variable from the FSM and add it as bytes to the variables list - var jumpXFloat = Fsm.FsmVariables.GetFsmFloat("Jump X").Value; - variables.AddRange(BitConverter.GetBytes(jumpXFloat)); - - Logger.Get().Info(this, $"Sending Jump state with variable: {jumpXFloat}"); - - SendStateUpdate((byte) State.Jump, variables); - })); - - Fsm.InsertMethod("S Jump", 0, CreateStateUpdateMethod(() => { - var variables = new List(); - - // Get the Jump X variable from the FSM and add it as bytes to the variables list - var jumpXFloat = Fsm.FsmVariables.GetFsmFloat("Jump X").Value; - variables.AddRange(BitConverter.GetBytes(jumpXFloat)); - - Logger.Get().Info(this, $"Sending Slam Jump state with variable: {jumpXFloat}"); - - SendStateUpdate((byte) State.SlamJump, variables); - })); - - Fsm.InsertMethod("S Attack Antic", 0, CreateStateUpdateMethod(() => { - var variables = new List(); - - var shockwaveXOriginFloat = Fsm.FsmVariables.GetFsmFloat("Shockwave X Origin").Value; - variables.AddRange(BitConverter.GetBytes(shockwaveXOriginFloat)); - - var shockwaveGoingRightBool = Fsm.FsmVariables.GetFsmBool("Shockwave Going Right").Value; - variables.AddRange(BitConverter.GetBytes(shockwaveGoingRightBool)); - - Logger.Get().Info(this, - $"Sending Slam Attack state with variables: {shockwaveXOriginFloat}, {shockwaveGoingRightBool}"); - SendStateUpdate((byte) State.SlamAttack, variables); - })); - - // - // Insert methods for resetting the update state, so we can start/receive the next update - // - foreach (var stateName in StateUpdateResetNames) { - Fsm.InsertMethod(stateName, 0, StateUpdateDone); - } - - Fsm.InsertMethod("Turn R", 8, StateUpdateDone); - Fsm.InsertMethod("Turn L", 8, StateUpdateDone); - } - - protected override void InternalTakeControl() { - foreach (var stateName in StateUpdateResetNames) { - RemoveOutgoingTransitions(stateName); - } - - // Make sure that the FSM doesn't even start at all, - // by removing transitions of one of the first states - RemoveOutgoingTransitions("Dormant"); - - RemoveOutgoingTransition("Hit", "Recover"); - } - - protected override void InternalReleaseControl() { - RestoreAllOutgoingTransitions(); - } - - protected override void StartQueuedUpdate(byte state, List variables) { - var variableArray = variables.ToArray(); - - var enumState = (State) state; - - // If we not initialized before this state update, we need to - // do it before we set the FSM states and variables - if (!_isInitialized) { - InitializeForIntermediateState(); - } - - _isInitialized = true; - - if (SimpleEventStates.TryGetValue(enumState, out var stateName)) { - Logger.Get().Info(this, $"Received {enumState} state"); - Fsm.SetState(stateName); - - return; - } - - switch ((State) state) { - case State.Jump: - var jumpXFloat = BitConverter.ToSingle(variableArray, 0); - - Logger.Get().Info(this, $"Received Jump state with variable: {jumpXFloat}"); - - Fsm.FsmVariables.GetFsmFloat("Jump X").Value = jumpXFloat; - - Fsm.SetState("Jump Antic"); - break; - case State.SlamJump: - var slamJumpXFloat = BitConverter.ToSingle(variableArray, 0); - - Logger.Get().Info(this, $"Received Slam Jump state with variable: {slamJumpXFloat}"); - - Fsm.FsmVariables.GetFsmFloat("Jump X").Value = slamJumpXFloat; - - Fsm.SetState("S Jump"); - break; - case State.SlamAttack: - var shockwaveXOriginFloat = BitConverter.ToSingle(variableArray, 0); - var shockwaveGoingRightBool = BitConverter.ToBoolean(variableArray, 4); - - Logger.Get().Info(this, - $"Received Slam Attack state with variables: {shockwaveXOriginFloat}, {shockwaveGoingRightBool}"); - - Fsm.FsmVariables.GetFsmFloat("Shockwave X Origin").Value = shockwaveXOriginFloat; - Fsm.FsmVariables.GetFsmBool("Shockwave Going Right").Value = shockwaveGoingRightBool; - - Fsm.SetState("S Attack Antic"); - break; - } - } - - protected override bool IsInterruptingState(byte state) { - return InterruptingStates.Contains((State) state); - } - - private void InitializeForIntermediateState() { - // The mesh renderer is disabled until the Start Fall state - GameObject.GetComponent().enabled = true; - - // Same holds for rigidbody properties - var rigidbody = GameObject.GetComponent(); - rigidbody.isKinematic = false; - rigidbody.gravityScale = 1; - } - - private enum State { - Fall = 0, - Jump, - TurnR, - TurnL, - Run, - JumpAttackRight, - JumpAttackLeft, - SlamJump, - SlamAttack, - StunTurnL, - StunTurnR, - StunStart, - OpenUp, - Hit, - StunFail, - Recover, - ToPhase2, - ToPhase3, - JumpAttack2, - Hit2, - Death, - } - } -} \ No newline at end of file diff --git a/HKMP/Game/Client/Entity/IEntity.cs b/HKMP/Game/Client/Entity/IEntity.cs deleted file mode 100644 index a274f702..00000000 --- a/HKMP/Game/Client/Entity/IEntity.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System.Collections.Generic; -using Hkmp.Math; - -namespace Hkmp.Game.Client.Entity { - internal interface IEntity { - bool IsControlled { get; } - bool AllowEventSending { get; set; } - - void TakeControl(); - - void ReleaseControl(); - - void UpdatePosition(Vector2 position); - - void UpdateState(byte state, List variables); - - void Destroy(); - } -} \ No newline at end of file diff --git a/HKMP/Game/Server/ServerManager.cs b/HKMP/Game/Server/ServerManager.cs index b25c408a..4ccbb1fd 100644 --- a/HKMP/Game/Server/ServerManager.cs +++ b/HKMP/Game/Server/ServerManager.cs @@ -479,37 +479,47 @@ private void OnEntityUpdate(ushort id, EntityUpdate entityUpdate) { playerData.CurrentScene, otherId => { _netServer.GetUpdateManagerForClient(otherId)?.UpdateEntityPosition( - entityUpdate.EntityType, entityUpdate.Id, entityUpdate.Position ); } ); } - - if (entityUpdate.UpdateTypes.Contains(EntityUpdateType.State)) { + + if (entityUpdate.UpdateTypes.Contains(EntityUpdateType.Scale)) { SendDataInSameScene( id, playerData.CurrentScene, otherId => { - _netServer.GetUpdateManagerForClient(otherId)?.UpdateEntityState( - entityUpdate.EntityType, + _netServer.GetUpdateManagerForClient(otherId)?.UpdateEntityScale( entityUpdate.Id, - entityUpdate.State + entityUpdate.Scale ); } ); } - - if (entityUpdate.UpdateTypes.Contains(EntityUpdateType.Variables)) { + + if (entityUpdate.UpdateTypes.Contains(EntityUpdateType.Animation)) { + SendDataInSameScene( + id, + playerData.CurrentScene, + otherId => { + _netServer.GetUpdateManagerForClient(otherId)?.UpdateEntityAnimation( + entityUpdate.Id, + entityUpdate.AnimationId + ); + } + ); + } + + if (entityUpdate.UpdateTypes.Contains(EntityUpdateType.Raw)) { SendDataInSameScene( id, playerData.CurrentScene, otherId => { - _netServer.GetUpdateManagerForClient(otherId)?.UpdateEntityVariables( - entityUpdate.EntityType, + _netServer.GetUpdateManagerForClient(otherId)?.AddEntityData( entityUpdate.Id, - entityUpdate.Variables + entityUpdate.RawData ); } ); diff --git a/HKMP/HKMP.csproj b/HKMP/HKMP.csproj index e892163a..a109fd9d 100644 --- a/HKMP/HKMP.csproj +++ b/HKMP/HKMP.csproj @@ -1,6 +1,6 @@  - - + + {F34118B2-515D-4C33-88E6-9CFEF2AD5A15} @@ -13,12 +13,12 @@ - + - - + + @@ -83,10 +83,10 @@ - - + + - + diff --git a/HKMP/Networking/Client/ClientUpdateManager.cs b/HKMP/Networking/Client/ClientUpdateManager.cs index 80a15550..19ba0970 100644 --- a/HKMP/Networking/Client/ClientUpdateManager.cs +++ b/HKMP/Networking/Client/ClientUpdateManager.cs @@ -1,7 +1,6 @@ using System.Collections.Generic; using Hkmp.Animation; using Hkmp.Game; -using Hkmp.Game.Client.Entity; using Hkmp.Math; using Hkmp.Networking.Packet; using Hkmp.Networking.Packet.Data; @@ -129,10 +128,9 @@ public void UpdatePlayerAnimation(AnimationClip clip, int frame = 0, bool[] effe /// /// Find an existing or create a new EntityUpdate instance in the current update packet. /// - /// The type of the entity. /// The ID of the entity. /// The existing or new EntityUpdate instance. - private EntityUpdate FindOrCreateEntityUpdate(EntityType entityType, byte entityId) { + private EntityUpdate FindOrCreateEntityUpdate(byte entityId) { EntityUpdate entityUpdate = null; PacketDataCollection entityUpdateCollection; @@ -145,7 +143,7 @@ private EntityUpdate FindOrCreateEntityUpdate(EntityType entityType, byte entity entityUpdateCollection = (PacketDataCollection) packetData; foreach (var existingPacketData in entityUpdateCollection.DataInstances) { var existingEntityUpdate = (EntityUpdate) existingPacketData; - if (existingEntityUpdate.EntityType.Equals((byte) entityType) && existingEntityUpdate.Id == entityId) { + if (existingEntityUpdate.Id == entityId) { entityUpdate = existingEntityUpdate; break; } @@ -159,7 +157,6 @@ private EntityUpdate FindOrCreateEntityUpdate(EntityType entityType, byte entity // If no existing instance was found, create one and add it to the (newly created) collection if (entityUpdate == null) { entityUpdate = new EntityUpdate { - EntityType = (byte) entityType, Id = entityId }; @@ -173,12 +170,11 @@ private EntityUpdate FindOrCreateEntityUpdate(EntityType entityType, byte entity /// /// Update an entity's position in the current packet. /// - /// The entity type. /// The ID of the entity. /// The new position of the entity. - public void UpdateEntityPosition(EntityType entityType, byte entityId, Vector2 position) { + public void UpdateEntityPosition(byte entityId, Vector2 position) { lock (Lock) { - var entityUpdate = FindOrCreateEntityUpdate(entityType, entityId); + var entityUpdate = FindOrCreateEntityUpdate(entityId); entityUpdate.UpdateTypes.Add(EntityUpdateType.Position); entityUpdate.Position = position; @@ -186,37 +182,44 @@ public void UpdateEntityPosition(EntityType entityType, byte entityId, Vector2 p } /// - /// Update an entity's state in the current packet. + /// Update an entity's scale in the current packet. /// - /// The entity type. /// The ID of the entity. - /// The new state of the entity. - public void UpdateEntityState(EntityType entityType, byte entityId, byte state) { + /// The new scale of the entity. + public void UpdateEntityScale(byte entityId, bool scale) { lock (Lock) { - var entityUpdate = FindOrCreateEntityUpdate(entityType, entityId); + var entityUpdate = FindOrCreateEntityUpdate(entityId); - entityUpdate.UpdateTypes.Add(EntityUpdateType.State); - entityUpdate.State = state; + entityUpdate.UpdateTypes.Add(EntityUpdateType.Scale); + entityUpdate.Scale = scale; } } /// - /// Update an entity's state and variables in the current packet. + /// Update an entity's animation ID in the current packet. /// - /// The entity type. /// The ID of the entity. - /// The new state of the entity. - /// List of entity variables for this update. - public void UpdateEntityStateAndVariables(EntityType entityType, byte entityId, byte state, - List fsmVariables) { + /// The new animation ID of the entity. + public void UpdateEntityAnimation(byte entityId, byte animationId) { lock (Lock) { - var entityUpdate = FindOrCreateEntityUpdate(entityType, entityId); + var entityUpdate = FindOrCreateEntityUpdate(entityId); - entityUpdate.UpdateTypes.Add(EntityUpdateType.State); - entityUpdate.State = state; + entityUpdate.UpdateTypes.Add(EntityUpdateType.Animation); + entityUpdate.AnimationId = animationId; + } + } + + /// + /// Add data to an entity's update in the current packet. + /// + /// The ID of the entity. + /// The enumerable of byte data to add. + public void AddEntityData(byte entityId, IEnumerable data) { + lock (Lock) { + var entityUpdate = FindOrCreateEntityUpdate(entityId); - entityUpdate.UpdateTypes.Add(EntityUpdateType.Variables); - entityUpdate.Variables.AddRange(fsmVariables); + entityUpdate.UpdateTypes.Add(EntityUpdateType.Raw); + entityUpdate.RawData.AddRange(data); } } diff --git a/HKMP/Networking/Packet/Data/EntityUpdate.cs b/HKMP/Networking/Packet/Data/EntityUpdate.cs index 7c81767d..0963e906 100644 --- a/HKMP/Networking/Packet/Data/EntityUpdate.cs +++ b/HKMP/Networking/Packet/Data/EntityUpdate.cs @@ -13,11 +13,6 @@ internal class EntityUpdate : IPacketData { /// public bool DropReliableDataIfNewerExists => false; - /// - /// The type of the entity. - /// - public byte EntityType { get; set; } - /// /// The ID of the entity. /// @@ -32,28 +27,30 @@ internal class EntityUpdate : IPacketData { /// The position of the entity. /// public Vector2 Position { get; set; } - + /// - /// The state of the entity. + /// The boolean representation of the scale of the entity. /// - public byte State { get; set; } - + public bool Scale { get; set; } + /// - /// A list of variables for the entity. + /// The ID of the animation of the entity. /// - public List Variables { get; } + public byte AnimationId { get; set; } + + public List RawData { get; } /// /// Construct the entity update data. /// public EntityUpdate() { UpdateTypes = new HashSet(); - Variables = new List(); + + RawData = new List(); } /// public void WriteData(IPacket packet) { - packet.Write(EntityType); packet.Write(Id); // Construct the byte flag representing update types @@ -78,24 +75,31 @@ public void WriteData(IPacket packet) { if (UpdateTypes.Contains(EntityUpdateType.Position)) { packet.Write(Position); } + + if (UpdateTypes.Contains(EntityUpdateType.Scale)) { + packet.Write(Scale); + } - if (UpdateTypes.Contains(EntityUpdateType.State)) { - packet.Write(State); + if (UpdateTypes.Contains(EntityUpdateType.Animation)) { + packet.Write(AnimationId); } - if (UpdateTypes.Contains(EntityUpdateType.Variables)) { - // First write the number of bytes we are writing - packet.Write((byte)Variables.Count); + if (UpdateTypes.Contains(EntityUpdateType.Raw)) { + if (RawData.Count > byte.MaxValue) { + Logger.Get().Error(this, "Length of raw data exceeded max value of byte"); + } + + var length = (byte)System.Math.Min(RawData.Count, byte.MaxValue); - foreach (var b in Variables) { - packet.Write(b); + packet.Write(length); + for (var i = 0; i < length; i++) { + packet.Write(RawData[i]); } } } /// public void ReadData(IPacket packet) { - EntityType = packet.ReadByte(); Id = packet.ReadByte(); // Read the byte flag representing update types and reconstruct it @@ -117,18 +121,20 @@ public void ReadData(IPacket packet) { if (UpdateTypes.Contains(EntityUpdateType.Position)) { Position = packet.ReadVector2(); } + + if (UpdateTypes.Contains(EntityUpdateType.Scale)) { + Scale = packet.ReadBool(); + } - if (UpdateTypes.Contains(EntityUpdateType.State)) { - State = packet.ReadByte(); + if (UpdateTypes.Contains(EntityUpdateType.Animation)) { + AnimationId = packet.ReadByte(); } - if (UpdateTypes.Contains(EntityUpdateType.Variables)) { - // We first read how many bytes are in the array - var numBytes = packet.ReadByte(); + if (UpdateTypes.Contains(EntityUpdateType.Raw)) { + var length = packet.ReadByte(); - for (var i = 0; i < numBytes; i++) { - var readByte = packet.ReadByte(); - Variables.Add(readByte); + for (var i = 0; i < length; i++) { + RawData.Add(packet.ReadByte()); } } } @@ -139,7 +145,8 @@ public void ReadData(IPacket packet) { /// internal enum EntityUpdateType { Position = 0, - State, - Variables, + Scale, + Animation, + Raw } } \ No newline at end of file diff --git a/HKMP/Networking/Server/ServerUpdateManager.cs b/HKMP/Networking/Server/ServerUpdateManager.cs index 2c486fde..13323dbd 100644 --- a/HKMP/Networking/Server/ServerUpdateManager.cs +++ b/HKMP/Networking/Server/ServerUpdateManager.cs @@ -255,10 +255,9 @@ public void UpdatePlayerAnimation(ushort id, ushort clipId, byte frame, bool[] e /// /// Find or create an entity update instance in the current packet. /// - /// The type of the entity. /// The ID of the entity. /// An instance of the entity update in the packet. - private EntityUpdate FindOrCreateEntityUpdate(byte entityType, byte entityId) { + private EntityUpdate FindOrCreateEntityUpdate(byte entityId) { EntityUpdate entityUpdate = null; PacketDataCollection entityUpdateCollection; @@ -271,7 +270,7 @@ private EntityUpdate FindOrCreateEntityUpdate(byte entityType, byte entityId) { entityUpdateCollection = (PacketDataCollection)packetData; foreach (var existingPacketData in entityUpdateCollection.DataInstances) { var existingEntityUpdate = (EntityUpdate)existingPacketData; - if (existingEntityUpdate.EntityType.Equals(entityType) && existingEntityUpdate.Id == entityId) { + if (existingEntityUpdate.Id == entityId) { entityUpdate = existingEntityUpdate; break; } @@ -285,7 +284,6 @@ private EntityUpdate FindOrCreateEntityUpdate(byte entityType, byte entityId) { // If no existing instance was found, create one and add it to the (newly created) collection if (entityUpdate == null) { entityUpdate = new EntityUpdate { - EntityType = entityType, Id = entityId }; @@ -299,45 +297,56 @@ private EntityUpdate FindOrCreateEntityUpdate(byte entityType, byte entityId) { /// /// Update an entity's position in the packet. /// - /// The type of the entity. /// The ID of the entity. /// The position of the entity. - public void UpdateEntityPosition(byte entityType, byte entityId, Vector2 position) { + public void UpdateEntityPosition(byte entityId, Vector2 position) { lock (Lock) { - var entityUpdate = FindOrCreateEntityUpdate(entityType, entityId); + var entityUpdate = FindOrCreateEntityUpdate(entityId); entityUpdate.UpdateTypes.Add(EntityUpdateType.Position); entityUpdate.Position = position; } } - + /// - /// Update an entity's state in the packet. + /// Update an entity's scale in the packet. /// - /// The type of the entity. /// The ID of the entity. - /// The state index of the entity. - public void UpdateEntityState(byte entityType, byte entityId, byte stateIndex) { + /// The boolean representation of the scale of the entity. + public void UpdateEntityScale(byte entityId, bool scale) { lock (Lock) { - var entityUpdate = FindOrCreateEntityUpdate(entityType, entityId); + var entityUpdate = FindOrCreateEntityUpdate(entityId); - entityUpdate.UpdateTypes.Add(EntityUpdateType.State); - entityUpdate.State = stateIndex; + entityUpdate.UpdateTypes.Add(EntityUpdateType.Scale); + entityUpdate.Scale = scale; } } + + /// + /// Update an entity's animation in the packet. + /// + /// The ID of the entity. + /// The animation ID of the entity. + public void UpdateEntityAnimation(byte entityId, byte animationId) { + lock (Lock) { + var entityUpdate = FindOrCreateEntityUpdate(entityId); + entityUpdate.UpdateTypes.Add(EntityUpdateType.Animation); + entityUpdate.AnimationId = animationId; + } + } + /// - /// Update an entity's variables in the packet. + /// Add data to an entity's update in the current packet. /// - /// The type of the entity. /// The ID of the entity. - /// The variables of the entity. - public void UpdateEntityVariables(byte entityType, byte entityId, List fsmVariables) { + /// The enumerable of byte data to add. + public void AddEntityData(byte entityId, IEnumerable data) { lock (Lock) { - var entityUpdate = FindOrCreateEntityUpdate(entityType, entityId); + var entityUpdate = FindOrCreateEntityUpdate(entityId); - entityUpdate.UpdateTypes.Add(EntityUpdateType.Variables); - entityUpdate.Variables.AddRange(fsmVariables); + entityUpdate.UpdateTypes.Add(EntityUpdateType.Raw); + entityUpdate.RawData.AddRange(data); } } From e6193493738111a063bb6b87247f8d3105c96f6c Mon Sep 17 00:00:00 2001 From: Extremelyd1 Date: Thu, 9 Jun 2022 23:06:32 +0200 Subject: [PATCH 002/216] Server entity data and scene host tracking --- HKMP/Game/Client/ClientManager.cs | 17 +- HKMP/Game/Client/Entity/Entity.cs | 86 ++--- HKMP/Game/Client/Entity/EntityManager.cs | 13 +- HKMP/Game/Server/ServerEntityData.cs | 32 ++ HKMP/Game/Server/ServerEntityKey.cs | 46 +++ HKMP/Game/Server/ServerManager.cs | 322 +++++++++++++----- HKMP/Game/Server/ServerPlayerData.cs | 5 + HKMP/Networking/Client/ClientUpdateManager.cs | 12 +- .../Packet/Data/ClientPlayerDisconnect.cs | 7 + HKMP/Networking/Packet/Data/EntityUpdate.cs | 69 +++- .../Packet/Data/PlayerEnterScene.cs | 27 +- .../Packet/Data/PlayerLeaveScene.cs | 23 ++ HKMP/Networking/Packet/UpdatePacket.cs | 2 +- HKMP/Networking/Server/ServerUpdateManager.cs | 20 +- 14 files changed, 506 insertions(+), 175 deletions(-) create mode 100644 HKMP/Game/Server/ServerEntityData.cs create mode 100644 HKMP/Game/Server/ServerEntityKey.cs create mode 100644 HKMP/Networking/Packet/Data/PlayerLeaveScene.cs diff --git a/HKMP/Game/Client/ClientManager.cs b/HKMP/Game/Client/ClientManager.cs index 3e3ed502..41ddd470 100644 --- a/HKMP/Game/Client/ClientManager.cs +++ b/HKMP/Game/Client/ClientManager.cs @@ -549,6 +549,11 @@ private void OnPlayerAlreadyInScene(ClientPlayerAlreadyInScene alreadyInScene) { _entityManager.InitializeSceneClient(); } + foreach (var entityUpdate in alreadyInScene.EntityUpdateList) { + Logger.Get().Info(this, $"Updating already in scene entity with ID: {entityUpdate.Id}"); + HandleEntityUpdate(entityUpdate); + } + // Whether there were players in the scene or not, we have now determined whether // we are the scene host _sceneHostDetermined = true; @@ -660,7 +665,15 @@ private void OnEntityUpdate(EntityUpdate entityUpdate) { if (!_sceneHostDetermined) { return; } + + HandleEntityUpdate(entityUpdate); + } + /// + /// Method for handling received entity updates. + /// + /// The entity update to handle. + private void HandleEntityUpdate(EntityUpdate entityUpdate) { if (entityUpdate.UpdateTypes.Contains(EntityUpdateType.Position)) { _entityManager.UpdateEntityPosition(entityUpdate.Id, entityUpdate.Position); } @@ -673,8 +686,8 @@ private void OnEntityUpdate(EntityUpdate entityUpdate) { _entityManager.UpdateEntityAnimation(entityUpdate.Id, entityUpdate.AnimationId); } - if (entityUpdate.UpdateTypes.Contains(EntityUpdateType.Raw)) { - _entityManager.UpdateEntityData(entityUpdate.Id, entityUpdate.RawData); + if (entityUpdate.UpdateTypes.Contains(EntityUpdateType.Data)) { + _entityManager.UpdateEntityData(entityUpdate.Id, entityUpdate.GenericData); } } diff --git a/HKMP/Game/Client/Entity/Entity.cs b/HKMP/Game/Client/Entity/Entity.cs index 07fe96eb..deef9d7b 100644 --- a/HKMP/Game/Client/Entity/Entity.cs +++ b/HKMP/Game/Client/Entity/Entity.cs @@ -3,6 +3,7 @@ using Hkmp.Collection; using Hkmp.Fsm; using Hkmp.Networking.Client; +using Hkmp.Networking.Packet.Data; using Hkmp.Util; using UnityEngine; using Vector2 = Hkmp.Math.Vector2; @@ -60,8 +61,7 @@ GameObject gameObject } } - On.tk2dSpriteAnimator.Play_string += OnAnimationPlayed; - On.tk2dSpriteAnimator.Play_tk2dSpriteAnimationClip += OnAnimationPlayed; + On.tk2dSpriteAnimator.Play_tk2dSpriteAnimationClip_float_float += OnAnimationPlayed; } _climber = _gameObject.GetComponent(); @@ -109,51 +109,36 @@ private void OnUpdate() { ); } } - - private void OnAnimationPlayed( - On.tk2dSpriteAnimator.orig_Play_string orig, - tk2dSpriteAnimator self, - string clipName - ) { - orig(self, clipName); - - if (self != _animator) { - return; - } - - HandlePlayedAnimation(clipName); - } - + + // TODO: mark animations that loop differently to the server so it knows to repeat the animation for players + // that newly enter a scene private void OnAnimationPlayed( - On.tk2dSpriteAnimator.orig_Play_tk2dSpriteAnimationClip orig, - tk2dSpriteAnimator self, - tk2dSpriteAnimationClip clip + On.tk2dSpriteAnimator.orig_Play_tk2dSpriteAnimationClip_float_float orig, + tk2dSpriteAnimator self, + tk2dSpriteAnimationClip clip, + float clipStartTime, + float overrideFps ) { - orig(self, clip); + orig(self, clip, clipStartTime, overrideFps); if (self != _animator) { return; } - - HandlePlayedAnimation(clip.name); - } - - // TODO: mark animations that loop differently to the server so it knows to repeat the animation for players - // that newly enter a scene - private void HandlePlayedAnimation(string clipName) { + if (_isControlled) { return; } - if (!_animationClipNameIds.TryGetValue(clipName, out var animationId)) { - Logger.Get().Warn(this, $"Entity '{_gameObject.name}' played unknown animation: {clipName}"); + if (!_animationClipNameIds.TryGetValue(clip.name, out var animationId)) { + Logger.Get().Warn(this, $"Entity '{_gameObject.name}' played unknown animation: {clip.name}"); return; } - // Logger.Get().Info(this, $"Entity '{_gameObject.name}' sends animation: {clipName}, {animationId}"); + Logger.Get().Info(this, $"Entity '{_gameObject.name}' sends animation: {clip.name}, {animationId}, {clip.wrapMode}"); _netClient.UpdateManager.UpdateEntityAnimation( _entityId, - animationId + animationId, + clip.wrapMode == tk2dSpriteAnimationClip.WrapMode.Loop ); } @@ -172,8 +157,10 @@ private void OnUpdateRotation() { if (newRotation != _lastRotation) { _lastRotation = newRotation; - var data = new List { (byte)DataType.Rotation }; - data.AddRange(BitConverter.GetBytes(newRotation.z)); + var data = new EntityNetworkData { + Type = EntityNetworkData.DataType.Rotation + }; + data.Data.AddRange(BitConverter.GetBytes(newRotation.z)); _netClient.UpdateManager.AddEntityData( _entityId, @@ -230,25 +217,15 @@ public void UpdateAnimation(byte animationId) { return; } - // Logger.Get().Info(this, $"Entity '{_gameObject.name}' received animation: {animationId}, {clipName}"); + Logger.Get().Info(this, $"Entity '{_gameObject.name}' received animation: {animationId}, {clipName}"); _animator.Play(clipName); } - public void UpdateData(List dataList) { - var data = dataList.ToArray(); - - if (data.Length == 0) { - return; - } - - var i = 0; - while (i < data.Length) { - var dataType = (DataType)data[i++]; - - if (dataType == DataType.Rotation) { - var rotation = BitConverter.ToSingle(data, i); - i += 4; - + public void UpdateData(List entityNetworkData) { + foreach (var data in entityNetworkData) { + if (data.Type == EntityNetworkData.DataType.Rotation) { + var rotation = BitConverter.ToSingle(data.Data.ToArray(), 0); + var transform = _gameObject.transform; var eulerAngles = transform.eulerAngles; transform.eulerAngles = new Vector3( @@ -256,21 +233,14 @@ public void UpdateData(List dataList) { eulerAngles.y, rotation ); - } else { - break; } } } public void Destroy() { MonoBehaviourUtil.Instance.OnUpdateEvent -= OnUpdate; - On.tk2dSpriteAnimator.Play_string -= OnAnimationPlayed; - On.tk2dSpriteAnimator.Play_tk2dSpriteAnimationClip -= OnAnimationPlayed; + On.tk2dSpriteAnimator.Play_tk2dSpriteAnimationClip_float_float -= OnAnimationPlayed; MonoBehaviourUtil.Instance.OnUpdateEvent -= OnUpdateRotation; } - - private enum DataType : byte { - Rotation = 0, - } } } \ No newline at end of file diff --git a/HKMP/Game/Client/Entity/EntityManager.cs b/HKMP/Game/Client/Entity/EntityManager.cs index 2e1465ca..f11c376d 100644 --- a/HKMP/Game/Client/Entity/EntityManager.cs +++ b/HKMP/Game/Client/Entity/EntityManager.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using Hkmp.Networking.Client; +using Hkmp.Networking.Packet.Data; using UnityEngine; using UnityEngine.SceneManagement; using Vector2 = Hkmp.Math.Vector2; @@ -43,6 +44,16 @@ public void InitializeSceneClient() { _isSceneHost = false; } + public void BecomeSceneHost() { + Logger.Get().Info(this, "Becoming scene host"); + + _isSceneHost = true; + + foreach (var entity in _entities.Values) { + entity.MakeHost(); + } + } + public void UpdateEntityPosition(byte entityId, Vector2 position) { if (_isSceneHost) { return; @@ -79,7 +90,7 @@ public void UpdateEntityAnimation(byte entityId, byte animationId) { entity.UpdateAnimation(animationId); } - public void UpdateEntityData(byte entityId, List data) { + public void UpdateEntityData(byte entityId, List data) { if (_isSceneHost) { return; } diff --git a/HKMP/Game/Server/ServerEntityData.cs b/HKMP/Game/Server/ServerEntityData.cs new file mode 100644 index 00000000..21482462 --- /dev/null +++ b/HKMP/Game/Server/ServerEntityData.cs @@ -0,0 +1,32 @@ +using System.Collections.Generic; +using Hkmp.Math; +using Hkmp.Networking.Packet.Data; + +namespace Hkmp.Game.Server; + +/// +/// Class containing all the relevant data managed by the server about an entity. +/// +internal class ServerEntityData { + /// + /// The last position of the entity. + /// + public Vector2 Position { get; set; } + /// + /// The last scale of the entity. + /// + public bool Scale { get; set; } + /// + /// The ID of the last played looped animation. + /// + public byte? AnimationId { get; set; } + + /// + /// Generic data associated with this entity. + /// + public List GenericData { get; set; } + + public ServerEntityData() { + GenericData = new List(); + } +} \ No newline at end of file diff --git a/HKMP/Game/Server/ServerEntityKey.cs b/HKMP/Game/Server/ServerEntityKey.cs new file mode 100644 index 00000000..46c420bf --- /dev/null +++ b/HKMP/Game/Server/ServerEntityKey.cs @@ -0,0 +1,46 @@ +using System; + +namespace Hkmp.Game.Server; + +/// +/// A key class that uniquely identifies an entity. +/// +internal class ServerEntityKey : IEquatable { + /// + /// The scene that the entity is in. + /// + public string Scene { get; } + /// + /// The ID of the entity. + /// + public byte EntityId { get; } + + public ServerEntityKey(string scene, byte entityId) { + Scene = scene; + EntityId = entityId; + } + + public override bool Equals(object obj) { + if (obj == null || GetType() != obj.GetType()) { + return false; + } + + return Equals((ServerEntityKey) obj); + } + + public bool Equals(ServerEntityKey other) { + if (other == null) { + return false; + } + + return Scene == other.Scene && EntityId == other.EntityId; + } + + public override int GetHashCode() { + unchecked { + var hashCode = Scene != null ? Scene.GetHashCode() : 0; + hashCode = (hashCode * 397) ^ EntityId.GetHashCode(); + return hashCode; + } + } +} \ No newline at end of file diff --git a/HKMP/Game/Server/ServerManager.cs b/HKMP/Game/Server/ServerManager.cs index 4ccbb1fd..483586b5 100644 --- a/HKMP/Game/Server/ServerManager.cs +++ b/HKMP/Game/Server/ServerManager.cs @@ -36,14 +36,18 @@ internal abstract class ServerManager : IServerManager { /// private readonly ConcurrentDictionary _playerData; + private readonly ConcurrentDictionary _entityData; + /// /// The white-list for managing player logins. /// private readonly WhiteList _whiteList; + /// /// Authorized list for managing player permission. /// private readonly AuthKeyList _authorizedList; + /// /// The list of banned users. /// @@ -53,11 +57,12 @@ internal abstract class ServerManager : IServerManager { /// The server game settings. /// protected readonly Settings.GameSettings GameSettings; + /// /// The server command manager instance. /// protected readonly ServerCommandManager CommandManager; - + /// /// The addon manager instance. /// @@ -72,13 +77,16 @@ internal abstract class ServerManager : IServerManager { /// public event Action PlayerConnectEvent; + /// public event Action PlayerDisconnectEvent; + /// public event Action PlayerEnterSceneEvent; + /// public event Action PlayerLeaveSceneEvent; - + #endregion /// @@ -95,6 +103,7 @@ PacketManager packetManager _netServer = netServer; GameSettings = gameSettings; _playerData = new ConcurrentDictionary(); + _entityData = new ConcurrentDictionary(); CommandManager = new ServerCommandManager(); var eventAggregator = new EventAggregator(); @@ -202,7 +211,9 @@ public void OnUpdateGameSettings() { return; } - _netServer.SetDataForAllClients(updateManager => { updateManager.UpdateGameSettings(GameSettings); }); + _netServer.SetDataForAllClients(updateManager => { + updateManager.UpdateGameSettings(GameSettings); + }); } /// @@ -232,9 +243,9 @@ private void OnHelloServer(ushort id, HelloServer helloServer) { if (idPlayerDataPair.Key == id) { continue; } - + clientInfo.Add((idPlayerDataPair.Key, idPlayerDataPair.Value.Username)); - + _netServer.GetUpdateManagerForClient(idPlayerDataPair.Key)?.AddPlayerConnectData( id, helloServer.Username @@ -246,7 +257,8 @@ private void OnHelloServer(ushort id, HelloServer helloServer) { try { PlayerConnectEvent?.Invoke(playerData); } catch (Exception e) { - Logger.Get().Warn(this, $"Exception thrown while invoking PlayerConnect event, {e.GetType()}, {e.Message}, {e.StackTrace}"); + Logger.Get().Warn(this, + $"Exception thrown while invoking PlayerConnect event, {e.GetType()}, {e.Message}, {e.StackTrace}"); } OnClientEnterScene(playerData); @@ -274,11 +286,12 @@ private void OnClientEnterScene(ushort id, ServerPlayerEnterScene playerEnterSce playerData.AnimationId = playerEnterScene.AnimationClipId; OnClientEnterScene(playerData); - + try { PlayerEnterSceneEvent?.Invoke(playerData); } catch (Exception e) { - Logger.Get().Warn(this, $"Exception thrown while invoking PlayerEnterScene event, {e.GetType()}, {e.Message}, {e.StackTrace}"); + Logger.Get().Warn(this, + $"Exception thrown while invoking PlayerEnterScene event, {e.GetType()}, {e.Message}, {e.StackTrace}"); } } @@ -313,7 +326,8 @@ private void OnClientEnterScene(ServerPlayerData playerData) { playerData.AnimationId ); - Logger.Get().Info(this, $"Sending that {idPlayerDataPair.Key} is already in scene to {playerData.Id}"); + Logger.Get().Info(this, + $"Sending that {idPlayerDataPair.Key} is already in scene to {playerData.Id}"); alreadyPlayersInScene = true; @@ -331,56 +345,48 @@ private void OnClientEnterScene(ServerPlayerData playerData) { } } - _netServer.GetUpdateManagerForClient(playerData.Id)?.AddPlayerAlreadyInSceneData( - enterSceneList, - !alreadyPlayersInScene - ); - } + var entityUpdateList = new List(); - /// - /// Callback method for when a player leaves a scene. - /// - /// The ID of the player. - private void OnClientLeaveScene(ushort id) { - if (!_playerData.TryGetValue(id, out var playerData)) { - Logger.Get().Warn(this, $"Received LeaveScene data from {id}, but player is not in mapping"); - return; - } - - var sceneName = playerData.CurrentScene; - - if (sceneName.Length == 0) { - Logger.Get().Info(this, - $"Received LeaveScene data from ID {id}, but there was no last scene registered"); - return; - } - - Logger.Get().Info(this, $"Received LeaveScene data from ID {id}, last scene: {sceneName}"); - - playerData.CurrentScene = ""; - - foreach (var idPlayerDataPair in _playerData.GetCopy()) { - // Skip source player - if (idPlayerDataPair.Key == id) { + foreach (var keyDataPair in _entityData.GetCopy()) { + var entityKey = keyDataPair.Key; + + // Check which entities are actually in the scene that the player is entering + if (!entityKey.Scene.Equals(playerData.CurrentScene)) { continue; } - - var otherPlayerData = idPlayerDataPair.Value; - - // Send the packet to all clients on the scene that the player left - // to indicate that this client has left their scene - if (otherPlayerData.CurrentScene.Equals(sceneName)) { - Logger.Get().Info(this, $"Sending leave scene packet to {idPlayerDataPair.Key}"); - - _netServer.GetUpdateManagerForClient(idPlayerDataPair.Key)?.AddPlayerLeaveSceneData(id); + + Logger.Get().Info(this, $"Sending that entity '{entityKey.EntityId}' is already in scene to '{playerData.Id}'"); + + var entityData = keyDataPair.Value; + var entityUpdate = new EntityUpdate { + Id = entityKey.EntityId, + Position = entityData.Position, + Scale = entityData.Scale, + GenericData = entityData.GenericData + }; + entityUpdate.UpdateTypes.Add(EntityUpdateType.Position); + entityUpdate.UpdateTypes.Add(EntityUpdateType.Scale); + entityUpdate.UpdateTypes.Add(EntityUpdateType.Data); + + if (entityData.AnimationId.HasValue) { + Logger.Get().Info(this, " Entity has looping animation"); + + entityUpdate.UpdateTypes.Add(EntityUpdateType.Animation); + entityUpdate.AnimationId = entityData.AnimationId.Value; } + + entityUpdateList.Add(entityUpdate); } - - try { - PlayerLeaveSceneEvent?.Invoke(playerData); - } catch (Exception e) { - Logger.Get().Warn(this, $"Exception thrown while invoking PlayerLeaveScene event, {e.GetType()}, {e.Message}, {e.StackTrace}"); + + if (!alreadyPlayersInScene) { + playerData.IsSceneHost = true; } + + _netServer.GetUpdateManagerForClient(playerData.Id)?.AddPlayerAlreadyInSceneData( + enterSceneList, + entityUpdateList, + !alreadyPlayersInScene + ); } /// @@ -473,6 +479,19 @@ private void OnEntityUpdate(ushort id, EntityUpdate entityUpdate) { return; } + // Create the key for the entity data + var serverEntityKey = new ServerEntityKey( + playerData.CurrentScene, + entityUpdate.Id + ); + + // Check with the created key whether we have an existing entry + if (!_entityData.TryGetValue(serverEntityKey, out var entityData)) { + // If the entry for this entity did not yet exist, we insert a new one + entityData = new ServerEntityData(); + _entityData[serverEntityKey] = entityData; + } + if (entityUpdate.UpdateTypes.Contains(EntityUpdateType.Position)) { SendDataInSameScene( id, @@ -484,8 +503,10 @@ private void OnEntityUpdate(ushort id, EntityUpdate entityUpdate) { ); } ); + + entityData.Position = entityUpdate.Position; } - + if (entityUpdate.UpdateTypes.Contains(EntityUpdateType.Scale)) { SendDataInSameScene( id, @@ -497,8 +518,10 @@ private void OnEntityUpdate(ushort id, EntityUpdate entityUpdate) { ); } ); + + entityData.Scale = entityUpdate.Scale; } - + if (entityUpdate.UpdateTypes.Contains(EntityUpdateType.Animation)) { SendDataInSameScene( id, @@ -510,19 +533,36 @@ private void OnEntityUpdate(ushort id, EntityUpdate entityUpdate) { ); } ); + + entityData.AnimationId = entityUpdate.AnimationLoops ? entityUpdate.AnimationId : null; + + if (entityUpdate.AnimationLoops) { + Logger.Get().Info(this, $"Storing looped animation: {entityUpdate.AnimationId}"); + } } - - if (entityUpdate.UpdateTypes.Contains(EntityUpdateType.Raw)) { + + if (entityUpdate.UpdateTypes.Contains(EntityUpdateType.Data)) { SendDataInSameScene( id, playerData.CurrentScene, otherId => { _netServer.GetUpdateManagerForClient(otherId)?.AddEntityData( entityUpdate.Id, - entityUpdate.RawData + entityUpdate.GenericData ); } ); + + foreach (var updateData in entityUpdate.GenericData) { + if (updateData.Type == EntityNetworkData.DataType.Rotation) { + var existingData = entityData.GenericData.Find( + d => d.Type == EntityNetworkData.DataType.Rotation + ); + if (existingData != null) { + existingData.Data = updateData.Data; + } + } + } } } @@ -553,41 +593,142 @@ public void InternalDisconnectPlayer(ushort id, DisconnectReason reason) { } /// - /// Process a disconnect for the player with the given ID. + /// Handle a player leaving a scene by transition, disconnect or timeout. /// - /// The ID of the player. - /// Whether this player timed out or disconnected normally. - private void ProcessPlayerDisconnect(ushort id, bool timeout = false) { - if (!timeout) { - // If this isn't a timeout, then we need to propagate this packet to the NetServer - _netServer.OnClientDisconnect(id); + /// The ID of the player that left the scene. + /// Whether the player disconnected from the server. + /// Whether the disconnect was due to connection timeout. + private void HandlePlayerLeaveScene(ushort id, bool disconnected, bool timeout = false) { + if (!_playerData.TryGetValue(id, out var playerData)) { + Logger.Get().Warn(this, $"Handling player leave scene (dc: {disconnected}) for ID {id}, but player is not in mapping"); + return; } - if (!_playerData.TryGetValue(id, out var playerData)) { + var sceneName = playerData.CurrentScene; + + if (!disconnected && sceneName.Length == 0) { + Logger.Get().Info(this, $"Handling player leave scene for ID {id}, but there was no last scene registered"); return; } + + Logger.Get().Info(this, $"Handling player leave scene (dc: {disconnected}) for ID {id}, last scene: {sceneName}"); + + playerData.CurrentScene = ""; var username = playerData.Username; + + // Keep track of whether the scene that the player has left is now empty + var isSceneNowEmpty = true; foreach (var idPlayerDataPair in _playerData.GetCopy()) { + // Skip source player if (idPlayerDataPair.Key == id) { continue; } - _netServer.GetUpdateManagerForClient(idPlayerDataPair.Key)?.AddPlayerDisconnectData( - id, - username, - timeout - ); - } + var otherPlayerData = idPlayerDataPair.Value; + + // Send a packet to all clients in the scene that the player has left their scene + if (otherPlayerData.CurrentScene == sceneName) { + Logger.Get().Info(this, $"Sending leave scene packet to {idPlayerDataPair.Key}"); + + // We have now found at least one player that is still in this scene + isSceneNowEmpty = false; + + var updateManager = _netServer.GetUpdateManagerForClient(idPlayerDataPair.Key); + + var otherPlayerBecomesSceneHost = false; + if (playerData.IsSceneHost) { + // If the leaving player was the scene host, we can make this player the new scene host + otherPlayerBecomesSceneHost = true; - // Now remove the client from the player data mapping - _playerData.Remove(id); + // Reset the scene host variable in the leaving player, so only a single other player + // becomes the scene host + playerData.IsSceneHost = false; + + // Also set the player data of the new scene host + otherPlayerData.IsSceneHost = true; + + Logger.Get().Info(this, $" {idPlayerDataPair.Key} has become scene host"); + } + + if (disconnected) { + updateManager.AddPlayerDisconnectData( + id, + username, + otherPlayerBecomesSceneHost, + timeout + ); + } else { + updateManager.AddPlayerLeaveSceneData( + id, + otherPlayerBecomesSceneHost + ); + } + } + } + + // In case there were no other players to make scene host, we still need to reset the leaving + // player's status of scene host + playerData.IsSceneHost = false; + // If the scene is now empty, we can remove all data from stored entities in that scene + if (isSceneNowEmpty) { + foreach (var keyDataPair in _entityData.GetCopy()) { + if (keyDataPair.Key.Scene == sceneName) { + _entityData.Remove(keyDataPair.Key); + } + } + } + + if (disconnected) { + // Now remove the client from the player data mapping + _playerData.Remove(id); + } + } + + /// + /// Callback method for when a player leaves a scene. + /// + /// The ID of the player. + private void OnClientLeaveScene(ushort id) { + if (!_playerData.TryGetValue(id, out var playerData)) { + Logger.Get().Warn(this, $"Received LeaveScene data from {id}, but player is not in mapping"); + return; + } + + HandlePlayerLeaveScene(id, false); + + try { + PlayerLeaveSceneEvent?.Invoke(playerData); + } catch (Exception e) { + Logger.Get().Warn(this, + $"Exception thrown while invoking PlayerLeaveScene event, {e.GetType()}, {e.Message}, {e.StackTrace}"); + } + } + + /// + /// Process a disconnect for the player with the given ID. + /// + /// The ID of the player. + /// Whether this player timed out or disconnected normally. + private void ProcessPlayerDisconnect(ushort id, bool timeout = false) { + if (!timeout) { + // If this isn't a timeout, then we need to propagate this packet to the NetServer + _netServer.OnClientDisconnect(id); + } + + if (!_playerData.TryGetValue(id, out var playerData)) { + return; + } + + HandlePlayerLeaveScene(id, true, timeout); + try { PlayerDisconnectEvent?.Invoke(playerData); } catch (Exception e) { - Logger.Get().Warn(this, $"Exception thrown while invoking PlayerDisconnect event, {e.GetType()}, {e.Message}, {e.StackTrace}"); + Logger.Get().Warn(this, + $"Exception thrown while invoking PlayerDisconnect event, {e.GetType()}, {e.Message}, {e.StackTrace}"); } } @@ -606,7 +747,9 @@ private void OnPlayerDeath(ushort id) { SendDataInSameScene( id, playerData.CurrentScene, - otherId => { _netServer.GetUpdateManagerForClient(otherId)?.AddPlayerDeathData(id); } + otherId => { + _netServer.GetUpdateManagerForClient(otherId)?.AddPlayerDeathData(id); + } ); } @@ -702,10 +845,11 @@ private void HandleInvalidLoginAddons(ServerUpdateManager updateManager) { private bool OnLoginRequest( ushort id, IPEndPoint endPoint, - LoginRequest loginRequest, + LoginRequest loginRequest, ServerUpdateManager updateManager ) { - Logger.Get().Info(this, $"Received login request from IP: {endPoint.Address}, username: {loginRequest.Username}"); + Logger.Get().Info(this, + $"Received login request from IP: {endPoint.Address}, username: {loginRequest.Username}"); if (_banList.IsIpBanned(endPoint.Address.ToString()) || _banList.Contains(loginRequest.AuthKey)) { updateManager.SetLoginResponse(new LoginResponse { @@ -722,7 +866,7 @@ ServerUpdateManager updateManager }); return false; } - + Logger.Get().Info(this, " Username was pre-listed, auth key has been added to whitelist"); _whiteList.Add(loginRequest.AuthKey); @@ -739,7 +883,7 @@ ServerUpdateManager updateManager return false; } } - + var addonData = loginRequest.AddonData; // Construct a string that contains all addons and respective versions by mapping the items in the addon data @@ -788,7 +932,7 @@ out var correspondingServerAddon var playerData = new ServerPlayerData( id, endPoint.Address.ToString(), - loginRequest.Username, + loginRequest.Username, loginRequest.AuthKey, _authorizedList ); @@ -863,17 +1007,17 @@ private void OnChatMessage(ushort id, ChatMessage chatMessage) { if (TryProcessCommand( new PlayerCommandSender( - _authorizedList.Contains(playerData.AuthKey), + _authorizedList.Contains(playerData.AuthKey), _netServer.GetUpdateManagerForClient(id) - ), + ), chatMessage.Message - )) { + )) { Logger.Get().Info(this, "Chat message was processed as command"); return; } var message = $"[{playerData.Username}]: {chatMessage.Message}"; - + foreach (var idPlayerDataPair in _playerData.GetCopy()) { _netServer.GetUpdateManagerForClient(idPlayerDataPair.Key)?.AddChatMessage(message); } @@ -906,7 +1050,7 @@ private void CheckValidMessage(string message) { if (message == null) { throw new ArgumentException("Message cannot be null"); } - + if (message.Length > ChatMessage.MaxMessageLength) { throw new ArgumentException($"Message length exceeds max length of {ChatMessage.MaxMessageLength}"); } @@ -921,7 +1065,7 @@ private void CheckValidMessage(string message) { /// public void SendMessage(ushort id, string message) { CheckValidMessage(message); - + var updateManager = _netServer.GetUpdateManagerForClient(id); updateManager?.AddChatMessage(message); } @@ -938,13 +1082,13 @@ public void SendMessage(IServerPlayer player, string message) { /// public void BroadcastMessage(string message) { CheckValidMessage(message); - + foreach (var player in _playerData.GetCopy().Values) { var updateManager = _netServer.GetUpdateManagerForClient(player.Id); updateManager?.AddChatMessage(message); } } - + /// public void DisconnectPlayer(ushort id, DisconnectReason reason) { if (!_playerData.TryGetValue(id, out _)) { diff --git a/HKMP/Game/Server/ServerPlayerData.cs b/HKMP/Game/Server/ServerPlayerData.cs index 060aed2a..143472c9 100644 --- a/HKMP/Game/Server/ServerPlayerData.cs +++ b/HKMP/Game/Server/ServerPlayerData.cs @@ -41,6 +41,11 @@ internal class ServerPlayerData : IServerPlayer { /// public byte SkinId { get; set; } + + /// + /// Whether this player is the host of their current scene. + /// + public bool IsSceneHost { get; set; } /// /// Reference of the authorized list for checking whether this player is authorized. diff --git a/HKMP/Networking/Client/ClientUpdateManager.cs b/HKMP/Networking/Client/ClientUpdateManager.cs index 19ba0970..b53d311d 100644 --- a/HKMP/Networking/Client/ClientUpdateManager.cs +++ b/HKMP/Networking/Client/ClientUpdateManager.cs @@ -200,12 +200,14 @@ public void UpdateEntityScale(byte entityId, bool scale) { /// /// The ID of the entity. /// The new animation ID of the entity. - public void UpdateEntityAnimation(byte entityId, byte animationId) { + /// Whether the animation of the entity loops. + public void UpdateEntityAnimation(byte entityId, byte animationId, bool animationLoops) { lock (Lock) { var entityUpdate = FindOrCreateEntityUpdate(entityId); entityUpdate.UpdateTypes.Add(EntityUpdateType.Animation); entityUpdate.AnimationId = animationId; + entityUpdate.AnimationLoops = animationLoops; } } @@ -213,13 +215,13 @@ public void UpdateEntityAnimation(byte entityId, byte animationId) { /// Add data to an entity's update in the current packet. /// /// The ID of the entity. - /// The enumerable of byte data to add. - public void AddEntityData(byte entityId, IEnumerable data) { + /// The entity network data to add. + public void AddEntityData(byte entityId, EntityNetworkData data) { lock (Lock) { var entityUpdate = FindOrCreateEntityUpdate(entityId); - entityUpdate.UpdateTypes.Add(EntityUpdateType.Raw); - entityUpdate.RawData.AddRange(data); + entityUpdate.UpdateTypes.Add(EntityUpdateType.Data); + entityUpdate.GenericData.Add(data); } } diff --git a/HKMP/Networking/Packet/Data/ClientPlayerDisconnect.cs b/HKMP/Networking/Packet/Data/ClientPlayerDisconnect.cs index 95dee88a..98a272a3 100644 --- a/HKMP/Networking/Packet/Data/ClientPlayerDisconnect.cs +++ b/HKMP/Networking/Packet/Data/ClientPlayerDisconnect.cs @@ -7,6 +7,11 @@ internal class ClientPlayerDisconnect : GenericClientData { /// The username of the player that disconnected. /// public string Username { get; set; } + + /// + /// Whether the player receiving this data becomes the new scene host. + /// + public bool NewSceneHost { get; set; } /// /// Whether the player timed out or disconnected normally. @@ -25,6 +30,7 @@ public ClientPlayerDisconnect() { public override void WriteData(IPacket packet) { packet.Write(Id); packet.Write(Username); + packet.Write(NewSceneHost); packet.Write(TimedOut); } @@ -32,6 +38,7 @@ public override void WriteData(IPacket packet) { public override void ReadData(IPacket packet) { Id = packet.ReadUShort(); Username = packet.ReadString(); + NewSceneHost = packet.ReadBool(); TimedOut = packet.ReadBool(); } } diff --git a/HKMP/Networking/Packet/Data/EntityUpdate.cs b/HKMP/Networking/Packet/Data/EntityUpdate.cs index 0963e906..440e220e 100644 --- a/HKMP/Networking/Packet/Data/EntityUpdate.cs +++ b/HKMP/Networking/Packet/Data/EntityUpdate.cs @@ -37,16 +37,20 @@ internal class EntityUpdate : IPacketData { /// The ID of the animation of the entity. /// public byte AnimationId { get; set; } + /// + /// Whether the animation of the entity loops. + /// + public bool AnimationLoops { get; set; } - public List RawData { get; } + public List GenericData { get; init; } /// /// Construct the entity update data. /// public EntityUpdate() { UpdateTypes = new HashSet(); - - RawData = new List(); + AnimationLoops = false; + GenericData = new List(); } /// @@ -82,18 +86,19 @@ public void WriteData(IPacket packet) { if (UpdateTypes.Contains(EntityUpdateType.Animation)) { packet.Write(AnimationId); + packet.Write(AnimationLoops); } - if (UpdateTypes.Contains(EntityUpdateType.Raw)) { - if (RawData.Count > byte.MaxValue) { - Logger.Get().Error(this, "Length of raw data exceeded max value of byte"); + if (UpdateTypes.Contains(EntityUpdateType.Data)) { + if (GenericData.Count > byte.MaxValue) { + Logger.Get().Error(this, "Length of entity network data instances exceeded max value of byte"); } - var length = (byte)System.Math.Min(RawData.Count, byte.MaxValue); + var length = (byte)System.Math.Min(GenericData.Count, byte.MaxValue); packet.Write(length); for (var i = 0; i < length; i++) { - packet.Write(RawData[i]); + GenericData[i].WriteData(packet); } } } @@ -128,18 +133,60 @@ public void ReadData(IPacket packet) { if (UpdateTypes.Contains(EntityUpdateType.Animation)) { AnimationId = packet.ReadByte(); + AnimationLoops = packet.ReadBool(); } - if (UpdateTypes.Contains(EntityUpdateType.Raw)) { + if (UpdateTypes.Contains(EntityUpdateType.Data)) { var length = packet.ReadByte(); for (var i = 0; i < length; i++) { - RawData.Add(packet.ReadByte()); + var entityNetworkData = new EntityNetworkData(); + entityNetworkData.ReadData(packet); + + GenericData.Add(entityNetworkData); } } } } + internal class EntityNetworkData { + public DataType Type { get; set; } + public List Data { get; set; } + + public EntityNetworkData() { + Data = new List(); + } + + public void WriteData(IPacket packet) { + packet.Write((byte)Type); + + if (Data.Count > byte.MaxValue) { + Logger.Get().Error(this, "Length of entity network data exceeded max value of byte"); + } + + var length = (byte)System.Math.Min(Data.Count, byte.MaxValue); + + packet.Write(length); + for (var i = 0; i < length; i++) { + packet.Write(Data[i]); + } + } + + public void ReadData(IPacket packet) { + Type = (DataType) packet.ReadByte(); + + var length = packet.ReadByte(); + + for (var i = 0; i < length; i++) { + Data.Add(packet.ReadByte()); + } + } + + public enum DataType : byte { + Rotation = 0, + } + } + /// /// Enumeration of entity update types. /// @@ -147,6 +194,6 @@ internal enum EntityUpdateType { Position = 0, Scale, Animation, - Raw + Data } } \ No newline at end of file diff --git a/HKMP/Networking/Packet/Data/PlayerEnterScene.cs b/HKMP/Networking/Packet/Data/PlayerEnterScene.cs index beaea9fe..2ba3ff43 100644 --- a/HKMP/Networking/Packet/Data/PlayerEnterScene.cs +++ b/HKMP/Networking/Packet/Data/PlayerEnterScene.cs @@ -85,6 +85,11 @@ internal class ClientPlayerAlreadyInScene : IPacketData { /// List of client player enter scene data instances. /// public List PlayerEnterSceneList { get; } + + /// + /// List of entity update instances. + /// + public List EntityUpdateList { get; } /// /// Whether the receiving player is scene host. @@ -96,6 +101,7 @@ internal class ClientPlayerAlreadyInScene : IPacketData { /// public ClientPlayerAlreadyInScene() { PlayerEnterSceneList = new List(); + EntityUpdateList = new List(); } /// @@ -108,13 +114,20 @@ public void WriteData(IPacket packet) { PlayerEnterSceneList[i].WriteData(packet); } + length = (byte)System.Math.Min(byte.MaxValue, EntityUpdateList.Count); + + packet.Write(length); + + for (var i = 0; i < length; i++) { + EntityUpdateList[i].WriteData(packet); + } + packet.Write(SceneHost); } /// public void ReadData(IPacket packet) { var length = packet.ReadByte(); - for (var i = 0; i < length; i++) { // Create new instance of generic type var instance = new ClientPlayerEnterScene(); @@ -126,6 +139,18 @@ public void ReadData(IPacket packet) { PlayerEnterSceneList.Add(instance); } + length = packet.ReadByte(); + for (var i = 0; i < length; i++) { + // Create new instance of entity update + var instance = new EntityUpdate(); + + // Read the packet data into the instance + instance.ReadData(packet); + + // And add it to our already initialized list + EntityUpdateList.Add(instance); + } + SceneHost = packet.ReadBool(); } } diff --git a/HKMP/Networking/Packet/Data/PlayerLeaveScene.cs b/HKMP/Networking/Packet/Data/PlayerLeaveScene.cs new file mode 100644 index 00000000..e5943ec0 --- /dev/null +++ b/HKMP/Networking/Packet/Data/PlayerLeaveScene.cs @@ -0,0 +1,23 @@ +namespace Hkmp.Networking.Packet.Data { + /// + /// Packet data for the client-bound player leave scene data. + /// + internal class ClientPlayerLeaveScene : GenericClientData { + /// + /// Whether the player receiving this data becomes the new scene host. + /// + public bool NewSceneHost { get; set; } + + /// + public override void WriteData(IPacket packet) { + packet.Write(Id); + packet.Write(NewSceneHost); + } + + /// + public override void ReadData(IPacket packet) { + Id = packet.ReadUShort(); + NewSceneHost = packet.ReadBool(); + } + } +} \ No newline at end of file diff --git a/HKMP/Networking/Packet/UpdatePacket.cs b/HKMP/Networking/Packet/UpdatePacket.cs index 70bb2cff..3e8400b8 100644 --- a/HKMP/Networking/Packet/UpdatePacket.cs +++ b/HKMP/Networking/Packet/UpdatePacket.cs @@ -895,7 +895,7 @@ protected override IPacketData InstantiatePacketDataFromId(ClientPacketId packet case ClientPacketId.PlayerAlreadyInScene: return new ClientPlayerAlreadyInScene(); case ClientPacketId.PlayerLeaveScene: - return new PacketDataCollection(); + return new PacketDataCollection(); case ClientPacketId.PlayerUpdate: return new PacketDataCollection(); case ClientPacketId.EntityUpdate: diff --git a/HKMP/Networking/Server/ServerUpdateManager.cs b/HKMP/Networking/Server/ServerUpdateManager.cs index 13323dbd..5b45db48 100644 --- a/HKMP/Networking/Server/ServerUpdateManager.cs +++ b/HKMP/Networking/Server/ServerUpdateManager.cs @@ -118,8 +118,9 @@ public void AddPlayerConnectData(ushort id, string username) { /// /// The ID of the player disconnecting. /// The username of the player disconnecting. + /// Whether the player this is sent to becomes the new scene host. /// Whether the player timed out or disconnected normally. - public void AddPlayerDisconnectData(ushort id, string username, bool timedOut = false) { + public void AddPlayerDisconnectData(ushort id, string username, bool newSceneHost, bool timedOut = false) { lock (Lock) { var playerDisconnect = FindOrCreatePacketData(id, ClientPacketId.PlayerDisconnect); @@ -165,9 +166,11 @@ ushort animationClipId /// Add player already in scene data to the current packet. /// /// An enumerable of ClientPlayerEnterScene instances to add. + /// An enumerable of EntityUpdate instances to add. /// Whether the player is the scene host. public void AddPlayerAlreadyInSceneData( IEnumerable playerEnterSceneList, + IEnumerable entityUpdateList, bool sceneHost ) { lock (Lock) { @@ -175,6 +178,7 @@ bool sceneHost SceneHost = sceneHost }; alreadyInScene.PlayerEnterSceneList.AddRange(playerEnterSceneList); + alreadyInScene.EntityUpdateList.AddRange(entityUpdateList); CurrentUpdatePacket.SetSendingPacketData(ClientPacketId.PlayerAlreadyInScene, alreadyInScene); } @@ -184,10 +188,12 @@ bool sceneHost /// Add player leave scene data to the current packet. /// /// The ID of the player. - public void AddPlayerLeaveSceneData(ushort id) { + /// Whether the player receiving this packet becomes the new scene host. + public void AddPlayerLeaveSceneData(ushort id, bool newSceneHost) { lock (Lock) { - var playerLeaveScene = FindOrCreatePacketData(id, ClientPacketId.PlayerLeaveScene); + var playerLeaveScene = FindOrCreatePacketData(id, ClientPacketId.PlayerLeaveScene); playerLeaveScene.Id = id; + playerLeaveScene.NewSceneHost = newSceneHost; } } @@ -340,13 +346,13 @@ public void UpdateEntityAnimation(byte entityId, byte animationId) { /// Add data to an entity's update in the current packet. /// /// The ID of the entity. - /// The enumerable of byte data to add. - public void AddEntityData(byte entityId, IEnumerable data) { + /// The list of entity network data to add. + public void AddEntityData(byte entityId, List data) { lock (Lock) { var entityUpdate = FindOrCreateEntityUpdate(entityId); - entityUpdate.UpdateTypes.Add(EntityUpdateType.Raw); - entityUpdate.RawData.AddRange(data); + entityUpdate.UpdateTypes.Add(EntityUpdateType.Data); + entityUpdate.GenericData.AddRange(data); } } From 78388092281a707aa71183cc0fd5b8c028632d1d Mon Sep 17 00:00:00 2001 From: Extremelyd1 Date: Fri, 10 Jun 2022 17:19:39 +0200 Subject: [PATCH 003/216] Fix animation wrap mode on scene entry --- HKMP/Game/Client/ClientManager.cs | 22 +++++++--- HKMP/Game/Client/Entity/Entity.cs | 42 +++++++++++++++--- HKMP/Game/Client/Entity/EntityManager.cs | 44 ++++++++++++------- HKMP/Game/Server/ServerEntityData.cs | 6 ++- HKMP/Game/Server/ServerManager.cs | 14 +++--- HKMP/Networking/Client/ClientUpdateManager.cs | 6 +-- HKMP/Networking/Packet/Data/EntityUpdate.cs | 9 ++-- HKMP/Networking/Server/ServerUpdateManager.cs | 4 +- 8 files changed, 102 insertions(+), 45 deletions(-) diff --git a/HKMP/Game/Client/ClientManager.cs b/HKMP/Game/Client/ClientManager.cs index 41ddd470..fe87c73b 100644 --- a/HKMP/Game/Client/ClientManager.cs +++ b/HKMP/Game/Client/ClientManager.cs @@ -204,7 +204,7 @@ ModSettings modSettings OnPlayerEnterScene); packetManager.RegisterClientPacketHandler(ClientPacketId.PlayerAlreadyInScene, OnPlayerAlreadyInScene); - packetManager.RegisterClientPacketHandler(ClientPacketId.PlayerLeaveScene, + packetManager.RegisterClientPacketHandler(ClientPacketId.PlayerLeaveScene, OnPlayerLeaveScene); packetManager.RegisterClientPacketHandler(ClientPacketId.PlayerUpdate, OnPlayerUpdate); packetManager.RegisterClientPacketHandler(ClientPacketId.EntityUpdate, OnEntityUpdate); @@ -551,7 +551,7 @@ private void OnPlayerAlreadyInScene(ClientPlayerAlreadyInScene alreadyInScene) { foreach (var entityUpdate in alreadyInScene.EntityUpdateList) { Logger.Get().Info(this, $"Updating already in scene entity with ID: {entityUpdate.Id}"); - HandleEntityUpdate(entityUpdate); + HandleEntityUpdate(entityUpdate, true); } // Whether there were players in the scene or not, we have now determined whether @@ -597,12 +597,16 @@ 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.Get().Info(this, $"Player {id} left scene"); + if (data.NewSceneHost) { + _entityManager.BecomeSceneHost(); + } + if (!_playerData.TryGetValue(id, out var playerData)) { Logger.Get().Warn(this, $"Could not find player data for player with ID {id}"); return; @@ -673,7 +677,8 @@ private void OnEntityUpdate(EntityUpdate entityUpdate) { /// Method for handling received entity updates. /// /// The entity update to handle. - private void HandleEntityUpdate(EntityUpdate entityUpdate) { + /// Whether this is the update from the already in scene packet. + private void HandleEntityUpdate(EntityUpdate entityUpdate, bool alreadyInSceneUpdate = false) { if (entityUpdate.UpdateTypes.Contains(EntityUpdateType.Position)) { _entityManager.UpdateEntityPosition(entityUpdate.Id, entityUpdate.Position); } @@ -683,7 +688,12 @@ private void HandleEntityUpdate(EntityUpdate entityUpdate) { } if (entityUpdate.UpdateTypes.Contains(EntityUpdateType.Animation)) { - _entityManager.UpdateEntityAnimation(entityUpdate.Id, entityUpdate.AnimationId); + _entityManager.UpdateEntityAnimation( + entityUpdate.Id, + entityUpdate.AnimationId, + entityUpdate.AnimationWrapMode, + alreadyInSceneUpdate + ); } if (entityUpdate.UpdateTypes.Contains(EntityUpdateType.Data)) { diff --git a/HKMP/Game/Client/Entity/Entity.cs b/HKMP/Game/Client/Entity/Entity.cs index deef9d7b..ef459541 100644 --- a/HKMP/Game/Client/Entity/Entity.cs +++ b/HKMP/Game/Client/Entity/Entity.cs @@ -109,9 +109,7 @@ private void OnUpdate() { ); } } - - // TODO: mark animations that loop differently to the server so it knows to repeat the animation for players - // that newly enter a scene + private void OnAnimationPlayed( On.tk2dSpriteAnimator.orig_Play_tk2dSpriteAnimationClip_float_float orig, tk2dSpriteAnimator self, @@ -138,7 +136,7 @@ float overrideFps _netClient.UpdateManager.UpdateEntityAnimation( _entityId, animationId, - clip.wrapMode == tk2dSpriteAnimationClip.WrapMode.Loop + (byte) clip.wrapMode ); } @@ -183,6 +181,9 @@ public void InitializeHost() { // TODO: parameters should be all FSM details to kickstart all FSMs of the game object public void MakeHost() { + // TODO: read all variables from the parameters and set the FSM variables of all FSMs + + InitializeHost(); } public void UpdatePosition(Vector2 position) { @@ -205,7 +206,7 @@ public void UpdateScale(bool scale) { } } - public void UpdateAnimation(byte animationId) { + public void UpdateAnimation(byte animationId, tk2dSpriteAnimationClip.WrapMode wrapMode, bool alreadyInSceneUpdate) { if (_animator == null) { Logger.Get().Warn(this, $"Entity '{_gameObject.name}' received animation while animator does not exist"); @@ -216,8 +217,37 @@ public void UpdateAnimation(byte animationId) { Logger.Get().Warn(this, $"Entity '{_gameObject.name}' received unknown animation ID: {animationId}"); return; } + + Logger.Get().Info(this, $"Entity '{_gameObject.name}' received animation: {animationId}, {clipName}, {wrapMode}"); + + if (alreadyInSceneUpdate) { + // Since this is an animation update from an entity that was already present in a scene, + // we need to determine where to start playing this specific animation + if (wrapMode == tk2dSpriteAnimationClip.WrapMode.Loop) { + _animator.Play(clipName); + return; + } + + var clip = _animator.GetClipByName(clipName); + + if (wrapMode == tk2dSpriteAnimationClip.WrapMode.LoopSection) { + // The clip loops in a specific section in the frames, so we start playing + // it from the start of that section + _animator.PlayFromFrame(clipName, clip.loopStart); + return; + } - Logger.Get().Info(this, $"Entity '{_gameObject.name}' received animation: {animationId}, {clipName}"); + if (wrapMode == tk2dSpriteAnimationClip.WrapMode.Once || + wrapMode == tk2dSpriteAnimationClip.WrapMode.Single) { + // Since the clip was played once, it stops on the last frame, + // so we emulate that by only "playing" the last frame of the clip + var clipLength = clip.frames.Length; + _animator.PlayFromFrame(clipName, clipLength - 1); + return; + } + } + + // Otherwise, default to just playing the clip _animator.Play(clipName); } diff --git a/HKMP/Game/Client/Entity/EntityManager.cs b/HKMP/Game/Client/Entity/EntityManager.cs index f11c376d..f30695a8 100644 --- a/HKMP/Game/Client/Entity/EntityManager.cs +++ b/HKMP/Game/Client/Entity/EntityManager.cs @@ -7,8 +7,12 @@ namespace Hkmp.Game.Client.Entity { internal class EntityManager { - private readonly List _validEntityFsms = new() { - "Crawler" + private readonly Dictionary _validEntityFsms = new() { + { "Crawler", "Crawler" }, + { "chaser", "Buzzer" }, + { "Zombie Swipe", "Zombie Runner" }, + { "Bouncer Control", "Fly" }, + { "BG Control", "Battle Gate" } }; private readonly NetClient _netClient; @@ -78,7 +82,12 @@ public void UpdateEntityScale(byte entityId, bool scale) { entity.UpdateScale(scale); } - public void UpdateEntityAnimation(byte entityId, byte animationId) { + public void UpdateEntityAnimation( + byte entityId, + byte animationId, + byte animationWrapMode, + bool alreadyInSceneUpdate + ) { if (_isSceneHost) { return; } @@ -87,7 +96,7 @@ public void UpdateEntityAnimation(byte entityId, byte animationId) { return; } - entity.UpdateAnimation(animationId); + entity.UpdateAnimation(animationId, (tk2dSpriteAnimationClip.WrapMode) animationWrapMode, alreadyInSceneUpdate); } public void UpdateEntityData(byte entityId, List data) { @@ -101,8 +110,6 @@ public void UpdateEntityData(byte entityId, List data) { entity.UpdateData(data); } - - // TODO: methods for transferring scene host to this client private void OnSceneChanged(Scene oldScene, Scene newScene) { Logger.Get().Info(this, "Clearing all registered entities"); @@ -123,17 +130,24 @@ private void OnSceneChanged(Scene oldScene, Scene newScene) { foreach (var fsm in Object.FindObjectsOfType()) { // Logger.Get().Info(this, $"Found FSM: {fsm.Fsm.Name}, {fsm.gameObject.name}"); - if (_validEntityFsms.Contains(fsm.Fsm.Name)) { - Logger.Get().Info(this, $"Registering entity '{fsm.gameObject.name}' with ID '{_lastId}'"); - - _entities[_lastId] = new Entity( - _netClient, - _lastId, - fsm.gameObject - ); + if (!_validEntityFsms.TryGetValue(fsm.Fsm.Name, out var objectName)) { + continue; + } - _lastId++; + var fsmGameObjectName = fsm.gameObject.name; + if (!fsmGameObjectName.Contains(objectName)) { + continue; } + + Logger.Get().Info(this, $"Registering entity '{fsmGameObjectName}' with ID '{_lastId}'"); + + _entities[_lastId] = new Entity( + _netClient, + _lastId, + fsm.gameObject + ); + + _lastId++; } // Find all Climber components diff --git a/HKMP/Game/Server/ServerEntityData.cs b/HKMP/Game/Server/ServerEntityData.cs index 21482462..6770fc9e 100644 --- a/HKMP/Game/Server/ServerEntityData.cs +++ b/HKMP/Game/Server/ServerEntityData.cs @@ -17,9 +17,13 @@ internal class ServerEntityData { /// public bool Scale { get; set; } /// - /// The ID of the last played looped animation. + /// The ID of the last played animation. /// public byte? AnimationId { get; set; } + /// + /// The wrap mode of the last played animation. + /// + public byte AnimationWrapMode { get; set; } /// /// Generic data associated with this entity. diff --git a/HKMP/Game/Server/ServerManager.cs b/HKMP/Game/Server/ServerManager.cs index 483586b5..0b18bdb4 100644 --- a/HKMP/Game/Server/ServerManager.cs +++ b/HKMP/Game/Server/ServerManager.cs @@ -369,10 +369,10 @@ private void OnClientEnterScene(ServerPlayerData playerData) { entityUpdate.UpdateTypes.Add(EntityUpdateType.Data); if (entityData.AnimationId.HasValue) { - Logger.Get().Info(this, " Entity has looping animation"); - entityUpdate.UpdateTypes.Add(EntityUpdateType.Animation); + entityUpdate.AnimationId = entityData.AnimationId.Value; + entityUpdate.AnimationWrapMode = entityData.AnimationWrapMode; } entityUpdateList.Add(entityUpdate); @@ -529,16 +529,14 @@ private void OnEntityUpdate(ushort id, EntityUpdate entityUpdate) { otherId => { _netServer.GetUpdateManagerForClient(otherId)?.UpdateEntityAnimation( entityUpdate.Id, - entityUpdate.AnimationId + entityUpdate.AnimationId, + entityUpdate.AnimationWrapMode ); } ); - entityData.AnimationId = entityUpdate.AnimationLoops ? entityUpdate.AnimationId : null; - - if (entityUpdate.AnimationLoops) { - Logger.Get().Info(this, $"Storing looped animation: {entityUpdate.AnimationId}"); - } + entityData.AnimationId = entityUpdate.AnimationId; + entityData.AnimationWrapMode = entityUpdate.AnimationWrapMode; } if (entityUpdate.UpdateTypes.Contains(EntityUpdateType.Data)) { diff --git a/HKMP/Networking/Client/ClientUpdateManager.cs b/HKMP/Networking/Client/ClientUpdateManager.cs index b53d311d..adc050b5 100644 --- a/HKMP/Networking/Client/ClientUpdateManager.cs +++ b/HKMP/Networking/Client/ClientUpdateManager.cs @@ -200,14 +200,14 @@ public void UpdateEntityScale(byte entityId, bool scale) { /// /// The ID of the entity. /// The new animation ID of the entity. - /// Whether the animation of the entity loops. - public void UpdateEntityAnimation(byte entityId, byte animationId, bool animationLoops) { + /// The wrap mode of the animation of the entity. + public void UpdateEntityAnimation(byte entityId, byte animationId, byte animationWrapMode) { lock (Lock) { var entityUpdate = FindOrCreateEntityUpdate(entityId); entityUpdate.UpdateTypes.Add(EntityUpdateType.Animation); entityUpdate.AnimationId = animationId; - entityUpdate.AnimationLoops = animationLoops; + entityUpdate.AnimationWrapMode = animationWrapMode; } } diff --git a/HKMP/Networking/Packet/Data/EntityUpdate.cs b/HKMP/Networking/Packet/Data/EntityUpdate.cs index 440e220e..9769fa01 100644 --- a/HKMP/Networking/Packet/Data/EntityUpdate.cs +++ b/HKMP/Networking/Packet/Data/EntityUpdate.cs @@ -38,9 +38,9 @@ internal class EntityUpdate : IPacketData { /// public byte AnimationId { get; set; } /// - /// Whether the animation of the entity loops. + /// The wrap mode of the animation. /// - public bool AnimationLoops { get; set; } + public byte AnimationWrapMode { get; set; } public List GenericData { get; init; } @@ -49,7 +49,6 @@ internal class EntityUpdate : IPacketData { /// public EntityUpdate() { UpdateTypes = new HashSet(); - AnimationLoops = false; GenericData = new List(); } @@ -86,7 +85,7 @@ public void WriteData(IPacket packet) { if (UpdateTypes.Contains(EntityUpdateType.Animation)) { packet.Write(AnimationId); - packet.Write(AnimationLoops); + packet.Write(AnimationWrapMode); } if (UpdateTypes.Contains(EntityUpdateType.Data)) { @@ -133,7 +132,7 @@ public void ReadData(IPacket packet) { if (UpdateTypes.Contains(EntityUpdateType.Animation)) { AnimationId = packet.ReadByte(); - AnimationLoops = packet.ReadBool(); + AnimationWrapMode = packet.ReadByte(); } if (UpdateTypes.Contains(EntityUpdateType.Data)) { diff --git a/HKMP/Networking/Server/ServerUpdateManager.cs b/HKMP/Networking/Server/ServerUpdateManager.cs index 5b45db48..5b952025 100644 --- a/HKMP/Networking/Server/ServerUpdateManager.cs +++ b/HKMP/Networking/Server/ServerUpdateManager.cs @@ -333,12 +333,14 @@ public void UpdateEntityScale(byte entityId, bool scale) { /// /// The ID of the entity. /// The animation ID of the entity. - public void UpdateEntityAnimation(byte entityId, byte animationId) { + /// The wrap mode of the animation of the entity. + public void UpdateEntityAnimation(byte entityId, byte animationId, byte animationWrapMode) { lock (Lock) { var entityUpdate = FindOrCreateEntityUpdate(entityId); entityUpdate.UpdateTypes.Add(EntityUpdateType.Animation); entityUpdate.AnimationId = animationId; + entityUpdate.AnimationWrapMode = animationWrapMode; } } From a72813e4773558022e9674410fb4aa4f9b66231e Mon Sep 17 00:00:00 2001 From: Extremelyd1 Date: Mon, 25 Jul 2022 22:37:32 +0200 Subject: [PATCH 004/216] Entity components and FSM action handling --- .../Client/Entity/Action/EntityFsmActions.cs | 91 +++++++ .../Entity/Action/HookedEntityAction.cs | 13 + .../Entity/Component/EntityComponent.cs | 36 +++ .../Entity/Component/RotationComponent.cs | 72 ++++++ HKMP/Game/Client/Entity/Entity.cs | 224 ++++++++++++------ HKMP/Game/Client/Entity/EntityManager.cs | 3 +- HKMP/Game/Server/ServerManager.cs | 4 +- HKMP/HKMP.csproj | 4 + HKMP/Networking/Packet/Data/EntityUpdate.cs | 7 +- 9 files changed, 382 insertions(+), 72 deletions(-) create mode 100644 HKMP/Game/Client/Entity/Action/EntityFsmActions.cs create mode 100644 HKMP/Game/Client/Entity/Action/HookedEntityAction.cs create mode 100644 HKMP/Game/Client/Entity/Component/EntityComponent.cs create mode 100644 HKMP/Game/Client/Entity/Component/RotationComponent.cs diff --git a/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs b/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs new file mode 100644 index 00000000..80246b0d --- /dev/null +++ b/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs @@ -0,0 +1,91 @@ +using System; +using System.Collections.Generic; +using Hkmp.Networking.Packet; +using Hkmp.Networking.Packet.Data; +using HutongGames.PlayMaker; +using HutongGames.PlayMaker.Actions; +using UnityEngine; + +namespace Hkmp.Game.Client.Entity.Action; + +internal static class EntityFsmActions { + private const string LogObjectName = "Hkmp.Game.Client.Entity.Action.EntityFsmActions"; + + public static readonly HashSet SupportedActionTypes = new() { + typeof(SpawnObjectFromGlobalPool) + }; + + public static void GetNetworkDataFromAction(EntityNetworkData data, FsmStateAction action) { + if (action is SpawnObjectFromGlobalPool spawnObjectFromGlobalPool) { + GetNetworkDataFromAction(data, spawnObjectFromGlobalPool); + } + + throw new InvalidOperationException($"Given action type: {action.GetType()} does not have an associated method to get"); + } + + public static void ApplyNetworkDataFromAction(EntityNetworkData data, FsmStateAction action) { + if (action is SpawnObjectFromGlobalPool spawnObjectFromGlobalPool) { + ApplyNetworkDataFromAction(data, spawnObjectFromGlobalPool); + } + + throw new InvalidOperationException($"Given action type: {action.GetType()} does not have an associated method to apply"); + } + + #region SpawnObjectFromGlobalPool + + private static void GetNetworkDataFromAction(EntityNetworkData data, SpawnObjectFromGlobalPool action) { + var spawnPoint = action.spawnPoint; + if (spawnPoint == null) { + data.Data.Add(0); + return; + } + + data.Data.Add(1); + + var position = spawnPoint.Value.transform.position; + data.Data.AddRange(BitConverter.GetBytes(position.x)); + data.Data.AddRange(BitConverter.GetBytes(position.y)); + data.Data.AddRange(BitConverter.GetBytes(position.z)); + + Logger.Get().Info(LogObjectName, $"Added entity network data: {position.x}, {position.y}, {position.z}"); + } + + private static void ApplyNetworkDataFromAction(EntityNetworkData data, SpawnObjectFromGlobalPool action) { + var packet = new Packet(data.Data.ToArray()); + + var position = Vector3.zero; + var euler = Vector3.up; + + var hasSpawnPoint = packet.ReadBool(); + if (hasSpawnPoint) { + var posX = packet.ReadFloat(); + var posY = packet.ReadFloat(); + var posZ = packet.ReadFloat(); + + Logger.Get().Info(LogObjectName, $"Applying entity network data: {posX}, {posY}, {posZ}"); + + position = new Vector3(posX, posY, posZ); + + if (!action.position.IsNone) { + position += action.position.Value; + } + + euler = !action.rotation.IsNone ? action.rotation.Value : action.spawnPoint.Value.transform.eulerAngles; + } else { + Logger.Get().Info(LogObjectName, $"Applying entity network data without spawnpoint"); + if (!action.position.IsNone) { + position = action.position.Value; + } + + if (!action.rotation.IsNone) { + euler = action.rotation.Value; + } + } + + if (action.gameObject != null) { + action.storeObject.Value = action.gameObject.Value.Spawn(position, Quaternion.Euler(euler)); + } + } + + #endregion +} \ No newline at end of file diff --git a/HKMP/Game/Client/Entity/Action/HookedEntityAction.cs b/HKMP/Game/Client/Entity/Action/HookedEntityAction.cs new file mode 100644 index 00000000..893422e0 --- /dev/null +++ b/HKMP/Game/Client/Entity/Action/HookedEntityAction.cs @@ -0,0 +1,13 @@ +using HutongGames.PlayMaker; + +namespace Hkmp.Game.Client.Entity.Action; + +internal class HookedEntityAction { + public FsmStateAction Action { get; set; } + + public int FsmIndex { get; set; } + + public int StateIndex { get; set; } + + public int ActionIndex { get; set; } +} \ No newline at end of file diff --git a/HKMP/Game/Client/Entity/Component/EntityComponent.cs b/HKMP/Game/Client/Entity/Component/EntityComponent.cs new file mode 100644 index 00000000..5e469ec5 --- /dev/null +++ b/HKMP/Game/Client/Entity/Component/EntityComponent.cs @@ -0,0 +1,36 @@ +using Hkmp.Networking.Client; +using Hkmp.Networking.Packet.Data; +using UnityEngine; + +namespace Hkmp.Game.Client.Entity.Component; + +internal abstract class EntityComponent { + private readonly NetClient _netClient; + private readonly byte _entityId; + + protected readonly GameObject HostObject; + protected readonly GameObject ClientObject; + + public bool IsControlled { get; set; } + + protected EntityComponent( + NetClient netClient, + byte entityId, + GameObject hostObject, + GameObject clientObject + ) { + _netClient = netClient; + _entityId = entityId; + + HostObject = hostObject; + ClientObject = clientObject; + } + + protected void SendData(EntityNetworkData data) { + _netClient.UpdateManager.AddEntityData(_entityId, data); + } + + public abstract void InitializeHost(); + public abstract void Update(EntityNetworkData data); + public abstract void Destroy(); +} \ No newline at end of file diff --git a/HKMP/Game/Client/Entity/Component/RotationComponent.cs b/HKMP/Game/Client/Entity/Component/RotationComponent.cs new file mode 100644 index 00000000..1f0b45dd --- /dev/null +++ b/HKMP/Game/Client/Entity/Component/RotationComponent.cs @@ -0,0 +1,72 @@ +using System; +using Hkmp.Networking.Client; +using Hkmp.Networking.Packet.Data; +using Hkmp.Util; +using UnityEngine; + +namespace Hkmp.Game.Client.Entity.Component; + +internal class RotationComponent : EntityComponent { + private readonly Climber _climber; + + private Vector3 _lastRotation; + + public RotationComponent( + NetClient netClient, + byte entityId, + GameObject hostObject, + GameObject clientObject, + Climber climber + ) : base(netClient, entityId, hostObject, clientObject) { + _climber = climber; + _climber.enabled = false; + + MonoBehaviourUtil.Instance.OnUpdateEvent += OnUpdateRotation; + } + + private void OnUpdateRotation() { + if (IsControlled) { + return; + } + + if (HostObject == null) { + return; + } + + var transform = HostObject.transform; + + var newRotation = transform.rotation.eulerAngles; + if (newRotation != _lastRotation) { + _lastRotation = newRotation; + + var data = new EntityNetworkData { + Type = EntityNetworkData.DataType.Rotation + }; + data.Data.AddRange(BitConverter.GetBytes(newRotation.z)); + + SendData(data); + } + } + + public override void InitializeHost() { + if (_climber != null) { + _climber.enabled = true; + } + } + + public override void Update(EntityNetworkData data) { + var rotation = BitConverter.ToSingle(data.Data.ToArray(), 0); + + var transform = ClientObject.transform; + var eulerAngles = transform.eulerAngles; + transform.eulerAngles = new Vector3( + eulerAngles.x, + eulerAngles.y, + rotation + ); + } + + public override void Destroy() { + MonoBehaviourUtil.Instance.OnUpdateEvent -= OnUpdateRotation; + } +} \ No newline at end of file diff --git a/HKMP/Game/Client/Entity/Entity.cs b/HKMP/Game/Client/Entity/Entity.cs index ef459541..2bbc992a 100644 --- a/HKMP/Game/Client/Entity/Entity.cs +++ b/HKMP/Game/Client/Entity/Entity.cs @@ -1,10 +1,13 @@ -using System; using System.Collections.Generic; +using System.Linq; using Hkmp.Collection; using Hkmp.Fsm; +using Hkmp.Game.Client.Entity.Action; +using Hkmp.Game.Client.Entity.Component; using Hkmp.Networking.Client; using Hkmp.Networking.Packet.Data; using Hkmp.Util; +using HutongGames.PlayMaker; using UnityEngine; using Vector2 = Hkmp.Math.Vector2; @@ -13,22 +16,23 @@ internal class Entity { private readonly NetClient _netClient; private readonly byte _entityId; - private readonly GameObject _gameObject; + private readonly GameObject _hostObject; + private readonly GameObject _clientObject; private readonly tk2dSpriteAnimator _animator; private readonly BiLookup _animationClipNameIds; - private readonly PlayMakerFSM[] _fsms; + private readonly List _fsms; - private readonly Climber _climber; + private readonly Dictionary _components; + + private readonly Dictionary _hookedActions; private bool _isControlled; private Vector3 _lastPosition; private Vector3 _lastScale; - private Vector3 _lastRotation; - public Entity( NetClient netClient, byte entityId, @@ -36,17 +40,24 @@ GameObject gameObject ) { _netClient = netClient; _entityId = entityId; - _gameObject = gameObject; + _hostObject = gameObject; _isControlled = true; + _clientObject = Object.Instantiate( + _hostObject, + _hostObject.transform.position, + _hostObject.transform.rotation + ); + _clientObject.SetActive(false); + // Add a position interpolation component to the enemy so we can smooth out position updates - _gameObject.AddComponent(); + _clientObject.AddComponent(); // Register an update event to send position updates MonoBehaviourUtil.Instance.OnUpdateEvent += OnUpdate; - _animator = _gameObject.GetComponent(); + _animator = _hostObject.GetComponent(); if (_animator != null) { _animationClipNameIds = new BiLookup(); @@ -56,7 +67,7 @@ GameObject gameObject if (index > byte.MaxValue) { Logger.Get().Error(this, - $"Too many animation clips to fit in a byte for entity: {_gameObject.name}"); + $"Too many animation clips to fit in a byte for entity: {_clientObject.name}"); break; } } @@ -64,17 +75,85 @@ GameObject gameObject On.tk2dSpriteAnimator.Play_tk2dSpriteAnimationClip_float_float += OnAnimationPlayed; } - _climber = _gameObject.GetComponent(); - if (_climber != null) { - MonoBehaviourUtil.Instance.OnUpdateEvent += OnUpdateRotation; + _fsms = _hostObject.GetComponents().ToList(); + + _hookedActions = new Dictionary(); + foreach (var fsm in _fsms) { + ProcessFsm(fsm); - _climber.enabled = false; + Logger.Get().Info(this, $"Disabling FSM: {fsm.Fsm.Name}"); + fsm.enabled = false; } + On.HutongGames.PlayMaker.FsmStateAction.OnEnter += OnActionEntered; + + _components = new Dictionary(); + FindComponents(); + } - _fsms = _gameObject.GetComponents(); - foreach (var fsm in _fsms) { - fsm.enabled = false; + private void ProcessFsm(PlayMakerFSM fsm) { + Logger.Get().Info(this, $"Processing FSM: {fsm.Fsm.Name}"); + + for (var i = 0; i < fsm.FsmStates.Length; i++) { + var state = fsm.FsmStates[i]; + + for (var j = 0; j < state.Actions.Length; j++) { + var action = state.Actions[j]; + + if (!EntityFsmActions.SupportedActionTypes.Contains(action.GetType())) { + continue; + } + + _hookedActions[action] = new HookedEntityAction { + Action = action, + FsmIndex = _fsms.IndexOf(fsm), + StateIndex = i, + ActionIndex = j + }; + Logger.Get().Info(this, $"Created hooked action: {action.GetType()}, {_fsms.IndexOf(fsm)}, {i}, {j}"); + } + } + } + + private void FindComponents() { + var climber = _clientObject.GetComponent(); + if (climber != null) { + _components[EntityNetworkData.DataType.Rotation] = new RotationComponent( + _netClient, + _entityId, + _hostObject, + _clientObject, + climber + ); + } + } + + private void OnActionEntered(On.HutongGames.PlayMaker.FsmStateAction.orig_OnEnter orig, FsmStateAction self) { + orig(self); + + if (_isControlled) { + return; + } + + if (!_hookedActions.TryGetValue(self, out var hookedEntityAction)) { + return; + } + + Logger.Get().Info(this, $"Hooked action was entered: {hookedEntityAction.FsmIndex}, {hookedEntityAction.StateIndex}, {hookedEntityAction.ActionIndex}"); + + var networkData = new EntityNetworkData { + Type = EntityNetworkData.DataType.Fsm + }; + + if (_fsms.Count > 1) { + networkData.Data.Add((byte)hookedEntityAction.FsmIndex); } + + networkData.Data.Add((byte) hookedEntityAction.StateIndex); + networkData.Data.Add((byte) hookedEntityAction.ActionIndex); + + EntityFsmActions.GetNetworkDataFromAction(networkData, self); + + _netClient.UpdateManager.AddEntityData(_entityId, networkData); } private void OnUpdate() { @@ -83,11 +162,11 @@ private void OnUpdate() { return; } - if (_gameObject == null) { + if (_clientObject == null) { return; } - var transform = _gameObject.transform; + var transform = _clientObject.transform; var newPosition = transform.position; if (newPosition != _lastPosition) { @@ -128,11 +207,11 @@ float overrideFps } if (!_animationClipNameIds.TryGetValue(clip.name, out var animationId)) { - Logger.Get().Warn(this, $"Entity '{_gameObject.name}' played unknown animation: {clip.name}"); + Logger.Get().Warn(this, $"Entity '{_clientObject.name}' played unknown animation: {clip.name}"); return; } - Logger.Get().Info(this, $"Entity '{_gameObject.name}' sends animation: {clip.name}, {animationId}, {clip.wrapMode}"); + // Logger.Get().Info(this, $"Entity '{_gameObject.name}' sends animation: {clip.name}, {animationId}, {clip.wrapMode}"); _netClient.UpdateManager.UpdateEntityAnimation( _entityId, animationId, @@ -140,43 +219,17 @@ float overrideFps ); } - private void OnUpdateRotation() { - if (_isControlled) { - return; - } - - if (_gameObject == null) { - return; - } - - var transform = _gameObject.transform; - - var newRotation = transform.rotation.eulerAngles; - if (newRotation != _lastRotation) { - _lastRotation = newRotation; - - var data = new EntityNetworkData { - Type = EntityNetworkData.DataType.Rotation - }; - data.Data.AddRange(BitConverter.GetBytes(newRotation.z)); - - _netClient.UpdateManager.AddEntityData( - _entityId, - data - ); - } - } - public void InitializeHost() { - if (_climber != null) { - _climber.enabled = true; - } - foreach (var fsm in _fsms) { + Logger.Get().Info(this, $"Enabling FSM: {fsm.Fsm.Name}"); fsm.enabled = true; } _isControlled = false; + + foreach (var component in _components.Values) { + component.IsControlled = false; + } } // TODO: parameters should be all FSM details to kickstart all FSMs of the game object @@ -189,11 +242,11 @@ public void MakeHost() { public void UpdatePosition(Vector2 position) { var unityPos = new Vector3(position.X, position.Y); - _gameObject.GetComponent().SetNewPosition(unityPos); + _clientObject.GetComponent().SetNewPosition(unityPos); } public void UpdateScale(bool scale) { - var transform = _gameObject.transform; + var transform = _clientObject.transform; var localScale = transform.localScale; var currentScaleX = localScale.x; @@ -209,16 +262,16 @@ public void UpdateScale(bool scale) { public void UpdateAnimation(byte animationId, tk2dSpriteAnimationClip.WrapMode wrapMode, bool alreadyInSceneUpdate) { if (_animator == null) { Logger.Get().Warn(this, - $"Entity '{_gameObject.name}' received animation while animator does not exist"); + $"Entity '{_clientObject.name}' received animation while animator does not exist"); return; } if (!_animationClipNameIds.TryGetValue(animationId, out var clipName)) { - Logger.Get().Warn(this, $"Entity '{_gameObject.name}' received unknown animation ID: {animationId}"); + Logger.Get().Warn(this, $"Entity '{_clientObject.name}' received unknown animation ID: {animationId}"); return; } - Logger.Get().Info(this, $"Entity '{_gameObject.name}' received animation: {animationId}, {clipName}, {wrapMode}"); + // Logger.Get().Info(this, $"Entity '{_gameObject.name}' received animation: {animationId}, {clipName}, {wrapMode}"); if (alreadyInSceneUpdate) { // Since this is an animation update from an entity that was already present in a scene, @@ -253,16 +306,50 @@ public void UpdateAnimation(byte animationId, tk2dSpriteAnimationClip.WrapMode w public void UpdateData(List entityNetworkData) { foreach (var data in entityNetworkData) { - if (data.Type == EntityNetworkData.DataType.Rotation) { - var rotation = BitConverter.ToSingle(data.Data.ToArray(), 0); + if (data.Type == EntityNetworkData.DataType.Fsm) { + PlayMakerFSM fsm; + byte stateIndex; + byte actionIndex; + + if (_fsms.Count > 1) { + // Do a check on the length of the data + if (data.Data.Count < 3) { + continue; + } + + var fsmIndex = data.Data[0]; + fsm = _fsms[fsmIndex]; + + stateIndex = data.Data[1]; + actionIndex = data.Data[2]; + + data.Data.RemoveRange(0, 3); + } else { + // Do a check on the length of the data + if (data.Data.Count < 2) { + continue; + } + + fsm = _fsms[0]; + + stateIndex = data.Data[0]; + actionIndex = data.Data[1]; + + data.Data.RemoveRange(0, 2); + } + + Logger.Get().Info(this, $"Received entity network data for FSM: {fsm.Fsm.Name}, {stateIndex}, {actionIndex}"); + + var state = fsm.FsmStates[stateIndex]; + var action = state.Actions[actionIndex]; + + EntityFsmActions.ApplyNetworkDataFromAction(data, action); - var transform = _gameObject.transform; - var eulerAngles = transform.eulerAngles; - transform.eulerAngles = new Vector3( - eulerAngles.x, - eulerAngles.y, - rotation - ); + continue; + } + + if (_components.TryGetValue(data.Type, out var component)) { + component.Update(data); } } } @@ -270,7 +357,10 @@ public void UpdateData(List entityNetworkData) { public void Destroy() { MonoBehaviourUtil.Instance.OnUpdateEvent -= OnUpdate; On.tk2dSpriteAnimator.Play_tk2dSpriteAnimationClip_float_float -= OnAnimationPlayed; - MonoBehaviourUtil.Instance.OnUpdateEvent -= OnUpdateRotation; + + foreach (var component in _components.Values) { + component.Destroy(); + } } } } \ No newline at end of file diff --git a/HKMP/Game/Client/Entity/EntityManager.cs b/HKMP/Game/Client/Entity/EntityManager.cs index f30695a8..e24fba75 100644 --- a/HKMP/Game/Client/Entity/EntityManager.cs +++ b/HKMP/Game/Client/Entity/EntityManager.cs @@ -12,7 +12,8 @@ internal class EntityManager { { "chaser", "Buzzer" }, { "Zombie Swipe", "Zombie Runner" }, { "Bouncer Control", "Fly" }, - { "BG Control", "Battle Gate" } + { "BG Control", "Battle Gate" }, + { "spitter", "Spitter" } }; private readonly NetClient _netClient; diff --git a/HKMP/Game/Server/ServerManager.cs b/HKMP/Game/Server/ServerManager.cs index 0b18bdb4..e2c95fc9 100644 --- a/HKMP/Game/Server/ServerManager.cs +++ b/HKMP/Game/Server/ServerManager.cs @@ -6,6 +6,7 @@ using Hkmp.Api.Server; using Hkmp.Concurrency; using Hkmp.Eventing; +using Hkmp.Game.Client.Entity; using Hkmp.Game.Command.Server; using Hkmp.Game.Server.Auth; using Hkmp.Networking.Packet; @@ -557,7 +558,8 @@ private void OnEntityUpdate(ushort id, EntityUpdate entityUpdate) { d => d.Type == EntityNetworkData.DataType.Rotation ); if (existingData != null) { - existingData.Data = updateData.Data; + existingData.Data.Clear(); + existingData.Data.AddRange(updateData.Data); } } } diff --git a/HKMP/HKMP.csproj b/HKMP/HKMP.csproj index a109fd9d..80b63196 100644 --- a/HKMP/HKMP.csproj +++ b/HKMP/HKMP.csproj @@ -30,6 +30,10 @@ $(References)\MMHOOK_Assembly-CSharp.dll False + + $(References)\MMHOOK_PlayMaker.dll + False + ..\HKMPServer\Lib\Newtonsoft.Json.dll False diff --git a/HKMP/Networking/Packet/Data/EntityUpdate.cs b/HKMP/Networking/Packet/Data/EntityUpdate.cs index 9769fa01..9a25c445 100644 --- a/HKMP/Networking/Packet/Data/EntityUpdate.cs +++ b/HKMP/Networking/Packet/Data/EntityUpdate.cs @@ -150,7 +150,7 @@ public void ReadData(IPacket packet) { internal class EntityNetworkData { public DataType Type { get; set; } - public List Data { get; set; } + public List Data { get; } public EntityNetworkData() { Data = new List(); @@ -180,9 +180,10 @@ public void ReadData(IPacket packet) { Data.Add(packet.ReadByte()); } } - + public enum DataType : byte { - Rotation = 0, + Fsm = 0, + Rotation } } From 8ca6d6d6027785e49c94ef3c235b3842363f0065 Mon Sep 17 00:00:00 2001 From: Extremelyd1 Date: Sat, 30 Jul 2022 15:41:35 +0200 Subject: [PATCH 005/216] Network entity object activity --- HKMP/Game/Client/ClientManager.cs | 4 + HKMP/Game/Client/Entity/Entity.cs | 95 ++++++++++++------- HKMP/Game/Client/Entity/EntityManager.cs | 12 +++ HKMP/Game/Server/ServerEntityData.cs | 4 + HKMP/Game/Server/ServerManager.cs | 15 +++ HKMP/Networking/Client/ClientUpdateManager.cs | 14 +++ HKMP/Networking/Packet/Data/EntityUpdate.cs | 14 +++ HKMP/Networking/Server/ServerUpdateManager.cs | 14 +++ 8 files changed, 140 insertions(+), 32 deletions(-) diff --git a/HKMP/Game/Client/ClientManager.cs b/HKMP/Game/Client/ClientManager.cs index fe87c73b..6ed2a767 100644 --- a/HKMP/Game/Client/ClientManager.cs +++ b/HKMP/Game/Client/ClientManager.cs @@ -696,6 +696,10 @@ private void HandleEntityUpdate(EntityUpdate entityUpdate, bool alreadyInSceneUp ); } + if (entityUpdate.UpdateTypes.Contains(EntityUpdateType.Active)) { + _entityManager.UpdateEntityIsActive(entityUpdate.Id, entityUpdate.IsActive); + } + if (entityUpdate.UpdateTypes.Contains(EntityUpdateType.Data)) { _entityManager.UpdateEntityData(entityUpdate.Id, entityUpdate.GenericData); } diff --git a/HKMP/Game/Client/Entity/Entity.cs b/HKMP/Game/Client/Entity/Entity.cs index 2bbc992a..d05266a2 100644 --- a/HKMP/Game/Client/Entity/Entity.cs +++ b/HKMP/Game/Client/Entity/Entity.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Linq; using Hkmp.Collection; @@ -9,6 +10,7 @@ using Hkmp.Util; using HutongGames.PlayMaker; using UnityEngine; +using Object = UnityEngine.Object; using Vector2 = Hkmp.Math.Vector2; namespace Hkmp.Game.Client.Entity { @@ -19,15 +21,20 @@ internal class Entity { private readonly GameObject _hostObject; private readonly GameObject _clientObject; - private readonly tk2dSpriteAnimator _animator; + private readonly tk2dSpriteAnimator _hostAnimator; + private readonly tk2dSpriteAnimator _clientAnimator; + private readonly BiLookup _animationClipNameIds; - private readonly List _fsms; + private readonly List _hostFsms; + private readonly List _clientFsms; private readonly Dictionary _components; private readonly Dictionary _hookedActions; + private bool _originalIsActive; + private bool _isControlled; private Vector3 _lastPosition; @@ -51,18 +58,24 @@ GameObject gameObject ); _clientObject.SetActive(false); + // Store whether the host object was active and set it not active until we know + // if we are scene host + _originalIsActive = _hostObject.activeSelf; + _hostObject.SetActive(false); + // Add a position interpolation component to the enemy so we can smooth out position updates _clientObject.AddComponent(); // Register an update event to send position updates MonoBehaviourUtil.Instance.OnUpdateEvent += OnUpdate; - _animator = _hostObject.GetComponent(); - if (_animator != null) { + _hostAnimator = _hostObject.GetComponent(); + _clientAnimator = _clientObject.GetComponent(); + if (_hostAnimator != null) { _animationClipNameIds = new BiLookup(); var index = 0; - foreach (var animationClip in _animator.Library.clips) { + foreach (var animationClip in _hostAnimator.Library.clips) { _animationClipNameIds.Add(animationClip.name, (byte)index++); if (index > byte.MaxValue) { @@ -75,23 +88,25 @@ GameObject gameObject On.tk2dSpriteAnimator.Play_tk2dSpriteAnimationClip_float_float += OnAnimationPlayed; } - _fsms = _hostObject.GetComponents().ToList(); + _hostFsms = _hostObject.GetComponents().ToList(); _hookedActions = new Dictionary(); - foreach (var fsm in _fsms) { - ProcessFsm(fsm); - - Logger.Get().Info(this, $"Disabling FSM: {fsm.Fsm.Name}"); - fsm.enabled = false; + foreach (var fsm in _hostFsms) { + ProcessHostFsm(fsm); } On.HutongGames.PlayMaker.FsmStateAction.OnEnter += OnActionEntered; + + _clientFsms = _clientObject.GetComponents().ToList(); + foreach (var fsm in _clientFsms) { + ProcessClientFsm(fsm); + } _components = new Dictionary(); FindComponents(); } - private void ProcessFsm(PlayMakerFSM fsm) { - Logger.Get().Info(this, $"Processing FSM: {fsm.Fsm.Name}"); + private void ProcessHostFsm(PlayMakerFSM fsm) { + Logger.Get().Info(this, $"Processing host FSM: {fsm.Fsm.Name}"); for (var i = 0; i < fsm.FsmStates.Length; i++) { var state = fsm.FsmStates[i]; @@ -105,15 +120,27 @@ private void ProcessFsm(PlayMakerFSM fsm) { _hookedActions[action] = new HookedEntityAction { Action = action, - FsmIndex = _fsms.IndexOf(fsm), + FsmIndex = _hostFsms.IndexOf(fsm), StateIndex = i, ActionIndex = j }; - Logger.Get().Info(this, $"Created hooked action: {action.GetType()}, {_fsms.IndexOf(fsm)}, {i}, {j}"); + Logger.Get().Info(this, $"Created hooked action: {action.GetType()}, {_hostFsms.IndexOf(fsm)}, {i}, {j}"); } } } + private void ProcessClientFsm(PlayMakerFSM fsm) { + Logger.Get().Info(this, $"Processing client FSM: {fsm.Fsm.Name}"); + + // Set the transition array of each state to an empty array + foreach (var state in fsm.FsmStates) { + state.Transitions = Array.Empty(); + } + + // Set the transition array for the global transitions to an empty array + fsm.Fsm.GlobalTransitions = Array.Empty(); + } + private void FindComponents() { var climber = _clientObject.GetComponent(); if (climber != null) { @@ -144,7 +171,7 @@ private void OnActionEntered(On.HutongGames.PlayMaker.FsmStateAction.orig_OnEnte Type = EntityNetworkData.DataType.Fsm }; - if (_fsms.Count > 1) { + if (_hostFsms.Count > 1) { networkData.Data.Add((byte)hookedEntityAction.FsmIndex); } @@ -198,7 +225,7 @@ float overrideFps ) { orig(self, clip, clipStartTime, overrideFps); - if (self != _animator) { + if (self != _hostAnimator) { return; } @@ -220,11 +247,10 @@ float overrideFps } public void InitializeHost() { - foreach (var fsm in _fsms) { - Logger.Get().Info(this, $"Enabling FSM: {fsm.Fsm.Name}"); - fsm.enabled = true; - } - + _hostObject.SetActive(_originalIsActive); + + _netClient.UpdateManager.UpdateEntityIsActive(_entityId, _originalIsActive); + _isControlled = false; foreach (var component in _components.Values) { @@ -260,9 +286,9 @@ public void UpdateScale(bool scale) { } public void UpdateAnimation(byte animationId, tk2dSpriteAnimationClip.WrapMode wrapMode, bool alreadyInSceneUpdate) { - if (_animator == null) { + if (_clientAnimator == null) { Logger.Get().Warn(this, - $"Entity '{_clientObject.name}' received animation while animator does not exist"); + $"Entity '{_clientObject.name}' received animation while client animator does not exist"); return; } @@ -277,16 +303,16 @@ public void UpdateAnimation(byte animationId, tk2dSpriteAnimationClip.WrapMode w // Since this is an animation update from an entity that was already present in a scene, // we need to determine where to start playing this specific animation if (wrapMode == tk2dSpriteAnimationClip.WrapMode.Loop) { - _animator.Play(clipName); + _clientAnimator.Play(clipName); return; } - var clip = _animator.GetClipByName(clipName); + var clip = _clientAnimator.GetClipByName(clipName); if (wrapMode == tk2dSpriteAnimationClip.WrapMode.LoopSection) { // The clip loops in a specific section in the frames, so we start playing // it from the start of that section - _animator.PlayFromFrame(clipName, clip.loopStart); + _clientAnimator.PlayFromFrame(clipName, clip.loopStart); return; } @@ -295,13 +321,18 @@ public void UpdateAnimation(byte animationId, tk2dSpriteAnimationClip.WrapMode w // Since the clip was played once, it stops on the last frame, // so we emulate that by only "playing" the last frame of the clip var clipLength = clip.frames.Length; - _animator.PlayFromFrame(clipName, clipLength - 1); + _clientAnimator.PlayFromFrame(clipName, clipLength - 1); return; } } // Otherwise, default to just playing the clip - _animator.Play(clipName); + _clientAnimator.Play(clipName); + } + + public void UpdateIsActive(bool active) { + Logger.Get().Info(this, $"Entity '{_clientObject.name}' received active: {active}"); + _clientObject.SetActive(active); } public void UpdateData(List entityNetworkData) { @@ -311,14 +342,14 @@ public void UpdateData(List entityNetworkData) { byte stateIndex; byte actionIndex; - if (_fsms.Count > 1) { + if (_clientFsms.Count > 1) { // Do a check on the length of the data if (data.Data.Count < 3) { continue; } var fsmIndex = data.Data[0]; - fsm = _fsms[fsmIndex]; + fsm = _clientFsms[fsmIndex]; stateIndex = data.Data[1]; actionIndex = data.Data[2]; @@ -330,7 +361,7 @@ public void UpdateData(List entityNetworkData) { continue; } - fsm = _fsms[0]; + fsm = _clientFsms[0]; stateIndex = data.Data[0]; actionIndex = data.Data[1]; diff --git a/HKMP/Game/Client/Entity/EntityManager.cs b/HKMP/Game/Client/Entity/EntityManager.cs index e24fba75..081b5dd6 100644 --- a/HKMP/Game/Client/Entity/EntityManager.cs +++ b/HKMP/Game/Client/Entity/EntityManager.cs @@ -100,6 +100,18 @@ bool alreadyInSceneUpdate entity.UpdateAnimation(animationId, (tk2dSpriteAnimationClip.WrapMode) animationWrapMode, alreadyInSceneUpdate); } + public void UpdateEntityIsActive(byte entityId, bool isActive) { + if (_isSceneHost) { + return; + } + + if (!_entities.TryGetValue(entityId, out var entity)) { + return; + } + + entity.UpdateIsActive(isActive); + } + public void UpdateEntityData(byte entityId, List data) { if (_isSceneHost) { return; diff --git a/HKMP/Game/Server/ServerEntityData.cs b/HKMP/Game/Server/ServerEntityData.cs index 6770fc9e..713201f0 100644 --- a/HKMP/Game/Server/ServerEntityData.cs +++ b/HKMP/Game/Server/ServerEntityData.cs @@ -24,6 +24,10 @@ internal class ServerEntityData { /// The wrap mode of the last played animation. /// public byte AnimationWrapMode { get; set; } + /// + /// Whether the entity is active. + /// + public bool IsActive { get; set; } /// /// Generic data associated with this entity. diff --git a/HKMP/Game/Server/ServerManager.cs b/HKMP/Game/Server/ServerManager.cs index e2c95fc9..8d4dc275 100644 --- a/HKMP/Game/Server/ServerManager.cs +++ b/HKMP/Game/Server/ServerManager.cs @@ -540,6 +540,21 @@ private void OnEntityUpdate(ushort id, EntityUpdate entityUpdate) { entityData.AnimationWrapMode = entityUpdate.AnimationWrapMode; } + if (entityUpdate.UpdateTypes.Contains(EntityUpdateType.Active)) { + SendDataInSameScene( + id, + playerData.CurrentScene, + otherId => { + _netServer.GetUpdateManagerForClient(otherId)?.UpdateEntityIsActive( + entityUpdate.Id, + entityUpdate.IsActive + ); + } + ); + + entityData.IsActive = entityUpdate.IsActive; + } + if (entityUpdate.UpdateTypes.Contains(EntityUpdateType.Data)) { SendDataInSameScene( id, diff --git a/HKMP/Networking/Client/ClientUpdateManager.cs b/HKMP/Networking/Client/ClientUpdateManager.cs index adc050b5..463c22d1 100644 --- a/HKMP/Networking/Client/ClientUpdateManager.cs +++ b/HKMP/Networking/Client/ClientUpdateManager.cs @@ -211,6 +211,20 @@ public void UpdateEntityAnimation(byte entityId, byte animationId, byte animatio } } + /// + /// Update whether an entity is active or not. + /// + /// The ID of the entity. + /// Whether the entity is active or not. + public void UpdateEntityIsActive(byte entityId, bool isActive) { + lock (Lock) { + var entityUpdate = FindOrCreateEntityUpdate(entityId); + + entityUpdate.UpdateTypes.Add(EntityUpdateType.Active); + entityUpdate.IsActive = isActive; + } + } + /// /// Add data to an entity's update in the current packet. /// diff --git a/HKMP/Networking/Packet/Data/EntityUpdate.cs b/HKMP/Networking/Packet/Data/EntityUpdate.cs index 9a25c445..cfda686c 100644 --- a/HKMP/Networking/Packet/Data/EntityUpdate.cs +++ b/HKMP/Networking/Packet/Data/EntityUpdate.cs @@ -42,6 +42,11 @@ internal class EntityUpdate : IPacketData { /// public byte AnimationWrapMode { get; set; } + /// + /// Whether the entity is active or not. + /// + public bool IsActive { get; set; } + public List GenericData { get; init; } /// @@ -88,6 +93,10 @@ public void WriteData(IPacket packet) { packet.Write(AnimationWrapMode); } + if (UpdateTypes.Contains(EntityUpdateType.Active)) { + packet.Write(IsActive); + } + if (UpdateTypes.Contains(EntityUpdateType.Data)) { if (GenericData.Count > byte.MaxValue) { Logger.Get().Error(this, "Length of entity network data instances exceeded max value of byte"); @@ -135,6 +144,10 @@ public void ReadData(IPacket packet) { AnimationWrapMode = packet.ReadByte(); } + if (UpdateTypes.Contains(EntityUpdateType.Active)) { + IsActive = packet.ReadBool(); + } + if (UpdateTypes.Contains(EntityUpdateType.Data)) { var length = packet.ReadByte(); @@ -194,6 +207,7 @@ internal enum EntityUpdateType { Position = 0, Scale, Animation, + Active, Data } } \ No newline at end of file diff --git a/HKMP/Networking/Server/ServerUpdateManager.cs b/HKMP/Networking/Server/ServerUpdateManager.cs index 5b952025..78025cb8 100644 --- a/HKMP/Networking/Server/ServerUpdateManager.cs +++ b/HKMP/Networking/Server/ServerUpdateManager.cs @@ -344,6 +344,20 @@ public void UpdateEntityAnimation(byte entityId, byte animationId, byte animatio } } + /// + /// Update whether an entity is active or not. + /// + /// The ID of the entity. + /// Whether the entity is active or not. + public void UpdateEntityIsActive(byte entityId, bool isActive) { + lock (Lock) { + var entityUpdate = FindOrCreateEntityUpdate(entityId); + + entityUpdate.UpdateTypes.Add(EntityUpdateType.Active); + entityUpdate.IsActive = isActive; + } + } + /// /// Add data to an entity's update in the current packet. /// From 97440d589830ea79204d0793ce67e0027fdad7c0 Mon Sep 17 00:00:00 2001 From: Extremelyd1 Date: Sun, 31 Jul 2022 20:06:11 +0200 Subject: [PATCH 006/216] Improvements to activity networking --- HKMP/Game/Client/Entity/Entity.cs | 97 +++++++++++++++++++++++++------ HKMP/Game/Server/ServerManager.cs | 2 + 2 files changed, 80 insertions(+), 19 deletions(-) diff --git a/HKMP/Game/Client/Entity/Entity.cs b/HKMP/Game/Client/Entity/Entity.cs index d05266a2..e565528f 100644 --- a/HKMP/Game/Client/Entity/Entity.cs +++ b/HKMP/Game/Client/Entity/Entity.cs @@ -1,4 +1,3 @@ -using System; using System.Collections.Generic; using System.Linq; using Hkmp.Collection; @@ -33,12 +32,13 @@ internal class Entity { private readonly Dictionary _hookedActions; - private bool _originalIsActive; - - private bool _isControlled; + private readonly bool _originalIsActive; + private bool _isControlled; + private Vector3 _lastPosition; private Vector3 _lastScale; + private bool _lastIsActive; public Entity( NetClient netClient, @@ -58,11 +58,14 @@ GameObject gameObject ); _clientObject.SetActive(false); - // Store whether the host object was active and set it not active until we know - // if we are scene host + // Store whether the host object was active and set it not active until we know if we are scene host _originalIsActive = _hostObject.activeSelf; _hostObject.SetActive(false); + _lastIsActive = _hostObject.activeInHierarchy; + + Logger.Get().Info(this, $"Entity '{_hostObject.name}' was original active: {_originalIsActive}, last active: {_lastIsActive}"); + // Add a position interpolation component to the enemy so we can smooth out position updates _clientObject.AddComponent(); @@ -132,13 +135,19 @@ private void ProcessHostFsm(PlayMakerFSM fsm) { private void ProcessClientFsm(PlayMakerFSM fsm) { Logger.Get().Info(this, $"Processing client FSM: {fsm.Fsm.Name}"); - // Set the transition array of each state to an empty array - foreach (var state in fsm.FsmStates) { - state.Transitions = Array.Empty(); - } - - // Set the transition array for the global transitions to an empty array - fsm.Fsm.GlobalTransitions = Array.Empty(); + fsm.Fsm.Stop(); + Object.Destroy(fsm); + + // // Set the transition array of each state to an empty array + // foreach (var state in fsm.FsmStates) { + // state.Transitions = Array.Empty(); + // } + // + // // Set the transition array for the global transitions to an empty array + // fsm.Fsm.GlobalTransitions = Array.Empty(); + // + // // Finally, stop the FSM which in turn stops all actions that are still running + // fsm.Fsm.Stop(); } private void FindComponents() { @@ -184,16 +193,22 @@ private void OnActionEntered(On.HutongGames.PlayMaker.FsmStateAction.orig_OnEnte } private void OnUpdate() { - // We don't send updates when this entity is controlled - if (_isControlled) { + if (_hostObject == null) { return; } + + var hostObjectActive = _hostObject.activeSelf; - if (_clientObject == null) { + if (_isControlled) { + if (hostObjectActive) { + Logger.Get().Info(this, $"Entity '{_hostObject.name}' host object became active, re-disabling"); + _hostObject.SetActive(false); + } + return; } - var transform = _clientObject.transform; + var transform = _hostObject.transform; var newPosition = transform.position; if (newPosition != _lastPosition) { @@ -214,6 +229,18 @@ private void OnUpdate() { newScale.x > 0 ); } + + var newActive = _hostObject.activeInHierarchy; + if (newActive != _lastIsActive) { + _lastIsActive = newActive; + + Logger.Get().Info(this, $"Entity '{_hostObject.name}' changed active: {newActive}"); + + _netClient.UpdateManager.UpdateEntityIsActive( + _entityId, + newActive + ); + } } private void OnAnimationPlayed( @@ -249,7 +276,30 @@ float overrideFps public void InitializeHost() { _hostObject.SetActive(_originalIsActive); - _netClient.UpdateManager.UpdateEntityIsActive(_entityId, _originalIsActive); + // Also update the last active variable to account for this potential change + // Otherwise we might trigger the update sending of activity twice + _lastIsActive = _hostObject.activeInHierarchy; + + Logger.Get().Info(this, $"Initializing entity '{_hostObject.name}' with active: {_originalIsActive}, sending active: {_lastIsActive}"); + + var currentObject = _hostObject; + while (currentObject != null) { + Logger.Get().Info(this, $" Object: {currentObject}, active: {currentObject.activeSelf}"); + + var transform = currentObject.transform; + if (transform == null) { + break; + } + + var parent = transform.parent; + if (parent == null) { + break; + } + + currentObject = parent.gameObject; + } + + _netClient.UpdateManager.UpdateEntityIsActive(_entityId, _lastIsActive); _isControlled = false; @@ -268,7 +318,16 @@ public void MakeHost() { public void UpdatePosition(Vector2 position) { var unityPos = new Vector3(position.X, position.Y); - _clientObject.GetComponent().SetNewPosition(unityPos); + if (_clientObject == null) { + return; + } + + var positionInterpolation = _clientObject.GetComponent(); + if (positionInterpolation == null) { + return; + } + + positionInterpolation.SetNewPosition(unityPos); } public void UpdateScale(bool scale) { diff --git a/HKMP/Game/Server/ServerManager.cs b/HKMP/Game/Server/ServerManager.cs index 8d4dc275..ce69b3dc 100644 --- a/HKMP/Game/Server/ServerManager.cs +++ b/HKMP/Game/Server/ServerManager.cs @@ -363,10 +363,12 @@ private void OnClientEnterScene(ServerPlayerData playerData) { Id = entityKey.EntityId, Position = entityData.Position, Scale = entityData.Scale, + IsActive = entityData.IsActive, GenericData = entityData.GenericData }; entityUpdate.UpdateTypes.Add(EntityUpdateType.Position); entityUpdate.UpdateTypes.Add(EntityUpdateType.Scale); + entityUpdate.UpdateTypes.Add(EntityUpdateType.Active); entityUpdate.UpdateTypes.Add(EntityUpdateType.Data); if (entityData.AnimationId.HasValue) { From 4b90153561f2429547f13ad5efc0818e2d477a6a Mon Sep 17 00:00:00 2001 From: Extremelyd1 Date: Sat, 6 Aug 2022 08:21:04 +0200 Subject: [PATCH 007/216] Use reflection to access FSM action methods --- .../Client/Entity/Action/EntityFsmActions.cs | 83 +++++++++++++++---- 1 file changed, 67 insertions(+), 16 deletions(-) diff --git a/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs b/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs index 80246b0d..70913e06 100644 --- a/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs +++ b/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs @@ -1,34 +1,85 @@ using System; using System.Collections.Generic; +using System.Reflection; using Hkmp.Networking.Packet; using Hkmp.Networking.Packet.Data; using HutongGames.PlayMaker; using HutongGames.PlayMaker.Actions; using UnityEngine; -namespace Hkmp.Game.Client.Entity.Action; +namespace Hkmp.Game.Client.Entity.Action; internal static class EntityFsmActions { - private const string LogObjectName = "Hkmp.Game.Client.Entity.Action.EntityFsmActions"; - - public static readonly HashSet SupportedActionTypes = new() { - typeof(SpawnObjectFromGlobalPool) - }; - + private const string LogObjectName = "Hkmp.Game.Client.Entity.Action.EntityFsmActions"; + + private const string GetMethodNamePrefix = "Get"; + private const string ApplyMethodNamePrefix = "Apply"; + + private const BindingFlags StaticNonPublicFlags = BindingFlags.Static | BindingFlags.NonPublic; + + public static readonly HashSet SupportedActionTypes = new(); + + private static readonly Dictionary TypeGetMethodInfos = new(); + private static readonly Dictionary TypeApplyMethodInfos = new(); + + 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 + return; + } + + // Filter out the base methods + var parameterType = parameterInfos[1].ParameterType; + if (parameterType.IsAbstract || !parameterType.IsSubclassOf(typeof(FsmStateAction))) { + return; + } + + 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"); + } + } + } + public static void GetNetworkDataFromAction(EntityNetworkData data, FsmStateAction action) { - if (action is SpawnObjectFromGlobalPool spawnObjectFromGlobalPool) { - GetNetworkDataFromAction(data, spawnObjectFromGlobalPool); + 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"); } - throw new InvalidOperationException($"Given action type: {action.GetType()} does not have an associated method to get"); + methodInfo.Invoke( + null, + StaticNonPublicFlags, + null, + new object[] { data, action }, + null! + ); } - + public static void ApplyNetworkDataFromAction(EntityNetworkData data, FsmStateAction action) { - if (action is SpawnObjectFromGlobalPool spawnObjectFromGlobalPool) { - ApplyNetworkDataFromAction(data, spawnObjectFromGlobalPool); + 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"); } - throw new InvalidOperationException($"Given action type: {action.GetType()} does not have an associated method to apply"); + methodInfo.Invoke( + null, + StaticNonPublicFlags, + null, + new object[] { data, action }, + null! + ); } #region SpawnObjectFromGlobalPool @@ -46,7 +97,7 @@ private static void GetNetworkDataFromAction(EntityNetworkData data, SpawnObject data.Data.AddRange(BitConverter.GetBytes(position.x)); data.Data.AddRange(BitConverter.GetBytes(position.y)); data.Data.AddRange(BitConverter.GetBytes(position.z)); - + Logger.Get().Info(LogObjectName, $"Added entity network data: {position.x}, {position.y}, {position.z}"); } @@ -61,7 +112,7 @@ private static void ApplyNetworkDataFromAction(EntityNetworkData data, SpawnObje var posX = packet.ReadFloat(); var posY = packet.ReadFloat(); var posZ = packet.ReadFloat(); - + Logger.Get().Info(LogObjectName, $"Applying entity network data: {posX}, {posY}, {posZ}"); position = new Vector3(posX, posY, posZ); From 2bae2ddff902395b15a5c7ffe9cb446a748a3812 Mon Sep 17 00:00:00 2001 From: Extremelyd1 Date: Sun, 14 Aug 2022 20:46:05 +0200 Subject: [PATCH 008/216] Fix client FSM interference --- .../Client/Entity/Action/EntityFsmActions.cs | 141 ++++++++++++++---- .../Client/Entity/Action/FsmActionHooks.cs | 52 +++++++ HKMP/Game/Client/Entity/Entity.cs | 43 +++--- HKMP/HKMP.csproj | 3 + 4 files changed, 189 insertions(+), 50 deletions(-) create mode 100644 HKMP/Game/Client/Entity/Action/FsmActionHooks.cs diff --git a/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs b/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs index 70913e06..4cbf0821 100644 --- a/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs +++ b/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs @@ -38,11 +38,13 @@ static EntityFsmActions() { return; } + Logger.Get().Info(LogObjectName, $"Parameter type that is subclass: {parameterType}"); SupportedActionTypes.Add(parameterType); if (methodInfo.Name.StartsWith(GetMethodNamePrefix)) { TypeGetMethodInfos.Add(parameterType, methodInfo); } else if (methodInfo.Name.StartsWith(ApplyMethodNamePrefix)) { + Logger.Get().Info(LogObjectName, $"Method info found: {methodInfo}"); TypeApplyMethodInfos.Add(parameterType, methodInfo); } else { throw new Exception("Method was defined that does not adhere to the method naming"); @@ -72,6 +74,8 @@ public static void ApplyNetworkDataFromAction(EntityNetworkData data, FsmStateAc throw new InvalidOperationException( $"Given action type: {action.GetType()} does not have an associated method to apply"); } + + Logger.Get().Info(LogObjectName, $"Apply method: {actionType}, {methodInfo}"); methodInfo.Invoke( null, @@ -80,6 +84,8 @@ public static void ApplyNetworkDataFromAction(EntityNetworkData data, FsmStateAc new object[] { data, action }, null! ); + + Logger.Get().Info(LogObjectName, $"After methodinfo invoke: {actionType}, {methodInfo}"); } #region SpawnObjectFromGlobalPool @@ -96,47 +102,130 @@ private static void GetNetworkDataFromAction(EntityNetworkData data, SpawnObject var position = spawnPoint.Value.transform.position; data.Data.AddRange(BitConverter.GetBytes(position.x)); data.Data.AddRange(BitConverter.GetBytes(position.y)); - data.Data.AddRange(BitConverter.GetBytes(position.z)); - Logger.Get().Info(LogObjectName, $"Added entity network data: {position.x}, {position.y}, {position.z}"); + if (action.rotation.IsNone) { + var rotation = spawnPoint.Value.transform.eulerAngles; + data.Data.AddRange(BitConverter.GetBytes(rotation.x)); + data.Data.AddRange(BitConverter.GetBytes(rotation.y)); + data.Data.AddRange(BitConverter.GetBytes(rotation.z)); + } + + Logger.Get().Info(LogObjectName, $"Added SOFGP entity network data: {position.x}, {position.y}, {position.z}"); } private static void ApplyNetworkDataFromAction(EntityNetworkData data, SpawnObjectFromGlobalPool action) { - var packet = new Packet(data.Data.ToArray()); + try { + var packet = new Packet(data.Data.ToArray()); + + var position = Vector3.zero; + var euler = Vector3.up; + + var hasSpawnPoint = packet.ReadBool(); + if (hasSpawnPoint) { + var posX = packet.ReadFloat(); + var posY = packet.ReadFloat(); + + Logger.Get().Info(LogObjectName, $"Applying SOFGP entity network data: {posX}, {posY}"); + + position = new Vector3(posX, posY); + + if (!action.position.IsNone) { + position += action.position.Value; + } + + if (!action.rotation.IsNone) { + euler = action.rotation.Value; + } else { + euler = new Vector3( + packet.ReadFloat(), + packet.ReadFloat(), + packet.ReadFloat() + ); + } + } else { + Logger.Get().Info(LogObjectName, "Applying SOFGP entity network data without spawnpoint"); + if (!action.position.IsNone) { + position = action.position.Value; + } + + if (!action.rotation.IsNone) { + euler = action.rotation.Value; + } + } - var position = Vector3.zero; - var euler = Vector3.up; + if (action.gameObject != null) { + action.storeObject.Value = action.gameObject.Value.Spawn(position, Quaternion.Euler(euler)); + } + } catch (Exception e) { + Logger.Get().Info(LogObjectName, $"Apply SOFGP exception: {e.GetType()}, {e.Message}, {e.StackTrace}"); + } + } - var hasSpawnPoint = packet.ReadBool(); - if (hasSpawnPoint) { - var posX = packet.ReadFloat(); - var posY = packet.ReadFloat(); - var posZ = packet.ReadFloat(); + #endregion + + #region FireAtTarget - Logger.Get().Info(LogObjectName, $"Applying entity network data: {posX}, {posY}, {posZ}"); + private static void GetNetworkDataFromAction(EntityNetworkData data, FireAtTarget action) { + // target + var target = action.target; - position = new Vector3(posX, posY, posZ); + var position = target.Value.transform.position; + data.Data.AddRange(BitConverter.GetBytes(position.x)); + data.Data.AddRange(BitConverter.GetBytes(position.y)); + + Logger.Get().Info(LogObjectName, $"Added FAT entity network data: {position.x}, {position.y}"); + } - if (!action.position.IsNone) { - position += action.position.Value; - } + private static void ApplyNetworkDataFromAction(EntityNetworkData data, FireAtTarget action) { + try { + var packet = new Packet(data.Data.ToArray()); + + var posX = packet.ReadFloat(); + var posY = packet.ReadFloat(); - euler = !action.rotation.IsNone ? action.rotation.Value : action.spawnPoint.Value.transform.eulerAngles; - } else { - Logger.Get().Info(LogObjectName, $"Applying entity network data without spawnpoint"); - if (!action.position.IsNone) { - position = action.position.Value; + Logger.Get().Info(LogObjectName, $"Applying FAT entity network data: {posX}, {posY}"); + + // var selfGameObject = ReflectionHelper.GetField(action, "self"); + + Logger.Get().Info(LogObjectName, $"Action: {action}"); + + var gameObject = action.gameObject; + Logger.Get().Info(LogObjectName, $"GameObject null: {gameObject == null}"); + Logger.Get().Info(LogObjectName, $"GameObject: {gameObject}"); + + var fsm = action.Fsm; + Logger.Get().Info(LogObjectName, $"Action FSM: {fsm}"); + + var ownerDefaultObject = gameObject.GameObject; + Logger.Get().Info(LogObjectName, $"ownerDefaultObject null: {ownerDefaultObject == null}"); + Logger.Get().Info(LogObjectName, $"ownerDefaultObject: {ownerDefaultObject}"); + + var selfGameObject = action.Fsm.GetOwnerDefaultTarget(action.gameObject); + + var selfPosition = selfGameObject.transform.position; + + var rigidBody = selfGameObject.GetComponent(); + if (rigidBody == null) { + return; } - if (!action.rotation.IsNone) { - euler = action.rotation.Value; + var num = Mathf.Atan2( + posY + action.position.Value.y - selfPosition.y, + posX + action.position.Value.x - selfPosition.x + ) * 57.295776f; + + if (!action.spread.IsNone) { + num += UnityEngine.Random.Range(-action.spread.Value, action.spread.Value); } - } - if (action.gameObject != null) { - action.storeObject.Value = action.gameObject.Value.Spawn(position, Quaternion.Euler(euler)); + rigidBody.velocity = new Vector2( + action.speed.Value * Mathf.Cos(num * ((float)System.Math.PI / 180f)), + action.speed.Value * Mathf.Sin(num * ((float)System.Math.PI / 180f)) + ); + } catch (Exception e) { + Logger.Get().Info(LogObjectName, $"Apply FAT exception: {e.GetType()}, {e.Message}, {e.StackTrace}"); } } - + #endregion } \ No newline at end of file diff --git a/HKMP/Game/Client/Entity/Action/FsmActionHooks.cs b/HKMP/Game/Client/Entity/Action/FsmActionHooks.cs new file mode 100644 index 00000000..2ac8894e --- /dev/null +++ b/HKMP/Game/Client/Entity/Action/FsmActionHooks.cs @@ -0,0 +1,52 @@ +using System; +using System.Collections.Generic; +using HutongGames.PlayMaker; +using MonoMod.RuntimeDetour; + +namespace Hkmp.Game.Client.Entity.Action; + +internal static class FsmActionHooks { + private static readonly Dictionary TypeEvents; + + static FsmActionHooks() { + TypeEvents = new Dictionary(); + } + + public static void RegisterFsmStateActionType(Type type, Action action) { + if (!TypeEvents.TryGetValue(type, out var fsmActionHook)) { + fsmActionHook = new FsmActionHook(); + + var onEnterMethodInfo = type.GetMethod("OnEnter"); + + // TODO: check if we need to keep track of hook + // ReSharper disable once ObjectCreationAsStatement + new Hook( + onEnterMethodInfo, + OnActionEntered + ); + + TypeEvents.Add(type, fsmActionHook); + } + + fsmActionHook.HookEvent += action; + } + + private static void OnActionEntered(Action orig, FsmStateAction self) { + orig(self); + + if (!TypeEvents.TryGetValue(self.GetType(), out var fsmActionHook)) { + Logger.Get().Warn("FsmActionHook", "Hook was fired but no associated hook class was found"); + return; + } + + fsmActionHook.InvokeEvent(self); + } + + private class FsmActionHook { + public event Action HookEvent; + + public void InvokeEvent(FsmStateAction fsmStateAction) { + HookEvent?.Invoke(fsmStateAction); + } + } +} \ No newline at end of file diff --git a/HKMP/Game/Client/Entity/Entity.cs b/HKMP/Game/Client/Entity/Entity.cs index e565528f..15821af4 100644 --- a/HKMP/Game/Client/Entity/Entity.cs +++ b/HKMP/Game/Client/Entity/Entity.cs @@ -97,7 +97,12 @@ GameObject gameObject foreach (var fsm in _hostFsms) { ProcessHostFsm(fsm); } - On.HutongGames.PlayMaker.FsmStateAction.OnEnter += OnActionEntered; + + // Remove all components that (re-)activate FSMs + foreach (var fsmActivator in _clientObject.GetComponents()) { + fsmActivator.StopAllCoroutines(); + Object.Destroy(fsmActivator); + } _clientFsms = _clientObject.GetComponents().ToList(); foreach (var fsm in _clientFsms) { @@ -128,28 +133,17 @@ private void ProcessHostFsm(PlayMakerFSM fsm) { ActionIndex = j }; Logger.Get().Info(this, $"Created hooked action: {action.GetType()}, {_hostFsms.IndexOf(fsm)}, {i}, {j}"); + + FsmActionHooks.RegisterFsmStateActionType(action.GetType(), OnActionEntered); } } } private void ProcessClientFsm(PlayMakerFSM fsm) { Logger.Get().Info(this, $"Processing client FSM: {fsm.Fsm.Name}"); - - fsm.Fsm.Stop(); - Object.Destroy(fsm); - - // // Set the transition array of each state to an empty array - // foreach (var state in fsm.FsmStates) { - // state.Transitions = Array.Empty(); - // } - // - // // Set the transition array for the global transitions to an empty array - // fsm.Fsm.GlobalTransitions = Array.Empty(); - // - // // Finally, stop the FSM which in turn stops all actions that are still running - // fsm.Fsm.Stop(); + fsm.enabled = false; } - + private void FindComponents() { var climber = _clientObject.GetComponent(); if (climber != null) { @@ -163,32 +157,32 @@ private void FindComponents() { } } - private void OnActionEntered(On.HutongGames.PlayMaker.FsmStateAction.orig_OnEnter orig, FsmStateAction self) { - orig(self); - + private void OnActionEntered(FsmStateAction self) { + Logger.Get().Info(this, $"ActionEntered: {self.GetType()}"); + if (_isControlled) { return; } - + if (!_hookedActions.TryGetValue(self, out var hookedEntityAction)) { return; } Logger.Get().Info(this, $"Hooked action was entered: {hookedEntityAction.FsmIndex}, {hookedEntityAction.StateIndex}, {hookedEntityAction.ActionIndex}"); - + var networkData = new EntityNetworkData { Type = EntityNetworkData.DataType.Fsm }; - + if (_hostFsms.Count > 1) { networkData.Data.Add((byte)hookedEntityAction.FsmIndex); } networkData.Data.Add((byte) hookedEntityAction.StateIndex); networkData.Data.Add((byte) hookedEntityAction.ActionIndex); - + EntityFsmActions.GetNetworkDataFromAction(networkData, self); - + _netClient.UpdateManager.AddEntityData(_entityId, networkData); } @@ -395,6 +389,7 @@ public void UpdateIsActive(bool active) { } public void UpdateData(List entityNetworkData) { + Logger.Get().Info(this, $"UpdateData called for entity: {_entityId}"); foreach (var data in entityNetworkData) { if (data.Type == EntityNetworkData.DataType.Fsm) { PlayMakerFSM fsm; diff --git a/HKMP/HKMP.csproj b/HKMP/HKMP.csproj index 80b63196..61363d69 100644 --- a/HKMP/HKMP.csproj +++ b/HKMP/HKMP.csproj @@ -34,6 +34,9 @@ $(References)\MMHOOK_PlayMaker.dll False + + lib\MonoMod.RuntimeDetour.dll + ..\HKMPServer\Lib\Newtonsoft.Json.dll False From 4ab33772a356fc162b47f6e9d0579be0e48940ed Mon Sep 17 00:00:00 2001 From: Extremelyd1 Date: Wed, 17 Aug 2022 23:01:44 +0200 Subject: [PATCH 009/216] Partial implementation of health manager component --- .../Client/Entity/Action/EntityFsmActions.cs | 41 ++--- .../Entity/Component/EntityComponent.cs | 9 +- .../Component/HealthManagerComponent.cs | 63 +++++++ .../Entity/Component/RotationComponent.cs | 28 ++- HKMP/Game/Client/Entity/Entity.cs | 160 +++++++++--------- HKMP/Game/Client/Entity/HostClientPair.cs | 6 + HKMP/Game/Server/ServerManager.cs | 3 +- HKMP/Networking/Packet/Data/EntityUpdate.cs | 20 ++- 8 files changed, 199 insertions(+), 131 deletions(-) create mode 100644 HKMP/Game/Client/Entity/Component/HealthManagerComponent.cs create mode 100644 HKMP/Game/Client/Entity/HostClientPair.cs diff --git a/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs b/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs index 4cbf0821..522d1dd6 100644 --- a/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs +++ b/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs @@ -93,21 +93,21 @@ public static void ApplyNetworkDataFromAction(EntityNetworkData data, FsmStateAc private static void GetNetworkDataFromAction(EntityNetworkData data, SpawnObjectFromGlobalPool action) { var spawnPoint = action.spawnPoint; if (spawnPoint == null) { - data.Data.Add(0); + data.Packet.Write(false); return; } - data.Data.Add(1); + data.Packet.Write(true); var position = spawnPoint.Value.transform.position; - data.Data.AddRange(BitConverter.GetBytes(position.x)); - data.Data.AddRange(BitConverter.GetBytes(position.y)); + data.Packet.Write(position.x); + data.Packet.Write(position.y); if (action.rotation.IsNone) { var rotation = spawnPoint.Value.transform.eulerAngles; - data.Data.AddRange(BitConverter.GetBytes(rotation.x)); - data.Data.AddRange(BitConverter.GetBytes(rotation.y)); - data.Data.AddRange(BitConverter.GetBytes(rotation.z)); + data.Packet.Write(rotation.x); + data.Packet.Write(rotation.y); + data.Packet.Write(rotation.z); } Logger.Get().Info(LogObjectName, $"Added SOFGP entity network data: {position.x}, {position.y}, {position.z}"); @@ -115,15 +115,13 @@ private static void GetNetworkDataFromAction(EntityNetworkData data, SpawnObject private static void ApplyNetworkDataFromAction(EntityNetworkData data, SpawnObjectFromGlobalPool action) { try { - var packet = new Packet(data.Data.ToArray()); - var position = Vector3.zero; var euler = Vector3.up; - var hasSpawnPoint = packet.ReadBool(); + var hasSpawnPoint = data.Packet.ReadBool(); if (hasSpawnPoint) { - var posX = packet.ReadFloat(); - var posY = packet.ReadFloat(); + var posX = data.Packet.ReadFloat(); + var posY = data.Packet.ReadFloat(); Logger.Get().Info(LogObjectName, $"Applying SOFGP entity network data: {posX}, {posY}"); @@ -137,9 +135,9 @@ private static void ApplyNetworkDataFromAction(EntityNetworkData data, SpawnObje euler = action.rotation.Value; } else { euler = new Vector3( - packet.ReadFloat(), - packet.ReadFloat(), - packet.ReadFloat() + data.Packet.ReadFloat(), + data.Packet.ReadFloat(), + data.Packet.ReadFloat() ); } } else { @@ -166,22 +164,19 @@ private static void ApplyNetworkDataFromAction(EntityNetworkData data, SpawnObje #region FireAtTarget private static void GetNetworkDataFromAction(EntityNetworkData data, FireAtTarget action) { - // target var target = action.target; var position = target.Value.transform.position; - data.Data.AddRange(BitConverter.GetBytes(position.x)); - data.Data.AddRange(BitConverter.GetBytes(position.y)); - + data.Packet.Write(position.x); + data.Packet.Write(position.y); + Logger.Get().Info(LogObjectName, $"Added FAT entity network data: {position.x}, {position.y}"); } private static void ApplyNetworkDataFromAction(EntityNetworkData data, FireAtTarget action) { try { - var packet = new Packet(data.Data.ToArray()); - - var posX = packet.ReadFloat(); - var posY = packet.ReadFloat(); + var posX = data.Packet.ReadFloat(); + var posY = data.Packet.ReadFloat(); Logger.Get().Info(LogObjectName, $"Applying FAT entity network data: {posX}, {posY}"); diff --git a/HKMP/Game/Client/Entity/Component/EntityComponent.cs b/HKMP/Game/Client/Entity/Component/EntityComponent.cs index 5e469ec5..f3c5d9a0 100644 --- a/HKMP/Game/Client/Entity/Component/EntityComponent.cs +++ b/HKMP/Game/Client/Entity/Component/EntityComponent.cs @@ -8,22 +8,19 @@ internal abstract class EntityComponent { private readonly NetClient _netClient; private readonly byte _entityId; - protected readonly GameObject HostObject; - protected readonly GameObject ClientObject; + protected readonly HostClientPair GameObject; public bool IsControlled { get; set; } protected EntityComponent( NetClient netClient, byte entityId, - GameObject hostObject, - GameObject clientObject + HostClientPair gameObject ) { _netClient = netClient; _entityId = entityId; - HostObject = hostObject; - ClientObject = clientObject; + GameObject = gameObject; } protected void SendData(EntityNetworkData data) { diff --git a/HKMP/Game/Client/Entity/Component/HealthManagerComponent.cs b/HKMP/Game/Client/Entity/Component/HealthManagerComponent.cs new file mode 100644 index 00000000..b2ff7f75 --- /dev/null +++ b/HKMP/Game/Client/Entity/Component/HealthManagerComponent.cs @@ -0,0 +1,63 @@ +using System; +using Hkmp.Networking.Client; +using Hkmp.Networking.Packet.Data; +using UnityEngine; + +namespace Hkmp.Game.Client.Entity.Component; + +// TODO: periodically (or on hit) sync the health of the entity so on scene host transfer we can reset health +internal class HealthManagerComponent : EntityComponent { + private readonly HostClientPair _healthManager; + + public HealthManagerComponent( + NetClient netClient, + byte entityId, + HostClientPair gameObject, + HostClientPair healthManager + ) : base(netClient, entityId, gameObject) { + _healthManager = healthManager; + + On.HealthManager.Die += HealthManagerOnDie; + } + + private void HealthManagerOnDie( + On.HealthManager.orig_Die orig, + HealthManager self, + float? attackDirection, + AttackTypes attackType, + bool ignoreEvasion + ) { + if (IsControlled) { + Logger.Get().Info(this, "Entity's health manager tried dying, cancelled because client"); + return; + } + + orig(self, attackDirection, attackType, ignoreEvasion); + + var data = new EntityNetworkData { + Type = EntityNetworkData.DataType.HealthManager + }; + + if (attackDirection.HasValue) { + data.Packet.Write(true); + data.Packet.Write(attackDirection.Value); + } else { + data.Packet.Write(false); + } + + data.Packet.Write((byte) attackType); + + data.Packet.Write(ignoreEvasion); + } + + public override void InitializeHost() { + } + + public override void Update(EntityNetworkData data) { + throw new System.NotImplementedException(); + } + + public override void Destroy() { + throw new System.NotImplementedException(); + } +} \ No newline at end of file diff --git a/HKMP/Game/Client/Entity/Component/RotationComponent.cs b/HKMP/Game/Client/Entity/Component/RotationComponent.cs index 1f0b45dd..bdd90444 100644 --- a/HKMP/Game/Client/Entity/Component/RotationComponent.cs +++ b/HKMP/Game/Client/Entity/Component/RotationComponent.cs @@ -1,26 +1,24 @@ -using System; using Hkmp.Networking.Client; using Hkmp.Networking.Packet.Data; using Hkmp.Util; using UnityEngine; -namespace Hkmp.Game.Client.Entity.Component; +namespace Hkmp.Game.Client.Entity.Component; internal class RotationComponent : EntityComponent { private readonly Climber _climber; - + private Vector3 _lastRotation; public RotationComponent( - NetClient netClient, + NetClient netClient, byte entityId, - GameObject hostObject, - GameObject clientObject, + HostClientPair gameObject, Climber climber - ) : base(netClient, entityId, hostObject, clientObject) { + ) : base(netClient, entityId, gameObject) { _climber = climber; _climber.enabled = false; - + MonoBehaviourUtil.Instance.OnUpdateEvent += OnUpdateRotation; } @@ -29,11 +27,11 @@ private void OnUpdateRotation() { return; } - if (HostObject == null) { + if (GameObject.Host == null) { return; } - var transform = HostObject.transform; + var transform = GameObject.Host.transform; var newRotation = transform.rotation.eulerAngles; if (newRotation != _lastRotation) { @@ -42,12 +40,12 @@ private void OnUpdateRotation() { var data = new EntityNetworkData { Type = EntityNetworkData.DataType.Rotation }; - data.Data.AddRange(BitConverter.GetBytes(newRotation.z)); + data.Packet.Write(newRotation.z); SendData(data); } } - + public override void InitializeHost() { if (_climber != null) { _climber.enabled = true; @@ -55,9 +53,9 @@ public override void InitializeHost() { } public override void Update(EntityNetworkData data) { - var rotation = BitConverter.ToSingle(data.Data.ToArray(), 0); - - var transform = ClientObject.transform; + var rotation = data.Packet.ReadFloat(); + + var transform = GameObject.Client.transform; var eulerAngles = transform.eulerAngles; transform.eulerAngles = new Vector3( eulerAngles.x, diff --git a/HKMP/Game/Client/Entity/Entity.cs b/HKMP/Game/Client/Entity/Entity.cs index 15821af4..372509c7 100644 --- a/HKMP/Game/Client/Entity/Entity.cs +++ b/HKMP/Game/Client/Entity/Entity.cs @@ -17,16 +17,13 @@ internal class Entity { private readonly NetClient _netClient; private readonly byte _entityId; - private readonly GameObject _hostObject; - private readonly GameObject _clientObject; + private readonly HostClientPair _object; - private readonly tk2dSpriteAnimator _hostAnimator; - private readonly tk2dSpriteAnimator _clientAnimator; + private readonly HostClientPair _animator; private readonly BiLookup _animationClipNameIds; - private readonly List _hostFsms; - private readonly List _clientFsms; + private readonly HostClientPair> _fsms; private readonly Dictionary _components; @@ -43,47 +40,51 @@ internal class Entity { public Entity( NetClient netClient, byte entityId, - GameObject gameObject + GameObject hostObject ) { _netClient = netClient; _entityId = entityId; - _hostObject = gameObject; _isControlled = true; - _clientObject = Object.Instantiate( - _hostObject, - _hostObject.transform.position, - _hostObject.transform.rotation - ); - _clientObject.SetActive(false); + _object = new HostClientPair { + Host = hostObject, + Client = Object.Instantiate( + hostObject, + hostObject.transform.position, + hostObject.transform.rotation + ) + }; + _object.Client.SetActive(false); // Store whether the host object was active and set it not active until we know if we are scene host - _originalIsActive = _hostObject.activeSelf; - _hostObject.SetActive(false); + _originalIsActive = _object.Host.activeSelf; + _object.Host.SetActive(false); - _lastIsActive = _hostObject.activeInHierarchy; + _lastIsActive = _object.Host.activeInHierarchy; - Logger.Get().Info(this, $"Entity '{_hostObject.name}' was original active: {_originalIsActive}, last active: {_lastIsActive}"); + Logger.Get().Info(this, $"Entity '{_object.Host.name}' was original active: {_originalIsActive}, last active: {_lastIsActive}"); // Add a position interpolation component to the enemy so we can smooth out position updates - _clientObject.AddComponent(); + _object.Client.AddComponent(); // Register an update event to send position updates MonoBehaviourUtil.Instance.OnUpdateEvent += OnUpdate; - _hostAnimator = _hostObject.GetComponent(); - _clientAnimator = _clientObject.GetComponent(); - if (_hostAnimator != null) { + _animator = new HostClientPair { + Host = _object.Host.GetComponent(), + Client = _object.Client.GetComponent() + }; + if (_animator.Host != null) { _animationClipNameIds = new BiLookup(); var index = 0; - foreach (var animationClip in _hostAnimator.Library.clips) { + foreach (var animationClip in _animator.Host.Library.clips) { _animationClipNameIds.Add(animationClip.name, (byte)index++); if (index > byte.MaxValue) { Logger.Get().Error(this, - $"Too many animation clips to fit in a byte for entity: {_clientObject.name}"); + $"Too many animation clips to fit in a byte for entity: {_object.Client.name}"); break; } } @@ -91,21 +92,23 @@ GameObject gameObject On.tk2dSpriteAnimator.Play_tk2dSpriteAnimationClip_float_float += OnAnimationPlayed; } - _hostFsms = _hostObject.GetComponents().ToList(); + _fsms = new HostClientPair> { + Host = _object.Host.GetComponents().ToList(), + Client = _object.Client.GetComponents().ToList() + }; _hookedActions = new Dictionary(); - foreach (var fsm in _hostFsms) { + foreach (var fsm in _fsms.Host) { ProcessHostFsm(fsm); } // Remove all components that (re-)activate FSMs - foreach (var fsmActivator in _clientObject.GetComponents()) { + foreach (var fsmActivator in _object.Client.GetComponents()) { fsmActivator.StopAllCoroutines(); Object.Destroy(fsmActivator); } - _clientFsms = _clientObject.GetComponents().ToList(); - foreach (var fsm in _clientFsms) { + foreach (var fsm in _fsms.Client) { ProcessClientFsm(fsm); } @@ -128,11 +131,11 @@ private void ProcessHostFsm(PlayMakerFSM fsm) { _hookedActions[action] = new HookedEntityAction { Action = action, - FsmIndex = _hostFsms.IndexOf(fsm), + FsmIndex = _fsms.Host.IndexOf(fsm), StateIndex = i, ActionIndex = j }; - Logger.Get().Info(this, $"Created hooked action: {action.GetType()}, {_hostFsms.IndexOf(fsm)}, {i}, {j}"); + Logger.Get().Info(this, $"Created hooked action: {action.GetType()}, {_fsms.Host.IndexOf(fsm)}, {i}, {j}"); FsmActionHooks.RegisterFsmStateActionType(action.GetType(), OnActionEntered); } @@ -145,13 +148,18 @@ private void ProcessClientFsm(PlayMakerFSM fsm) { } private void FindComponents() { - var climber = _clientObject.GetComponent(); + var hostHealthManager = _object.Host.GetComponent(); + var clientHealthManager = _object.Client.GetComponent(); + if (hostHealthManager != null && clientHealthManager != null) { + + } + + var climber = _object.Client.GetComponent(); if (climber != null) { _components[EntityNetworkData.DataType.Rotation] = new RotationComponent( _netClient, _entityId, - _hostObject, - _clientObject, + _object, climber ); } @@ -174,12 +182,12 @@ private void OnActionEntered(FsmStateAction self) { Type = EntityNetworkData.DataType.Fsm }; - if (_hostFsms.Count > 1) { - networkData.Data.Add((byte)hookedEntityAction.FsmIndex); + if (_fsms.Host.Count > 1) { + networkData.Packet.Write((byte)hookedEntityAction.FsmIndex); } - networkData.Data.Add((byte) hookedEntityAction.StateIndex); - networkData.Data.Add((byte) hookedEntityAction.ActionIndex); + networkData.Packet.Write((byte) hookedEntityAction.StateIndex); + networkData.Packet.Write((byte) hookedEntityAction.ActionIndex); EntityFsmActions.GetNetworkDataFromAction(networkData, self); @@ -187,22 +195,22 @@ private void OnActionEntered(FsmStateAction self) { } private void OnUpdate() { - if (_hostObject == null) { + if (_object.Host == null) { return; } - var hostObjectActive = _hostObject.activeSelf; + var hostObjectActive = _object.Host.activeSelf; if (_isControlled) { if (hostObjectActive) { - Logger.Get().Info(this, $"Entity '{_hostObject.name}' host object became active, re-disabling"); - _hostObject.SetActive(false); + Logger.Get().Info(this, $"Entity '{_object.Host.name}' host object became active, re-disabling"); + _object.Host.SetActive(false); } return; } - var transform = _hostObject.transform; + var transform = _object.Host.transform; var newPosition = transform.position; if (newPosition != _lastPosition) { @@ -224,11 +232,11 @@ private void OnUpdate() { ); } - var newActive = _hostObject.activeInHierarchy; + var newActive = _object.Host.activeInHierarchy; if (newActive != _lastIsActive) { _lastIsActive = newActive; - Logger.Get().Info(this, $"Entity '{_hostObject.name}' changed active: {newActive}"); + Logger.Get().Info(this, $"Entity '{_object.Host.name}' changed active: {newActive}"); _netClient.UpdateManager.UpdateEntityIsActive( _entityId, @@ -246,7 +254,7 @@ float overrideFps ) { orig(self, clip, clipStartTime, overrideFps); - if (self != _hostAnimator) { + if (self != _animator.Host) { return; } @@ -255,7 +263,7 @@ float overrideFps } if (!_animationClipNameIds.TryGetValue(clip.name, out var animationId)) { - Logger.Get().Warn(this, $"Entity '{_clientObject.name}' played unknown animation: {clip.name}"); + Logger.Get().Warn(this, $"Entity '{_object.Client.name}' played unknown animation: {clip.name}"); return; } @@ -268,15 +276,15 @@ float overrideFps } public void InitializeHost() { - _hostObject.SetActive(_originalIsActive); + _object.Host.SetActive(_originalIsActive); // Also update the last active variable to account for this potential change // Otherwise we might trigger the update sending of activity twice - _lastIsActive = _hostObject.activeInHierarchy; + _lastIsActive = _object.Host.activeInHierarchy; - Logger.Get().Info(this, $"Initializing entity '{_hostObject.name}' with active: {_originalIsActive}, sending active: {_lastIsActive}"); + Logger.Get().Info(this, $"Initializing entity '{_object.Host.name}' with active: {_originalIsActive}, sending active: {_lastIsActive}"); - var currentObject = _hostObject; + var currentObject = _object.Host; while (currentObject != null) { Logger.Get().Info(this, $" Object: {currentObject}, active: {currentObject.activeSelf}"); @@ -312,11 +320,11 @@ public void MakeHost() { public void UpdatePosition(Vector2 position) { var unityPos = new Vector3(position.X, position.Y); - if (_clientObject == null) { + if (_object.Client == null) { return; } - var positionInterpolation = _clientObject.GetComponent(); + var positionInterpolation = _object.Client.GetComponent(); if (positionInterpolation == null) { return; } @@ -325,7 +333,7 @@ public void UpdatePosition(Vector2 position) { } public void UpdateScale(bool scale) { - var transform = _clientObject.transform; + var transform = _object.Client.transform; var localScale = transform.localScale; var currentScaleX = localScale.x; @@ -339,14 +347,14 @@ public void UpdateScale(bool scale) { } public void UpdateAnimation(byte animationId, tk2dSpriteAnimationClip.WrapMode wrapMode, bool alreadyInSceneUpdate) { - if (_clientAnimator == null) { + if (_animator.Client == null) { Logger.Get().Warn(this, - $"Entity '{_clientObject.name}' received animation while client animator does not exist"); + $"Entity '{_object.Client.name}' received animation while client animator does not exist"); return; } if (!_animationClipNameIds.TryGetValue(animationId, out var clipName)) { - Logger.Get().Warn(this, $"Entity '{_clientObject.name}' received unknown animation ID: {animationId}"); + Logger.Get().Warn(this, $"Entity '{_object.Client.name}' received unknown animation ID: {animationId}"); return; } @@ -356,16 +364,16 @@ public void UpdateAnimation(byte animationId, tk2dSpriteAnimationClip.WrapMode w // Since this is an animation update from an entity that was already present in a scene, // we need to determine where to start playing this specific animation if (wrapMode == tk2dSpriteAnimationClip.WrapMode.Loop) { - _clientAnimator.Play(clipName); + _animator.Client.Play(clipName); return; } - var clip = _clientAnimator.GetClipByName(clipName); + var clip = _animator.Client.GetClipByName(clipName); if (wrapMode == tk2dSpriteAnimationClip.WrapMode.LoopSection) { // The clip loops in a specific section in the frames, so we start playing // it from the start of that section - _clientAnimator.PlayFromFrame(clipName, clip.loopStart); + _animator.Client.PlayFromFrame(clipName, clip.loopStart); return; } @@ -374,18 +382,18 @@ public void UpdateAnimation(byte animationId, tk2dSpriteAnimationClip.WrapMode w // Since the clip was played once, it stops on the last frame, // so we emulate that by only "playing" the last frame of the clip var clipLength = clip.frames.Length; - _clientAnimator.PlayFromFrame(clipName, clipLength - 1); + _animator.Client.PlayFromFrame(clipName, clipLength - 1); return; } } // Otherwise, default to just playing the clip - _clientAnimator.Play(clipName); + _animator.Client.Play(clipName); } public void UpdateIsActive(bool active) { - Logger.Get().Info(this, $"Entity '{_clientObject.name}' received active: {active}"); - _clientObject.SetActive(active); + Logger.Get().Info(this, $"Entity '{_object.Client.name}' received active: {active}"); + _object.Client.SetActive(active); } public void UpdateData(List entityNetworkData) { @@ -396,31 +404,27 @@ public void UpdateData(List entityNetworkData) { byte stateIndex; byte actionIndex; - if (_clientFsms.Count > 1) { + if (_fsms.Client.Count > 1) { // Do a check on the length of the data - if (data.Data.Count < 3) { + if (data.Packet.Length < 3) { continue; } - var fsmIndex = data.Data[0]; - fsm = _clientFsms[fsmIndex]; - - stateIndex = data.Data[1]; - actionIndex = data.Data[2]; + var fsmIndex = data.Packet.ReadByte(); + fsm = _fsms.Client[fsmIndex]; - data.Data.RemoveRange(0, 3); + stateIndex = data.Packet.ReadByte(); + actionIndex = data.Packet.ReadByte(); } else { // Do a check on the length of the data - if (data.Data.Count < 2) { + if (data.Packet.Length < 2) { continue; } - fsm = _clientFsms[0]; + fsm = _fsms.Client[0]; - stateIndex = data.Data[0]; - actionIndex = data.Data[1]; - - data.Data.RemoveRange(0, 2); + stateIndex = data.Packet.ReadByte(); + actionIndex = data.Packet.ReadByte(); } Logger.Get().Info(this, $"Received entity network data for FSM: {fsm.Fsm.Name}, {stateIndex}, {actionIndex}"); diff --git a/HKMP/Game/Client/Entity/HostClientPair.cs b/HKMP/Game/Client/Entity/HostClientPair.cs new file mode 100644 index 00000000..550678e3 --- /dev/null +++ b/HKMP/Game/Client/Entity/HostClientPair.cs @@ -0,0 +1,6 @@ +namespace Hkmp.Game.Client.Entity; + +internal class HostClientPair { + public T Client { get; set; } + public T Host { get; set; } +} \ No newline at end of file diff --git a/HKMP/Game/Server/ServerManager.cs b/HKMP/Game/Server/ServerManager.cs index ce69b3dc..c055cbba 100644 --- a/HKMP/Game/Server/ServerManager.cs +++ b/HKMP/Game/Server/ServerManager.cs @@ -575,8 +575,7 @@ private void OnEntityUpdate(ushort id, EntityUpdate entityUpdate) { d => d.Type == EntityNetworkData.DataType.Rotation ); if (existingData != null) { - existingData.Data.Clear(); - existingData.Data.AddRange(updateData.Data); + existingData.Packet = updateData.Packet; } } } diff --git a/HKMP/Networking/Packet/Data/EntityUpdate.cs b/HKMP/Networking/Packet/Data/EntityUpdate.cs index cfda686c..7236128b 100644 --- a/HKMP/Networking/Packet/Data/EntityUpdate.cs +++ b/HKMP/Networking/Packet/Data/EntityUpdate.cs @@ -163,24 +163,26 @@ public void ReadData(IPacket packet) { internal class EntityNetworkData { public DataType Type { get; set; } - public List Data { get; } + public Packet Packet { get; set; } public EntityNetworkData() { - Data = new List(); + Packet = new Packet(); } public void WriteData(IPacket packet) { packet.Write((byte)Type); + + var data = Packet.ToArray(); - if (Data.Count > byte.MaxValue) { + if (data.Length > byte.MaxValue) { Logger.Get().Error(this, "Length of entity network data exceeded max value of byte"); } - var length = (byte)System.Math.Min(Data.Count, byte.MaxValue); + var length = (byte)System.Math.Min(data.Length, byte.MaxValue); packet.Write(length); for (var i = 0; i < length; i++) { - packet.Write(Data[i]); + packet.Write(data[i]); } } @@ -188,14 +190,18 @@ public void ReadData(IPacket packet) { Type = (DataType) packet.ReadByte(); var length = packet.ReadByte(); - + var data = new byte[length]; + for (var i = 0; i < length; i++) { - Data.Add(packet.ReadByte()); + data[i] = packet.ReadByte(); } + + Packet = new Packet(data); } public enum DataType : byte { Fsm = 0, + HealthManager, Rotation } } From ef9ec07cf42177082957a7882c411b1a1037b446 Mon Sep 17 00:00:00 2001 From: Extremelyd1 Date: Fri, 19 Aug 2022 17:40:24 +0200 Subject: [PATCH 010/216] Finish health manager component --- .../Client/Entity/Action/EntityFsmActions.cs | 159 +++++++----------- .../Entity/Component/EntityComponent.cs | 2 + .../Component/HealthManagerComponent.cs | 64 +++++-- HKMP/Game/Client/Entity/Entity.cs | 33 ++-- 4 files changed, 124 insertions(+), 134 deletions(-) diff --git a/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs b/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs index 522d1dd6..e27f00a3 100644 --- a/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs +++ b/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Reflection; -using Hkmp.Networking.Packet; using Hkmp.Networking.Packet.Data; using HutongGames.PlayMaker; using HutongGames.PlayMaker.Actions; @@ -38,13 +37,11 @@ static EntityFsmActions() { return; } - Logger.Get().Info(LogObjectName, $"Parameter type that is subclass: {parameterType}"); SupportedActionTypes.Add(parameterType); if (methodInfo.Name.StartsWith(GetMethodNamePrefix)) { TypeGetMethodInfos.Add(parameterType, methodInfo); } else if (methodInfo.Name.StartsWith(ApplyMethodNamePrefix)) { - Logger.Get().Info(LogObjectName, $"Method info found: {methodInfo}"); TypeApplyMethodInfos.Add(parameterType, methodInfo); } else { throw new Exception("Method was defined that does not adhere to the method naming"); @@ -60,10 +57,10 @@ public static void GetNetworkDataFromAction(EntityNetworkData data, FsmStateActi } methodInfo.Invoke( - null, - StaticNonPublicFlags, - null, - new object[] { data, action }, + null, + StaticNonPublicFlags, + null, + new object[] { data, action }, null! ); } @@ -74,18 +71,14 @@ public static void ApplyNetworkDataFromAction(EntityNetworkData data, FsmStateAc throw new InvalidOperationException( $"Given action type: {action.GetType()} does not have an associated method to apply"); } - - Logger.Get().Info(LogObjectName, $"Apply method: {actionType}, {methodInfo}"); methodInfo.Invoke( - null, + null, StaticNonPublicFlags, - null, - new object[] { data, action }, + null, + new object[] { data, action }, null! ); - - Logger.Get().Info(LogObjectName, $"After methodinfo invoke: {actionType}, {methodInfo}"); } #region SpawnObjectFromGlobalPool @@ -109,58 +102,49 @@ private static void GetNetworkDataFromAction(EntityNetworkData data, SpawnObject data.Packet.Write(rotation.y); data.Packet.Write(rotation.z); } - - Logger.Get().Info(LogObjectName, $"Added SOFGP entity network data: {position.x}, {position.y}, {position.z}"); } private static void ApplyNetworkDataFromAction(EntityNetworkData data, SpawnObjectFromGlobalPool action) { - try { - var position = Vector3.zero; - var euler = Vector3.up; - - var hasSpawnPoint = data.Packet.ReadBool(); - if (hasSpawnPoint) { - var posX = data.Packet.ReadFloat(); - var posY = data.Packet.ReadFloat(); - - Logger.Get().Info(LogObjectName, $"Applying SOFGP entity network data: {posX}, {posY}"); - - position = new Vector3(posX, posY); - - if (!action.position.IsNone) { - position += action.position.Value; - } - - if (!action.rotation.IsNone) { - euler = action.rotation.Value; - } else { - euler = new Vector3( - data.Packet.ReadFloat(), - data.Packet.ReadFloat(), - data.Packet.ReadFloat() - ); - } + var position = Vector3.zero; + var euler = Vector3.up; + + var hasSpawnPoint = data.Packet.ReadBool(); + if (hasSpawnPoint) { + var posX = data.Packet.ReadFloat(); + var posY = data.Packet.ReadFloat(); + + position = new Vector3(posX, posY); + + if (!action.position.IsNone) { + position += action.position.Value; + } + + if (!action.rotation.IsNone) { + euler = action.rotation.Value; } else { - Logger.Get().Info(LogObjectName, "Applying SOFGP entity network data without spawnpoint"); - if (!action.position.IsNone) { - position = action.position.Value; - } - - if (!action.rotation.IsNone) { - euler = action.rotation.Value; - } + euler = new Vector3( + data.Packet.ReadFloat(), + data.Packet.ReadFloat(), + data.Packet.ReadFloat() + ); + } + } else { + if (!action.position.IsNone) { + position = action.position.Value; } - if (action.gameObject != null) { - action.storeObject.Value = action.gameObject.Value.Spawn(position, Quaternion.Euler(euler)); + if (!action.rotation.IsNone) { + euler = action.rotation.Value; } - } catch (Exception e) { - Logger.Get().Info(LogObjectName, $"Apply SOFGP exception: {e.GetType()}, {e.Message}, {e.StackTrace}"); + } + + if (action.gameObject != null) { + action.storeObject.Value = action.gameObject.Value.Spawn(position, Quaternion.Euler(euler)); } } #endregion - + #region FireAtTarget private static void GetNetworkDataFromAction(EntityNetworkData data, FireAtTarget action) { @@ -169,58 +153,35 @@ private static void GetNetworkDataFromAction(EntityNetworkData data, FireAtTarge var position = target.Value.transform.position; data.Packet.Write(position.x); data.Packet.Write(position.y); - - Logger.Get().Info(LogObjectName, $"Added FAT entity network data: {position.x}, {position.y}"); } private static void ApplyNetworkDataFromAction(EntityNetworkData data, FireAtTarget action) { - try { - var posX = data.Packet.ReadFloat(); - var posY = data.Packet.ReadFloat(); + var posX = data.Packet.ReadFloat(); + var posY = data.Packet.ReadFloat(); - Logger.Get().Info(LogObjectName, $"Applying FAT entity network data: {posX}, {posY}"); - - // var selfGameObject = ReflectionHelper.GetField(action, "self"); - - Logger.Get().Info(LogObjectName, $"Action: {action}"); - - var gameObject = action.gameObject; - Logger.Get().Info(LogObjectName, $"GameObject null: {gameObject == null}"); - Logger.Get().Info(LogObjectName, $"GameObject: {gameObject}"); - - var fsm = action.Fsm; - Logger.Get().Info(LogObjectName, $"Action FSM: {fsm}"); - - var ownerDefaultObject = gameObject.GameObject; - Logger.Get().Info(LogObjectName, $"ownerDefaultObject null: {ownerDefaultObject == null}"); - Logger.Get().Info(LogObjectName, $"ownerDefaultObject: {ownerDefaultObject}"); - - var selfGameObject = action.Fsm.GetOwnerDefaultTarget(action.gameObject); - - var selfPosition = selfGameObject.transform.position; - - var rigidBody = selfGameObject.GetComponent(); - if (rigidBody == null) { - return; - } + var selfGameObject = action.Fsm.GetOwnerDefaultTarget(action.gameObject); - var num = Mathf.Atan2( - posY + action.position.Value.y - selfPosition.y, - posX + action.position.Value.x - selfPosition.x - ) * 57.295776f; + var selfPosition = selfGameObject.transform.position; - if (!action.spread.IsNone) { - num += UnityEngine.Random.Range(-action.spread.Value, action.spread.Value); - } + var rigidBody = selfGameObject.GetComponent(); + if (rigidBody == null) { + return; + } + + var num = Mathf.Atan2( + posY + action.position.Value.y - selfPosition.y, + posX + action.position.Value.x - selfPosition.x + ) * 57.295776f; - rigidBody.velocity = new Vector2( - action.speed.Value * Mathf.Cos(num * ((float)System.Math.PI / 180f)), - action.speed.Value * Mathf.Sin(num * ((float)System.Math.PI / 180f)) - ); - } catch (Exception e) { - Logger.Get().Info(LogObjectName, $"Apply FAT exception: {e.GetType()}, {e.Message}, {e.StackTrace}"); + if (!action.spread.IsNone) { + num += UnityEngine.Random.Range(-action.spread.Value, action.spread.Value); } + + rigidBody.velocity = new Vector2( + action.speed.Value * Mathf.Cos(num * ((float)System.Math.PI / 180f)), + action.speed.Value * Mathf.Sin(num * ((float)System.Math.PI / 180f)) + ); } - + #endregion } \ No newline at end of file diff --git a/HKMP/Game/Client/Entity/Component/EntityComponent.cs b/HKMP/Game/Client/Entity/Component/EntityComponent.cs index f3c5d9a0..ad07b0c0 100644 --- a/HKMP/Game/Client/Entity/Component/EntityComponent.cs +++ b/HKMP/Game/Client/Entity/Component/EntityComponent.cs @@ -21,6 +21,8 @@ HostClientPair gameObject _entityId = entityId; GameObject = gameObject; + + IsControlled = true; } protected void SendData(EntityNetworkData data) { diff --git a/HKMP/Game/Client/Entity/Component/HealthManagerComponent.cs b/HKMP/Game/Client/Entity/Component/HealthManagerComponent.cs index b2ff7f75..83f6db35 100644 --- a/HKMP/Game/Client/Entity/Component/HealthManagerComponent.cs +++ b/HKMP/Game/Client/Entity/Component/HealthManagerComponent.cs @@ -1,37 +1,54 @@ -using System; using Hkmp.Networking.Client; using Hkmp.Networking.Packet.Data; using UnityEngine; -namespace Hkmp.Game.Client.Entity.Component; +namespace Hkmp.Game.Client.Entity.Component; // TODO: periodically (or on hit) sync the health of the entity so on scene host transfer we can reset health internal class HealthManagerComponent : EntityComponent { private readonly HostClientPair _healthManager; + private bool _allowDeath; + public HealthManagerComponent( - NetClient netClient, - byte entityId, + NetClient netClient, + byte entityId, HostClientPair gameObject, HostClientPair healthManager ) : base(netClient, entityId, gameObject) { _healthManager = healthManager; - + On.HealthManager.Die += HealthManagerOnDie; } private void HealthManagerOnDie( - On.HealthManager.orig_Die orig, - HealthManager self, - float? attackDirection, - AttackTypes attackType, + On.HealthManager.orig_Die orig, + HealthManager self, + float? attackDirection, + AttackTypes attackType, bool ignoreEvasion ) { - if (IsControlled) { - Logger.Get().Info(this, "Entity's health manager tried dying, cancelled because client"); + if (self != _healthManager.Host && self != _healthManager.Client) { + orig(self, attackDirection, attackType, ignoreEvasion); return; } + if (self == _healthManager.Client) { + if (!_allowDeath) { + Logger.Get().Info(this, "HealthManager Die was called on client entity"); + } else { + Logger.Get().Info(this, "HealthManager Die was called on client entity, but it is allowed death"); + + orig(self, attackDirection, attackType, ignoreEvasion); + + _allowDeath = false; + } + + return; + } + + Logger.Get().Info(this, "HealthManager Die was called on host entity"); + orig(self, attackDirection, attackType, ignoreEvasion); var data = new EntityNetworkData { @@ -45,19 +62,38 @@ bool ignoreEvasion data.Packet.Write(false); } - data.Packet.Write((byte) attackType); + data.Packet.Write((byte)attackType); data.Packet.Write(ignoreEvasion); + + SendData(data); } public override void InitializeHost() { } public override void Update(EntityNetworkData data) { - throw new System.NotImplementedException(); + Logger.Get().Info(this, "Received health manager update"); + + if (!IsControlled) { + Logger.Get().Info(this, " Entity was not controlled"); + return; + } + + var attackDirection = new float?(); + if (data.Packet.ReadBool()) { + attackDirection = data.Packet.ReadFloat(); + } + + var attackType = (AttackTypes)data.Packet.ReadByte(); + var ignoreEvasion = data.Packet.ReadBool(); + + // Set a boolean to indicate that the client health manager is allowed to execute the Die method + _allowDeath = true; + _healthManager.Client.Die(attackDirection, attackType, ignoreEvasion); } public override void Destroy() { - throw new System.NotImplementedException(); + On.HealthManager.Die -= HealthManagerOnDie; } } \ No newline at end of file diff --git a/HKMP/Game/Client/Entity/Entity.cs b/HKMP/Game/Client/Entity/Entity.cs index 372509c7..f0988e25 100644 --- a/HKMP/Game/Client/Entity/Entity.cs +++ b/HKMP/Game/Client/Entity/Entity.cs @@ -151,7 +151,18 @@ private void FindComponents() { var hostHealthManager = _object.Host.GetComponent(); var clientHealthManager = _object.Client.GetComponent(); if (hostHealthManager != null && clientHealthManager != null) { - + Logger.Get().Info(this, $"Adding health manager to entity: {_object.Host.name}"); + var healthManager = new HostClientPair { + Host = hostHealthManager, + Client = clientHealthManager + }; + + _components[EntityNetworkData.DataType.HealthManager] = new HealthManagerComponent( + _netClient, + _entityId, + _object, + healthManager + ); } var climber = _object.Client.GetComponent(); @@ -166,8 +177,6 @@ private void FindComponents() { } private void OnActionEntered(FsmStateAction self) { - Logger.Get().Info(this, $"ActionEntered: {self.GetType()}"); - if (_isControlled) { return; } @@ -284,23 +293,6 @@ public void InitializeHost() { Logger.Get().Info(this, $"Initializing entity '{_object.Host.name}' with active: {_originalIsActive}, sending active: {_lastIsActive}"); - var currentObject = _object.Host; - while (currentObject != null) { - Logger.Get().Info(this, $" Object: {currentObject}, active: {currentObject.activeSelf}"); - - var transform = currentObject.transform; - if (transform == null) { - break; - } - - var parent = transform.parent; - if (parent == null) { - break; - } - - currentObject = parent.gameObject; - } - _netClient.UpdateManager.UpdateEntityIsActive(_entityId, _lastIsActive); _isControlled = false; @@ -397,7 +389,6 @@ public void UpdateIsActive(bool active) { } public void UpdateData(List entityNetworkData) { - Logger.Get().Info(this, $"UpdateData called for entity: {_entityId}"); foreach (var data in entityNetworkData) { if (data.Type == EntityNetworkData.DataType.Fsm) { PlayMakerFSM fsm; From bc79fc615eef3893f128b60b27746d2af010554a Mon Sep 17 00:00:00 2001 From: Extremelyd1 Date: Fri, 19 Aug 2022 18:00:43 +0200 Subject: [PATCH 011/216] Merge master --- HKMP/Game/Client/ClientManager.cs | 2 +- .../Client/Entity/Action/FsmActionHooks.cs | 3 +- .../Component/HealthManagerComponent.cs | 11 ++-- HKMP/Game/Client/Entity/Entity.cs | 32 +++++----- HKMP/Game/Client/Entity/EntityManager.cs | 10 +-- HKMP/Game/Server/ServerManager.cs | 61 +++---------------- HKMP/Networking/Packet/Data/EntityUpdate.cs | 5 +- 7 files changed, 39 insertions(+), 85 deletions(-) diff --git a/HKMP/Game/Client/ClientManager.cs b/HKMP/Game/Client/ClientManager.cs index 7c1d879a..fdf8c61a 100644 --- a/HKMP/Game/Client/ClientManager.cs +++ b/HKMP/Game/Client/ClientManager.cs @@ -545,7 +545,7 @@ private void OnPlayerAlreadyInScene(ClientPlayerAlreadyInScene alreadyInScene) { } foreach (var entityUpdate in alreadyInScene.EntityUpdateList) { - Logger.Get().Info(this, $"Updating already in scene entity with ID: {entityUpdate.Id}"); + Logger.Info($"Updating already in scene entity with ID: {entityUpdate.Id}"); HandleEntityUpdate(entityUpdate, true); } diff --git a/HKMP/Game/Client/Entity/Action/FsmActionHooks.cs b/HKMP/Game/Client/Entity/Action/FsmActionHooks.cs index 2ac8894e..28168d8e 100644 --- a/HKMP/Game/Client/Entity/Action/FsmActionHooks.cs +++ b/HKMP/Game/Client/Entity/Action/FsmActionHooks.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using Hkmp.Logging; using HutongGames.PlayMaker; using MonoMod.RuntimeDetour; @@ -35,7 +36,7 @@ private static void OnActionEntered(Action orig, FsmStateAction orig(self); if (!TypeEvents.TryGetValue(self.GetType(), out var fsmActionHook)) { - Logger.Get().Warn("FsmActionHook", "Hook was fired but no associated hook class was found"); + Logger.Warn("Hook was fired but no associated hook class was found"); return; } diff --git a/HKMP/Game/Client/Entity/Component/HealthManagerComponent.cs b/HKMP/Game/Client/Entity/Component/HealthManagerComponent.cs index 83f6db35..481a38be 100644 --- a/HKMP/Game/Client/Entity/Component/HealthManagerComponent.cs +++ b/HKMP/Game/Client/Entity/Component/HealthManagerComponent.cs @@ -1,6 +1,7 @@ using Hkmp.Networking.Client; using Hkmp.Networking.Packet.Data; using UnityEngine; +using Logger = Hkmp.Logging.Logger; namespace Hkmp.Game.Client.Entity.Component; @@ -35,9 +36,9 @@ bool ignoreEvasion if (self == _healthManager.Client) { if (!_allowDeath) { - Logger.Get().Info(this, "HealthManager Die was called on client entity"); + Logger.Info("HealthManager Die was called on client entity"); } else { - Logger.Get().Info(this, "HealthManager Die was called on client entity, but it is allowed death"); + Logger.Info("HealthManager Die was called on client entity, but it is allowed death"); orig(self, attackDirection, attackType, ignoreEvasion); @@ -47,7 +48,7 @@ bool ignoreEvasion return; } - Logger.Get().Info(this, "HealthManager Die was called on host entity"); + Logger.Info("HealthManager Die was called on host entity"); orig(self, attackDirection, attackType, ignoreEvasion); @@ -73,10 +74,10 @@ public override void InitializeHost() { } public override void Update(EntityNetworkData data) { - Logger.Get().Info(this, "Received health manager update"); + Logger.Info("Received health manager update"); if (!IsControlled) { - Logger.Get().Info(this, " Entity was not controlled"); + Logger.Info(" Entity was not controlled"); return; } diff --git a/HKMP/Game/Client/Entity/Entity.cs b/HKMP/Game/Client/Entity/Entity.cs index b5e28c1b..2b104bdf 100644 --- a/HKMP/Game/Client/Entity/Entity.cs +++ b/HKMP/Game/Client/Entity/Entity.cs @@ -64,7 +64,7 @@ GameObject hostObject _lastIsActive = _object.Host.activeInHierarchy; - Logger.Get().Info(this, $"Entity '{_object.Host.name}' was original active: {_originalIsActive}, last active: {_lastIsActive}"); + Logger.Info($"Entity '{_object.Host.name}' was original active: {_originalIsActive}, last active: {_lastIsActive}"); // Add a position interpolation component to the enemy so we can smooth out position updates _object.Client.AddComponent(); @@ -84,8 +84,7 @@ GameObject hostObject _animationClipNameIds.Add(animationClip.name, (byte)index++); if (index > byte.MaxValue) { - Logger.Get().Error(this, - $"Too many animation clips to fit in a byte for entity: {_object.Client.name}"); + Logger.Error($"Too many animation clips to fit in a byte for entity: {_object.Client.name}"); break; } } @@ -118,7 +117,7 @@ GameObject hostObject } private void ProcessHostFsm(PlayMakerFSM fsm) { - Logger.Get().Info(this, $"Processing host FSM: {fsm.Fsm.Name}"); + Logger.Info($"Processing host FSM: {fsm.Fsm.Name}"); for (var i = 0; i < fsm.FsmStates.Length; i++) { var state = fsm.FsmStates[i]; @@ -136,7 +135,7 @@ private void ProcessHostFsm(PlayMakerFSM fsm) { StateIndex = i, ActionIndex = j }; - Logger.Get().Info(this, $"Created hooked action: {action.GetType()}, {_fsms.Host.IndexOf(fsm)}, {i}, {j}"); + Logger.Info($"Created hooked action: {action.GetType()}, {_fsms.Host.IndexOf(fsm)}, {i}, {j}"); FsmActionHooks.RegisterFsmStateActionType(action.GetType(), OnActionEntered); } @@ -144,7 +143,7 @@ private void ProcessHostFsm(PlayMakerFSM fsm) { } private void ProcessClientFsm(PlayMakerFSM fsm) { - Logger.Get().Info(this, $"Processing client FSM: {fsm.Fsm.Name}"); + Logger.Info( $"Processing client FSM: {fsm.Fsm.Name}"); fsm.enabled = false; } @@ -152,7 +151,7 @@ private void FindComponents() { var hostHealthManager = _object.Host.GetComponent(); var clientHealthManager = _object.Client.GetComponent(); if (hostHealthManager != null && clientHealthManager != null) { - Logger.Get().Info(this, $"Adding health manager to entity: {_object.Host.name}"); + Logger.Info($"Adding health manager to entity: {_object.Host.name}"); var healthManager = new HostClientPair { Host = hostHealthManager, Client = clientHealthManager @@ -186,7 +185,7 @@ private void OnActionEntered(FsmStateAction self) { return; } - Logger.Get().Info(this, $"Hooked action was entered: {hookedEntityAction.FsmIndex}, {hookedEntityAction.StateIndex}, {hookedEntityAction.ActionIndex}"); + Logger.Info($"Hooked action was entered: {hookedEntityAction.FsmIndex}, {hookedEntityAction.StateIndex}, {hookedEntityAction.ActionIndex}"); var networkData = new EntityNetworkData { Type = EntityNetworkData.DataType.Fsm @@ -213,7 +212,7 @@ private void OnUpdate() { if (_isControlled) { if (hostObjectActive) { - Logger.Get().Info(this, $"Entity '{_object.Host.name}' host object became active, re-disabling"); + Logger.Info($"Entity '{_object.Host.name}' host object became active, re-disabling"); _object.Host.SetActive(false); } @@ -246,7 +245,7 @@ private void OnUpdate() { if (newActive != _lastIsActive) { _lastIsActive = newActive; - Logger.Get().Info(this, $"Entity '{_object.Host.name}' changed active: {newActive}"); + Logger.Info($"Entity '{_object.Host.name}' changed active: {newActive}"); _netClient.UpdateManager.UpdateEntityIsActive( _entityId, @@ -273,7 +272,7 @@ float overrideFps } if (!_animationClipNameIds.TryGetValue(clip.name, out var animationId)) { - Logger.Get().Warn(this, $"Entity '{_object.Client.name}' played unknown animation: {clip.name}"); + Logger.Warn($"Entity '{_object.Client.name}' played unknown animation: {clip.name}"); return; } @@ -292,7 +291,7 @@ public void InitializeHost() { // Otherwise we might trigger the update sending of activity twice _lastIsActive = _object.Host.activeInHierarchy; - Logger.Get().Info(this, $"Initializing entity '{_object.Host.name}' with active: {_originalIsActive}, sending active: {_lastIsActive}"); + Logger.Info($"Initializing entity '{_object.Host.name}' with active: {_originalIsActive}, sending active: {_lastIsActive}"); _netClient.UpdateManager.UpdateEntityIsActive(_entityId, _lastIsActive); @@ -341,13 +340,12 @@ public void UpdateScale(bool scale) { public void UpdateAnimation(byte animationId, tk2dSpriteAnimationClip.WrapMode wrapMode, bool alreadyInSceneUpdate) { if (_animator.Client == null) { - Logger.Get().Warn(this, - $"Entity '{_object.Client.name}' received animation while client animator does not exist"); + Logger.Warn($"Entity '{_object.Client.name}' received animation while client animator does not exist"); return; } if (!_animationClipNameIds.TryGetValue(animationId, out var clipName)) { - Logger.Get().Warn(this, $"Entity '{_object.Client.name}' received unknown animation ID: {animationId}"); + Logger.Warn($"Entity '{_object.Client.name}' received unknown animation ID: {animationId}"); return; } @@ -385,7 +383,7 @@ public void UpdateAnimation(byte animationId, tk2dSpriteAnimationClip.WrapMode w } public void UpdateIsActive(bool active) { - Logger.Get().Info(this, $"Entity '{_object.Client.name}' received active: {active}"); + Logger.Info($"Entity '{_object.Client.name}' received active: {active}"); _object.Client.SetActive(active); } @@ -419,7 +417,7 @@ public void UpdateData(List entityNetworkData) { actionIndex = data.Packet.ReadByte(); } - Logger.Get().Info(this, $"Received entity network data for FSM: {fsm.Fsm.Name}, {stateIndex}, {actionIndex}"); + Logger.Info($"Received entity network data for FSM: {fsm.Fsm.Name}, {stateIndex}, {actionIndex}"); var state = fsm.FsmStates[stateIndex]; var action = state.Actions[actionIndex]; diff --git a/HKMP/Game/Client/Entity/EntityManager.cs b/HKMP/Game/Client/Entity/EntityManager.cs index 8bdf80c4..cf488794 100644 --- a/HKMP/Game/Client/Entity/EntityManager.cs +++ b/HKMP/Game/Client/Entity/EntityManager.cs @@ -35,7 +35,7 @@ public EntityManager(NetClient netClient) { } public void InitializeSceneHost() { - Logger.Get().Info(this, "Releasing control of all registered entities"); + Logger.Info("Releasing control of all registered entities"); _isSceneHost = true; @@ -45,13 +45,13 @@ public void InitializeSceneHost() { } public void InitializeSceneClient() { - Logger.Get().Info(this, "Taking control of all registered entities"); + Logger.Info("Taking control of all registered entities"); _isSceneHost = false; } public void BecomeSceneHost() { - Logger.Get().Info(this, "Becoming scene host"); + Logger.Info("Becoming scene host"); _isSceneHost = true; @@ -153,7 +153,7 @@ private void OnSceneChanged(Scene oldScene, Scene newScene) { continue; } - Logger.Get().Info(this, $"Registering entity '{fsmGameObjectName}' with ID '{_lastId}'"); + Logger.Info($"Registering entity '{fsmGameObjectName}' with ID '{_lastId}'"); _entities[_lastId] = new Entity( _netClient, @@ -166,7 +166,7 @@ private void OnSceneChanged(Scene oldScene, Scene newScene) { // Find all Climber components foreach (var climber in Object.FindObjectsOfType()) { - Logger.Get().Info(this, $"Registering entity '{climber.name}' with ID '{_lastId}'"); + Logger.Info($"Registering entity '{climber.name}' with ID '{_lastId}'"); _entities[_lastId] = new Entity( _netClient, diff --git a/HKMP/Game/Server/ServerManager.cs b/HKMP/Game/Server/ServerManager.cs index 2613045c..38c19c2e 100644 --- a/HKMP/Game/Server/ServerManager.cs +++ b/HKMP/Game/Server/ServerManager.cs @@ -6,7 +6,6 @@ using Hkmp.Api.Server; using Hkmp.Concurrency; using Hkmp.Eventing; -using Hkmp.Game.Client.Entity; using Hkmp.Game.Command.Server; using Hkmp.Game.Server.Auth; using Hkmp.Networking.Packet; @@ -354,7 +353,7 @@ private void OnClientEnterScene(ServerPlayerData playerData) { continue; } - Logger.Get().Info(this, $"Sending that entity '{entityKey.EntityId}' is already in scene to '{playerData.Id}'"); + Logger.Info($"Sending that entity '{entityKey.EntityId}' is already in scene to '{playerData.Id}'"); var entityData = keyDataPair.Value; var entityUpdate = new EntityUpdate { @@ -400,33 +399,7 @@ private void OnClientLeaveScene(ushort id) { return; } - var sceneName = playerData.CurrentScene; - - if (sceneName.Length == 0) { - Logger.Info($"Received LeaveScene data from ID {id}, but there was no last scene registered"); - return; - } - - Logger.Info($"Received LeaveScene data from ID {id}, last scene: {sceneName}"); - - playerData.CurrentScene = ""; - - foreach (var idPlayerDataPair in _playerData.GetCopy()) { - // Skip source player - if (idPlayerDataPair.Key == id) { - continue; - } - - var otherPlayerData = idPlayerDataPair.Value; - - // Send the packet to all clients on the scene that the player left - // to indicate that this client has left their scene - if (otherPlayerData.CurrentScene.Equals(sceneName)) { - Logger.Info($"Sending leave scene packet to {idPlayerDataPair.Key}"); - - _netServer.GetUpdateManagerForClient(idPlayerDataPair.Key)?.AddPlayerLeaveSceneData(id); - } - } + HandlePlayerLeaveScene(id, false); try { PlayerLeaveSceneEvent?.Invoke(playerData); @@ -659,18 +632,18 @@ public void InternalDisconnectPlayer(ushort id, DisconnectReason reason) { /// Whether the disconnect was due to connection timeout. private void HandlePlayerLeaveScene(ushort id, bool disconnected, bool timeout = false) { if (!_playerData.TryGetValue(id, out var playerData)) { - Logger.Get().Warn(this, $"Handling player leave scene (dc: {disconnected}) for ID {id}, but player is not in mapping"); + Logger.Warn($"Handling player leave scene (dc: {disconnected}) for ID {id}, but player is not in mapping"); return; } var sceneName = playerData.CurrentScene; if (!disconnected && sceneName.Length == 0) { - Logger.Get().Info(this, $"Handling player leave scene for ID {id}, but there was no last scene registered"); + Logger.Info($"Handling player leave scene for ID {id}, but there was no last scene registered"); return; } - Logger.Get().Info(this, $"Handling player leave scene (dc: {disconnected}) for ID {id}, last scene: {sceneName}"); + Logger.Info($"Handling player leave scene (dc: {disconnected}) for ID {id}, last scene: {sceneName}"); playerData.CurrentScene = ""; @@ -689,7 +662,7 @@ private void HandlePlayerLeaveScene(ushort id, bool disconnected, bool timeout = // Send a packet to all clients in the scene that the player has left their scene if (otherPlayerData.CurrentScene == sceneName) { - Logger.Get().Info(this, $"Sending leave scene packet to {idPlayerDataPair.Key}"); + Logger.Info($"Sending leave scene packet to {idPlayerDataPair.Key}"); // We have now found at least one player that is still in this scene isSceneNowEmpty = false; @@ -708,7 +681,7 @@ private void HandlePlayerLeaveScene(ushort id, bool disconnected, bool timeout = // Also set the player data of the new scene host otherPlayerData.IsSceneHost = true; - Logger.Get().Info(this, $" {idPlayerDataPair.Key} has become scene host"); + Logger.Info($" {idPlayerDataPair.Key} has become scene host"); } if (disconnected) { @@ -745,26 +718,6 @@ private void HandlePlayerLeaveScene(ushort id, bool disconnected, bool timeout = _playerData.Remove(id); } } - - /// - /// Callback method for when a player leaves a scene. - /// - /// The ID of the player. - private void OnClientLeaveScene(ushort id) { - if (!_playerData.TryGetValue(id, out var playerData)) { - Logger.Get().Warn(this, $"Received LeaveScene data from {id}, but player is not in mapping"); - return; - } - - HandlePlayerLeaveScene(id, false); - - try { - PlayerLeaveSceneEvent?.Invoke(playerData); - } catch (Exception e) { - Logger.Get().Warn(this, - $"Exception thrown while invoking PlayerLeaveScene event, {e.GetType()}, {e.Message}, {e.StackTrace}"); - } - } /// /// Process a disconnect for the player with the given ID. diff --git a/HKMP/Networking/Packet/Data/EntityUpdate.cs b/HKMP/Networking/Packet/Data/EntityUpdate.cs index 7236128b..bbec207e 100644 --- a/HKMP/Networking/Packet/Data/EntityUpdate.cs +++ b/HKMP/Networking/Packet/Data/EntityUpdate.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using Hkmp.Logging; using Hkmp.Math; namespace Hkmp.Networking.Packet.Data { @@ -99,7 +100,7 @@ public void WriteData(IPacket packet) { if (UpdateTypes.Contains(EntityUpdateType.Data)) { if (GenericData.Count > byte.MaxValue) { - Logger.Get().Error(this, "Length of entity network data instances exceeded max value of byte"); + Logger.Error("Length of entity network data instances exceeded max value of byte"); } var length = (byte)System.Math.Min(GenericData.Count, byte.MaxValue); @@ -175,7 +176,7 @@ public void WriteData(IPacket packet) { var data = Packet.ToArray(); if (data.Length > byte.MaxValue) { - Logger.Get().Error(this, "Length of entity network data exceeded max value of byte"); + Logger.Error("Length of entity network data exceeded max value of byte"); } var length = (byte)System.Math.Min(data.Length, byte.MaxValue); From 705af97b08954f093c813ba4ffb9e9d2398ec5a3 Mon Sep 17 00:00:00 2001 From: Extremelyd1 Date: Fri, 19 Aug 2022 18:01:02 +0200 Subject: [PATCH 012/216] Remove false knight file from master merge --- HKMP/Game/Client/Entity/FalseKnight.cs | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 HKMP/Game/Client/Entity/FalseKnight.cs diff --git a/HKMP/Game/Client/Entity/FalseKnight.cs b/HKMP/Game/Client/Entity/FalseKnight.cs deleted file mode 100644 index e69de29b..00000000 From ada4d3e524cab1ed8b1bd53a0c082ce23e676824 Mon Sep 17 00:00:00 2001 From: Extremelyd1 Date: Fri, 19 Aug 2022 23:05:09 +0200 Subject: [PATCH 013/216] Implement collider component, fix animations --- .../Entity/Component/ColliderComponent.cs | 68 +++++++++++++++++++ HKMP/Game/Client/Entity/Entity.cs | 45 +++++++++++- HKMP/Game/Server/ServerManager.cs | 19 ++++-- HKMP/Networking/Packet/Data/EntityUpdate.cs | 3 +- 4 files changed, 124 insertions(+), 11 deletions(-) create mode 100644 HKMP/Game/Client/Entity/Component/ColliderComponent.cs diff --git a/HKMP/Game/Client/Entity/Component/ColliderComponent.cs b/HKMP/Game/Client/Entity/Component/ColliderComponent.cs new file mode 100644 index 00000000..aae662c4 --- /dev/null +++ b/HKMP/Game/Client/Entity/Component/ColliderComponent.cs @@ -0,0 +1,68 @@ +using Hkmp.Networking.Client; +using Hkmp.Networking.Packet.Data; +using Hkmp.Util; +using UnityEngine; +using Logger = Hkmp.Logging.Logger; + +namespace Hkmp.Game.Client.Entity.Component; + +internal class ColliderComponent : EntityComponent { + private readonly HostClientPair _collider; + + private bool? _lastEnabled; + + public ColliderComponent( + NetClient netClient, + byte entityId, + HostClientPair gameObject, + HostClientPair collider + ) : base(netClient, entityId, gameObject) { + _collider = collider; + + MonoBehaviourUtil.Instance.OnUpdateEvent += OnUpdateCollider; + } + + private void OnUpdateCollider() { + if (IsControlled) { + return; + } + + if (_collider.Host == null) { + return; + } + + var newEnabled = _collider.Host.enabled; + if (!_lastEnabled.HasValue || newEnabled != _lastEnabled.Value) { + Logger.Info($"Collider of {GameObject.Host.name} enabled changed to: {newEnabled}"); + _lastEnabled = newEnabled; + + var data = new EntityNetworkData { + Type = EntityNetworkData.DataType.Collider + }; + data.Packet.Write(newEnabled); + + SendData(data); + } + } + + public override void InitializeHost() { + } + + public override void Update(EntityNetworkData data) { + Logger.Info($"Received collider update for {GameObject.Client.name}"); + + if (!IsControlled) { + Logger.Info(" Entity was not controlled"); + return; + } + + var enabled = data.Packet.ReadBool(); + _collider.Client.enabled = enabled; + + Logger.Info($" Enabled: {enabled}"); + } + + public override void Destroy() { + MonoBehaviourUtil.Instance.OnUpdateEvent -= OnUpdateCollider; + } +} \ No newline at end of file diff --git a/HKMP/Game/Client/Entity/Entity.cs b/HKMP/Game/Client/Entity/Entity.cs index 2b104bdf..e46de970 100644 --- a/HKMP/Game/Client/Entity/Entity.cs +++ b/HKMP/Game/Client/Entity/Entity.cs @@ -38,6 +38,8 @@ internal class Entity { private Vector3 _lastScale; private bool _lastIsActive; + private bool _allowClientAnimation; + public Entity( NetClient netClient, byte entityId, @@ -151,7 +153,6 @@ private void FindComponents() { var hostHealthManager = _object.Host.GetComponent(); var clientHealthManager = _object.Client.GetComponent(); if (hostHealthManager != null && clientHealthManager != null) { - Logger.Info($"Adding health manager to entity: {_object.Host.name}"); var healthManager = new HostClientPair { Host = hostHealthManager, Client = clientHealthManager @@ -174,6 +175,24 @@ private void FindComponents() { climber ); } + + var hostCollider = _object.Host.GetComponent(); + var clientCollider = _object.Client.GetComponent(); + if (hostCollider != null && clientCollider != null) { + Logger.Info($"Adding collider component to entity: {_object.Host.name}"); + + var collider = new HostClientPair { + Host = hostCollider, + Client = clientCollider + }; + + _components[EntityNetworkData.DataType.Collider] = new ColliderComponent( + _netClient, + _entityId, + _object, + collider + ); + } } private void OnActionEntered(FsmStateAction self) { @@ -261,6 +280,20 @@ private void OnAnimationPlayed( float clipStartTime, float overrideFps ) { + if (self == _animator.Client) { + if (!_allowClientAnimation) { + Logger.Info($"Entity '{_object.Client.name}' client animator tried playing animation"); + } else { + Logger.Info($"Entity '{_object.Client.name}' client animator was allowed to play animation"); + + orig(self, clip, clipStartTime, overrideFps); + + _allowClientAnimation = false; + } + + return; + } + orig(self, clip, clipStartTime, overrideFps); if (self != _animator.Host) { @@ -276,7 +309,7 @@ float overrideFps return; } - // Logger.Get().Info(this, $"Entity '{_gameObject.name}' sends animation: {clip.name}, {animationId}, {clip.wrapMode}"); + Logger.Info($"Entity '{_object.Host.name}' sends animation: {clip.name}, {animationId}, {clip.wrapMode}"); _netClient.UpdateManager.UpdateEntityAnimation( _entityId, animationId, @@ -349,7 +382,11 @@ public void UpdateAnimation(byte animationId, tk2dSpriteAnimationClip.WrapMode w return; } - // Logger.Get().Info(this, $"Entity '{_gameObject.name}' received animation: {animationId}, {clipName}, {wrapMode}"); + Logger.Info($"Entity '{_object.Client.name}' received animation: {animationId}, {clipName}, {wrapMode}"); + + // All paths lead to calling the Play method of the sprite animator that is hooked, so we allow the call + // through the hook + _allowClientAnimation = true; if (alreadyInSceneUpdate) { // Since this is an animation update from an entity that was already present in a scene, @@ -374,6 +411,8 @@ public void UpdateAnimation(byte animationId, tk2dSpriteAnimationClip.WrapMode w // so we emulate that by only "playing" the last frame of the clip var clipLength = clip.frames.Length; _animator.Client.PlayFromFrame(clipName, clipLength - 1); + + Logger.Info($" Played animation: {clipName}, {clipLength - 1} on {_animator.Client.name}, {_animator.Client.GetHashCode()}"); return; } } diff --git a/HKMP/Game/Server/ServerManager.cs b/HKMP/Game/Server/ServerManager.cs index 38c19c2e..ef1ec4a6 100644 --- a/HKMP/Game/Server/ServerManager.cs +++ b/HKMP/Game/Server/ServerManager.cs @@ -585,14 +585,19 @@ private void OnEntityUpdate(ushort id, EntityUpdate entityUpdate) { } ); + void ReplaceExistingDataWithSameType(EntityNetworkData.DataType type, Packet data) { + var existingData = entityData.GenericData.Find( + d => d.Type == type + ); + if (existingData != null) { + existingData.Packet = data; + } + } + foreach (var updateData in entityUpdate.GenericData) { - if (updateData.Type == EntityNetworkData.DataType.Rotation) { - var existingData = entityData.GenericData.Find( - d => d.Type == EntityNetworkData.DataType.Rotation - ); - if (existingData != null) { - existingData.Packet = updateData.Packet; - } + if (updateData.Type == EntityNetworkData.DataType.Rotation + || updateData.Type == EntityNetworkData.DataType.Collider) { + ReplaceExistingDataWithSameType(updateData.Type, updateData.Packet); } } } diff --git a/HKMP/Networking/Packet/Data/EntityUpdate.cs b/HKMP/Networking/Packet/Data/EntityUpdate.cs index bbec207e..a264821d 100644 --- a/HKMP/Networking/Packet/Data/EntityUpdate.cs +++ b/HKMP/Networking/Packet/Data/EntityUpdate.cs @@ -203,7 +203,8 @@ public void ReadData(IPacket packet) { public enum DataType : byte { Fsm = 0, HealthManager, - Rotation + Rotation, + Collider } } From d7eaebe6c5f6a8410d366dd54309d9085aad2c8f Mon Sep 17 00:00:00 2001 From: Extremelyd1 Date: Sat, 20 Aug 2022 20:52:29 +0200 Subject: [PATCH 014/216] Add support for a few enemies, fix FSM action hook --- .../Client/Entity/Action/EntityFsmActions.cs | 218 ++++++++++++++---- .../Component/HealthManagerComponent.cs | 2 + HKMP/Game/Client/Entity/Entity.cs | 19 +- HKMP/Game/Client/Entity/EntityManager.cs | 10 +- 4 files changed, 197 insertions(+), 52 deletions(-) diff --git a/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs b/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs index e27f00a3..e9a552e1 100644 --- a/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs +++ b/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs @@ -5,12 +5,11 @@ using HutongGames.PlayMaker; using HutongGames.PlayMaker.Actions; using UnityEngine; +using Logger = Hkmp.Logging.Logger; namespace Hkmp.Game.Client.Entity.Action; internal static class EntityFsmActions { - private const string LogObjectName = "Hkmp.Game.Client.Entity.Action.EntityFsmActions"; - private const string GetMethodNamePrefix = "Get"; private const string ApplyMethodNamePrefix = "Apply"; @@ -49,20 +48,23 @@ static EntityFsmActions() { } } - public static void GetNetworkDataFromAction(EntityNetworkData data, FsmStateAction action) { + 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"); } - methodInfo.Invoke( + 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; } public static void ApplyNetworkDataFromAction(EntityNetworkData data, FsmStateAction action) { @@ -72,49 +74,35 @@ public static void ApplyNetworkDataFromAction(EntityNetworkData data, FsmStateAc $"Given action type: {action.GetType()} does not have an associated method to apply"); } - methodInfo.Invoke( - null, - StaticNonPublicFlags, - null, - new object[] { data, action }, - null! - ); + 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; + } + } } #region SpawnObjectFromGlobalPool - private static void GetNetworkDataFromAction(EntityNetworkData data, SpawnObjectFromGlobalPool action) { - var spawnPoint = action.spawnPoint; - if (spawnPoint == null) { - data.Packet.Write(false); - return; - } - - data.Packet.Write(true); - - var position = spawnPoint.Value.transform.position; - data.Packet.Write(position.x); - data.Packet.Write(position.y); - - if (action.rotation.IsNone) { - var rotation = spawnPoint.Value.transform.eulerAngles; - data.Packet.Write(rotation.x); - data.Packet.Write(rotation.y); - data.Packet.Write(rotation.z); - } - } - - private static void ApplyNetworkDataFromAction(EntityNetworkData data, SpawnObjectFromGlobalPool action) { + private static bool GetNetworkDataFromAction(EntityNetworkData data, SpawnObjectFromGlobalPool action) { var position = Vector3.zero; var euler = Vector3.up; - var hasSpawnPoint = data.Packet.ReadBool(); - if (hasSpawnPoint) { - var posX = data.Packet.ReadFloat(); - var posY = data.Packet.ReadFloat(); - - position = new Vector3(posX, posY); - + var spawnPoint = action.spawnPoint.Value; + if (spawnPoint != null) { + position = spawnPoint.transform.position; if (!action.position.IsNone) { position += action.position.Value; } @@ -122,11 +110,7 @@ private static void ApplyNetworkDataFromAction(EntityNetworkData data, SpawnObje if (!action.rotation.IsNone) { euler = action.rotation.Value; } else { - euler = new Vector3( - data.Packet.ReadFloat(), - data.Packet.ReadFloat(), - data.Packet.ReadFloat() - ); + euler = spawnPoint.transform.eulerAngles; } } else { if (!action.position.IsNone) { @@ -138,6 +122,29 @@ private static void ApplyNetworkDataFromAction(EntityNetworkData data, SpawnObje } } + data.Packet.Write(position.x); + data.Packet.Write(position.y); + data.Packet.Write(position.z); + + data.Packet.Write(euler.x); + data.Packet.Write(euler.y); + data.Packet.Write(euler.z); + + return true; + } + + private static void ApplyNetworkDataFromAction(EntityNetworkData data, SpawnObjectFromGlobalPool action) { + var position = new Vector3( + data.Packet.ReadFloat(), + data.Packet.ReadFloat(), + data.Packet.ReadFloat() + ); + var euler = new Vector3( + data.Packet.ReadFloat(), + data.Packet.ReadFloat(), + data.Packet.ReadFloat() + ); + if (action.gameObject != null) { action.storeObject.Value = action.gameObject.Value.Spawn(position, Quaternion.Euler(euler)); } @@ -147,12 +154,14 @@ private static void ApplyNetworkDataFromAction(EntityNetworkData data, SpawnObje #region FireAtTarget - private static void GetNetworkDataFromAction(EntityNetworkData data, FireAtTarget action) { + private static bool GetNetworkDataFromAction(EntityNetworkData data, FireAtTarget action) { var target = action.target; var position = target.Value.transform.position; data.Packet.Write(position.x); data.Packet.Write(position.y); + + return true; } private static void ApplyNetworkDataFromAction(EntityNetworkData data, FireAtTarget action) { @@ -184,4 +193,121 @@ private static void ApplyNetworkDataFromAction(EntityNetworkData data, FireAtTar } #endregion + + #region SetScale + + private static bool GetNetworkDataFromAction(EntityNetworkData data, SetScale action) { + var gameObject = action.Fsm.GetOwnerDefaultTarget(action.gameObject); + if (gameObject == action.Fsm.GameObject) { + return false; + } + + var scale = action.vector.IsNone ? gameObject.transform.localScale : action.vector.Value; + if (!action.x.IsNone) { + scale.x = action.x.Value; + } + + if (!action.y.IsNone) { + scale.y = action.y.Value; + } + + if (!action.z.IsNone) { + scale.z = action.z.Value; + } + + data.Packet.Write(scale.x); + data.Packet.Write(scale.y); + data.Packet.Write(scale.z); + + return true; + } + + private static void ApplyNetworkDataFromAction(EntityNetworkData data, SetScale action) { + var gameObject = action.Fsm.GetOwnerDefaultTarget(action.gameObject); + if (gameObject == action.Fsm.GameObject) { + return; + } + + var scale = new Vector3( + data.Packet.ReadFloat(), + data.Packet.ReadFloat(), + data.Packet.ReadFloat() + ); + + gameObject.transform.localScale = scale; + } + + #endregion + + #region SetFsmBool + + private static bool GetNetworkDataFromAction(EntityNetworkData data, SetFsmBool action) { + // TODO: if action.setValue can be a reference, make sure to network it + return true; + } + + private static void ApplyNetworkDataFromAction(EntityNetworkData data, SetFsmBool action) { + if (action.setValue == null) { + return; + } + + var gameObject = action.Fsm.GetOwnerDefaultTarget(action.gameObject); + if (gameObject == action.Fsm.GameObject) { + return; + } + + if (gameObject == null) { + return; + } + + var fsm = ActionHelpers.GetGameObjectFsm(gameObject, action.fsmName.Value); + if (fsm == null) { + return; + } + + var fsmBool = fsm.FsmVariables.FindFsmBool(action.variableName.Value); + if (fsmBool == null) { + return; + } + + fsmBool.Value = action.setValue.Value; + } + + #endregion + + #region SetFsmFloat + + private static bool GetNetworkDataFromAction(EntityNetworkData data, SetFsmFloat action) { + // TODO: if action.setValue can be a reference, make sure to network it + return true; + } + + private static void ApplyNetworkDataFromAction(EntityNetworkData data, SetFsmFloat action) { + if (action.setValue == null) { + return; + } + + var gameObject = action.Fsm.GetOwnerDefaultTarget(action.gameObject); + if (gameObject == action.Fsm.GameObject) { + return; + } + + if (gameObject == null) { + return; + } + + var fsm = ActionHelpers.GetGameObjectFsm(gameObject, action.fsmName.Value); + if (fsm == null) { + return; + } + + var fsmFloat = fsm.FsmVariables.GetFsmFloat(action.variableName.Value); + if (fsmFloat == null) { + return; + } + + fsmFloat.Value = action.setValue.Value; + } + + #endregion } \ No newline at end of file diff --git a/HKMP/Game/Client/Entity/Component/HealthManagerComponent.cs b/HKMP/Game/Client/Entity/Component/HealthManagerComponent.cs index 481a38be..a0c3334d 100644 --- a/HKMP/Game/Client/Entity/Component/HealthManagerComponent.cs +++ b/HKMP/Game/Client/Entity/Component/HealthManagerComponent.cs @@ -5,6 +5,8 @@ namespace Hkmp.Game.Client.Entity.Component; +// TODO: make sure that the data sent on death is saved as state on the server, so new clients entering +// scenes can start with the entity disabled/already dead // TODO: periodically (or on hit) sync the health of the entity so on scene host transfer we can reset health internal class HealthManagerComponent : EntityComponent { private readonly HostClientPair _healthManager; diff --git a/HKMP/Game/Client/Entity/Entity.cs b/HKMP/Game/Client/Entity/Entity.cs index e46de970..ff533660 100644 --- a/HKMP/Game/Client/Entity/Entity.cs +++ b/HKMP/Game/Client/Entity/Entity.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Linq; using Hkmp.Collection; @@ -29,6 +30,7 @@ internal class Entity { private readonly Dictionary _components; private readonly Dictionary _hookedActions; + private readonly HashSet _hookedTypes; private readonly bool _originalIsActive; @@ -100,6 +102,7 @@ GameObject hostObject }; _hookedActions = new Dictionary(); + _hookedTypes = new HashSet(); foreach (var fsm in _fsms.Host) { ProcessHostFsm(fsm); } @@ -139,7 +142,11 @@ private void ProcessHostFsm(PlayMakerFSM fsm) { }; Logger.Info($"Created hooked action: {action.GetType()}, {_fsms.Host.IndexOf(fsm)}, {i}, {j}"); - FsmActionHooks.RegisterFsmStateActionType(action.GetType(), OnActionEntered); + if (!_hookedTypes.Contains(action.GetType())) { + _hookedTypes.Add(action.GetType()); + + FsmActionHooks.RegisterFsmStateActionType(action.GetType(), OnActionEntered); + } } } } @@ -216,10 +223,12 @@ private void OnActionEntered(FsmStateAction self) { networkData.Packet.Write((byte) hookedEntityAction.StateIndex); networkData.Packet.Write((byte) hookedEntityAction.ActionIndex); - - EntityFsmActions.GetNetworkDataFromAction(networkData, self); - - _netClient.UpdateManager.AddEntityData(_entityId, networkData); + + // Only if the GetNetworkDataFromAction method returns true do we add the entity data + // for sending + if (EntityFsmActions.GetNetworkDataFromAction(networkData, self)) { + _netClient.UpdateManager.AddEntityData(_entityId, networkData); + } } private void OnUpdate() { diff --git a/HKMP/Game/Client/Entity/EntityManager.cs b/HKMP/Game/Client/Entity/EntityManager.cs index cf488794..0695823a 100644 --- a/HKMP/Game/Client/Entity/EntityManager.cs +++ b/HKMP/Game/Client/Entity/EntityManager.cs @@ -8,13 +8,21 @@ namespace Hkmp.Game.Client.Entity { internal class EntityManager { + /// + /// Dictionary that maps all FSM names to game object names for all valid entities. + /// Valid entities are entities that should be managed by the entity system. + /// private readonly Dictionary _validEntityFsms = new() { { "Crawler", "Crawler" }, { "chaser", "Buzzer" }, { "Zombie Swipe", "Zombie Runner" }, { "Bouncer Control", "Fly" }, { "BG Control", "Battle Gate" }, - { "spitter", "Spitter" } + { "spitter", "Spitter" }, + { "Zombie Guard", "Zombie Guard" }, + { "Zombie Leap", "Zombie Leaper" }, + { "Hatcher", "Hatcher" }, + { "Control", "Hatcher Baby Spawner" } }; private readonly NetClient _netClient; From f9aae01a85875b2c6e725b8463102ed436d7e99d Mon Sep 17 00:00:00 2001 From: Extremelyd1 Date: Thu, 25 Aug 2022 19:01:13 +0200 Subject: [PATCH 015/216] Handle entity deaths on scene enter, update workflow --- .github/workflows/dotnet.yml | 2 +- HKMP/Game/Client/Entity/Entity.cs | 104 +++++++++++++---------- HKMP/Game/Client/Entity/EntityManager.cs | 4 + 3 files changed, 66 insertions(+), 44 deletions(-) diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index cbe76540..a39fc40a 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/um6xy7.gpg -O deps.zip.gpg + run: wget https://files.catbox.moe/2ibmgp.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 diff --git a/HKMP/Game/Client/Entity/Entity.cs b/HKMP/Game/Client/Entity/Entity.cs index ff533660..caaea7fd 100644 --- a/HKMP/Game/Client/Entity/Entity.cs +++ b/HKMP/Game/Client/Entity/Entity.cs @@ -22,7 +22,7 @@ internal class Entity { private readonly HostClientPair _object; private readonly HostClientPair _animator; - + private readonly BiLookup _animationClipNameIds; private readonly HostClientPair> _fsms; @@ -35,7 +35,7 @@ internal class Entity { private readonly bool _originalIsActive; private bool _isControlled; - + private Vector3 _lastPosition; private Vector3 _lastScale; private bool _lastIsActive; @@ -67,8 +67,9 @@ GameObject hostObject _object.Host.SetActive(false); _lastIsActive = _object.Host.activeInHierarchy; - - Logger.Info($"Entity '{_object.Host.name}' was original active: {_originalIsActive}, last active: {_lastIsActive}"); + + Logger.Info( + $"Entity '{_object.Host.name}' was original active: {_originalIsActive}, last active: {_lastIsActive}"); // Add a position interpolation component to the enemy so we can smooth out position updates _object.Client.AddComponent(); @@ -100,7 +101,7 @@ GameObject hostObject Host = _object.Host.GetComponents().ToList(), Client = _object.Client.GetComponents().ToList() }; - + _hookedActions = new Dictionary(); _hookedTypes = new HashSet(); foreach (var fsm in _fsms.Host) { @@ -116,7 +117,7 @@ GameObject hostObject foreach (var fsm in _fsms.Client) { ProcessClientFsm(fsm); } - + _components = new Dictionary(); FindComponents(); } @@ -144,7 +145,7 @@ private void ProcessHostFsm(PlayMakerFSM fsm) { if (!_hookedTypes.Contains(action.GetType())) { _hookedTypes.Add(action.GetType()); - + FsmActionHooks.RegisterFsmStateActionType(action.GetType(), OnActionEntered); } } @@ -152,10 +153,10 @@ private void ProcessHostFsm(PlayMakerFSM fsm) { } private void ProcessClientFsm(PlayMakerFSM fsm) { - Logger.Info( $"Processing client FSM: {fsm.Fsm.Name}"); + Logger.Info($"Processing client FSM: {fsm.Fsm.Name}"); fsm.enabled = false; } - + private void FindComponents() { var hostHealthManager = _object.Host.GetComponent(); var clientHealthManager = _object.Client.GetComponent(); @@ -172,7 +173,7 @@ private void FindComponents() { healthManager ); } - + var climber = _object.Client.GetComponent(); if (climber != null) { _components[EntityNetworkData.DataType.Rotation] = new RotationComponent( @@ -201,28 +202,29 @@ private void FindComponents() { ); } } - + private void OnActionEntered(FsmStateAction self) { if (_isControlled) { return; } - + if (!_hookedActions.TryGetValue(self, out var hookedEntityAction)) { return; } - - Logger.Info($"Hooked action was entered: {hookedEntityAction.FsmIndex}, {hookedEntityAction.StateIndex}, {hookedEntityAction.ActionIndex}"); - + + Logger.Info( + $"Hooked action was entered: {hookedEntityAction.FsmIndex}, {hookedEntityAction.StateIndex}, {hookedEntityAction.ActionIndex}"); + var networkData = new EntityNetworkData { Type = EntityNetworkData.DataType.Fsm }; - + if (_fsms.Host.Count > 1) { networkData.Packet.Write((byte)hookedEntityAction.FsmIndex); } - - networkData.Packet.Write((byte) hookedEntityAction.StateIndex); - networkData.Packet.Write((byte) hookedEntityAction.ActionIndex); + + networkData.Packet.Write((byte)hookedEntityAction.StateIndex); + networkData.Packet.Write((byte)hookedEntityAction.ActionIndex); // Only if the GetNetworkDataFromAction method returns true do we add the entity data // for sending @@ -233,9 +235,22 @@ private void OnActionEntered(FsmStateAction self) { private void OnUpdate() { if (_object.Host == null) { + if (_lastIsActive) { + // If the host object was active, but now it null (or destroyed in Unity), we can send + // to the server that the entity can be regarded as inactive + Logger.Info($"Entity '{_object.Client.name}' host object is null (or destroyed) and was active"); + + _lastIsActive = false; + + _netClient.UpdateManager.UpdateEntityIsActive( + _entityId, + false + ); + } + return; } - + var hostObjectActive = _object.Host.activeSelf; if (_isControlled) { @@ -243,7 +258,7 @@ private void OnUpdate() { Logger.Info($"Entity '{_object.Host.name}' host object became active, re-disabling"); _object.Host.SetActive(false); } - + return; } @@ -272,9 +287,9 @@ private void OnUpdate() { var newActive = _object.Host.activeInHierarchy; if (newActive != _lastIsActive) { _lastIsActive = newActive; - + Logger.Info($"Entity '{_object.Host.name}' changed active: {newActive}"); - + _netClient.UpdateManager.UpdateEntityIsActive( _entityId, newActive @@ -283,10 +298,10 @@ private void OnUpdate() { } private void OnAnimationPlayed( - On.tk2dSpriteAnimator.orig_Play_tk2dSpriteAnimationClip_float_float orig, - tk2dSpriteAnimator self, - tk2dSpriteAnimationClip clip, - float clipStartTime, + On.tk2dSpriteAnimator.orig_Play_tk2dSpriteAnimationClip_float_float orig, + tk2dSpriteAnimator self, + tk2dSpriteAnimationClip clip, + float clipStartTime, float overrideFps ) { if (self == _animator.Client) { @@ -294,7 +309,7 @@ float overrideFps Logger.Info($"Entity '{_object.Client.name}' client animator tried playing animation"); } else { Logger.Info($"Entity '{_object.Client.name}' client animator was allowed to play animation"); - + orig(self, clip, clipStartTime, overrideFps); _allowClientAnimation = false; @@ -308,7 +323,7 @@ float overrideFps if (self != _animator.Host) { return; } - + if (_isControlled) { return; } @@ -322,21 +337,22 @@ float overrideFps _netClient.UpdateManager.UpdateEntityAnimation( _entityId, animationId, - (byte) clip.wrapMode + (byte)clip.wrapMode ); } public void InitializeHost() { _object.Host.SetActive(_originalIsActive); - + // Also update the last active variable to account for this potential change // Otherwise we might trigger the update sending of activity twice _lastIsActive = _object.Host.activeInHierarchy; - Logger.Info($"Initializing entity '{_object.Host.name}' with active: {_originalIsActive}, sending active: {_lastIsActive}"); + Logger.Info( + $"Initializing entity '{_object.Host.name}' with active: {_originalIsActive}, sending active: {_lastIsActive}"); _netClient.UpdateManager.UpdateEntityIsActive(_entityId, _lastIsActive); - + _isControlled = false; foreach (var component in _components.Values) { @@ -347,7 +363,7 @@ public void InitializeHost() { // TODO: parameters should be all FSM details to kickstart all FSMs of the game object public void MakeHost() { // TODO: read all variables from the parameters and set the FSM variables of all FSMs - + InitializeHost(); } @@ -380,7 +396,8 @@ public void UpdateScale(bool scale) { } } - public void UpdateAnimation(byte animationId, tk2dSpriteAnimationClip.WrapMode wrapMode, bool alreadyInSceneUpdate) { + public void UpdateAnimation(byte animationId, tk2dSpriteAnimationClip.WrapMode wrapMode, + bool alreadyInSceneUpdate) { if (_animator.Client == null) { Logger.Warn($"Entity '{_object.Client.name}' received animation while client animator does not exist"); return; @@ -390,9 +407,9 @@ public void UpdateAnimation(byte animationId, tk2dSpriteAnimationClip.WrapMode w Logger.Warn($"Entity '{_object.Client.name}' received unknown animation ID: {animationId}"); return; } - + Logger.Info($"Entity '{_object.Client.name}' received animation: {animationId}, {clipName}, {wrapMode}"); - + // All paths lead to calling the Play method of the sprite animator that is hooked, so we allow the call // through the hook _allowClientAnimation = true; @@ -404,7 +421,7 @@ public void UpdateAnimation(byte animationId, tk2dSpriteAnimationClip.WrapMode w _animator.Client.Play(clipName); return; } - + var clip = _animator.Client.GetClipByName(clipName); if (wrapMode == tk2dSpriteAnimationClip.WrapMode.LoopSection) { @@ -421,11 +438,12 @@ public void UpdateAnimation(byte animationId, tk2dSpriteAnimationClip.WrapMode w var clipLength = clip.frames.Length; _animator.Client.PlayFromFrame(clipName, clipLength - 1); - Logger.Info($" Played animation: {clipName}, {clipLength - 1} on {_animator.Client.name}, {_animator.Client.GetHashCode()}"); + Logger.Info( + $" Played animation: {clipName}, {clipLength - 1} on {_animator.Client.name}, {_animator.Client.GetHashCode()}"); return; } } - + // Otherwise, default to just playing the clip _animator.Client.Play(clipName); } @@ -458,7 +476,7 @@ public void UpdateData(List entityNetworkData) { if (data.Packet.Length < 2) { continue; } - + fsm = _fsms.Client[0]; stateIndex = data.Packet.ReadByte(); @@ -471,10 +489,10 @@ public void UpdateData(List entityNetworkData) { var action = state.Actions[actionIndex]; EntityFsmActions.ApplyNetworkDataFromAction(data, action); - + continue; } - + if (_components.TryGetValue(data.Type, out var component)) { component.Update(data); } diff --git a/HKMP/Game/Client/Entity/EntityManager.cs b/HKMP/Game/Client/Entity/EntityManager.cs index 0695823a..bef0d6e4 100644 --- a/HKMP/Game/Client/Entity/EntityManager.cs +++ b/HKMP/Game/Client/Entity/EntityManager.cs @@ -150,6 +150,10 @@ private void OnSceneChanged(Scene oldScene, Scene newScene) { // Find all PlayMakerFSM components foreach (var fsm in Object.FindObjectsOfType()) { + if (fsm.gameObject.scene != newScene) { + return; + } + // Logger.Get().Info(this, $"Found FSM: {fsm.Fsm.Name}, {fsm.gameObject.name}"); if (!_validEntityFsms.TryGetValue(fsm.Fsm.Name, out var objectName)) { From 66d0efbbbe190e9c5f7063b3d9286c5c20c4d4fe Mon Sep 17 00:00:00 2001 From: Extremelyd1 Date: Fri, 26 Aug 2022 21:47:46 +0200 Subject: [PATCH 016/216] Update documentation on entity system classes --- .../Client/Entity/Action/EntityFsmActions.cs | 41 +++++++ .../Client/Entity/Action/FsmActionHooks.cs | 27 +++++ .../Entity/Action/HookedEntityAction.cs | 15 +++ .../Entity/Component/ColliderComponent.cs | 14 +++ .../Entity/Component/EntityComponent.cs | 29 +++++ .../Component/HealthManagerComponent.cs | 19 ++++ .../Entity/Component/RotationComponent.cs | 14 +++ HKMP/Game/Client/Entity/Entity.cs | 107 ++++++++++++++++++ HKMP/Game/Client/Entity/EntityManager.cs | 57 ++++++++++ HKMP/Game/Client/Entity/HostClientPair.cs | 10 ++ HKMP/Networking/Packet/Data/EntityUpdate.cs | 20 ++++ 11 files changed, 353 insertions(+) diff --git a/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs b/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs index e9a552e1..75be63ec 100644 --- a/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs +++ b/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs @@ -9,17 +9,43 @@ 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(); + /// + /// 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(); + /// + /// Static constructor that initializes the set and dictionaries by checking all methods in the class. + /// + /// static EntityFsmActions() { var methodInfos = typeof(EntityFsmActions).GetMethods(StaticNonPublicFlags); @@ -48,6 +74,14 @@ static EntityFsmActions() { } } + /// + /// 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)) { @@ -67,6 +101,13 @@ public static bool GetNetworkDataFromAction(EntityNetworkData data, FsmStateActi 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)) { diff --git a/HKMP/Game/Client/Entity/Action/FsmActionHooks.cs b/HKMP/Game/Client/Entity/Action/FsmActionHooks.cs index 28168d8e..b42b7c62 100644 --- a/HKMP/Game/Client/Entity/Action/FsmActionHooks.cs +++ b/HKMP/Game/Client/Entity/Action/FsmActionHooks.cs @@ -6,13 +6,25 @@ namespace Hkmp.Game.Client.Entity.Action; +/// +/// Static class for registering callbacks on the "OnEnter" method of an class. +/// internal static class FsmActionHooks { + /// + /// Dictionary mapping types (subtypes of ) to an hook class. + /// private static readonly Dictionary TypeEvents; static FsmActionHooks() { TypeEvents = new Dictionary(); } + /// + /// Register an action as callback on the "OnEnter" method of an class. + /// + /// The subtype of to register the callback for. + /// The action that will be called when the "OnEnter" method executes with the instance + /// as the parameter to the action. public static void RegisterFsmStateActionType(Type type, Action action) { if (!TypeEvents.TryGetValue(type, out var fsmActionHook)) { fsmActionHook = new FsmActionHook(); @@ -32,6 +44,11 @@ public static void RegisterFsmStateActionType(Type type, Action fsmActionHook.HookEvent += action; } + /// + /// Callback method on the "OnEnter" method for a specific class. + /// + /// The original method. + /// The instance on which it was called. private static void OnActionEntered(Action orig, FsmStateAction self) { orig(self); @@ -43,9 +60,19 @@ private static void OnActionEntered(Action orig, FsmStateAction fsmActionHook.InvokeEvent(self); } + /// + /// A wrapper class containing an event for all callbacks of a hook. + /// private class FsmActionHook { + /// + /// Event for the callbacks on the hook. + /// public event Action HookEvent; + /// + /// Invokes all (if any) callbacks to this hook. + /// + /// The instance on which the hook triggered. public void InvokeEvent(FsmStateAction fsmStateAction) { HookEvent?.Invoke(fsmStateAction); } diff --git a/HKMP/Game/Client/Entity/Action/HookedEntityAction.cs b/HKMP/Game/Client/Entity/Action/HookedEntityAction.cs index 893422e0..311d844c 100644 --- a/HKMP/Game/Client/Entity/Action/HookedEntityAction.cs +++ b/HKMP/Game/Client/Entity/Action/HookedEntityAction.cs @@ -2,12 +2,27 @@ namespace Hkmp.Game.Client.Entity.Action; +/// +/// A hooked FSM action for an entity. +/// internal class HookedEntityAction { + /// + /// The instance of the action that was hooked. + /// public FsmStateAction Action { get; set; } + /// + /// The index of the FSM in which the action was hooked. + /// public int FsmIndex { get; set; } + /// + /// The index of the state in which the action was hooked. + /// public int StateIndex { get; set; } + /// + /// The index of the hooked action. + /// public int ActionIndex { get; set; } } \ No newline at end of file diff --git a/HKMP/Game/Client/Entity/Component/ColliderComponent.cs b/HKMP/Game/Client/Entity/Component/ColliderComponent.cs index aae662c4..7fdc0aa4 100644 --- a/HKMP/Game/Client/Entity/Component/ColliderComponent.cs +++ b/HKMP/Game/Client/Entity/Component/ColliderComponent.cs @@ -6,9 +6,17 @@ namespace Hkmp.Game.Client.Entity.Component; +/// +/// This component manages the unity component of an entity. internal class ColliderComponent : EntityComponent { + /// + /// Host-client pair for the box collider of the entity. + /// private readonly HostClientPair _collider; + /// + /// Optional bool indicating whether the collider was last enabled. + /// private bool? _lastEnabled; public ColliderComponent( @@ -22,6 +30,9 @@ HostClientPair collider MonoBehaviourUtil.Instance.OnUpdateEvent += OnUpdateCollider; } + /// + /// Callback for checking the collider each update. + /// private void OnUpdateCollider() { if (IsControlled) { return; @@ -45,9 +56,11 @@ private void OnUpdateCollider() { } } + /// public override void InitializeHost() { } + /// public override void Update(EntityNetworkData data) { Logger.Info($"Received collider update for {GameObject.Client.name}"); @@ -62,6 +75,7 @@ public override void Update(EntityNetworkData data) { Logger.Info($" Enabled: {enabled}"); } + /// public override void Destroy() { MonoBehaviourUtil.Instance.OnUpdateEvent -= OnUpdateCollider; } diff --git a/HKMP/Game/Client/Entity/Component/EntityComponent.cs b/HKMP/Game/Client/Entity/Component/EntityComponent.cs index ad07b0c0..ee18ec3e 100644 --- a/HKMP/Game/Client/Entity/Component/EntityComponent.cs +++ b/HKMP/Game/Client/Entity/Component/EntityComponent.cs @@ -4,12 +4,27 @@ namespace Hkmp.Game.Client.Entity.Component; +/// +/// Class for a generalizable part of an entity that requires networking for a specific feature. +/// internal abstract class EntityComponent { + /// + /// The net client for networking. + /// private readonly NetClient _netClient; + /// + /// The ID of the entity. + /// private readonly byte _entityId; + /// + /// Host-client pair of the game objects of the entity. + /// protected readonly HostClientPair GameObject; + /// + /// Whether the entity is controlled. + /// public bool IsControlled { get; set; } protected EntityComponent( @@ -25,11 +40,25 @@ HostClientPair gameObject IsControlled = true; } + /// + /// Send the given . + /// + /// The data to send. protected void SendData(EntityNetworkData data) { _netClient.UpdateManager.AddEntityData(_entityId, data); } + /// + /// Initializes the entity component when the client user is the scene host. + /// public abstract void InitializeHost(); + /// + /// Update the entity component with the given data. + /// + /// The data to update with. public abstract void Update(EntityNetworkData data); + /// + /// Destroy the entity component. + /// public abstract void Destroy(); } \ No newline at end of file diff --git a/HKMP/Game/Client/Entity/Component/HealthManagerComponent.cs b/HKMP/Game/Client/Entity/Component/HealthManagerComponent.cs index a0c3334d..471cde92 100644 --- a/HKMP/Game/Client/Entity/Component/HealthManagerComponent.cs +++ b/HKMP/Game/Client/Entity/Component/HealthManagerComponent.cs @@ -8,9 +8,17 @@ namespace Hkmp.Game.Client.Entity.Component; // TODO: make sure that the data sent on death is saved as state on the server, so new clients entering // scenes can start with the entity disabled/already dead // TODO: periodically (or on hit) sync the health of the entity so on scene host transfer we can reset health +/// +/// This component manages the component of the entity. internal class HealthManagerComponent : EntityComponent { + /// + /// Host-client pair of health manager components of the entity. + /// private readonly HostClientPair _healthManager; + /// + /// Boolean indicating whether the health manager of the client entity is allowed to die. + /// private bool _allowDeath; public HealthManagerComponent( @@ -24,6 +32,14 @@ HostClientPair healthManager On.HealthManager.Die += HealthManagerOnDie; } + /// + /// Callback method for when the health manager dies. + /// + /// The original method. + /// The health manager instance. + /// The direction of the attack that caused the death. + /// The type of attack that caused the death. + /// Whether to ignore evasion. private void HealthManagerOnDie( On.HealthManager.orig_Die orig, HealthManager self, @@ -72,9 +88,11 @@ bool ignoreEvasion SendData(data); } + /// public override void InitializeHost() { } + /// public override void Update(EntityNetworkData data) { Logger.Info("Received health manager update"); @@ -96,6 +114,7 @@ public override void Update(EntityNetworkData data) { _healthManager.Client.Die(attackDirection, attackType, ignoreEvasion); } + /// public override void Destroy() { On.HealthManager.Die -= HealthManagerOnDie; } diff --git a/HKMP/Game/Client/Entity/Component/RotationComponent.cs b/HKMP/Game/Client/Entity/Component/RotationComponent.cs index bdd90444..b5ef07d8 100644 --- a/HKMP/Game/Client/Entity/Component/RotationComponent.cs +++ b/HKMP/Game/Client/Entity/Component/RotationComponent.cs @@ -5,9 +5,17 @@ namespace Hkmp.Game.Client.Entity.Component; +/// +/// This component manages the rotation of the entity. internal class RotationComponent : EntityComponent { + /// + /// The unity component of the entity. + /// private readonly Climber _climber; + /// + /// The last rotation of the entity. + /// private Vector3 _lastRotation; public RotationComponent( @@ -22,6 +30,9 @@ Climber climber MonoBehaviourUtil.Instance.OnUpdateEvent += OnUpdateRotation; } + /// + /// Callback method to check for rotation updates. + /// private void OnUpdateRotation() { if (IsControlled) { return; @@ -46,12 +57,14 @@ private void OnUpdateRotation() { } } + /// public override void InitializeHost() { if (_climber != null) { _climber.enabled = true; } } + /// public override void Update(EntityNetworkData data) { var rotation = data.Packet.ReadFloat(); @@ -64,6 +77,7 @@ public override void Update(EntityNetworkData data) { ); } + /// public override void Destroy() { MonoBehaviourUtil.Instance.OnUpdateEvent -= OnUpdateRotation; } diff --git a/HKMP/Game/Client/Entity/Entity.cs b/HKMP/Game/Client/Entity/Entity.cs index caaea7fd..23e56717 100644 --- a/HKMP/Game/Client/Entity/Entity.cs +++ b/HKMP/Game/Client/Entity/Entity.cs @@ -15,31 +15,81 @@ using Logger = Hkmp.Logging.Logger; namespace Hkmp.Game.Client.Entity { + + /// + /// A networked entity that is either sending behaviour updates to the server or is entirely controlled by + /// updates from the server. + /// internal class Entity { + /// + /// The net client for networking. + /// private readonly NetClient _netClient; + /// + /// The ID of the entity. + /// private readonly byte _entityId; + /// + /// Host-client pair for the game objects. + /// private readonly HostClientPair _object; + /// + /// Host-client pair for the sprite animators. + /// private readonly HostClientPair _animator; + /// + /// Bi-directional lookup for animation clip names to IDs. + /// private readonly BiLookup _animationClipNameIds; + /// + /// Host-client pair for the lists of FSMs on the entity. + /// private readonly HostClientPair> _fsms; + /// + /// Dictionary mapping data types to entity components. + /// private readonly Dictionary _components; + /// + /// Dictionary mapping FSM actions to their entity action data instances. + /// private readonly Dictionary _hookedActions; + /// + /// Set of FSM action types that have been hooked to prevent duplicate hooks. + /// private readonly HashSet _hookedTypes; + /// + /// Whether the unity game object for the host entity was originally active. + /// private readonly bool _originalIsActive; + /// + /// Whether the entity is controlled, i.e. in control by updates from the server. + /// private bool _isControlled; + /// + /// The last position of the entity. + /// private Vector3 _lastPosition; + /// + /// The last scale of the entity. + /// private Vector3 _lastScale; + /// + /// Whether the game object for the entity was last active. + /// private bool _lastIsActive; + /// + /// Whether to allow the client entity to animate itself. + /// private bool _allowClientAnimation; public Entity( @@ -122,6 +172,10 @@ GameObject hostObject FindComponents(); } + /// + /// Processes the given FSM for the host entity by hooking supported FSM actions. + /// + /// The Playmaker FSM to process. private void ProcessHostFsm(PlayMakerFSM fsm) { Logger.Info($"Processing host FSM: {fsm.Fsm.Name}"); @@ -152,11 +206,18 @@ private void ProcessHostFsm(PlayMakerFSM fsm) { } } + /// + /// Processes the given FSM for the client entity by disabling it. + /// + /// The Playmaker FSM to process. private void ProcessClientFsm(PlayMakerFSM fsm) { Logger.Info($"Processing client FSM: {fsm.Fsm.Name}"); fsm.enabled = false; } + /// + /// Check the host and client objects for components that are supported for networking. + /// private void FindComponents() { var hostHealthManager = _object.Host.GetComponent(); var clientHealthManager = _object.Client.GetComponent(); @@ -203,6 +264,10 @@ private void FindComponents() { } } + /// + /// Callback method for entering a hooked FSM action. + /// + /// The FSM action instance that was entered. private void OnActionEntered(FsmStateAction self) { if (_isControlled) { return; @@ -233,6 +298,9 @@ private void OnActionEntered(FsmStateAction self) { } } + /// + /// Callback method for handling updates. + /// private void OnUpdate() { if (_object.Host == null) { if (_lastIsActive) { @@ -297,6 +365,14 @@ private void OnUpdate() { } } + /// + /// Callback method for when the sprite animator plays an animation. + /// + /// The original method. + /// The sprite animator instance. + /// The animation clip that was played. + /// The start time of the animation clip. + /// The FPS override for the clip. private void OnAnimationPlayed( On.tk2dSpriteAnimator.orig_Play_tk2dSpriteAnimationClip_float_float orig, tk2dSpriteAnimator self, @@ -341,6 +417,9 @@ float overrideFps ); } + /// + /// Initializes the entity when the client user is the scene host. + /// public void InitializeHost() { _object.Host.SetActive(_originalIsActive); @@ -361,12 +440,19 @@ public void InitializeHost() { } // TODO: parameters should be all FSM details to kickstart all FSMs of the game object + /// + /// Makes the entity a host entity if the client user became the scene host. + /// public void MakeHost() { // TODO: read all variables from the parameters and set the FSM variables of all FSMs InitializeHost(); } + /// + /// Updates the position of the client entity. + /// + /// The new position. public void UpdatePosition(Vector2 position) { var unityPos = new Vector3(position.X, position.Y); @@ -382,6 +468,10 @@ public void UpdatePosition(Vector2 position) { positionInterpolation.SetNewPosition(unityPos); } + /// + /// Updates the scale of the client entity. + /// + /// The new scale. public void UpdateScale(bool scale) { var transform = _object.Client.transform; var localScale = transform.localScale; @@ -396,6 +486,12 @@ public void UpdateScale(bool scale) { } } + /// + /// Updates the animation of the client entity. + /// + /// The ID of the animation. + /// The wrap mode of the animation clip. + /// Whether this update is when entering a new scene. public void UpdateAnimation(byte animationId, tk2dSpriteAnimationClip.WrapMode wrapMode, bool alreadyInSceneUpdate) { if (_animator.Client == null) { @@ -448,11 +544,19 @@ public void UpdateAnimation(byte animationId, tk2dSpriteAnimationClip.WrapMode w _animator.Client.Play(clipName); } + /// + /// Updates whether the game object for the client entity is active. + /// + /// The new value for active. public void UpdateIsActive(bool active) { Logger.Info($"Entity '{_object.Client.name}' received active: {active}"); _object.Client.SetActive(active); } + /// + /// Updates generic data for the client entity. + /// + /// A list of data to update the client entity with. public void UpdateData(List entityNetworkData) { foreach (var data in entityNetworkData) { if (data.Type == EntityNetworkData.DataType.Fsm) { @@ -499,6 +603,9 @@ public void UpdateData(List entityNetworkData) { } } + /// + /// Destroys the entity. + /// public void Destroy() { MonoBehaviourUtil.Instance.OnUpdateEvent -= OnUpdate; On.tk2dSpriteAnimator.Play_tk2dSpriteAnimationClip_float_float -= OnAnimationPlayed; diff --git a/HKMP/Game/Client/Entity/EntityManager.cs b/HKMP/Game/Client/Entity/EntityManager.cs index bef0d6e4..be0bf58b 100644 --- a/HKMP/Game/Client/Entity/EntityManager.cs +++ b/HKMP/Game/Client/Entity/EntityManager.cs @@ -7,6 +7,10 @@ using Logger = Hkmp.Logging.Logger; namespace Hkmp.Game.Client.Entity { + + /// + /// Manager class that handles entity creation, updating, networking and destruction. + /// internal class EntityManager { /// /// Dictionary that maps all FSM names to game object names for all valid entities. @@ -25,12 +29,24 @@ internal class EntityManager { { "Control", "Hatcher Baby Spawner" } }; + /// + /// The net client for networking. + /// private readonly NetClient _netClient; + /// + /// Dictionary mapping entity IDs to their respective entity instances. + /// private readonly Dictionary _entities; + /// + /// Whether the client user is the scene host. + /// private bool _isSceneHost; + /// + /// The last used ID of an entity. + /// private byte _lastId; public EntityManager(NetClient netClient) { @@ -42,6 +58,9 @@ public EntityManager(NetClient netClient) { UnityEngine.SceneManagement.SceneManager.activeSceneChanged += OnSceneChanged; } + /// + /// Initializes the entity manager if we are the scene host. + /// public void InitializeSceneHost() { Logger.Info("Releasing control of all registered entities"); @@ -52,12 +71,18 @@ public void InitializeSceneHost() { } } + /// + /// Initializes the entity manager if we are a scene client. + /// public void InitializeSceneClient() { Logger.Info("Taking control of all registered entities"); _isSceneHost = false; } + /// + /// Updates the entity manager if we become the scene host. + /// public void BecomeSceneHost() { Logger.Info("Becoming scene host"); @@ -68,6 +93,11 @@ public void BecomeSceneHost() { } } + /// + /// Update the position for the entity with the given ID. + /// + /// The entity ID. + /// The new position. public void UpdateEntityPosition(byte entityId, Vector2 position) { if (_isSceneHost) { return; @@ -80,6 +110,11 @@ public void UpdateEntityPosition(byte entityId, Vector2 position) { entity.UpdatePosition(position); } + /// + /// Update the scale for the entity with the given ID. + /// + /// The entity ID. + /// The new scale. public void UpdateEntityScale(byte entityId, bool scale) { if (_isSceneHost) { return; @@ -92,6 +127,13 @@ public void UpdateEntityScale(byte entityId, bool scale) { entity.UpdateScale(scale); } + /// + /// Update the animation for the entity with the given ID. + /// + /// The entity ID. + /// The ID of the animation. + /// The wrap mode of the animation. + /// Whether this update is when we are entering the scene. public void UpdateEntityAnimation( byte entityId, byte animationId, @@ -109,6 +151,11 @@ bool alreadyInSceneUpdate entity.UpdateAnimation(animationId, (tk2dSpriteAnimationClip.WrapMode) animationWrapMode, alreadyInSceneUpdate); } + /// + /// Update whether the entity with the given ID is active. + /// + /// The entity ID. + /// The new value for active. public void UpdateEntityIsActive(byte entityId, bool isActive) { if (_isSceneHost) { return; @@ -121,6 +168,11 @@ public void UpdateEntityIsActive(byte entityId, bool isActive) { entity.UpdateIsActive(isActive); } + /// + /// Update the entity with the given ID with the given generic data. + /// + /// The ID of the entity. + /// The list of data to update the entity with. public void UpdateEntityData(byte entityId, List data) { if (_isSceneHost) { return; @@ -133,6 +185,11 @@ public void UpdateEntityData(byte entityId, List data) { entity.UpdateData(data); } + /// + /// Callback method for when the scene changes. + /// + /// The old scene. + /// The new scene. private void OnSceneChanged(Scene oldScene, Scene newScene) { Logger.Info("Clearing all registered entities"); diff --git a/HKMP/Game/Client/Entity/HostClientPair.cs b/HKMP/Game/Client/Entity/HostClientPair.cs index 550678e3..1e44db86 100644 --- a/HKMP/Game/Client/Entity/HostClientPair.cs +++ b/HKMP/Game/Client/Entity/HostClientPair.cs @@ -1,6 +1,16 @@ namespace Hkmp.Game.Client.Entity; +/// +/// Simple generic class for a pair of objects that are shared by the client and host. +/// +/// The type of the objects. internal class HostClientPair { + /// + /// The client object. + /// public T Client { get; set; } + /// + /// The host object. + /// public T Host { get; set; } } \ No newline at end of file diff --git a/HKMP/Networking/Packet/Data/EntityUpdate.cs b/HKMP/Networking/Packet/Data/EntityUpdate.cs index a264821d..2ac66959 100644 --- a/HKMP/Networking/Packet/Data/EntityUpdate.cs +++ b/HKMP/Networking/Packet/Data/EntityUpdate.cs @@ -162,14 +162,27 @@ public void ReadData(IPacket packet) { } } + /// + /// Generic data for a networked entity. + /// internal class EntityNetworkData { + /// + /// The type of the data. + /// public DataType Type { get; set; } + /// + /// Packet instance containing the data for easy reading and writing of data. + /// public Packet Packet { get; set; } public EntityNetworkData() { Packet = new Packet(); } + /// + /// Write the data into the given packet. + /// + /// The packet to write into. public void WriteData(IPacket packet) { packet.Write((byte)Type); @@ -187,6 +200,10 @@ public void WriteData(IPacket packet) { } } + /// + /// Read the data from the given packet. + /// + /// The packet to read from. public void ReadData(IPacket packet) { Type = (DataType) packet.ReadByte(); @@ -200,6 +217,9 @@ public void ReadData(IPacket packet) { Packet = new Packet(data); } + /// + /// Enum for data types. + /// public enum DataType : byte { Fsm = 0, HealthManager, From 9399a2d90026af019dd78ed5e517b6c32365002b Mon Sep 17 00:00:00 2001 From: Extremelyd1 Date: Fri, 11 Nov 2022 12:48:42 +0100 Subject: [PATCH 017/216] Fix references and gitignore --- .gitignore | 843 ++++++++++++++++++++++++----------------------- HKMP/HKMP.csproj | 199 +++++------ 2 files changed, 522 insertions(+), 520 deletions(-) diff --git a/.gitignore b/.gitignore index 6fe6656e..3f1607e2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,422 +1,423 @@ - -# Created by https://www.toptal.com/developers/gitignore/api/csharp,unity -# Edit at https://www.toptal.com/developers/gitignore?templates=csharp,unity - -### Csharp ### -## Ignore Visual Studio temporary files, build results, and -## files generated by popular Visual Studio add-ons. -## -## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore - -# User-specific files -*.rsuser -*.suo -*.user -*.userosscache -*.sln.docstates - -# User-specific files (MonoDevelop/Xamarin Studio) -*.userprefs - -# Mono auto generated files -mono_crash.* - -# Build results -[Dd]ebug/ -[Dd]ebugPublic/ -[Rr]elease/ -[Rr]eleases/ -x64/ -x86/ -[Aa][Rr][Mm]/ -[Aa][Rr][Mm]64/ -bld/ -[Bb]in/ -[Oo]bj/ -[Ll]og/ -[Ll]ogs/ - -# Visual Studio 2015/2017 cache/options directory -.vs/ -# Uncomment if you have tasks that create the project's static files in wwwroot -#wwwroot/ - -# Visual Studio 2017 auto generated files -Generated\ Files/ - -# MSTest test Results -[Tt]est[Rr]esult*/ -[Bb]uild[Ll]og.* - -# NUnit -*.VisualState.xml -TestResult.xml -nunit-*.xml - -# Build Results of an ATL Project -[Dd]ebugPS/ -[Rr]eleasePS/ -dlldata.c - -# Benchmark Results -BenchmarkDotNet.Artifacts/ - -# .NET Core -project.lock.json -project.fragment.lock.json -artifacts/ - -# StyleCop -StyleCopReport.xml - -# Files built by Visual Studio -*_i.c -*_p.c -*_h.h -*.ilk -*.meta -*.obj -*.iobj -*.pch -*.pdb -*.ipdb -*.pgc -*.pgd -*.rsp -*.sbr -*.tlb -*.tli -*.tlh -*.tmp -*.tmp_proj -*_wpftmp.csproj -*.log -*.vspscc -*.vssscc -.builds -*.pidb -*.svclog -*.scc - -# Chutzpah Test files -_Chutzpah* - -# Visual C++ cache files -ipch/ -*.aps -*.ncb -*.opendb -*.opensdf -*.sdf -*.cachefile -*.VC.db -*.VC.VC.opendb - -# Visual Studio profiler -*.psess -*.vsp -*.vspx -*.sap - -# Visual Studio Trace Files -*.e2e - -# TFS 2012 Local Workspace -$tf/ - -# Guidance Automation Toolkit -*.gpState - -# ReSharper is a .NET coding add-in -_ReSharper*/ -*.[Rr]e[Ss]harper -*.DotSettings.user - -# TeamCity is a build add-in -_TeamCity* - -# DotCover is a Code Coverage Tool -*.dotCover - -# AxoCover is a Code Coverage Tool -.axoCover/* -!.axoCover/settings.json - -# Coverlet is a free, cross platform Code Coverage Tool -coverage*[.json, .xml, .info] - -# Visual Studio code coverage results -*.coverage -*.coveragexml - -# NCrunch -_NCrunch_* -.*crunch*.local.xml -nCrunchTemp_* - -# MightyMoose -*.mm.* -AutoTest.Net/ - -# Web workbench (sass) -.sass-cache/ - -# Installshield output folder -[Ee]xpress/ - -# DocProject is a documentation generator add-in -DocProject/buildhelp/ -DocProject/Help/*.HxT -DocProject/Help/*.HxC -DocProject/Help/*.hhc -DocProject/Help/*.hhk -DocProject/Help/*.hhp -DocProject/Help/Html2 -DocProject/Help/html - -# Click-Once directory -publish/ - -# Publish Web Output -*.[Pp]ublish.xml -*.azurePubxml -# Note: Comment the next line if you want to checkin your web deploy settings, -# but database connection strings (with potential passwords) will be unencrypted -*.pubxml -*.publishproj - -# Microsoft Azure Web App publish settings. Comment the next line if you want to -# checkin your Azure Web App publish settings, but sensitive information contained -# in these scripts will be unencrypted -PublishScripts/ - -# NuGet Packages -*.nupkg -# NuGet Symbol Packages -*.snupkg -# The packages folder can be ignored because of Package Restore -**/[Pp]ackages/* -# except build/, which is used as an MSBuild target. -!**/[Pp]ackages/build/ -# Uncomment if necessary however generally it will be regenerated when needed -#!**/[Pp]ackages/repositories.config -# NuGet v3's project.json files produces more ignorable files -*.nuget.props -*.nuget.targets - -# Microsoft Azure Build Output -csx/ -*.build.csdef - -# Microsoft Azure Emulator -ecf/ -rcf/ - -# Windows Store app package directories and files -AppPackages/ -BundleArtifacts/ -Package.StoreAssociation.xml -_pkginfo.txt -*.appx -*.appxbundle -*.appxupload - -# Visual Studio cache files -# files ending in .cache can be ignored -*.[Cc]ache -# but keep track of directories ending in .cache -!?*.[Cc]ache/ - -# Others -ClientBin/ -~$* -*~ -*.dbmdl -*.dbproj.schemaview -*.jfm -*.pfx -*.publishsettings -orleans.codegen.cs - -# Including strong name files can present a security risk -# (https://github.com/github/gitignore/pull/2483#issue-259490424) -#*.snk - -# Since there are multiple workflows, uncomment next line to ignore bower_components -# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) -#bower_components/ - -# RIA/Silverlight projects -Generated_Code/ - -# Backup & report files from converting an old project file -# to a newer Visual Studio version. Backup files are not needed, -# because we have git ;-) -_UpgradeReport_Files/ -Backup*/ -UpgradeLog*.XML -UpgradeLog*.htm -ServiceFabricBackup/ -*.rptproj.bak - -# SQL Server files -*.mdf -*.ldf -*.ndf - -# Business Intelligence projects -*.rdl.data -*.bim.layout -*.bim_*.settings -*.rptproj.rsuser -*- [Bb]ackup.rdl -*- [Bb]ackup ([0-9]).rdl -*- [Bb]ackup ([0-9][0-9]).rdl - -# Microsoft Fakes -FakesAssemblies/ - -# GhostDoc plugin setting file -*.GhostDoc.xml - -# Node.js Tools for Visual Studio -.ntvs_analysis.dat -node_modules/ - -# Visual Studio 6 build log -*.plg - -# Visual Studio 6 workspace options file -*.opt - -# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) -*.vbw - -# Visual Studio LightSwitch build output -**/*.HTMLClient/GeneratedArtifacts -**/*.DesktopClient/GeneratedArtifacts -**/*.DesktopClient/ModelManifest.xml -**/*.Server/GeneratedArtifacts -**/*.Server/ModelManifest.xml -_Pvt_Extensions - -# Paket dependency manager -.paket/paket.exe -paket-files/ - -# FAKE - F# Make -.fake/ - -# CodeRush personal settings -.cr/personal - -# Python Tools for Visual Studio (PTVS) -__pycache__/ -*.pyc - -# Cake - Uncomment if you are using it -# tools/** -# !tools/packages.config - -# Tabs Studio -*.tss - -# Telerik's JustMock configuration file -*.jmconfig - -# BizTalk build output -*.btp.cs -*.btm.cs -*.odx.cs -*.xsd.cs - -# OpenCover UI analysis results -OpenCover/ - -# Azure Stream Analytics local run output -ASALocalRun/ - -# MSBuild Binary and Structured Log -*.binlog - -# NVidia Nsight GPU debugger configuration file -*.nvuser - -# MFractors (Xamarin productivity tool) working folder -.mfractor/ - -# Local History for Visual Studio -.localhistory/ - -# BeatPulse healthcheck temp database -healthchecksdb - -# Backup folder for Package Reference Convert tool in Visual Studio 2017 -MigrationBackup/ - -# Ionide (cross platform F# VS Code tools) working folder -.ionide/ - -### Unity ### -# This .gitignore file should be placed at the root of your Unity project directory -# -# Get latest from https://github.com/github/gitignore/blob/master/Unity.gitignore -/[Ll]ibrary/ -/[Tt]emp/ -/[Oo]bj/ -/[Bb]uild/ -/[Bb]uilds/ -/[Ll]ogs/ -/[Uu]ser[Ss]ettings/ - -# MemoryCaptures can get excessive in size. -# They also could contain extremely sensitive data -/[Mm]emoryCaptures/ - -# Asset meta data should only be ignored when the corresponding asset is also ignored -!/[Aa]ssets/**/*.meta - -# Uncomment this line if you wish to ignore the asset store tools plugin -# /[Aa]ssets/AssetStoreTools* - -# Autogenerated Jetbrains Rider plugin -/[Aa]ssets/Plugins/Editor/JetBrains* - -# Visual Studio cache directory - -# Gradle cache directory -.gradle/ - -# Autogenerated VS/MD/Consulo solution and project files -ExportedObj/ -.consulo/ -*.unityproj -*.booproj -*.svd -*.mdb - -# Unity3D generated meta files -*.pidb.meta -*.pdb.meta -*.mdb.meta - -# Unity3D generated file on crash reports -sysinfo.txt - -# Builds -*.apk -*.unitypackage - -# Crashlytics generated file -crashlytics-build.properties - -# Autogenerated files -InitTestScene*.unity.meta -InitTestScene*.unity - - -# End of https://www.toptal.com/developers/gitignore/api/csharp,unity - -.idea/ -*/lib/ + +# Created by https://www.toptal.com/developers/gitignore/api/csharp,unity +# Edit at https://www.toptal.com/developers/gitignore?templates=csharp,unity + +### Csharp ### +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Mono auto generated files +mono_crash.* + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ +[Ll]ogs/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUnit +*.VisualState.xml +TestResult.xml +nunit-*.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Coverlet is a free, cross platform Code Coverage Tool +coverage*[.json, .xml, .info] + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# NuGet Symbol Packages +*.snupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx +*.appxbundle +*.appxupload + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!?*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser +*- [Bb]ackup.rdl +*- [Bb]ackup ([0-9]).rdl +*- [Bb]ackup ([0-9][0-9]).rdl + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# CodeRush personal settings +.cr/personal + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ + +# BeatPulse healthcheck temp database +healthchecksdb + +# Backup folder for Package Reference Convert tool in Visual Studio 2017 +MigrationBackup/ + +# Ionide (cross platform F# VS Code tools) working folder +.ionide/ + +### Unity ### +# This .gitignore file should be placed at the root of your Unity project directory +# +# Get latest from https://github.com/github/gitignore/blob/master/Unity.gitignore +/[Ll]ibrary/ +/[Tt]emp/ +/[Oo]bj/ +/[Bb]uild/ +/[Bb]uilds/ +/[Ll]ogs/ +/[Uu]ser[Ss]ettings/ + +# MemoryCaptures can get excessive in size. +# They also could contain extremely sensitive data +/[Mm]emoryCaptures/ + +# Asset meta data should only be ignored when the corresponding asset is also ignored +!/[Aa]ssets/**/*.meta + +# Uncomment this line if you wish to ignore the asset store tools plugin +# /[Aa]ssets/AssetStoreTools* + +# Autogenerated Jetbrains Rider plugin +/[Aa]ssets/Plugins/Editor/JetBrains* + +# Visual Studio cache directory + +# Gradle cache directory +.gradle/ + +# Autogenerated VS/MD/Consulo solution and project files +ExportedObj/ +.consulo/ +*.unityproj +*.booproj +*.svd +*.mdb + +# Unity3D generated meta files +*.pidb.meta +*.pdb.meta +*.mdb.meta + +# Unity3D generated file on crash reports +sysinfo.txt + +# Builds +*.apk +*.unitypackage + +# Crashlytics generated file +crashlytics-build.properties + +# Autogenerated files +InitTestScene*.unity.meta +InitTestScene*.unity + + +# End of https://www.toptal.com/developers/gitignore/api/csharp,unity + +.idea/ +*/lib/ +*/Lib/ LocalBuildProperties.props \ No newline at end of file diff --git a/HKMP/HKMP.csproj b/HKMP/HKMP.csproj index 61363d69..745f56ab 100644 --- a/HKMP/HKMP.csproj +++ b/HKMP/HKMP.csproj @@ -1,99 +1,100 @@ - - - - - - {F34118B2-515D-4C33-88E6-9CFEF2AD5A15} - Hkmp - HKMP - 0.0.0.0 - net472 - true - latest - - - - - - - - - - - - - - $(References)\Assembly-CSharp.dll - False - - - $(References)\MMHOOK_Assembly-CSharp.dll - False - - - $(References)\MMHOOK_PlayMaker.dll - False - - - lib\MonoMod.RuntimeDetour.dll - - - ..\HKMPServer\Lib\Newtonsoft.Json.dll - False - - - $(References)\PlayMaker.dll - False - - - $(References)\UnityEngine.dll - False - - - $(References)\UnityEngine.AudioModule.dll - False - - - $(References)\UnityEngine.CoreModule.dll - False - - - $(References)\UnityEngine.ImageConversionModule.dll - False - - - $(References)\UnityEngine.InputLegacyModule.dll - False - - - $(References)\UnityEngine.ParticleSystemModule.dll - False - - - $(References)\UnityEngine.Physics2DModule.dll - False - - - $(References)\UnityEngine.TextRenderingModule.dll - False - - - $(References)\UnityEngine.UI.dll - False - - - $(References)\UnityEngine.UIModule.dll - False - - - - - - - - - - - - + + + + + + {F34118B2-515D-4C33-88E6-9CFEF2AD5A15} + Hkmp + HKMP + 0.0.0.0 + net472 + true + latest + + + + + + + + + + + + + + $(References)\Assembly-CSharp.dll + False + + + $(References)\MMHOOK_Assembly-CSharp.dll + False + + + $(References)\MMHOOK_PlayMaker.dll + False + + + $(References)\MonoMod.RuntimeDetour.dll + False + + + ..\HKMPServer\Lib\Newtonsoft.Json.dll + False + + + $(References)\PlayMaker.dll + False + + + $(References)\UnityEngine.dll + False + + + $(References)\UnityEngine.AudioModule.dll + False + + + $(References)\UnityEngine.CoreModule.dll + False + + + $(References)\UnityEngine.ImageConversionModule.dll + False + + + $(References)\UnityEngine.InputLegacyModule.dll + False + + + $(References)\UnityEngine.ParticleSystemModule.dll + False + + + $(References)\UnityEngine.Physics2DModule.dll + False + + + $(References)\UnityEngine.TextRenderingModule.dll + False + + + $(References)\UnityEngine.UI.dll + False + + + $(References)\UnityEngine.UIModule.dll + False + + + + + + + + + + + + From d4bfba25c8689e907f4fc0592c83b260d1975259 Mon Sep 17 00:00:00 2001 From: Extremelyd1 Date: Fri, 11 Nov 2022 20:24:23 +0100 Subject: [PATCH 018/216] Minor improvements and add zombie shield --- HKMP/Game/Client/Entity/Entity.cs | 1238 +++++++++++----------- HKMP/Game/Client/Entity/EntityManager.cs | 499 ++++----- HKMP/Version.cs | 20 +- 3 files changed, 881 insertions(+), 876 deletions(-) diff --git a/HKMP/Game/Client/Entity/Entity.cs b/HKMP/Game/Client/Entity/Entity.cs index 23e56717..16939643 100644 --- a/HKMP/Game/Client/Entity/Entity.cs +++ b/HKMP/Game/Client/Entity/Entity.cs @@ -1,618 +1,622 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Hkmp.Collection; -using Hkmp.Fsm; -using Hkmp.Game.Client.Entity.Action; -using Hkmp.Game.Client.Entity.Component; -using Hkmp.Networking.Client; -using Hkmp.Networking.Packet.Data; -using Hkmp.Util; -using HutongGames.PlayMaker; -using UnityEngine; -using Object = UnityEngine.Object; -using Vector2 = Hkmp.Math.Vector2; -using Logger = Hkmp.Logging.Logger; - -namespace Hkmp.Game.Client.Entity { - - /// - /// A networked entity that is either sending behaviour updates to the server or is entirely controlled by - /// updates from the server. - /// - internal class Entity { - /// - /// The net client for networking. - /// - private readonly NetClient _netClient; - /// - /// The ID of the entity. - /// - private readonly byte _entityId; - - /// - /// Host-client pair for the game objects. - /// - private readonly HostClientPair _object; - - /// - /// Host-client pair for the sprite animators. - /// - private readonly HostClientPair _animator; - - /// - /// Bi-directional lookup for animation clip names to IDs. - /// - private readonly BiLookup _animationClipNameIds; - - /// - /// Host-client pair for the lists of FSMs on the entity. - /// - private readonly HostClientPair> _fsms; - - /// - /// Dictionary mapping data types to entity components. - /// - private readonly Dictionary _components; - - /// - /// Dictionary mapping FSM actions to their entity action data instances. - /// - private readonly Dictionary _hookedActions; - /// - /// Set of FSM action types that have been hooked to prevent duplicate hooks. - /// - private readonly HashSet _hookedTypes; - - /// - /// Whether the unity game object for the host entity was originally active. - /// - private readonly bool _originalIsActive; - - /// - /// Whether the entity is controlled, i.e. in control by updates from the server. - /// - private bool _isControlled; - - /// - /// The last position of the entity. - /// - private Vector3 _lastPosition; - /// - /// The last scale of the entity. - /// - private Vector3 _lastScale; - /// - /// Whether the game object for the entity was last active. - /// - private bool _lastIsActive; - - /// - /// Whether to allow the client entity to animate itself. - /// - private bool _allowClientAnimation; - - public Entity( - NetClient netClient, - byte entityId, - GameObject hostObject - ) { - _netClient = netClient; - _entityId = entityId; - - _isControlled = true; - - _object = new HostClientPair { - Host = hostObject, - Client = Object.Instantiate( - hostObject, - hostObject.transform.position, - hostObject.transform.rotation - ) - }; - _object.Client.SetActive(false); - - // Store whether the host object was active and set it not active until we know if we are scene host - _originalIsActive = _object.Host.activeSelf; - _object.Host.SetActive(false); - - _lastIsActive = _object.Host.activeInHierarchy; - - Logger.Info( - $"Entity '{_object.Host.name}' was original active: {_originalIsActive}, last active: {_lastIsActive}"); - - // Add a position interpolation component to the enemy so we can smooth out position updates - _object.Client.AddComponent(); - - // Register an update event to send position updates - MonoBehaviourUtil.Instance.OnUpdateEvent += OnUpdate; - - _animator = new HostClientPair { - Host = _object.Host.GetComponent(), - Client = _object.Client.GetComponent() - }; - if (_animator.Host != null) { - _animationClipNameIds = new BiLookup(); - - var index = 0; - foreach (var animationClip in _animator.Host.Library.clips) { - _animationClipNameIds.Add(animationClip.name, (byte)index++); - - if (index > byte.MaxValue) { - Logger.Error($"Too many animation clips to fit in a byte for entity: {_object.Client.name}"); - break; - } - } - - On.tk2dSpriteAnimator.Play_tk2dSpriteAnimationClip_float_float += OnAnimationPlayed; - } - - _fsms = new HostClientPair> { - Host = _object.Host.GetComponents().ToList(), - Client = _object.Client.GetComponents().ToList() - }; - - _hookedActions = new Dictionary(); - _hookedTypes = new HashSet(); - foreach (var fsm in _fsms.Host) { - ProcessHostFsm(fsm); - } - - // Remove all components that (re-)activate FSMs - foreach (var fsmActivator in _object.Client.GetComponents()) { - fsmActivator.StopAllCoroutines(); - Object.Destroy(fsmActivator); - } - - foreach (var fsm in _fsms.Client) { - ProcessClientFsm(fsm); - } - - _components = new Dictionary(); - FindComponents(); - } - - /// - /// Processes the given FSM for the host entity by hooking supported FSM actions. - /// - /// The Playmaker FSM to process. - private void ProcessHostFsm(PlayMakerFSM fsm) { - Logger.Info($"Processing host FSM: {fsm.Fsm.Name}"); - - for (var i = 0; i < fsm.FsmStates.Length; i++) { - var state = fsm.FsmStates[i]; - - for (var j = 0; j < state.Actions.Length; j++) { - var action = state.Actions[j]; - - if (!EntityFsmActions.SupportedActionTypes.Contains(action.GetType())) { - continue; - } - - _hookedActions[action] = new HookedEntityAction { - Action = action, - FsmIndex = _fsms.Host.IndexOf(fsm), - StateIndex = i, - ActionIndex = j - }; - Logger.Info($"Created hooked action: {action.GetType()}, {_fsms.Host.IndexOf(fsm)}, {i}, {j}"); - - if (!_hookedTypes.Contains(action.GetType())) { - _hookedTypes.Add(action.GetType()); - - FsmActionHooks.RegisterFsmStateActionType(action.GetType(), OnActionEntered); - } - } - } - } - - /// - /// Processes the given FSM for the client entity by disabling it. - /// - /// The Playmaker FSM to process. - private void ProcessClientFsm(PlayMakerFSM fsm) { - Logger.Info($"Processing client FSM: {fsm.Fsm.Name}"); - fsm.enabled = false; - } - - /// - /// Check the host and client objects for components that are supported for networking. - /// - private void FindComponents() { - var hostHealthManager = _object.Host.GetComponent(); - var clientHealthManager = _object.Client.GetComponent(); - if (hostHealthManager != null && clientHealthManager != null) { - var healthManager = new HostClientPair { - Host = hostHealthManager, - Client = clientHealthManager - }; - - _components[EntityNetworkData.DataType.HealthManager] = new HealthManagerComponent( - _netClient, - _entityId, - _object, - healthManager - ); - } - - var climber = _object.Client.GetComponent(); - if (climber != null) { - _components[EntityNetworkData.DataType.Rotation] = new RotationComponent( - _netClient, - _entityId, - _object, - climber - ); - } - - var hostCollider = _object.Host.GetComponent(); - var clientCollider = _object.Client.GetComponent(); - if (hostCollider != null && clientCollider != null) { - Logger.Info($"Adding collider component to entity: {_object.Host.name}"); - - var collider = new HostClientPair { - Host = hostCollider, - Client = clientCollider - }; - - _components[EntityNetworkData.DataType.Collider] = new ColliderComponent( - _netClient, - _entityId, - _object, - collider - ); - } - } - - /// - /// Callback method for entering a hooked FSM action. - /// - /// The FSM action instance that was entered. - private void OnActionEntered(FsmStateAction self) { - if (_isControlled) { - return; - } - - if (!_hookedActions.TryGetValue(self, out var hookedEntityAction)) { - return; - } - - Logger.Info( - $"Hooked action was entered: {hookedEntityAction.FsmIndex}, {hookedEntityAction.StateIndex}, {hookedEntityAction.ActionIndex}"); - - var networkData = new EntityNetworkData { - Type = EntityNetworkData.DataType.Fsm - }; - - if (_fsms.Host.Count > 1) { - networkData.Packet.Write((byte)hookedEntityAction.FsmIndex); - } - - networkData.Packet.Write((byte)hookedEntityAction.StateIndex); - networkData.Packet.Write((byte)hookedEntityAction.ActionIndex); - - // Only if the GetNetworkDataFromAction method returns true do we add the entity data - // for sending - if (EntityFsmActions.GetNetworkDataFromAction(networkData, self)) { - _netClient.UpdateManager.AddEntityData(_entityId, networkData); - } - } - - /// - /// Callback method for handling updates. - /// - private void OnUpdate() { - if (_object.Host == null) { - if (_lastIsActive) { - // If the host object was active, but now it null (or destroyed in Unity), we can send - // to the server that the entity can be regarded as inactive - Logger.Info($"Entity '{_object.Client.name}' host object is null (or destroyed) and was active"); - - _lastIsActive = false; - - _netClient.UpdateManager.UpdateEntityIsActive( - _entityId, - false - ); - } - - return; - } - - var hostObjectActive = _object.Host.activeSelf; - - if (_isControlled) { - if (hostObjectActive) { - Logger.Info($"Entity '{_object.Host.name}' host object became active, re-disabling"); - _object.Host.SetActive(false); - } - - return; - } - - var transform = _object.Host.transform; - - var newPosition = transform.position; - if (newPosition != _lastPosition) { - _lastPosition = newPosition; - - _netClient.UpdateManager.UpdateEntityPosition( - _entityId, - new Vector2(newPosition.x, newPosition.y) - ); - } - - var newScale = transform.localScale; - if (newScale != _lastScale) { - _lastScale = newScale; - - _netClient.UpdateManager.UpdateEntityScale( - _entityId, - newScale.x > 0 - ); - } - - var newActive = _object.Host.activeInHierarchy; - if (newActive != _lastIsActive) { - _lastIsActive = newActive; - - Logger.Info($"Entity '{_object.Host.name}' changed active: {newActive}"); - - _netClient.UpdateManager.UpdateEntityIsActive( - _entityId, - newActive - ); - } - } - - /// - /// Callback method for when the sprite animator plays an animation. - /// - /// The original method. - /// The sprite animator instance. - /// The animation clip that was played. - /// The start time of the animation clip. - /// The FPS override for the clip. - private void OnAnimationPlayed( - On.tk2dSpriteAnimator.orig_Play_tk2dSpriteAnimationClip_float_float orig, - tk2dSpriteAnimator self, - tk2dSpriteAnimationClip clip, - float clipStartTime, - float overrideFps - ) { - if (self == _animator.Client) { - if (!_allowClientAnimation) { - Logger.Info($"Entity '{_object.Client.name}' client animator tried playing animation"); - } else { - Logger.Info($"Entity '{_object.Client.name}' client animator was allowed to play animation"); - - orig(self, clip, clipStartTime, overrideFps); - - _allowClientAnimation = false; - } - - return; - } - - orig(self, clip, clipStartTime, overrideFps); - - if (self != _animator.Host) { - return; - } - - if (_isControlled) { - return; - } - - if (!_animationClipNameIds.TryGetValue(clip.name, out var animationId)) { - Logger.Warn($"Entity '{_object.Client.name}' played unknown animation: {clip.name}"); - return; - } - - Logger.Info($"Entity '{_object.Host.name}' sends animation: {clip.name}, {animationId}, {clip.wrapMode}"); - _netClient.UpdateManager.UpdateEntityAnimation( - _entityId, - animationId, - (byte)clip.wrapMode - ); - } - - /// - /// Initializes the entity when the client user is the scene host. - /// - public void InitializeHost() { - _object.Host.SetActive(_originalIsActive); - - // Also update the last active variable to account for this potential change - // Otherwise we might trigger the update sending of activity twice - _lastIsActive = _object.Host.activeInHierarchy; - - Logger.Info( - $"Initializing entity '{_object.Host.name}' with active: {_originalIsActive}, sending active: {_lastIsActive}"); - - _netClient.UpdateManager.UpdateEntityIsActive(_entityId, _lastIsActive); - - _isControlled = false; - - foreach (var component in _components.Values) { - component.IsControlled = false; - } - } - - // TODO: parameters should be all FSM details to kickstart all FSMs of the game object - /// - /// Makes the entity a host entity if the client user became the scene host. - /// - public void MakeHost() { - // TODO: read all variables from the parameters and set the FSM variables of all FSMs - - InitializeHost(); - } - - /// - /// Updates the position of the client entity. - /// - /// The new position. - public void UpdatePosition(Vector2 position) { - var unityPos = new Vector3(position.X, position.Y); - - if (_object.Client == null) { - return; - } - - var positionInterpolation = _object.Client.GetComponent(); - if (positionInterpolation == null) { - return; - } - - positionInterpolation.SetNewPosition(unityPos); - } - - /// - /// Updates the scale of the client entity. - /// - /// The new scale. - public void UpdateScale(bool scale) { - var transform = _object.Client.transform; - var localScale = transform.localScale; - var currentScaleX = localScale.x; - - if (currentScaleX > 0 != scale) { - transform.localScale = new Vector3( - currentScaleX * -1, - localScale.y, - localScale.z - ); - } - } - - /// - /// Updates the animation of the client entity. - /// - /// The ID of the animation. - /// The wrap mode of the animation clip. - /// Whether this update is when entering a new scene. - public void UpdateAnimation(byte animationId, tk2dSpriteAnimationClip.WrapMode wrapMode, - bool alreadyInSceneUpdate) { - if (_animator.Client == null) { - Logger.Warn($"Entity '{_object.Client.name}' received animation while client animator does not exist"); - return; - } - - if (!_animationClipNameIds.TryGetValue(animationId, out var clipName)) { - Logger.Warn($"Entity '{_object.Client.name}' received unknown animation ID: {animationId}"); - return; - } - - Logger.Info($"Entity '{_object.Client.name}' received animation: {animationId}, {clipName}, {wrapMode}"); - - // All paths lead to calling the Play method of the sprite animator that is hooked, so we allow the call - // through the hook - _allowClientAnimation = true; - - if (alreadyInSceneUpdate) { - // Since this is an animation update from an entity that was already present in a scene, - // we need to determine where to start playing this specific animation - if (wrapMode == tk2dSpriteAnimationClip.WrapMode.Loop) { - _animator.Client.Play(clipName); - return; - } - - var clip = _animator.Client.GetClipByName(clipName); - - if (wrapMode == tk2dSpriteAnimationClip.WrapMode.LoopSection) { - // The clip loops in a specific section in the frames, so we start playing - // it from the start of that section - _animator.Client.PlayFromFrame(clipName, clip.loopStart); - return; - } - - if (wrapMode == tk2dSpriteAnimationClip.WrapMode.Once || - wrapMode == tk2dSpriteAnimationClip.WrapMode.Single) { - // Since the clip was played once, it stops on the last frame, - // so we emulate that by only "playing" the last frame of the clip - var clipLength = clip.frames.Length; - _animator.Client.PlayFromFrame(clipName, clipLength - 1); - - Logger.Info( - $" Played animation: {clipName}, {clipLength - 1} on {_animator.Client.name}, {_animator.Client.GetHashCode()}"); - return; - } - } - - // Otherwise, default to just playing the clip - _animator.Client.Play(clipName); - } - - /// - /// Updates whether the game object for the client entity is active. - /// - /// The new value for active. - public void UpdateIsActive(bool active) { - Logger.Info($"Entity '{_object.Client.name}' received active: {active}"); - _object.Client.SetActive(active); - } - - /// - /// Updates generic data for the client entity. - /// - /// A list of data to update the client entity with. - public void UpdateData(List entityNetworkData) { - foreach (var data in entityNetworkData) { - if (data.Type == EntityNetworkData.DataType.Fsm) { - PlayMakerFSM fsm; - byte stateIndex; - byte actionIndex; - - if (_fsms.Client.Count > 1) { - // Do a check on the length of the data - if (data.Packet.Length < 3) { - continue; - } - - var fsmIndex = data.Packet.ReadByte(); - fsm = _fsms.Client[fsmIndex]; - - stateIndex = data.Packet.ReadByte(); - actionIndex = data.Packet.ReadByte(); - } else { - // Do a check on the length of the data - if (data.Packet.Length < 2) { - continue; - } - - fsm = _fsms.Client[0]; - - stateIndex = data.Packet.ReadByte(); - actionIndex = data.Packet.ReadByte(); - } - - Logger.Info($"Received entity network data for FSM: {fsm.Fsm.Name}, {stateIndex}, {actionIndex}"); - - var state = fsm.FsmStates[stateIndex]; - var action = state.Actions[actionIndex]; - - EntityFsmActions.ApplyNetworkDataFromAction(data, action); - - continue; - } - - if (_components.TryGetValue(data.Type, out var component)) { - component.Update(data); - } - } - } - - /// - /// Destroys the entity. - /// - public void Destroy() { - MonoBehaviourUtil.Instance.OnUpdateEvent -= OnUpdate; - On.tk2dSpriteAnimator.Play_tk2dSpriteAnimationClip_float_float -= OnAnimationPlayed; - - foreach (var component in _components.Values) { - component.Destroy(); - } - } - } +using System; +using System.Collections.Generic; +using System.Linq; +using Hkmp.Collection; +using Hkmp.Fsm; +using Hkmp.Game.Client.Entity.Action; +using Hkmp.Game.Client.Entity.Component; +using Hkmp.Networking.Client; +using Hkmp.Networking.Packet.Data; +using Hkmp.Util; +using HutongGames.PlayMaker; +using UnityEngine; +using Object = UnityEngine.Object; +using Vector2 = Hkmp.Math.Vector2; +using Logger = Hkmp.Logging.Logger; + +namespace Hkmp.Game.Client.Entity { + + /// + /// A networked entity that is either sending behaviour updates to the server or is entirely controlled by + /// updates from the server. + /// + internal class Entity { + /// + /// The net client for networking. + /// + private readonly NetClient _netClient; + /// + /// The ID of the entity. + /// + private readonly byte _entityId; + + /// + /// Host-client pair for the game objects. + /// + private readonly HostClientPair _object; + + /// + /// Host-client pair for the sprite animators. + /// + private readonly HostClientPair _animator; + + /// + /// Bi-directional lookup for animation clip names to IDs. + /// + private readonly BiLookup _animationClipNameIds; + + /// + /// Host-client pair for the lists of FSMs on the entity. + /// + private readonly HostClientPair> _fsms; + + /// + /// Dictionary mapping data types to entity components. + /// + private readonly Dictionary _components; + + /// + /// Dictionary mapping FSM actions to their entity action data instances. + /// + private readonly Dictionary _hookedActions; + /// + /// Set of FSM action types that have been hooked to prevent duplicate hooks. + /// + private readonly HashSet _hookedTypes; + + /// + /// Whether the unity game object for the host entity was originally active. + /// + private readonly bool _originalIsActive; + + /// + /// Whether the entity is controlled, i.e. in control by updates from the server. + /// + private bool _isControlled; + + /// + /// The last position of the entity. + /// + private Vector3 _lastPosition; + /// + /// The last scale of the entity. + /// + private Vector3 _lastScale; + /// + /// Whether the game object for the entity was last active. + /// + private bool _lastIsActive; + + /// + /// Whether to allow the client entity to animate itself. + /// + private bool _allowClientAnimation; + + public Entity( + NetClient netClient, + byte entityId, + GameObject hostObject + ) { + _netClient = netClient; + _entityId = entityId; + + _isControlled = true; + + _object = new HostClientPair { + Host = hostObject, + Client = Object.Instantiate( + hostObject, + hostObject.transform.position, + hostObject.transform.rotation + ) + }; + _object.Client.SetActive(false); + + // Store whether the host object was active and set it not active until we know if we are scene host + _originalIsActive = _object.Host.activeSelf; + _object.Host.SetActive(false); + + _lastIsActive = _object.Host.activeInHierarchy; + + Logger.Info( + $"Entity '{_object.Host.name}' was original active: {_originalIsActive}, last active: {_lastIsActive}"); + + // Add a position interpolation component to the enemy so we can smooth out position updates + _object.Client.AddComponent(); + + // Register an update event to send position updates + MonoBehaviourUtil.Instance.OnUpdateEvent += OnUpdate; + + _animator = new HostClientPair { + Host = _object.Host.GetComponent(), + Client = _object.Client.GetComponent() + }; + if (_animator.Host != null) { + _animationClipNameIds = new BiLookup(); + + var index = 0; + foreach (var animationClip in _animator.Host.Library.clips) { + if (_animationClipNameIds.ContainsFirst(animationClip.name)) { + continue; + } + + _animationClipNameIds.Add(animationClip.name, (byte)index++); + + if (index > byte.MaxValue) { + Logger.Error($"Too many animation clips to fit in a byte for entity: {_object.Client.name}"); + break; + } + } + + On.tk2dSpriteAnimator.Play_tk2dSpriteAnimationClip_float_float += OnAnimationPlayed; + } + + _fsms = new HostClientPair> { + Host = _object.Host.GetComponents().ToList(), + Client = _object.Client.GetComponents().ToList() + }; + + _hookedActions = new Dictionary(); + _hookedTypes = new HashSet(); + foreach (var fsm in _fsms.Host) { + ProcessHostFsm(fsm); + } + + // Remove all components that (re-)activate FSMs + foreach (var fsmActivator in _object.Client.GetComponents()) { + fsmActivator.StopAllCoroutines(); + Object.Destroy(fsmActivator); + } + + foreach (var fsm in _fsms.Client) { + ProcessClientFsm(fsm); + } + + _components = new Dictionary(); + FindComponents(); + } + + /// + /// Processes the given FSM for the host entity by hooking supported FSM actions. + /// + /// The Playmaker FSM to process. + private void ProcessHostFsm(PlayMakerFSM fsm) { + Logger.Info($"Processing host FSM: {fsm.Fsm.Name}"); + + for (var i = 0; i < fsm.FsmStates.Length; i++) { + var state = fsm.FsmStates[i]; + + for (var j = 0; j < state.Actions.Length; j++) { + var action = state.Actions[j]; + + if (!EntityFsmActions.SupportedActionTypes.Contains(action.GetType())) { + continue; + } + + _hookedActions[action] = new HookedEntityAction { + Action = action, + FsmIndex = _fsms.Host.IndexOf(fsm), + StateIndex = i, + ActionIndex = j + }; + Logger.Info($"Created hooked action: {action.GetType()}, {_fsms.Host.IndexOf(fsm)}, {i}, {j}"); + + if (!_hookedTypes.Contains(action.GetType())) { + _hookedTypes.Add(action.GetType()); + + FsmActionHooks.RegisterFsmStateActionType(action.GetType(), OnActionEntered); + } + } + } + } + + /// + /// Processes the given FSM for the client entity by disabling it. + /// + /// The Playmaker FSM to process. + private void ProcessClientFsm(PlayMakerFSM fsm) { + Logger.Info($"Processing client FSM: {fsm.Fsm.Name}"); + fsm.enabled = false; + } + + /// + /// Check the host and client objects for components that are supported for networking. + /// + private void FindComponents() { + var hostHealthManager = _object.Host.GetComponent(); + var clientHealthManager = _object.Client.GetComponent(); + if (hostHealthManager != null && clientHealthManager != null) { + var healthManager = new HostClientPair { + Host = hostHealthManager, + Client = clientHealthManager + }; + + _components[EntityNetworkData.DataType.HealthManager] = new HealthManagerComponent( + _netClient, + _entityId, + _object, + healthManager + ); + } + + var climber = _object.Client.GetComponent(); + if (climber != null) { + _components[EntityNetworkData.DataType.Rotation] = new RotationComponent( + _netClient, + _entityId, + _object, + climber + ); + } + + var hostCollider = _object.Host.GetComponent(); + var clientCollider = _object.Client.GetComponent(); + if (hostCollider != null && clientCollider != null) { + Logger.Info($"Adding collider component to entity: {_object.Host.name}"); + + var collider = new HostClientPair { + Host = hostCollider, + Client = clientCollider + }; + + _components[EntityNetworkData.DataType.Collider] = new ColliderComponent( + _netClient, + _entityId, + _object, + collider + ); + } + } + + /// + /// Callback method for entering a hooked FSM action. + /// + /// The FSM action instance that was entered. + private void OnActionEntered(FsmStateAction self) { + if (_isControlled) { + return; + } + + if (!_hookedActions.TryGetValue(self, out var hookedEntityAction)) { + return; + } + + Logger.Info( + $"Hooked action was entered: {hookedEntityAction.FsmIndex}, {hookedEntityAction.StateIndex}, {hookedEntityAction.ActionIndex}"); + + var networkData = new EntityNetworkData { + Type = EntityNetworkData.DataType.Fsm + }; + + if (_fsms.Host.Count > 1) { + networkData.Packet.Write((byte)hookedEntityAction.FsmIndex); + } + + networkData.Packet.Write((byte)hookedEntityAction.StateIndex); + networkData.Packet.Write((byte)hookedEntityAction.ActionIndex); + + // Only if the GetNetworkDataFromAction method returns true do we add the entity data + // for sending + if (EntityFsmActions.GetNetworkDataFromAction(networkData, self)) { + _netClient.UpdateManager.AddEntityData(_entityId, networkData); + } + } + + /// + /// Callback method for handling updates. + /// + private void OnUpdate() { + if (_object.Host == null) { + if (_lastIsActive) { + // If the host object was active, but now it null (or destroyed in Unity), we can send + // to the server that the entity can be regarded as inactive + Logger.Info($"Entity '{_object.Client.name}' host object is null (or destroyed) and was active"); + + _lastIsActive = false; + + _netClient.UpdateManager.UpdateEntityIsActive( + _entityId, + false + ); + } + + return; + } + + var hostObjectActive = _object.Host.activeSelf; + + if (_isControlled) { + if (hostObjectActive) { + Logger.Info($"Entity '{_object.Host.name}' host object became active, re-disabling"); + _object.Host.SetActive(false); + } + + return; + } + + var transform = _object.Host.transform; + + var newPosition = transform.position; + if (newPosition != _lastPosition) { + _lastPosition = newPosition; + + _netClient.UpdateManager.UpdateEntityPosition( + _entityId, + new Vector2(newPosition.x, newPosition.y) + ); + } + + var newScale = transform.localScale; + if (newScale != _lastScale) { + _lastScale = newScale; + + _netClient.UpdateManager.UpdateEntityScale( + _entityId, + newScale.x > 0 + ); + } + + var newActive = _object.Host.activeInHierarchy; + if (newActive != _lastIsActive) { + _lastIsActive = newActive; + + Logger.Info($"Entity '{_object.Host.name}' changed active: {newActive}"); + + _netClient.UpdateManager.UpdateEntityIsActive( + _entityId, + newActive + ); + } + } + + /// + /// Callback method for when the sprite animator plays an animation. + /// + /// The original method. + /// The sprite animator instance. + /// The animation clip that was played. + /// The start time of the animation clip. + /// The FPS override for the clip. + private void OnAnimationPlayed( + On.tk2dSpriteAnimator.orig_Play_tk2dSpriteAnimationClip_float_float orig, + tk2dSpriteAnimator self, + tk2dSpriteAnimationClip clip, + float clipStartTime, + float overrideFps + ) { + if (self == _animator.Client) { + if (!_allowClientAnimation) { + Logger.Info($"Entity '{_object.Client.name}' client animator tried playing animation"); + } else { + // Logger.Info($"Entity '{_object.Client.name}' client animator was allowed to play animation"); + + orig(self, clip, clipStartTime, overrideFps); + + _allowClientAnimation = false; + } + + return; + } + + orig(self, clip, clipStartTime, overrideFps); + + if (self != _animator.Host) { + return; + } + + if (_isControlled) { + return; + } + + if (!_animationClipNameIds.TryGetValue(clip.name, out var animationId)) { + Logger.Warn($"Entity '{_object.Client.name}' played unknown animation: {clip.name}"); + return; + } + + Logger.Info($"Entity '{_object.Host.name}' sends animation: {clip.name}, {animationId}, {clip.wrapMode}"); + _netClient.UpdateManager.UpdateEntityAnimation( + _entityId, + animationId, + (byte)clip.wrapMode + ); + } + + /// + /// Initializes the entity when the client user is the scene host. + /// + public void InitializeHost() { + _object.Host.SetActive(_originalIsActive); + + // Also update the last active variable to account for this potential change + // Otherwise we might trigger the update sending of activity twice + _lastIsActive = _object.Host.activeInHierarchy; + + Logger.Info( + $"Initializing entity '{_object.Host.name}' with active: {_originalIsActive}, sending active: {_lastIsActive}"); + + _netClient.UpdateManager.UpdateEntityIsActive(_entityId, _lastIsActive); + + _isControlled = false; + + foreach (var component in _components.Values) { + component.IsControlled = false; + } + } + + // TODO: parameters should be all FSM details to kickstart all FSMs of the game object + /// + /// Makes the entity a host entity if the client user became the scene host. + /// + public void MakeHost() { + // TODO: read all variables from the parameters and set the FSM variables of all FSMs + + InitializeHost(); + } + + /// + /// Updates the position of the client entity. + /// + /// The new position. + public void UpdatePosition(Vector2 position) { + var unityPos = new Vector3(position.X, position.Y); + + if (_object.Client == null) { + return; + } + + var positionInterpolation = _object.Client.GetComponent(); + if (positionInterpolation == null) { + return; + } + + positionInterpolation.SetNewPosition(unityPos); + } + + /// + /// Updates the scale of the client entity. + /// + /// The new scale. + public void UpdateScale(bool scale) { + var transform = _object.Client.transform; + var localScale = transform.localScale; + var currentScaleX = localScale.x; + + if (currentScaleX > 0 != scale) { + transform.localScale = new Vector3( + currentScaleX * -1, + localScale.y, + localScale.z + ); + } + } + + /// + /// Updates the animation of the client entity. + /// + /// The ID of the animation. + /// The wrap mode of the animation clip. + /// Whether this update is when entering a new scene. + public void UpdateAnimation(byte animationId, tk2dSpriteAnimationClip.WrapMode wrapMode, + bool alreadyInSceneUpdate) { + if (_animator.Client == null) { + Logger.Warn($"Entity '{_object.Client.name}' received animation while client animator does not exist"); + return; + } + + if (!_animationClipNameIds.TryGetValue(animationId, out var clipName)) { + Logger.Warn($"Entity '{_object.Client.name}' received unknown animation ID: {animationId}"); + return; + } + + // Logger.Info($"Entity '{_object.Client.name}' received animation: {animationId}, {clipName}, {wrapMode}"); + + // All paths lead to calling the Play method of the sprite animator that is hooked, so we allow the call + // through the hook + _allowClientAnimation = true; + + if (alreadyInSceneUpdate) { + // Since this is an animation update from an entity that was already present in a scene, + // we need to determine where to start playing this specific animation + if (wrapMode == tk2dSpriteAnimationClip.WrapMode.Loop) { + _animator.Client.Play(clipName); + return; + } + + var clip = _animator.Client.GetClipByName(clipName); + + if (wrapMode == tk2dSpriteAnimationClip.WrapMode.LoopSection) { + // The clip loops in a specific section in the frames, so we start playing + // it from the start of that section + _animator.Client.PlayFromFrame(clipName, clip.loopStart); + return; + } + + if (wrapMode == tk2dSpriteAnimationClip.WrapMode.Once || + wrapMode == tk2dSpriteAnimationClip.WrapMode.Single) { + // Since the clip was played once, it stops on the last frame, + // so we emulate that by only "playing" the last frame of the clip + var clipLength = clip.frames.Length; + _animator.Client.PlayFromFrame(clipName, clipLength - 1); + + // Logger.Info( + // $" Played animation: {clipName}, {clipLength - 1} on {_animator.Client.name}, {_animator.Client.GetHashCode()}"); + return; + } + } + + // Otherwise, default to just playing the clip + _animator.Client.Play(clipName); + } + + /// + /// Updates whether the game object for the client entity is active. + /// + /// The new value for active. + public void UpdateIsActive(bool active) { + Logger.Info($"Entity '{_object.Client.name}' received active: {active}"); + _object.Client.SetActive(active); + } + + /// + /// Updates generic data for the client entity. + /// + /// A list of data to update the client entity with. + public void UpdateData(List entityNetworkData) { + foreach (var data in entityNetworkData) { + if (data.Type == EntityNetworkData.DataType.Fsm) { + PlayMakerFSM fsm; + byte stateIndex; + byte actionIndex; + + if (_fsms.Client.Count > 1) { + // Do a check on the length of the data + if (data.Packet.Length < 3) { + continue; + } + + var fsmIndex = data.Packet.ReadByte(); + fsm = _fsms.Client[fsmIndex]; + + stateIndex = data.Packet.ReadByte(); + actionIndex = data.Packet.ReadByte(); + } else { + // Do a check on the length of the data + if (data.Packet.Length < 2) { + continue; + } + + fsm = _fsms.Client[0]; + + stateIndex = data.Packet.ReadByte(); + actionIndex = data.Packet.ReadByte(); + } + + Logger.Info($"Received entity network data for FSM: {fsm.Fsm.Name}, {stateIndex}, {actionIndex}"); + + var state = fsm.FsmStates[stateIndex]; + var action = state.Actions[actionIndex]; + + EntityFsmActions.ApplyNetworkDataFromAction(data, action); + + continue; + } + + if (_components.TryGetValue(data.Type, out var component)) { + component.Update(data); + } + } + } + + /// + /// Destroys the entity. + /// + public void Destroy() { + MonoBehaviourUtil.Instance.OnUpdateEvent -= OnUpdate; + On.tk2dSpriteAnimator.Play_tk2dSpriteAnimationClip_float_float -= OnAnimationPlayed; + + foreach (var component in _components.Values) { + component.Destroy(); + } + } + } } \ No newline at end of file diff --git a/HKMP/Game/Client/Entity/EntityManager.cs b/HKMP/Game/Client/Entity/EntityManager.cs index be0bf58b..f237071b 100644 --- a/HKMP/Game/Client/Entity/EntityManager.cs +++ b/HKMP/Game/Client/Entity/EntityManager.cs @@ -1,250 +1,251 @@ -using System.Collections.Generic; -using Hkmp.Networking.Client; -using Hkmp.Networking.Packet.Data; -using UnityEngine; -using UnityEngine.SceneManagement; -using Vector2 = Hkmp.Math.Vector2; -using Logger = Hkmp.Logging.Logger; - -namespace Hkmp.Game.Client.Entity { - - /// - /// Manager class that handles entity creation, updating, networking and destruction. - /// - internal class EntityManager { - /// - /// Dictionary that maps all FSM names to game object names for all valid entities. - /// Valid entities are entities that should be managed by the entity system. - /// - private readonly Dictionary _validEntityFsms = new() { - { "Crawler", "Crawler" }, - { "chaser", "Buzzer" }, - { "Zombie Swipe", "Zombie Runner" }, - { "Bouncer Control", "Fly" }, - { "BG Control", "Battle Gate" }, - { "spitter", "Spitter" }, - { "Zombie Guard", "Zombie Guard" }, - { "Zombie Leap", "Zombie Leaper" }, - { "Hatcher", "Hatcher" }, - { "Control", "Hatcher Baby Spawner" } - }; - - /// - /// The net client for networking. - /// - private readonly NetClient _netClient; - - /// - /// Dictionary mapping entity IDs to their respective entity instances. - /// - private readonly Dictionary _entities; - - /// - /// Whether the client user is the scene host. - /// - private bool _isSceneHost; - - /// - /// The last used ID of an entity. - /// - private byte _lastId; - - public EntityManager(NetClient netClient) { - _netClient = netClient; - _entities = new Dictionary(); - - _lastId = 0; - - UnityEngine.SceneManagement.SceneManager.activeSceneChanged += OnSceneChanged; - } - - /// - /// Initializes the entity manager if we are the scene host. - /// - public void InitializeSceneHost() { - Logger.Info("Releasing control of all registered entities"); - - _isSceneHost = true; - - foreach (var entity in _entities.Values) { - entity.InitializeHost(); - } - } - - /// - /// Initializes the entity manager if we are a scene client. - /// - public void InitializeSceneClient() { - Logger.Info("Taking control of all registered entities"); - - _isSceneHost = false; - } - - /// - /// Updates the entity manager if we become the scene host. - /// - public void BecomeSceneHost() { - Logger.Info("Becoming scene host"); - - _isSceneHost = true; - - foreach (var entity in _entities.Values) { - entity.MakeHost(); - } - } - - /// - /// Update the position for the entity with the given ID. - /// - /// The entity ID. - /// The new position. - public void UpdateEntityPosition(byte entityId, Vector2 position) { - if (_isSceneHost) { - return; - } - - if (!_entities.TryGetValue(entityId, out var entity)) { - return; - } - - entity.UpdatePosition(position); - } - - /// - /// Update the scale for the entity with the given ID. - /// - /// The entity ID. - /// The new scale. - public void UpdateEntityScale(byte entityId, bool scale) { - if (_isSceneHost) { - return; - } - - if (!_entities.TryGetValue(entityId, out var entity)) { - return; - } - - entity.UpdateScale(scale); - } - - /// - /// Update the animation for the entity with the given ID. - /// - /// The entity ID. - /// The ID of the animation. - /// The wrap mode of the animation. - /// Whether this update is when we are entering the scene. - public void UpdateEntityAnimation( - byte entityId, - byte animationId, - byte animationWrapMode, - bool alreadyInSceneUpdate - ) { - if (_isSceneHost) { - return; - } - - if (!_entities.TryGetValue(entityId, out var entity)) { - return; - } - - entity.UpdateAnimation(animationId, (tk2dSpriteAnimationClip.WrapMode) animationWrapMode, alreadyInSceneUpdate); - } - - /// - /// Update whether the entity with the given ID is active. - /// - /// The entity ID. - /// The new value for active. - public void UpdateEntityIsActive(byte entityId, bool isActive) { - if (_isSceneHost) { - return; - } - - if (!_entities.TryGetValue(entityId, out var entity)) { - return; - } - - entity.UpdateIsActive(isActive); - } - - /// - /// Update the entity with the given ID with the given generic data. - /// - /// The ID of the entity. - /// The list of data to update the entity with. - public void UpdateEntityData(byte entityId, List data) { - if (_isSceneHost) { - return; - } - - if (!_entities.TryGetValue(entityId, out var entity)) { - return; - } - - entity.UpdateData(data); - } - - /// - /// Callback method for when the scene changes. - /// - /// The old scene. - /// The new scene. - private void OnSceneChanged(Scene oldScene, Scene newScene) { - Logger.Info("Clearing all registered entities"); - - foreach (var entity in _entities.Values) { - entity.Destroy(); - } - - _entities.Clear(); - - _lastId = 0; - - if (!_netClient.IsConnected) { - return; - } - - // Find all PlayMakerFSM components - foreach (var fsm in Object.FindObjectsOfType()) { - if (fsm.gameObject.scene != newScene) { - return; - } - - // Logger.Get().Info(this, $"Found FSM: {fsm.Fsm.Name}, {fsm.gameObject.name}"); - - if (!_validEntityFsms.TryGetValue(fsm.Fsm.Name, out var objectName)) { - continue; - } - - var fsmGameObjectName = fsm.gameObject.name; - if (!fsmGameObjectName.Contains(objectName)) { - continue; - } - - Logger.Info($"Registering entity '{fsmGameObjectName}' with ID '{_lastId}'"); - - _entities[_lastId] = new Entity( - _netClient, - _lastId, - fsm.gameObject - ); - - _lastId++; - } - - // Find all Climber components - foreach (var climber in Object.FindObjectsOfType()) { - Logger.Info($"Registering entity '{climber.name}' with ID '{_lastId}'"); - - _entities[_lastId] = new Entity( - _netClient, - _lastId, - climber.gameObject - ); - - _lastId++; - } - } - } +using System.Collections.Generic; +using Hkmp.Networking.Client; +using Hkmp.Networking.Packet.Data; +using UnityEngine; +using UnityEngine.SceneManagement; +using Vector2 = Hkmp.Math.Vector2; +using Logger = Hkmp.Logging.Logger; + +namespace Hkmp.Game.Client.Entity { + + /// + /// Manager class that handles entity creation, updating, networking and destruction. + /// + internal class EntityManager { + /// + /// Dictionary that maps all FSM names to game object names for all valid entities. + /// Valid entities are entities that should be managed by the entity system. + /// + private readonly Dictionary _validEntityFsms = new() { + { "Crawler", "Crawler" }, + { "chaser", "Buzzer" }, + { "Zombie Swipe", "Zombie Runner" }, + { "Bouncer Control", "Fly" }, + { "BG Control", "Battle Gate" }, + { "spitter", "Spitter" }, + { "Zombie Guard", "Zombie Guard" }, + { "Zombie Leap", "Zombie Leaper" }, + { "Hatcher", "Hatcher" }, + { "Control", "Hatcher Baby Spawner" }, + { "ZombieShieldControl", "Zombie Shield" } // TODO: check weird position sliding + }; + + /// + /// The net client for networking. + /// + private readonly NetClient _netClient; + + /// + /// Dictionary mapping entity IDs to their respective entity instances. + /// + private readonly Dictionary _entities; + + /// + /// Whether the client user is the scene host. + /// + private bool _isSceneHost; + + /// + /// The last used ID of an entity. + /// + private byte _lastId; + + public EntityManager(NetClient netClient) { + _netClient = netClient; + _entities = new Dictionary(); + + _lastId = 0; + + UnityEngine.SceneManagement.SceneManager.activeSceneChanged += OnSceneChanged; + } + + /// + /// Initializes the entity manager if we are the scene host. + /// + public void InitializeSceneHost() { + Logger.Info("Releasing control of all registered entities"); + + _isSceneHost = true; + + foreach (var entity in _entities.Values) { + entity.InitializeHost(); + } + } + + /// + /// Initializes the entity manager if we are a scene client. + /// + public void InitializeSceneClient() { + Logger.Info("Taking control of all registered entities"); + + _isSceneHost = false; + } + + /// + /// Updates the entity manager if we become the scene host. + /// + public void BecomeSceneHost() { + Logger.Info("Becoming scene host"); + + _isSceneHost = true; + + foreach (var entity in _entities.Values) { + entity.MakeHost(); + } + } + + /// + /// Update the position for the entity with the given ID. + /// + /// The entity ID. + /// The new position. + public void UpdateEntityPosition(byte entityId, Vector2 position) { + if (_isSceneHost) { + return; + } + + if (!_entities.TryGetValue(entityId, out var entity)) { + return; + } + + entity.UpdatePosition(position); + } + + /// + /// Update the scale for the entity with the given ID. + /// + /// The entity ID. + /// The new scale. + public void UpdateEntityScale(byte entityId, bool scale) { + if (_isSceneHost) { + return; + } + + if (!_entities.TryGetValue(entityId, out var entity)) { + return; + } + + entity.UpdateScale(scale); + } + + /// + /// Update the animation for the entity with the given ID. + /// + /// The entity ID. + /// The ID of the animation. + /// The wrap mode of the animation. + /// Whether this update is when we are entering the scene. + public void UpdateEntityAnimation( + byte entityId, + byte animationId, + byte animationWrapMode, + bool alreadyInSceneUpdate + ) { + if (_isSceneHost) { + return; + } + + if (!_entities.TryGetValue(entityId, out var entity)) { + return; + } + + entity.UpdateAnimation(animationId, (tk2dSpriteAnimationClip.WrapMode) animationWrapMode, alreadyInSceneUpdate); + } + + /// + /// Update whether the entity with the given ID is active. + /// + /// The entity ID. + /// The new value for active. + public void UpdateEntityIsActive(byte entityId, bool isActive) { + if (_isSceneHost) { + return; + } + + if (!_entities.TryGetValue(entityId, out var entity)) { + return; + } + + entity.UpdateIsActive(isActive); + } + + /// + /// Update the entity with the given ID with the given generic data. + /// + /// The ID of the entity. + /// The list of data to update the entity with. + public void UpdateEntityData(byte entityId, List data) { + if (_isSceneHost) { + return; + } + + if (!_entities.TryGetValue(entityId, out var entity)) { + return; + } + + entity.UpdateData(data); + } + + /// + /// Callback method for when the scene changes. + /// + /// The old scene. + /// The new scene. + private void OnSceneChanged(Scene oldScene, Scene newScene) { + Logger.Info("Clearing all registered entities"); + + foreach (var entity in _entities.Values) { + entity.Destroy(); + } + + _entities.Clear(); + + _lastId = 0; + + if (!_netClient.IsConnected) { + return; + } + + // Find all PlayMakerFSM components + foreach (var fsm in Object.FindObjectsOfType()) { + if (fsm.gameObject.scene != newScene) { + continue; + } + + Logger.Info($"Found FSM: {fsm.Fsm.Name}, {fsm.gameObject.name}"); + + if (!_validEntityFsms.TryGetValue(fsm.Fsm.Name, out var objectName)) { + continue; + } + + var fsmGameObjectName = fsm.gameObject.name; + if (!fsmGameObjectName.Contains(objectName)) { + continue; + } + + Logger.Info($"Registering entity '{fsmGameObjectName}' with ID '{_lastId}'"); + + _entities[_lastId] = new Entity( + _netClient, + _lastId, + fsm.gameObject + ); + + _lastId++; + } + + // Find all Climber components + foreach (var climber in Object.FindObjectsOfType()) { + Logger.Info($"Registering entity '{climber.name}' with ID '{_lastId}'"); + + _entities[_lastId] = new Entity( + _netClient, + _lastId, + climber.gameObject + ); + + _lastId++; + } + } + } } \ No newline at end of file diff --git a/HKMP/Version.cs b/HKMP/Version.cs index 073ade01..b320314b 100644 --- a/HKMP/Version.cs +++ b/HKMP/Version.cs @@ -1,11 +1,11 @@ -namespace Hkmp { - /// - /// Static class that stores the version. - /// - internal static class Version { - /// - /// The version as a string. - /// - public const string String = "2.1.0"; - } +namespace Hkmp { + /// + /// Static class that stores the version. + /// + internal static class Version { + /// + /// The version as a string. + /// + public const string String = "2.1.0-es"; + } } \ No newline at end of file From 95cda3f353b69aae2a7d88b3093e5f9c7c40b96a Mon Sep 17 00:00:00 2001 From: Extremelyd1 Date: Sat, 12 Nov 2022 15:02:13 +0100 Subject: [PATCH 019/216] Added a few enemies in crossroads --- HKMP/Game/Client/Entity/Entity.cs | 6 ++++ HKMP/Game/Client/Entity/EntityManager.cs | 41 +++++++++++++++--------- 2 files changed, 31 insertions(+), 16 deletions(-) diff --git a/HKMP/Game/Client/Entity/Entity.cs b/HKMP/Game/Client/Entity/Entity.cs index 16939643..25e2359e 100644 --- a/HKMP/Game/Client/Entity/Entity.cs +++ b/HKMP/Game/Client/Entity/Entity.cs @@ -266,6 +266,12 @@ private void FindComponents() { collider ); } + + // Find Walker MonoBehaviour and remove it from the client object + var walker = _object.Client.GetComponent(); + if (walker != null) { + Object.Destroy(walker); + } } /// diff --git a/HKMP/Game/Client/Entity/EntityManager.cs b/HKMP/Game/Client/Entity/EntityManager.cs index f237071b..a2479a14 100644 --- a/HKMP/Game/Client/Entity/EntityManager.cs +++ b/HKMP/Game/Client/Entity/EntityManager.cs @@ -16,18 +16,19 @@ internal class EntityManager { /// Dictionary that maps all FSM names to game object names for all valid entities. /// Valid entities are entities that should be managed by the entity system. /// - private readonly Dictionary _validEntityFsms = new() { - { "Crawler", "Crawler" }, - { "chaser", "Buzzer" }, - { "Zombie Swipe", "Zombie Runner" }, - { "Bouncer Control", "Fly" }, - { "BG Control", "Battle Gate" }, - { "spitter", "Spitter" }, - { "Zombie Guard", "Zombie Guard" }, - { "Zombie Leap", "Zombie Leaper" }, - { "Hatcher", "Hatcher" }, - { "Control", "Hatcher Baby Spawner" }, - { "ZombieShieldControl", "Zombie Shield" } // TODO: check weird position sliding + private readonly Dictionary _validEntityFsms = new() { + { "Crawler", new [] { "Crawler" } }, + { "chaser", new [] { "Buzzer" } }, + { "Zombie Swipe", new [] { "Zombie Runner", "Zombie Barger", "Zombie Hornhead" } }, + { "Bouncer Control", new [] { "Fly" } }, + { "BG Control", new [] { "Battle Gate" } }, + { "spitter", new [] { "Spitter" } }, + { "Zombie Guard", new [] { "Zombie Guard" } }, + { "Zombie Leap", new [] { "Zombie Leaper" } }, + { "Hatcher", new [] { "Hatcher" } }, + { "Control", new [] { "Hatcher Baby Spawner" } }, + { "ZombieShieldControl", new [] { "Zombie Shield" } }, + { "Worm Control", new [] { "Worm" } } }; /// @@ -214,16 +215,24 @@ private void OnSceneChanged(Scene oldScene, Scene newScene) { Logger.Info($"Found FSM: {fsm.Fsm.Name}, {fsm.gameObject.name}"); - if (!_validEntityFsms.TryGetValue(fsm.Fsm.Name, out var objectName)) { + if (!_validEntityFsms.TryGetValue(fsm.Fsm.Name, out var validObjNames)) { continue; } - var fsmGameObjectName = fsm.gameObject.name; - if (!fsmGameObjectName.Contains(objectName)) { + var fsmGameObjName = fsm.gameObject.name; + var hasValidObjName = false; + foreach (var validObjName in validObjNames) { + if (fsmGameObjName.Contains(validObjName)) { + hasValidObjName = true; + break; + } + } + + if (!hasValidObjName) { continue; } - Logger.Info($"Registering entity '{fsmGameObjectName}' with ID '{_lastId}'"); + Logger.Info($"Registering entity '{fsmGameObjName}' with ID '{_lastId}'"); _entities[_lastId] = new Entity( _netClient, From fcee6bb708f9d10af33a7531520c7fc5a99cb4ed Mon Sep 17 00:00:00 2001 From: Extremelyd1 Date: Sat, 26 Nov 2022 19:45:45 +0100 Subject: [PATCH 020/216] Register spawned entities --- .../Client/Entity/Action/EntityFsmActions.cs | 84 ++++++++++++++++++- HKMP/Game/Client/Entity/EntityManager.cs | 40 +++++---- 2 files changed, 104 insertions(+), 20 deletions(-) diff --git a/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs b/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs index 75be63ec..82aad75a 100644 --- a/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs +++ b/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs @@ -33,6 +33,11 @@ internal static class EntityFsmActions { /// public static readonly HashSet SupportedActionTypes = new(); + /// + /// Event that is called when an entity is spawned from an object. + /// + public static event Action EntitySpawnEvent; + /// /// Dictionary mapping a type of an FSM action to the corresponding method info of the "get" method in this class. /// @@ -171,6 +176,8 @@ private static bool GetNetworkDataFromAction(EntityNetworkData data, SpawnObject data.Packet.Write(euler.y); data.Packet.Write(euler.z); + EntitySpawnEvent?.Invoke(action.storeObject.Value); + return true; } @@ -187,7 +194,10 @@ private static void ApplyNetworkDataFromAction(EntityNetworkData data, SpawnObje ); if (action.gameObject != null) { - action.storeObject.Value = action.gameObject.Value.Spawn(position, Quaternion.Euler(euler)); + var spawnedObject = action.gameObject.Value.Spawn(position, Quaternion.Euler(euler)); + action.storeObject.Value = spawnedObject; + + EntitySpawnEvent?.Invoke(spawnedObject); } } @@ -351,4 +361,74 @@ private static void ApplyNetworkDataFromAction(EntityNetworkData data, SetFsmFlo } #endregion -} \ No newline at end of file + + #region SetParticleEmission + + private static bool GetNetworkDataFromAction(EntityNetworkData data, SetParticleEmission action) { + return true; + } + + private static void ApplyNetworkDataFromAction(EntityNetworkData data, SetParticleEmission action) { + if (action.emission == null) { + return; + } + + var gameObject = action.Fsm.GetOwnerDefaultTarget(action.gameObject); + if (gameObject == null) { + return; + } + + var particleSystem = gameObject.GetComponent(); + if (particleSystem == null) { + return; + } + +#pragma warning disable CS0618 + particleSystem.enableEmission = action.emission.Value; +#pragma warning restore CS0618 + } + + #endregion + + #region PlayParticleEmitter + + private static bool GetNetworkDataFromAction(EntityNetworkData data, PlayParticleEmitter action) { + return true; + } + + private static void ApplyNetworkDataFromAction(EntityNetworkData data, PlayParticleEmitter action) { + if (action.emit == null) { + return; + } + + var gameObject = action.Fsm.GetOwnerDefaultTarget(action.gameObject); + if (gameObject == null) { + return; + } + + var particleSystem = gameObject.GetComponent(); + if (particleSystem == null) { + return; + } + + if (particleSystem.isPlaying && action.emit.Value <= 0) { + particleSystem.Play(); + } else if (action.emit.Value > 0) { + particleSystem.Emit(action.emit.Value); + } + } + + #endregion + + #region SetGameObject + + private static bool GetNetworkDataFromAction(EntityNetworkData data, SetGameObject action) { + return true; + } + + private static void ApplyNetworkDataFromAction(EntityNetworkData data, SetGameObject action) { + action.variable.Value = action.gameObject.Value; + } + + #endregion +} diff --git a/HKMP/Game/Client/Entity/EntityManager.cs b/HKMP/Game/Client/Entity/EntityManager.cs index 089fe885..e9bb8e2e 100644 --- a/HKMP/Game/Client/Entity/EntityManager.cs +++ b/HKMP/Game/Client/Entity/EntityManager.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using Hkmp.Game.Client.Entity.Action; using Hkmp.Networking.Client; using Hkmp.Networking.Packet.Data; using UnityEngine; @@ -28,7 +29,9 @@ internal class EntityManager { { "Hatcher", new [] { "Hatcher" } }, { "Control", new [] { "Hatcher Baby Spawner" } }, { "ZombieShieldControl", new [] { "Zombie Shield" } }, - { "Worm Control", new [] { "Worm" } } + { "Worm Control", new [] { "Worm" } }, + { "Roller", new [] { "Roller" } }, + { "Blocker Control", new [] { "Blocker" } } }; /// @@ -57,6 +60,7 @@ public EntityManager(NetClient netClient) { _lastId = 0; + EntityFsmActions.EntitySpawnEvent += RegisterGameObjectAsEntity; UnityEngine.SceneManagement.SceneManager.activeSceneChanged += OnSceneChanged; } @@ -232,29 +236,29 @@ private void OnSceneChanged(Scene oldScene, Scene newScene) { continue; } - Logger.Info($"Registering entity '{fsmGameObjName}' with ID '{_lastId}'"); - - _entities[_lastId] = new Entity( - _netClient, - _lastId, - fsm.gameObject - ); - - _lastId++; + RegisterGameObjectAsEntity(fsm.gameObject); } // Find all Climber components foreach (var climber in Object.FindObjectsOfType()) { - Logger.Info($"Registering entity '{climber.name}' with ID '{_lastId}'"); + RegisterGameObjectAsEntity(climber.gameObject); + } + } - _entities[_lastId] = new Entity( - _netClient, - _lastId, - climber.gameObject - ); + /// + /// Register a given game object as an entity. + /// + /// The game object to register. + private void RegisterGameObjectAsEntity(GameObject gameObject) { + Logger.Info($"Registering entity '{gameObject.name}' with ID '{_lastId}'"); + + _entities[_lastId] = new Entity( + _netClient, + _lastId, + gameObject + ); - _lastId++; - } + _lastId++; } } } From 4bd953efcd96f3d075934ff8b255da8fb736488f Mon Sep 17 00:00:00 2001 From: Extremelyd1 Date: Sun, 27 Nov 2022 11:24:07 +0100 Subject: [PATCH 021/216] Improve entity spawning --- HKMP/Game/Client/Entity/Entity.cs | 6 ++ HKMP/Game/Client/Entity/EntityManager.cs | 77 +++++++++++++++++------- 2 files changed, 60 insertions(+), 23 deletions(-) diff --git a/HKMP/Game/Client/Entity/Entity.cs b/HKMP/Game/Client/Entity/Entity.cs index a81a2e9e..7eacb723 100644 --- a/HKMP/Game/Client/Entity/Entity.cs +++ b/HKMP/Game/Client/Entity/Entity.cs @@ -272,6 +272,12 @@ private void FindComponents() { if (walker != null) { Object.Destroy(walker); } + + // Find RigidBody2D MonoBehaviour and set it to be kinematic so it doesn't do physics on its own + var rigidBody = _object.Client.GetComponent(); + if (rigidBody != null) { + rigidBody.isKinematic = true; + } } /// diff --git a/HKMP/Game/Client/Entity/EntityManager.cs b/HKMP/Game/Client/Entity/EntityManager.cs index e9bb8e2e..00386ef1 100644 --- a/HKMP/Game/Client/Entity/EntityManager.cs +++ b/HKMP/Game/Client/Entity/EntityManager.cs @@ -60,7 +60,7 @@ public EntityManager(NetClient netClient) { _lastId = 0; - EntityFsmActions.EntitySpawnEvent += RegisterGameObjectAsEntity; + EntityFsmActions.EntitySpawnEvent += OnGameObjectSpawned; UnityEngine.SceneManagement.SceneManager.activeSceneChanged += OnSceneChanged; } @@ -191,6 +191,16 @@ public void UpdateEntityData(byte entityId, List data) { entity.UpdateData(data); } + /// + /// Callback method for when a game object is spawned from an existing entity. + /// + /// + private void OnGameObjectSpawned(GameObject gameObject) { + foreach (var fsm in gameObject.GetComponents()) { + ProcessGameObjectFsm(fsm, true); + } + } + /// /// Callback method for when the scene changes. /// @@ -216,27 +226,8 @@ private void OnSceneChanged(Scene oldScene, Scene newScene) { if (fsm.gameObject.scene != newScene) { continue; } - - Logger.Info($"Found FSM: {fsm.Fsm.Name}, {fsm.gameObject.name}"); - - if (!_validEntityFsms.TryGetValue(fsm.Fsm.Name, out var validObjNames)) { - continue; - } - var fsmGameObjName = fsm.gameObject.name; - var hasValidObjName = false; - foreach (var validObjName in validObjNames) { - if (fsmGameObjName.Contains(validObjName)) { - hasValidObjName = true; - break; - } - } - - if (!hasValidObjName) { - continue; - } - - RegisterGameObjectAsEntity(fsm.gameObject); + ProcessGameObjectFsm(fsm); } // Find all Climber components @@ -245,18 +236,58 @@ private void OnSceneChanged(Scene oldScene, Scene newScene) { } } + /// + /// Process the FSM of a game object to check whether the game object should be registered as an entity. + /// + /// The FSM to process. + /// Whether the game object for this FSM was spawned while in the scene, + /// instead of at scene start + private void ProcessGameObjectFsm(PlayMakerFSM fsm, bool spawnedInScene = false) { + Logger.Info($"Processing FSM: {fsm.Fsm.Name}, {fsm.gameObject.name}"); + + if (!_validEntityFsms.TryGetValue(fsm.Fsm.Name, out var validObjNames)) { + return; + } + + var fsmGameObjName = fsm.gameObject.name; + var hasValidObjName = false; + foreach (var validObjName in validObjNames) { + if (fsmGameObjName.Contains(validObjName)) { + hasValidObjName = true; + break; + } + } + + if (!hasValidObjName) { + return; + } + + RegisterGameObjectAsEntity(fsm.gameObject, spawnedInScene); + } + /// /// Register a given game object as an entity. /// /// The game object to register. - private void RegisterGameObjectAsEntity(GameObject gameObject) { + /// Whether the game object was spawned while in the scene, instead of at scene + /// start + private void RegisterGameObjectAsEntity(GameObject gameObject, bool spawnedInScene = false) { Logger.Info($"Registering entity '{gameObject.name}' with ID '{_lastId}'"); - _entities[_lastId] = new Entity( + var entity = new Entity( _netClient, _lastId, gameObject ); + _entities[_lastId] = entity; + + if (spawnedInScene) { + if (_isSceneHost) { + entity.InitializeHost(); + } else { + entity.UpdateIsActive(true); + } + } _lastId++; } From 0e1d4c3ae8c3cac5f92a13d4b2af037d2f0cbee0 Mon Sep 17 00:00:00 2001 From: Extremelyd1 Date: Sun, 27 Nov 2022 22:37:02 +0100 Subject: [PATCH 022/216] Initialze spawned entity properly --- .../Client/Entity/Action/EntityFsmActions.cs | 110 ++++++++++++++++++ HKMP/Game/Client/Entity/Entity.cs | 6 +- HKMP/Game/Client/Entity/EntityInitializer.cs | 43 +++++++ HKMP/Game/Client/Entity/EntityManager.cs | 7 ++ 4 files changed, 163 insertions(+), 3 deletions(-) create mode 100644 HKMP/Game/Client/Entity/EntityInitializer.cs diff --git a/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs b/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs index 82aad75a..485510ce 100644 --- a/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs +++ b/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs @@ -6,6 +6,8 @@ using HutongGames.PlayMaker.Actions; using UnityEngine; using Logger = Hkmp.Logging.Logger; +// ReSharper disable UnusedMember.Local +// ReSharper disable UnusedParameter.Local namespace Hkmp.Game.Client.Entity.Action; @@ -431,4 +433,112 @@ private static void ApplyNetworkDataFromAction(EntityNetworkData data, SetGameOb } #endregion + + #region GetOwner + + private static bool GetNetworkDataFromAction(EntityNetworkData data, GetOwner action) { + return true; + } + + private static void ApplyNetworkDataFromAction(EntityNetworkData data, GetOwner action) { + action.storeGameObject.Value = action.Owner; + } + + #endregion + + #region GetHero + + private static bool GetNetworkDataFromAction(EntityNetworkData data, GetHero action) { + return true; + } + + private static void ApplyNetworkDataFromAction(EntityNetworkData data, GetHero action) { + var heroController = HeroController.instance; + action.storeResult.Value = heroController == null ? null : heroController.gameObject; + } + + #endregion + + #region FindChild + + private static bool GetNetworkDataFromAction(EntityNetworkData data, FindChild action) { + return true; + } + + private static void ApplyNetworkDataFromAction(EntityNetworkData data, FindChild action) { + var gameObject = action.Fsm.GetOwnerDefaultTarget(action.gameObject); + if (gameObject == null) { + return; + } + + var transform = gameObject.transform.Find(action.childName.Value); + action.storeResult.Value = transform == null ? null : transform.gameObject; + } + + #endregion + + #region FindGameObject + + private static bool GetNetworkDataFromAction(EntityNetworkData data, FindGameObject action) { + return true; + } + + private static void ApplyNetworkDataFromAction(EntityNetworkData data, FindGameObject action) { + if (action.withTag.Value == "Untagged") { + action.store.Value = GameObject.Find(action.objectName.Value); + return; + } + + if (string.IsNullOrEmpty(action.objectName.Value)) { + action.store.Value = GameObject.FindGameObjectWithTag(action.withTag.Value); + return; + } + + foreach (var gameObject in GameObject.FindGameObjectsWithTag(action.withTag.Value)) { + if (gameObject.name == action.objectName.Value) { + action.store.Value = gameObject; + return; + } + } + + action.store.Value = null; + } + + #endregion + + #region FindAlertRange + + private static bool GetNetworkDataFromAction(EntityNetworkData data, FindAlertRange action) { + return true; + } + + private static void ApplyNetworkDataFromAction(EntityNetworkData data, FindAlertRange action) { + action.storeResult.Value = AlertRange.Find(action.target.GetSafe(action), action.childName); + } + + #endregion + + #region GetParent + + private static bool GetNetworkDataFromAction(EntityNetworkData data, GetParent action) { + return true; + } + + private static void ApplyNetworkDataFromAction(EntityNetworkData data, GetParent action) { + var gameObject = action.Fsm.GetOwnerDefaultTarget(action.gameObject); + if (gameObject == null) { + action.storeResult.Value = null; + return; + } + + var parent = gameObject.transform.parent; + if (parent == null) { + action.storeResult.Value = null; + return; + } + + action.storeResult.Value = parent.gameObject; + } + + #endregion } diff --git a/HKMP/Game/Client/Entity/Entity.cs b/HKMP/Game/Client/Entity/Entity.cs index 7eacb723..60bedc42 100644 --- a/HKMP/Game/Client/Entity/Entity.cs +++ b/HKMP/Game/Client/Entity/Entity.cs @@ -199,7 +199,7 @@ private void ProcessHostFsm(PlayMakerFSM fsm) { StateIndex = i, ActionIndex = j }; - Logger.Info($"Created hooked action: {action.GetType()}, {_fsms.Host.IndexOf(fsm)}, {i}, {j}"); + Logger.Info($"Created hooked action: {action.GetType()}, {_fsms.Host.IndexOf(fsm)}, {state.Name}, {j}"); if (!_hookedTypes.Contains(action.GetType())) { _hookedTypes.Add(action.GetType()); @@ -603,10 +603,10 @@ public void UpdateData(List entityNetworkData) { actionIndex = data.Packet.ReadByte(); } - Logger.Info($"Received entity network data for FSM: {fsm.Fsm.Name}, {stateIndex}, {actionIndex}"); - var state = fsm.FsmStates[stateIndex]; var action = state.Actions[actionIndex]; + + Logger.Info($"Received entity network data for FSM: {fsm.Fsm.Name}, {state.Name}, {actionIndex} ({action.GetType()})"); EntityFsmActions.ApplyNetworkDataFromAction(data, action); diff --git a/HKMP/Game/Client/Entity/EntityInitializer.cs b/HKMP/Game/Client/Entity/EntityInitializer.cs new file mode 100644 index 00000000..acee6394 --- /dev/null +++ b/HKMP/Game/Client/Entity/EntityInitializer.cs @@ -0,0 +1,43 @@ +using System.Linq; +using Hkmp.Game.Client.Entity.Action; +using Hkmp.Networking.Packet.Data; + +namespace Hkmp.Game.Client.Entity; + +/// +/// Class that manages initializing client-side entities to ensure they have correct references within FSM actions +/// to game objects (such as child objects). +/// +internal static class EntityInitializer { + /// + /// Array of state names that indicates that it is a initializing state. + /// + private static readonly string[] InitStateNames = { + "init", + "initiate", + "initialise", + "initialize" + }; + + /// + /// Initialize the FSM of a client entity by finding initialize states and executing the actions in those states. + /// + /// The FSM to initialize. + public static void InitializeClientFsm(PlayMakerFSM fsm) { + // Check for all states whether they are initialize states + foreach (var state in fsm.FsmStates) { + if (!InitStateNames.Contains(state.Name.ToLower())) { + continue; + } + + // Go over each action and try to execute it by applying empty data to it + foreach (var action in state.Actions) { + if (EntityFsmActions.SupportedActionTypes.Contains(action.GetType())) { + var data = new EntityNetworkData(); + EntityFsmActions.ApplyNetworkDataFromAction(data, action); + } + } + } + } + +} diff --git a/HKMP/Game/Client/Entity/EntityManager.cs b/HKMP/Game/Client/Entity/EntityManager.cs index 00386ef1..f2a7e7cf 100644 --- a/HKMP/Game/Client/Entity/EntityManager.cs +++ b/HKMP/Game/Client/Entity/EntityManager.cs @@ -262,6 +262,13 @@ private void ProcessGameObjectFsm(PlayMakerFSM fsm, bool spawnedInScene = false) return; } + // If this entity spawned while in the scene already and we are a scene client, we need to initialize + // the FSM of the entity manually + if (spawnedInScene && !_isSceneHost) { + Logger.Info($"Manually initializing client entity FSM: {fsm.Fsm.Name}, {fsm.gameObject.name}"); + EntityInitializer.InitializeClientFsm(fsm); + } + RegisterGameObjectAsEntity(fsm.gameObject, spawnedInScene); } From 939829e5bce9246b4c4df6ed981ceef401f048a5 Mon Sep 17 00:00:00 2001 From: Extremelyd1 Date: Sat, 3 Dec 2022 21:58:16 +0100 Subject: [PATCH 023/216] Persistent entity spawning Fix map icon issue --- .editorconfig | 4 +- HKMP/Game/Client/ClientManager.cs | 14 ++ .../Client/Entity/Action/EntityFsmActions.cs | 9 +- HKMP/Game/Client/Entity/Entity.cs | 16 ++ HKMP/Game/Client/Entity/EntityManager.cs | 140 ++++++++++++------ HKMP/Game/Client/Entity/EntityRegistry.cs | 101 +++++++++++++ HKMP/Game/Client/Entity/EntitySpawner.cs | 71 +++++++++ HKMP/Game/Client/Entity/EntityType.cs | 24 +++ HKMP/Game/Client/MapManager.cs | 11 +- HKMP/Game/Server/ServerEntityData.cs | 18 ++- HKMP/Game/Server/ServerEntityKey.cs | 2 +- HKMP/Game/Server/ServerManager.cs | 65 +++++++- HKMP/HKMP.csproj | 1 + HKMP/Networking/Client/ClientUpdateManager.cs | 27 ++++ HKMP/Networking/Packet/Data/EntitySpawn.cs | 43 ++++++ .../Packet/Data/PlayerEnterScene.cs | 26 ++++ HKMP/Networking/Packet/PacketId.cs | 14 +- HKMP/Networking/Packet/UpdatePacket.cs | 26 +++- HKMP/Networking/Server/ServerUpdateManager.cs | 29 ++++ HKMP/Resource/entity-registry.json | 87 +++++++++++ HKMP/Util/FileUtil.cs | 24 +++ 21 files changed, 693 insertions(+), 59 deletions(-) create mode 100644 HKMP/Game/Client/Entity/EntityRegistry.cs create mode 100644 HKMP/Game/Client/Entity/EntitySpawner.cs create mode 100644 HKMP/Networking/Packet/Data/EntitySpawn.cs create mode 100644 HKMP/Resource/entity-registry.json diff --git a/.editorconfig b/.editorconfig index 34a262b1..3faba73a 100644 --- a/.editorconfig +++ b/.editorconfig @@ -63,11 +63,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/HKMP/Game/Client/ClientManager.cs b/HKMP/Game/Client/ClientManager.cs index 55d1573b..0f79d49d 100644 --- a/HKMP/Game/Client/ClientManager.cs +++ b/HKMP/Game/Client/ClientManager.cs @@ -212,6 +212,7 @@ ModSettings modSettings packetManager.RegisterClientPacketHandler(ClientPacketId.PlayerUpdate, OnPlayerUpdate); packetManager.RegisterClientPacketHandler(ClientPacketId.PlayerMapUpdate, OnPlayerMapUpdate); + packetManager.RegisterClientPacketHandler(ClientPacketId.EntitySpawn, OnEntitySpawn); packetManager.RegisterClientPacketHandler(ClientPacketId.EntityUpdate, OnEntityUpdate); packetManager.RegisterClientPacketHandler(ClientPacketId.GameSettingsUpdated, OnGameSettingsUpdated); @@ -596,6 +597,11 @@ private void OnPlayerAlreadyInScene(ClientPlayerAlreadyInScene alreadyInScene) { _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); + } + foreach (var entityUpdate in alreadyInScene.EntityUpdateList) { Logger.Info($"Updating already in scene entity with ID: {entityUpdate.Id}"); HandleEntityUpdate(entityUpdate, true); @@ -715,6 +721,14 @@ 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) { + _entityManager.SpawnEntity(entitySpawn.Id, entitySpawn.SpawningType, entitySpawn.SpawnedType); + } + /// /// Callback method for when an entity update is received. /// diff --git a/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs b/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs index 485510ce..49d70d78 100644 --- a/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs +++ b/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs @@ -38,7 +38,7 @@ internal static class EntityFsmActions { /// /// Event that is called when an entity is spawned from an object. /// - public static event Action EntitySpawnEvent; + public static event Action EntitySpawnEvent; /// /// Dictionary mapping a type of an FSM action to the corresponding method info of the "get" method in this class. @@ -178,7 +178,7 @@ private static bool GetNetworkDataFromAction(EntityNetworkData data, SpawnObject data.Packet.Write(euler.y); data.Packet.Write(euler.z); - EntitySpawnEvent?.Invoke(action.storeObject.Value); + EntitySpawnEvent?.Invoke(action, action.storeObject.Value); return true; } @@ -199,7 +199,10 @@ private static void ApplyNetworkDataFromAction(EntityNetworkData data, SpawnObje var spawnedObject = action.gameObject.Value.Spawn(position, Quaternion.Euler(euler)); action.storeObject.Value = spawnedObject; - EntitySpawnEvent?.Invoke(spawnedObject); + // TODO: this might give an issue if the packets for two of these actions get out of order and the IDs + // of the spawned entities get switched. This only holds in the case where two different entities are + // spawned + EntitySpawnEvent?.Invoke(action, spawnedObject); } } diff --git a/HKMP/Game/Client/Entity/Entity.cs b/HKMP/Game/Client/Entity/Entity.cs index 60bedc42..3836a2cb 100644 --- a/HKMP/Game/Client/Entity/Entity.cs +++ b/HKMP/Game/Client/Entity/Entity.cs @@ -30,6 +30,11 @@ internal class Entity { /// private readonly byte _entityId; + /// + /// The type of the entity. + /// + public EntityType Type { get; } + /// /// Host-client pair for the game objects. /// @@ -95,11 +100,14 @@ internal class Entity { public Entity( NetClient netClient, byte entityId, + EntityType type, GameObject hostObject ) { _netClient = netClient; _entityId = entityId; + Type = type; + _isControlled = true; _object = new HostClientPair { @@ -630,5 +638,13 @@ public void Destroy() { component.Destroy(); } } + + /// + /// Get the list of client FSMs. + /// + /// A list containing the client FSM instances. + public List GetClientFsms() { + return _fsms.Client; + } } } diff --git a/HKMP/Game/Client/Entity/EntityManager.cs b/HKMP/Game/Client/Entity/EntityManager.cs index f2a7e7cf..2d3192f9 100644 --- a/HKMP/Game/Client/Entity/EntityManager.cs +++ b/HKMP/Game/Client/Entity/EntityManager.cs @@ -2,6 +2,7 @@ using Hkmp.Game.Client.Entity.Action; using Hkmp.Networking.Client; using Hkmp.Networking.Packet.Data; +using HutongGames.PlayMaker; using UnityEngine; using UnityEngine.SceneManagement; using Vector2 = Hkmp.Math.Vector2; @@ -14,30 +15,14 @@ namespace Hkmp.Game.Client.Entity { /// internal class EntityManager { /// - /// Dictionary that maps all FSM names to game object names for all valid entities. - /// Valid entities are entities that should be managed by the entity system. + /// The net client for networking. /// - private readonly Dictionary _validEntityFsms = new() { - { "Crawler", new [] { "Crawler" } }, - { "chaser", new [] { "Buzzer" } }, - { "Zombie Swipe", new [] { "Zombie Runner", "Zombie Barger", "Zombie Hornhead" } }, - { "Bouncer Control", new [] { "Fly" } }, - { "BG Control", new [] { "Battle Gate" } }, - { "spitter", new [] { "Spitter" } }, - { "Zombie Guard", new [] { "Zombie Guard" } }, - { "Zombie Leap", new [] { "Zombie Leaper" } }, - { "Hatcher", new [] { "Hatcher" } }, - { "Control", new [] { "Hatcher Baby Spawner" } }, - { "ZombieShieldControl", new [] { "Zombie Shield" } }, - { "Worm Control", new [] { "Worm" } }, - { "Roller", new [] { "Roller" } }, - { "Blocker Control", new [] { "Blocker" } } - }; + private readonly NetClient _netClient; /// - /// The net client for networking. + /// The entity registry for lookups of game object names, FSM names and entity types. /// - private readonly NetClient _netClient; + private readonly EntityRegistry _entityRegistry; /// /// Dictionary mapping entity IDs to their respective entity instances. @@ -56,9 +41,12 @@ internal class EntityManager { public EntityManager(NetClient netClient) { _netClient = netClient; + _entityRegistry = new EntityRegistry(); _entities = new Dictionary(); _lastId = 0; + + _entityRegistry.LoadRegistry(); EntityFsmActions.EntitySpawnEvent += OnGameObjectSpawned; UnityEngine.SceneManagement.SceneManager.activeSceneChanged += OnSceneChanged; @@ -99,6 +87,50 @@ public void BecomeSceneHost() { } } + /// + /// Spawn an entity with the given ID and type, that was spawned from the given entity type. + /// + /// The ID of the entity. + /// The type of the entity that spawned the new entity. + /// The type of the spawned entity. + public void SpawnEntity(byte id, EntityType spawningType, EntityType spawnedType) { + Logger.Info($"Trying to spawn entity with ID {id} with types: {spawningType}, {spawnedType}"); + + if (_entities.ContainsKey(id)) { + Logger.Info($" Entity with ID {id} already exists, assuming it has been spawned by action"); + return; + } + + List clientFsms = null; + foreach (var existingEntity in _entities.Values) { + if (existingEntity.Type == spawningType) { + clientFsms = existingEntity.GetClientFsms(); + break; + } + } + + if (clientFsms == null) { + Logger.Warn($"Could not find entity with same type for spawning"); + return; + } + + var gameObject = EntitySpawner.SpawnEntityGameObject(spawningType, spawnedType, clientFsms); + + foreach (var fsm in gameObject.GetComponents()) { + if (!_entityRegistry.TryGetEntry(gameObject.name, fsm.Fsm.Name, out _)) { + EntityInitializer.InitializeClientFsm(fsm); + } + } + + var entity = new Entity( + _netClient, + id, + spawnedType, + gameObject + ); + _entities[id] = entity; + } + /// /// Update the position for the entity with the given ID. /// @@ -194,10 +226,11 @@ public void UpdateEntityData(byte entityId, List data) { /// /// Callback method for when a game object is spawned from an existing entity. /// - /// - private void OnGameObjectSpawned(GameObject gameObject) { + /// The action from which the game object was spawned. + /// The game object that was spawned. + private void OnGameObjectSpawned(FsmStateAction action, GameObject gameObject) { foreach (var fsm in gameObject.GetComponents()) { - ProcessGameObjectFsm(fsm, true); + ProcessGameObjectFsm(fsm, true, action.Fsm.FsmComponent); } } @@ -232,7 +265,11 @@ private void OnSceneChanged(Scene oldScene, Scene newScene) { // Find all Climber components foreach (var climber in Object.FindObjectsOfType()) { - RegisterGameObjectAsEntity(climber.gameObject); + if (!_entityRegistry.TryGetEntry(climber.gameObject.name, EntityType.Tiktik, out var entry)) { + continue; + } + + RegisterGameObjectAsEntity(climber.gameObject, entry.Type); } } @@ -242,23 +279,16 @@ private void OnSceneChanged(Scene oldScene, Scene newScene) { /// The FSM to process. /// Whether the game object for this FSM was spawned while in the scene, /// instead of at scene start - private void ProcessGameObjectFsm(PlayMakerFSM fsm, bool spawnedInScene = false) { + /// If spawnedInScene, then this is the FSM of the entity that spawned it; + /// otherwise null. + private void ProcessGameObjectFsm( + PlayMakerFSM fsm, + bool spawnedInScene = false, + PlayMakerFSM spawningFsm = null + ) { Logger.Info($"Processing FSM: {fsm.Fsm.Name}, {fsm.gameObject.name}"); - if (!_validEntityFsms.TryGetValue(fsm.Fsm.Name, out var validObjNames)) { - return; - } - - var fsmGameObjName = fsm.gameObject.name; - var hasValidObjName = false; - foreach (var validObjName in validObjNames) { - if (fsmGameObjName.Contains(validObjName)) { - hasValidObjName = true; - break; - } - } - - if (!hasValidObjName) { + if (!_entityRegistry.TryGetEntry(fsm.gameObject.name, fsm.Fsm.Name, out var entry)) { return; } @@ -269,21 +299,47 @@ private void ProcessGameObjectFsm(PlayMakerFSM fsm, bool spawnedInScene = false) EntityInitializer.InitializeClientFsm(fsm); } - RegisterGameObjectAsEntity(fsm.gameObject, spawnedInScene); + RegisterGameObjectAsEntity(fsm.gameObject, entry.Type, spawnedInScene, spawningFsm); } /// /// Register a given game object as an entity. /// /// The game object to register. + /// The type of the entity. /// Whether the game object was spawned while in the scene, instead of at scene /// start - private void RegisterGameObjectAsEntity(GameObject gameObject, bool spawnedInScene = false) { - Logger.Info($"Registering entity '{gameObject.name}' with ID '{_lastId}'"); + /// If spawnedInScene, then this is the FSM of the entity that spawned it; + /// otherwise null. + private void RegisterGameObjectAsEntity( + GameObject gameObject, + EntityType type, + bool spawnedInScene = false, + PlayMakerFSM spawningFsm = null + ) { + // First find a usable ID that is not registered to an entity already + while (_entities.ContainsKey(_lastId)) { + _lastId++; + } + Logger.Info($"Registering entity ({type}) '{gameObject.name}' with ID '{_lastId}'"); + + if (spawnedInScene && _isSceneHost) { + var spawningObjectName = spawningFsm!.gameObject.name; + var spawningFsmName = spawningFsm!.Fsm.Name; + if (_entityRegistry.TryGetEntry(spawningObjectName, spawningFsmName, out var entry)) { + Logger.Info($"Notifying server of entity ({spawningObjectName}, {entry.Type}) spawning entity ({gameObject.name}, {type}) with ID {_lastId}"); + _netClient.UpdateManager.SetEntitySpawn(_lastId, entry.Type, type); + } + } + + // TODO: maybe we need to check whether this entity game object has already been registered, which can + // happen with game objects that have multiple FSMs + var entity = new Entity( _netClient, _lastId, + type, gameObject ); _entities[_lastId] = entity; diff --git a/HKMP/Game/Client/Entity/EntityRegistry.cs b/HKMP/Game/Client/Entity/EntityRegistry.cs new file mode 100644 index 00000000..69fb4b96 --- /dev/null +++ b/HKMP/Game/Client/Entity/EntityRegistry.cs @@ -0,0 +1,101 @@ +using System.Collections.Generic; +using Hkmp.Logging; +using Hkmp.Util; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; + +namespace Hkmp.Game.Client.Entity; + +/// +/// Class that manages loading and storing of entity data. Such as names of game objects, names of FSMs and +/// corresponding types. +/// +internal class EntityRegistry { + /// + /// The file path of the embedded resource file for the entity registry. + /// + private const string EntityRegistryFilePath = "Hkmp.Resource.entity-registry.json"; + + /// + /// List of all entity registry entries that are loaded from the embedded file. + /// + private List Entries { get; set; } + + public void LoadRegistry() { + Entries = FileUtil.LoadObjectFromEmbeddedJson>(EntityRegistryFilePath); + if (Entries == null) { + Logger.Warn("Could not load entity registry"); + } + } + + /// + /// Try to get the entity registry entry given a game object name and a FSM name. + /// + /// The name of the game object. + /// The name of the FSM. + /// The entry if it is found; otherwise null. + /// True if the entry was found; otherwise false. + public bool TryGetEntry(string gameObjectName, string fsmName, out EntityRegistryEntry foundEntry) { + foreach (var entry in Entries) { + if (!entry.FsmName.Equals(fsmName)) { + continue; + } + + if (gameObjectName.Contains(entry.BaseObjectName)) { + foundEntry = entry; + return true; + } + } + + foundEntry = null; + return false; + } + + /// + /// Try to get the entity registry entry given a game object name and an entity type. + /// + /// The name of the game object. + /// The type of the entity. + /// The entry if it is found; otherwise null. + /// True if the entry was found; otherwise false. + public bool TryGetEntry(string gameObjectName, EntityType type, out EntityRegistryEntry foundEntry) { + foreach (var entry in Entries) { + if (!entry.Type.Equals(type)) { + continue; + } + + if (gameObjectName.Contains(entry.BaseObjectName)) { + foundEntry = entry; + return true; + } + } + + foundEntry = null; + return false; + } +} + +/// +/// Class representing a single entry in the entity registry that contains the relevant data for an entity type. +/// +internal class EntityRegistryEntry { + /// + /// The base of the game object name of the entity. + /// For example: "Zombie Leaper", which in-game can be represented as "Zombie Leaper (Clone) (1)" + /// + [JsonProperty("base_object_name")] + public string BaseObjectName { get; set; } + + /// + /// The type of the entity. + /// + [JsonConverter(typeof(StringEnumConverter))] + [JsonProperty("type")] + public EntityType Type { get; set; } + + /// + /// The name of the FSM that this entity has. Can be empty if the entity does not have a FSM. + /// + [JsonProperty("fsm_name")] + public string FsmName { get; set; } +} diff --git a/HKMP/Game/Client/Entity/EntitySpawner.cs b/HKMP/Game/Client/Entity/EntitySpawner.cs new file mode 100644 index 00000000..9941ef30 --- /dev/null +++ b/HKMP/Game/Client/Entity/EntitySpawner.cs @@ -0,0 +1,71 @@ +using System.Collections.Generic; +using Hkmp.Util; +using HutongGames.PlayMaker.Actions; +using UnityEngine; +using Logger = Hkmp.Logging.Logger; + +namespace Hkmp.Game.Client.Entity; + +/// +/// Static class that has implementations for spawning entities that are usually spawned by other entities in-game. +/// +internal static class EntitySpawner { + /// + /// Spawn the game object for an entity with the given type that is spawned from the other given type. + /// + /// The type of the entity that spawns the new entity. + /// The type of the spawned entity. + /// The list of client FSMs to spawn the game object. + /// The game object for the spawned entity. + public static GameObject SpawnEntityGameObject( + EntityType spawningType, + EntityType spawnedType, + List clientFsms + ) { + Logger.Info($"Trying to spawn entity game object for: {spawningType}, {spawnedType}"); + + if (spawningType == EntityType.ElderBaldur && spawnedType == EntityType.Baldur) { + return SpawnBaldurGameObject(clientFsms); + } + + return null; + } + + private static GameObject SpawnBaldurGameObject(List clientFsms) { + var fsm = clientFsms[0]; + + var setGameObjectAction = fsm.GetFirstAction("Roller"); + var spawnAction = fsm.GetFirstAction("Fire"); + + var gameObject = setGameObjectAction.gameObject.Value; + + var position = Vector3.zero; + var euler = Vector3.up; + if (spawnAction.spawnPoint.Value != null) { + position = spawnAction.spawnPoint.Value.transform.position; + if (!spawnAction.position.IsNone) { + position += spawnAction.position.Value; + } + + if (spawnAction.rotation.IsNone) { + euler = spawnAction.spawnPoint.Value.transform.eulerAngles; + } else { + euler = spawnAction.rotation.Value; + } + } else { + if (!spawnAction.position.IsNone) { + position = spawnAction.position.Value; + } + + if (!spawnAction.rotation.IsNone) { + euler = spawnAction.rotation.Value; + } + } + + var spawnedObject = gameObject.Spawn(position, Quaternion.Euler(euler)); + spawnAction.storeObject.Value = spawnedObject; + + return spawnedObject; + } + +} diff --git a/HKMP/Game/Client/Entity/EntityType.cs b/HKMP/Game/Client/Entity/EntityType.cs index e69de29b..097a3b7c 100644 --- a/HKMP/Game/Client/Entity/EntityType.cs +++ b/HKMP/Game/Client/Entity/EntityType.cs @@ -0,0 +1,24 @@ +namespace Hkmp.Game.Client.Entity; + +/// +/// Enumeration of entity types. The entries more closely resemble the canonical name rather than the internal naming. +/// +internal enum EntityType { + Crawlid = 0, + Tiktik, + Vengefly, + WanderingHusk, + HuskBully, + HuskHornhead, + Gruzzer, + BattleGate, + AspidHunter, + HuskGuard, + LeapingHusk, + AspidMother, + AspidHatchling, + HuskWarrior, + Goam, + Baldur, + ElderBaldur +} diff --git a/HKMP/Game/Client/MapManager.cs b/HKMP/Game/Client/MapManager.cs index 71fbf319..61c6f5e3 100644 --- a/HKMP/Game/Client/MapManager.cs +++ b/HKMP/Game/Client/MapManager.cs @@ -181,8 +181,15 @@ private bool TryGetMapLocation(out Vector3 mapLocation) { 0f ); - var size = sceneObject.GetComponent().sprite.bounds.size; - var gameMapScale = gameMap.transform.localScale; + var spriteRenderer = sceneObject.GetComponent(); + if (spriteRenderer == null) { + // The sprite renderer being null happens in some transitions, but does not mean the player does + // not have a map icon anymore + mapLocation = _lastPosition; + return true; + } + + var size = spriteRenderer.sprite.bounds.size; Vector3 position; diff --git a/HKMP/Game/Server/ServerEntityData.cs b/HKMP/Game/Server/ServerEntityData.cs index 713201f0..12b2e14c 100644 --- a/HKMP/Game/Server/ServerEntityData.cs +++ b/HKMP/Game/Server/ServerEntityData.cs @@ -1,13 +1,27 @@ using System.Collections.Generic; +using Hkmp.Game.Client.Entity; using Hkmp.Math; using Hkmp.Networking.Packet.Data; -namespace Hkmp.Game.Server; +namespace Hkmp.Game.Server; /// /// Class containing all the relevant data managed by the server about an entity. /// internal class ServerEntityData { + /// + /// Whether this entity spawned while in a scene already. + /// + public bool Spawned { get; set; } + /// + /// The type of the entity that spawned the new entity. + /// + public EntityType SpawningType { get; set; } + /// + /// The type of the entity that was spawned. + /// + public EntityType SpawnedType { get; set; } + /// /// The last position of the entity. /// @@ -37,4 +51,4 @@ internal class ServerEntityData { public ServerEntityData() { GenericData = new List(); } -} \ No newline at end of file +} diff --git a/HKMP/Game/Server/ServerEntityKey.cs b/HKMP/Game/Server/ServerEntityKey.cs index 46c420bf..da85f1b3 100644 --- a/HKMP/Game/Server/ServerEntityKey.cs +++ b/HKMP/Game/Server/ServerEntityKey.cs @@ -43,4 +43,4 @@ public override int GetHashCode() { return hashCode; } } -} \ No newline at end of file +} diff --git a/HKMP/Game/Server/ServerManager.cs b/HKMP/Game/Server/ServerManager.cs index ef16bda0..fd21e29d 100644 --- a/HKMP/Game/Server/ServerManager.cs +++ b/HKMP/Game/Server/ServerManager.cs @@ -126,6 +126,7 @@ PacketManager packetManager packetManager.RegisterServerPacketHandler(ServerPacketId.PlayerUpdate, OnPlayerUpdate); packetManager.RegisterServerPacketHandler(ServerPacketId.PlayerMapUpdate, OnPlayerMapUpdate); + packetManager.RegisterServerPacketHandler(ServerPacketId.EntitySpawn, OnEntitySpawn); packetManager.RegisterServerPacketHandler(ServerPacketId.EntityUpdate, OnEntityUpdate); packetManager.RegisterServerPacketHandler(ServerPacketId.PlayerDisconnect, OnPlayerDisconnect); packetManager.RegisterServerPacketHandler(ServerPacketId.PlayerDeath, OnPlayerDeath); @@ -360,6 +361,7 @@ private void OnClientEnterScene(ServerPlayerData playerData) { } } + var entitySpawnList = new List(); var entityUpdateList = new List(); foreach (var keyDataPair in _entityData) { @@ -369,10 +371,22 @@ private void OnClientEnterScene(ServerPlayerData playerData) { if (!entityKey.Scene.Equals(playerData.CurrentScene)) { continue; } + + var entityData = keyDataPair.Value; + if (entityData.Spawned) { + Logger.Info($"Sending that entity '{entityKey.EntityId}' has spawned in the scene to '{playerData.Id}'"); + + var entitySpawn = new EntitySpawn { + Id = entityKey.EntityId, + SpawningType = entityData.SpawningType, + SpawnedType = entityData.SpawnedType + }; + + entitySpawnList.Add(entitySpawn); + } Logger.Info($"Sending that entity '{entityKey.EntityId}' is already in scene to '{playerData.Id}'"); - var entityData = keyDataPair.Value; var entityUpdate = new EntityUpdate { Id = entityKey.EntityId, Position = entityData.Position, @@ -401,6 +415,7 @@ private void OnClientEnterScene(ServerPlayerData playerData) { _netServer.GetUpdateManagerForClient(playerData.Id)?.AddPlayerAlreadyInSceneData( enterSceneList, + entitySpawnList, entityUpdateList, !alreadyPlayersInScene ); @@ -545,6 +560,54 @@ private void OnPlayerMapUpdate(ushort id, PlayerMapUpdate playerMapUpdate) { } } + /// + /// Callback method for when an entity spawn is received from a player. + /// + /// The ID of the player. + /// The EntitySpawn packet data. + private void OnEntitySpawn(ushort id, EntitySpawn entitySpawn) { + if (!_playerData.TryGetValue(id, out var playerData)) { + Logger.Info($"Received EntitySpawn data, but player with ID {id} is not in mapping"); + return; + } + + // If the player is not the scene host, ignore this data + if (!playerData.IsSceneHost) { + return; + } + + // Create the key for the entity data + var serverEntityKey = new ServerEntityKey( + playerData.CurrentScene, + entitySpawn.Id + ); + + // Check with the created key whether we have an existing entry + if (!_entityData.TryGetValue(serverEntityKey, out var entityData)) { + // If the entry for this entity did not yet exist, we insert a new one + entityData = new ServerEntityData(); + _entityData[serverEntityKey] = entityData; + } + + Logger.Info($"Received EntitySpawn from {id}, with entity {entitySpawn.Id}, {entitySpawn.SpawningType}, {entitySpawn.SpawnedType}"); + + entityData.Spawned = true; + entityData.SpawningType = entitySpawn.SpawningType; + entityData.SpawnedType = entitySpawn.SpawnedType; + + SendDataInSameScene( + id, + playerData.CurrentScene, + otherId => { + _netServer.GetUpdateManagerForClient(otherId)?.SetEntitySpawn( + entitySpawn.Id, + entitySpawn.SpawningType, + entitySpawn.SpawnedType + ); + } + ); + } + /// /// Callback method for when an entity update is received from a player. /// diff --git a/HKMP/HKMP.csproj b/HKMP/HKMP.csproj index 5e75c29f..cebed9f7 100644 --- a/HKMP/HKMP.csproj +++ b/HKMP/HKMP.csproj @@ -19,6 +19,7 @@ + diff --git a/HKMP/Networking/Client/ClientUpdateManager.cs b/HKMP/Networking/Client/ClientUpdateManager.cs index 10c931f7..a9c89d52 100644 --- a/HKMP/Networking/Client/ClientUpdateManager.cs +++ b/HKMP/Networking/Client/ClientUpdateManager.cs @@ -4,6 +4,7 @@ using System.Net.Sockets; using Hkmp.Animation; using Hkmp.Game; +using Hkmp.Game.Client.Entity; using Hkmp.Math; using Hkmp.Networking.Packet; using Hkmp.Networking.Packet.Data; @@ -149,6 +150,32 @@ public void UpdatePlayerAnimation(AnimationClip clip, int frame = 0, bool[] effe } } + /// + /// Set entity spawn data for an entity that spawned later in the scene. + /// + /// The ID of the entity. + /// The type of the entity that spawned the new entity. + /// The type of the entity that was spawned. + public void SetEntitySpawn(byte id, EntityType spawningType, EntityType spawnedType) { + lock (Lock) { + PacketDataCollection entitySpawnCollection; + + // Check if there is an existing data collection or create one if not + if (CurrentUpdatePacket.TryGetSendingPacketData(ServerPacketId.EntitySpawn, out var packetData)) { + entitySpawnCollection = (PacketDataCollection) packetData; + } else { + entitySpawnCollection = new PacketDataCollection(); + CurrentUpdatePacket.SetSendingPacketData(ServerPacketId.EntitySpawn, entitySpawnCollection); + } + + entitySpawnCollection.DataInstances.Add(new EntitySpawn { + Id = id, + SpawningType = spawningType, + SpawnedType = spawnedType + }); + } + } + /// /// Find an existing or create a new EntityUpdate instance in the current update packet. /// diff --git a/HKMP/Networking/Packet/Data/EntitySpawn.cs b/HKMP/Networking/Packet/Data/EntitySpawn.cs new file mode 100644 index 00000000..b8143919 --- /dev/null +++ b/HKMP/Networking/Packet/Data/EntitySpawn.cs @@ -0,0 +1,43 @@ +using Hkmp.Game.Client.Entity; + +namespace Hkmp.Networking.Packet.Data; + +/// +/// Packet data for an entity spawn. Either from entering a scene or from spawning while in a scene. +/// +internal class EntitySpawn : IPacketData { + /// + public bool IsReliable => true; + + /// + public bool DropReliableDataIfNewerExists => false; + + /// + /// The ID of the spawned entity. + /// + public byte Id { get; set; } + + /// + /// The type of the entity that spawned the new entity. + /// + public EntityType SpawningType { get; set; } + + /// + /// The type of the entity that was spawned. + /// + public EntityType SpawnedType { get; set; } + + /// + public void WriteData(IPacket packet) { + packet.Write(Id); + packet.Write((ushort) SpawningType); + packet.Write((ushort) SpawnedType); + } + + /// + public void ReadData(IPacket packet) { + Id = packet.ReadByte(); + SpawningType = (EntityType) packet.ReadUShort(); + SpawnedType = (EntityType) packet.ReadUShort(); + } +} diff --git a/HKMP/Networking/Packet/Data/PlayerEnterScene.cs b/HKMP/Networking/Packet/Data/PlayerEnterScene.cs index b47b12a7..9a2f518e 100644 --- a/HKMP/Networking/Packet/Data/PlayerEnterScene.cs +++ b/HKMP/Networking/Packet/Data/PlayerEnterScene.cs @@ -86,6 +86,11 @@ internal class ClientPlayerAlreadyInScene : IPacketData { /// public List PlayerEnterSceneList { get; } + /// + /// List of entity spawn instances. + /// + public List EntitySpawnList { get; } + /// /// List of entity update instances. /// @@ -101,6 +106,7 @@ internal class ClientPlayerAlreadyInScene : IPacketData { /// public ClientPlayerAlreadyInScene() { PlayerEnterSceneList = new List(); + EntitySpawnList = new List(); EntityUpdateList = new List(); } @@ -114,6 +120,14 @@ public void WriteData(IPacket packet) { PlayerEnterSceneList[i].WriteData(packet); } + length = (byte) System.Math.Min(byte.MaxValue, EntitySpawnList.Count); + + packet.Write(length); + + for (var i = 0; i < length; i++) { + EntitySpawnList[i].WriteData(packet); + } + length = (byte)System.Math.Min(byte.MaxValue, EntityUpdateList.Count); packet.Write(length); @@ -139,6 +153,18 @@ public void ReadData(IPacket packet) { PlayerEnterSceneList.Add(instance); } + length = packet.ReadByte(); + for (var i = 0; i < length; i++) { + // Create new instance of entity update + var instance = new EntitySpawn(); + + // Read the packet data into the instance + instance.ReadData(packet); + + // And add it to our already initialized list + EntitySpawnList.Add(instance); + } + length = packet.ReadByte(); for (var i = 0; i < length; i++) { // Create new instance of entity update diff --git a/HKMP/Networking/Packet/PacketId.cs b/HKMP/Networking/Packet/PacketId.cs index 8ba30591..c31fd580 100644 --- a/HKMP/Networking/Packet/PacketId.cs +++ b/HKMP/Networking/Packet/PacketId.cs @@ -52,6 +52,11 @@ internal enum ClientPacketId { /// Update of player map position. /// PlayerMapUpdate, + + /// + /// Notify that an entity has spawned. + /// + EntitySpawn, /// /// Update of realtime entity values. @@ -81,7 +86,7 @@ internal enum ClientPacketId { /// /// Player sent chat message. /// - ChatMessage = 15 + ChatMessage = 16 } /// @@ -113,6 +118,11 @@ public enum ServerPacketId { /// PlayerMapUpdate, + /// + /// Notify that an entity has spawned. + /// + EntitySpawn, + /// /// Update of realtime entity values. /// @@ -146,6 +156,6 @@ public enum ServerPacketId { /// /// Player sent chat message. /// - ChatMessage = 11 + ChatMessage = 12 } } diff --git a/HKMP/Networking/Packet/UpdatePacket.cs b/HKMP/Networking/Packet/UpdatePacket.cs index 2bec9837..47afb2bb 100644 --- a/HKMP/Networking/Packet/UpdatePacket.cs +++ b/HKMP/Networking/Packet/UpdatePacket.cs @@ -316,11 +316,25 @@ private void ReadPacketData( Packet packet, Dictionary packetData ) { - // Read the byte flag representing which packets - // are included in this update - var dataPacketIdFlag = packet.ReadUShort(); + // Figure out the size of the packet ID enum + var enumValues = (T[]) Enum.GetValues(typeof(T)); + var packetIdSize = (byte) enumValues.Length; + + // Read the byte flag representing which packets are included in this update + // The number of bytes we read is dependent on the size of the enum + ulong dataPacketIdFlag = 0; + if (packetIdSize <= 8) { + dataPacketIdFlag = packet.ReadByte(); + } else if (packetIdSize <= 16) { + dataPacketIdFlag = packet.ReadUShort(); + } else if (packetIdSize <= 32) { + dataPacketIdFlag = packet.ReadUInt(); + } else if (packetIdSize <= 64) { + dataPacketIdFlag = packet.ReadULong(); + } + // Keep track of value of current bit - var currentTypeValue = 1; + ulong currentTypeValue = 1; var packetIdValues = Enum.GetValues(typeof(T)); foreach (T packetId in packetIdValues) { @@ -854,6 +868,8 @@ protected override IPacketData InstantiatePacketDataFromId(ServerPacketId packet return new PlayerUpdate(); case ServerPacketId.PlayerMapUpdate: return new PlayerMapUpdate(); + case ServerPacketId.EntitySpawn: + return new PacketDataCollection(); case ServerPacketId.EntityUpdate: return new PacketDataCollection(); case ServerPacketId.PlayerEnterScene: @@ -903,6 +919,8 @@ protected override IPacketData InstantiatePacketDataFromId(ClientPacketId packet return new PacketDataCollection(); case ClientPacketId.PlayerMapUpdate: return new PacketDataCollection(); + case ClientPacketId.EntitySpawn: + return new PacketDataCollection(); case ClientPacketId.EntityUpdate: return new PacketDataCollection(); case ClientPacketId.PlayerDeath: diff --git a/HKMP/Networking/Server/ServerUpdateManager.cs b/HKMP/Networking/Server/ServerUpdateManager.cs index 49b5e909..c1ea1b1d 100644 --- a/HKMP/Networking/Server/ServerUpdateManager.cs +++ b/HKMP/Networking/Server/ServerUpdateManager.cs @@ -3,6 +3,7 @@ using System.Net; using System.Net.Sockets; using Hkmp.Game; +using Hkmp.Game.Client.Entity; using Hkmp.Math; using Hkmp.Networking.Packet; using Hkmp.Networking.Packet.Data; @@ -167,10 +168,12 @@ ushort animationClipId /// Add player already in scene data to the current packet. /// /// An enumerable of ClientPlayerEnterScene instances to add. + /// An enumerable of EntitySpawn instances to add. /// An enumerable of EntityUpdate instances to add. /// Whether the player is the scene host. public void AddPlayerAlreadyInSceneData( IEnumerable playerEnterSceneList, + IEnumerable entitySpawnList, IEnumerable entityUpdateList, bool sceneHost ) { @@ -179,6 +182,7 @@ bool sceneHost SceneHost = sceneHost }; alreadyInScene.PlayerEnterSceneList.AddRange(playerEnterSceneList); + alreadyInScene.EntitySpawnList.AddRange(entitySpawnList); alreadyInScene.EntityUpdateList.AddRange(entityUpdateList); CurrentUpdatePacket.SetSendingPacketData(ClientPacketId.PlayerAlreadyInScene, alreadyInScene); @@ -271,6 +275,31 @@ public void UpdatePlayerAnimation(ushort id, ushort clipId, byte frame, bool[] e } } + /// + /// Set entity spawn data for an entity that spawned. + /// + /// The ID of the entity. + /// The type of the entity that spawned the new entity. + /// The type of the entity that was spawned. + public void SetEntitySpawn(byte id, EntityType spawningType, EntityType spawnedType) { + lock (Lock) { + PacketDataCollection entitySpawnCollection; + + if (CurrentUpdatePacket.TryGetSendingPacketData(ClientPacketId.EntitySpawn, out var packetData)) { + entitySpawnCollection = (PacketDataCollection) packetData; + } else { + entitySpawnCollection = new PacketDataCollection(); + CurrentUpdatePacket.SetSendingPacketData(ClientPacketId.EntitySpawn, entitySpawnCollection); + } + + entitySpawnCollection.DataInstances.Add(new EntitySpawn { + Id = id, + SpawningType = spawningType, + SpawnedType = spawnedType + }); + } + } + /// /// Find or create an entity update instance in the current packet. /// diff --git a/HKMP/Resource/entity-registry.json b/HKMP/Resource/entity-registry.json new file mode 100644 index 00000000..61b831b3 --- /dev/null +++ b/HKMP/Resource/entity-registry.json @@ -0,0 +1,87 @@ +[ + { + "base_object_name": "Crawler", + "type": "Crawlid", + "fsm_name": "Crawler" + }, + { + "base_object_name": "Climber", + "type": "Tiktik", + "fsm_name": "" + }, + { + "base_object_name": "Buzzer", + "type": "Vengefly", + "fsm_name": "chaser" + }, + { + "base_object_name": "Zombie Runner", + "type": "WanderingHusk", + "fsm_name": "Zombie Swipe" + }, + { + "base_object_name": "Zombie Barger", + "type": "HuskBully", + "fsm_name": "Zombie Swipe" + }, + { + "base_object_name": "Zombie Hornhead", + "type": "HuskHornhead", + "fsm_name": "Zombie Swipe" + }, + { + "base_object_name": "Fly", + "type": "Gruzzer", + "fsm_name": "Bouncer Control" + }, + { + "base_object_name": "Battle Gate", + "type": "BattleGate", + "fsm_name": "BG Control" + }, + { + "base_object_name": "Spitter", + "type": "AspidHunter", + "fsm_name": "spitter" + }, + { + "base_object_name": "Zombie Guard", + "type": "HuskGuard", + "fsm_name": "Zombie Guard" + }, + { + "base_object_name": "Zombie Leaper", + "type": "LeapingHusk", + "fsm_name": "Zombie Leap" + }, + { + "base_object_name": "Hatcher", + "type": "AspidMother", + "fsm_name": "Hatcher" + }, + { + "base_object_name": "Hatcher Baby Spawner", + "type": "AspidHatchling", + "fsm_name": "Control" + }, + { + "base_object_name": "Zombie Shield", + "type": "HuskWarrior", + "fsm_name": "ZombieShieldControl" + }, + { + "base_object_name": "Worm", + "type": "Goam", + "fsm_name": "Worm Control" + }, + { + "base_object_name": "Roller", + "type": "Baldur", + "fsm_name": "Roller" + }, + { + "base_object_name": "Blocker", + "type": "ElderBaldur", + "fsm_name": "Blocker Control" + } +] diff --git a/HKMP/Util/FileUtil.cs b/HKMP/Util/FileUtil.cs index 9a51b53a..0beb18e6 100644 --- a/HKMP/Util/FileUtil.cs +++ b/HKMP/Util/FileUtil.cs @@ -26,6 +26,30 @@ public static T LoadObjectFromJsonFile(string filePath) { } } + /// + /// Load an object from an embedded JSON file at the given path. + /// + /// The path of the embedded file. + /// The type of the object to load. + /// An instance of the loaded object, or the default value if it could not be loaded. + public static T LoadObjectFromEmbeddedJson(string path) { + try { + var resourceStream = Assembly.GetExecutingAssembly().GetManifestResourceStream(path); + if (resourceStream == null) { + Logger.Warn($"Could not get resource stream for path: {path}"); + return default; + } + + using var streamReader = new StreamReader(resourceStream); + var fileString = streamReader.ReadToEnd(); + + return JsonConvert.DeserializeObject(fileString); + } catch (Exception e) { + Logger.Warn($"Could not read embedded resource at path: {path}, exception: {e.GetType()}, {e.Message}"); + return default; + } + } + /// /// Write an object to a JSON file at the given path. /// From b6c8517d8d951b06d5cdcbdd41bed8870353abce Mon Sep 17 00:00:00 2001 From: Extremelyd1 Date: Sun, 4 Dec 2022 20:53:09 +0100 Subject: [PATCH 024/216] Improve entity spawning and entity data tracking --- HKMP/Game/Client/Entity/EntityManager.cs | 152 ++++++++++++-------- HKMP/Game/Client/Entity/EntityType.cs | 3 +- HKMP/Game/Server/ServerEntityData.cs | 8 +- HKMP/Game/Server/ServerManager.cs | 37 +++-- HKMP/Networking/Packet/Data/EntityUpdate.cs | 2 +- HKMP/Networking/Packet/UpdatePacket.cs | 3 +- HKMP/Resource/entity-registry.json | 5 + 7 files changed, 132 insertions(+), 78 deletions(-) diff --git a/HKMP/Game/Client/Entity/EntityManager.cs b/HKMP/Game/Client/Entity/EntityManager.cs index 2d3192f9..17c735a6 100644 --- a/HKMP/Game/Client/Entity/EntityManager.cs +++ b/HKMP/Game/Client/Entity/EntityManager.cs @@ -9,7 +9,6 @@ using Logger = Hkmp.Logging.Logger; namespace Hkmp.Game.Client.Entity { - /// /// Manager class that handles entity creation, updating, networking and destruction. /// @@ -45,10 +44,11 @@ public EntityManager(NetClient netClient) { _entities = new Dictionary(); _lastId = 0; - + _entityRegistry.LoadRegistry(); EntityFsmActions.EntitySpawnEvent += OnGameObjectSpawned; + UnityEngine.SceneManagement.SceneManager.sceneLoaded += OnSceneLoaded; UnityEngine.SceneManagement.SceneManager.activeSceneChanged += OnSceneChanged; } @@ -100,7 +100,7 @@ public void SpawnEntity(byte id, EntityType spawningType, EntityType spawnedType Logger.Info($" Entity with ID {id} already exists, assuming it has been spawned by action"); return; } - + List clientFsms = null; foreach (var existingEntity in _entities.Values) { if (existingEntity.Type == spawningType) { @@ -121,7 +121,7 @@ public void SpawnEntity(byte id, EntityType spawningType, EntityType spawnedType EntityInitializer.InitializeClientFsm(fsm); } } - + var entity = new Entity( _netClient, id, @@ -173,8 +173,8 @@ public void UpdateEntityScale(byte entityId, bool scale) { /// The wrap mode of the animation. /// Whether this update is when we are entering the scene. public void UpdateEntityAnimation( - byte entityId, - byte animationId, + byte entityId, + byte animationId, byte animationWrapMode, bool alreadyInSceneUpdate ) { @@ -186,7 +186,8 @@ bool alreadyInSceneUpdate return; } - entity.UpdateAnimation(animationId, (tk2dSpriteAnimationClip.WrapMode) animationWrapMode, alreadyInSceneUpdate); + entity.UpdateAnimation(animationId, (tk2dSpriteAnimationClip.WrapMode) animationWrapMode, + alreadyInSceneUpdate); } /// @@ -230,7 +231,30 @@ public void UpdateEntityData(byte entityId, List data) { /// The game object that was spawned. private void OnGameObjectSpawned(FsmStateAction action, GameObject gameObject) { foreach (var fsm in gameObject.GetComponents()) { - ProcessGameObjectFsm(fsm, true, action.Fsm.FsmComponent); + if (!ProcessGameObjectFsm(fsm, out var entity)) { + continue; + } + + if (_isSceneHost) { + // Since an entity was created and we are the scene host, we need to notify the server + var spawningObjectName = action.Fsm.GameObject.name; + var spawningFsmName = action.Fsm.Name; + if (_entityRegistry.TryGetEntry(spawningObjectName, spawningFsmName, out var entry)) { + Logger.Info( + $"Notifying server of entity ({spawningObjectName}, {entry.Type}) spawning entity ({gameObject.name}, {entity.Type}) with ID {_lastId}"); + _netClient.UpdateManager.SetEntitySpawn(_lastId, entry.Type, entity.Type); + } + } else { + // Since an entity was created and we are not the scene host, we need to manually initialize + // all the FSM on the client + foreach (var clientFsm in entity.GetClientFsms()) { + Logger.Info($"Manually initializing client entity FSM: {clientFsm.Fsm.Name}, {clientFsm.gameObject.name}"); + EntityInitializer.InitializeClientFsm(clientFsm); + } + + // We also need to update the 'active' state of the entity since it was spawned + entity.UpdateIsActive(true); + } } } @@ -240,7 +264,7 @@ private void OnGameObjectSpawned(FsmStateAction action, GameObject gameObject) { /// The old scene. /// The new scene. private void OnSceneChanged(Scene oldScene, Scene newScene) { - Logger.Info("Clearing all registered entities"); + Logger.Info($"Scene changed, clearing registered entities"); foreach (var entity in _entities.Values) { entity.Destroy(); @@ -253,22 +277,60 @@ private void OnSceneChanged(Scene oldScene, Scene newScene) { if (!_netClient.IsConnected) { return; } - + + FindEntitiesInScene(newScene, false); + } + + /// + /// Callback method for when a scene is loaded. + /// + /// The scene that is loaded. + /// The load scene mode. + private void OnSceneLoaded(Scene scene, LoadSceneMode mode) { + var currentSceneName = UnityEngine.SceneManagement.SceneManager.GetActiveScene().name; + // If this scene is a boss or boss-defeated scene it starts with the same name, so we skip all other + // loaded scenes + if (!scene.name.StartsWith(currentSceneName)) { + return; + } + + Logger.Info($"Additional scene loaded ({scene.name}), looking for entities"); + + FindEntitiesInScene(scene, true); + } + + /// + /// Find entities to register in the given scene. + /// + /// The scene to find entities in. + /// Whether this scene was loaded late. + private void FindEntitiesInScene(Scene scene, bool lateLoad) { // Find all PlayMakerFSM components foreach (var fsm in Object.FindObjectsOfType()) { - if (fsm.gameObject.scene != newScene) { + // Logger.Info($"Found FSM: {fsm.Fsm.Name} in scene: {fsm.gameObject.scene.name}"); + if (fsm.gameObject.scene != scene) { + continue; + } + + if (!ProcessGameObjectFsm(fsm, out var entity) || !lateLoad) { continue; } - ProcessGameObjectFsm(fsm); + if (_isSceneHost) { + // Since this is a late load it needs to be initialized as host if we are the scene host + entity.InitializeHost(); + } else { + // Since this is a late load we need to update the 'active' state of the entity + entity.UpdateIsActive(true); + } } - + // Find all Climber components foreach (var climber in Object.FindObjectsOfType()) { if (!_entityRegistry.TryGetEntry(climber.gameObject.name, EntityType.Tiktik, out var entry)) { continue; } - + RegisterGameObjectAsEntity(climber.gameObject, entry.Type); } } @@ -277,62 +339,34 @@ private void OnSceneChanged(Scene oldScene, Scene newScene) { /// Process the FSM of a game object to check whether the game object should be registered as an entity. /// /// The FSM to process. - /// Whether the game object for this FSM was spawned while in the scene, - /// instead of at scene start - /// If spawnedInScene, then this is the FSM of the entity that spawned it; - /// otherwise null. - private void ProcessGameObjectFsm( - PlayMakerFSM fsm, - bool spawnedInScene = false, - PlayMakerFSM spawningFsm = null - ) { - Logger.Info($"Processing FSM: {fsm.Fsm.Name}, {fsm.gameObject.name}"); + /// The resulting entity if one was created; otherwise null. + /// True if an entity was created; otherwise false. + private bool ProcessGameObjectFsm(PlayMakerFSM fsm, out Entity entity) { + // Logger.Info($"Processing FSM: {fsm.Fsm.Name}, {fsm.gameObject.name}"); if (!_entityRegistry.TryGetEntry(fsm.gameObject.name, fsm.Fsm.Name, out var entry)) { - return; + entity = null; + return false; } - // If this entity spawned while in the scene already and we are a scene client, we need to initialize - // the FSM of the entity manually - if (spawnedInScene && !_isSceneHost) { - Logger.Info($"Manually initializing client entity FSM: {fsm.Fsm.Name}, {fsm.gameObject.name}"); - EntityInitializer.InitializeClientFsm(fsm); - } - - RegisterGameObjectAsEntity(fsm.gameObject, entry.Type, spawnedInScene, spawningFsm); + entity = RegisterGameObjectAsEntity(fsm.gameObject, entry.Type); + return true; } /// - /// Register a given game object as an entity. + /// Register a given game object as an entity and return that entity. /// /// The game object to register. /// The type of the entity. - /// Whether the game object was spawned while in the scene, instead of at scene - /// start - /// If spawnedInScene, then this is the FSM of the entity that spawned it; - /// otherwise null. - private void RegisterGameObjectAsEntity( - GameObject gameObject, - EntityType type, - bool spawnedInScene = false, - PlayMakerFSM spawningFsm = null - ) { + /// The entity that was created. + private Entity RegisterGameObjectAsEntity(GameObject gameObject, EntityType type) { // First find a usable ID that is not registered to an entity already while (_entities.ContainsKey(_lastId)) { _lastId++; } - + Logger.Info($"Registering entity ({type}) '{gameObject.name}' with ID '{_lastId}'"); - if (spawnedInScene && _isSceneHost) { - var spawningObjectName = spawningFsm!.gameObject.name; - var spawningFsmName = spawningFsm!.Fsm.Name; - if (_entityRegistry.TryGetEntry(spawningObjectName, spawningFsmName, out var entry)) { - Logger.Info($"Notifying server of entity ({spawningObjectName}, {entry.Type}) spawning entity ({gameObject.name}, {type}) with ID {_lastId}"); - _netClient.UpdateManager.SetEntitySpawn(_lastId, entry.Type, type); - } - } - // TODO: maybe we need to check whether this entity game object has already been registered, which can // happen with game objects that have multiple FSMs @@ -344,15 +378,9 @@ private void RegisterGameObjectAsEntity( ); _entities[_lastId] = entity; - if (spawnedInScene) { - if (_isSceneHost) { - entity.InitializeHost(); - } else { - entity.UpdateIsActive(true); - } - } - _lastId++; + + return entity; } } } diff --git a/HKMP/Game/Client/Entity/EntityType.cs b/HKMP/Game/Client/Entity/EntityType.cs index 097a3b7c..8fd710a6 100644 --- a/HKMP/Game/Client/Entity/EntityType.cs +++ b/HKMP/Game/Client/Entity/EntityType.cs @@ -20,5 +20,6 @@ internal enum EntityType { HuskWarrior, Goam, Baldur, - ElderBaldur + ElderBaldur, + FalseKnight } diff --git a/HKMP/Game/Server/ServerEntityData.cs b/HKMP/Game/Server/ServerEntityData.cs index 12b2e14c..0c8d7d0c 100644 --- a/HKMP/Game/Server/ServerEntityData.cs +++ b/HKMP/Game/Server/ServerEntityData.cs @@ -2,6 +2,7 @@ using Hkmp.Game.Client.Entity; using Hkmp.Math; using Hkmp.Networking.Packet.Data; +using JetBrains.Annotations; namespace Hkmp.Game.Server; @@ -25,11 +26,12 @@ internal class ServerEntityData { /// /// The last position of the entity. /// + [CanBeNull] public Vector2 Position { get; set; } /// /// The last scale of the entity. /// - public bool Scale { get; set; } + public bool? Scale { get; set; } /// /// The ID of the last played animation. /// @@ -41,12 +43,12 @@ internal class ServerEntityData { /// /// Whether the entity is active. /// - public bool IsActive { get; set; } + public bool? IsActive { get; set; } /// /// Generic data associated with this entity. /// - public List GenericData { get; set; } + public List GenericData { get; } public ServerEntityData() { GenericData = new List(); diff --git a/HKMP/Game/Server/ServerManager.cs b/HKMP/Game/Server/ServerManager.cs index fd21e29d..044a7510 100644 --- a/HKMP/Game/Server/ServerManager.cs +++ b/HKMP/Game/Server/ServerManager.cs @@ -388,16 +388,28 @@ private void OnClientEnterScene(ServerPlayerData playerData) { Logger.Info($"Sending that entity '{entityKey.EntityId}' is already in scene to '{playerData.Id}'"); var entityUpdate = new EntityUpdate { - Id = entityKey.EntityId, - Position = entityData.Position, - Scale = entityData.Scale, - IsActive = entityData.IsActive, - GenericData = entityData.GenericData + Id = entityKey.EntityId }; - entityUpdate.UpdateTypes.Add(EntityUpdateType.Position); - entityUpdate.UpdateTypes.Add(EntityUpdateType.Scale); - entityUpdate.UpdateTypes.Add(EntityUpdateType.Active); - entityUpdate.UpdateTypes.Add(EntityUpdateType.Data); + + if (entityData.Position != null) { + entityUpdate.UpdateTypes.Add(EntityUpdateType.Position); + entityUpdate.Position = entityData.Position; + } + + if (entityData.Scale.HasValue) { + entityUpdate.UpdateTypes.Add(EntityUpdateType.Scale); + entityUpdate.Scale = entityData.Scale.Value; + } + + if (entityData.IsActive.HasValue) { + entityUpdate.UpdateTypes.Add(EntityUpdateType.Active); + entityUpdate.IsActive = entityData.IsActive.Value; + } + + if (entityData.GenericData.Count > 0) { + entityUpdate.UpdateTypes.Add(EntityUpdateType.Data); + entityUpdate.GenericData.AddRange(entityData.GenericData); + } if (entityData.AnimationId.HasValue) { entityUpdate.UpdateTypes.Add(EntityUpdateType.Animation); @@ -710,7 +722,12 @@ void ReplaceExistingDataWithSameType(EntityNetworkData.DataType type, Packet dat var existingData = entityData.GenericData.Find( d => d.Type == type ); - if (existingData != null) { + if (existingData == null) { + entityData.GenericData.Add(new EntityNetworkData { + Type = type, + Packet = data + }); + } else { existingData.Packet = data; } } diff --git a/HKMP/Networking/Packet/Data/EntityUpdate.cs b/HKMP/Networking/Packet/Data/EntityUpdate.cs index dfde0966..807a8282 100644 --- a/HKMP/Networking/Packet/Data/EntityUpdate.cs +++ b/HKMP/Networking/Packet/Data/EntityUpdate.cs @@ -48,7 +48,7 @@ internal class EntityUpdate : IPacketData { /// public bool IsActive { get; set; } - public List GenericData { get; init; } + public List GenericData { get; } /// /// Construct the entity update data. diff --git a/HKMP/Networking/Packet/UpdatePacket.cs b/HKMP/Networking/Packet/UpdatePacket.cs index 47afb2bb..f4fbf722 100644 --- a/HKMP/Networking/Packet/UpdatePacket.cs +++ b/HKMP/Networking/Packet/UpdatePacket.cs @@ -588,7 +588,8 @@ public bool ReadPacket() { // Input the dictionary into the resend dictionary keyed by its sequence number _resendAddonPacketData[seq] = addonDataDict; } - } catch { + } catch (Exception e) { + Logger.Info($"Exception while reading packet: {e.GetType()}, {e.Message}, {e.StackTrace}"); return false; } diff --git a/HKMP/Resource/entity-registry.json b/HKMP/Resource/entity-registry.json index 61b831b3..fb7f25c7 100644 --- a/HKMP/Resource/entity-registry.json +++ b/HKMP/Resource/entity-registry.json @@ -83,5 +83,10 @@ "base_object_name": "Blocker", "type": "ElderBaldur", "fsm_name": "Blocker Control" + }, + { + "base_object_name": "False Knight New", + "type": "FalseKnight", + "fsm_name": "FalseyControl" } ] From 687f5977b157651a1f0017f73748a3ca9cced4f8 Mon Sep 17 00:00:00 2001 From: Extremelyd1 Date: Sat, 13 May 2023 10:32:28 +0200 Subject: [PATCH 025/216] Optimize imports --- HKMP/Api/Client/IPlayerMapEntry.cs | 2 +- HKMP/Fsm/FsmPatcher.cs | 2 +- HKMP/Game/Client/Entity/Action/EntityFsmActions.cs | 4 +++- HKMP/Game/Client/Entity/Entity.cs | 2 +- HKMP/Game/Client/Entity/EntityManager.cs | 2 +- HKMP/Game/Command/Server/AnnounceCommand.cs | 1 - HKMP/Game/Settings/ServerSettings.cs | 1 + HKMP/Networking/Packet/Data/EntityUpdate.cs | 1 + HKMP/Networking/Packet/Data/LoginRequest.cs | 1 - HKMP/Networking/Packet/Packet.cs | 2 -- HKMP/Networking/Server/ServerUpdateManager.cs | 1 + HKMP/Ui/Component/ChatInputComponent.cs | 2 -- HKMPServer/Command/ConsoleInputManager.cs | 1 - HKMPServer/Launcher.cs | 1 - 14 files changed, 10 insertions(+), 13 deletions(-) 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/Fsm/FsmPatcher.cs b/HKMP/Fsm/FsmPatcher.cs index fc6f819b..a34df891 100644 --- a/HKMP/Fsm/FsmPatcher.cs +++ b/HKMP/Fsm/FsmPatcher.cs @@ -1,6 +1,6 @@ +using Hkmp.Logging; using Hkmp.Util; using HutongGames.PlayMaker.Actions; -using Logger = Hkmp.Logging.Logger; namespace Hkmp.Fsm; diff --git a/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs b/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs index 49d70d78..2b81887b 100644 --- a/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs +++ b/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs @@ -6,6 +6,8 @@ using HutongGames.PlayMaker.Actions; using UnityEngine; using Logger = Hkmp.Logging.Logger; +using Random = UnityEngine.Random; + // ReSharper disable UnusedMember.Local // ReSharper disable UnusedParameter.Local @@ -239,7 +241,7 @@ private static void ApplyNetworkDataFromAction(EntityNetworkData data, FireAtTar ) * 57.295776f; if (!action.spread.IsNone) { - num += UnityEngine.Random.Range(-action.spread.Value, action.spread.Value); + num += Random.Range(-action.spread.Value, action.spread.Value); } rigidBody.velocity = new Vector2( diff --git a/HKMP/Game/Client/Entity/Entity.cs b/HKMP/Game/Client/Entity/Entity.cs index 94a7fca6..3a891cd3 100644 --- a/HKMP/Game/Client/Entity/Entity.cs +++ b/HKMP/Game/Client/Entity/Entity.cs @@ -10,9 +10,9 @@ using Hkmp.Util; using HutongGames.PlayMaker; using UnityEngine; +using Logger = Hkmp.Logging.Logger; using Object = UnityEngine.Object; using Vector2 = Hkmp.Math.Vector2; -using Logger = Hkmp.Logging.Logger; namespace Hkmp.Game.Client.Entity; diff --git a/HKMP/Game/Client/Entity/EntityManager.cs b/HKMP/Game/Client/Entity/EntityManager.cs index 17c735a6..94bb2e43 100644 --- a/HKMP/Game/Client/Entity/EntityManager.cs +++ b/HKMP/Game/Client/Entity/EntityManager.cs @@ -5,8 +5,8 @@ using HutongGames.PlayMaker; using UnityEngine; using UnityEngine.SceneManagement; -using Vector2 = Hkmp.Math.Vector2; using Logger = Hkmp.Logging.Logger; +using Vector2 = Hkmp.Math.Vector2; namespace Hkmp.Game.Client.Entity { /// diff --git a/HKMP/Game/Command/Server/AnnounceCommand.cs b/HKMP/Game/Command/Server/AnnounceCommand.cs index acefc233..fa9c73b3 100644 --- a/HKMP/Game/Command/Server/AnnounceCommand.cs +++ b/HKMP/Game/Command/Server/AnnounceCommand.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Concurrent; using Hkmp.Api.Command.Server; -using Hkmp.Concurrency; using Hkmp.Game.Server; using Hkmp.Networking.Server; diff --git a/HKMP/Game/Settings/ServerSettings.cs b/HKMP/Game/Settings/ServerSettings.cs index 78c609fa..69a747e6 100644 --- a/HKMP/Game/Settings/ServerSettings.cs +++ b/HKMP/Game/Settings/ServerSettings.cs @@ -1,5 +1,6 @@ using System; using Hkmp.Api.Server; + // ReSharper disable UnusedAutoPropertyAccessor.Global // ReSharper disable AutoPropertyCanBeMadeGetOnly.Global diff --git a/HKMP/Networking/Packet/Data/EntityUpdate.cs b/HKMP/Networking/Packet/Data/EntityUpdate.cs index 481660c7..9d015370 100644 --- a/HKMP/Networking/Packet/Data/EntityUpdate.cs +++ b/HKMP/Networking/Packet/Data/EntityUpdate.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using Hkmp.Logging; using Hkmp.Math; namespace Hkmp.Networking.Packet.Data; diff --git a/HKMP/Networking/Packet/Data/LoginRequest.cs b/HKMP/Networking/Packet/Data/LoginRequest.cs index 7574d4e7..7830add1 100644 --- a/HKMP/Networking/Packet/Data/LoginRequest.cs +++ b/HKMP/Networking/Packet/Data/LoginRequest.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using Hkmp.Api.Addon; -using Hkmp.Util; namespace Hkmp.Networking.Packet.Data; diff --git a/HKMP/Networking/Packet/Packet.cs b/HKMP/Networking/Packet/Packet.cs index 98ccaa04..f6949fdb 100644 --- a/HKMP/Networking/Packet/Packet.cs +++ b/HKMP/Networking/Packet/Packet.cs @@ -1,9 +1,7 @@ using System; using System.Collections.Generic; using System.Text; -using Hkmp.Logging; using Hkmp.Math; -using Hkmp.Util; namespace Hkmp.Networking.Packet; diff --git a/HKMP/Networking/Server/ServerUpdateManager.cs b/HKMP/Networking/Server/ServerUpdateManager.cs index 0337b251..6e1828dd 100644 --- a/HKMP/Networking/Server/ServerUpdateManager.cs +++ b/HKMP/Networking/Server/ServerUpdateManager.cs @@ -4,6 +4,7 @@ using System.Net.Sockets; using Hkmp.Game; using Hkmp.Game.Client.Entity; +using Hkmp.Game.Settings; using Hkmp.Math; using Hkmp.Networking.Packet; using Hkmp.Networking.Packet.Data; diff --git a/HKMP/Ui/Component/ChatInputComponent.cs b/HKMP/Ui/Component/ChatInputComponent.cs index 92fe202e..02559e08 100644 --- a/HKMP/Ui/Component/ChatInputComponent.cs +++ b/HKMP/Ui/Component/ChatInputComponent.cs @@ -1,10 +1,8 @@ using System; -using System.Collections.Generic; using Hkmp.Networking.Packet.Data; using Hkmp.Ui.Resources; using Hkmp.Util; using UnityEngine; -using UnityEngine.UI; namespace Hkmp.Ui.Component; diff --git a/HKMPServer/Command/ConsoleInputManager.cs b/HKMPServer/Command/ConsoleInputManager.cs index 66df62d5..fc1977bd 100644 --- a/HKMPServer/Command/ConsoleInputManager.cs +++ b/HKMPServer/Command/ConsoleInputManager.cs @@ -1,6 +1,5 @@ using System; using System.Threading; -using System.Threading.Tasks; namespace HkmpServer.Command { /// diff --git a/HKMPServer/Launcher.cs b/HKMPServer/Launcher.cs index 6d8f7b35..a7ccdaf7 100644 --- a/HKMPServer/Launcher.cs +++ b/HKMPServer/Launcher.cs @@ -1,7 +1,6 @@ using System; using System.IO; using System.Reflection; -using System.Threading.Tasks; namespace HkmpServer { /// From ce22a2f4f12aca2bec3265773dce4b8524174de3 Mon Sep 17 00:00:00 2001 From: Extremelyd1 Date: Sat, 13 May 2023 23:27:29 +0200 Subject: [PATCH 026/216] Improvements to entity spawning system --- .../Client/Entity/Action/EntityFsmActions.cs | 11 ++ HKMP/Game/Client/Entity/Entity.cs | 109 ++++++++++-------- HKMP/Game/Client/Entity/EntityManager.cs | 50 ++++---- HKMP/Game/Client/Entity/EntityRegistry.cs | 12 +- HKMP/Networking/Packet/UpdatePacket.cs | 2 +- 5 files changed, 107 insertions(+), 77 deletions(-) diff --git a/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs b/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs index 2b81887b..3612f349 100644 --- a/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs +++ b/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs @@ -186,6 +186,17 @@ private static bool GetNetworkDataFromAction(EntityNetworkData data, SpawnObject } private static void ApplyNetworkDataFromAction(EntityNetworkData data, SpawnObjectFromGlobalPool action) { + // We first check whether applying this action results in the spawning of an entity that is managed by the + // system. Because if so, it would already be handled by an EntitySpawn packet instead, and this will only + // duplicate the spawning and leave it uncontrolled + var toSpawnObject = action.gameObject.Value; + foreach (var fsm in toSpawnObject.GetComponents()) { + if (EntityRegistry.TryGetEntry(fsm.gameObject.name, fsm.Fsm.Name, out var entry)) { + Logger.Debug($"Tried applying SpawnObjectFromGlobalPool network data, but to spawn object is entity: {entry.Type}"); + return; + } + } + var position = new Vector3( data.Packet.ReadFloat(), data.Packet.ReadFloat(), diff --git a/HKMP/Game/Client/Entity/Entity.cs b/HKMP/Game/Client/Entity/Entity.cs index 3a891cd3..bc7a3334 100644 --- a/HKMP/Game/Client/Entity/Entity.cs +++ b/HKMP/Game/Client/Entity/Entity.cs @@ -11,7 +11,6 @@ using HutongGames.PlayMaker; using UnityEngine; using Logger = Hkmp.Logging.Logger; -using Object = UnityEngine.Object; using Vector2 = Hkmp.Math.Vector2; namespace Hkmp.Game.Client.Entity; @@ -38,7 +37,7 @@ internal class Entity { /// /// Host-client pair for the game objects. /// - private readonly HostClientPair _object; + public HostClientPair Object { get; init; } /// /// Host-client pair for the sprite animators. @@ -110,34 +109,34 @@ GameObject hostObject _isControlled = true; - _object = new HostClientPair { + Object = new HostClientPair { Host = hostObject, - Client = Object.Instantiate( + Client = UnityEngine.Object.Instantiate( hostObject, hostObject.transform.position, hostObject.transform.rotation ) }; - _object.Client.SetActive(false); + Object.Client.SetActive(false); // Store whether the host object was active and set it not active until we know if we are scene host - _originalIsActive = _object.Host.activeSelf; - _object.Host.SetActive(false); + _originalIsActive = Object.Host.activeSelf; + Object.Host.SetActive(false); - _lastIsActive = _object.Host.activeInHierarchy; + _lastIsActive = Object.Host.activeInHierarchy; Logger.Info( - $"Entity '{_object.Host.name}' was original active: {_originalIsActive}, last active: {_lastIsActive}"); + $"Entity '{Object.Host.name}' was original active: {_originalIsActive}, last active: {_lastIsActive}"); // Add a position interpolation component to the enemy so we can smooth out position updates - _object.Client.AddComponent(); + Object.Client.AddComponent(); // Register an update event to send position updates MonoBehaviourUtil.Instance.OnUpdateEvent += OnUpdate; _animator = new HostClientPair { - Host = _object.Host.GetComponent(), - Client = _object.Client.GetComponent() + Host = Object.Host.GetComponent(), + Client = Object.Client.GetComponent() }; if (_animator.Host != null) { _animationClipNameIds = new BiLookup(); @@ -151,17 +150,27 @@ GameObject hostObject _animationClipNameIds.Add(animationClip.name, (byte)index++); if (index > byte.MaxValue) { - Logger.Error($"Too many animation clips to fit in a byte for entity: {_object.Client.name}"); + Logger.Error($"Too many animation clips to fit in a byte for entity: {Object.Client.name}"); break; } } On.tk2dSpriteAnimator.Play_tk2dSpriteAnimationClip_float_float += OnAnimationPlayed; + + // Always disallow the client object from being recycled, because it will simply be destroyed + On.ObjectPool.Recycle_GameObject += (orig, obj) => { + if (obj == Object.Client) { + Logger.Debug($"Client object of entity: {_entityId}, {type} tried to be recycled"); + return; + } + + orig(obj); + }; } _fsms = new HostClientPair> { - Host = _object.Host.GetComponents().ToList(), - Client = _object.Client.GetComponents().ToList() + Host = Object.Host.GetComponents().ToList(), + Client = Object.Client.GetComponents().ToList() }; _hookedActions = new Dictionary(); @@ -171,9 +180,9 @@ GameObject hostObject } // Remove all components that (re-)activate FSMs - foreach (var fsmActivator in _object.Client.GetComponents()) { + foreach (var fsmActivator in Object.Client.GetComponents()) { fsmActivator.StopAllCoroutines(); - Object.Destroy(fsmActivator); + UnityEngine.Object.Destroy(fsmActivator); } foreach (var fsm in _fsms.Client) { @@ -231,8 +240,8 @@ private void ProcessClientFsm(PlayMakerFSM fsm) { /// Check the host and client objects for components that are supported for networking. /// private void FindComponents() { - var hostHealthManager = _object.Host.GetComponent(); - var clientHealthManager = _object.Client.GetComponent(); + var hostHealthManager = Object.Host.GetComponent(); + var clientHealthManager = Object.Client.GetComponent(); if (hostHealthManager != null && clientHealthManager != null) { var healthManager = new HostClientPair { Host = hostHealthManager, @@ -242,25 +251,25 @@ private void FindComponents() { _components[EntityNetworkData.DataType.HealthManager] = new HealthManagerComponent( _netClient, _entityId, - _object, + Object, healthManager ); } - var climber = _object.Client.GetComponent(); + var climber = Object.Client.GetComponent(); if (climber != null) { _components[EntityNetworkData.DataType.Rotation] = new RotationComponent( _netClient, _entityId, - _object, + Object, climber ); } - var hostCollider = _object.Host.GetComponent(); - var clientCollider = _object.Client.GetComponent(); + var hostCollider = Object.Host.GetComponent(); + var clientCollider = Object.Client.GetComponent(); if (hostCollider != null && clientCollider != null) { - Logger.Info($"Adding collider component to entity: {_object.Host.name}"); + Logger.Info($"Adding collider component to entity: {Object.Host.name}"); var collider = new HostClientPair { Host = hostCollider, @@ -270,19 +279,19 @@ private void FindComponents() { _components[EntityNetworkData.DataType.Collider] = new ColliderComponent( _netClient, _entityId, - _object, + Object, collider ); } // Find Walker MonoBehaviour and remove it from the client object - var walker = _object.Client.GetComponent(); + var walker = Object.Client.GetComponent(); if (walker != null) { - Object.Destroy(walker); + UnityEngine.Object.Destroy(walker); } // Find RigidBody2D MonoBehaviour and set it to be kinematic so it doesn't do physics on its own - var rigidBody = _object.Client.GetComponent(); + var rigidBody = Object.Client.GetComponent(); if (rigidBody != null) { rigidBody.isKinematic = true; } @@ -326,11 +335,11 @@ private void OnActionEntered(FsmStateAction self) { /// Callback method for handling updates. /// private void OnUpdate() { - if (_object.Host == null) { + if (Object.Host == null) { if (_lastIsActive) { // If the host object was active, but now it null (or destroyed in Unity), we can send // to the server that the entity can be regarded as inactive - Logger.Info($"Entity '{_object.Client.name}' host object is null (or destroyed) and was active"); + Logger.Info($"Entity '{Object.Client.name}' host object is null (or destroyed) and was active"); _lastIsActive = false; @@ -343,18 +352,18 @@ private void OnUpdate() { return; } - var hostObjectActive = _object.Host.activeSelf; + var hostObjectActive = Object.Host.activeSelf; if (_isControlled) { if (hostObjectActive) { - Logger.Info($"Entity '{_object.Host.name}' host object became active, re-disabling"); - _object.Host.SetActive(false); + Logger.Info($"Entity '{Object.Host.name}' host object became active, re-disabling"); + Object.Host.SetActive(false); } return; } - var transform = _object.Host.transform; + var transform = Object.Host.transform; var newPosition = transform.position; if (newPosition != _lastPosition) { @@ -376,11 +385,11 @@ private void OnUpdate() { ); } - var newActive = _object.Host.activeInHierarchy; + var newActive = Object.Host.activeInHierarchy; if (newActive != _lastIsActive) { _lastIsActive = newActive; - Logger.Info($"Entity '{_object.Host.name}' changed active: {newActive}"); + Logger.Info($"Entity '{Object.Host.name}' changed active: {newActive}"); _netClient.UpdateManager.UpdateEntityIsActive( _entityId, @@ -406,7 +415,7 @@ float overrideFps ) { if (self == _animator.Client) { if (!_allowClientAnimation) { - Logger.Info($"Entity '{_object.Client.name}' client animator tried playing animation"); + Logger.Info($"Entity '{Object.Client.name}' client animator tried playing animation"); } else { // Logger.Info($"Entity '{_object.Client.name}' client animator was allowed to play animation"); @@ -429,11 +438,11 @@ float overrideFps } if (!_animationClipNameIds.TryGetValue(clip.name, out var animationId)) { - Logger.Warn($"Entity '{_object.Client.name}' played unknown animation: {clip.name}"); + Logger.Warn($"Entity '{Object.Client.name}' played unknown animation: {clip.name}"); return; } - Logger.Info($"Entity '{_object.Host.name}' sends animation: {clip.name}, {animationId}, {clip.wrapMode}"); + Logger.Info($"Entity '{Object.Host.name}' sends animation: {clip.name}, {animationId}, {clip.wrapMode}"); _netClient.UpdateManager.UpdateEntityAnimation( _entityId, animationId, @@ -445,14 +454,14 @@ float overrideFps /// Initializes the entity when the client user is the scene host. /// public void InitializeHost() { - _object.Host.SetActive(_originalIsActive); + Object.Host.SetActive(_originalIsActive); // Also update the last active variable to account for this potential change // Otherwise we might trigger the update sending of activity twice - _lastIsActive = _object.Host.activeInHierarchy; + _lastIsActive = Object.Host.activeInHierarchy; Logger.Info( - $"Initializing entity '{_object.Host.name}' with active: {_originalIsActive}, sending active: {_lastIsActive}"); + $"Initializing entity '{Object.Host.name}' with active: {_originalIsActive}, sending active: {_lastIsActive}"); _netClient.UpdateManager.UpdateEntityIsActive(_entityId, _lastIsActive); @@ -480,11 +489,11 @@ public void MakeHost() { public void UpdatePosition(Vector2 position) { var unityPos = new Vector3(position.X, position.Y); - if (_object.Client == null) { + if (Object.Client == null) { return; } - var positionInterpolation = _object.Client.GetComponent(); + var positionInterpolation = Object.Client.GetComponent(); if (positionInterpolation == null) { return; } @@ -497,7 +506,7 @@ public void UpdatePosition(Vector2 position) { /// /// The new scale. public void UpdateScale(bool scale) { - var transform = _object.Client.transform; + var transform = Object.Client.transform; var localScale = transform.localScale; var currentScaleX = localScale.x; @@ -519,12 +528,12 @@ public void UpdateScale(bool scale) { public void UpdateAnimation(byte animationId, tk2dSpriteAnimationClip.WrapMode wrapMode, bool alreadyInSceneUpdate) { if (_animator.Client == null) { - Logger.Warn($"Entity '{_object.Client.name}' received animation while client animator does not exist"); + Logger.Warn($"Entity '{Object.Client.name}' received animation while client animator does not exist"); return; } if (!_animationClipNameIds.TryGetValue(animationId, out var clipName)) { - Logger.Warn($"Entity '{_object.Client.name}' received unknown animation ID: {animationId}"); + Logger.Warn($"Entity '{Object.Client.name}' received unknown animation ID: {animationId}"); return; } @@ -573,8 +582,8 @@ public void UpdateAnimation(byte animationId, tk2dSpriteAnimationClip.WrapMode w /// /// The new value for active. public void UpdateIsActive(bool active) { - Logger.Info($"Entity '{_object.Client.name}' received active: {active}"); - _object.Client.SetActive(active); + Logger.Info($"Entity '{Object.Client.name}' received active: {active}"); + Object.Client.SetActive(active); } /// diff --git a/HKMP/Game/Client/Entity/EntityManager.cs b/HKMP/Game/Client/Entity/EntityManager.cs index 94bb2e43..e709b203 100644 --- a/HKMP/Game/Client/Entity/EntityManager.cs +++ b/HKMP/Game/Client/Entity/EntityManager.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Linq; using Hkmp.Game.Client.Entity.Action; using Hkmp.Networking.Client; using Hkmp.Networking.Packet.Data; @@ -18,11 +19,6 @@ internal class EntityManager { /// private readonly NetClient _netClient; - /// - /// The entity registry for lookups of game object names, FSM names and entity types. - /// - private readonly EntityRegistry _entityRegistry; - /// /// Dictionary mapping entity IDs to their respective entity instances. /// @@ -40,13 +36,10 @@ internal class EntityManager { public EntityManager(NetClient netClient) { _netClient = netClient; - _entityRegistry = new EntityRegistry(); _entities = new Dictionary(); _lastId = 0; - _entityRegistry.LoadRegistry(); - EntityFsmActions.EntitySpawnEvent += OnGameObjectSpawned; UnityEngine.SceneManagement.SceneManager.sceneLoaded += OnSceneLoaded; UnityEngine.SceneManagement.SceneManager.activeSceneChanged += OnSceneChanged; @@ -96,11 +89,14 @@ public void BecomeSceneHost() { public void SpawnEntity(byte id, EntityType spawningType, EntityType spawnedType) { Logger.Info($"Trying to spawn entity with ID {id} with types: {spawningType}, {spawnedType}"); + // If an entity with the new ID already exists, we return if (_entities.ContainsKey(id)) { Logger.Info($" Entity with ID {id} already exists, assuming it has been spawned by action"); return; } + // Find the list of client FSMs that correspond to an entity with the given type in our current scene + // Doesn't matter which instance of entity it is, because the FSMs will be the same List clientFsms = null; foreach (var existingEntity in _entities.Values) { if (existingEntity.Type == spawningType) { @@ -109,6 +105,7 @@ public void SpawnEntity(byte id, EntityType spawningType, EntityType spawnedType } } + // If no such FSMs exist we return again, because we can't spawn the new entity if (clientFsms == null) { Logger.Warn($"Could not find entity with same type for spawning"); return; @@ -117,7 +114,7 @@ public void SpawnEntity(byte id, EntityType spawningType, EntityType spawnedType var gameObject = EntitySpawner.SpawnEntityGameObject(spawningType, spawnedType, clientFsms); foreach (var fsm in gameObject.GetComponents()) { - if (!_entityRegistry.TryGetEntry(gameObject.name, fsm.Fsm.Name, out _)) { + if (!EntityRegistry.TryGetEntry(gameObject.name, fsm.Fsm.Name, out _)) { EntityInitializer.InitializeClientFsm(fsm); } } @@ -230,8 +227,13 @@ public void UpdateEntityData(byte entityId, List data) { /// The action from which the game object was spawned. /// The game object that was spawned. private void OnGameObjectSpawned(FsmStateAction action, GameObject gameObject) { + if (_entities.Values.Any(entity => entity.Object.Host == gameObject)) { + Logger.Debug("Spawned object was already a registered entity"); + return; + } + foreach (var fsm in gameObject.GetComponents()) { - if (!ProcessGameObjectFsm(fsm, out var entity)) { + if (!ProcessGameObjectFsm(fsm, out var entity, out var entityId)) { continue; } @@ -239,11 +241,14 @@ private void OnGameObjectSpawned(FsmStateAction action, GameObject gameObject) { // Since an entity was created and we are the scene host, we need to notify the server var spawningObjectName = action.Fsm.GameObject.name; var spawningFsmName = action.Fsm.Name; - if (_entityRegistry.TryGetEntry(spawningObjectName, spawningFsmName, out var entry)) { + if (EntityRegistry.TryGetEntry(spawningObjectName, spawningFsmName, out var entry)) { Logger.Info( - $"Notifying server of entity ({spawningObjectName}, {entry.Type}) spawning entity ({gameObject.name}, {entity.Type}) with ID {_lastId}"); - _netClient.UpdateManager.SetEntitySpawn(_lastId, entry.Type, entity.Type); + $"Notifying server of entity ({spawningObjectName}, {entry.Type}) spawning entity ({gameObject.name}, {entity.Type}) with ID {entityId}"); + _netClient.UpdateManager.SetEntitySpawn(entityId, entry.Type, entity.Type); } + + // Also initialize the entity as host, since otherwise it will stay disabled + entity.InitializeHost(); } else { // Since an entity was created and we are not the scene host, we need to manually initialize // all the FSM on the client @@ -312,7 +317,7 @@ private void FindEntitiesInScene(Scene scene, bool lateLoad) { continue; } - if (!ProcessGameObjectFsm(fsm, out var entity) || !lateLoad) { + if (!ProcessGameObjectFsm(fsm, out var entity, out _) || !lateLoad) { continue; } @@ -327,11 +332,11 @@ private void FindEntitiesInScene(Scene scene, bool lateLoad) { // Find all Climber components foreach (var climber in Object.FindObjectsOfType()) { - if (!_entityRegistry.TryGetEntry(climber.gameObject.name, EntityType.Tiktik, out var entry)) { + if (!EntityRegistry.TryGetEntry(climber.gameObject.name, EntityType.Tiktik, out var entry)) { continue; } - RegisterGameObjectAsEntity(climber.gameObject, entry.Type); + RegisterGameObjectAsEntity(climber.gameObject, entry.Type, out _); } } @@ -340,16 +345,18 @@ private void FindEntitiesInScene(Scene scene, bool lateLoad) { /// /// The FSM to process. /// The resulting entity if one was created; otherwise null. + /// The ID of the entity if one was created; otherwise 0. /// True if an entity was created; otherwise false. - private bool ProcessGameObjectFsm(PlayMakerFSM fsm, out Entity entity) { + private bool ProcessGameObjectFsm(PlayMakerFSM fsm, out Entity entity, out byte entityId) { // Logger.Info($"Processing FSM: {fsm.Fsm.Name}, {fsm.gameObject.name}"); - if (!_entityRegistry.TryGetEntry(fsm.gameObject.name, fsm.Fsm.Name, out var entry)) { + if (!EntityRegistry.TryGetEntry(fsm.gameObject.name, fsm.Fsm.Name, out var entry)) { entity = null; + entityId = 0; return false; } - entity = RegisterGameObjectAsEntity(fsm.gameObject, entry.Type); + entity = RegisterGameObjectAsEntity(fsm.gameObject, entry.Type, out entityId); return true; } @@ -358,8 +365,9 @@ private bool ProcessGameObjectFsm(PlayMakerFSM fsm, out Entity entity) { /// /// The game object to register. /// The type of the entity. + /// The ID of the registered entity. /// The entity that was created. - private Entity RegisterGameObjectAsEntity(GameObject gameObject, EntityType type) { + private Entity RegisterGameObjectAsEntity(GameObject gameObject, EntityType type, out byte entityId) { // First find a usable ID that is not registered to an entity already while (_entities.ContainsKey(_lastId)) { _lastId++; @@ -378,6 +386,8 @@ private Entity RegisterGameObjectAsEntity(GameObject gameObject, EntityType type ); _entities[_lastId] = entity; + entityId = _lastId; + _lastId++; return entity; diff --git a/HKMP/Game/Client/Entity/EntityRegistry.cs b/HKMP/Game/Client/Entity/EntityRegistry.cs index 69fb4b96..fc204a7a 100644 --- a/HKMP/Game/Client/Entity/EntityRegistry.cs +++ b/HKMP/Game/Client/Entity/EntityRegistry.cs @@ -7,10 +7,10 @@ namespace Hkmp.Game.Client.Entity; /// -/// Class that manages loading and storing of entity data. Such as names of game objects, names of FSMs and +/// Static class that manages loading and storing of entity data. Such as names of game objects, names of FSMs and /// corresponding types. /// -internal class EntityRegistry { +internal static class EntityRegistry { /// /// The file path of the embedded resource file for the entity registry. /// @@ -19,9 +19,9 @@ internal class EntityRegistry { /// /// List of all entity registry entries that are loaded from the embedded file. /// - private List Entries { get; set; } + private static List Entries { get; } - public void LoadRegistry() { + static EntityRegistry() { Entries = FileUtil.LoadObjectFromEmbeddedJson>(EntityRegistryFilePath); if (Entries == null) { Logger.Warn("Could not load entity registry"); @@ -35,7 +35,7 @@ public void LoadRegistry() { /// The name of the FSM. /// The entry if it is found; otherwise null. /// True if the entry was found; otherwise false. - public bool TryGetEntry(string gameObjectName, string fsmName, out EntityRegistryEntry foundEntry) { + public static bool TryGetEntry(string gameObjectName, string fsmName, out EntityRegistryEntry foundEntry) { foreach (var entry in Entries) { if (!entry.FsmName.Equals(fsmName)) { continue; @@ -58,7 +58,7 @@ public bool TryGetEntry(string gameObjectName, string fsmName, out EntityRegistr /// The type of the entity. /// The entry if it is found; otherwise null. /// True if the entry was found; otherwise false. - public bool TryGetEntry(string gameObjectName, EntityType type, out EntityRegistryEntry foundEntry) { + public static bool TryGetEntry(string gameObjectName, EntityType type, out EntityRegistryEntry foundEntry) { foreach (var entry in Entries) { if (!entry.Type.Equals(type)) { continue; diff --git a/HKMP/Networking/Packet/UpdatePacket.cs b/HKMP/Networking/Packet/UpdatePacket.cs index 5a613557..e290f604 100644 --- a/HKMP/Networking/Packet/UpdatePacket.cs +++ b/HKMP/Networking/Packet/UpdatePacket.cs @@ -911,7 +911,7 @@ protected override IPacketData InstantiatePacketDataFromId(ClientPacketId packet case ClientPacketId.PlayerAlreadyInScene: return new ClientPlayerAlreadyInScene(); case ClientPacketId.PlayerLeaveScene: - return new PacketDataCollection(); + return new PacketDataCollection(); case ClientPacketId.PlayerUpdate: return new PacketDataCollection(); case ClientPacketId.PlayerMapUpdate: From aae71c41486893952f7d24761a3e07ceca6b5b7c Mon Sep 17 00:00:00 2001 From: Extremelyd1 Date: Sun, 14 May 2023 18:59:11 +0200 Subject: [PATCH 027/216] Add SetVelocity2d as entity action --- .../Client/Entity/Action/EntityFsmActions.cs | 98 ++++++++++++++++--- HKMP/Game/Client/Entity/Entity.cs | 2 +- 2 files changed, 84 insertions(+), 16 deletions(-) diff --git a/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs b/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs index 3612f349..5ce69d45 100644 --- a/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs +++ b/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs @@ -62,13 +62,13 @@ static EntityFsmActions() { var parameterInfos = methodInfo.GetParameters(); if (parameterInfos.Length != 2) { // Can't be a method that gets or applies entity network data - return; + continue; } // Filter out the base methods var parameterType = parameterInfos[1].ParameterType; if (parameterType.IsAbstract || !parameterType.IsSubclassOf(typeof(FsmStateAction))) { - return; + continue; } SupportedActionTypes.Add(parameterType); @@ -144,9 +144,36 @@ public static void ApplyNetworkDataFromAction(EntityNetworkData data, FsmStateAc } } + /// + /// 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) { + foreach (var fsm in gameObject.GetComponents()) { + if (EntityRegistry.TryGetEntry(fsm.gameObject.name, fsm.Fsm.Name, out _)) { + return true; + } + } + + return false; + } + #region SpawnObjectFromGlobalPool private static bool GetNetworkDataFromAction(EntityNetworkData data, SpawnObjectFromGlobalPool action) { + EntitySpawnEvent?.Invoke(action, action.storeObject.Value); + + // We first check whether this action results in the spawning of an entity that is managed by the + // system. Because if so, it would already be handled by an EntitySpawn packet instead, and this will only + // duplicate the spawning and leave it uncontrolled. So we don't send the data at all + var toSpawnObject = action.storeObject.Value; + if (IsObjectInRegistry(toSpawnObject)) { + Logger.Debug($"Tried getting SpawnObjectFromGlobalPool network data, but spawned object is entity"); + return false; + } + var position = Vector3.zero; var euler = Vector3.up; @@ -180,23 +207,10 @@ private static bool GetNetworkDataFromAction(EntityNetworkData data, SpawnObject data.Packet.Write(euler.y); data.Packet.Write(euler.z); - EntitySpawnEvent?.Invoke(action, action.storeObject.Value); - return true; } private static void ApplyNetworkDataFromAction(EntityNetworkData data, SpawnObjectFromGlobalPool action) { - // We first check whether applying this action results in the spawning of an entity that is managed by the - // system. Because if so, it would already be handled by an EntitySpawn packet instead, and this will only - // duplicate the spawning and leave it uncontrolled - var toSpawnObject = action.gameObject.Value; - foreach (var fsm in toSpawnObject.GetComponents()) { - if (EntityRegistry.TryGetEntry(fsm.gameObject.name, fsm.Fsm.Name, out var entry)) { - Logger.Debug($"Tried applying SpawnObjectFromGlobalPool network data, but to spawn object is entity: {entry.Type}"); - return; - } - } - var position = new Vector3( data.Packet.ReadFloat(), data.Packet.ReadFloat(), @@ -557,4 +571,58 @@ private static void ApplyNetworkDataFromAction(EntityNetworkData data, GetParent } #endregion + + #region SetVelocity2d + + private static bool GetNetworkDataFromAction(EntityNetworkData data, SetVelocity2d action) { + var gameObject = action.Fsm.GetOwnerDefaultTarget(action.gameObject); + if (gameObject == null) { + return false; + } + + if (IsObjectInRegistry(gameObject)) { + Logger.Debug("Tried getting SetVelocity2d network data, but entity is in registry"); + return false; + } + + var rigidbody = gameObject.GetComponent(); + if (rigidbody == null) { + return false; + } + + var vector = action.vector.IsNone ? rigidbody.velocity : action.vector.Value; + if (!action.x.IsNone) { + vector.x = action.x.Value; + } + + if (!action.y.IsNone) { + vector.y = action.y.Value; + } + + data.Packet.Write(vector.x); + data.Packet.Write(vector.y); + + return true; + } + + private static void ApplyNetworkDataFromAction(EntityNetworkData data, SetVelocity2d action) { + var vector = new Vector2( + data.Packet.ReadFloat(), + data.Packet.ReadFloat() + ); + + var gameObject = action.Fsm.GetOwnerDefaultTarget(action.gameObject); + if (gameObject == null) { + return; + } + + var rigidbody = gameObject.GetComponent(); + if (rigidbody == null) { + return; + } + + rigidbody.velocity = vector; + } + + #endregion } diff --git a/HKMP/Game/Client/Entity/Entity.cs b/HKMP/Game/Client/Entity/Entity.cs index bc7a3334..00441cdf 100644 --- a/HKMP/Game/Client/Entity/Entity.cs +++ b/HKMP/Game/Client/Entity/Entity.cs @@ -37,7 +37,7 @@ internal class Entity { /// /// Host-client pair for the game objects. /// - public HostClientPair Object { get; init; } + public HostClientPair Object { get; } /// /// Host-client pair for the sprite animators. From 43153b6bc7f8d0cdc74917e971e776b582ce0bdb Mon Sep 17 00:00:00 2001 From: Extremelyd1 Date: Sat, 20 May 2023 22:39:14 +0200 Subject: [PATCH 028/216] Progress towards scene host transfer --- HKMP/Game/Client/ClientManager.cs | 17 +- HKMP/Game/Client/Entity/Entity.cs | 260 ++++++- HKMP/Game/Client/Entity/EntityManager.cs | 634 +++++++++--------- HKMP/Game/Client/Entity/FsmSnapshot.cs | 52 ++ HKMP/Game/Server/ServerEntityData.cs | 6 + HKMP/Game/Server/ServerManager.cs | 57 +- HKMP/HKMP.csproj | 8 +- HKMP/Networking/Client/ClientUpdateManager.cs | 20 + .../Packet/Data/ClientPlayerDisconnect.cs | 7 - HKMP/Networking/Packet/Data/EntityUpdate.cs | 250 ++++++- .../Packet/Data/PlayerLeaveScene.cs | 28 +- HKMP/Networking/Packet/IPacket.cs | 12 + HKMP/Networking/Packet/Packet.cs | 14 + HKMP/Networking/Packet/PacketId.cs | 7 +- HKMP/Networking/Packet/UpdatePacket.cs | 2 + HKMP/Networking/Server/ServerUpdateManager.cs | 39 +- 16 files changed, 1020 insertions(+), 393 deletions(-) create mode 100644 HKMP/Game/Client/Entity/FsmSnapshot.cs diff --git a/HKMP/Game/Client/ClientManager.cs b/HKMP/Game/Client/ClientManager.cs index c176a593..c58238f5 100644 --- a/HKMP/Game/Client/ClientManager.cs +++ b/HKMP/Game/Client/ClientManager.cs @@ -219,6 +219,7 @@ ModSettings modSettings OnPlayerMapUpdate); packetManager.RegisterClientPacketHandler(ClientPacketId.EntitySpawn, OnEntitySpawn); packetManager.RegisterClientPacketHandler(ClientPacketId.EntityUpdate, OnEntityUpdate); + packetManager.RegisterClientPacketHandler(ClientPacketId.SceneHostTransfer, OnSceneHostTransfer); packetManager.RegisterClientPacketHandler(ClientPacketId.ServerSettingsUpdated, OnServerSettingsUpdated); packetManager.RegisterClientPacketHandler(ClientPacketId.ChatMessage, OnChatMessage); @@ -665,10 +666,6 @@ private void OnPlayerLeaveScene(ClientPlayerLeaveScene data) { Logger.Info($"Player {id} left scene"); - if (data.NewSceneHost) { - _entityManager.BecomeSceneHost(); - } - if (!_playerData.TryGetValue(id, out var playerData)) { Logger.Info($"Could not find player data for player with ID {id}"); return; @@ -751,6 +748,12 @@ private void OnEntityUpdate(EntityUpdate entityUpdate) { HandleEntityUpdate(entityUpdate); } + private void OnSceneHostTransfer() { + Logger.Info("Received scene host transfer"); + + _entityManager.BecomeSceneHost(); + } + /// /// Method for handling received entity updates. /// @@ -781,6 +784,10 @@ private void HandleEntityUpdate(EntityUpdate entityUpdate, bool alreadyInSceneUp if (entityUpdate.UpdateTypes.Contains(EntityUpdateType.Data)) { _entityManager.UpdateEntityData(entityUpdate.Id, entityUpdate.GenericData); } + + if (entityUpdate.UpdateTypes.Contains(EntityUpdateType.HostFsm)) { + _entityManager.UpdateHostEntityFsmData(entityUpdate.Id, entityUpdate.HostFsmData); + } } /// @@ -927,7 +934,7 @@ private void OnSceneChange(Scene oldScene, Scene newScene) { _sceneHostDetermined = false; // Ignore scene changes from and to non-gameplay scenes - if (SceneUtil.IsNonGameplayScene(oldScene.name) && SceneUtil.IsNonGameplayScene(newScene.name)) { + if (SceneUtil.IsNonGameplayScene(oldScene.name)) { return; } diff --git a/HKMP/Game/Client/Entity/Entity.cs b/HKMP/Game/Client/Entity/Entity.cs index 00441cdf..f2741ced 100644 --- a/HKMP/Game/Client/Entity/Entity.cs +++ b/HKMP/Game/Client/Entity/Entity.cs @@ -96,6 +96,12 @@ internal class Entity { /// private bool _allowClientAnimation; + /// + /// List of snapshots for each FSM of a host entity that contain latest values for state and FSM variables. + /// Used to check whether state/variables change and to update the server accordingly. + /// + private List _fsmSnapshots; + public Entity( NetClient netClient, byte entityId, @@ -131,7 +137,7 @@ GameObject hostObject // Add a position interpolation component to the enemy so we can smooth out position updates Object.Client.AddComponent(); - // Register an update event to send position updates + // Register an update event to send position updates and check for certain value changes MonoBehaviourUtil.Instance.OnUpdateEvent += OnUpdate; _animator = new HostClientPair { @@ -156,17 +162,17 @@ GameObject hostObject } On.tk2dSpriteAnimator.Play_tk2dSpriteAnimationClip_float_float += OnAnimationPlayed; - - // Always disallow the client object from being recycled, because it will simply be destroyed - On.ObjectPool.Recycle_GameObject += (orig, obj) => { - if (obj == Object.Client) { - Logger.Debug($"Client object of entity: {_entityId}, {type} tried to be recycled"); - return; - } - - orig(obj); - }; } + + // Always disallow the client object from being recycled, because it will simply be destroyed + On.ObjectPool.Recycle_GameObject += (orig, obj) => { + if (obj == Object.Client) { + Logger.Debug($"Client object of entity: {_entityId}, {type} tried to be recycled"); + return; + } + + orig(obj); + }; _fsms = new HostClientPair> { Host = Object.Host.GetComponents().ToList(), @@ -175,6 +181,7 @@ GameObject hostObject _hookedActions = new Dictionary(); _hookedTypes = new HashSet(); + _fsmSnapshots = new List(); foreach (var fsm in _fsms.Host) { ProcessHostFsm(fsm); } @@ -225,6 +232,31 @@ private void ProcessHostFsm(PlayMakerFSM fsm) { } } } + + var snapshot = new FsmSnapshot { + CurrentState = fsm.ActiveStateName + }; + + foreach (var f in fsm.FsmVariables.FloatVariables) { + snapshot.Floats.Add(f.Name, f.Value); + } + foreach (var i in fsm.FsmVariables.IntVariables) { + snapshot.Ints.Add(i.Name, i.Value); + } + foreach (var b in fsm.FsmVariables.BoolVariables) { + snapshot.Bools.Add(b.Name, b.Value); + } + foreach (var s in fsm.FsmVariables.StringVariables) { + snapshot.Strings.Add(s.Name, s.Value); + } + foreach (var vec2 in fsm.FsmVariables.Vector2Variables) { + snapshot.Vector2s.Add(vec2.Name, vec2.Value); + } + foreach (var vec3 in fsm.FsmVariables.Vector3Variables) { + snapshot.Vector3s.Add(vec3.Name, vec3.Value); + } + + _fsmSnapshots.Add(snapshot); } /// @@ -396,6 +428,112 @@ private void OnUpdate() { newActive ); } + + for (byte fsmIndex = 0; fsmIndex < _fsms.Host.Count; fsmIndex++) { + var fsm = _fsms.Host[fsmIndex]; + var snapshot = _fsmSnapshots[fsmIndex]; + + var data = new EntityHostFsmData(); + + var lastStateName = snapshot.CurrentState; + if (fsm.ActiveStateName != lastStateName) { + snapshot.CurrentState = fsm.ActiveStateName; + + data.Types.Add(EntityHostFsmData.Type.State); + data.CurrentState = (byte) Array.IndexOf(fsm.FsmStates, fsm.Fsm.ActiveState); + } + + // Define a method that allows generalization of checking for changes in all FSM variables + void CondAddData( + VarType[] fsmVars, + Dictionary snapshotDict, + Func fsmVarName, + Func fsmVarValue, + EntityHostFsmData.Type type, + Dictionary dataDict + ) { + for (byte i = 0; i < fsmVars.Length; i++) { + var fsmVar = fsmVars[i]; + + var name = fsmVarName.Invoke(fsmVar); + if (!snapshotDict.TryGetValue(name, out var lastValue)) { + Logger.Warn($"No last value found for FSM var: {name}"); + continue; + } + + var value = fsmVarValue.Invoke(fsmVar); + if (!value.Equals(lastValue)) { + // Update the value in the snapshot since it changed + snapshotDict[name] = value; + + data.Types.Add(type); + // Some funky casting here to make sure we can use this method with Vector2 and Vector3 + // 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; + } else if (value is Vector3 vec3) { + dataDict[i] = (DataType) (object) (Hkmp.Math.Vector3) vec3; + } else { + dataDict[i] = (DataType) (object) value; + } + } + } + } + + CondAddData( + fsm.FsmVariables.FloatVariables, + snapshot.Floats, + fsmFloat => fsmFloat.Name, + fsmFloat => fsmFloat.Value, + EntityHostFsmData.Type.Floats, + data.Floats + ); + CondAddData( + fsm.FsmVariables.IntVariables, + snapshot.Ints, + fsmInt => fsmInt.Name, + fsmInt => fsmInt.Value, + EntityHostFsmData.Type.Ints, + data.Ints + ); + CondAddData( + fsm.FsmVariables.BoolVariables, + snapshot.Bools, + fsmBool => fsmBool.Name, + fsmBool => fsmBool.Value, + EntityHostFsmData.Type.Bools, + data.Bools + ); + CondAddData( + fsm.FsmVariables.StringVariables, + snapshot.Strings, + fsmString => fsmString.Name, + fsmString => fsmString.Value, + EntityHostFsmData.Type.Strings, + data.Strings + ); + CondAddData( + fsm.FsmVariables.Vector2Variables, + snapshot.Vector2s, + fsmVec2 => fsmVec2.Name, + fsmVec2 => fsmVec2.Value, + EntityHostFsmData.Type.Vector2s, + data.Vec2s + ); + CondAddData( + fsm.FsmVariables.Vector3Variables, + snapshot.Vector3s, + fsmVec3 => fsmVec3.Name, + fsmVec3 => fsmVec3.Value, + EntityHostFsmData.Type.Vector3s, + data.Vec3s + ); + + if (data.Types.Count > 0) { + _netClient.UpdateManager.AddEntityHostFsmData(_entityId, fsmIndex, data); + } + } } /// @@ -472,13 +610,13 @@ public void InitializeHost() { } } - // TODO: parameters should be all FSM details to kickstart all FSMs of the game object /// /// Makes the entity a host entity if the client user became the scene host. /// public void MakeHost() { - // TODO: read all variables from the parameters and set the FSM variables of all FSMs - + // TODO: disable client object/FSMs, enable host object/FSMs, set current state from snapshots in all FSMs + // TODO: copy position, scale, animation, etc. from client to host (perhaps before disabling/enabling client/host) + InitializeHost(); } @@ -636,6 +774,100 @@ public void UpdateData(List entityNetworkData) { } } + /// + /// Update the FSMs of the host entity to prepare for host transfer or disconnects. + /// + /// Dictionary mapping FSM index to data. + public void UpdateHostFsmData(Dictionary hostFsmData) { + foreach (var fsmPair in hostFsmData) { + var fsmIndex = fsmPair.Key; + var data = fsmPair.Value; + + if (_fsms.Host.Count <= fsmIndex) { + Logger.Warn($"Tried to update host FSM data for unknown FSM index: {fsmIndex}"); + continue; + } + + var fsm = _fsms.Host[fsmIndex]; + var snapshot = _fsmSnapshots[fsmIndex]; + + if (data.Types.Contains(EntityHostFsmData.Type.State)) { + var states = fsm.FsmStates; + if (states.Length <= data.CurrentState) { + Logger.Warn($"Tried to update host FSM state for unknown state index: {data.CurrentState}"); + } else { + snapshot.CurrentState = states[data.CurrentState].Name; + } + } + + void CondUpdateVars( + EntityHostFsmData.Type type, + Dictionary dataDict, + FsmType[] fsmVarArray, + Action 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}"); + } else { + setValueAction.Invoke(fsmVarArray[pair.Key], (UnityType) (object) pair.Value); + } + } + } + } + + CondUpdateVars( + EntityHostFsmData.Type.Floats, + data.Floats, + fsm.FsmVariables.FloatVariables, + (fsmVar, value) => { + fsmVar.Value = value; + snapshot.Floats[fsmVar.Name] = value; + }); + CondUpdateVars( + EntityHostFsmData.Type.Ints, + data.Ints, + fsm.FsmVariables.IntVariables, + (fsmVar, value) => { + fsmVar.Value = value; + snapshot.Ints[fsmVar.Name] = value; + }); + CondUpdateVars( + EntityHostFsmData.Type.Bools, + data.Bools, + fsm.FsmVariables.BoolVariables, + (fsmVar, value) => { + fsmVar.Value = value; + snapshot.Bools[fsmVar.Name] = value; + }); + CondUpdateVars( + EntityHostFsmData.Type.Strings, + data.Strings, + fsm.FsmVariables.StringVariables, + (fsmVar, value) => { + fsmVar.Value = value; + snapshot.Strings[fsmVar.Name] = value; + }); + CondUpdateVars( + EntityHostFsmData.Type.Vector2s, + data.Vec2s, + fsm.FsmVariables.Vector2Variables, + (fsmVar, value) => { + fsmVar.Value = value; + snapshot.Vector2s[fsmVar.Name] = value; + }); + CondUpdateVars( + EntityHostFsmData.Type.Vector3s, + data.Vec3s, + fsm.FsmVariables.Vector3Variables, + (fsmVar, value) => { + fsmVar.Value = value; + snapshot.Vector3s[fsmVar.Name] = value; + }); + } + } + /// /// Destroys the entity. /// diff --git a/HKMP/Game/Client/Entity/EntityManager.cs b/HKMP/Game/Client/Entity/EntityManager.cs index e709b203..9b48ab6e 100644 --- a/HKMP/Game/Client/Entity/EntityManager.cs +++ b/HKMP/Game/Client/Entity/EntityManager.cs @@ -9,388 +9,406 @@ using Logger = Hkmp.Logging.Logger; using Vector2 = Hkmp.Math.Vector2; -namespace Hkmp.Game.Client.Entity { +namespace Hkmp.Game.Client.Entity; + +/// +/// Manager class that handles entity creation, updating, networking and destruction. +/// +internal class EntityManager { /// - /// Manager class that handles entity creation, updating, networking and destruction. + /// The net client for networking. /// - internal class EntityManager { - /// - /// The net client for networking. - /// - private readonly NetClient _netClient; - - /// - /// Dictionary mapping entity IDs to their respective entity instances. - /// - private readonly Dictionary _entities; - - /// - /// Whether the client user is the scene host. - /// - private bool _isSceneHost; - - /// - /// The last used ID of an entity. - /// - private byte _lastId; - - public EntityManager(NetClient netClient) { - _netClient = netClient; - _entities = new Dictionary(); - - _lastId = 0; - - EntityFsmActions.EntitySpawnEvent += OnGameObjectSpawned; - UnityEngine.SceneManagement.SceneManager.sceneLoaded += OnSceneLoaded; - UnityEngine.SceneManagement.SceneManager.activeSceneChanged += OnSceneChanged; - } + private readonly NetClient _netClient; - /// - /// Initializes the entity manager if we are the scene host. - /// - public void InitializeSceneHost() { - Logger.Info("Releasing control of all registered entities"); + /// + /// Dictionary mapping entity IDs to their respective entity instances. + /// + private readonly Dictionary _entities; - _isSceneHost = true; + /// + /// Whether the client user is the scene host. + /// + private bool _isSceneHost; - foreach (var entity in _entities.Values) { - entity.InitializeHost(); - } - } + /// + /// The last used ID of an entity. + /// + private byte _lastId; + + public EntityManager(NetClient netClient) { + _netClient = netClient; + _entities = new Dictionary(); + + _lastId = 0; + + EntityFsmActions.EntitySpawnEvent += OnGameObjectSpawned; + UnityEngine.SceneManagement.SceneManager.sceneLoaded += OnSceneLoaded; + UnityEngine.SceneManagement.SceneManager.activeSceneChanged += OnSceneChanged; + } + + /// + /// Initializes the entity manager if we are the scene host. + /// + public void InitializeSceneHost() { + Logger.Info("Releasing control of all registered entities"); - /// - /// Initializes the entity manager if we are a scene client. - /// - public void InitializeSceneClient() { - Logger.Info("Taking control of all registered entities"); + _isSceneHost = true; - _isSceneHost = false; + foreach (var entity in _entities.Values) { + entity.InitializeHost(); } + } - /// - /// Updates the entity manager if we become the scene host. - /// - public void BecomeSceneHost() { - Logger.Info("Becoming scene host"); + /// + /// Initializes the entity manager if we are a scene client. + /// + public void InitializeSceneClient() { + Logger.Info("Taking control of all registered entities"); - _isSceneHost = true; + _isSceneHost = false; + } - foreach (var entity in _entities.Values) { - entity.MakeHost(); - } + /// + /// Updates the entity manager if we become the scene host. + /// + public void BecomeSceneHost() { + Logger.Info("Becoming scene host"); + + _isSceneHost = true; + + foreach (var entity in _entities.Values) { + entity.MakeHost(); } + } - /// - /// Spawn an entity with the given ID and type, that was spawned from the given entity type. - /// - /// The ID of the entity. - /// The type of the entity that spawned the new entity. - /// The type of the spawned entity. - public void SpawnEntity(byte id, EntityType spawningType, EntityType spawnedType) { - Logger.Info($"Trying to spawn entity with ID {id} with types: {spawningType}, {spawnedType}"); - - // If an entity with the new ID already exists, we return - if (_entities.ContainsKey(id)) { - Logger.Info($" Entity with ID {id} already exists, assuming it has been spawned by action"); - return; - } + /// + /// Spawn an entity with the given ID and type, that was spawned from the given entity type. + /// + /// The ID of the entity. + /// The type of the entity that spawned the new entity. + /// The type of the spawned entity. + public void SpawnEntity(byte id, EntityType spawningType, EntityType spawnedType) { + Logger.Info($"Trying to spawn entity with ID {id} with types: {spawningType}, {spawnedType}"); + + // If an entity with the new ID already exists, we return + if (_entities.ContainsKey(id)) { + Logger.Info($" Entity with ID {id} already exists, assuming it has been spawned by action"); + return; + } - // Find the list of client FSMs that correspond to an entity with the given type in our current scene - // Doesn't matter which instance of entity it is, because the FSMs will be the same - List clientFsms = null; - foreach (var existingEntity in _entities.Values) { - if (existingEntity.Type == spawningType) { - clientFsms = existingEntity.GetClientFsms(); - break; - } + // Find the list of client FSMs that correspond to an entity with the given type in our current scene + // Doesn't matter which instance of entity it is, because the FSMs will be the same + List clientFsms = null; + foreach (var existingEntity in _entities.Values) { + if (existingEntity.Type == spawningType) { + clientFsms = existingEntity.GetClientFsms(); + break; } + } - // If no such FSMs exist we return again, because we can't spawn the new entity - if (clientFsms == null) { - Logger.Warn($"Could not find entity with same type for spawning"); - return; - } + // If no such FSMs exist we return again, because we can't spawn the new entity + if (clientFsms == null) { + Logger.Warn($"Could not find entity with same type for spawning"); + return; + } - var gameObject = EntitySpawner.SpawnEntityGameObject(spawningType, spawnedType, clientFsms); + var gameObject = EntitySpawner.SpawnEntityGameObject(spawningType, spawnedType, clientFsms); - foreach (var fsm in gameObject.GetComponents()) { - if (!EntityRegistry.TryGetEntry(gameObject.name, fsm.Fsm.Name, out _)) { - EntityInitializer.InitializeClientFsm(fsm); - } + foreach (var fsm in gameObject.GetComponents()) { + if (!EntityRegistry.TryGetEntry(gameObject.name, fsm.Fsm.Name, out _)) { + EntityInitializer.InitializeClientFsm(fsm); } + } + + var entity = new Entity( + _netClient, + id, + spawnedType, + gameObject + ); + _entities[id] = entity; + } - var entity = new Entity( - _netClient, - id, - spawnedType, - gameObject - ); - _entities[id] = entity; + /// + /// Update the position for the entity with the given ID. + /// + /// The entity ID. + /// The new position. + public void UpdateEntityPosition(byte entityId, Vector2 position) { + if (_isSceneHost) { + return; } - /// - /// Update the position for the entity with the given ID. - /// - /// The entity ID. - /// The new position. - public void UpdateEntityPosition(byte entityId, Vector2 position) { - if (_isSceneHost) { - return; - } + if (!_entities.TryGetValue(entityId, out var entity)) { + return; + } - if (!_entities.TryGetValue(entityId, out var entity)) { - return; - } + entity.UpdatePosition(position); + } - entity.UpdatePosition(position); + /// + /// Update the scale for the entity with the given ID. + /// + /// The entity ID. + /// The new scale. + public void UpdateEntityScale(byte entityId, bool scale) { + if (_isSceneHost) { + return; } - /// - /// Update the scale for the entity with the given ID. - /// - /// The entity ID. - /// The new scale. - public void UpdateEntityScale(byte entityId, bool scale) { - if (_isSceneHost) { - return; - } + if (!_entities.TryGetValue(entityId, out var entity)) { + return; + } - if (!_entities.TryGetValue(entityId, out var entity)) { - return; - } + entity.UpdateScale(scale); + } - entity.UpdateScale(scale); + /// + /// Update the animation for the entity with the given ID. + /// + /// The entity ID. + /// The ID of the animation. + /// The wrap mode of the animation. + /// Whether this update is when we are entering the scene. + public void UpdateEntityAnimation( + byte entityId, + byte animationId, + byte animationWrapMode, + bool alreadyInSceneUpdate + ) { + if (_isSceneHost) { + return; } - /// - /// Update the animation for the entity with the given ID. - /// - /// The entity ID. - /// The ID of the animation. - /// The wrap mode of the animation. - /// Whether this update is when we are entering the scene. - public void UpdateEntityAnimation( - byte entityId, - byte animationId, - byte animationWrapMode, - bool alreadyInSceneUpdate - ) { - if (_isSceneHost) { - return; - } + if (!_entities.TryGetValue(entityId, out var entity)) { + return; + } - if (!_entities.TryGetValue(entityId, out var entity)) { - return; - } + entity.UpdateAnimation(animationId, (tk2dSpriteAnimationClip.WrapMode) animationWrapMode, + alreadyInSceneUpdate); + } + + /// + /// Update whether the entity with the given ID is active. + /// + /// The entity ID. + /// The new value for active. + public void UpdateEntityIsActive(byte entityId, bool isActive) { + if (_isSceneHost) { + return; + } - entity.UpdateAnimation(animationId, (tk2dSpriteAnimationClip.WrapMode) animationWrapMode, - alreadyInSceneUpdate); + if (!_entities.TryGetValue(entityId, out var entity)) { + return; } - /// - /// Update whether the entity with the given ID is active. - /// - /// The entity ID. - /// The new value for active. - public void UpdateEntityIsActive(byte entityId, bool isActive) { - if (_isSceneHost) { - return; - } + entity.UpdateIsActive(isActive); + } - if (!_entities.TryGetValue(entityId, out var entity)) { - return; - } + /// + /// Update the entity with the given ID with the given generic data. + /// + /// The ID of the entity. + /// The list of data to update the entity with. + public void UpdateEntityData(byte entityId, List data) { + if (_isSceneHost) { + return; + } - entity.UpdateIsActive(isActive); + if (!_entities.TryGetValue(entityId, out var entity)) { + return; } - /// - /// Update the entity with the given ID with the given generic data. - /// - /// The ID of the entity. - /// The list of data to update the entity with. - public void UpdateEntityData(byte entityId, List data) { - if (_isSceneHost) { - return; - } + entity.UpdateData(data); + } - if (!_entities.TryGetValue(entityId, out var entity)) { - return; - } + /// + /// Update the host FSMs of the entity with the given ID with the given data. + /// + /// The ID of the entity. + /// Dictionary mapping FSM index to FSM data. + public void UpdateHostEntityFsmData(byte entityId, Dictionary hostFsmData) { + if (_isSceneHost) { + return; + } - entity.UpdateData(data); + if (!_entities.TryGetValue(entityId, out var entity)) { + return; + } + + entity.UpdateHostFsmData(hostFsmData); + } + + /// + /// Callback method for when a game object is spawned from an existing entity. + /// + /// The action from which the game object was spawned. + /// The game object that was spawned. + private void OnGameObjectSpawned(FsmStateAction action, GameObject gameObject) { + if (_entities.Values.Any(entity => entity.Object.Host == gameObject)) { + Logger.Debug("Spawned object was already a registered entity"); + return; } - /// - /// Callback method for when a game object is spawned from an existing entity. - /// - /// The action from which the game object was spawned. - /// The game object that was spawned. - private void OnGameObjectSpawned(FsmStateAction action, GameObject gameObject) { - if (_entities.Values.Any(entity => entity.Object.Host == gameObject)) { - Logger.Debug("Spawned object was already a registered entity"); - return; + foreach (var fsm in gameObject.GetComponents()) { + if (!ProcessGameObjectFsm(fsm, out var entity, out var entityId)) { + continue; } - foreach (var fsm in gameObject.GetComponents()) { - if (!ProcessGameObjectFsm(fsm, out var entity, out var entityId)) { - continue; + if (_isSceneHost) { + // Since an entity was created and we are the scene host, we need to notify the server + var spawningObjectName = action.Fsm.GameObject.name; + var spawningFsmName = action.Fsm.Name; + if (EntityRegistry.TryGetEntry(spawningObjectName, spawningFsmName, out var entry)) { + Logger.Info( + $"Notifying server of entity ({spawningObjectName}, {entry.Type}) spawning entity ({gameObject.name}, {entity.Type}) with ID {entityId}"); + _netClient.UpdateManager.SetEntitySpawn(entityId, entry.Type, entity.Type); } - - if (_isSceneHost) { - // Since an entity was created and we are the scene host, we need to notify the server - var spawningObjectName = action.Fsm.GameObject.name; - var spawningFsmName = action.Fsm.Name; - if (EntityRegistry.TryGetEntry(spawningObjectName, spawningFsmName, out var entry)) { - Logger.Info( - $"Notifying server of entity ({spawningObjectName}, {entry.Type}) spawning entity ({gameObject.name}, {entity.Type}) with ID {entityId}"); - _netClient.UpdateManager.SetEntitySpawn(entityId, entry.Type, entity.Type); - } - // Also initialize the entity as host, since otherwise it will stay disabled - entity.InitializeHost(); - } else { - // Since an entity was created and we are not the scene host, we need to manually initialize - // all the FSM on the client - foreach (var clientFsm in entity.GetClientFsms()) { - Logger.Info($"Manually initializing client entity FSM: {clientFsm.Fsm.Name}, {clientFsm.gameObject.name}"); - EntityInitializer.InitializeClientFsm(clientFsm); - } - - // We also need to update the 'active' state of the entity since it was spawned - entity.UpdateIsActive(true); + // Also initialize the entity as host, since otherwise it will stay disabled + entity.InitializeHost(); + } else { + // Since an entity was created and we are not the scene host, we need to manually initialize + // all the FSM on the client + foreach (var clientFsm in entity.GetClientFsms()) { + Logger.Info($"Manually initializing client entity FSM: {clientFsm.Fsm.Name}, {clientFsm.gameObject.name}"); + EntityInitializer.InitializeClientFsm(clientFsm); } + + // We also need to update the 'active' state of the entity since it was spawned + entity.UpdateIsActive(true); } } + } - /// - /// Callback method for when the scene changes. - /// - /// The old scene. - /// The new scene. - private void OnSceneChanged(Scene oldScene, Scene newScene) { - Logger.Info($"Scene changed, clearing registered entities"); - - foreach (var entity in _entities.Values) { - entity.Destroy(); - } - - _entities.Clear(); + /// + /// Callback method for when the scene changes. Will clear existing entities and start checking for + /// new entities. + /// + /// The old scene. + /// The new scene. + private void OnSceneChanged(Scene oldScene, Scene newScene) { + Logger.Info($"Scene changed, clearing registered entities"); + + foreach (var entity in _entities.Values) { + entity.Destroy(); + } - _lastId = 0; + _entities.Clear(); - if (!_netClient.IsConnected) { - return; - } + _lastId = 0; - FindEntitiesInScene(newScene, false); + if (!_netClient.IsConnected) { + return; } - /// - /// Callback method for when a scene is loaded. - /// - /// The scene that is loaded. - /// The load scene mode. - private void OnSceneLoaded(Scene scene, LoadSceneMode mode) { - var currentSceneName = UnityEngine.SceneManagement.SceneManager.GetActiveScene().name; - // If this scene is a boss or boss-defeated scene it starts with the same name, so we skip all other - // loaded scenes - if (!scene.name.StartsWith(currentSceneName)) { - return; - } - - Logger.Info($"Additional scene loaded ({scene.name}), looking for entities"); + FindEntitiesInScene(newScene, false); + } - FindEntitiesInScene(scene, true); + /// + /// Callback method for when a scene is loaded. + /// + /// The scene that is loaded. + /// The load scene mode. + private void OnSceneLoaded(Scene scene, LoadSceneMode mode) { + var currentSceneName = UnityEngine.SceneManagement.SceneManager.GetActiveScene().name; + // If this scene is a boss or boss-defeated scene it starts with the same name, so we skip all other + // loaded scenes + if (!scene.name.StartsWith(currentSceneName)) { + return; } - /// - /// Find entities to register in the given scene. - /// - /// The scene to find entities in. - /// Whether this scene was loaded late. - private void FindEntitiesInScene(Scene scene, bool lateLoad) { - // Find all PlayMakerFSM components - foreach (var fsm in Object.FindObjectsOfType()) { - // Logger.Info($"Found FSM: {fsm.Fsm.Name} in scene: {fsm.gameObject.scene.name}"); - if (fsm.gameObject.scene != scene) { - continue; - } + Logger.Info($"Additional scene loaded ({scene.name}), looking for entities"); - if (!ProcessGameObjectFsm(fsm, out var entity, out _) || !lateLoad) { - continue; - } + FindEntitiesInScene(scene, true); + } - if (_isSceneHost) { - // Since this is a late load it needs to be initialized as host if we are the scene host - entity.InitializeHost(); - } else { - // Since this is a late load we need to update the 'active' state of the entity - entity.UpdateIsActive(true); - } + /// + /// Find entities to register in the given scene. + /// + /// The scene to find entities in. + /// Whether this scene was loaded late. + private void FindEntitiesInScene(Scene scene, bool lateLoad) { + // Find all PlayMakerFSM components + foreach (var fsm in Object.FindObjectsOfType()) { + // Logger.Info($"Found FSM: {fsm.Fsm.Name} in scene: {fsm.gameObject.scene.name}"); + if (fsm.gameObject.scene != scene) { + continue; } - // Find all Climber components - foreach (var climber in Object.FindObjectsOfType()) { - if (!EntityRegistry.TryGetEntry(climber.gameObject.name, EntityType.Tiktik, out var entry)) { - continue; - } + if (!ProcessGameObjectFsm(fsm, out var entity, out _) || !lateLoad) { + continue; + } - RegisterGameObjectAsEntity(climber.gameObject, entry.Type, out _); + if (_isSceneHost) { + // Since this is a late load it needs to be initialized as host if we are the scene host + entity.InitializeHost(); + } else { + // Since this is a late load we need to update the 'active' state of the entity + entity.UpdateIsActive(true); } } - /// - /// Process the FSM of a game object to check whether the game object should be registered as an entity. - /// - /// The FSM to process. - /// The resulting entity if one was created; otherwise null. - /// The ID of the entity if one was created; otherwise 0. - /// True if an entity was created; otherwise false. - private bool ProcessGameObjectFsm(PlayMakerFSM fsm, out Entity entity, out byte entityId) { - // Logger.Info($"Processing FSM: {fsm.Fsm.Name}, {fsm.gameObject.name}"); - - if (!EntityRegistry.TryGetEntry(fsm.gameObject.name, fsm.Fsm.Name, out var entry)) { - entity = null; - entityId = 0; - return false; + // Find all Climber components + foreach (var climber in Object.FindObjectsOfType()) { + if (!EntityRegistry.TryGetEntry(climber.gameObject.name, EntityType.Tiktik, out var entry)) { + continue; } - entity = RegisterGameObjectAsEntity(fsm.gameObject, entry.Type, out entityId); - return true; + RegisterGameObjectAsEntity(climber.gameObject, entry.Type, out _); } + } - /// - /// Register a given game object as an entity and return that entity. - /// - /// The game object to register. - /// The type of the entity. - /// The ID of the registered entity. - /// The entity that was created. - private Entity RegisterGameObjectAsEntity(GameObject gameObject, EntityType type, out byte entityId) { - // First find a usable ID that is not registered to an entity already - while (_entities.ContainsKey(_lastId)) { - _lastId++; - } + /// + /// Process the FSM of a game object to check whether the game object should be registered as an entity. + /// + /// The FSM to process. + /// The resulting entity if one was created; otherwise null. + /// The ID of the entity if one was created; otherwise 0. + /// True if an entity was created; otherwise false. + private bool ProcessGameObjectFsm(PlayMakerFSM fsm, out Entity entity, out byte entityId) { + // Logger.Info($"Processing FSM: {fsm.Fsm.Name}, {fsm.gameObject.name}"); + + if (!EntityRegistry.TryGetEntry(fsm.gameObject.name, fsm.Fsm.Name, out var entry)) { + entity = null; + entityId = 0; + return false; + } - Logger.Info($"Registering entity ({type}) '{gameObject.name}' with ID '{_lastId}'"); + entity = RegisterGameObjectAsEntity(fsm.gameObject, entry.Type, out entityId); + return true; + } - // TODO: maybe we need to check whether this entity game object has already been registered, which can - // happen with game objects that have multiple FSMs + /// + /// Register a given game object as an entity and return that entity. + /// + /// The game object to register. + /// The type of the entity. + /// The ID of the registered entity. + /// The entity that was created. + private Entity RegisterGameObjectAsEntity(GameObject gameObject, EntityType type, out byte entityId) { + // First find a usable ID that is not registered to an entity already + while (_entities.ContainsKey(_lastId)) { + _lastId++; + } - var entity = new Entity( - _netClient, - _lastId, - type, - gameObject - ); - _entities[_lastId] = entity; + Logger.Info($"Registering entity ({type}) '{gameObject.name}' with ID '{_lastId}'"); - entityId = _lastId; + // TODO: maybe we need to check whether this entity game object has already been registered, which can + // happen with game objects that have multiple FSMs - _lastId++; + var entity = new Entity( + _netClient, + _lastId, + type, + gameObject + ); + _entities[_lastId] = entity; - return entity; - } + entityId = _lastId; + + _lastId++; + + return entity; } -} +} \ No newline at end of file diff --git a/HKMP/Game/Client/Entity/FsmSnapshot.cs b/HKMP/Game/Client/Entity/FsmSnapshot.cs new file mode 100644 index 00000000..89f41107 --- /dev/null +++ b/HKMP/Game/Client/Entity/FsmSnapshot.cs @@ -0,0 +1,52 @@ +using System.Collections.Generic; +using UnityEngine; + +namespace Hkmp.Game.Client.Entity; + +/// +/// Snapshot of FSM that includes current state and current values for all FSM variables. +/// Used to check for any changes in state/variables to network with the server. +/// +internal class FsmSnapshot { + /// + /// The name of the current (or last) state of the FSM. + /// + public string CurrentState { get; set; } + + /// + /// Dictionary of names of float variables and corresponding (current/last) value. + /// + public Dictionary Floats { get; } + /// + /// Dictionary of names of int variables and corresponding (current/last) value. + /// + public Dictionary Ints { get; } + /// + /// Dictionary of names of bool variables and corresponding (current/last) value. + /// + public Dictionary Bools { get; } + /// + /// Dictionary of names of string variables and corresponding (current/last) value. + /// + public Dictionary Strings { get; } + /// + /// Dictionary of names of vector2 variables and corresponding (current/last) value. + /// + public Dictionary Vector2s { get; } + /// + /// Dictionary of names of vector3 variables and corresponding (current/last) value. + /// + public Dictionary Vector3s { get; } + + /// + /// Construct the snapshot by initializing all dictionaries. + /// + public FsmSnapshot() { + Floats = new Dictionary(); + Ints = new Dictionary(); + Bools = new Dictionary(); + Strings = new Dictionary(); + Vector2s = new Dictionary(); + Vector3s = new Dictionary(); + } +} diff --git a/HKMP/Game/Server/ServerEntityData.cs b/HKMP/Game/Server/ServerEntityData.cs index 0c8d7d0c..a65d2e56 100644 --- a/HKMP/Game/Server/ServerEntityData.cs +++ b/HKMP/Game/Server/ServerEntityData.cs @@ -49,8 +49,14 @@ internal class ServerEntityData { /// Generic data associated with this entity. /// public List GenericData { get; } + + /// + /// Host FSM data to keep track of for transferring scene host. + /// + public Dictionary HostFsmData { get; } public ServerEntityData() { GenericData = new List(); + HostFsmData = new Dictionary(); } } diff --git a/HKMP/Game/Server/ServerManager.cs b/HKMP/Game/Server/ServerManager.cs index ec99e588..a9a1ef3a 100644 --- a/HKMP/Game/Server/ServerManager.cs +++ b/HKMP/Game/Server/ServerManager.cs @@ -405,6 +405,13 @@ private void OnClientEnterScene(ServerPlayerData playerData) { entityUpdate.UpdateTypes.Add(EntityUpdateType.Scale); entityUpdate.Scale = entityData.Scale.Value; } + + if (entityData.AnimationId.HasValue) { + entityUpdate.UpdateTypes.Add(EntityUpdateType.Animation); + + entityUpdate.AnimationId = entityData.AnimationId.Value; + entityUpdate.AnimationWrapMode = entityData.AnimationWrapMode; + } if (entityData.IsActive.HasValue) { entityUpdate.UpdateTypes.Add(EntityUpdateType.Active); @@ -416,11 +423,12 @@ private void OnClientEnterScene(ServerPlayerData playerData) { entityUpdate.GenericData.AddRange(entityData.GenericData); } - if (entityData.AnimationId.HasValue) { - entityUpdate.UpdateTypes.Add(EntityUpdateType.Animation); + if (entityData.HostFsmData.Count > 0) { + entityUpdate.UpdateTypes.Add(EntityUpdateType.HostFsm); - entityUpdate.AnimationId = entityData.AnimationId.Value; - entityUpdate.AnimationWrapMode = entityData.AnimationWrapMode; + foreach (var pair in entityData.HostFsmData) { + entityUpdate.HostFsmData[pair.Key] = pair.Value; + } } entityUpdateList.Add(entityUpdate); @@ -741,6 +749,32 @@ void ReplaceExistingDataWithSameType(EntityNetworkData.DataType type, Packet dat } } } + + if (entityUpdate.UpdateTypes.Contains(EntityUpdateType.HostFsm)) { + foreach (var pair in entityUpdate.HostFsmData) { + var fsmIndex = pair.Key; + var data = pair.Value; + + if (!entityData.HostFsmData.TryGetValue(fsmIndex, out var existingData)) { + existingData = new EntityHostFsmData(); + entityData.HostFsmData[fsmIndex] = existingData; + } + + existingData.MergeData(data); + + SendDataInSameScene( + id, + playerData.CurrentScene, + otherId => { + _netServer.GetUpdateManagerForClient(otherId)?.AddEntityHostFsmData( + entityUpdate.Id, + fsmIndex, + data + ); + } + ); + } + } } /// @@ -813,10 +847,9 @@ private void HandlePlayerLeaveScene(ushort id, bool disconnected, bool timeout = var updateManager = _netServer.GetUpdateManagerForClient(idPlayerDataPair.Key); - var otherPlayerBecomesSceneHost = false; if (playerData.IsSceneHost) { // If the leaving player was the scene host, we can make this player the new scene host - otherPlayerBecomesSceneHost = true; + updateManager.SetSceneHostTransfer(); // Reset the scene host variable in the leaving player, so only a single other player // becomes the scene host @@ -829,17 +862,9 @@ private void HandlePlayerLeaveScene(ushort id, bool disconnected, bool timeout = } if (disconnected) { - updateManager.AddPlayerDisconnectData( - id, - username, - otherPlayerBecomesSceneHost, - timeout - ); + updateManager.AddPlayerDisconnectData(id, username, timeout); } else { - updateManager.AddPlayerLeaveSceneData( - id, - otherPlayerBecomesSceneHost - ); + updateManager.AddPlayerLeaveSceneData(id); } } } diff --git a/HKMP/HKMP.csproj b/HKMP/HKMP.csproj index e90cde65..cebed9f7 100644 --- a/HKMP/HKMP.csproj +++ b/HKMP/HKMP.csproj @@ -13,7 +13,7 @@ - + @@ -96,10 +96,10 @@ - - + + - + diff --git a/HKMP/Networking/Client/ClientUpdateManager.cs b/HKMP/Networking/Client/ClientUpdateManager.cs index 248b53be..d5618470 100644 --- a/HKMP/Networking/Client/ClientUpdateManager.cs +++ b/HKMP/Networking/Client/ClientUpdateManager.cs @@ -287,6 +287,26 @@ public void AddEntityData(byte entityId, EntityNetworkData data) { } } + /// + /// Add host entity FSM data to the current packet. + /// + /// The ID of the entity. + /// The index of the FSM of the entity. + /// The host FSM data to add. + public void AddEntityHostFsmData(byte entityId, byte fsmIndex, EntityHostFsmData data) { + lock (Lock) { + var entityUpdate = FindOrCreateEntityUpdate(entityId); + + entityUpdate.UpdateTypes.Add(EntityUpdateType.HostFsm); + + if (entityUpdate.HostFsmData.TryGetValue(fsmIndex, out var existingData)) { + existingData.MergeData(data); + } else { + entityUpdate.HostFsmData.Add(fsmIndex, data); + } + } + } + /// /// Set that the player has disconnected in the current packet. /// diff --git a/HKMP/Networking/Packet/Data/ClientPlayerDisconnect.cs b/HKMP/Networking/Packet/Data/ClientPlayerDisconnect.cs index 6c989f34..6efa5eee 100644 --- a/HKMP/Networking/Packet/Data/ClientPlayerDisconnect.cs +++ b/HKMP/Networking/Packet/Data/ClientPlayerDisconnect.cs @@ -9,11 +9,6 @@ internal class ClientPlayerDisconnect : GenericClientData { /// public string Username { get; set; } - /// - /// Whether the player receiving this data becomes the new scene host. - /// - public bool NewSceneHost { get; set; } - /// /// Whether the player timed out or disconnected normally. /// @@ -31,7 +26,6 @@ public ClientPlayerDisconnect() { public override void WriteData(IPacket packet) { packet.Write(Id); packet.Write(Username); - packet.Write(NewSceneHost); packet.Write(TimedOut); } @@ -39,7 +33,6 @@ public override void WriteData(IPacket packet) { public override void ReadData(IPacket packet) { Id = packet.ReadUShort(); Username = packet.ReadString(); - NewSceneHost = packet.ReadBool(); TimedOut = packet.ReadBool(); } } diff --git a/HKMP/Networking/Packet/Data/EntityUpdate.cs b/HKMP/Networking/Packet/Data/EntityUpdate.cs index 9d015370..5df52cdf 100644 --- a/HKMP/Networking/Packet/Data/EntityUpdate.cs +++ b/HKMP/Networking/Packet/Data/EntityUpdate.cs @@ -43,13 +43,15 @@ internal class EntityUpdate : IPacketData { /// The wrap mode of the animation. /// public byte AnimationWrapMode { get; set; } - + /// /// Whether the entity is active or not. /// public bool IsActive { get; set; } public List GenericData { get; } + + public Dictionary HostFsmData { get; } /// /// Construct the entity update data. @@ -57,6 +59,7 @@ internal class EntityUpdate : IPacketData { public EntityUpdate() { UpdateTypes = new HashSet(); GenericData = new List(); + HostFsmData = new Dictionary(); } /// @@ -111,6 +114,17 @@ public void WriteData(IPacket packet) { GenericData[i].WriteData(packet); } } + + if (UpdateTypes.Contains(EntityUpdateType.HostFsm)) { + var length = (byte) HostFsmData.Count; + packet.Write(length); + + foreach (var pair in HostFsmData) { + packet.Write(pair.Key); + + pair.Value.WriteData(packet); + } + } } /// @@ -160,6 +174,19 @@ public void ReadData(IPacket packet) { GenericData.Add(entityNetworkData); } } + + if (UpdateTypes.Contains(EntityUpdateType.HostFsm)) { + var length = packet.ReadByte(); + + for (var i = 0; i < length; i++) { + var key = packet.ReadByte(); + + var data = new EntityHostFsmData(); + data.ReadData(packet); + + HostFsmData.Add(key, data); + } + } } } @@ -180,10 +207,7 @@ public EntityNetworkData() { Packet = new Packet(); } - /// - /// Write the data into the given packet. - /// - /// The packet to write into. + /// public void WriteData(IPacket packet) { packet.Write((byte)Type); @@ -201,10 +225,7 @@ public void WriteData(IPacket packet) { } } - /// - /// Read the data from the given packet. - /// - /// The packet to read from. + /// public void ReadData(IPacket packet) { Type = (DataType) packet.ReadByte(); @@ -229,6 +250,214 @@ public enum DataType : byte { } } +/// +/// Class containing data for host FSMs including state and FSM variables. +/// Used to make host transfer easier since all clients receive updates on host FSM details. +/// +internal class EntityHostFsmData { + /// + /// The types of content that is in this data class. + /// + public HashSet Types { get; } + + /// + /// The index of the current (or last) state of the FSM. + /// + public byte CurrentState { get; set; } + + /// + /// Dictionary containing indices of float variables to their respective values. + /// + public Dictionary Floats { get; } + /// + /// Dictionary containing indices of int variables to their respective values. + /// + public Dictionary Ints { get; } + /// + /// Dictionary containing indices of bool variables to their respective values. + /// + public Dictionary Bools { get; } + /// + /// Dictionary containing indices of string variables to their respective values. + /// + public Dictionary Strings { get; } + /// + /// Dictionary containing indices of vector2 variables to their respective values. + /// + public Dictionary Vec2s { get; } + /// + /// Dictionary containing indices of vector3 variables to their respective values. + /// + public Dictionary Vec3s { get; } + + public EntityHostFsmData() { + Types = new HashSet(); + + Floats = new Dictionary(); + Ints = new Dictionary(); + Bools = new Dictionary(); + Strings = new Dictionary(); + Vec2s = new Dictionary(); + Vec3s = new Dictionary(); + } + + /// + /// Merges the data from the given data class into the current one. + /// + /// The other instance. + public void MergeData(EntityHostFsmData otherData) { + if (otherData.Types.Contains(Type.State)) { + Types.Add(Type.State); + + CurrentState = otherData.CurrentState; + } + + if (otherData.Types.Contains(Type.Floats)) { + Types.Add(Type.Floats); + + foreach (var pair in otherData.Floats) { + Floats[pair.Key] = pair.Value; + } + } + + if (otherData.Types.Contains(Type.Ints)) { + Types.Add(Type.Ints); + + foreach (var pair in otherData.Ints) { + Ints[pair.Key] = pair.Value; + } + } + + if (otherData.Types.Contains(Type.Bools)) { + Types.Add(Type.Bools); + + foreach (var pair in otherData.Bools) { + Bools[pair.Key] = pair.Value; + } + } + + if (otherData.Types.Contains(Type.Strings)) { + Types.Add(Type.Strings); + + foreach (var pair in otherData.Strings) { + Strings[pair.Key] = pair.Value; + } + } + + if (otherData.Types.Contains(Type.Vector2s)) { + Types.Add(Type.Vector2s); + + foreach (var pair in otherData.Vec2s) { + Vec2s[pair.Key] = pair.Value; + } + } + + if (otherData.Types.Contains(Type.Vector3s)) { + Types.Add(Type.Vector3s); + + foreach (var pair in otherData.Vec3s) { + Vec3s[pair.Key] = pair.Value; + } + } + } + + /// + public void WriteData(IPacket packet) { + // Construct the byte flag representing update types + byte updateTypeFlag = 0; + // Keep track of value of current bit + byte currentTypeValue = 1; + + for (var i = 0; i < Enum.GetNames(typeof(Type)).Length; i++) { + // Cast the current index of the loop to a PlayerUpdateType and check if it is + // contained in the update type list, if so, we add the current bit to the flag + if (Types.Contains((Type) i)) { + updateTypeFlag |= currentTypeValue; + } + + currentTypeValue *= 2; + } + + // Write the update type flag + packet.Write(updateTypeFlag); + + if (Types.Contains(Type.State)) { + packet.Write(CurrentState); + } + + void WriteVarDict(Type type, Dictionary dict, Action writeValue) { + if (Types.Contains(type)) { + var length = (byte) dict.Count; + packet.Write(length); + + foreach (var pair in dict) { + packet.Write(pair.Key); + writeValue.Invoke(pair.Value); + } + } + } + + WriteVarDict(Type.Floats, Floats, packet.Write); + WriteVarDict(Type.Ints, Ints, packet.Write); + WriteVarDict(Type.Bools, Bools, packet.Write); + WriteVarDict(Type.Strings, Strings, packet.Write); + WriteVarDict(Type.Vector2s, Vec2s, packet.Write); + WriteVarDict(Type.Vector3s, Vec3s, packet.Write); + } + + /// + public void ReadData(IPacket packet) { + // Read the byte flag representing update types and reconstruct it + var updateTypeFlag = packet.ReadByte(); + // Keep track of value of current bit + var currentTypeValue = 1; + + for (var i = 0; i < Enum.GetNames(typeof(Type)).Length; i++) { + // If this bit was set in our flag, we add the type to the list + if ((updateTypeFlag & currentTypeValue) != 0) { + Types.Add((Type) i); + } + + // Increase the value of current bit + currentTypeValue *= 2; + } + + if (Types.Contains(Type.State)) { + CurrentState = packet.ReadByte(); + } + + void ReadVarDict(Type type, Dictionary dict, Func readValue) { + if (Types.Contains(type)) { + var length = packet.ReadByte(); + + for (var i = 0; i < length; i++) { + dict.Add(packet.ReadByte(), readValue.Invoke()); + } + } + } + + ReadVarDict(Type.Floats, Floats, packet.ReadFloat); + ReadVarDict(Type.Ints, Ints, packet.ReadInt); + ReadVarDict(Type.Bools, Bools, packet.ReadBool); + ReadVarDict(Type.Strings, Strings, packet.ReadString); + ReadVarDict(Type.Vector2s, Vec2s, packet.ReadVector2); + ReadVarDict(Type.Vector3s, Vec3s, packet.ReadVector3); + } + + /// + /// Enum for update types of this class. + /// + public enum Type : byte { + State, + Floats, + Ints, + Bools, + Strings, + Vector2s, + Vector3s + } +} + /// /// Enumeration of entity update types. /// @@ -237,5 +466,6 @@ internal enum EntityUpdateType { Scale, Animation, Active, - Data + Data, + HostFsm } diff --git a/HKMP/Networking/Packet/Data/PlayerLeaveScene.cs b/HKMP/Networking/Packet/Data/PlayerLeaveScene.cs index e5943ec0..6f183ccc 100644 --- a/HKMP/Networking/Packet/Data/PlayerLeaveScene.cs +++ b/HKMP/Networking/Packet/Data/PlayerLeaveScene.cs @@ -1,23 +1,7 @@ -namespace Hkmp.Networking.Packet.Data { - /// - /// Packet data for the client-bound player leave scene data. - /// - internal class ClientPlayerLeaveScene : GenericClientData { - /// - /// Whether the player receiving this data becomes the new scene host. - /// - public bool NewSceneHost { get; set; } +namespace Hkmp.Networking.Packet.Data; - /// - public override void WriteData(IPacket packet) { - packet.Write(Id); - packet.Write(NewSceneHost); - } - - /// - public override void ReadData(IPacket packet) { - Id = packet.ReadUShort(); - NewSceneHost = packet.ReadBool(); - } - } -} \ No newline at end of file +/// +/// Packet data for the client-bound player leave scene data. +/// +internal class ClientPlayerLeaveScene : GenericClientData { +} diff --git a/HKMP/Networking/Packet/IPacket.cs b/HKMP/Networking/Packet/IPacket.cs index b909ed3a..fd97b717 100644 --- a/HKMP/Networking/Packet/IPacket.cs +++ b/HKMP/Networking/Packet/IPacket.cs @@ -96,6 +96,12 @@ public interface IPacket { /// /// The Vector2 value. void Write(Vector2 value); + + /// + /// Write a Vector3 (12 bytes) to the packet. Simply a wrapper for writing the X, Y and Z floats to the packet. + /// + /// The Vector3 value. + void Write(Vector3 value); #endregion @@ -186,6 +192,12 @@ public interface IPacket { /// /// The Vector2 value. Vector2 ReadVector2(); + + /// + /// Read a Vector3 (12 bytes) from the packet. Simply a wrapper for reading the X, Y and Z floats from the packet. + /// + /// The Vector3 value. + Vector3 ReadVector3(); #endregion } diff --git a/HKMP/Networking/Packet/Packet.cs b/HKMP/Networking/Packet/Packet.cs index f6949fdb..81fd1424 100644 --- a/HKMP/Networking/Packet/Packet.cs +++ b/HKMP/Networking/Packet/Packet.cs @@ -190,6 +190,13 @@ public void Write(Vector2 value) { Write(value.X); Write(value.Y); } + + /// + public void Write(Vector3 value) { + Write(value.X); + Write(value.Y); + Write(value.Z); + } #endregion @@ -399,6 +406,13 @@ public Vector2 ReadVector2() { // check whether there are enough bytes left to read and throw exceptions if not return new Vector2(ReadFloat(), ReadFloat()); } + + /// + public Vector3 ReadVector3() { + // Simply construct the Vector3 by reading a float from the packet thrice, which should + // check whether there are enough bytes left to read and throw exceptions if not + return new Vector3(ReadFloat(), ReadFloat(), ReadFloat()); + } #endregion diff --git a/HKMP/Networking/Packet/PacketId.cs b/HKMP/Networking/Packet/PacketId.cs index 7ac29c98..91015f5f 100644 --- a/HKMP/Networking/Packet/PacketId.cs +++ b/HKMP/Networking/Packet/PacketId.cs @@ -64,6 +64,11 @@ internal enum ClientPacketId { /// EntityUpdate, + /// + /// Notify that the player becomes scene host of their current scene. + /// + SceneHostTransfer, + /// /// Notify that a player has died. /// @@ -87,7 +92,7 @@ internal enum ClientPacketId { /// /// Player sent chat message. /// - ChatMessage = 16 + ChatMessage = 17 } /// diff --git a/HKMP/Networking/Packet/UpdatePacket.cs b/HKMP/Networking/Packet/UpdatePacket.cs index e290f604..e1b561f3 100644 --- a/HKMP/Networking/Packet/UpdatePacket.cs +++ b/HKMP/Networking/Packet/UpdatePacket.cs @@ -920,6 +920,8 @@ protected override IPacketData InstantiatePacketDataFromId(ClientPacketId packet return new PacketDataCollection(); case ClientPacketId.EntityUpdate: return new PacketDataCollection(); + case ClientPacketId.SceneHostTransfer: + return new ReliableEmptyData(); case ClientPacketId.PlayerDeath: return new PacketDataCollection(); case ClientPacketId.PlayerTeamUpdate: diff --git a/HKMP/Networking/Server/ServerUpdateManager.cs b/HKMP/Networking/Server/ServerUpdateManager.cs index 6e1828dd..ed7ea306 100644 --- a/HKMP/Networking/Server/ServerUpdateManager.cs +++ b/HKMP/Networking/Server/ServerUpdateManager.cs @@ -122,9 +122,8 @@ public void AddPlayerConnectData(ushort id, string username) { /// /// The ID of the player disconnecting. /// The username of the player disconnecting. - /// Whether the player this is sent to becomes the new scene host. /// Whether the player timed out or disconnected normally. - public void AddPlayerDisconnectData(ushort id, string username, bool newSceneHost, bool timedOut = false) { + public void AddPlayerDisconnectData(ushort id, string username, bool timedOut = false) { lock (Lock) { var playerDisconnect = FindOrCreatePacketData(id, ClientPacketId.PlayerDisconnect); @@ -194,13 +193,11 @@ bool sceneHost /// /// Add player leave scene data to the current packet. /// - /// The ID of the player. - /// Whether the player receiving this packet becomes the new scene host. - public void AddPlayerLeaveSceneData(ushort id, bool newSceneHost) { + /// The ID of the leaving player. + public void AddPlayerLeaveSceneData(ushort id) { lock (Lock) { var playerLeaveScene = FindOrCreatePacketData(id, ClientPacketId.PlayerLeaveScene); playerLeaveScene.Id = id; - playerLeaveScene.NewSceneHost = newSceneHost; } } @@ -415,6 +412,35 @@ public void AddEntityData(byte entityId, List data) { entityUpdate.GenericData.AddRange(data); } } + + /// + /// Add host entity FSM data to the current packet. + /// + /// The ID of the entity. + /// The index of the FSM of the entity. + /// The host FSM data to add. + public void AddEntityHostFsmData(byte entityId, byte fsmIndex, EntityHostFsmData data) { + lock (Lock) { + var entityUpdate = FindOrCreateEntityUpdate(entityId); + + entityUpdate.UpdateTypes.Add(EntityUpdateType.HostFsm); + + if (entityUpdate.HostFsmData.TryGetValue(fsmIndex, out var existingData)) { + existingData.MergeData(data); + } else { + entityUpdate.HostFsmData.Add(fsmIndex, data); + } + } + } + + /// + /// Set that the receiving player should become scene host of their current scene. + /// + public void SetSceneHostTransfer() { + lock (Lock) { + CurrentUpdatePacket.SetSendingPacketData(ClientPacketId.SceneHostTransfer, new ReliableEmptyData()); + } + } /// /// Add player death data to the current packet. @@ -475,6 +501,7 @@ public void UpdateServerSettings(ServerSettings serverSettings) { /// /// Set that the client is disconnected from the server with the given reason. /// + /// The reason for the disconnect. public void SetDisconnect(DisconnectReason reason) { lock (Lock) { CurrentUpdatePacket.SetSendingPacketData( From 7879e1922af2b6737666c6d07fc2bcb38fdd96e5 Mon Sep 17 00:00:00 2001 From: Extremelyd1 Date: Sun, 21 May 2023 19:50:46 +0200 Subject: [PATCH 029/216] Finish scene host transfer --- .../Client/Entity/Action/EntityFsmActions.cs | 19 ++-- HKMP/Game/Client/Entity/Entity.cs | 94 ++++++++++++++----- HKMP/Game/Client/Entity/EntityInitializer.cs | 2 +- HKMP/Game/Client/Entity/EntityManager.cs | 11 ++- 4 files changed, 91 insertions(+), 35 deletions(-) diff --git a/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs b/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs index 5ce69d45..8a5bdd7e 100644 --- a/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs +++ b/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs @@ -225,11 +225,6 @@ private static void ApplyNetworkDataFromAction(EntityNetworkData data, SpawnObje if (action.gameObject != null) { var spawnedObject = action.gameObject.Value.Spawn(position, Quaternion.Euler(euler)); action.storeObject.Value = spawnedObject; - - // TODO: this might give an issue if the packets for two of these actions get out of order and the IDs - // of the spawned entities get switched. This only holds in the case where two different entities are - // spawned - EntitySpawnEvent?.Invoke(action, spawnedObject); } } @@ -401,19 +396,29 @@ private static bool GetNetworkDataFromAction(EntityNetworkData data, SetParticle } private static void ApplyNetworkDataFromAction(EntityNetworkData data, SetParticleEmission action) { - if (action.emission == null) { + Logger.Debug($"Apply SetParticleEmission"); + + if (action == null) { + Logger.Debug(" Action is null"); return; } + + if (action.emission == null) { + Logger.Debug(" Action emission is null"); + } var gameObject = action.Fsm.GetOwnerDefaultTarget(action.gameObject); if (gameObject == null) { + Logger.Debug(" OwnerDefaultTarget is null"); return; } var particleSystem = gameObject.GetComponent(); if (particleSystem == null) { - return; + Logger.Debug(" Particle system is null"); } + + Logger.Debug($" Emission: {action.emission.Value}"); #pragma warning disable CS0618 particleSystem.enableEmission = action.emission.Value; diff --git a/HKMP/Game/Client/Entity/Entity.cs b/HKMP/Game/Client/Entity/Entity.cs index f2741ced..8b1110d1 100644 --- a/HKMP/Game/Client/Entity/Entity.cs +++ b/HKMP/Game/Client/Entity/Entity.cs @@ -614,10 +614,56 @@ public void InitializeHost() { /// Makes the entity a host entity if the client user became the scene host. /// public void MakeHost() { - // TODO: disable client object/FSMs, enable host object/FSMs, set current state from snapshots in all FSMs - // TODO: copy position, scale, animation, etc. from client to host (perhaps before disabling/enabling client/host) + Logger.Info($"Making entity ({_entityId}, {Type}) a host entity"); + + var clientPos = Object.Client.transform.position; + Object.Host.transform.position = clientPos; + + var clientScale = Object.Client.transform.localScale; + Object.Host.transform.localScale = clientScale; + + var clientActive = Object.Client.activeSelf; + Object.Client.SetActive(false); + Object.Host.SetActive(clientActive); + + _lastIsActive = Object.Host.activeInHierarchy; - InitializeHost(); + _isControlled = false; + + foreach (var component in _components.Values) { + component.IsControlled = false; + } + + for (var fsmIndex = 0; fsmIndex < _fsms.Host.Count; fsmIndex++) { + var fsm = _fsms.Host[fsmIndex]; + + // Force initialize the host FSM, since it might have been disabled before initializing + EntityInitializer.InitializeFsm(fsm); + + var snapshot = _fsmSnapshots[fsmIndex]; + + foreach (var pair in snapshot.Floats) { + fsm.FsmVariables.GetFsmFloat(pair.Key).Value = pair.Value; + } + foreach (var pair in snapshot.Ints) { + fsm.FsmVariables.GetFsmInt(pair.Key).Value = pair.Value; + } + foreach (var pair in snapshot.Bools) { + fsm.FsmVariables.GetFsmBool(pair.Key).Value = pair.Value; + } + foreach (var pair in snapshot.Strings) { + fsm.FsmVariables.GetFsmString(pair.Key).Value = pair.Value; + } + foreach (var pair in snapshot.Vector2s) { + fsm.FsmVariables.GetFsmVector2(pair.Key).Value = pair.Value; + } + foreach (var pair in snapshot.Vector3s) { + fsm.FsmVariables.GetFsmVector3(pair.Key).Value = pair.Value; + } + + // Set the state as the very last thing in the transfer to kickstart the FSM + fsm.SetState(snapshot.CurrentState); + } } /// @@ -800,70 +846,70 @@ public void UpdateHostFsmData(Dictionary hostFsmData) { } } - void CondUpdateVars( + void CondUpdateVars( EntityHostFsmData.Type type, Dictionary dataDict, FsmType[] fsmVarArray, - Action setValueAction + Action 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}"); } else { - setValueAction.Invoke(fsmVarArray[pair.Key], (UnityType) (object) pair.Value); + setValueAction.Invoke(fsmVarArray[pair.Key], pair.Value); } } } } - CondUpdateVars( + CondUpdateVars( EntityHostFsmData.Type.Floats, data.Floats, fsm.FsmVariables.FloatVariables, (fsmVar, value) => { - fsmVar.Value = value; - snapshot.Floats[fsmVar.Name] = value; + fsmVar.Value = (float) value; + snapshot.Floats[fsmVar.Name] = (float) value; }); - CondUpdateVars( + CondUpdateVars( EntityHostFsmData.Type.Ints, data.Ints, fsm.FsmVariables.IntVariables, (fsmVar, value) => { - fsmVar.Value = value; - snapshot.Ints[fsmVar.Name] = value; + fsmVar.Value = (int) value; + snapshot.Ints[fsmVar.Name] = (int) value; }); - CondUpdateVars( + CondUpdateVars( EntityHostFsmData.Type.Bools, data.Bools, fsm.FsmVariables.BoolVariables, (fsmVar, value) => { - fsmVar.Value = value; - snapshot.Bools[fsmVar.Name] = value; + fsmVar.Value = (bool) value; + snapshot.Bools[fsmVar.Name] = (bool) value; }); - CondUpdateVars( + CondUpdateVars( EntityHostFsmData.Type.Strings, data.Strings, fsm.FsmVariables.StringVariables, (fsmVar, value) => { - fsmVar.Value = value; - snapshot.Strings[fsmVar.Name] = value; + fsmVar.Value = (string) value; + snapshot.Strings[fsmVar.Name] = (string) value; }); - CondUpdateVars( + CondUpdateVars( EntityHostFsmData.Type.Vector2s, data.Vec2s, fsm.FsmVariables.Vector2Variables, (fsmVar, value) => { - fsmVar.Value = value; - snapshot.Vector2s[fsmVar.Name] = value; + fsmVar.Value = (UnityEngine.Vector2) (Vector2) value; + snapshot.Vector2s[fsmVar.Name] = (UnityEngine.Vector2) (Vector2) value; }); - CondUpdateVars( + CondUpdateVars( EntityHostFsmData.Type.Vector3s, data.Vec3s, fsm.FsmVariables.Vector3Variables, (fsmVar, value) => { - fsmVar.Value = value; - snapshot.Vector3s[fsmVar.Name] = value; + fsmVar.Value = (Vector3) (Hkmp.Math.Vector3) value; + snapshot.Vector3s[fsmVar.Name] = (Vector3) (Hkmp.Math.Vector3) value; }); } } diff --git a/HKMP/Game/Client/Entity/EntityInitializer.cs b/HKMP/Game/Client/Entity/EntityInitializer.cs index acee6394..6f697709 100644 --- a/HKMP/Game/Client/Entity/EntityInitializer.cs +++ b/HKMP/Game/Client/Entity/EntityInitializer.cs @@ -23,7 +23,7 @@ internal static class EntityInitializer { /// Initialize the FSM of a client entity by finding initialize states and executing the actions in those states. /// /// The FSM to initialize. - public static void InitializeClientFsm(PlayMakerFSM fsm) { + public static void InitializeFsm(PlayMakerFSM fsm) { // Check for all states whether they are initialize states foreach (var state in fsm.FsmStates) { if (!InitStateNames.Contains(state.Name.ToLower())) { diff --git a/HKMP/Game/Client/Entity/EntityManager.cs b/HKMP/Game/Client/Entity/EntityManager.cs index 9b48ab6e..d2edaab0 100644 --- a/HKMP/Game/Client/Entity/EntityManager.cs +++ b/HKMP/Game/Client/Entity/EntityManager.cs @@ -114,9 +114,10 @@ public void SpawnEntity(byte id, EntityType spawningType, EntityType spawnedType var gameObject = EntitySpawner.SpawnEntityGameObject(spawningType, spawnedType, clientFsms); + // Make sure to initialize all entities that should be in the system foreach (var fsm in gameObject.GetComponents()) { - if (!EntityRegistry.TryGetEntry(gameObject.name, fsm.Fsm.Name, out _)) { - EntityInitializer.InitializeClientFsm(fsm); + if (EntityRegistry.TryGetEntry(gameObject.name, fsm.Fsm.Name, out _)) { + EntityInitializer.InitializeFsm(fsm); } } @@ -272,7 +273,7 @@ private void OnGameObjectSpawned(FsmStateAction action, GameObject gameObject) { // all the FSM on the client foreach (var clientFsm in entity.GetClientFsms()) { Logger.Info($"Manually initializing client entity FSM: {clientFsm.Fsm.Name}, {clientFsm.gameObject.name}"); - EntityInitializer.InitializeClientFsm(clientFsm); + EntityInitializer.InitializeFsm(clientFsm); } // We also need to update the 'active' state of the entity since it was spawned @@ -351,6 +352,10 @@ private void FindEntitiesInScene(Scene scene, bool lateLoad) { // Find all Climber components foreach (var climber in Object.FindObjectsOfType()) { + if (climber.gameObject.scene != scene) { + continue; + } + if (!EntityRegistry.TryGetEntry(climber.gameObject.name, EntityType.Tiktik, out var entry)) { continue; } From 97150094b0710b68b83149a44a8b168bbe1cad91 Mon Sep 17 00:00:00 2001 From: Extremelyd1 Date: Mon, 22 May 2023 22:21:11 +0200 Subject: [PATCH 030/216] Fix issue with null client entity --- .../Client/Entity/Action/EntityFsmActions.cs | 14 ++---------- HKMP/Game/Client/Entity/Entity.cs | 22 ++++++++++++++++--- 2 files changed, 21 insertions(+), 15 deletions(-) diff --git a/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs b/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs index 8a5bdd7e..22929e49 100644 --- a/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs +++ b/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs @@ -396,30 +396,20 @@ private static bool GetNetworkDataFromAction(EntityNetworkData data, SetParticle } private static void ApplyNetworkDataFromAction(EntityNetworkData data, SetParticleEmission action) { - Logger.Debug($"Apply SetParticleEmission"); - - if (action == null) { - Logger.Debug(" Action is null"); + if (action?.emission == null) { return; } - - if (action.emission == null) { - Logger.Debug(" Action emission is null"); - } var gameObject = action.Fsm.GetOwnerDefaultTarget(action.gameObject); if (gameObject == null) { - Logger.Debug(" OwnerDefaultTarget is null"); return; } var particleSystem = gameObject.GetComponent(); if (particleSystem == null) { - Logger.Debug(" Particle system is null"); + return; } - Logger.Debug($" Emission: {action.emission.Value}"); - #pragma warning disable CS0618 particleSystem.enableEmission = action.emission.Value; #pragma warning restore CS0618 diff --git a/HKMP/Game/Client/Entity/Entity.cs b/HKMP/Game/Client/Entity/Entity.cs index 8b1110d1..688d77b1 100644 --- a/HKMP/Game/Client/Entity/Entity.cs +++ b/HKMP/Game/Client/Entity/Entity.cs @@ -100,7 +100,7 @@ internal class Entity { /// List of snapshots for each FSM of a host entity that contain latest values for state and FSM variables. /// Used to check whether state/variables change and to update the server accordingly. /// - private List _fsmSnapshots; + private readonly List _fsmSnapshots; public Entity( NetClient netClient, @@ -616,6 +616,21 @@ public void InitializeHost() { public void MakeHost() { Logger.Info($"Making entity ({_entityId}, {Type}) a host entity"); + // If the client object is null, we don't have to care about doing anything for the host object anymore + if (Object.Client == null) { + if (Object.Host != null) { + Object.Host.SetActive(false); + } + + _isControlled = false; + + foreach (var component in _components.Values) { + component.IsControlled = false; + } + + return; + } + var clientPos = Object.Client.transform.position; Object.Host.transform.position = clientPos; @@ -660,8 +675,9 @@ public void MakeHost() { foreach (var pair in snapshot.Vector3s) { fsm.FsmVariables.GetFsmVector3(pair.Key).Value = pair.Value; } - - // Set the state as the very last thing in the transfer to kickstart the FSM + + // Re-init the FSM and set the state as the very last thing in the transfer to kickstart the FSM + fsm.Fsm.Reinitialize(); fsm.SetState(snapshot.CurrentState); } } From d21f8faf3db0618edc844ee24656c0c6725d7917 Mon Sep 17 00:00:00 2001 From: Extremelyd1 Date: Wed, 24 May 2023 19:16:40 +0200 Subject: [PATCH 031/216] Add a bunch of FSM actions, add DamageHero component --- .../Client/Entity/Action/EntityFsmActions.cs | 290 +++++++++++++++++- .../Entity/Component/DamageHeroComponent.cs | 72 +++++ .../Component/HealthManagerComponent.cs | 73 ++++- HKMP/Game/Client/Entity/Entity.cs | 25 +- HKMP/Game/Client/Entity/EntityInitializer.cs | 3 +- HKMP/Game/Server/ServerManager.cs | 3 +- HKMP/Networking/Packet/Data/EntityUpdate.cs | 6 +- 7 files changed, 450 insertions(+), 22 deletions(-) create mode 100644 HKMP/Game/Client/Entity/Component/DamageHeroComponent.cs diff --git a/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs b/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs index 22929e49..c7bf92d5 100644 --- a/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs +++ b/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs @@ -320,14 +320,14 @@ private static void ApplyNetworkDataFromAction(EntityNetworkData data, SetScale #region SetFsmBool private static bool GetNetworkDataFromAction(EntityNetworkData data, SetFsmBool action) { - // TODO: if action.setValue can be a reference, make sure to network it + var setValue = action.setValue.Value; + data.Packet.Write(setValue); + return true; } private static void ApplyNetworkDataFromAction(EntityNetworkData data, SetFsmBool action) { - if (action.setValue == null) { - return; - } + var setValue = data.Packet.ReadBool(); var gameObject = action.Fsm.GetOwnerDefaultTarget(action.gameObject); if (gameObject == action.Fsm.GameObject) { @@ -348,7 +348,7 @@ private static void ApplyNetworkDataFromAction(EntityNetworkData data, SetFsmBoo return; } - fsmBool.Value = action.setValue.Value; + fsmBool.Value = setValue; } #endregion @@ -620,4 +620,284 @@ private static void ApplyNetworkDataFromAction(EntityNetworkData data, SetVeloci } #endregion + + #region SetMeshRenderer + + private static bool GetNetworkDataFromAction(EntityNetworkData data, SetMeshRenderer action) { + return true; + } + + private static void ApplyNetworkDataFromAction(EntityNetworkData data, SetMeshRenderer action) { + var gameObject = action.Fsm.GetOwnerDefaultTarget(action.gameObject); + if (gameObject == null) { + return; + } + + var meshRenderer = gameObject.GetComponent(); + if (meshRenderer == null) { + return; + } + + meshRenderer.enabled = action.active.Value; + } + + #endregion + + #region SetPosition + + private static bool GetNetworkDataFromAction(EntityNetworkData data, SetPosition action) { + var gameObject = action.Fsm.GetOwnerDefaultTarget(action.gameObject); + if (gameObject == null) { + return false; + } + + if (IsObjectInRegistry(gameObject)) { + Logger.Debug("Tried getting SetPosition network data, but entity is in registry"); + return false; + } + + // TODO: this action is used for initialization currently and uses static values + // If we come across this action and it uses references, we need to not only uncomment the below, but + // also think about how to use static values for entity initialization and the dynamic values for + // non-initialization purposes + + // Vector3 vector3; + // if (!action.vector.IsNone) { + // vector3 = action.vector.Value; + // } else { + // if (action.space == Space.World) { + // vector3 = gameObject.transform.position; + // } else { + // vector3 = gameObject.transform.localPosition; + // } + // } + // + // if (!action.x.IsNone) { + // vector3.x = action.x.Value; + // } + // + // if (!action.y.IsNone) { + // vector3.y = action.y.Value; + // } + // + // if (!action.z.IsNone) { + // vector3.z = action.z.Value; + // } + // + // data.Packet.Write(vector3.x); + // data.Packet.Write(vector3.y); + // data.Packet.Write(vector3.z); + + return true; + } + + private static void ApplyNetworkDataFromAction(EntityNetworkData data, SetPosition action) { + // See comment in "Get" method above + + // var vector = new Vector3( + // data.Packet.ReadFloat(), + // data.Packet.ReadFloat() + // ); + + var gameObject = action.Fsm.GetOwnerDefaultTarget(action.gameObject); + if (gameObject == null) { + return; + } + + Vector3 vector3; + if (!action.vector.IsNone) { + vector3 = action.vector.Value; + } else { + if (action.space == Space.World) { + vector3 = gameObject.transform.position; + } else { + vector3 = gameObject.transform.localPosition; + } + } + + if (!action.x.IsNone) { + vector3.x = action.x.Value; + } + + if (!action.y.IsNone) { + vector3.y = action.y.Value; + } + + if (!action.z.IsNone) { + vector3.z = action.z.Value; + } + + if (action.space == Space.World) { + gameObject.transform.position = vector3; + } else { + gameObject.transform.localPosition = vector3; + } + } + + #endregion + + #region ActivateGameObject + + private static bool GetNetworkDataFromAction(EntityNetworkData data, ActivateGameObject action) { + var gameObject = action.Fsm.GetOwnerDefaultTarget(action.gameObject); + if (gameObject == null) { + return false; + } + + if (IsObjectInRegistry(gameObject)) { + Logger.Debug("Tried getting ActivateGameObject network data, but entity is in registry"); + return false; + } + + return true; + } + + private static void ApplyNetworkDataFromAction(EntityNetworkData data, ActivateGameObject action) { + var gameObject = action.Fsm.GetOwnerDefaultTarget(action.gameObject); + if (gameObject == null) { + return; + } + + void SetActiveRecursively(GameObject go, bool state) { + go.SetActive(state); + + foreach (UnityEngine.Component component in go.transform) { + SetActiveRecursively(component.gameObject, state); + } + } + + if (action.recursive.Value) { + SetActiveRecursively(gameObject, action.activate.Value); + } else { + gameObject.SetActive(action.activate.Value); + } + } + + #endregion + + #region Tk2dPlayAnimation + + private static bool GetNetworkDataFromAction(EntityNetworkData data, Tk2dPlayAnimation action) { + var gameObject = action.Fsm.GetOwnerDefaultTarget(action.gameObject); + if (gameObject == null) { + return false; + } + + if (IsObjectInRegistry(gameObject)) { + Logger.Debug("Tried getting Tk2dPlayAnimation network data, but entity is in registry"); + return false; + } + + return true; + } + + private static void ApplyNetworkDataFromAction(EntityNetworkData data, Tk2dPlayAnimation action) { + var gameObject = action.Fsm.GetOwnerDefaultTarget(action.gameObject); + if (gameObject == null) { + return; + } + + var animator = gameObject.GetComponent(); + if (animator == null) { + return; + } + + animator.Play(action.clipName.Value); + } + + #endregion + + #region Tk2dPlayFrame + + private static bool GetNetworkDataFromAction(EntityNetworkData data, Tk2dPlayFrame action) { + var gameObject = action.Fsm.GetOwnerDefaultTarget(action.gameObject); + if (gameObject == null) { + return false; + } + + if (IsObjectInRegistry(gameObject)) { + Logger.Debug("Tried getting Tk2dPlayFrame network data, but entity is in registry"); + return false; + } + + return true; + } + + private static void ApplyNetworkDataFromAction(EntityNetworkData data, Tk2dPlayFrame action) { + var gameObject = action.Fsm.GetOwnerDefaultTarget(action.gameObject); + if (gameObject == null) { + return; + } + + var animator = gameObject.GetComponent(); + if (animator == null) { + return; + } + + animator.PlayFromFrame(action.frame.Value); + } + + #endregion + + #region Tk2dPlayAnimationWithEvents + + private static bool GetNetworkDataFromAction(EntityNetworkData data, Tk2dPlayAnimationWithEvents action) { + var gameObject = action.Fsm.GetOwnerDefaultTarget(action.gameObject); + if (gameObject == null) { + return false; + } + + if (IsObjectInRegistry(gameObject)) { + Logger.Debug("Tried getting Tk2dPlayAnimationWithEvents network data, but entity is in registry"); + return false; + } + + return true; + } + + private static void ApplyNetworkDataFromAction(EntityNetworkData data, Tk2dPlayAnimationWithEvents action) { + var gameObject = action.Fsm.GetOwnerDefaultTarget(action.gameObject); + if (gameObject == null) { + return; + } + + var animator = gameObject.GetComponent(); + if (animator == null) { + return; + } + + animator.Play(action.clipName.Value); + } + + #endregion + + #region SpawnBlood + + private static bool GetNetworkDataFromAction(EntityNetworkData data, SpawnBlood action) { + if (GlobalPrefabDefaults.Instance == null) { + return false; + } + + return true; + } + + private static void ApplyNetworkDataFromAction(EntityNetworkData data, SpawnBlood action) { + var position = action.position.Value; + if (action.spawnPoint.Value != null) { + position += action.spawnPoint.Value.transform.position; + } + + GlobalPrefabDefaults.Instance.SpawnBlood( + position, + (short) action.spawnMin.Value, + (short) action.spawnMax.Value, + action.speedMin.Value, + action.speedMax.Value, + action.angleMin.Value, + action.angleMax.Value, + action.colorOverride.IsNone ? new Color?() : action.colorOverride.Value + ); + } + + #endregion } diff --git a/HKMP/Game/Client/Entity/Component/DamageHeroComponent.cs b/HKMP/Game/Client/Entity/Component/DamageHeroComponent.cs new file mode 100644 index 00000000..21f69231 --- /dev/null +++ b/HKMP/Game/Client/Entity/Component/DamageHeroComponent.cs @@ -0,0 +1,72 @@ +using Hkmp.Networking.Client; +using Hkmp.Networking.Packet.Data; +using Hkmp.Util; +using UnityEngine; + +namespace Hkmp.Game.Client.Entity.Component; + +/// +/// This component manages the rotation of the entity. +internal class DamageHeroComponent : EntityComponent { + /// + /// The host-client pair of unity components of the entity. + /// + private readonly HostClientPair _damageHero; + + /// + /// The last value of damage dealt for the damage hero component. + /// + private int _lastDamageDealt; + + public DamageHeroComponent( + NetClient netClient, + byte entityId, + HostClientPair gameObject, + HostClientPair damageHero + ) : base(netClient, entityId, gameObject) { + _damageHero = damageHero; + _lastDamageDealt = damageHero.Host.damageDealt; + + MonoBehaviourUtil.Instance.OnUpdateEvent += OnUpdate; + } + + /// + /// Callback method to check for damage hero updates. + /// + private void OnUpdate() { + if (IsControlled) { + return; + } + + if (GameObject.Host == null) { + return; + } + + var newDamageDealt = _damageHero.Host.damageDealt; + if (newDamageDealt != _lastDamageDealt) { + _lastDamageDealt = newDamageDealt; + + var data = new EntityNetworkData { + Type = EntityNetworkData.DataType.DamageHero + }; + data.Packet.Write((byte) newDamageDealt); + + SendData(data); + } + } + + /// + public override void InitializeHost() { + } + + /// + public override void Update(EntityNetworkData data) { + var damageDealt = data.Packet.ReadByte(); + _damageHero.Client.damageDealt = damageDealt; + } + + /// + public override void Destroy() { + MonoBehaviourUtil.Instance.OnUpdateEvent -= OnUpdate; + } +} \ No newline at end of file diff --git a/HKMP/Game/Client/Entity/Component/HealthManagerComponent.cs b/HKMP/Game/Client/Entity/Component/HealthManagerComponent.cs index 471cde92..25c88a2e 100644 --- a/HKMP/Game/Client/Entity/Component/HealthManagerComponent.cs +++ b/HKMP/Game/Client/Entity/Component/HealthManagerComponent.cs @@ -1,5 +1,6 @@ using Hkmp.Networking.Client; using Hkmp.Networking.Packet.Data; +using Hkmp.Util; using UnityEngine; using Logger = Hkmp.Logging.Logger; @@ -21,6 +22,16 @@ internal class HealthManagerComponent : EntityComponent { /// private bool _allowDeath; + /// + /// The last value for the "invincible" variable of the health manager. + /// + private bool _lastInvincible; + + /// + /// The last value for the "invincibleFromDirection" variable of the health manager. + /// + private int _lastInvincibleFromDirection; + public HealthManagerComponent( NetClient netClient, byte entityId, @@ -29,7 +40,11 @@ HostClientPair healthManager ) : base(netClient, entityId, gameObject) { _healthManager = healthManager; + _lastInvincible = healthManager.Host.IsInvincible; + _lastInvincibleFromDirection = healthManager.Host.InvincibleFromDirection; + On.HealthManager.Die += HealthManagerOnDie; + MonoBehaviourUtil.Instance.OnUpdateEvent += OnUpdate; } /// @@ -71,7 +86,7 @@ bool ignoreEvasion orig(self, attackDirection, attackType, ignoreEvasion); var data = new EntityNetworkData { - Type = EntityNetworkData.DataType.HealthManager + Type = EntityNetworkData.DataType.Death }; if (attackDirection.HasValue) { @@ -88,6 +103,35 @@ bool ignoreEvasion SendData(data); } + /// + /// Callback method for updates to check whether invincibility changes. + /// + private void OnUpdate() { + var data = new EntityNetworkData { + Type = EntityNetworkData.DataType.Invincibility + }; + + var shouldSend = false; + + var newInvincible = _healthManager.Host.IsInvincible; + if (newInvincible != _lastInvincible) { + _lastInvincible = newInvincible; + shouldSend = true; + } + data.Packet.Write(newInvincible); + + var newInvincibleFromDir = _healthManager.Host.InvincibleFromDirection; + if (newInvincibleFromDir != _lastInvincibleFromDirection) { + _lastInvincibleFromDirection = newInvincibleFromDir; + shouldSend = true; + } + data.Packet.Write((byte) newInvincibleFromDir); + + if (shouldSend) { + SendData(data); + } + } + /// public override void InitializeHost() { } @@ -101,21 +145,30 @@ public override void Update(EntityNetworkData data) { return; } - var attackDirection = new float?(); - if (data.Packet.ReadBool()) { - attackDirection = data.Packet.ReadFloat(); - } + if (data.Type == EntityNetworkData.DataType.Death) { + var attackDirection = new float?(); + if (data.Packet.ReadBool()) { + attackDirection = data.Packet.ReadFloat(); + } - var attackType = (AttackTypes)data.Packet.ReadByte(); - var ignoreEvasion = data.Packet.ReadBool(); + var attackType = (AttackTypes) data.Packet.ReadByte(); + var ignoreEvasion = data.Packet.ReadBool(); - // Set a boolean to indicate that the client health manager is allowed to execute the Die method - _allowDeath = true; - _healthManager.Client.Die(attackDirection, attackType, ignoreEvasion); + // Set a boolean to indicate that the client health manager is allowed to execute the Die method + _allowDeath = true; + _healthManager.Client.Die(attackDirection, attackType, ignoreEvasion); + } else if (data.Type == EntityNetworkData.DataType.Invincibility) { + var newInvincible = data.Packet.ReadBool(); + var newInvincibleFromDir = data.Packet.ReadByte(); + + _healthManager.Client.IsInvincible = newInvincible; + _healthManager.Client.InvincibleFromDirection = newInvincibleFromDir; + } } /// public override void Destroy() { On.HealthManager.Die -= HealthManagerOnDie; + MonoBehaviourUtil.Instance.OnUpdateEvent -= OnUpdate; } } \ No newline at end of file diff --git a/HKMP/Game/Client/Entity/Entity.cs b/HKMP/Game/Client/Entity/Entity.cs index 688d77b1..f245fce4 100644 --- a/HKMP/Game/Client/Entity/Entity.cs +++ b/HKMP/Game/Client/Entity/Entity.cs @@ -265,6 +265,7 @@ private void ProcessHostFsm(PlayMakerFSM fsm) { /// The Playmaker FSM to process. private void ProcessClientFsm(PlayMakerFSM fsm) { Logger.Info($"Processing client FSM: {fsm.Fsm.Name}"); + EntityInitializer.InitializeFsm(fsm); fsm.enabled = false; } @@ -280,12 +281,14 @@ private void FindComponents() { Client = clientHealthManager }; - _components[EntityNetworkData.DataType.HealthManager] = new HealthManagerComponent( + var hmComponent = new HealthManagerComponent( _netClient, _entityId, Object, healthManager ); + _components[EntityNetworkData.DataType.Death] = hmComponent; + _components[EntityNetworkData.DataType.Invincibility] = hmComponent; } var climber = Object.Client.GetComponent(); @@ -315,6 +318,24 @@ private void FindComponents() { collider ); } + + var hostDamageHero = Object.Host.GetComponent(); + var clientDamageHero = Object.Client.GetComponent(); + if (hostDamageHero != null && clientDamageHero != null) { + Logger.Info($"Adding DamageHero component to entity: {Object.Host.name}"); + + var damageHero = new HostClientPair { + Host = hostDamageHero, + Client = clientDamageHero + }; + + _components[EntityNetworkData.DataType.DamageHero] = new DamageHeroComponent( + _netClient, + _entityId, + Object, + damageHero + ); + } // Find Walker MonoBehaviour and remove it from the client object var walker = Object.Client.GetComponent(); @@ -937,7 +958,7 @@ public void Destroy() { MonoBehaviourUtil.Instance.OnUpdateEvent -= OnUpdate; On.tk2dSpriteAnimator.Play_tk2dSpriteAnimationClip_float_float -= OnAnimationPlayed; - foreach (var component in _components.Values) { + foreach (var component in _components.Values.Distinct()) { component.Destroy(); } } diff --git a/HKMP/Game/Client/Entity/EntityInitializer.cs b/HKMP/Game/Client/Entity/EntityInitializer.cs index 6f697709..1f890e7b 100644 --- a/HKMP/Game/Client/Entity/EntityInitializer.cs +++ b/HKMP/Game/Client/Entity/EntityInitializer.cs @@ -16,7 +16,8 @@ internal static class EntityInitializer { "init", "initiate", "initialise", - "initialize" + "initialize", + "dormant" }; /// diff --git a/HKMP/Game/Server/ServerManager.cs b/HKMP/Game/Server/ServerManager.cs index a9a1ef3a..c1109dc1 100644 --- a/HKMP/Game/Server/ServerManager.cs +++ b/HKMP/Game/Server/ServerManager.cs @@ -743,8 +743,7 @@ void ReplaceExistingDataWithSameType(EntityNetworkData.DataType type, Packet dat } foreach (var updateData in entityUpdate.GenericData) { - if (updateData.Type == EntityNetworkData.DataType.Rotation - || updateData.Type == EntityNetworkData.DataType.Collider) { + if (updateData.Type > EntityNetworkData.DataType.Death) { ReplaceExistingDataWithSameType(updateData.Type, updateData.Packet); } } diff --git a/HKMP/Networking/Packet/Data/EntityUpdate.cs b/HKMP/Networking/Packet/Data/EntityUpdate.cs index 5df52cdf..3044b05f 100644 --- a/HKMP/Networking/Packet/Data/EntityUpdate.cs +++ b/HKMP/Networking/Packet/Data/EntityUpdate.cs @@ -244,9 +244,11 @@ public void ReadData(IPacket packet) { /// public enum DataType : byte { Fsm = 0, - HealthManager, + Death, + Invincibility, Rotation, - Collider + Collider, + DamageHero } } From 221a1c68f2a6c88345d9768ceaf7d5aafe7a6d73 Mon Sep 17 00:00:00 2001 From: Extremelyd1 Date: Sat, 27 May 2023 23:58:11 +0200 Subject: [PATCH 032/216] Add mesh renderer component, add non-fsm entities --- HKMP/Game/Client/ClientManager.cs | 40 +--- .../Client/Entity/Action/EntityFsmActions.cs | 2 +- .../Entity/Component/DamageHeroComponent.cs | 4 +- .../Entity/Component/MeshRendererComponent.cs | 75 +++++++ HKMP/Game/Client/Entity/Entity.cs | 18 ++ HKMP/Game/Client/Entity/EntityManager.cs | 193 +++++++++--------- HKMP/Game/Client/Entity/EntityRegistry.cs | 61 +++++- HKMP/Game/Client/Entity/EntityType.cs | 5 +- HKMP/Networking/Packet/Data/EntityUpdate.cs | 3 +- HKMP/Resource/entity-registry.json | 40 ++++ 10 files changed, 296 insertions(+), 145 deletions(-) create mode 100644 HKMP/Game/Client/Entity/Component/MeshRendererComponent.cs diff --git a/HKMP/Game/Client/ClientManager.cs b/HKMP/Game/Client/ClientManager.cs index c58238f5..6db64542 100644 --- a/HKMP/Game/Client/ClientManager.cs +++ b/HKMP/Game/Client/ClientManager.cs @@ -614,7 +614,7 @@ private void OnPlayerAlreadyInScene(ClientPlayerAlreadyInScene alreadyInScene) { foreach (var entityUpdate in alreadyInScene.EntityUpdateList) { Logger.Info($"Updating already in scene entity with ID: {entityUpdate.Id}"); - HandleEntityUpdate(entityUpdate, true); + _entityManager.HandleEntityUpdate(entityUpdate, true); } // Whether there were players in the scene or not, we have now determined whether @@ -745,7 +745,7 @@ private void OnEntityUpdate(EntityUpdate entityUpdate) { return; } - HandleEntityUpdate(entityUpdate); + _entityManager.HandleEntityUpdate(entityUpdate); } private void OnSceneHostTransfer() { @@ -753,42 +753,6 @@ private void OnSceneHostTransfer() { _entityManager.BecomeSceneHost(); } - - /// - /// Method for handling received entity updates. - /// - /// The entity update to handle. - /// Whether this is the update from the already in scene packet. - private void HandleEntityUpdate(EntityUpdate entityUpdate, bool alreadyInSceneUpdate = false) { - if (entityUpdate.UpdateTypes.Contains(EntityUpdateType.Position)) { - _entityManager.UpdateEntityPosition(entityUpdate.Id, entityUpdate.Position); - } - - if (entityUpdate.UpdateTypes.Contains(EntityUpdateType.Scale)) { - _entityManager.UpdateEntityScale(entityUpdate.Id, entityUpdate.Scale); - } - - if (entityUpdate.UpdateTypes.Contains(EntityUpdateType.Animation)) { - _entityManager.UpdateEntityAnimation( - entityUpdate.Id, - entityUpdate.AnimationId, - entityUpdate.AnimationWrapMode, - alreadyInSceneUpdate - ); - } - - if (entityUpdate.UpdateTypes.Contains(EntityUpdateType.Active)) { - _entityManager.UpdateEntityIsActive(entityUpdate.Id, entityUpdate.IsActive); - } - - if (entityUpdate.UpdateTypes.Contains(EntityUpdateType.Data)) { - _entityManager.UpdateEntityData(entityUpdate.Id, entityUpdate.GenericData); - } - - if (entityUpdate.UpdateTypes.Contains(EntityUpdateType.HostFsm)) { - _entityManager.UpdateHostEntityFsmData(entityUpdate.Id, entityUpdate.HostFsmData); - } - } /// /// Callback method for when the server settings are updated by the server. diff --git a/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs b/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs index c7bf92d5..4cfb093c 100644 --- a/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs +++ b/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs @@ -152,7 +152,7 @@ public static void ApplyNetworkDataFromAction(EntityNetworkData data, FsmStateAc /// true if the given game object is in the entity registry; otherwise false. private static bool IsObjectInRegistry(GameObject gameObject) { foreach (var fsm in gameObject.GetComponents()) { - if (EntityRegistry.TryGetEntry(fsm.gameObject.name, fsm.Fsm.Name, out _)) { + if (EntityRegistry.TryGetEntry(fsm.gameObject, fsm.Fsm.Name, out _)) { return true; } } diff --git a/HKMP/Game/Client/Entity/Component/DamageHeroComponent.cs b/HKMP/Game/Client/Entity/Component/DamageHeroComponent.cs index 21f69231..eece8168 100644 --- a/HKMP/Game/Client/Entity/Component/DamageHeroComponent.cs +++ b/HKMP/Game/Client/Entity/Component/DamageHeroComponent.cs @@ -6,7 +6,7 @@ namespace Hkmp.Game.Client.Entity.Component; /// -/// This component manages the rotation of the entity. +/// This component manages the damage that an entity deals to the player. internal class DamageHeroComponent : EntityComponent { /// /// The host-client pair of unity components of the entity. @@ -14,7 +14,7 @@ internal class DamageHeroComponent : EntityComponent { private readonly HostClientPair _damageHero; /// - /// The last value of damage dealt for the damage hero component. + /// The last value of damage dealt for the damage hero. /// private int _lastDamageDealt; diff --git a/HKMP/Game/Client/Entity/Component/MeshRendererComponent.cs b/HKMP/Game/Client/Entity/Component/MeshRendererComponent.cs new file mode 100644 index 00000000..3fa24027 --- /dev/null +++ b/HKMP/Game/Client/Entity/Component/MeshRendererComponent.cs @@ -0,0 +1,75 @@ +using Hkmp.Networking.Client; +using Hkmp.Networking.Packet.Data; +using Hkmp.Util; +using UnityEngine; + +namespace Hkmp.Game.Client.Entity.Component; + +// TODO: optimization idea: only add this component to objects where the FSM has an action that enables/disables the +// mesh renderer + +/// +/// This component manages the mesh renderer of the entity. +internal class MeshRendererComponent : EntityComponent { + /// + /// The host-client pair of unity components of the entity. + /// + private readonly HostClientPair _meshRenderer; + + /// + /// The last value of 'enabled' for the mesh renderer. + /// + private bool _lastEnabled; + + public MeshRendererComponent( + NetClient netClient, + byte entityId, + HostClientPair gameObject, + HostClientPair meshRenderer + ) : base(netClient, entityId, gameObject) { + _meshRenderer = meshRenderer; + _lastEnabled = meshRenderer.Host.enabled; + + MonoBehaviourUtil.Instance.OnUpdateEvent += OnUpdate; + } + + /// + /// Callback method to check for mesh renderer updates. + /// + private void OnUpdate() { + if (IsControlled) { + return; + } + + if (GameObject.Host == null) { + return; + } + + var newEnabled = _meshRenderer.Host.enabled; + if (newEnabled != _lastEnabled) { + _lastEnabled = newEnabled; + + var data = new EntityNetworkData { + Type = EntityNetworkData.DataType.MeshRenderer + }; + data.Packet.Write(newEnabled); + + SendData(data); + } + } + + /// + public override void InitializeHost() { + } + + /// + public override void Update(EntityNetworkData data) { + var enabled = data.Packet.ReadBool(); + _meshRenderer.Client.enabled = enabled; + } + + /// + public override void Destroy() { + MonoBehaviourUtil.Instance.OnUpdateEvent -= OnUpdate; + } +} \ No newline at end of file diff --git a/HKMP/Game/Client/Entity/Entity.cs b/HKMP/Game/Client/Entity/Entity.cs index f245fce4..7fa81aa5 100644 --- a/HKMP/Game/Client/Entity/Entity.cs +++ b/HKMP/Game/Client/Entity/Entity.cs @@ -337,6 +337,24 @@ private void FindComponents() { ); } + var hostMeshRenderer = Object.Host.GetComponent(); + var clientMeshRenderer = Object.Client.GetComponent(); + if (hostMeshRenderer != null && clientMeshRenderer != null) { + Logger.Info($"Adding MeshRenderer component to entity: {Object.Host.name}"); + + var meshRenderer = new HostClientPair { + Host = hostMeshRenderer, + Client = clientMeshRenderer + }; + + _components[EntityNetworkData.DataType.MeshRenderer] = new MeshRendererComponent( + _netClient, + _entityId, + Object, + meshRenderer + ); + } + // Find Walker MonoBehaviour and remove it from the client object var walker = Object.Client.GetComponent(); if (walker != null) { diff --git a/HKMP/Game/Client/Entity/EntityManager.cs b/HKMP/Game/Client/Entity/EntityManager.cs index d2edaab0..2fe85962 100644 --- a/HKMP/Game/Client/Entity/EntityManager.cs +++ b/HKMP/Game/Client/Entity/EntityManager.cs @@ -7,7 +7,6 @@ using UnityEngine; using UnityEngine.SceneManagement; using Logger = Hkmp.Logging.Logger; -using Vector2 = Hkmp.Math.Vector2; namespace Hkmp.Game.Client.Entity; @@ -35,9 +34,17 @@ internal class EntityManager { /// private byte _lastId; + /// + /// Queue of entity updates that have not been applied yet because of a missing entity. + /// Usually this occurs because the entities are loaded later than the updates are received when the local player + /// enters a new scene. + /// + private readonly Queue _receivedUpdates; + public EntityManager(NetClient netClient) { _netClient = netClient; _entities = new Dictionary(); + _receivedUpdates = new Queue(); _lastId = 0; @@ -116,7 +123,7 @@ public void SpawnEntity(byte id, EntityType spawningType, EntityType spawnedType // Make sure to initialize all entities that should be in the system foreach (var fsm in gameObject.GetComponents()) { - if (EntityRegistry.TryGetEntry(gameObject.name, fsm.Fsm.Name, out _)) { + if (EntityRegistry.TryGetEntry(gameObject, fsm.Fsm.Name, out _)) { EntityInitializer.InitializeFsm(fsm); } } @@ -129,115 +136,51 @@ public void SpawnEntity(byte id, EntityType spawningType, EntityType spawnedType ); _entities[id] = entity; } - + /// - /// Update the position for the entity with the given ID. + /// Method for handling received entity updates. /// - /// The entity ID. - /// The new position. - public void UpdateEntityPosition(byte entityId, Vector2 position) { + /// The entity update to handle. + /// Whether this is the update from the already in scene packet. + public void HandleEntityUpdate(EntityUpdate entityUpdate, bool alreadyInSceneUpdate = false) { if (_isSceneHost) { return; } - if (!_entities.TryGetValue(entityId, out var entity)) { - return; - } - - entity.UpdatePosition(position); - } - - /// - /// Update the scale for the entity with the given ID. - /// - /// The entity ID. - /// The new scale. - public void UpdateEntityScale(byte entityId, bool scale) { - if (_isSceneHost) { - return; - } - - if (!_entities.TryGetValue(entityId, out var entity)) { - return; - } - - entity.UpdateScale(scale); - } - - /// - /// Update the animation for the entity with the given ID. - /// - /// The entity ID. - /// The ID of the animation. - /// The wrap mode of the animation. - /// Whether this update is when we are entering the scene. - public void UpdateEntityAnimation( - byte entityId, - byte animationId, - byte animationWrapMode, - bool alreadyInSceneUpdate - ) { - if (_isSceneHost) { - return; - } - - if (!_entities.TryGetValue(entityId, out var entity)) { + if (!_entities.TryGetValue(entityUpdate.Id, out var entity)) { + Logger.Debug($"Could not find entity ({entityUpdate.Id}) to apply update for; storing update for now"); + _receivedUpdates.Enqueue(entityUpdate); + return; } - - entity.UpdateAnimation(animationId, (tk2dSpriteAnimationClip.WrapMode) animationWrapMode, - alreadyInSceneUpdate); - } - - /// - /// Update whether the entity with the given ID is active. - /// - /// The entity ID. - /// The new value for active. - public void UpdateEntityIsActive(byte entityId, bool isActive) { - if (_isSceneHost) { - return; + + if (entityUpdate.UpdateTypes.Contains(EntityUpdateType.Position)) { + entity.UpdatePosition(entityUpdate.Position); } - if (!_entities.TryGetValue(entityId, out var entity)) { - return; + if (entityUpdate.UpdateTypes.Contains(EntityUpdateType.Scale)) { + entity.UpdateScale(entityUpdate.Scale); } - - entity.UpdateIsActive(isActive); - } - - /// - /// Update the entity with the given ID with the given generic data. - /// - /// The ID of the entity. - /// The list of data to update the entity with. - public void UpdateEntityData(byte entityId, List data) { - if (_isSceneHost) { - return; + + if (entityUpdate.UpdateTypes.Contains(EntityUpdateType.Animation)) { + entity.UpdateAnimation( + entityUpdate.AnimationId, + (tk2dSpriteAnimationClip.WrapMode) entityUpdate.AnimationWrapMode, + alreadyInSceneUpdate + ); } - if (!_entities.TryGetValue(entityId, out var entity)) { - return; + if (entityUpdate.UpdateTypes.Contains(EntityUpdateType.Active)) { + entity.UpdateIsActive(entityUpdate.IsActive); } - entity.UpdateData(data); - } - - /// - /// Update the host FSMs of the entity with the given ID with the given data. - /// - /// The ID of the entity. - /// Dictionary mapping FSM index to FSM data. - public void UpdateHostEntityFsmData(byte entityId, Dictionary hostFsmData) { - if (_isSceneHost) { - return; + if (entityUpdate.UpdateTypes.Contains(EntityUpdateType.Data)) { + entity.UpdateData(entityUpdate.GenericData); } - if (!_entities.TryGetValue(entityId, out var entity)) { - return; + if (entityUpdate.UpdateTypes.Contains(EntityUpdateType.HostFsm)) { + entity.UpdateHostFsmData(entityUpdate.HostFsmData); } - - entity.UpdateHostFsmData(hostFsmData); } /// @@ -260,7 +203,7 @@ private void OnGameObjectSpawned(FsmStateAction action, GameObject gameObject) { // Since an entity was created and we are the scene host, we need to notify the server var spawningObjectName = action.Fsm.GameObject.name; var spawningFsmName = action.Fsm.Name; - if (EntityRegistry.TryGetEntry(spawningObjectName, spawningFsmName, out var entry)) { + if (EntityRegistry.TryGetEntry(action.Fsm.GameObject, spawningFsmName, out var entry)) { Logger.Info( $"Notifying server of entity ({spawningObjectName}, {entry.Type}) spawning entity ({gameObject.name}, {entity.Type}) with ID {entityId}"); _netClient.UpdateManager.SetEntitySpawn(entityId, entry.Type, entity.Type); @@ -282,6 +225,21 @@ private void OnGameObjectSpawned(FsmStateAction action, GameObject gameObject) { } } + /// + /// Check to see if there are received un-applied entity updates. + /// + private void CheckReceivedUpdates() { + while (_receivedUpdates.Count != 0) { + var update = _receivedUpdates.Dequeue(); + + if (_entities.TryGetValue(update.Id, out _)) { + Logger.Debug("Found un-applied entity update, applying now"); + + HandleEntityUpdate(update); + } + } + } + /// /// Callback method for when the scene changes. Will clear existing entities and start checking for /// new entities. @@ -304,6 +262,10 @@ private void OnSceneChanged(Scene oldScene, Scene newScene) { } FindEntitiesInScene(newScene, false); + + // Since we have tried finding entities in the scene, we also check whether there are un-applied updates for + // those entities + CheckReceivedUpdates(); } /// @@ -322,6 +284,10 @@ private void OnSceneLoaded(Scene scene, LoadSceneMode mode) { Logger.Info($"Additional scene loaded ({scene.name}), looking for entities"); FindEntitiesInScene(scene, true); + + // Since we have tried finding entities in the scene, we also check whether there are un-applied updates for + // those entities + CheckReceivedUpdates(); } /// @@ -331,12 +297,15 @@ private void OnSceneLoaded(Scene scene, LoadSceneMode mode) { /// Whether this scene was loaded late. private void FindEntitiesInScene(Scene scene, bool lateLoad) { // Find all PlayMakerFSM components - foreach (var fsm in Object.FindObjectsOfType()) { + var fsms = Object.FindObjectsOfType(); + + foreach (var fsm in fsms) { // Logger.Info($"Found FSM: {fsm.Fsm.Name} in scene: {fsm.gameObject.scene.name}"); if (fsm.gameObject.scene != scene) { continue; } + // Process the FSM of the game object and only proceed if it was successful and it is a late scene load if (!ProcessGameObjectFsm(fsm, out var entity, out _) || !lateLoad) { continue; } @@ -350,6 +319,38 @@ private void FindEntitiesInScene(Scene scene, bool lateLoad) { } } + // Check specifically for children of FSM game objects + foreach (var fsm in fsms) { + var gameObject = fsm.gameObject; + + for (var i = 0; i < gameObject.transform.childCount; i++) { + var child = gameObject.transform.GetChild(i); + var childObj = child.gameObject; + + if (!EntityRegistry.TryGetEntryWithParent( + childObj.name, + gameObject.name, + out var entry + )) { + continue; + } + + Logger.Debug($"Found child of '{gameObject.name}' to be registered: {childObj.name}, {entry.Type}"); + + var entity = RegisterGameObjectAsEntity(childObj, entry.Type, out _); + + if (lateLoad) { + if (_isSceneHost) { + // Since this is a late load it needs to be initialized as host if we are the scene host + entity.InitializeHost(); + } else { + // Since this is a late load we need to update the 'active' state of the entity + entity.UpdateIsActive(true); + } + } + } + } + // Find all Climber components foreach (var climber in Object.FindObjectsOfType()) { if (climber.gameObject.scene != scene) { @@ -374,7 +375,7 @@ private void FindEntitiesInScene(Scene scene, bool lateLoad) { private bool ProcessGameObjectFsm(PlayMakerFSM fsm, out Entity entity, out byte entityId) { // Logger.Info($"Processing FSM: {fsm.Fsm.Name}, {fsm.gameObject.name}"); - if (!EntityRegistry.TryGetEntry(fsm.gameObject.name, fsm.Fsm.Name, out var entry)) { + if (!EntityRegistry.TryGetEntry(fsm.gameObject, fsm.Fsm.Name, out var entry)) { entity = null; entityId = 0; return false; diff --git a/HKMP/Game/Client/Entity/EntityRegistry.cs b/HKMP/Game/Client/Entity/EntityRegistry.cs index fc204a7a..9f42fc63 100644 --- a/HKMP/Game/Client/Entity/EntityRegistry.cs +++ b/HKMP/Game/Client/Entity/EntityRegistry.cs @@ -1,8 +1,9 @@ using System.Collections.Generic; -using Hkmp.Logging; using Hkmp.Util; using Newtonsoft.Json; using Newtonsoft.Json.Converters; +using UnityEngine; +using Logger = Hkmp.Logging.Logger; namespace Hkmp.Game.Client.Entity; @@ -29,25 +30,44 @@ static EntityRegistry() { } /// - /// Try to get the entity registry entry given a game object name and a FSM name. + /// Try to get the entity registry entry given a game object and a FSM name. /// - /// The name of the game object. + /// The game object. /// The name of the FSM. /// The entry if it is found; otherwise null. /// True if the entry was found; otherwise false. - public static bool TryGetEntry(string gameObjectName, string fsmName, out EntityRegistryEntry foundEntry) { + public static bool TryGetEntry(GameObject gameObject, string fsmName, out EntityRegistryEntry foundEntry) { + foundEntry = null; + foreach (var entry in Entries) { + if (entry.FsmName == null) { + continue; + } + if (!entry.FsmName.Equals(fsmName)) { continue; } - if (gameObjectName.Contains(entry.BaseObjectName)) { + if (gameObject.name.Contains(entry.BaseObjectName)) { + // If a parent name is defined on the entry, the parent of the object needs to match + if (entry.ParentName != null) { + var parent = gameObject.transform.parent; + // No parent, so no match to the entry + if (parent == null) { + return false; + } + + // Parent name does not match the entry + if (!parent.gameObject.name.Contains(entry.ParentName)) { + return false; + } + } + foundEntry = entry; return true; } } - foundEntry = null; return false; } @@ -73,6 +93,29 @@ public static bool TryGetEntry(string gameObjectName, EntityType type, out Entit foundEntry = null; return false; } + + /// + /// Try to get the entity registry entry given a game object name and the name of the parent game object. + /// + /// The name of the game object. + /// The name of the parent game object. + /// The entry if it is found; otherwise null. + /// True if the entry was found; otherwise false. + public static bool TryGetEntryWithParent(string gameObjectName, string parentName, out EntityRegistryEntry foundEntry) { + foreach (var entry in Entries) { + if (entry.BaseObjectName == null || entry.ParentName == null) { + continue; + } + + if (gameObjectName.Contains(entry.BaseObjectName) && parentName.Contains(entry.ParentName)) { + foundEntry = entry; + return true; + } + } + + foundEntry = null; + return false; + } } /// @@ -98,4 +141,10 @@ internal class EntityRegistryEntry { /// [JsonProperty("fsm_name")] public string FsmName { get; set; } + + /// + /// The name of the parent of this object. Can be empty if there is no parent or it is not relevant. + /// + [JsonProperty("parent_name")] + public string ParentName { get; set; } } diff --git a/HKMP/Game/Client/Entity/EntityType.cs b/HKMP/Game/Client/Entity/EntityType.cs index 8fd710a6..0575763c 100644 --- a/HKMP/Game/Client/Entity/EntityType.cs +++ b/HKMP/Game/Client/Entity/EntityType.cs @@ -21,5 +21,8 @@ internal enum EntityType { Goam, Baldur, ElderBaldur, - FalseKnight + FalseKnight, + FalseKnightHead, + FalseKnightBarrels, + FalseKnightFloor } diff --git a/HKMP/Networking/Packet/Data/EntityUpdate.cs b/HKMP/Networking/Packet/Data/EntityUpdate.cs index 3044b05f..346884a3 100644 --- a/HKMP/Networking/Packet/Data/EntityUpdate.cs +++ b/HKMP/Networking/Packet/Data/EntityUpdate.cs @@ -248,7 +248,8 @@ public enum DataType : byte { Invincibility, Rotation, Collider, - DamageHero + DamageHero, + MeshRenderer } } diff --git a/HKMP/Resource/entity-registry.json b/HKMP/Resource/entity-registry.json index fb7f25c7..59ba34c9 100644 --- a/HKMP/Resource/entity-registry.json +++ b/HKMP/Resource/entity-registry.json @@ -88,5 +88,45 @@ "base_object_name": "False Knight New", "type": "FalseKnight", "fsm_name": "FalseyControl" + }, + { + "base_object_name": "Head", + "type": "FalseKnightHead", + "fsm_name": "Health Check" + }, + { + "base_object_name": "FK Barrel Summon", + "type": "FalseKnightBarrels", + "fsm_name": "summon" + }, + { + "base_object_name": "Normal 1", + "type": "FalseKnightFloor", + "parent_name": "FK Floor" + }, + { + "base_object_name": "Normal 2", + "type": "FalseKnightFloor", + "parent_name": "FK Floor" + }, + { + "base_object_name": "Cracked 1", + "type": "FalseKnightFloor", + "parent_name": "FK Floor" + }, + { + "base_object_name": "Cracked 2", + "type": "FalseKnightFloor", + "parent_name": "FK Floor" + }, + { + "base_object_name": "Broken", + "type": "FalseKnightFloor", + "parent_name": "FK Floor" + }, + { + "base_object_name": "Break Floor", + "type": "FalseKnightFloor", + "parent_name": "FK Floor" } ] From 1f68ee5558b21fbb8c3eb95385a259ab0b9628bc Mon Sep 17 00:00:00 2001 From: Extremelyd1 Date: Mon, 29 May 2023 15:30:27 +0200 Subject: [PATCH 033/216] Add velocity component for FK, various other fixes --- .../Client/Entity/Action/EntityFsmActions.cs | 295 +++++++++++++++++- .../Entity/Component/ColliderComponent.cs | 3 +- .../Entity/Component/DamageHeroComponent.cs | 1 + .../Component/HealthManagerComponent.cs | 2 + .../Entity/Component/MeshRendererComponent.cs | 1 + .../Entity/Component/RotationComponent.cs | 19 +- .../Entity/Component/VelocityComponent.cs | 99 ++++++ HKMP/Game/Client/Entity/Entity.cs | 172 +++++++--- HKMP/Game/Client/Entity/EntityInitializer.cs | 4 + HKMP/Networking/Packet/Data/EntityUpdate.cs | 3 +- 10 files changed, 540 insertions(+), 59 deletions(-) create mode 100644 HKMP/Game/Client/Entity/Component/VelocityComponent.cs diff --git a/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs b/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs index 4cfb093c..a2b39e71 100644 --- a/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs +++ b/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs @@ -6,6 +6,7 @@ using HutongGames.PlayMaker.Actions; using UnityEngine; using Logger = Hkmp.Logging.Logger; +using Object = UnityEngine.Object; using Random = UnityEngine.Random; // ReSharper disable UnusedMember.Local @@ -228,6 +229,79 @@ private static void ApplyNetworkDataFromAction(EntityNetworkData data, SpawnObje } } + #endregion + + #region CreateObject + + private static bool GetNetworkDataFromAction(EntityNetworkData data, CreateObject action) { + EntitySpawnEvent?.Invoke(action, action.storeObject.Value); + + // We first check whether this action results in the spawning of an entity that is managed by the + // system. Because if so, it would already be handled by an EntitySpawn packet instead, and this will only + // duplicate the spawning and leave it uncontrolled. So we don't send the data at all + var toSpawnObject = action.storeObject.Value; + if (IsObjectInRegistry(toSpawnObject)) { + Logger.Debug($"Tried getting CreateObject network data, but spawned object is entity"); + return false; + } + + var original = action.gameObject.Value; + if (original == null) { + return false; + } + + var position = Vector3.zero; + var euler = Vector3.zero; + + if (action.spawnPoint.Value != null) { + position = action.spawnPoint.Value.transform.position; + if (!action.position.IsNone) { + position += action.position.Value; + } + + euler = !action.rotation.IsNone ? action.rotation.Value : action.spawnPoint.Value.transform.eulerAngles; + } else { + if (!action.position.IsNone) { + position = action.position.Value; + } + + if (!action.rotation.IsNone) { + euler = action.rotation.Value; + } + } + + data.Packet.Write(position.x); + data.Packet.Write(position.y); + data.Packet.Write(position.z); + + data.Packet.Write(euler.x); + data.Packet.Write(euler.y); + data.Packet.Write(euler.z); + + return true; + } + + private static void ApplyNetworkDataFromAction(EntityNetworkData data, CreateObject action) { + var position = new Vector3( + data.Packet.ReadFloat(), + data.Packet.ReadFloat(), + data.Packet.ReadFloat() + ); + var euler = new Vector3( + data.Packet.ReadFloat(), + data.Packet.ReadFloat(), + data.Packet.ReadFloat() + ); + + var original = action.gameObject.Value; + if (original == null) { + return; + } + + var spawnedObject = Object.Instantiate(original, position, Quaternion.Euler(euler)); + action.storeObject.Value = spawnedObject; + } + #endregion #region FireAtTarget @@ -301,16 +375,16 @@ private static bool GetNetworkDataFromAction(EntityNetworkData data, SetScale ac } private static void ApplyNetworkDataFromAction(EntityNetworkData data, SetScale action) { - var gameObject = action.Fsm.GetOwnerDefaultTarget(action.gameObject); - if (gameObject == action.Fsm.GameObject) { - return; - } - var scale = new Vector3( data.Packet.ReadFloat(), data.Packet.ReadFloat(), data.Packet.ReadFloat() ); + + var gameObject = action.Fsm.GetOwnerDefaultTarget(action.gameObject); + if (gameObject == action.Fsm.GameObject) { + return; + } gameObject.transform.localScale = scale; } @@ -320,6 +394,15 @@ private static void ApplyNetworkDataFromAction(EntityNetworkData data, SetScale #region SetFsmBool private static bool GetNetworkDataFromAction(EntityNetworkData data, SetFsmBool action) { + if (action.setValue == null) { + return false; + } + + var gameObject = action.Fsm.GetOwnerDefaultTarget(action.gameObject); + if (gameObject == action.Fsm.GameObject) { + return false; + } + var setValue = action.setValue.Value; data.Packet.Write(setValue); @@ -330,10 +413,6 @@ private static void ApplyNetworkDataFromAction(EntityNetworkData data, SetFsmBoo var setValue = data.Packet.ReadBool(); var gameObject = action.Fsm.GetOwnerDefaultTarget(action.gameObject); - if (gameObject == action.Fsm.GameObject) { - return; - } - if (gameObject == null) { return; } @@ -357,6 +436,15 @@ private static void ApplyNetworkDataFromAction(EntityNetworkData data, SetFsmBoo private static bool GetNetworkDataFromAction(EntityNetworkData data, SetFsmFloat action) { // TODO: if action.setValue can be a reference, make sure to network it + if (action.setValue == null) { + return false; + } + + var gameObject = action.Fsm.GetOwnerDefaultTarget(action.gameObject); + if (gameObject == action.Fsm.GameObject) { + return false; + } + return true; } @@ -365,11 +453,48 @@ private static void ApplyNetworkDataFromAction(EntityNetworkData data, SetFsmFlo return; } + var gameObject = action.Fsm.GetOwnerDefaultTarget(action.gameObject); + if (gameObject == null) { + return; + } + + var fsm = ActionHelpers.GetGameObjectFsm(gameObject, action.fsmName.Value); + if (fsm == null) { + return; + } + + var fsmFloat = fsm.FsmVariables.GetFsmFloat(action.variableName.Value); + if (fsmFloat == null) { + return; + } + + fsmFloat.Value = action.setValue.Value; + } + + #endregion + + #region SetFsmString + + private static bool GetNetworkDataFromAction(EntityNetworkData data, SetFsmString action) { + // TODO: if action.setValue can be a reference, make sure to network it + if (action.setValue == null) { + return false; + } + var gameObject = action.Fsm.GetOwnerDefaultTarget(action.gameObject); if (gameObject == action.Fsm.GameObject) { + return false; + } + + return true; + } + + private static void ApplyNetworkDataFromAction(EntityNetworkData data, SetFsmString action) { + if (action.setValue == null) { return; } + var gameObject = action.Fsm.GetOwnerDefaultTarget(action.gameObject); if (gameObject == null) { return; } @@ -379,12 +504,12 @@ private static void ApplyNetworkDataFromAction(EntityNetworkData data, SetFsmFlo return; } - var fsmFloat = fsm.FsmVariables.GetFsmFloat(action.variableName.Value); - if (fsmFloat == null) { + var fsmString = fsm.FsmVariables.GetFsmString(action.variableName.Value); + if (fsmString == null) { return; } - fsmFloat.Value = action.setValue.Value; + fsmString.Value = action.setValue.Value; } #endregion @@ -414,7 +539,63 @@ private static void ApplyNetworkDataFromAction(EntityNetworkData data, SetPartic particleSystem.enableEmission = action.emission.Value; #pragma warning restore CS0618 } + + #endregion + #region SetParticleEmissionRate + + private static bool GetNetworkDataFromAction(EntityNetworkData data, SetParticleEmissionRate action) { + return true; + } + + private static void ApplyNetworkDataFromAction(EntityNetworkData data, SetParticleEmissionRate action) { + if (action.gameObject == null) { + return; + } + + var gameObject = action.Fsm.GetOwnerDefaultTarget(action.gameObject); + if (gameObject == null) { + return; + } + + var particleSystem = gameObject.GetComponent(); + if (particleSystem == null) { + return; + } + +#pragma warning disable CS0618 + particleSystem.emissionRate = action.emissionRate.Value; +#pragma warning restore CS0618 + } + + #endregion + + #region SetParticleEmissionSpeed + + private static bool GetNetworkDataFromAction(EntityNetworkData data, SetParticleEmissionSpeed action) { + return true; + } + + private static void ApplyNetworkDataFromAction(EntityNetworkData data, SetParticleEmissionSpeed action) { + if (action.gameObject == null) { + return; + } + + var gameObject = action.Fsm.GetOwnerDefaultTarget(action.gameObject); + if (gameObject == null) { + return; + } + + var particleSystem = gameObject.GetComponent(); + if (particleSystem == null) { + return; + } + +#pragma warning disable CS0618 + particleSystem.startSpeed = action.emissionSpeed.Value; +#pragma warning restore CS0618 + } + #endregion #region PlayParticleEmitter @@ -900,4 +1081,94 @@ private static void ApplyNetworkDataFromAction(EntityNetworkData data, SpawnBloo } #endregion + + #region SendEventByName + + private static bool GetNetworkDataFromAction(EntityNetworkData data, SendEventByName action) { + if (action.eventTarget.gameObject.GameObject.Value == action.Fsm.GameObject.gameObject) { + return false; + } + + return true; + } + + private static void ApplyNetworkDataFromAction(EntityNetworkData data, SendEventByName action) { + if (action.delay.Value < 1.0 / 1000.0) { + action.Fsm.Event(action.eventTarget, action.sendEvent.Value); + } else { + action.Fsm.DelayedEvent( + action.eventTarget, + FsmEvent.GetFsmEvent(action.sendEvent.Value), + action.delay.Value + ); + } + } + + #endregion + + #region SendHealthManagerDeathEvent + + private static bool GetNetworkDataFromAction(EntityNetworkData data, SendHealthManagerDeathEvent action) { + return true; + } + + private static void ApplyNetworkDataFromAction(EntityNetworkData data, SendHealthManagerDeathEvent action) { + var gameObject = action.target.OwnerOption == OwnerDefaultOption.UseOwner + ? action.Owner + : action.target.GameObject.Value; + + if (gameObject == null) { + return; + } + + var healthManager = gameObject.GetComponent(); + if (healthManager == null) { + return; + } + + healthManager.SendDeathEvent(); + } + + #endregion + + #region GetVelocity2d + + private static bool GetNetworkDataFromAction(EntityNetworkData data, GetVelocity2d action) { + var obj = action.Fsm.GetOwnerDefaultTarget(action.gameObject); + if (obj == null) { + Logger.Debug("GetVelocity2d no obj"); + return false; + } + + var rigidbody = obj.GetComponent(); + if (rigidbody == null) { + Logger.Debug("GetVelocity2d no rigidbody"); + return false; + } + + var vel = rigidbody.velocity; + + Logger.Debug($"GetVelocity2d: Current velocity: {vel.x}, {vel.y}, {rigidbody.GetInstanceID()}"); + Logger.Debug($"GetVelocity2d: Set value: {action.y.Value}"); + + return false; + } + + private static void ApplyNetworkDataFromAction(EntityNetworkData data, GetVelocity2d action) { + } + + #endregion + + #region FloatMultiplyV2 + + private static bool GetNetworkDataFromAction(EntityNetworkData data, FloatMultiplyV2 action) { + Logger.Debug($"FloatMultiplyV2: New multiplied value: {action.floatVariable.Value}"); + + return false; + } + + private static void ApplyNetworkDataFromAction(EntityNetworkData data, FloatMultiplyV2 action) { + } + + #endregion } diff --git a/HKMP/Game/Client/Entity/Component/ColliderComponent.cs b/HKMP/Game/Client/Entity/Component/ColliderComponent.cs index 7fdc0aa4..c4bd1991 100644 --- a/HKMP/Game/Client/Entity/Component/ColliderComponent.cs +++ b/HKMP/Game/Client/Entity/Component/ColliderComponent.cs @@ -70,8 +70,9 @@ public override void Update(EntityNetworkData data) { } var enabled = data.Packet.ReadBool(); + _collider.Host.enabled = enabled; _collider.Client.enabled = enabled; - + Logger.Info($" Enabled: {enabled}"); } diff --git a/HKMP/Game/Client/Entity/Component/DamageHeroComponent.cs b/HKMP/Game/Client/Entity/Component/DamageHeroComponent.cs index eece8168..b4ef7103 100644 --- a/HKMP/Game/Client/Entity/Component/DamageHeroComponent.cs +++ b/HKMP/Game/Client/Entity/Component/DamageHeroComponent.cs @@ -62,6 +62,7 @@ public override void InitializeHost() { /// public override void Update(EntityNetworkData data) { var damageDealt = data.Packet.ReadByte(); + _damageHero.Host.damageDealt = damageDealt; _damageHero.Client.damageDealt = damageDealt; } diff --git a/HKMP/Game/Client/Entity/Component/HealthManagerComponent.cs b/HKMP/Game/Client/Entity/Component/HealthManagerComponent.cs index 25c88a2e..527eb90c 100644 --- a/HKMP/Game/Client/Entity/Component/HealthManagerComponent.cs +++ b/HKMP/Game/Client/Entity/Component/HealthManagerComponent.cs @@ -161,6 +161,8 @@ public override void Update(EntityNetworkData data) { var newInvincible = data.Packet.ReadBool(); var newInvincibleFromDir = data.Packet.ReadByte(); + _healthManager.Host.IsInvincible = newInvincible; + _healthManager.Host.InvincibleFromDirection = newInvincibleFromDir; _healthManager.Client.IsInvincible = newInvincible; _healthManager.Client.InvincibleFromDirection = newInvincibleFromDir; } diff --git a/HKMP/Game/Client/Entity/Component/MeshRendererComponent.cs b/HKMP/Game/Client/Entity/Component/MeshRendererComponent.cs index 3fa24027..04ac62ee 100644 --- a/HKMP/Game/Client/Entity/Component/MeshRendererComponent.cs +++ b/HKMP/Game/Client/Entity/Component/MeshRendererComponent.cs @@ -65,6 +65,7 @@ public override void InitializeHost() { /// public override void Update(EntityNetworkData data) { var enabled = data.Packet.ReadBool(); + _meshRenderer.Host.enabled = enabled; _meshRenderer.Client.enabled = enabled; } diff --git a/HKMP/Game/Client/Entity/Component/RotationComponent.cs b/HKMP/Game/Client/Entity/Component/RotationComponent.cs index b5ef07d8..818ff3b4 100644 --- a/HKMP/Game/Client/Entity/Component/RotationComponent.cs +++ b/HKMP/Game/Client/Entity/Component/RotationComponent.cs @@ -68,13 +68,18 @@ public override void InitializeHost() { public override void Update(EntityNetworkData data) { var rotation = data.Packet.ReadFloat(); - var transform = GameObject.Client.transform; - var eulerAngles = transform.eulerAngles; - transform.eulerAngles = new Vector3( - eulerAngles.x, - eulerAngles.y, - rotation - ); + SetRotation(GameObject.Host); + SetRotation(GameObject.Client); + + void SetRotation(GameObject obj) { + var transform = obj.transform; + var eulerAngles = transform.eulerAngles; + transform.eulerAngles = new Vector3( + eulerAngles.x, + eulerAngles.y, + rotation + ); + } } /// diff --git a/HKMP/Game/Client/Entity/Component/VelocityComponent.cs b/HKMP/Game/Client/Entity/Component/VelocityComponent.cs new file mode 100644 index 00000000..d91519ec --- /dev/null +++ b/HKMP/Game/Client/Entity/Component/VelocityComponent.cs @@ -0,0 +1,99 @@ +using Hkmp.Networking.Client; +using Hkmp.Networking.Packet.Data; +using Hkmp.Util; +using UnityEngine; +using Logger = Hkmp.Logging.Logger; + +namespace Hkmp.Game.Client.Entity.Component; + +/// +/// This component manages the velocity of the entity. +internal class VelocityComponent : EntityComponent { + /// + /// The host unity component of the entity. + /// + private readonly Rigidbody2D _rigidbody; + + /// + /// The last value of 'velocity' for the rigidbody. + /// + private Vector2 _lastVelocity; + + /// + /// The velocity received from updates to this component. Used to keep track of the velocity as we cannot + /// apply it the rigidbody of our host object as long as the host object is not active. + /// + private Vector2? _receivedVelocity; + + public VelocityComponent( + NetClient netClient, + byte entityId, + HostClientPair gameObject, + Rigidbody2D rigidbody + ) : base(netClient, entityId, gameObject) { + _rigidbody = rigidbody; + _lastVelocity = rigidbody.velocity; + + MonoBehaviourUtil.Instance.OnUpdateEvent += OnUpdate; + } + + /// + /// Callback method to check for mesh renderer updates. + /// + private void OnUpdate() { + if (IsControlled) { + return; + } + + if (GameObject.Host == null) { + return; + } + + if (_receivedVelocity.HasValue && GameObject.Host.activeInHierarchy) { + Logger.Debug($"Velocity component: applied received velocity: {_receivedVelocity.Value.x}, {_receivedVelocity.Value.y}"); + + _rigidbody.velocity = _receivedVelocity.Value; + _receivedVelocity = null; + } + + var newVelocity = _rigidbody.velocity; + Logger.Debug($"Velocity component: {newVelocity.x}, {newVelocity.y}"); + if (newVelocity != _lastVelocity) { + Logger.Debug($" Velocity component changed"); + + _lastVelocity = newVelocity; + + var data = new EntityNetworkData { + Type = EntityNetworkData.DataType.Velocity + }; + data.Packet.Write(newVelocity.x); + data.Packet.Write(newVelocity.y); + + SendData(data); + } + } + + /// + public override void InitializeHost() { + } + + /// + public override void Update(EntityNetworkData data) { + if (!IsControlled) { + return; + } + + var velocity = new Vector2( + data.Packet.ReadFloat(), + data.Packet.ReadFloat() + ); + _receivedVelocity = velocity; + + Logger.Debug($"Velocity component received: {velocity.x}, {velocity.y}, {_rigidbody.GetInstanceID()}"); + } + + /// + public override void Destroy() { + MonoBehaviourUtil.Instance.OnUpdateEvent -= OnUpdate; + } +} \ No newline at end of file diff --git a/HKMP/Game/Client/Entity/Entity.cs b/HKMP/Game/Client/Entity/Entity.cs index 7fa81aa5..af898a69 100644 --- a/HKMP/Game/Client/Entity/Entity.cs +++ b/HKMP/Game/Client/Entity/Entity.cs @@ -124,6 +124,9 @@ GameObject hostObject ) }; Object.Client.SetActive(false); + Object.Client.transform.localScale = Object.Host.transform.lossyScale; + + DisableRegisteredChildren(); // Store whether the host object was active and set it not active until we know if we are scene host _originalIsActive = Object.Host.activeSelf; @@ -200,6 +203,22 @@ GameObject hostObject FindComponents(); } + /// + /// Disable children of the client object that are themselves registered entities. + /// + private void DisableRegisteredChildren() { + for (var i = 0; i < Object.Client.transform.childCount; i++) { + var child = Object.Client.transform.GetChild(i); + var childObj = child.gameObject; + + if (childObj.GetComponents().Any( + fsm => EntityRegistry.TryGetEntry(childObj, fsm.Fsm.Name, out _) + )) { + childObj.SetActive(false); + } + } + } + /// /// Processes the given FSM for the host entity by hooking supported FSM actions. /// @@ -212,6 +231,9 @@ private void ProcessHostFsm(PlayMakerFSM fsm) { for (var j = 0; j < state.Actions.Length; j++) { var action = state.Actions[j]; + if (!action.Enabled) { + continue; + } if (!EntityFsmActions.SupportedActionTypes.Contains(action.GetType())) { continue; @@ -223,7 +245,7 @@ private void ProcessHostFsm(PlayMakerFSM fsm) { StateIndex = i, ActionIndex = j }; - Logger.Info($"Created hooked action: {action.GetType()}, {_fsms.Host.IndexOf(fsm)}, {state.Name}, {j}"); + // Logger.Info($"Created hooked action: {action.GetType()}, {_fsms.Host.IndexOf(fsm)}, {state.Name}, {j}"); if (!_hookedTypes.Contains(action.GetType())) { _hookedTypes.Add(action.GetType()); @@ -354,6 +376,21 @@ private void FindComponents() { meshRenderer ); } + + // Only adding velocity component to False Knight for now, since it doesn't use gravity + if (Type == EntityType.FalseKnight) { + var rigidbody = Object.Host.GetComponent(); + if (rigidbody != null) { + Logger.Info($"Adding Velocity component to entity: {Object.Host.name}"); + + _components[EntityNetworkData.DataType.Velocity] = new VelocityComponent( + _netClient, + _entityId, + Object, + rigidbody + ); + } + } // Find Walker MonoBehaviour and remove it from the client object var walker = Object.Client.GetComponent(); @@ -382,7 +419,7 @@ private void OnActionEntered(FsmStateAction self) { } Logger.Info( - $"Hooked action was entered: {hookedEntityAction.FsmIndex}, {hookedEntityAction.StateIndex}, {hookedEntityAction.ActionIndex}"); + $"Entity ({_entityId}, {Type}) hooked action: {self.Fsm.Name}, {self.State.Name}, {self.GetType()} ({hookedEntityAction.FsmIndex}, {hookedEntityAction.StateIndex}, {hookedEntityAction.ActionIndex})"); var networkData = new EntityNetworkData { Type = EntityNetworkData.DataType.Fsm @@ -446,7 +483,7 @@ private void OnUpdate() { ); } - var newScale = transform.localScale; + var newScale = transform.lossyScale; if (newScale != _lastScale) { _lastScale = newScale; @@ -480,6 +517,8 @@ private void OnUpdate() { data.Types.Add(EntityHostFsmData.Type.State); data.CurrentState = (byte) Array.IndexOf(fsm.FsmStates, fsm.Fsm.ActiveState); + + Logger.Debug($"Entity ({_entityId}, {Type}) host changed states: {lastStateName}, {fsm.ActiveStateName}"); } // Define a method that allows generalization of checking for changes in all FSM variables @@ -504,6 +543,10 @@ Dictionary dataDict if (!value.Equals(lastValue)) { // Update the value in the snapshot since it changed snapshotDict[name] = value; + + if (value is float) { + Logger.Debug($"Entity ({_entityId}, {Type}) FSM changed float: {name}, {value}"); + } data.Types.Add(type); // Some funky casting here to make sure we can use this method with Vector2 and Vector3 @@ -619,7 +662,7 @@ float overrideFps return; } - Logger.Info($"Entity '{Object.Host.name}' sends animation: {clip.name}, {animationId}, {clip.wrapMode}"); + // Logger.Info($"Entity '{Object.Host.name}' sends animation: {clip.name}, {animationId}, {clip.wrapMode}"); _netClient.UpdateManager.UpdateEntityAnimation( _entityId, animationId, @@ -673,8 +716,26 @@ public void MakeHost() { var clientPos = Object.Client.transform.position; Object.Host.transform.position = clientPos; + // Since the scale of the client object is the entire scale we have and the host object scale can be in a + // hierarchy, we need to calculate what the new local scale of the host will be to match the client scale var clientScale = Object.Client.transform.localScale; - Object.Host.transform.localScale = clientScale; + var hostLocalScale = Object.Host.transform.localScale; + var hostLossyScale = Object.Host.transform.lossyScale; + + var hierarchyScaleX = hostLossyScale.x / hostLocalScale.x; + var newScaleX = clientScale.x / hierarchyScaleX; + var hierarchyScaleY = hostLossyScale.y / hostLocalScale.y; + var newScaleY = clientScale.y / hierarchyScaleY; + var hierarchyScaleZ = hostLossyScale.z / hostLocalScale.z; + var newScaleZ = clientScale.z / hierarchyScaleZ; + + Object.Host.transform.localScale = new Vector3(newScaleX, newScaleY, newScaleZ); + + if (_animator.Client != null) { + var clientAnimation = _animator.Client.CurrentClip.name; + var wrapMode = _animator.Client.CurrentClip.wrapMode; + LateUpdateAnimation(_animator.Host, clientAnimation, wrapMode); + } var clientActive = Object.Client.activeSelf; Object.Client.SetActive(false); @@ -697,7 +758,10 @@ public void MakeHost() { var snapshot = _fsmSnapshots[fsmIndex]; foreach (var pair in snapshot.Floats) { + Logger.Debug($" Setting float var: {pair.Key}, {pair.Value}"); fsm.FsmVariables.GetFsmFloat(pair.Key).Value = pair.Value; + + Logger.Debug($" Value of FSM float: {fsm.FsmVariables.GetFsmFloat(pair.Key).Value}"); } foreach (var pair in snapshot.Ints) { fsm.FsmVariables.GetFsmInt(pair.Key).Value = pair.Value; @@ -714,9 +778,10 @@ public void MakeHost() { foreach (var pair in snapshot.Vector3s) { fsm.FsmVariables.GetFsmVector3(pair.Key).Value = pair.Value; } + + Logger.Debug($" Setting FSM state: {snapshot.CurrentState}"); - // Re-init the FSM and set the state as the very last thing in the transfer to kickstart the FSM - fsm.Fsm.Reinitialize(); + // Set the state as the very last thing in the transfer to kickstart the FSM fsm.SetState(snapshot.CurrentState); } } @@ -726,7 +791,11 @@ public void MakeHost() { /// /// The new position. public void UpdatePosition(Vector2 position) { - var unityPos = new Vector3(position.X, position.Y); + var unityPos = new Vector3( + position.X, + position.Y, + Object.Host.transform.position.z + ); if (Object.Client == null) { return; @@ -750,10 +819,20 @@ public void UpdateScale(bool scale) { var currentScaleX = localScale.x; if (currentScaleX > 0 != scale) { + // We use the host scale as reference, specifically the lossy scale as we + // don't have a hierarchy on the client entity + var hostScale = Object.Host.transform.lossyScale; + var hostScaleX = hostScale.x; + + var newScaleX = System.Math.Abs(hostScaleX); + if (!scale) { + newScaleX *= -1; + } + transform.localScale = new Vector3( - currentScaleX * -1, - localScale.y, - localScale.z + newScaleX, + hostScale.y, + hostScale.z ); } } @@ -764,8 +843,11 @@ public void UpdateScale(bool scale) { /// The ID of the animation. /// The wrap mode of the animation clip. /// Whether this update is when entering a new scene. - public void UpdateAnimation(byte animationId, tk2dSpriteAnimationClip.WrapMode wrapMode, - bool alreadyInSceneUpdate) { + public void UpdateAnimation( + byte animationId, + tk2dSpriteAnimationClip.WrapMode wrapMode, + bool alreadyInSceneUpdate + ) { if (_animator.Client == null) { Logger.Warn($"Entity '{Object.Client.name}' received animation while client animator does not exist"); return; @@ -785,35 +867,49 @@ public void UpdateAnimation(byte animationId, tk2dSpriteAnimationClip.WrapMode w if (alreadyInSceneUpdate) { // Since this is an animation update from an entity that was already present in a scene, // we need to determine where to start playing this specific animation - if (wrapMode == tk2dSpriteAnimationClip.WrapMode.Loop) { - _animator.Client.Play(clipName); - return; - } + LateUpdateAnimation(_animator.Client, clipName, wrapMode); + } - var clip = _animator.Client.GetClipByName(clipName); + // Otherwise, default to just playing the clip + _animator.Client.Play(clipName); + } - if (wrapMode == tk2dSpriteAnimationClip.WrapMode.LoopSection) { - // The clip loops in a specific section in the frames, so we start playing - // it from the start of that section - _animator.Client.PlayFromFrame(clipName, clip.loopStart); - return; - } + /// + /// Update the animation for the given animator with the given clip name and wrap mode. This assumes that we need + /// to replicate animation behaviour for a late update. + /// + /// The sprite animator to update. + /// The name of the animation clip. + /// The wrap mode for the animation. + private void LateUpdateAnimation( + tk2dSpriteAnimator animator, + string clipName, + tk2dSpriteAnimationClip.WrapMode wrapMode + ) { + if (wrapMode == tk2dSpriteAnimationClip.WrapMode.Loop) { + animator.Play(clipName); + return; + } - if (wrapMode == tk2dSpriteAnimationClip.WrapMode.Once || - wrapMode == tk2dSpriteAnimationClip.WrapMode.Single) { - // Since the clip was played once, it stops on the last frame, - // so we emulate that by only "playing" the last frame of the clip - var clipLength = clip.frames.Length; - _animator.Client.PlayFromFrame(clipName, clipLength - 1); + var clip = animator.GetClipByName(clipName); - // Logger.Info( - // $" Played animation: {clipName}, {clipLength - 1} on {_animator.Client.name}, {_animator.Client.GetHashCode()}"); - return; - } + if (wrapMode == tk2dSpriteAnimationClip.WrapMode.LoopSection) { + // The clip loops in a specific section in the frames, so we start playing + // it from the start of that section + animator.PlayFromFrame(clipName, clip.loopStart); + return; } - // Otherwise, default to just playing the clip - _animator.Client.Play(clipName); + if (wrapMode == tk2dSpriteAnimationClip.WrapMode.Once || + wrapMode == tk2dSpriteAnimationClip.WrapMode.Single) { + // Since the clip was played once, it stops on the last frame, + // so we emulate that by only "playing" the last frame of the clip + var clipLength = clip.frames.Length; + animator.PlayFromFrame(clipName, clipLength - 1); + + // Logger.Info( + // $" Played animation: {clipName}, {clipLength - 1} on {_animator.Client.name}, {_animator.Client.GetHashCode()}"); + } } /// @@ -821,7 +917,7 @@ public void UpdateAnimation(byte animationId, tk2dSpriteAnimationClip.WrapMode w /// /// The new value for active. public void UpdateIsActive(bool active) { - Logger.Info($"Entity '{Object.Client.name}' received active: {active}"); + // Logger.Info($"Entity '{Object.Client.name}' received active: {active}"); Object.Client.SetActive(active); } @@ -862,7 +958,7 @@ public void UpdateData(List entityNetworkData) { var state = fsm.FsmStates[stateIndex]; var action = state.Actions[actionIndex]; - Logger.Info($"Received entity network data for FSM: {fsm.Fsm.Name}, {state.Name}, {actionIndex} ({action.GetType()})"); + // Logger.Info($"Received entity network data for FSM: {fsm.Fsm.Name}, {state.Name}, {actionIndex} ({action.GetType()})"); EntityFsmActions.ApplyNetworkDataFromAction(data, action); diff --git a/HKMP/Game/Client/Entity/EntityInitializer.cs b/HKMP/Game/Client/Entity/EntityInitializer.cs index 1f890e7b..ca27fb9d 100644 --- a/HKMP/Game/Client/Entity/EntityInitializer.cs +++ b/HKMP/Game/Client/Entity/EntityInitializer.cs @@ -33,6 +33,10 @@ public static void InitializeFsm(PlayMakerFSM fsm) { // Go over each action and try to execute it by applying empty data to it foreach (var action in state.Actions) { + if (!action.Enabled) { + continue; + } + if (EntityFsmActions.SupportedActionTypes.Contains(action.GetType())) { var data = new EntityNetworkData(); EntityFsmActions.ApplyNetworkDataFromAction(data, action); diff --git a/HKMP/Networking/Packet/Data/EntityUpdate.cs b/HKMP/Networking/Packet/Data/EntityUpdate.cs index 346884a3..3c53da95 100644 --- a/HKMP/Networking/Packet/Data/EntityUpdate.cs +++ b/HKMP/Networking/Packet/Data/EntityUpdate.cs @@ -249,7 +249,8 @@ public enum DataType : byte { Rotation, Collider, DamageHero, - MeshRenderer + MeshRenderer, + Velocity } } From c98640e5d3f4685561b4489e3e9bc28815771552 Mon Sep 17 00:00:00 2001 From: Extremelyd1 Date: Mon, 29 May 2023 16:26:13 +0200 Subject: [PATCH 034/216] Remove debug logs --- .../Client/Entity/Action/EntityFsmActions.cs | 41 ------------------- .../Entity/Component/VelocityComponent.cs | 8 ---- HKMP/Game/Client/Entity/Entity.cs | 11 +---- 3 files changed, 2 insertions(+), 58 deletions(-) diff --git a/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs b/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs index a2b39e71..e0710885 100644 --- a/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs +++ b/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs @@ -1130,45 +1130,4 @@ private static void ApplyNetworkDataFromAction(EntityNetworkData data, SendHealt } #endregion - - #region GetVelocity2d - - private static bool GetNetworkDataFromAction(EntityNetworkData data, GetVelocity2d action) { - var obj = action.Fsm.GetOwnerDefaultTarget(action.gameObject); - if (obj == null) { - Logger.Debug("GetVelocity2d no obj"); - return false; - } - - var rigidbody = obj.GetComponent(); - if (rigidbody == null) { - Logger.Debug("GetVelocity2d no rigidbody"); - return false; - } - - var vel = rigidbody.velocity; - - Logger.Debug($"GetVelocity2d: Current velocity: {vel.x}, {vel.y}, {rigidbody.GetInstanceID()}"); - Logger.Debug($"GetVelocity2d: Set value: {action.y.Value}"); - - return false; - } - - private static void ApplyNetworkDataFromAction(EntityNetworkData data, GetVelocity2d action) { - } - - #endregion - - #region FloatMultiplyV2 - - private static bool GetNetworkDataFromAction(EntityNetworkData data, FloatMultiplyV2 action) { - Logger.Debug($"FloatMultiplyV2: New multiplied value: {action.floatVariable.Value}"); - - return false; - } - - private static void ApplyNetworkDataFromAction(EntityNetworkData data, FloatMultiplyV2 action) { - } - - #endregion } diff --git a/HKMP/Game/Client/Entity/Component/VelocityComponent.cs b/HKMP/Game/Client/Entity/Component/VelocityComponent.cs index d91519ec..1c5c9200 100644 --- a/HKMP/Game/Client/Entity/Component/VelocityComponent.cs +++ b/HKMP/Game/Client/Entity/Component/VelocityComponent.cs @@ -2,7 +2,6 @@ using Hkmp.Networking.Packet.Data; using Hkmp.Util; using UnityEngine; -using Logger = Hkmp.Logging.Logger; namespace Hkmp.Game.Client.Entity.Component; @@ -50,17 +49,12 @@ private void OnUpdate() { } if (_receivedVelocity.HasValue && GameObject.Host.activeInHierarchy) { - Logger.Debug($"Velocity component: applied received velocity: {_receivedVelocity.Value.x}, {_receivedVelocity.Value.y}"); - _rigidbody.velocity = _receivedVelocity.Value; _receivedVelocity = null; } var newVelocity = _rigidbody.velocity; - Logger.Debug($"Velocity component: {newVelocity.x}, {newVelocity.y}"); if (newVelocity != _lastVelocity) { - Logger.Debug($" Velocity component changed"); - _lastVelocity = newVelocity; var data = new EntityNetworkData { @@ -88,8 +82,6 @@ public override void Update(EntityNetworkData data) { data.Packet.ReadFloat() ); _receivedVelocity = velocity; - - Logger.Debug($"Velocity component received: {velocity.x}, {velocity.y}, {_rigidbody.GetInstanceID()}"); } /// diff --git a/HKMP/Game/Client/Entity/Entity.cs b/HKMP/Game/Client/Entity/Entity.cs index af898a69..49dfd85c 100644 --- a/HKMP/Game/Client/Entity/Entity.cs +++ b/HKMP/Game/Client/Entity/Entity.cs @@ -544,10 +544,6 @@ Dictionary dataDict // Update the value in the snapshot since it changed snapshotDict[name] = value; - if (value is float) { - Logger.Debug($"Entity ({_entityId}, {Type}) FSM changed float: {name}, {value}"); - } - data.Types.Add(type); // Some funky casting here to make sure we can use this method with Vector2 and Vector3 // Since there is a mismatch between our Hkmp.Math.Vector2 and Unity's Vector2 @@ -662,7 +658,7 @@ float overrideFps return; } - // Logger.Info($"Entity '{Object.Host.name}' sends animation: {clip.name}, {animationId}, {clip.wrapMode}"); + Logger.Info($"Entity '{Object.Host.name}' sends animation: {clip.name}, {animationId}, {clip.wrapMode}"); _netClient.UpdateManager.UpdateEntityAnimation( _entityId, animationId, @@ -758,10 +754,7 @@ public void MakeHost() { var snapshot = _fsmSnapshots[fsmIndex]; foreach (var pair in snapshot.Floats) { - Logger.Debug($" Setting float var: {pair.Key}, {pair.Value}"); fsm.FsmVariables.GetFsmFloat(pair.Key).Value = pair.Value; - - Logger.Debug($" Value of FSM float: {fsm.FsmVariables.GetFsmFloat(pair.Key).Value}"); } foreach (var pair in snapshot.Ints) { fsm.FsmVariables.GetFsmInt(pair.Key).Value = pair.Value; @@ -858,7 +851,7 @@ bool alreadyInSceneUpdate return; } - // Logger.Info($"Entity '{_object.Client.name}' received animation: {animationId}, {clipName}, {wrapMode}"); + // Logger.Info($"Entity '{Object.Client.name}' received animation: {animationId}, {clipName}, {wrapMode}"); // All paths lead to calling the Play method of the sprite animator that is hooked, so we allow the call // through the hook From e354703526932641e760a9780cffe48ebd1a058d Mon Sep 17 00:00:00 2001 From: Extremelyd1 Date: Mon, 29 May 2023 22:05:25 +0200 Subject: [PATCH 035/216] Fix animation issue with host transfer --- HKMP/Game/Client/Entity/Entity.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/HKMP/Game/Client/Entity/Entity.cs b/HKMP/Game/Client/Entity/Entity.cs index 49dfd85c..403ade57 100644 --- a/HKMP/Game/Client/Entity/Entity.cs +++ b/HKMP/Game/Client/Entity/Entity.cs @@ -688,6 +688,9 @@ public void InitializeHost() { } } + // TODO: also track the current action within states of an FSM and don't replay actions that have already happened + // make sure to replay actions that use "everyFrame" + /// /// Makes the entity a host entity if the client user became the scene host. /// @@ -733,6 +736,11 @@ public void MakeHost() { LateUpdateAnimation(_animator.Host, clientAnimation, wrapMode); } + // Make sure that the sprite animator doesn't play the default clip after enabling the object + if (_animator.Host != null) { + _animator.Host.playAutomatically = false; + } + var clientActive = Object.Client.activeSelf; Object.Client.SetActive(false); Object.Host.SetActive(clientActive); From 9ecdf8ada84c8755e9dd1ab0e985d4b226ed33d2 Mon Sep 17 00:00:00 2001 From: Extremelyd1 Date: Sat, 3 Jun 2023 09:31:05 +0200 Subject: [PATCH 036/216] Add action registry for smooth host transfer --- .../Client/Entity/Action/ActionRegistry.cs | 84 ++++++++++ HKMP/Game/Client/Entity/Entity.cs | 32 ++-- HKMP/HKMP.csproj | 1 + HKMP/Resource/action-registry.json | 152 ++++++++++++++++++ 4 files changed, 258 insertions(+), 11 deletions(-) create mode 100644 HKMP/Game/Client/Entity/Action/ActionRegistry.cs create mode 100644 HKMP/Resource/action-registry.json 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/Entity.cs b/HKMP/Game/Client/Entity/Entity.cs index 403ade57..85937f63 100644 --- a/HKMP/Game/Client/Entity/Entity.cs +++ b/HKMP/Game/Client/Entity/Entity.cs @@ -688,9 +688,6 @@ public void InitializeHost() { } } - // TODO: also track the current action within states of an FSM and don't replay actions that have already happened - // make sure to replay actions that use "everyFrame" - /// /// Makes the entity a host entity if the client user became the scene host. /// @@ -730,17 +727,17 @@ public void MakeHost() { Object.Host.transform.localScale = new Vector3(newScaleX, newScaleY, newScaleZ); + // Make sure that the sprite animator doesn't play the default clip after enabling the object + if (_animator.Host != null) { + _animator.Host.playAutomatically = false; + } + if (_animator.Client != null) { var clientAnimation = _animator.Client.CurrentClip.name; var wrapMode = _animator.Client.CurrentClip.wrapMode; LateUpdateAnimation(_animator.Host, clientAnimation, wrapMode); } - // Make sure that the sprite animator doesn't play the default clip after enabling the object - if (_animator.Host != null) { - _animator.Host.playAutomatically = false; - } - var clientActive = Object.Client.activeSelf; Object.Client.SetActive(false); Object.Host.SetActive(clientActive); @@ -781,9 +778,18 @@ public void MakeHost() { } Logger.Debug($" Setting FSM state: {snapshot.CurrentState}"); - - // Set the state as the very last thing in the transfer to kickstart the FSM + + // Before setting the state, we replace the actions of the to-be state to only include the ones that + // should be executed again (including actions with "everyFrame" on true or that continuously check + // collisions for example). + var state = fsm.GetState(snapshot.CurrentState); + var oldActions = state.Actions; + var newActions = oldActions.Where(ActionRegistry.IsActionContinuous).ToArray(); + + // Replace the actions, set the state and reset the actions again + state.Actions = newActions; fsm.SetState(snapshot.CurrentState); + state.Actions = oldActions; } } @@ -919,7 +925,11 @@ tk2dSpriteAnimationClip.WrapMode wrapMode /// The new value for active. public void UpdateIsActive(bool active) { // Logger.Info($"Entity '{Object.Client.name}' received active: {active}"); - Object.Client.SetActive(active); + if (Object.Client != null) { + Object.Client.SetActive(active); + } else { + Logger.Warn($"Entity ({_entityId}, {Type}) could not update active, because client object is null"); + } } /// diff --git a/HKMP/HKMP.csproj b/HKMP/HKMP.csproj index cebed9f7..c930c939 100644 --- a/HKMP/HKMP.csproj +++ b/HKMP/HKMP.csproj @@ -20,6 +20,7 @@ + diff --git a/HKMP/Resource/action-registry.json b/HKMP/Resource/action-registry.json new file mode 100644 index 00000000..90a03771 --- /dev/null +++ b/HKMP/Resource/action-registry.json @@ -0,0 +1,152 @@ +[ + { + "type": "GetPosition", + "update_field": "everyFrame" + }, + { + "type": "GetVelocity2d", + "update_field": "everyFrame" + }, + { + "type": "SetVelocity2d", + "update_field": "everyFrame" + }, + { + "type": "FloatMultiply", + "update_field": "everyFrame" + }, + { + "type": "FloatMultiplyV2", + "update_field": "everyFrame" + }, + { + "type": "SetFloatValue", + "update_field": "everyFrame" + }, + { + "type": "FloatSubtract", + "update_field": "everyFrame" + }, + { + "type": "FloatDivide", + "update_field": "everyFrame" + }, + { + "type": "FloatCompare", + "update_field": "everyFrame" + }, + { + "type": "FloatClamp", + "update_field": "everyFrame" + }, + { + "type": "SetIntValue", + "update_field": "everyFrame" + }, + { + "type": "IntCompare", + "update_field": "everyFrame" + }, + { + "type": "IntAdd", + "update_field": "everyFrame" + }, + { + "type": "IntOperator", + "update_field": "everyFrame" + }, + { + "type": "IntSwitch", + "update_field": "everyFrame" + }, + { + "type": "SetFsmInt", + "update_field": "everyFrame" + }, + { + "type": "GetFsmInt", + "update_field": "everyFrame" + }, + { + "type": "BoolTest", + "update_field": "everyFrame" + }, + { + "type": "SetBoolValue", + "update_field": "everyFrame" + }, + { + "type": "SetFsmBool", + "update_field": "everyFrame" + }, + { + "type": "CheckCollisionSideEnter" + }, + { + "type": "CheckCollisionSide" + }, + { + "type": "SendEventByName", + "update_field": "everyFrame" + }, + { + "type": "RayCast2d", + "update_field": "repeatInterval" + }, + { + "type": "Tk2dWatchAnimationEvents" + }, + { + "type": "Tk2dPlayAnimationWithEvents" + }, + { + "type": "GetDistance", + "update_field": "everyFrame" + }, + { + "type": "CheckTargetDirection", + "update_field": "everyFrame" + }, + { + "type": "BoolTestMulti", + "update_field": "everyFrame" + }, + { + "type": "Wait" + }, + { + "type": "WaitRandom", + "update_field": "everyFrame" + }, + { + "type": "ActivateGameObject", + "update_field": "everyFrame" + }, + { + "type": "SetScale", + "update_field": "everyFrame" + }, + { + "type": "SetParticleEmissionRate", + "update_field": "everyFrame" + }, + { + "type": "SetParticleEmissionSpeed", + "update_field": "everyFrame" + }, + { + "type": "NextFrameEvent" + }, + { + "type": "SetVector2XY", + "update_field": "everyFrame" + }, + { + "type": "SetVector3XYZ", + "update_field": "everyFrame" + }, + { + "type": "SetVector3Value", + "update_field": "everyFrame" + } +] From ca1fe08c3f197dc762d30c1461237ee389874a68 Mon Sep 17 00:00:00 2001 From: Extremelyd1 Date: Sun, 4 Jun 2023 23:04:45 +0200 Subject: [PATCH 037/216] Add support for Brooding Mawlek and fixes for it --- .../Client/Entity/Action/EntityFsmActions.cs | 171 ++++++++++++++++++ .../Entity/Component/ZPositionComponent.cs | 81 +++++++++ HKMP/Game/Client/Entity/Entity.cs | 10 + HKMP/Game/Client/Entity/EntityType.cs | 7 +- HKMP/HKMP.csproj | 4 + HKMP/Networking/Packet/Data/EntityUpdate.cs | 3 +- HKMP/Resource/entity-registry.json | 30 +++ 7 files changed, 304 insertions(+), 2 deletions(-) create mode 100644 HKMP/Game/Client/Entity/Component/ZPositionComponent.cs diff --git a/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs b/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs index e0710885..00af7e2c 100644 --- a/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs +++ b/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs @@ -4,6 +4,8 @@ using Hkmp.Networking.Packet.Data; using HutongGames.PlayMaker; using HutongGames.PlayMaker.Actions; +using Mono.Cecil.Cil; +using MonoMod.Cil; using UnityEngine; using Logger = Hkmp.Logging.Logger; using Object = UnityEngine.Object; @@ -52,6 +54,8 @@ internal static class EntityFsmActions { /// private static readonly Dictionary TypeApplyMethodInfos = new(); + private static readonly Dictionary> RandomActionValues = new(); + /// /// Static constructor that initializes the set and dictionaries by checking all methods in the class. /// @@ -82,6 +86,9 @@ static EntityFsmActions() { throw new Exception("Method was defined that does not adhere to the method naming"); } } + + // Register the IL hook for modifying the FSM action method + IL.HutongGames.PlayMaker.Actions.FlingObjectsFromGlobalPool.OnEnter += FlingObjectsFromGlobalPoolOnEnter; } /// @@ -160,6 +167,49 @@ private static bool IsObjectInRegistry(GameObject gameObject) { return false; } + + /// + /// IL edit method for modifying the + /// method to store the results of the random calls. + /// + private static void FlingObjectsFromGlobalPoolOnEnter(ILContext il) { + try { + // Create a cursor for this context + var c = new ILCursor(il); + + // Emit instructions for Random.Range calls for 1 int and 4 floats + EmitInstructions(); + EmitInstructions(); + EmitInstructions(); + EmitInstructions(); + EmitInstructions(); + + void EmitInstructions() { + // 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 int off the stack (our random value) and + c.EmitDelegate>((value, instance) => { + if (!RandomActionValues.TryGetValue(instance, out var queue)) { + queue = new Queue(); + RandomActionValues[instance] = queue; + } + + queue.Enqueue(value); + + return value; + }); + } + } catch (Exception e) { + Logger.Error($"Could not change FlingObjectFromGlobalPool#OnEnter IL:\n{e}"); + } + } #region SpawnObjectFromGlobalPool @@ -231,6 +281,126 @@ private static void ApplyNetworkDataFromAction(EntityNetworkData data, SpawnObje #endregion + #region FlingObjectsFromGlobalPool + + private static bool GetNetworkDataFromAction(EntityNetworkData data, FlingObjectsFromGlobalPool action) { + var position = Vector3.zero; + + var spawnPoint = action.spawnPoint.Value; + if (spawnPoint != null) { + position = spawnPoint.transform.position; + if (!action.position.IsNone) { + position += action.position.Value; + } + } else if (!action.position.IsNone) { + position = action.position.Value; + } + + if (!RandomActionValues.TryGetValue(action, out var queue)) { + return false; + } + + if (queue.Count == 0) { + Logger.Debug("Getting data for FlingObjectFromGlobalPool has not enough items in queue 1"); + return false; + } + + data.Packet.Write(position.x); + data.Packet.Write(position.y); + data.Packet.Write(position.z); + + var numSpawns = (int) queue.Dequeue(); + data.Packet.Write((byte) numSpawns); + + for (var i = 0; i < numSpawns; i++) { + if (action.originVariationX != null) { + if (queue.Count == 0) { + Logger.Debug("Getting data for FlingObjectFromGlobalPool has not enough items in queue 2"); + return false; + } + + var originVariationX = (float) queue.Dequeue(); + data.Packet.Write(originVariationX); + } + + if (action.originVariationY != null) { + if (queue.Count == 0) { + Logger.Debug("Getting data for FlingObjectFromGlobalPool has not enough items in queue 3"); + return false; + } + + var originVariationY = (float) queue.Dequeue(); + data.Packet.Write(originVariationY); + } + + if (queue.Count < 2) { + Logger.Debug("Getting data for FlingObjectFromGlobalPool has not enough items in queue 4"); + queue.Clear(); + return false; + } + + var speed = (float) queue.Dequeue(); + var angle = (float) queue.Dequeue(); + + data.Packet.Write(speed); + data.Packet.Write(angle); + } + + queue.Clear(); + return true; + } + + private static void ApplyNetworkDataFromAction(EntityNetworkData data, FlingObjectsFromGlobalPool action) { + var position = new Vector3( + data.Packet.ReadFloat(), + data.Packet.ReadFloat(), + data.Packet.ReadFloat() + ); + + var numSpawns = data.Packet.ReadByte(); + for (var i = 0; i < numSpawns; i++) { + var go = action.gameObject.Value.Spawn(position, Quaternion.Euler(Vector3.zero)); + + var originAdjusted = false; + if (action.originVariationX != null) { + var originVariationX = data.Packet.ReadFloat(); + position.x += originVariationX; + + originAdjusted = true; + } + + if (action.originVariationY != null) { + var originVariationY = data.Packet.ReadFloat(); + position.y += originVariationY; + + originAdjusted = true; + } + + if (originAdjusted) { + go.transform.position = position; + } + + var speed = data.Packet.ReadFloat(); + var angle = data.Packet.ReadFloat(); + + var x = speed * Mathf.Cos(angle * ((float) System.Math.PI / 180f)); + var y = speed * Mathf.Sin(angle * ((float) System.Math.PI / 180f)); + + var rigidBody = go.GetComponent(); + if (rigidBody == null) { + return; + } + + rigidBody.velocity = new Vector2(x, y); + + if (!action.FSM.IsNone) { + FSMUtility.LocateFSM(go, action.FSM.Value).SendEvent(action.FSMEvent.Value); + } + } + } + + #endregion + #region CreateObject private static bool GetNetworkDataFromAction(EntityNetworkData data, CreateObject action) { @@ -1054,6 +1224,7 @@ private static void ApplyNetworkDataFromAction(EntityNetworkData data, Tk2dPlayA #region SpawnBlood + // TODO: network speed and angle private static bool GetNetworkDataFromAction(EntityNetworkData data, SpawnBlood action) { if (GlobalPrefabDefaults.Instance == null) { return false; diff --git a/HKMP/Game/Client/Entity/Component/ZPositionComponent.cs b/HKMP/Game/Client/Entity/Component/ZPositionComponent.cs new file mode 100644 index 00000000..8e7249e7 --- /dev/null +++ b/HKMP/Game/Client/Entity/Component/ZPositionComponent.cs @@ -0,0 +1,81 @@ +using Hkmp.Networking.Client; +using Hkmp.Networking.Packet.Data; +using Hkmp.Util; +using UnityEngine; +using Logger = Hkmp.Logging.Logger; + +namespace Hkmp.Game.Client.Entity.Component; + +/// +/// This component manages the Z-position of an entity. +internal class ZPositionComponent : EntityComponent { + /// + /// The last value of the Z position. + /// + private float _lastZ; + + public ZPositionComponent( + NetClient netClient, + byte entityId, + HostClientPair gameObject + ) : base(netClient, entityId, gameObject) { + _lastZ = gameObject.Host.transform.position.z; + + MonoBehaviourUtil.Instance.OnUpdateEvent += OnUpdate; + } + + /// + /// Callback for checking the Z-position each update. + /// + private void OnUpdate() { + if (IsControlled) { + return; + } + + if (GameObject.Host == null) { + return; + } + + var newZ = GameObject.Host.transform.position.z; + if (!_lastZ.Equals(newZ)) { + _lastZ = newZ; + + var data = new EntityNetworkData { + Type = EntityNetworkData.DataType.ZPosition + }; + data.Packet.Write(newZ); + + SendData(data); + } + } + + /// + public override void InitializeHost() { + } + + /// + public override void Update(EntityNetworkData data) { + if (!IsControlled) { + return; + } + + var newZ = data.Packet.ReadFloat(); + + SetZ(GameObject.Host); + SetZ(GameObject.Client); + + void SetZ(GameObject gameObject) { + var position = gameObject.transform.position; + gameObject.transform.position = new Vector3( + position.x, + position.y, + newZ + ); + } + } + + /// + public override void Destroy() { + MonoBehaviourUtil.Instance.OnUpdateEvent -= OnUpdate; + } +} \ No newline at end of file diff --git a/HKMP/Game/Client/Entity/Entity.cs b/HKMP/Game/Client/Entity/Entity.cs index 85937f63..8232cc2b 100644 --- a/HKMP/Game/Client/Entity/Entity.cs +++ b/HKMP/Game/Client/Entity/Entity.cs @@ -391,6 +391,16 @@ private void FindComponents() { ); } } + + if (Type == EntityType.BroodingMawlek) { + Logger.Info($"Adding ZPosition component to entity: {Object.Host.name}"); + + _components[EntityNetworkData.DataType.ZPosition] = new ZPositionComponent( + _netClient, + _entityId, + Object + ); + } // Find Walker MonoBehaviour and remove it from the client object var walker = Object.Client.GetComponent(); diff --git a/HKMP/Game/Client/Entity/EntityType.cs b/HKMP/Game/Client/Entity/EntityType.cs index 0575763c..2524a952 100644 --- a/HKMP/Game/Client/Entity/EntityType.cs +++ b/HKMP/Game/Client/Entity/EntityType.cs @@ -24,5 +24,10 @@ internal enum EntityType { FalseKnight, FalseKnightHead, FalseKnightBarrels, - FalseKnightFloor + FalseKnightFloor, + GruzMother, + BroodingMawlek, + BroodingMawlekArm, + BroodingMawlekHead, + BroodingMawlekDummy } diff --git a/HKMP/HKMP.csproj b/HKMP/HKMP.csproj index c930c939..b30b825a 100644 --- a/HKMP/HKMP.csproj +++ b/HKMP/HKMP.csproj @@ -40,6 +40,10 @@ $(References)\MonoMod.RuntimeDetour.dll False + + $(References)\Mono.Cecil.dll + False + ..\HKMPServer\Lib\Newtonsoft.Json.dll False diff --git a/HKMP/Networking/Packet/Data/EntityUpdate.cs b/HKMP/Networking/Packet/Data/EntityUpdate.cs index 3c53da95..d09aa7d2 100644 --- a/HKMP/Networking/Packet/Data/EntityUpdate.cs +++ b/HKMP/Networking/Packet/Data/EntityUpdate.cs @@ -250,7 +250,8 @@ public enum DataType : byte { Collider, DamageHero, MeshRenderer, - Velocity + Velocity, + ZPosition } } diff --git a/HKMP/Resource/entity-registry.json b/HKMP/Resource/entity-registry.json index 59ba34c9..2a73269e 100644 --- a/HKMP/Resource/entity-registry.json +++ b/HKMP/Resource/entity-registry.json @@ -128,5 +128,35 @@ "base_object_name": "Break Floor", "type": "FalseKnightFloor", "parent_name": "FK Floor" + }, + { + "base_object_name": "Giant Fly", + "type": "GruzMother", + "fsm_name": "Big Fly Control" + }, + { + "base_object_name": "Mawlek Body", + "type": "BroodingMawlek", + "fsm_name": "Mawlek Control" + }, + { + "base_object_name": "Mawlek Arm L", + "type": "BroodingMawlekArm", + "fsm_name": "Mawlek Arm Control" + }, + { + "base_object_name": "Mawlek Arm R", + "type": "BroodingMawlekArm", + "fsm_name": "Mawlek Arm Control" + }, + { + "base_object_name": "Mawlek Head", + "type": "BroodingMawlekHead", + "fsm_name": "Mawlek Head" + }, + { + "base_object_name": "Dummy", + "type": "BroodingMawlekDummy", + "parent_name": "Mawlek Body" } ] From 8fdc5b006dca9e7ed220129083446221f7a93cab Mon Sep 17 00:00:00 2001 From: Extremelyd1 Date: Mon, 5 Jun 2023 21:50:57 +0200 Subject: [PATCH 038/216] Disable registered entities from becoming active --- .../Client/Entity/Action/EntityFsmActions.cs | 62 ++++++++++++------- HKMP/Game/Client/Entity/Entity.cs | 37 +++++++++-- 2 files changed, 72 insertions(+), 27 deletions(-) diff --git a/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs b/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs index 00af7e2c..67063770 100644 --- a/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs +++ b/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs @@ -165,6 +165,13 @@ private static bool IsObjectInRegistry(GameObject gameObject) { } } + var parent = gameObject.transform.parent; + if (parent != null) { + if (EntityRegistry.TryGetEntryWithParent(gameObject.name, parent.name, out _)) { + return true; + } + } + return false; } @@ -321,6 +328,8 @@ private static bool GetNetworkDataFromAction(EntityNetworkData data, FlingObject var originVariationX = (float) queue.Dequeue(); data.Packet.Write(originVariationX); + } else { + data.Packet.Write(0f); } if (action.originVariationY != null) { @@ -331,6 +340,8 @@ private static bool GetNetworkDataFromAction(EntityNetworkData data, FlingObject var originVariationY = (float) queue.Dequeue(); data.Packet.Write(originVariationY); + } else { + data.Packet.Write(0f); } if (queue.Count < 2) { @@ -361,24 +372,13 @@ private static void ApplyNetworkDataFromAction(EntityNetworkData data, FlingObje for (var i = 0; i < numSpawns; i++) { var go = action.gameObject.Value.Spawn(position, Quaternion.Euler(Vector3.zero)); - var originAdjusted = false; - if (action.originVariationX != null) { - var originVariationX = data.Packet.ReadFloat(); - position.x += originVariationX; + var originVariationX = data.Packet.ReadFloat(); + position.x += originVariationX; - originAdjusted = true; - } + var originVariationY = data.Packet.ReadFloat(); + position.y += originVariationY; - if (action.originVariationY != null) { - var originVariationY = data.Packet.ReadFloat(); - position.y += originVariationY; - - originAdjusted = true; - } - - if (originAdjusted) { - go.transform.position = position; - } + go.transform.position = position; var speed = data.Packet.ReadFloat(); var angle = data.Packet.ReadFloat(); @@ -1224,16 +1224,32 @@ private static void ApplyNetworkDataFromAction(EntityNetworkData data, Tk2dPlayA #region SpawnBlood - // TODO: network speed and angle private static bool GetNetworkDataFromAction(EntityNetworkData data, SpawnBlood action) { if (GlobalPrefabDefaults.Instance == null) { return false; } + + data.Packet.Write((short) action.spawnMin.Value); + data.Packet.Write((short) action.spawnMax.Value); + + data.Packet.Write(action.speedMin.Value); + data.Packet.Write(action.speedMax.Value); + data.Packet.Write(action.angleMin.Value); + data.Packet.Write(action.angleMax.Value); return true; } private static void ApplyNetworkDataFromAction(EntityNetworkData data, SpawnBlood action) { + var spawnMin = data.Packet.ReadShort(); + var spawnMax = data.Packet.ReadShort(); + + var speedMin = data.Packet.ReadFloat(); + var speedMax = data.Packet.ReadFloat(); + + var angleMin = data.Packet.ReadFloat(); + var angleMax = data.Packet.ReadFloat(); + var position = action.position.Value; if (action.spawnPoint.Value != null) { position += action.spawnPoint.Value.transform.position; @@ -1241,12 +1257,12 @@ private static void ApplyNetworkDataFromAction(EntityNetworkData data, SpawnBloo GlobalPrefabDefaults.Instance.SpawnBlood( position, - (short) action.spawnMin.Value, - (short) action.spawnMax.Value, - action.speedMin.Value, - action.speedMax.Value, - action.angleMin.Value, - action.angleMax.Value, + spawnMin, + spawnMax, + speedMin, + speedMax, + angleMin, + angleMax, action.colorOverride.IsNone ? new Color?() : action.colorOverride.Value ); } diff --git a/HKMP/Game/Client/Entity/Entity.cs b/HKMP/Game/Client/Entity/Entity.cs index 8232cc2b..52aa079c 100644 --- a/HKMP/Game/Client/Entity/Entity.cs +++ b/HKMP/Game/Client/Entity/Entity.cs @@ -102,6 +102,11 @@ internal class Entity { /// private readonly List _fsmSnapshots; + /// + /// List of children that should stay disabled because they are handled by their own separate entity. + /// + private readonly List _disabledChildren; + public Entity( NetClient netClient, byte entityId, @@ -126,6 +131,7 @@ GameObject hostObject Object.Client.SetActive(false); Object.Client.transform.localScale = Object.Host.transform.lossyScale; + _disabledChildren = new List(); DisableRegisteredChildren(); // Store whether the host object was active and set it not active until we know if we are scene host @@ -207,14 +213,29 @@ GameObject hostObject /// Disable children of the client object that are themselves registered entities. /// private void DisableRegisteredChildren() { + var objName = Object.Client.name; + for (var i = 0; i < Object.Client.transform.childCount; i++) { var child = Object.Client.transform.GetChild(i); var childObj = child.gameObject; - - if (childObj.GetComponents().Any( - fsm => EntityRegistry.TryGetEntry(childObj, fsm.Fsm.Name, out _) - )) { + var childName = childObj.name; + + if (childObj.GetComponents().Any(fsm => + EntityRegistry.TryGetEntry( + childObj, + fsm.Fsm.Name, + out _ + ) || + EntityRegistry.TryGetEntryWithParent( + childName, + objName, + out _ + ) + )) { + Logger.Debug($"Found registered child '{childName}' of entity '{objName}', disabling and adding to list"); childObj.SetActive(false); + + _disabledChildren.Add(childObj); } } } @@ -477,6 +498,14 @@ private void OnUpdate() { Logger.Info($"Entity '{Object.Host.name}' host object became active, re-disabling"); Object.Host.SetActive(false); } + + foreach (var childObj in _disabledChildren) { + if (childObj.activeSelf) { + Logger.Info($"Child object '{childObj.name}' of entity '{Object.Host.name}' became active, re-disabling"); + + childObj.SetActive(false); + } + } return; } From f35ef6907824562a13e6d744a538691dad9f369f Mon Sep 17 00:00:00 2001 From: Extremelyd1 Date: Fri, 9 Jun 2023 15:57:01 +0200 Subject: [PATCH 039/216] Add gravity scale entity component --- .../Client/Entity/Action/EntityFsmActions.cs | 35 ++++++++ .../Entity/Component/GravityScaleComponent.cs | 86 +++++++++++++++++++ .../Entity/Component/VelocityComponent.cs | 4 +- HKMP/Game/Client/Entity/Entity.cs | 31 ++++++- HKMP/Networking/Packet/Data/EntityUpdate.cs | 1 + 5 files changed, 154 insertions(+), 3 deletions(-) create mode 100644 HKMP/Game/Client/Entity/Component/GravityScaleComponent.cs diff --git a/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs b/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs index 67063770..d13c155f 100644 --- a/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs +++ b/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs @@ -4,6 +4,7 @@ using Hkmp.Networking.Packet.Data; using HutongGames.PlayMaker; using HutongGames.PlayMaker.Actions; +using Modding; using Mono.Cecil.Cil; using MonoMod.Cil; using UnityEngine; @@ -835,6 +836,28 @@ private static void ApplyNetworkDataFromAction(EntityNetworkData data, GetHero a #endregion + #region GetChild + + private static bool GetNetworkDataFromAction(EntityNetworkData data, GetChild action) { + return true; + } + + private static void ApplyNetworkDataFromAction(EntityNetworkData data, GetChild action) { + var gameObject = action.Fsm.GetOwnerDefaultTarget(action.gameObject); + + var result = ReflectionHelper.CallMethod( + typeof(GetChild), + "DoGetChildByName", + gameObject, + action.childName.Value, + action.withTag.Value + ); + + action.storeResult.Value = result; + } + + #endregion + #region FindChild private static bool GetNetworkDataFromAction(EntityNetworkData data, FindChild action) { @@ -882,6 +905,18 @@ private static void ApplyNetworkDataFromAction(EntityNetworkData data, FindGameO #endregion + #region SetProperty + + private static bool GetNetworkDataFromAction(EntityNetworkData data, SetProperty action) { + return true; + } + + private static void ApplyNetworkDataFromAction(EntityNetworkData data, SetProperty action) { + action.targetProperty.SetValue(); + } + + #endregion + #region FindAlertRange private static bool GetNetworkDataFromAction(EntityNetworkData data, FindAlertRange action) { diff --git a/HKMP/Game/Client/Entity/Component/GravityScaleComponent.cs b/HKMP/Game/Client/Entity/Component/GravityScaleComponent.cs new file mode 100644 index 00000000..0cbfd547 --- /dev/null +++ b/HKMP/Game/Client/Entity/Component/GravityScaleComponent.cs @@ -0,0 +1,86 @@ +using Hkmp.Networking.Client; +using Hkmp.Networking.Packet.Data; +using Hkmp.Util; +using UnityEngine; + +namespace Hkmp.Game.Client.Entity.Component; + +/// +/// This component manages the gravity scale of an entity. +internal class GravityScaleComponent : EntityComponent { + /// + /// The host unity component of the entity. + /// + private readonly Rigidbody2D _rigidbody; + + /// + /// The last value of the gravity scale. + /// + private float _lastScale; + + /// + /// The gravity scale received from updates to this component. Used to keep track of the gravity scale as we + /// cannot apply it the rigidbody of our host object as long as the host object is not active. + /// + private float? _receivedGravityScale; + + public GravityScaleComponent( + NetClient netClient, + byte entityId, + HostClientPair gameObject, + Rigidbody2D rigidbody + ) : base(netClient, entityId, gameObject) { + _rigidbody = rigidbody; + _lastScale = rigidbody.gravityScale; + + MonoBehaviourUtil.Instance.OnUpdateEvent += OnUpdate; + } + + /// + /// Callback for checking the gravity scale each update. + /// + private void OnUpdate() { + if (IsControlled) { + return; + } + + if (GameObject.Host == null) { + return; + } + + if (_receivedGravityScale.HasValue && GameObject.Host.activeInHierarchy) { + _rigidbody.gravityScale = _receivedGravityScale.Value; + _receivedGravityScale = null; + } + + var newGravityScale = _rigidbody.gravityScale; + if (!newGravityScale.Equals(_lastScale)) { + _lastScale = newGravityScale; + + var data = new EntityNetworkData { + Type = EntityNetworkData.DataType.GravityScale + }; + data.Packet.Write(newGravityScale); + + SendData(data); + } + } + + /// + public override void InitializeHost() { + } + + /// + public override void Update(EntityNetworkData data) { + if (!IsControlled) { + return; + } + + _receivedGravityScale = data.Packet.ReadFloat(); + } + + /// + public override void Destroy() { + MonoBehaviourUtil.Instance.OnUpdateEvent -= OnUpdate; + } +} \ No newline at end of file diff --git a/HKMP/Game/Client/Entity/Component/VelocityComponent.cs b/HKMP/Game/Client/Entity/Component/VelocityComponent.cs index 1c5c9200..59d49016 100644 --- a/HKMP/Game/Client/Entity/Component/VelocityComponent.cs +++ b/HKMP/Game/Client/Entity/Component/VelocityComponent.cs @@ -37,7 +37,7 @@ Rigidbody2D rigidbody } /// - /// Callback method to check for mesh renderer updates. + /// Callback method to check for updates. /// private void OnUpdate() { if (IsControlled) { @@ -55,7 +55,7 @@ private void OnUpdate() { var newVelocity = _rigidbody.velocity; if (newVelocity != _lastVelocity) { - _lastVelocity = newVelocity; + _lastVelocity = newVelocity; var data = new EntityNetworkData { Type = EntityNetworkData.DataType.Velocity diff --git a/HKMP/Game/Client/Entity/Entity.cs b/HKMP/Game/Client/Entity/Entity.cs index 52aa079c..d0f20bb8 100644 --- a/HKMP/Game/Client/Entity/Entity.cs +++ b/HKMP/Game/Client/Entity/Entity.cs @@ -207,6 +207,21 @@ GameObject hostObject _components = new Dictionary(); FindComponents(); + + // // Debug code that logs each action's OnEnter method call + // foreach (var fsm in _fsms.Host) { + // foreach (var state in fsm.FsmStates) { + // foreach (var action in state.Actions) { + // FsmActionHooks.RegisterFsmStateActionType(action.GetType(), stateAction => { + // if (stateAction != action) { + // return; + // } + // + // Logger.Debug($"Entity ({_entityId}, {Type}) has host FSM enter action: {state.Name}, {action.GetType()}, {state.Actions.ToList().IndexOf(action)}"); + // }); + // } + // } + // } } /// @@ -398,6 +413,8 @@ private void FindComponents() { ); } + // TODO: if this gets out of hand with a lot of specifics for a lot of types, perhaps move it to a config file + // Only adding velocity component to False Knight for now, since it doesn't use gravity if (Type == EntityType.FalseKnight) { var rigidbody = Object.Host.GetComponent(); @@ -413,14 +430,25 @@ private void FindComponents() { } } + // Adding a few components specifically for BroodingMawlek if (Type == EntityType.BroodingMawlek) { Logger.Info($"Adding ZPosition component to entity: {Object.Host.name}"); - _components[EntityNetworkData.DataType.ZPosition] = new ZPositionComponent( _netClient, _entityId, Object ); + + var rigidbody = Object.Host.GetComponent(); + if (rigidbody != null) { + Logger.Info($"Adding GravityScale component to entity: {Object.Host.name}"); + _components[EntityNetworkData.DataType.GravityScale] = new GravityScaleComponent( + _netClient, + _entityId, + Object, + rigidbody + ); + } } // Find Walker MonoBehaviour and remove it from the client object @@ -748,6 +776,7 @@ public void MakeHost() { return; } + // TODO: employ a similar strategy for positions as is done with scale var clientPos = Object.Client.transform.position; Object.Host.transform.position = clientPos; diff --git a/HKMP/Networking/Packet/Data/EntityUpdate.cs b/HKMP/Networking/Packet/Data/EntityUpdate.cs index d09aa7d2..987e1c17 100644 --- a/HKMP/Networking/Packet/Data/EntityUpdate.cs +++ b/HKMP/Networking/Packet/Data/EntityUpdate.cs @@ -251,6 +251,7 @@ public enum DataType : byte { DamageHero, MeshRenderer, Velocity, + GravityScale, ZPosition } } From 476d59b825cb73d6708f7ecd9dc01a68fe3a1b06 Mon Sep 17 00:00:00 2001 From: Extremelyd1 Date: Sun, 18 Jun 2023 14:19:03 +0200 Subject: [PATCH 040/216] Refactor entity detection to respect object hierarchy --- HKMP/Fsm/PositionInterpolation.cs | 10 +- .../Client/Entity/Action/EntityFsmActions.cs | 15 +- HKMP/Game/Client/Entity/Entity.cs | 235 ++++++++--------- HKMP/Game/Client/Entity/EntityInitializer.cs | 13 + HKMP/Game/Client/Entity/EntityManager.cs | 238 +++++------------- HKMP/Game/Client/Entity/EntityProcessor.cs | 188 ++++++++++++++ HKMP/Game/Client/Entity/EntityRegistry.cs | 129 +++++----- HKMP/Resource/entity-registry.json | 60 ++--- HKMP/Util/GameObjectExtensions.cs | 10 + 9 files changed, 486 insertions(+), 412 deletions(-) create mode 100644 HKMP/Game/Client/Entity/EntityProcessor.cs 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/Entity/Action/EntityFsmActions.cs b/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs index d13c155f..0969daeb 100644 --- a/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs +++ b/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs @@ -160,20 +160,7 @@ public static void ApplyNetworkDataFromAction(EntityNetworkData data, FsmStateAc /// 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) { - foreach (var fsm in gameObject.GetComponents()) { - if (EntityRegistry.TryGetEntry(fsm.gameObject, fsm.Fsm.Name, out _)) { - return true; - } - } - - var parent = gameObject.transform.parent; - if (parent != null) { - if (EntityRegistry.TryGetEntryWithParent(gameObject.name, parent.name, out _)) { - return true; - } - } - - return false; + return EntityRegistry.TryGetEntry(gameObject, out _); } /// diff --git a/HKMP/Game/Client/Entity/Entity.cs b/HKMP/Game/Client/Entity/Entity.cs index d0f20bb8..c93dbb62 100644 --- a/HKMP/Game/Client/Entity/Entity.cs +++ b/HKMP/Game/Client/Entity/Entity.cs @@ -24,10 +24,16 @@ internal class Entity { /// The net client for networking. /// private readonly NetClient _netClient; + + /// + /// Whether the entity has a parent entity. + /// + private readonly bool _hasParent; + /// /// The ID of the entity. /// - private readonly byte _entityId; + public byte Id { get; } /// /// The type of the entity. @@ -102,43 +108,48 @@ internal class Entity { /// private readonly List _fsmSnapshots; - /// - /// List of children that should stay disabled because they are handled by their own separate entity. - /// - private readonly List _disabledChildren; - public Entity( NetClient netClient, - byte entityId, + byte id, EntityType type, - GameObject hostObject + GameObject hostObject, + GameObject clientObject = null ) { _netClient = netClient; - _entityId = entityId; + Id = id; Type = type; _isControlled = true; - Object = new HostClientPair { - Host = hostObject, - Client = UnityEngine.Object.Instantiate( - hostObject, - hostObject.transform.position, - hostObject.transform.rotation - ) - }; - Object.Client.SetActive(false); - Object.Client.transform.localScale = Object.Host.transform.lossyScale; + if (clientObject == null) { + Object = new HostClientPair { + Host = hostObject, + Client = UnityEngine.Object.Instantiate( + hostObject, + hostObject.transform.position, + hostObject.transform.rotation + ) + }; + + _hasParent = false; + } else { + Object = new HostClientPair { + Host = hostObject, + Client = clientObject + }; + + _hasParent = true; + } - _disabledChildren = new List(); - DisableRegisteredChildren(); + Object.Client.transform.localScale = _hasParent + ? Object.Host.transform.localScale + : Object.Host.transform.lossyScale; // Store whether the host object was active and set it not active until we know if we are scene host _originalIsActive = Object.Host.activeSelf; - Object.Host.SetActive(false); - _lastIsActive = Object.Host.activeInHierarchy; + _lastIsActive = _hasParent ? Object.Host.activeSelf : Object.Host.activeInHierarchy; Logger.Info( $"Entity '{Object.Host.name}' was original active: {_originalIsActive}, last active: {_lastIsActive}"); @@ -176,7 +187,7 @@ GameObject hostObject // Always disallow the client object from being recycled, because it will simply be destroyed On.ObjectPool.Recycle_GameObject += (orig, obj) => { if (obj == Object.Client) { - Logger.Debug($"Client object of entity: {_entityId}, {type} tried to be recycled"); + Logger.Debug($"Client object of entity: {Id}, {type} tried to be recycled"); return; } @@ -207,6 +218,9 @@ GameObject hostObject _components = new Dictionary(); FindComponents(); + + Object.Host.SetActive(false); + Object.Client.SetActive(false); // // Debug code that logs each action's OnEnter method call // foreach (var fsm in _fsms.Host) { @@ -217,44 +231,13 @@ GameObject hostObject // return; // } // - // Logger.Debug($"Entity ({_entityId}, {Type}) has host FSM enter action: {state.Name}, {action.GetType()}, {state.Actions.ToList().IndexOf(action)}"); + // Logger.Debug($"Entity ({Id}, {Type}) has host FSM enter action: {state.Name}, {action.GetType()}, {state.Actions.ToList().IndexOf(action)}"); // }); // } // } // } } - /// - /// Disable children of the client object that are themselves registered entities. - /// - private void DisableRegisteredChildren() { - var objName = Object.Client.name; - - for (var i = 0; i < Object.Client.transform.childCount; i++) { - var child = Object.Client.transform.GetChild(i); - var childObj = child.gameObject; - var childName = childObj.name; - - if (childObj.GetComponents().Any(fsm => - EntityRegistry.TryGetEntry( - childObj, - fsm.Fsm.Name, - out _ - ) || - EntityRegistry.TryGetEntryWithParent( - childName, - objName, - out _ - ) - )) { - Logger.Debug($"Found registered child '{childName}' of entity '{objName}', disabling and adding to list"); - childObj.SetActive(false); - - _disabledChildren.Add(childObj); - } - } - } - /// /// Processes the given FSM for the host entity by hooking supported FSM actions. /// @@ -341,7 +324,7 @@ private void FindComponents() { var hmComponent = new HealthManagerComponent( _netClient, - _entityId, + Id, Object, healthManager ); @@ -353,7 +336,7 @@ private void FindComponents() { if (climber != null) { _components[EntityNetworkData.DataType.Rotation] = new RotationComponent( _netClient, - _entityId, + Id, Object, climber ); @@ -371,7 +354,7 @@ private void FindComponents() { _components[EntityNetworkData.DataType.Collider] = new ColliderComponent( _netClient, - _entityId, + Id, Object, collider ); @@ -389,7 +372,7 @@ private void FindComponents() { _components[EntityNetworkData.DataType.DamageHero] = new DamageHeroComponent( _netClient, - _entityId, + Id, Object, damageHero ); @@ -407,7 +390,7 @@ private void FindComponents() { _components[EntityNetworkData.DataType.MeshRenderer] = new MeshRendererComponent( _netClient, - _entityId, + Id, Object, meshRenderer ); @@ -423,7 +406,7 @@ private void FindComponents() { _components[EntityNetworkData.DataType.Velocity] = new VelocityComponent( _netClient, - _entityId, + Id, Object, rigidbody ); @@ -435,7 +418,7 @@ private void FindComponents() { Logger.Info($"Adding ZPosition component to entity: {Object.Host.name}"); _components[EntityNetworkData.DataType.ZPosition] = new ZPositionComponent( _netClient, - _entityId, + Id, Object ); @@ -444,7 +427,7 @@ private void FindComponents() { Logger.Info($"Adding GravityScale component to entity: {Object.Host.name}"); _components[EntityNetworkData.DataType.GravityScale] = new GravityScaleComponent( _netClient, - _entityId, + Id, Object, rigidbody ); @@ -478,7 +461,7 @@ private void OnActionEntered(FsmStateAction self) { } Logger.Info( - $"Entity ({_entityId}, {Type}) hooked action: {self.Fsm.Name}, {self.State.Name}, {self.GetType()} ({hookedEntityAction.FsmIndex}, {hookedEntityAction.StateIndex}, {hookedEntityAction.ActionIndex})"); + $"Entity ({Id}, {Type}) hooked action: {self.Fsm.Name}, {self.State.Name}, {self.GetType()} ({hookedEntityAction.FsmIndex}, {hookedEntityAction.StateIndex}, {hookedEntityAction.ActionIndex})"); var networkData = new EntityNetworkData { Type = EntityNetworkData.DataType.Fsm @@ -494,7 +477,7 @@ private void OnActionEntered(FsmStateAction self) { // Only if the GetNetworkDataFromAction method returns true do we add the entity data // for sending if (EntityFsmActions.GetNetworkDataFromAction(networkData, self)) { - _netClient.UpdateManager.AddEntityData(_entityId, networkData); + _netClient.UpdateManager.AddEntityData(Id, networkData); } } @@ -511,7 +494,7 @@ private void OnUpdate() { _lastIsActive = false; _netClient.UpdateManager.UpdateEntityIsActive( - _entityId, + Id, false ); } @@ -526,48 +509,40 @@ private void OnUpdate() { Logger.Info($"Entity '{Object.Host.name}' host object became active, re-disabling"); Object.Host.SetActive(false); } - - foreach (var childObj in _disabledChildren) { - if (childObj.activeSelf) { - Logger.Info($"Child object '{childObj.name}' of entity '{Object.Host.name}' became active, re-disabling"); - - childObj.SetActive(false); - } - } return; } var transform = Object.Host.transform; - var newPosition = transform.position; + var newPosition = _hasParent ? transform.localPosition : transform.position; if (newPosition != _lastPosition) { _lastPosition = newPosition; _netClient.UpdateManager.UpdateEntityPosition( - _entityId, + Id, new Vector2(newPosition.x, newPosition.y) ); } - var newScale = transform.lossyScale; + var newScale = _hasParent ? transform.localScale : transform.lossyScale; if (newScale != _lastScale) { _lastScale = newScale; _netClient.UpdateManager.UpdateEntityScale( - _entityId, + Id, newScale.x > 0 ); } - var newActive = Object.Host.activeInHierarchy; + var newActive = _hasParent ? Object.Host.activeSelf : Object.Host.activeInHierarchy; if (newActive != _lastIsActive) { _lastIsActive = newActive; Logger.Info($"Entity '{Object.Host.name}' changed active: {newActive}"); _netClient.UpdateManager.UpdateEntityIsActive( - _entityId, + Id, newActive ); } @@ -585,7 +560,7 @@ private void OnUpdate() { data.Types.Add(EntityHostFsmData.Type.State); data.CurrentState = (byte) Array.IndexOf(fsm.FsmStates, fsm.Fsm.ActiveState); - Logger.Debug($"Entity ({_entityId}, {Type}) host changed states: {lastStateName}, {fsm.ActiveStateName}"); + Logger.Debug($"Entity ({Id}, {Type}) host changed states: {lastStateName}, {fsm.ActiveStateName}"); } // Define a method that allows generalization of checking for changes in all FSM variables @@ -676,7 +651,7 @@ Dictionary dataDict ); if (data.Types.Count > 0) { - _netClient.UpdateManager.AddEntityHostFsmData(_entityId, fsmIndex, data); + _netClient.UpdateManager.AddEntityHostFsmData(Id, fsmIndex, data); } } } @@ -727,7 +702,7 @@ float overrideFps Logger.Info($"Entity '{Object.Host.name}' sends animation: {clip.name}, {animationId}, {clip.wrapMode}"); _netClient.UpdateManager.UpdateEntityAnimation( - _entityId, + Id, animationId, (byte)clip.wrapMode ); @@ -741,12 +716,12 @@ public void InitializeHost() { // Also update the last active variable to account for this potential change // Otherwise we might trigger the update sending of activity twice - _lastIsActive = Object.Host.activeInHierarchy; + _lastIsActive = _hasParent ? Object.Host.activeSelf : Object.Host.activeInHierarchy; Logger.Info( $"Initializing entity '{Object.Host.name}' with active: {_originalIsActive}, sending active: {_lastIsActive}"); - _netClient.UpdateManager.UpdateEntityIsActive(_entityId, _lastIsActive); + _netClient.UpdateManager.UpdateEntityIsActive(Id, _lastIsActive); _isControlled = false; @@ -759,7 +734,7 @@ public void InitializeHost() { /// Makes the entity a host entity if the client user became the scene host. /// public void MakeHost() { - Logger.Info($"Making entity ({_entityId}, {Type}) a host entity"); + Logger.Info($"Making entity ({Id}, {Type}) a host entity"); // If the client object is null, we don't have to care about doing anything for the host object anymore if (Object.Client == null) { @@ -776,41 +751,48 @@ public void MakeHost() { return; } - // TODO: employ a similar strategy for positions as is done with scale - var clientPos = Object.Client.transform.position; - Object.Host.transform.position = clientPos; - - // Since the scale of the client object is the entire scale we have and the host object scale can be in a - // hierarchy, we need to calculate what the new local scale of the host will be to match the client scale - var clientScale = Object.Client.transform.localScale; - var hostLocalScale = Object.Host.transform.localScale; - var hostLossyScale = Object.Host.transform.lossyScale; - - var hierarchyScaleX = hostLossyScale.x / hostLocalScale.x; - var newScaleX = clientScale.x / hierarchyScaleX; - var hierarchyScaleY = hostLossyScale.y / hostLocalScale.y; - var newScaleY = clientScale.y / hierarchyScaleY; - var hierarchyScaleZ = hostLossyScale.z / hostLocalScale.z; - var newScaleZ = clientScale.z / hierarchyScaleZ; + if (_hasParent) { + Object.Host.transform.localPosition = _lastPosition = Object.Client.transform.localPosition; + Object.Host.transform.localScale = _lastScale = Object.Client.transform.localScale; + } else { + var clientPos = Object.Client.transform.localPosition; + var parentPos = Vector3.zero; + if (Object.Host.transform.parent != null) { + parentPos = Object.Host.transform.parent.position; + } + + var newPosX = clientPos.x - parentPos.x; + var newPosY = clientPos.y - parentPos.y; + var newPosZ = clientPos.z - parentPos.z; + + Object.Host.transform.localPosition = _lastPosition = new Vector3(newPosX, newPosY, newPosZ); + + // Since the scale of the client object is the entire scale we have and the host object scale can be in a + // hierarchy, we need to calculate what the new local scale of the host will be to match the client scale + var clientScale = Object.Client.transform.localScale; + var hostLocalScale = Object.Host.transform.localScale; + var hostLossyScale = Object.Host.transform.lossyScale; + + var hierarchyScaleX = hostLossyScale.x / hostLocalScale.x; + var newScaleX = clientScale.x / hierarchyScaleX; + var hierarchyScaleY = hostLossyScale.y / hostLocalScale.y; + var newScaleY = clientScale.y / hierarchyScaleY; + var hierarchyScaleZ = hostLossyScale.z / hostLocalScale.z; + var newScaleZ = clientScale.z / hierarchyScaleZ; - Object.Host.transform.localScale = new Vector3(newScaleX, newScaleY, newScaleZ); + Object.Host.transform.localScale = _lastScale = new Vector3(newScaleX, newScaleY, newScaleZ); + } // Make sure that the sprite animator doesn't play the default clip after enabling the object if (_animator.Host != null) { _animator.Host.playAutomatically = false; } - - if (_animator.Client != null) { - var clientAnimation = _animator.Client.CurrentClip.name; - var wrapMode = _animator.Client.CurrentClip.wrapMode; - LateUpdateAnimation(_animator.Host, clientAnimation, wrapMode); - } var clientActive = Object.Client.activeSelf; Object.Client.SetActive(false); Object.Host.SetActive(clientActive); - _lastIsActive = Object.Host.activeInHierarchy; + _lastIsActive = _hasParent ? Object.Host.activeSelf : Object.Host.activeInHierarchy; _isControlled = false; @@ -818,6 +800,18 @@ public void MakeHost() { component.IsControlled = false; } + if (_animator.Client != null) { + var currentClip = _animator.Client.CurrentClip; + if (currentClip != null) { + var clientAnimation = currentClip.name; + var wrapMode = currentClip.wrapMode; + + Logger.Debug($"MakeHost ({Id}, {Type}) animation: {clientAnimation}, {wrapMode}"); + + LateUpdateAnimation(_animator.Host, clientAnimation, wrapMode); + } + } + for (var fsmIndex = 0; fsmIndex < _fsms.Host.Count; fsmIndex++) { var fsm = _fsms.Host[fsmIndex]; @@ -844,13 +838,18 @@ public void MakeHost() { foreach (var pair in snapshot.Vector3s) { fsm.FsmVariables.GetFsmVector3(pair.Key).Value = pair.Value; } - - Logger.Debug($" Setting FSM state: {snapshot.CurrentState}"); - + // Before setting the state, we replace the actions of the to-be state to only include the ones that // should be executed again (including actions with "everyFrame" on true or that continuously check // collisions for example). var state = fsm.GetState(snapshot.CurrentState); + if (state == null) { + Logger.Debug(" Not setting FSM state, because current state is empty"); + continue; + } + + Logger.Debug($" Setting FSM state: {snapshot.CurrentState}"); + var oldActions = state.Actions; var newActions = oldActions.Where(ActionRegistry.IsActionContinuous).ToArray(); @@ -869,7 +868,7 @@ public void UpdatePosition(Vector2 position) { var unityPos = new Vector3( position.X, position.Y, - Object.Host.transform.position.z + _hasParent ? Object.Host.transform.localPosition.z : Object.Host.transform.position.z ); if (Object.Client == null) { @@ -933,7 +932,7 @@ bool alreadyInSceneUpdate return; } - // Logger.Info($"Entity '{Object.Client.name}' received animation: {animationId}, {clipName}, {wrapMode}"); + Logger.Info($"Entity '{Object.Client.name}' received animation: {animationId}, {clipName}, {wrapMode}"); // All paths lead to calling the Play method of the sprite animator that is hooked, so we allow the call // through the hook @@ -967,6 +966,8 @@ tk2dSpriteAnimationClip.WrapMode wrapMode } var clip = animator.GetClipByName(clipName); + + Logger.Debug($"Entity ({Id}, {Type}) LateUpdateAnimation: {clip.name}, {wrapMode}"); if (wrapMode == tk2dSpriteAnimationClip.WrapMode.LoopSection) { // The clip loops in a specific section in the frames, so we start playing @@ -996,7 +997,7 @@ public void UpdateIsActive(bool active) { if (Object.Client != null) { Object.Client.SetActive(active); } else { - Logger.Warn($"Entity ({_entityId}, {Type}) could not update active, because client object is null"); + Logger.Warn($"Entity ({Id}, {Type}) could not update active, because client object is null"); } } diff --git a/HKMP/Game/Client/Entity/EntityInitializer.cs b/HKMP/Game/Client/Entity/EntityInitializer.cs index ca27fb9d..0361e076 100644 --- a/HKMP/Game/Client/Entity/EntityInitializer.cs +++ b/HKMP/Game/Client/Entity/EntityInitializer.cs @@ -1,6 +1,8 @@ +using System; using System.Linq; using Hkmp.Game.Client.Entity.Action; using Hkmp.Networking.Packet.Data; +using HutongGames.PlayMaker.Actions; namespace Hkmp.Game.Client.Entity; @@ -20,6 +22,13 @@ internal static class EntityInitializer { "dormant" }; + /// + /// Array of types of actions that should be skipped during initialization. + /// + private static readonly Type[] ToSkipTypes = { + typeof(Tk2dPlayAnimation) + }; + /// /// Initialize the FSM of a client entity by finding initialize states and executing the actions in those states. /// @@ -36,6 +45,10 @@ public static void InitializeFsm(PlayMakerFSM fsm) { if (!action.Enabled) { continue; } + + if (ToSkipTypes.Contains(action.GetType())) { + continue; + } if (EntityFsmActions.SupportedActionTypes.Contains(action.GetType())) { var data = new EntityNetworkData(); diff --git a/HKMP/Game/Client/Entity/EntityManager.cs b/HKMP/Game/Client/Entity/EntityManager.cs index 2fe85962..f1cbbf2f 100644 --- a/HKMP/Game/Client/Entity/EntityManager.cs +++ b/HKMP/Game/Client/Entity/EntityManager.cs @@ -3,6 +3,7 @@ using Hkmp.Game.Client.Entity.Action; using Hkmp.Networking.Client; using Hkmp.Networking.Packet.Data; +using Hkmp.Util; using HutongGames.PlayMaker; using UnityEngine; using UnityEngine.SceneManagement; @@ -29,11 +30,6 @@ internal class EntityManager { /// private bool _isSceneHost; - /// - /// The last used ID of an entity. - /// - private byte _lastId; - /// /// Queue of entity updates that have not been applied yet because of a missing entity. /// Usually this occurs because the entities are loaded later than the updates are received when the local player @@ -45,8 +41,8 @@ public EntityManager(NetClient netClient) { _netClient = netClient; _entities = new Dictionary(); _receivedUpdates = new Queue(); - - _lastId = 0; + + EntityProcessor.Initialize(_entities, netClient); EntityFsmActions.EntitySpawnEvent += OnGameObjectSpawned; UnityEngine.SceneManagement.SceneManager.sceneLoaded += OnSceneLoaded; @@ -105,14 +101,10 @@ public void SpawnEntity(byte id, EntityType spawningType, EntityType spawnedType // Find the list of client FSMs that correspond to an entity with the given type in our current scene // Doesn't matter which instance of entity it is, because the FSMs will be the same - List clientFsms = null; - foreach (var existingEntity in _entities.Values) { - if (existingEntity.Type == spawningType) { - clientFsms = existingEntity.GetClientFsms(); - break; - } - } - + var clientFsms = _entities.Values.FirstOrDefault( + e => e.Type == spawningType + )?.GetClientFsms(); + // If no such FSMs exist we return again, because we can't spawn the new entity if (clientFsms == null) { Logger.Warn($"Could not find entity with same type for spawning"); @@ -121,20 +113,16 @@ public void SpawnEntity(byte id, EntityType spawningType, EntityType spawnedType var gameObject = EntitySpawner.SpawnEntityGameObject(spawningType, spawnedType, clientFsms); - // Make sure to initialize all entities that should be in the system - foreach (var fsm in gameObject.GetComponents()) { - if (EntityRegistry.TryGetEntry(gameObject, fsm.Fsm.Name, out _)) { - EntityInitializer.InitializeFsm(fsm); - } - } + var processor = new EntityProcessor { + GameObject = gameObject, + IsSceneHost = _isSceneHost, + LateLoad = true, + SpawnedId = id + }.Process(); - var entity = new Entity( - _netClient, - id, - spawnedType, - gameObject - ); - _entities[id] = entity; + if (!processor.Success) { + Logger.Warn($"Could not process game object of spawned entity: {gameObject.name}"); + } } /// @@ -189,39 +177,38 @@ public void HandleEntityUpdate(EntityUpdate entityUpdate, bool alreadyInSceneUpd /// The action from which the game object was spawned. /// The game object that was spawned. private void OnGameObjectSpawned(FsmStateAction action, GameObject gameObject) { - if (_entities.Values.Any(entity => entity.Object.Host == gameObject)) { + if (_entities.Values.Any(existingEntity => existingEntity.Object.Host == gameObject)) { Logger.Debug("Spawned object was already a registered entity"); return; } - foreach (var fsm in gameObject.GetComponents()) { - if (!ProcessGameObjectFsm(fsm, out var entity, out var entityId)) { - continue; - } + var processor = new EntityProcessor { + GameObject = gameObject, + IsSceneHost = _isSceneHost, + LateLoad = true + }.Process(); - if (_isSceneHost) { - // Since an entity was created and we are the scene host, we need to notify the server - var spawningObjectName = action.Fsm.GameObject.name; - var spawningFsmName = action.Fsm.Name; - if (EntityRegistry.TryGetEntry(action.Fsm.GameObject, spawningFsmName, out var entry)) { - Logger.Info( - $"Notifying server of entity ({spawningObjectName}, {entry.Type}) spawning entity ({gameObject.name}, {entity.Type}) with ID {entityId}"); - _netClient.UpdateManager.SetEntitySpawn(entityId, entry.Type, entity.Type); - } - - // Also initialize the entity as host, since otherwise it will stay disabled - entity.InitializeHost(); - } else { - // Since an entity was created and we are not the scene host, we need to manually initialize - // all the FSM on the client - foreach (var clientFsm in entity.GetClientFsms()) { - Logger.Info($"Manually initializing client entity FSM: {clientFsm.Fsm.Name}, {clientFsm.gameObject.name}"); - EntityInitializer.InitializeFsm(clientFsm); - } - - // We also need to update the 'active' state of the entity since it was spawned - entity.UpdateIsActive(true); - } + if (!processor.Success) { + return; + } + + if (!_isSceneHost) { + Logger.Warn("Game object was spawned while not scene host, this shouldn't happen"); + return; + } + + // Since an entity was created and we are the scene host, we need to notify the server + var spawningObjectName = action.Fsm.GameObject.name; + if (EntityRegistry.TryGetEntry(action.Fsm.GameObject, out var entry)) { + var topLevelEntity = processor.Entities[0]; + + Logger.Info( + $"Notifying server of entity ({spawningObjectName}, {entry.Type}) spawning entity ({gameObject.name}, {topLevelEntity.Type}) with ID {topLevelEntity.Id}"); + _netClient.UpdateManager.SetEntitySpawn( + topLevelEntity.Id, + entry.Type, + topLevelEntity.Type + ); } } @@ -255,8 +242,6 @@ private void OnSceneChanged(Scene oldScene, Scene newScene) { _entities.Clear(); - _lastId = 0; - if (!_netClient.IsConnected) { return; } @@ -297,124 +282,25 @@ private void OnSceneLoaded(Scene scene, LoadSceneMode mode) { /// Whether this scene was loaded late. private void FindEntitiesInScene(Scene scene, bool lateLoad) { // Find all PlayMakerFSM components - var fsms = Object.FindObjectsOfType(); - - foreach (var fsm in fsms) { - // Logger.Info($"Found FSM: {fsm.Fsm.Name} in scene: {fsm.gameObject.scene.name}"); - if (fsm.gameObject.scene != scene) { - continue; - } - - // Process the FSM of the game object and only proceed if it was successful and it is a late scene load - if (!ProcessGameObjectFsm(fsm, out var entity, out _) || !lateLoad) { - continue; - } - - if (_isSceneHost) { - // Since this is a late load it needs to be initialized as host if we are the scene host - entity.InitializeHost(); - } else { - // Since this is a late load we need to update the 'active' state of the entity - entity.UpdateIsActive(true); - } - } - - // Check specifically for children of FSM game objects - foreach (var fsm in fsms) { - var gameObject = fsm.gameObject; - - for (var i = 0; i < gameObject.transform.childCount; i++) { - var child = gameObject.transform.GetChild(i); - var childObj = child.gameObject; - - if (!EntityRegistry.TryGetEntryWithParent( - childObj.name, - gameObject.name, - out var entry - )) { - continue; - } - - Logger.Debug($"Found child of '{gameObject.name}' to be registered: {childObj.name}, {entry.Type}"); - - var entity = RegisterGameObjectAsEntity(childObj, entry.Type, out _); - - if (lateLoad) { - if (_isSceneHost) { - // Since this is a late load it needs to be initialized as host if we are the scene host - entity.InitializeHost(); - } else { - // Since this is a late load we need to update the 'active' state of the entity - entity.UpdateIsActive(true); - } - } - } - } - - // Find all Climber components - foreach (var climber in Object.FindObjectsOfType()) { - if (climber.gameObject.scene != scene) { - continue; - } - - if (!EntityRegistry.TryGetEntry(climber.gameObject.name, EntityType.Tiktik, out var entry)) { - continue; - } - - RegisterGameObjectAsEntity(climber.gameObject, entry.Type, out _); + // Filter out FSMs with GameObjects not in the current scene + // Project each FSM to their GameObject + // Project each GameObject into its children including itself + // Concatenate all GameObjects for Climber components + // Filter out GameObjects not in the current scene + var objectsToCheck = Object.FindObjectsOfType() + .Where(fsm => fsm.gameObject.scene == scene) + .Select(fsm => fsm.gameObject) + .SelectMany(obj => obj.GetChildren().Prepend(obj)) + .Concat(Object.FindObjectsOfType().Select(climber => climber.gameObject)) + .Where(obj => obj.scene == scene) + .Distinct(); + + foreach (var obj in objectsToCheck) { + new EntityProcessor { + GameObject = obj, + IsSceneHost = _isSceneHost, + LateLoad = lateLoad + }.Process(); } } - - /// - /// Process the FSM of a game object to check whether the game object should be registered as an entity. - /// - /// The FSM to process. - /// The resulting entity if one was created; otherwise null. - /// The ID of the entity if one was created; otherwise 0. - /// True if an entity was created; otherwise false. - private bool ProcessGameObjectFsm(PlayMakerFSM fsm, out Entity entity, out byte entityId) { - // Logger.Info($"Processing FSM: {fsm.Fsm.Name}, {fsm.gameObject.name}"); - - if (!EntityRegistry.TryGetEntry(fsm.gameObject, fsm.Fsm.Name, out var entry)) { - entity = null; - entityId = 0; - return false; - } - - entity = RegisterGameObjectAsEntity(fsm.gameObject, entry.Type, out entityId); - return true; - } - - /// - /// Register a given game object as an entity and return that entity. - /// - /// The game object to register. - /// The type of the entity. - /// The ID of the registered entity. - /// The entity that was created. - private Entity RegisterGameObjectAsEntity(GameObject gameObject, EntityType type, out byte entityId) { - // First find a usable ID that is not registered to an entity already - while (_entities.ContainsKey(_lastId)) { - _lastId++; - } - - Logger.Info($"Registering entity ({type}) '{gameObject.name}' with ID '{_lastId}'"); - - // TODO: maybe we need to check whether this entity game object has already been registered, which can - // happen with game objects that have multiple FSMs - - var entity = new Entity( - _netClient, - _lastId, - type, - gameObject - ); - _entities[_lastId] = entity; - - entityId = _lastId; - - _lastId++; - - return entity; - } } \ No newline at end of file diff --git a/HKMP/Game/Client/Entity/EntityProcessor.cs b/HKMP/Game/Client/Entity/EntityProcessor.cs new file mode 100644 index 00000000..e3b6adef --- /dev/null +++ b/HKMP/Game/Client/Entity/EntityProcessor.cs @@ -0,0 +1,188 @@ +using System.Collections.Generic; +using System.Linq; +using Hkmp.Networking.Client; +using Hkmp.Util; +using UnityEngine; +using Logger = Hkmp.Logging.Logger; + +namespace Hkmp.Game.Client.Entity; + +/// +/// Data processing class that receives a set of parameters and processing a given game object into an entity if +/// applicable. Will recursively process children of the game object as well and report success and the list of +/// entities created. +/// +internal class EntityProcessor { + /// + /// Reference to the dictionary of entities from the entity manager. + /// + private static Dictionary _entities; + /// + /// The net client used to pass onto constructed entities. + /// + private static NetClient _netClient; + + /// + /// The last used entity ID. + /// + private static byte _lastId; + + /// + /// The game object to process. + /// + public GameObject GameObject { get; init; } + /// + /// Whether the local client is the scene host. + /// + public bool IsSceneHost { get; init; } + /// + /// Whether the processing of this entity should happen under the assumption that was a late load of the + /// game object. + /// + public bool LateLoad { get; init; } + /// + /// Whether the game object was spawned and should have the designated ID. + /// + public byte? SpawnedId { get; init; } + + /// + /// The list of entities that were created during the processing. + /// + public List Entities { get; } = new(); + /// + /// Whether the processing of the entity was a success. + /// + public bool Success => Entities.Count > 0; + + /// + /// Initialize the entity processor with a reference to the entity dict and the net client. + /// + /// A reference to the dictionary of entities from the entity manager. + /// The net client instance to pass onto constructed entities. + public static void Initialize(Dictionary entities, NetClient netClient) { + _entities = entities; + _netClient = netClient; + + UnityEngine.SceneManagement.SceneManager.activeSceneChanged += (_, _) => { + _lastId = 0; + }; + } + + /// + /// Process the game object set in this instance with the parameter set in this instance. + /// + /// The instance of this class for convenience. + public EntityProcessor Process() { + Process(GameObject); + return this; + } + + /// + /// Process the given game object to (potentially) become an entity. Check for child objects as well and will + /// recursively process those. + /// + /// The game object to process. + /// Entity registry entries to check against for the entity or null to use all + /// top-level entries. + /// The client object of the parent entity for this entity or null if no such + /// parent exists. + private void Process( + GameObject gameObject, + IEnumerable entries = null, + GameObject parentClientObject = null + ) { + EntityRegistryEntry foundEntry; + + // If the given entries are null, query the entity registry for all top-level entries + // Otherwise only use the given entries (used for child entities) + if (entries == null) { + if (!EntityRegistry.TryGetEntry(gameObject, out foundEntry)) { + return; + } + } else { + if (!EntityRegistry.TryGetEntry(entries, gameObject, out foundEntry)) { + return; + } + } + + byte id; + + // If a spawned ID is defined we check whether an entity with the given ID already exists + // Otherwise we find a new ID that isn't used yet + if (SpawnedId.HasValue) { + id = SpawnedId.Value; + + if (_entities.ContainsKey(id)) { + Logger.Warn($"Tried registering entity with forced ID ({id}), but an entity with the ID already exists"); + return; + } + } else { + if (_entities.Count >= byte.MaxValue) { + Logger.Error("Could not register entity because ID space is full!"); + return; + } + + // First find a usable ID that is not registered to an entity already + while (_entities.ContainsKey(_lastId)) { + _lastId++; + } + + id = _lastId; + _lastId++; + } + + // Depending on whether a parent object was given we create the entity with this parent object + Entity entity; + if (parentClientObject == null) { + Logger.Info($"Registering entity ({foundEntry.Type}) '{gameObject.name}' with ID '{_lastId}'"); + + entity = new Entity( + _netClient, + id, + foundEntry.Type, + gameObject + ); + } else { + Logger.Info($"Registering entity ({foundEntry.Type}) '{gameObject.name}' with ID '{_lastId}' with parent: {parentClientObject.name}"); + + // Find the correct child of the client object of the parent entity + var clientObject = parentClientObject.GetChildren() + .FirstOrDefault(c => c.name.Contains(foundEntry.BaseObjectName)); + if (clientObject == null) { + Logger.Warn("Could not find child of client object of parent entity"); + return; + } + + Logger.Debug($"Found child of client object of parent entity: {clientObject.name}, {clientObject.GetInstanceID()}"); + + entity = new Entity( + _netClient, + id, + foundEntry.Type, + gameObject, + clientObject + ); + } + + _entities[id] = entity; + + Entities.Add(entity); + + // If this entry has child entries, we recursively check the children of the object as well + if (foundEntry.Children != null) { + foreach (var childObj in gameObject.GetChildren()) { + Process(childObj, foundEntry.Children, entity.Object.Client); + } + } + + if (LateLoad) { + if (IsSceneHost) { + // Since this is a late load it needs to be initialized as host if we are the scene host + entity.InitializeHost(); + } else { + // Since this is a late load we need to update the 'active' state of the entity + entity.UpdateIsActive(true); + } + } + } +} diff --git a/HKMP/Game/Client/Entity/EntityRegistry.cs b/HKMP/Game/Client/Entity/EntityRegistry.cs index 9f42fc63..3432f396 100644 --- a/HKMP/Game/Client/Entity/EntityRegistry.cs +++ b/HKMP/Game/Client/Entity/EntityRegistry.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Linq; using Hkmp.Util; using Newtonsoft.Json; using Newtonsoft.Json.Converters; @@ -30,91 +31,71 @@ static EntityRegistry() { } /// - /// Try to get the entity registry entry given a game object and a FSM name. + /// Try to get the corresponding entry from the given enumerable of entries and the given game object. /// - /// The game object. - /// The name of the FSM. - /// The entry if it is found; otherwise null. - /// True if the entry was found; otherwise false. - public static bool TryGetEntry(GameObject gameObject, string fsmName, out EntityRegistryEntry foundEntry) { - foundEntry = null; - - foreach (var entry in Entries) { - if (entry.FsmName == null) { - continue; - } + /// The game object to find the entry for. + /// The entry if it was found; otherwise null. + /// true if the entry was found; otherwise false. + public static bool TryGetEntry( + GameObject gameObject, + out EntityRegistryEntry foundEntry + ) { + return TryGetEntry(Entries, gameObject, out foundEntry); + } - if (!entry.FsmName.Equals(fsmName)) { - continue; - } + /// + /// Try to get the corresponding entry from the given enumerable of entries and the given game object. + /// + /// The enumerable of entries to check for. + /// The game object to find the entry for. + /// The entry if it was found; otherwise null. + /// true if the entry was found; otherwise false. + public static bool TryGetEntry( + IEnumerable entries, + GameObject gameObject, + out EntityRegistryEntry foundEntry + ) { + foundEntry = null; - if (gameObject.name.Contains(entry.BaseObjectName)) { - // If a parent name is defined on the entry, the parent of the object needs to match - if (entry.ParentName != null) { - var parent = gameObject.transform.parent; - // No parent, so no match to the entry - if (parent == null) { - return false; - } + var entry = entries.FirstOrDefault( + entry => gameObject.name.Contains(entry.BaseObjectName) + ); - // Parent name does not match the entry - if (!parent.gameObject.name.Contains(entry.ParentName)) { - return false; - } - } - - foundEntry = entry; - return true; - } + if (entry == null) { + return false; + } + + // If the entry has an FSM name defined and the child object does not have any FSM components + // that match this name, we return + if (entry.FsmName != null && !gameObject.GetComponents().Any( + childFsm => childFsm.Fsm.Name.Equals(entry.FsmName) + )) { + return false; } - return false; - } - - /// - /// Try to get the entity registry entry given a game object name and an entity type. - /// - /// The name of the game object. - /// The type of the entity. - /// The entry if it is found; otherwise null. - /// True if the entry was found; otherwise false. - public static bool TryGetEntry(string gameObjectName, EntityType type, out EntityRegistryEntry foundEntry) { - foreach (var entry in Entries) { - if (!entry.Type.Equals(type)) { - continue; + // If the entry has a parent name defined, we need to check if the parent of the game object matches it + if (entry.ParentName != null) { + var parent = gameObject.transform.parent; + // No parent at all, so it trivially doesn't match the name + if (parent == null) { + return false; } - if (gameObjectName.Contains(entry.BaseObjectName)) { - foundEntry = entry; - return true; + if (!parent.gameObject.name.Contains(entry.ParentName)) { + return false; } } - foundEntry = null; - return false; - } - - /// - /// Try to get the entity registry entry given a game object name and the name of the parent game object. - /// - /// The name of the game object. - /// The name of the parent game object. - /// The entry if it is found; otherwise null. - /// True if the entry was found; otherwise false. - public static bool TryGetEntryWithParent(string gameObjectName, string parentName, out EntityRegistryEntry foundEntry) { - foreach (var entry in Entries) { - if (entry.BaseObjectName == null || entry.ParentName == null) { - continue; - } - - if (gameObjectName.Contains(entry.BaseObjectName) && parentName.Contains(entry.ParentName)) { - foundEntry = entry; - return true; + // Specifically check if for the entries of type Tiktik, the game object has a Climber component + // Otherwise we might run into game objects that contain "Climber" in their name that aren't actually Tiktiks + if (entry.Type == EntityType.Tiktik) { + if (gameObject.GetComponent() == null) { + return false; } } - foundEntry = null; - return false; + foundEntry = entry; + return true; } } @@ -147,4 +128,10 @@ internal class EntityRegistryEntry { /// [JsonProperty("parent_name")] public string ParentName { get; set; } + + /// + /// List of entries that are children of this entry. + /// + [JsonProperty("children")] + public List Children { get; set; } } diff --git a/HKMP/Resource/entity-registry.json b/HKMP/Resource/entity-registry.json index 2a73269e..58ec02f0 100644 --- a/HKMP/Resource/entity-registry.json +++ b/HKMP/Resource/entity-registry.json @@ -6,8 +6,7 @@ }, { "base_object_name": "Climber", - "type": "Tiktik", - "fsm_name": "" + "type": "Tiktik" }, { "base_object_name": "Buzzer", @@ -87,12 +86,14 @@ { "base_object_name": "False Knight New", "type": "FalseKnight", - "fsm_name": "FalseyControl" - }, - { - "base_object_name": "Head", - "type": "FalseKnightHead", - "fsm_name": "Health Check" + "fsm_name": "FalseyControl", + "children": [ + { + "base_object_name": "Head", + "type": "FalseKnightHead", + "fsm_name": "Health Check" + } + ] }, { "base_object_name": "FK Barrel Summon", @@ -137,26 +138,27 @@ { "base_object_name": "Mawlek Body", "type": "BroodingMawlek", - "fsm_name": "Mawlek Control" - }, - { - "base_object_name": "Mawlek Arm L", - "type": "BroodingMawlekArm", - "fsm_name": "Mawlek Arm Control" - }, - { - "base_object_name": "Mawlek Arm R", - "type": "BroodingMawlekArm", - "fsm_name": "Mawlek Arm Control" - }, - { - "base_object_name": "Mawlek Head", - "type": "BroodingMawlekHead", - "fsm_name": "Mawlek Head" - }, - { - "base_object_name": "Dummy", - "type": "BroodingMawlekDummy", - "parent_name": "Mawlek Body" + "fsm_name": "Mawlek Control", + "children": [ + { + "base_object_name": "Mawlek Arm L", + "type": "BroodingMawlekArm", + "fsm_name": "Mawlek Arm Control" + }, + { + "base_object_name": "Mawlek Arm R", + "type": "BroodingMawlekArm", + "fsm_name": "Mawlek Arm Control" + }, + { + "base_object_name": "Mawlek Head", + "type": "BroodingMawlekHead", + "fsm_name": "Mawlek Head" + }, + { + "base_object_name": "Dummy", + "type": "BroodingMawlekDummy" + } + ] } ] diff --git a/HKMP/Util/GameObjectExtensions.cs b/HKMP/Util/GameObjectExtensions.cs index c983b551..0d988e06 100644 --- a/HKMP/Util/GameObjectExtensions.cs +++ b/HKMP/Util/GameObjectExtensions.cs @@ -1,3 +1,4 @@ +using System.Collections.Generic; using UnityEngine; namespace Hkmp.Util; @@ -28,4 +29,13 @@ string name return null; } + + public static IEnumerable GetChildren(this GameObject gameObject) { + var children = new List(); + for (var i = 0; i < gameObject.transform.childCount; i++) { + children.Add(gameObject.transform.GetChild(i).gameObject); + } + + return children; + } } From 096c2043719440fc8adcbf0a8a19988b3d94957e Mon Sep 17 00:00:00 2001 From: Extremelyd1 Date: Mon, 19 Jun 2023 22:38:30 +0200 Subject: [PATCH 041/216] Refactor specific component initialization Add Hornet to system --- .../Client/Entity/Action/EntityFsmActions.cs | 185 +++++++++++++----- .../Entity/Component/ClimberComponent.cs | 39 ++++ .../Entity/Component/ColliderComponent.cs | 2 +- .../Entity/Component/ComponentFactory.cs | 44 +++++ .../Entity/Component/DamageHeroComponent.cs | 2 +- .../Entity/Component/EntityComponent.cs | 20 ++ .../Entity/Component/GravityScaleComponent.cs | 2 +- .../Component/HealthManagerComponent.cs | 8 +- .../Entity/Component/MeshRendererComponent.cs | 2 +- .../Entity/Component/RotationComponent.cs | 16 +- .../Entity/Component/VelocityComponent.cs | 2 +- .../Entity/Component/ZPositionComponent.cs | 2 +- HKMP/Game/Client/Entity/Entity.cs | 157 ++++++++------- HKMP/Game/Client/Entity/EntityInitializer.cs | 6 +- HKMP/Game/Client/Entity/EntityProcessor.cs | 11 +- HKMP/Game/Client/Entity/EntityRegistry.cs | 7 + HKMP/Game/Client/Entity/EntityType.cs | 3 +- HKMP/Game/Server/ServerEntityData.cs | 2 +- HKMP/Game/Server/ServerManager.cs | 5 +- HKMP/Networking/Client/ClientUpdateManager.cs | 2 +- HKMP/Networking/Packet/Data/EntityUpdate.cs | 25 +-- HKMP/Networking/Server/ServerUpdateManager.cs | 2 +- HKMP/Resource/action-registry.json | 11 +- HKMP/Resource/entity-registry.json | 14 ++ 24 files changed, 397 insertions(+), 172 deletions(-) create mode 100644 HKMP/Game/Client/Entity/Component/ClimberComponent.cs create mode 100644 HKMP/Game/Client/Entity/Component/ComponentFactory.cs diff --git a/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs b/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs index 0969daeb..8b0e451f 100644 --- a/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs +++ b/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs @@ -1029,54 +1029,6 @@ private static bool GetNetworkDataFromAction(EntityNetworkData data, SetPosition return false; } - // TODO: this action is used for initialization currently and uses static values - // If we come across this action and it uses references, we need to not only uncomment the below, but - // also think about how to use static values for entity initialization and the dynamic values for - // non-initialization purposes - - // Vector3 vector3; - // if (!action.vector.IsNone) { - // vector3 = action.vector.Value; - // } else { - // if (action.space == Space.World) { - // vector3 = gameObject.transform.position; - // } else { - // vector3 = gameObject.transform.localPosition; - // } - // } - // - // if (!action.x.IsNone) { - // vector3.x = action.x.Value; - // } - // - // if (!action.y.IsNone) { - // vector3.y = action.y.Value; - // } - // - // if (!action.z.IsNone) { - // vector3.z = action.z.Value; - // } - // - // data.Packet.Write(vector3.x); - // data.Packet.Write(vector3.y); - // data.Packet.Write(vector3.z); - - return true; - } - - private static void ApplyNetworkDataFromAction(EntityNetworkData data, SetPosition action) { - // See comment in "Get" method above - - // var vector = new Vector3( - // data.Packet.ReadFloat(), - // data.Packet.ReadFloat() - // ); - - var gameObject = action.Fsm.GetOwnerDefaultTarget(action.gameObject); - if (gameObject == null) { - return; - } - Vector3 vector3; if (!action.vector.IsNone) { vector3 = action.vector.Value; @@ -1099,6 +1051,55 @@ private static void ApplyNetworkDataFromAction(EntityNetworkData data, SetPositi if (!action.z.IsNone) { vector3.z = action.z.Value; } + + data.Packet.Write(vector3.x); + data.Packet.Write(vector3.y); + data.Packet.Write(vector3.z); + + return true; + } + + private static void ApplyNetworkDataFromAction(EntityNetworkData data, SetPosition action) { + Vector3 vector3; + + var gameObject = action.Fsm.GetOwnerDefaultTarget(action.gameObject); + + if (data == null) { + if (gameObject == null) { + return; + } + + if (!action.vector.IsNone) { + vector3 = action.vector.Value; + } else { + if (action.space == Space.World) { + vector3 = gameObject.transform.position; + } else { + vector3 = gameObject.transform.localPosition; + } + } + + if (!action.x.IsNone) { + vector3.x = action.x.Value; + } + + if (!action.y.IsNone) { + vector3.y = action.y.Value; + } + + if (!action.z.IsNone) { + vector3.z = action.z.Value; + } + } else { + vector3 = new Vector3( + data.Packet.ReadFloat(), + data.Packet.ReadFloat() + ); + + if (gameObject == null) { + return; + } + } if (action.space == Space.World) { gameObject.transform.position = vector3; @@ -1339,4 +1340,92 @@ private static void ApplyNetworkDataFromAction(EntityNetworkData data, SendHealt } #endregion + + #region ActivateAllChildren + + private static bool GetNetworkDataFromAction(EntityNetworkData data, ActivateAllChildren action) { + return true; + } + + private static void ApplyNetworkDataFromAction(EntityNetworkData data, ActivateAllChildren action) { + var gameObject = action.gameObject.Value; + if (gameObject == null) { + return; + } + + foreach (UnityEngine.Component component in gameObject.transform) { + component.gameObject.SetActive(action.activate); + } + } + + #endregion + + #region SetBoxCollider2DSizeVector + + private static bool GetNetworkDataFromAction(EntityNetworkData data, SetBoxCollider2DSizeVector action) { + return true; + } + + private static void ApplyNetworkDataFromAction(EntityNetworkData data, SetBoxCollider2DSizeVector action) { + var gameObject = action.Fsm.GetOwnerDefaultTarget(action.gameObject1); + if (gameObject == null) { + return; + } + + var collider = gameObject.GetComponent(); + if (collider == null) { + return; + } + + if (!action.size.IsNone) { + collider.size = action.size.Value; + } + + if (!action.offset.IsNone) { + collider.offset = action.offset.Value; + } + } + + #endregion + + #region SetVelocityAsAngle + + private static bool GetNetworkDataFromAction(EntityNetworkData data, SetVelocityAsAngle action) { + var gameObject = action.Fsm.GetOwnerDefaultTarget(action.gameObject); + if (gameObject == null) { + return false; + } + + if (IsObjectInRegistry(gameObject)) { + Logger.Debug("Tried getting SetVelocityAsAngle network data, but entity is in registry"); + return false; + } + + data.Packet.Write(action.speed.Value); + data.Packet.Write(action.angle.Value); + + return true; + } + + private static void ApplyNetworkDataFromAction(EntityNetworkData data, SetVelocityAsAngle action) { + var speed = data.Packet.ReadFloat(); + var angle = data.Packet.ReadFloat(); + + var gameObject = action.Fsm.GetOwnerDefaultTarget(action.gameObject); + if (gameObject == null) { + return; + } + + var rigidbody = gameObject.GetComponent(); + if (rigidbody == null) { + return; + } + + var x = speed * Mathf.Cos(angle * ((float) System.Math.PI / 180f)); + var y = speed * Mathf.Sin(angle * ((float) System.Math.PI / 180f)); + + rigidbody.velocity = new Vector2(x, y); + } + + #endregion } diff --git a/HKMP/Game/Client/Entity/Component/ClimberComponent.cs b/HKMP/Game/Client/Entity/Component/ClimberComponent.cs new file mode 100644 index 00000000..7d3a78fc --- /dev/null +++ b/HKMP/Game/Client/Entity/Component/ClimberComponent.cs @@ -0,0 +1,39 @@ +using Hkmp.Networking.Client; +using Hkmp.Networking.Packet.Data; +using UnityEngine; + +namespace Hkmp.Game.Client.Entity.Component; + +/// +/// This component manages the climber behaviour of the entity. +internal class ClimberComponent : EntityComponent { + /// + /// The unity component of the entity. + /// + private readonly Climber _climber; + + public ClimberComponent( + NetClient netClient, + byte entityId, + HostClientPair gameObject, + Climber climber + ) : base(netClient, entityId, gameObject) { + _climber = climber; + _climber.enabled = false; + } + + /// + public override void InitializeHost() { + if (_climber != null) { + _climber.enabled = true; + } + } + + /// + public override void Update(EntityNetworkData data) { + } + + /// + public override void Destroy() { + } +} \ No newline at end of file diff --git a/HKMP/Game/Client/Entity/Component/ColliderComponent.cs b/HKMP/Game/Client/Entity/Component/ColliderComponent.cs index c4bd1991..a9eab9d0 100644 --- a/HKMP/Game/Client/Entity/Component/ColliderComponent.cs +++ b/HKMP/Game/Client/Entity/Component/ColliderComponent.cs @@ -48,7 +48,7 @@ private void OnUpdateCollider() { _lastEnabled = newEnabled; var data = new EntityNetworkData { - Type = EntityNetworkData.DataType.Collider + Type = EntityComponentType.Collider }; data.Packet.Write(newEnabled); diff --git a/HKMP/Game/Client/Entity/Component/ComponentFactory.cs b/HKMP/Game/Client/Entity/Component/ComponentFactory.cs new file mode 100644 index 00000000..c969c346 --- /dev/null +++ b/HKMP/Game/Client/Entity/Component/ComponentFactory.cs @@ -0,0 +1,44 @@ +using System; +using Hkmp.Networking.Client; +using UnityEngine; + +namespace Hkmp.Game.Client.Entity.Component; + +/// +/// Factory class that instantiates by type and additional parameters. +/// +internal static class ComponentFactory { + /// + /// Instantiate an by their type. + /// + /// The type of the component. + /// The net client for passing to the constructor of the component. + /// The entity ID for passing to the constructor of the component. + /// The host and client objects for passing to the constructor of the component. + /// The instantiated entity component. + /// Thrown if the type is not one that can be instantiated + /// here. + public static EntityComponent InstantiateByType( + EntityComponentType type, + NetClient netClient, + byte entityId, + HostClientPair objects + ) { + Rigidbody2D rigidBody; + + switch (type) { + case EntityComponentType.Rotation: + return new RotationComponent(netClient, entityId, objects); + case EntityComponentType.Velocity: + rigidBody = objects.Host.GetComponent(); + return new VelocityComponent(netClient, entityId, objects, rigidBody); + case EntityComponentType.GravityScale: + rigidBody = objects.Host.GetComponent(); + return new GravityScaleComponent(netClient, entityId, objects, rigidBody); + case EntityComponentType.ZPosition: + return new ZPositionComponent(netClient, entityId, objects); + default: + throw new ArgumentOutOfRangeException(nameof(type), type, $"Could not instantiate entity component for type: {type}"); + } + } +} diff --git a/HKMP/Game/Client/Entity/Component/DamageHeroComponent.cs b/HKMP/Game/Client/Entity/Component/DamageHeroComponent.cs index b4ef7103..fcf99319 100644 --- a/HKMP/Game/Client/Entity/Component/DamageHeroComponent.cs +++ b/HKMP/Game/Client/Entity/Component/DamageHeroComponent.cs @@ -47,7 +47,7 @@ private void OnUpdate() { _lastDamageDealt = newDamageDealt; var data = new EntityNetworkData { - Type = EntityNetworkData.DataType.DamageHero + Type = EntityComponentType.DamageHero }; data.Packet.Write((byte) newDamageDealt); diff --git a/HKMP/Game/Client/Entity/Component/EntityComponent.cs b/HKMP/Game/Client/Entity/Component/EntityComponent.cs index ee18ec3e..9c98bdbb 100644 --- a/HKMP/Game/Client/Entity/Component/EntityComponent.cs +++ b/HKMP/Game/Client/Entity/Component/EntityComponent.cs @@ -1,5 +1,7 @@ using Hkmp.Networking.Client; using Hkmp.Networking.Packet.Data; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; using UnityEngine; namespace Hkmp.Game.Client.Entity.Component; @@ -61,4 +63,22 @@ protected void SendData(EntityNetworkData data) { /// Destroy the entity component. /// public abstract void Destroy(); +} + +/// +/// Enum for data types. +/// +[JsonConverter(typeof(StringEnumConverter))] +internal enum EntityComponentType : byte { + Fsm = 0, + Death, + Invincibility, + Rotation, + Collider, + DamageHero, + MeshRenderer, + Velocity, + GravityScale, + ZPosition, + Climber } \ No newline at end of file diff --git a/HKMP/Game/Client/Entity/Component/GravityScaleComponent.cs b/HKMP/Game/Client/Entity/Component/GravityScaleComponent.cs index 0cbfd547..53527c42 100644 --- a/HKMP/Game/Client/Entity/Component/GravityScaleComponent.cs +++ b/HKMP/Game/Client/Entity/Component/GravityScaleComponent.cs @@ -58,7 +58,7 @@ private void OnUpdate() { _lastScale = newGravityScale; var data = new EntityNetworkData { - Type = EntityNetworkData.DataType.GravityScale + Type = EntityComponentType.GravityScale }; data.Packet.Write(newGravityScale); diff --git a/HKMP/Game/Client/Entity/Component/HealthManagerComponent.cs b/HKMP/Game/Client/Entity/Component/HealthManagerComponent.cs index 527eb90c..bb88fe47 100644 --- a/HKMP/Game/Client/Entity/Component/HealthManagerComponent.cs +++ b/HKMP/Game/Client/Entity/Component/HealthManagerComponent.cs @@ -86,7 +86,7 @@ bool ignoreEvasion orig(self, attackDirection, attackType, ignoreEvasion); var data = new EntityNetworkData { - Type = EntityNetworkData.DataType.Death + Type = EntityComponentType.Death }; if (attackDirection.HasValue) { @@ -108,7 +108,7 @@ bool ignoreEvasion /// private void OnUpdate() { var data = new EntityNetworkData { - Type = EntityNetworkData.DataType.Invincibility + Type = EntityComponentType.Invincibility }; var shouldSend = false; @@ -145,7 +145,7 @@ public override void Update(EntityNetworkData data) { return; } - if (data.Type == EntityNetworkData.DataType.Death) { + if (data.Type == EntityComponentType.Death) { var attackDirection = new float?(); if (data.Packet.ReadBool()) { attackDirection = data.Packet.ReadFloat(); @@ -157,7 +157,7 @@ public override void Update(EntityNetworkData data) { // Set a boolean to indicate that the client health manager is allowed to execute the Die method _allowDeath = true; _healthManager.Client.Die(attackDirection, attackType, ignoreEvasion); - } else if (data.Type == EntityNetworkData.DataType.Invincibility) { + } else if (data.Type == EntityComponentType.Invincibility) { var newInvincible = data.Packet.ReadBool(); var newInvincibleFromDir = data.Packet.ReadByte(); diff --git a/HKMP/Game/Client/Entity/Component/MeshRendererComponent.cs b/HKMP/Game/Client/Entity/Component/MeshRendererComponent.cs index 04ac62ee..63a04bfd 100644 --- a/HKMP/Game/Client/Entity/Component/MeshRendererComponent.cs +++ b/HKMP/Game/Client/Entity/Component/MeshRendererComponent.cs @@ -50,7 +50,7 @@ private void OnUpdate() { _lastEnabled = newEnabled; var data = new EntityNetworkData { - Type = EntityNetworkData.DataType.MeshRenderer + Type = EntityComponentType.MeshRenderer }; data.Packet.Write(newEnabled); diff --git a/HKMP/Game/Client/Entity/Component/RotationComponent.cs b/HKMP/Game/Client/Entity/Component/RotationComponent.cs index 818ff3b4..a849f476 100644 --- a/HKMP/Game/Client/Entity/Component/RotationComponent.cs +++ b/HKMP/Game/Client/Entity/Component/RotationComponent.cs @@ -8,11 +8,6 @@ namespace Hkmp.Game.Client.Entity.Component; /// /// This component manages the rotation of the entity. internal class RotationComponent : EntityComponent { - /// - /// The unity component of the entity. - /// - private readonly Climber _climber; - /// /// The last rotation of the entity. /// @@ -21,12 +16,8 @@ internal class RotationComponent : EntityComponent { public RotationComponent( NetClient netClient, byte entityId, - HostClientPair gameObject, - Climber climber + HostClientPair gameObject ) : base(netClient, entityId, gameObject) { - _climber = climber; - _climber.enabled = false; - MonoBehaviourUtil.Instance.OnUpdateEvent += OnUpdateRotation; } @@ -49,7 +40,7 @@ private void OnUpdateRotation() { _lastRotation = newRotation; var data = new EntityNetworkData { - Type = EntityNetworkData.DataType.Rotation + Type = EntityComponentType.Rotation }; data.Packet.Write(newRotation.z); @@ -59,9 +50,6 @@ private void OnUpdateRotation() { /// public override void InitializeHost() { - if (_climber != null) { - _climber.enabled = true; - } } /// diff --git a/HKMP/Game/Client/Entity/Component/VelocityComponent.cs b/HKMP/Game/Client/Entity/Component/VelocityComponent.cs index 59d49016..e77b9367 100644 --- a/HKMP/Game/Client/Entity/Component/VelocityComponent.cs +++ b/HKMP/Game/Client/Entity/Component/VelocityComponent.cs @@ -58,7 +58,7 @@ private void OnUpdate() { _lastVelocity = newVelocity; var data = new EntityNetworkData { - Type = EntityNetworkData.DataType.Velocity + Type = EntityComponentType.Velocity }; data.Packet.Write(newVelocity.x); data.Packet.Write(newVelocity.y); diff --git a/HKMP/Game/Client/Entity/Component/ZPositionComponent.cs b/HKMP/Game/Client/Entity/Component/ZPositionComponent.cs index 8e7249e7..c9012582 100644 --- a/HKMP/Game/Client/Entity/Component/ZPositionComponent.cs +++ b/HKMP/Game/Client/Entity/Component/ZPositionComponent.cs @@ -41,7 +41,7 @@ private void OnUpdate() { _lastZ = newZ; var data = new EntityNetworkData { - Type = EntityNetworkData.DataType.ZPosition + Type = EntityComponentType.ZPosition }; data.Packet.Write(newZ); diff --git a/HKMP/Game/Client/Entity/Entity.cs b/HKMP/Game/Client/Entity/Entity.cs index c93dbb62..cdafd210 100644 --- a/HKMP/Game/Client/Entity/Entity.cs +++ b/HKMP/Game/Client/Entity/Entity.cs @@ -63,7 +63,7 @@ internal class Entity { /// /// Dictionary mapping data types to entity components. /// - private readonly Dictionary _components; + private readonly Dictionary _components; /// /// Dictionary mapping FSM actions to their entity action data instances. @@ -113,7 +113,8 @@ public Entity( byte id, EntityType type, GameObject hostObject, - GameObject clientObject = null + GameObject clientObject = null, + params EntityComponentType[] types ) { _netClient = netClient; Id = id; @@ -216,8 +217,8 @@ public Entity( ProcessClientFsm(fsm); } - _components = new Dictionary(); - FindComponents(); + _components = new Dictionary(); + HandleComponents(types); Object.Host.SetActive(false); Object.Client.SetActive(false); @@ -313,7 +314,9 @@ private void ProcessClientFsm(PlayMakerFSM fsm) { /// /// Check the host and client objects for components that are supported for networking. /// - private void FindComponents() { + private void HandleComponents(EntityComponentType[] types) { + var addedComponentsString = $"Adding components to entity ({Object.Host.name}, {Id}):"; + var hostHealthManager = Object.Host.GetComponent(); var clientHealthManager = Object.Client.GetComponent(); if (hostHealthManager != null && clientHealthManager != null) { @@ -328,18 +331,27 @@ private void FindComponents() { Object, healthManager ); - _components[EntityNetworkData.DataType.Death] = hmComponent; - _components[EntityNetworkData.DataType.Invincibility] = hmComponent; + _components[EntityComponentType.Death] = hmComponent; + _components[EntityComponentType.Invincibility] = hmComponent; + + addedComponentsString += " Death Invincibility"; } var climber = Object.Client.GetComponent(); if (climber != null) { - _components[EntityNetworkData.DataType.Rotation] = new RotationComponent( + _components[EntityComponentType.Climber] = new ClimberComponent( _netClient, Id, Object, climber ); + _components[EntityComponentType.Rotation] = new RotationComponent( + _netClient, + Id, + Object + ); + + addedComponentsString += " Climber Rotation"; } var hostCollider = Object.Host.GetComponent(); @@ -352,12 +364,14 @@ private void FindComponents() { Client = clientCollider }; - _components[EntityNetworkData.DataType.Collider] = new ColliderComponent( + _components[EntityComponentType.Collider] = new ColliderComponent( _netClient, Id, Object, collider ); + + addedComponentsString += " Collider"; } var hostDamageHero = Object.Host.GetComponent(); @@ -370,12 +384,14 @@ private void FindComponents() { Client = clientDamageHero }; - _components[EntityNetworkData.DataType.DamageHero] = new DamageHeroComponent( + _components[EntityComponentType.DamageHero] = new DamageHeroComponent( _netClient, Id, Object, damageHero ); + + addedComponentsString += " DamageHero"; } var hostMeshRenderer = Object.Host.GetComponent(); @@ -388,50 +404,14 @@ private void FindComponents() { Client = clientMeshRenderer }; - _components[EntityNetworkData.DataType.MeshRenderer] = new MeshRendererComponent( + _components[EntityComponentType.MeshRenderer] = new MeshRendererComponent( _netClient, Id, Object, meshRenderer ); - } - - // TODO: if this gets out of hand with a lot of specifics for a lot of types, perhaps move it to a config file - - // Only adding velocity component to False Knight for now, since it doesn't use gravity - if (Type == EntityType.FalseKnight) { - var rigidbody = Object.Host.GetComponent(); - if (rigidbody != null) { - Logger.Info($"Adding Velocity component to entity: {Object.Host.name}"); - - _components[EntityNetworkData.DataType.Velocity] = new VelocityComponent( - _netClient, - Id, - Object, - rigidbody - ); - } - } - - // Adding a few components specifically for BroodingMawlek - if (Type == EntityType.BroodingMawlek) { - Logger.Info($"Adding ZPosition component to entity: {Object.Host.name}"); - _components[EntityNetworkData.DataType.ZPosition] = new ZPositionComponent( - _netClient, - Id, - Object - ); - var rigidbody = Object.Host.GetComponent(); - if (rigidbody != null) { - Logger.Info($"Adding GravityScale component to entity: {Object.Host.name}"); - _components[EntityNetworkData.DataType.GravityScale] = new GravityScaleComponent( - _netClient, - Id, - Object, - rigidbody - ); - } + addedComponentsString += " MeshRenderer"; } // Find Walker MonoBehaviour and remove it from the client object @@ -445,6 +425,15 @@ private void FindComponents() { if (rigidBody != null) { rigidBody.isKinematic = true; } + + // Instantiate all types defined in the entity registry, which are passed to the constructor + foreach (var type in types) { + _components[type] = ComponentFactory.InstantiateByType(type, _netClient, Id, Object); + + addedComponentsString += $" {type}"; + } + + Logger.Debug(addedComponentsString); } /// @@ -464,7 +453,7 @@ private void OnActionEntered(FsmStateAction self) { $"Entity ({Id}, {Type}) hooked action: {self.Fsm.Name}, {self.State.Name}, {self.GetType()} ({hookedEntityAction.FsmIndex}, {hookedEntityAction.StateIndex}, {hookedEntityAction.ActionIndex})"); var networkData = new EntityNetworkData { - Type = EntityNetworkData.DataType.Fsm + Type = EntityComponentType.Fsm }; if (_fsms.Host.Count > 1) { @@ -526,12 +515,25 @@ private void OnUpdate() { } var newScale = _hasParent ? transform.localScale : transform.lossyScale; - if (newScale != _lastScale) { + var newScaleX = newScale.x > 0; + var newScaleY = newScale.y > 0; + var lastScaleX = _lastScale.x > 0; + var lastScaleY = _lastScale.y > 0; + if (newScaleX != lastScaleX || newScaleY != lastScaleY) { _lastScale = newScale; + byte scaleToSend = 0; + if (newScaleX) { + scaleToSend |= 1; + } + + if (newScaleY) { + scaleToSend |= 2; + } + _netClient.UpdateManager.UpdateEntityScale( Id, - newScale.x > 0 + scaleToSend ); } @@ -727,6 +729,7 @@ public void InitializeHost() { foreach (var component in _components.Values) { component.IsControlled = false; + component.InitializeHost(); } } @@ -792,6 +795,13 @@ public void MakeHost() { Object.Client.SetActive(false); Object.Host.SetActive(clientActive); + if (clientActive) { + var rigidBody = Object.Host.GetComponent(); + if (rigidBody != null) { + rigidBody.isKinematic = false; + } + } + _lastIsActive = _hasParent ? Object.Host.activeSelf : Object.Host.activeInHierarchy; _isControlled = false; @@ -887,28 +897,41 @@ public void UpdatePosition(Vector2 position) { /// Updates the scale of the client entity. /// /// The new scale. - public void UpdateScale(bool scale) { + public void UpdateScale(byte scale) { var transform = Object.Client.transform; var localScale = transform.localScale; var currentScaleX = localScale.x; + var currentScaleY = localScale.y; - if (currentScaleX > 0 != scale) { - // We use the host scale as reference, specifically the lossy scale as we - // don't have a hierarchy on the client entity - var hostScale = Object.Host.transform.lossyScale; - var hostScaleX = hostScale.x; - - var newScaleX = System.Math.Abs(hostScaleX); - if (!scale) { - newScaleX *= -1; + var scaleXPos = (scale & 1) != 0; + var scaleYPos = (scale & 2) != 0; + + // We use the host scale as reference, either lossy or local depending on if we have a parent + var hostScale = _hasParent ? Object.Host.transform.localScale : Object.Host.transform.lossyScale; + var hostScaleX = hostScale.x; + var hostScaleY = hostScale.y; + + // Check whether the sign of the scale is equal to that of the received update + // Otherwise, construct the new scale by using the host scale and correct setting the sign + if (currentScaleX > 0 != scaleXPos) { + currentScaleX = System.Math.Abs(hostScaleX); + if (!scaleXPos) { + currentScaleX *= -1; } + } - transform.localScale = new Vector3( - newScaleX, - hostScale.y, - hostScale.z - ); + if (currentScaleY > 0 != scaleYPos) { + currentScaleY = System.Math.Abs(hostScaleY); + if (!scaleYPos) { + currentScaleY *= -1; + } } + + transform.localScale = new Vector3( + currentScaleX, + currentScaleY, + hostScale.z + ); } /// @@ -1007,7 +1030,7 @@ public void UpdateIsActive(bool active) { /// A list of data to update the client entity with. public void UpdateData(List entityNetworkData) { foreach (var data in entityNetworkData) { - if (data.Type == EntityNetworkData.DataType.Fsm) { + if (data.Type == EntityComponentType.Fsm) { PlayMakerFSM fsm; byte stateIndex; byte actionIndex; diff --git a/HKMP/Game/Client/Entity/EntityInitializer.cs b/HKMP/Game/Client/Entity/EntityInitializer.cs index 0361e076..f91b33d0 100644 --- a/HKMP/Game/Client/Entity/EntityInitializer.cs +++ b/HKMP/Game/Client/Entity/EntityInitializer.cs @@ -19,7 +19,8 @@ internal static class EntityInitializer { "initiate", "initialise", "initialize", - "dormant" + "dormant", + "pause" }; /// @@ -51,8 +52,7 @@ public static void InitializeFsm(PlayMakerFSM fsm) { } if (EntityFsmActions.SupportedActionTypes.Contains(action.GetType())) { - var data = new EntityNetworkData(); - EntityFsmActions.ApplyNetworkDataFromAction(data, action); + EntityFsmActions.ApplyNetworkDataFromAction(null, action); } } } diff --git a/HKMP/Game/Client/Entity/EntityProcessor.cs b/HKMP/Game/Client/Entity/EntityProcessor.cs index e3b6adef..76a76fbe 100644 --- a/HKMP/Game/Client/Entity/EntityProcessor.cs +++ b/HKMP/Game/Client/Entity/EntityProcessor.cs @@ -1,5 +1,7 @@ +using System; using System.Collections.Generic; using System.Linq; +using Hkmp.Game.Client.Entity.Component; using Hkmp.Networking.Client; using Hkmp.Util; using UnityEngine; @@ -131,6 +133,9 @@ private void Process( _lastId++; } + // Get the array of component types for the entity or create an empty one if it is null + var componentTypes = foundEntry.ComponentTypes ?? Array.Empty(); + // Depending on whether a parent object was given we create the entity with this parent object Entity entity; if (parentClientObject == null) { @@ -140,7 +145,8 @@ private void Process( _netClient, id, foundEntry.Type, - gameObject + gameObject, + types: componentTypes ); } else { Logger.Info($"Registering entity ({foundEntry.Type}) '{gameObject.name}' with ID '{_lastId}' with parent: {parentClientObject.name}"); @@ -160,7 +166,8 @@ private void Process( id, foundEntry.Type, gameObject, - clientObject + clientObject, + componentTypes ); } diff --git a/HKMP/Game/Client/Entity/EntityRegistry.cs b/HKMP/Game/Client/Entity/EntityRegistry.cs index 3432f396..89f883bb 100644 --- a/HKMP/Game/Client/Entity/EntityRegistry.cs +++ b/HKMP/Game/Client/Entity/EntityRegistry.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Linq; +using Hkmp.Game.Client.Entity.Component; using Hkmp.Util; using Newtonsoft.Json; using Newtonsoft.Json.Converters; @@ -129,6 +130,12 @@ internal class EntityRegistryEntry { [JsonProperty("parent_name")] public string ParentName { get; set; } + /// + /// Array of additional entity component that should be initialized for the entity. + /// + [JsonProperty("components")] + public EntityComponentType[] ComponentTypes { get; set; } + /// /// List of entries that are children of this entry. /// diff --git a/HKMP/Game/Client/Entity/EntityType.cs b/HKMP/Game/Client/Entity/EntityType.cs index 2524a952..539cf20f 100644 --- a/HKMP/Game/Client/Entity/EntityType.cs +++ b/HKMP/Game/Client/Entity/EntityType.cs @@ -29,5 +29,6 @@ internal enum EntityType { BroodingMawlek, BroodingMawlekArm, BroodingMawlekHead, - BroodingMawlekDummy + BroodingMawlekDummy, + Hornet } diff --git a/HKMP/Game/Server/ServerEntityData.cs b/HKMP/Game/Server/ServerEntityData.cs index a65d2e56..8e29f213 100644 --- a/HKMP/Game/Server/ServerEntityData.cs +++ b/HKMP/Game/Server/ServerEntityData.cs @@ -31,7 +31,7 @@ internal class ServerEntityData { /// /// The last scale of the entity. /// - public bool? Scale { get; set; } + public byte? Scale { get; set; } /// /// The ID of the last played animation. /// diff --git a/HKMP/Game/Server/ServerManager.cs b/HKMP/Game/Server/ServerManager.cs index c1109dc1..aa8c7f93 100644 --- a/HKMP/Game/Server/ServerManager.cs +++ b/HKMP/Game/Server/ServerManager.cs @@ -9,6 +9,7 @@ using Hkmp.Api.Server; using Hkmp.Eventing; using Hkmp.Eventing.ServerEvents; +using Hkmp.Game.Client.Entity.Component; using Hkmp.Game.Command.Server; using Hkmp.Game.Server.Auth; using Hkmp.Game.Settings; @@ -728,7 +729,7 @@ private void OnEntityUpdate(ushort id, EntityUpdate entityUpdate) { } ); - void ReplaceExistingDataWithSameType(EntityNetworkData.DataType type, Packet data) { + void ReplaceExistingDataWithSameType(EntityComponentType type, Packet data) { var existingData = entityData.GenericData.Find( d => d.Type == type ); @@ -743,7 +744,7 @@ void ReplaceExistingDataWithSameType(EntityNetworkData.DataType type, Packet dat } foreach (var updateData in entityUpdate.GenericData) { - if (updateData.Type > EntityNetworkData.DataType.Death) { + if (updateData.Type > EntityComponentType.Death) { ReplaceExistingDataWithSameType(updateData.Type, updateData.Packet); } } diff --git a/HKMP/Networking/Client/ClientUpdateManager.cs b/HKMP/Networking/Client/ClientUpdateManager.cs index d5618470..2c73bbcf 100644 --- a/HKMP/Networking/Client/ClientUpdateManager.cs +++ b/HKMP/Networking/Client/ClientUpdateManager.cs @@ -234,7 +234,7 @@ public void UpdateEntityPosition(byte entityId, Vector2 position) { /// /// The ID of the entity. /// The new scale of the entity. - public void UpdateEntityScale(byte entityId, bool scale) { + public void UpdateEntityScale(byte entityId, byte scale) { lock (Lock) { var entityUpdate = FindOrCreateEntityUpdate(entityId); diff --git a/HKMP/Networking/Packet/Data/EntityUpdate.cs b/HKMP/Networking/Packet/Data/EntityUpdate.cs index 987e1c17..7c95e00f 100644 --- a/HKMP/Networking/Packet/Data/EntityUpdate.cs +++ b/HKMP/Networking/Packet/Data/EntityUpdate.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using Hkmp.Game.Client.Entity.Component; using Hkmp.Logging; using Hkmp.Math; @@ -33,7 +34,7 @@ internal class EntityUpdate : IPacketData { /// /// The boolean representation of the scale of the entity. /// - public bool Scale { get; set; } + public byte Scale { get; set; } /// /// The ID of the animation of the entity. @@ -152,7 +153,7 @@ public void ReadData(IPacket packet) { } if (UpdateTypes.Contains(EntityUpdateType.Scale)) { - Scale = packet.ReadBool(); + Scale = packet.ReadByte(); } if (UpdateTypes.Contains(EntityUpdateType.Animation)) { @@ -197,7 +198,7 @@ internal class EntityNetworkData { /// /// The type of the data. /// - public DataType Type { get; set; } + public EntityComponentType Type { get; set; } /// /// Packet instance containing the data for easy reading and writing of data. /// @@ -227,7 +228,7 @@ public void WriteData(IPacket packet) { /// public void ReadData(IPacket packet) { - Type = (DataType) packet.ReadByte(); + Type = (EntityComponentType) packet.ReadByte(); var length = packet.ReadByte(); var data = new byte[length]; @@ -238,22 +239,6 @@ public void ReadData(IPacket packet) { Packet = new Packet(data); } - - /// - /// Enum for data types. - /// - public enum DataType : byte { - Fsm = 0, - Death, - Invincibility, - Rotation, - Collider, - DamageHero, - MeshRenderer, - Velocity, - GravityScale, - ZPosition - } } /// diff --git a/HKMP/Networking/Server/ServerUpdateManager.cs b/HKMP/Networking/Server/ServerUpdateManager.cs index ed7ea306..e8a559a9 100644 --- a/HKMP/Networking/Server/ServerUpdateManager.cs +++ b/HKMP/Networking/Server/ServerUpdateManager.cs @@ -360,7 +360,7 @@ public void UpdateEntityPosition(byte entityId, Vector2 position) { /// /// The ID of the entity. /// The boolean representation of the scale of the entity. - public void UpdateEntityScale(byte entityId, bool scale) { + public void UpdateEntityScale(byte entityId, byte scale) { lock (Lock) { var entityUpdate = FindOrCreateEntityUpdate(entityId); diff --git a/HKMP/Resource/action-registry.json b/HKMP/Resource/action-registry.json index 90a03771..d07a671e 100644 --- a/HKMP/Resource/action-registry.json +++ b/HKMP/Resource/action-registry.json @@ -115,8 +115,7 @@ "type": "Wait" }, { - "type": "WaitRandom", - "update_field": "everyFrame" + "type": "WaitRandom" }, { "type": "ActivateGameObject", @@ -148,5 +147,13 @@ { "type": "SetVector3Value", "update_field": "everyFrame" + }, + { + "type": "FloatTestToBool", + "update_field": "everyFrame" + }, + { + "type": "FloatInRange", + "update_field": "everyFrame" } ] diff --git a/HKMP/Resource/entity-registry.json b/HKMP/Resource/entity-registry.json index 58ec02f0..c6109ac6 100644 --- a/HKMP/Resource/entity-registry.json +++ b/HKMP/Resource/entity-registry.json @@ -87,6 +87,9 @@ "base_object_name": "False Knight New", "type": "FalseKnight", "fsm_name": "FalseyControl", + "components": [ + "Velocity" + ], "children": [ { "base_object_name": "Head", @@ -139,6 +142,9 @@ "base_object_name": "Mawlek Body", "type": "BroodingMawlek", "fsm_name": "Mawlek Control", + "components": [ + "ZPosition", "GravityScale" + ], "children": [ { "base_object_name": "Mawlek Arm L", @@ -160,5 +166,13 @@ "type": "BroodingMawlekDummy" } ] + }, + { + "base_object_name": "Hornet Boss", + "type": "Hornet", + "fsm_name": "Control", + "components": [ + "GravityScale", "Rotation" + ] } ] From dc5ee0e096cfef1651e1a66bf3a3325f5d6598b3 Mon Sep 17 00:00:00 2001 From: Extremelyd1 Date: Thu, 22 Jun 2023 19:24:27 +0200 Subject: [PATCH 042/216] Add all Greenpath enemies, fix entity registry detection --- .../Client/Entity/Action/EntityFsmActions.cs | 154 +++++++++++++++++- HKMP/Game/Client/Entity/Entity.cs | 17 +- HKMP/Game/Client/Entity/EntityInitializer.cs | 4 +- HKMP/Game/Client/Entity/EntityRegistry.cs | 63 ++++--- HKMP/Game/Client/Entity/EntityType.cs | 14 ++ HKMP/Resource/action-registry.json | 36 ++++ HKMP/Resource/entity-registry.json | 88 ++++++++++ 7 files changed, 334 insertions(+), 42 deletions(-) diff --git a/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs b/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs index 8b0e451f..b8964eb6 100644 --- a/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs +++ b/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs @@ -440,6 +440,7 @@ private static bool GetNetworkDataFromAction(EntityNetworkData data, CreateObjec } private static void ApplyNetworkDataFromAction(EntityNetworkData data, CreateObject action) { + Logger.Debug("ApplyNetworkDataFromAction CreateObject"); var position = new Vector3( data.Packet.ReadFloat(), data.Packet.ReadFloat(), @@ -777,7 +778,7 @@ private static void ApplyNetworkDataFromAction(EntityNetworkData data, PlayParti return; } - if (particleSystem.isPlaying && action.emit.Value <= 0) { + if (!particleSystem.isPlaying && action.emit.Value <= 0) { particleSystem.Play(); } else if (action.emit.Value > 0) { particleSystem.Emit(action.emit.Value); @@ -786,6 +787,34 @@ private static void ApplyNetworkDataFromAction(EntityNetworkData data, PlayParti #endregion + #region StopParticleEmitter + + private static bool GetNetworkDataFromAction(EntityNetworkData data, StopParticleEmitter action) { + return true; + } + + private static void ApplyNetworkDataFromAction(EntityNetworkData data, StopParticleEmitter action) { + if (action.gameObject == null) { + return; + } + + var gameObject = action.Fsm.GetOwnerDefaultTarget(action.gameObject); + if (gameObject == null) { + return; + } + + var particleSystem = gameObject.GetComponent(); + if (particleSystem == null) { + return; + } + + if (particleSystem.isPlaying) { + particleSystem.Stop(); + } + } + + #endregion + #region SetGameObject private static bool GetNetworkDataFromAction(EntityNetworkData data, SetGameObject action) { @@ -1428,4 +1457,127 @@ private static void ApplyNetworkDataFromAction(EntityNetworkData data, SetVeloci } #endregion + + #region iTweenScaleTo + + private static bool GetNetworkDataFromAction(EntityNetworkData data, iTweenScaleTo action) { + return action.loopType == iTween.LoopType.none; + } + + private static void ApplyNetworkDataFromAction(EntityNetworkData data, iTweenScaleTo action) { + var gameObject = action.Fsm.GetOwnerDefaultTarget(action.gameObject); + if (gameObject == null) { + return; + } + + var vector = action.vectorScale.IsNone ? Vector3.zero : action.vectorScale.Value; + + if (!action.transformScale.IsNone && action.transformScale.Value) { + vector = action.transformScale.Value.transform.localScale + vector; + } + + var id = ReflectionHelper.GetField(action, "itweenID"); + + iTween.ScaleTo(gameObject, iTween.Hash( + "scale", + vector, + "name", + action.id.IsNone ? "" : action.id.Value, + action.speed.IsNone ? "time" : "speed", + (float) (action.speed.IsNone ? action.time.IsNone ? 1.0 : action.time.Value : (double) action.speed.Value), + "delay", + (float) (action.delay.IsNone ? 0.0 : (double) action.delay.Value), + "easetype", + action.easeType, + "looptype", + action.loopType, + "oncomplete", + "iTweenOnComplete", + "oncompleteparams", + id, + "onstart", + "iTweenOnStart", + "onstartparams", + id, + "ignoretimescale", + (action.realTime.IsNone ? 0 : action.realTime.Value ? 1 : 0) > 0 + )); + } + + #endregion + + #region SetTag + + private static bool GetNetworkDataFromAction(EntityNetworkData data, SetTag action) { + return true; + } + + private static void ApplyNetworkDataFromAction(EntityNetworkData data, SetTag action) { + var gameObject = action.Fsm.GetOwnerDefaultTarget(action.gameObject); + if (gameObject == null) { + return; + } + + gameObject.tag = action.tag.Value; + } + + #endregion + + #region DestroyObject + + private static bool GetNetworkDataFromAction(EntityNetworkData data, DestroyObject action) { + return true; + } + + private static void ApplyNetworkDataFromAction(EntityNetworkData data, DestroyObject action) { + var gameObject = action.gameObject.Value; + if (gameObject == null) { + return; + } + + var delay = action.delay.Value; + if (delay <= 0) { + Object.Destroy(gameObject); + } else { + Object.Destroy(gameObject, delay); + } + + if (action.detachChildren.Value) { + gameObject.transform.DetachChildren(); + } + } + + #endregion + + #region SetGravity2dScale + + private static bool GetNetworkDataFromAction(EntityNetworkData data, SetGravity2dScale action) { + var gameObject = action.Fsm.GetOwnerDefaultTarget(action.gameObject); + if (gameObject == null) { + return false; + } + + if (IsObjectInRegistry(gameObject)) { + Logger.Debug("Tried getting SetGravity2dScale network data, but entity is in registry"); + return false; + } + + return true; + } + + private static void ApplyNetworkDataFromAction(EntityNetworkData data, SetGravity2dScale action) { + var gameObject = action.Fsm.GetOwnerDefaultTarget(action.gameObject); + if (gameObject == null) { + return; + } + + var rigidBody = gameObject.GetComponent(); + if (rigidBody == null) { + return; + } + + rigidBody.gravityScale = action.gravityScale.Value; + } + + #endregion } diff --git a/HKMP/Game/Client/Entity/Entity.cs b/HKMP/Game/Client/Entity/Entity.cs index cdafd210..dd8b37b4 100644 --- a/HKMP/Game/Client/Entity/Entity.cs +++ b/HKMP/Game/Client/Entity/Entity.cs @@ -775,13 +775,16 @@ public void MakeHost() { var clientScale = Object.Client.transform.localScale; var hostLocalScale = Object.Host.transform.localScale; var hostLossyScale = Object.Host.transform.lossyScale; - - var hierarchyScaleX = hostLossyScale.x / hostLocalScale.x; - var newScaleX = clientScale.x / hierarchyScaleX; - var hierarchyScaleY = hostLossyScale.y / hostLocalScale.y; - var newScaleY = clientScale.y / hierarchyScaleY; - var hierarchyScaleZ = hostLossyScale.z / hostLocalScale.z; - var newScaleZ = clientScale.z / hierarchyScaleZ; + + var newScaleX = hostLocalScale.x == 0 || hostLossyScale.x == 0 + ? 0f + : clientScale.x / (hostLossyScale.x / hostLocalScale.x); + var newScaleY = hostLocalScale.y == 0 || hostLossyScale.y == 0 + ? 0f + : clientScale.y / (hostLossyScale.y / hostLocalScale.y); + var newScaleZ = hostLocalScale.z == 0 || hostLossyScale.z == 0 + ? 0f + : clientScale.z / (hostLossyScale.z / hostLocalScale.z); Object.Host.transform.localScale = _lastScale = new Vector3(newScaleX, newScaleY, newScaleZ); } diff --git a/HKMP/Game/Client/Entity/EntityInitializer.cs b/HKMP/Game/Client/Entity/EntityInitializer.cs index f91b33d0..7aea2f2e 100644 --- a/HKMP/Game/Client/Entity/EntityInitializer.cs +++ b/HKMP/Game/Client/Entity/EntityInitializer.cs @@ -1,7 +1,6 @@ using System; using System.Linq; using Hkmp.Game.Client.Entity.Action; -using Hkmp.Networking.Packet.Data; using HutongGames.PlayMaker.Actions; namespace Hkmp.Game.Client.Entity; @@ -20,7 +19,8 @@ internal static class EntityInitializer { "initialise", "initialize", "dormant", - "pause" + "pause", + "init pause" }; /// diff --git a/HKMP/Game/Client/Entity/EntityRegistry.cs b/HKMP/Game/Client/Entity/EntityRegistry.cs index 89f883bb..ba3e1f3c 100644 --- a/HKMP/Game/Client/Entity/EntityRegistry.cs +++ b/HKMP/Game/Client/Entity/EntityRegistry.cs @@ -56,47 +56,46 @@ public static bool TryGetEntry( GameObject gameObject, out EntityRegistryEntry foundEntry ) { - foundEntry = null; + foreach (var entry in entries) { + if (!gameObject.name.Contains(entry.BaseObjectName)) { + continue; + } - var entry = entries.FirstOrDefault( - entry => gameObject.name.Contains(entry.BaseObjectName) - ); + // If the entry has an FSM name defined and the child object does not have any FSM components + // that match this name, we continue + if (entry.FsmName != null && !gameObject.GetComponents().Any( + childFsm => childFsm.Fsm.Name.Equals(entry.FsmName) + )) { + continue; + } - if (entry == null) { - return false; - } - - // If the entry has an FSM name defined and the child object does not have any FSM components - // that match this name, we return - if (entry.FsmName != null && !gameObject.GetComponents().Any( - childFsm => childFsm.Fsm.Name.Equals(entry.FsmName) - )) { - return false; - } + // If the entry has a parent name defined, we need to check if the parent of the game object matches it + if (entry.ParentName != null) { + var parent = gameObject.transform.parent; + // No parent at all, so it trivially doesn't match the name + if (parent == null) { + continue; + } - // If the entry has a parent name defined, we need to check if the parent of the game object matches it - if (entry.ParentName != null) { - var parent = gameObject.transform.parent; - // No parent at all, so it trivially doesn't match the name - if (parent == null) { - return false; + if (!parent.gameObject.name.Contains(entry.ParentName)) { + continue; + } } - if (!parent.gameObject.name.Contains(entry.ParentName)) { - return false; + // Specifically check if for the entries of type Tiktik, the game object has a Climber component + // Otherwise we might run into game objects that contain "Climber" in their name that aren't actually Tiktiks + if (entry.Type == EntityType.Tiktik) { + if (gameObject.GetComponent() == null) { + continue; + } } - } - // Specifically check if for the entries of type Tiktik, the game object has a Climber component - // Otherwise we might run into game objects that contain "Climber" in their name that aren't actually Tiktiks - if (entry.Type == EntityType.Tiktik) { - if (gameObject.GetComponent() == null) { - return false; - } + foundEntry = entry; + return true; } - foundEntry = entry; - return true; + foundEntry = null; + return false; } } diff --git a/HKMP/Game/Client/Entity/EntityType.cs b/HKMP/Game/Client/Entity/EntityType.cs index 539cf20f..fccface6 100644 --- a/HKMP/Game/Client/Entity/EntityType.cs +++ b/HKMP/Game/Client/Entity/EntityType.cs @@ -30,5 +30,19 @@ internal enum EntityType { BroodingMawlekArm, BroodingMawlekHead, BroodingMawlekDummy, + Mosscreep, + Mosskin, + VolatileMosskin, + FoolEater, + Squit, + Obble, + Gulka, + Durandoo, + Duranda, + MassiveMossCharger, + MossCharger, + MossKnight, + Aluba, + VengeflyKing, Hornet } diff --git a/HKMP/Resource/action-registry.json b/HKMP/Resource/action-registry.json index d07a671e..f72ec85a 100644 --- a/HKMP/Resource/action-registry.json +++ b/HKMP/Resource/action-registry.json @@ -125,6 +125,10 @@ "type": "SetScale", "update_field": "everyFrame" }, + { + "type": "GetScale", + "update_field": "everyFrame" + }, { "type": "SetParticleEmissionRate", "update_field": "everyFrame" @@ -155,5 +159,37 @@ { "type": "FloatInRange", "update_field": "everyFrame" + }, + { + "type": "AddForce2dV2", + "update_field": "everyFrame" + }, + { + "type": "CheckAlertRange", + "update_field": "everyFrame" + }, + { + "type": "CheckCanSeeHero", + "update_field": "everyFrame" + }, + { + "type": "BoolFlipEveryFrame", + "update_field": "everyFrame" + }, + { + "type": "GetAngleToTarget2D", + "update_field": "everyFrame" + }, + { + "type": "DistanceBetweenPoints2D", + "update_field": "everyFrame" + }, + { + "type": "SetGameObject", + "update_field": "everyFrame" + }, + { + "type": "FaceDirection", + "update_field": "everyFrame" } ] diff --git a/HKMP/Resource/entity-registry.json b/HKMP/Resource/entity-registry.json index c6109ac6..b36bc4a5 100644 --- a/HKMP/Resource/entity-registry.json +++ b/HKMP/Resource/entity-registry.json @@ -167,6 +167,94 @@ } ] }, + { + "base_object_name": "Moss Walker", + "type": "Mosscreep", + "fsm_name": "Moss Walker", + "components": [ + "Rotation" + ] + }, + { + "base_object_name": "Mossman_Runner", + "type": "Mosskin", + "fsm_name": "Zombie Swipe" + }, + { + "base_object_name": "Mossman_Shaker", + "type": "VolatileMosskin", + "fsm_name": "Fungus Zombie Attack" + }, + { + "base_object_name": "Plant Trap", + "type": "FoolEater", + "fsm_name": "Plant Trap Control" + }, + { + "base_object_name": "Mosquito", + "type": "Squit", + "fsm_name": "Mozzie", + "components": [ + "Rotation" + ] + }, + { + "base_object_name": "Fat Fly", + "type": "Obble", + "fsm_name": "Fatty Fly Attack" + }, + { + "base_object_name": "Plant Turret", + "type": "Gulka", + "fsm_name": "Plant Turret", + "components": [ + "Rotation" + ] + }, + { + "base_object_name": "Acid Walker", + "type": "Durandoo", + "fsm_name": "Acid Walker", + "components": [ + "Velocity" + ] + }, + { + "base_object_name": "Acid Flyer", + "type": "Duranda", + "fsm_name": "Acid Flyer" + }, + { + "base_object_name": "Mega Moss Charger", + "type": "MassiveMossCharger", + "fsm_name": "Mossy Control", + "components": [ + "GravityScale" + ] + }, + { + "base_object_name": "Moss Charger", + "type": "MossCharger", + "fsm_name": "Mossy Control" + }, + { + "base_object_name": "Moss Knight", + "type": "MossKnight", + "fsm_name": "Moss Knight Control" + }, + { + "base_object_name": "Lazy Flyer", + "type": "Aluba", + "fsm_name": "Lazy Flyer", + "components": [ + "Velocity" + ] + }, + { + "base_object_name": "Giant Buzzer", + "type": "VengeflyKing", + "fsm_name": "Big Buzzer" + }, { "base_object_name": "Hornet Boss", "type": "Hornet", From 9465a808e380407ad2816c6bcf2d955d1b82253d Mon Sep 17 00:00:00 2001 From: Extremelyd1 Date: Sat, 24 Jun 2023 23:45:55 +0200 Subject: [PATCH 043/216] Fix entity spawning for Vengefly --- .../Client/Entity/Action/EntityFsmActions.cs | 26 ++++-- .../Entity/Component/ComponentFactory.cs | 8 ++ .../Entity/Component/EnemySpawnerComponent.cs | 87 +++++++++++++++++++ .../Entity/Component/EntityComponent.cs | 3 +- HKMP/Game/Client/Entity/EntityManager.cs | 70 +++++++++------ HKMP/Game/Client/Entity/EntityRegistry.cs | 8 +- HKMP/Game/Client/Entity/EntitySpawnDetails.cs | 32 +++++++ HKMP/Game/Client/Entity/EntitySpawner.cs | 76 ++++++++++++++-- HKMP/Game/Client/Entity/EntityType.cs | 1 + HKMP/Resource/entity-registry.json | 7 ++ 10 files changed, 277 insertions(+), 41 deletions(-) create mode 100644 HKMP/Game/Client/Entity/Component/EnemySpawnerComponent.cs create mode 100644 HKMP/Game/Client/Entity/EntitySpawnDetails.cs diff --git a/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs b/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs index b8964eb6..f70bb15b 100644 --- a/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs +++ b/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs @@ -44,7 +44,7 @@ internal static class EntityFsmActions { /// /// Event that is called when an entity is spawned from an object. /// - public static event Action EntitySpawnEvent; + public static event Action EntitySpawnEvent; /// /// Dictionary mapping a type of an FSM action to the corresponding method info of the "get" method in this class. @@ -163,6 +163,14 @@ 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. + public static void CallEntitySpawnEvent(EntitySpawnDetails details) { + EntitySpawnEvent?.Invoke(details); + } + /// /// IL edit method for modifying the /// method to store the results of the random calls. @@ -209,8 +217,12 @@ void EmitInstructions() { #region SpawnObjectFromGlobalPool private static bool GetNetworkDataFromAction(EntityNetworkData data, SpawnObjectFromGlobalPool action) { - EntitySpawnEvent?.Invoke(action, action.storeObject.Value); - + EntitySpawnEvent?.Invoke(new EntitySpawnDetails { + Type = EntitySpawnType.FsmAction, + Action = action, + GameObject = action.storeObject.Value + }); + // We first check whether this action results in the spawning of an entity that is managed by the // system. Because if so, it would already be handled by an EntitySpawn packet instead, and this will only // duplicate the spawning and leave it uncontrolled. So we don't send the data at all @@ -392,8 +404,12 @@ private static void ApplyNetworkDataFromAction(EntityNetworkData data, FlingObje #region CreateObject private static bool GetNetworkDataFromAction(EntityNetworkData data, CreateObject action) { - EntitySpawnEvent?.Invoke(action, action.storeObject.Value); - + EntitySpawnEvent?.Invoke(new EntitySpawnDetails { + Type = EntitySpawnType.FsmAction, + Action = action, + GameObject = action.storeObject.Value + }); + // We first check whether this action results in the spawning of an entity that is managed by the // system. Because if so, it would already be handled by an EntitySpawn packet instead, and this will only // duplicate the spawning and leave it uncontrolled. So we don't send the data at all diff --git a/HKMP/Game/Client/Entity/Component/ComponentFactory.cs b/HKMP/Game/Client/Entity/Component/ComponentFactory.cs index c969c346..c526e8c8 100644 --- a/HKMP/Game/Client/Entity/Component/ComponentFactory.cs +++ b/HKMP/Game/Client/Entity/Component/ComponentFactory.cs @@ -37,6 +37,14 @@ HostClientPair objects return new GravityScaleComponent(netClient, entityId, objects, rigidBody); case EntityComponentType.ZPosition: return new ZPositionComponent(netClient, entityId, objects); + case EntityComponentType.EnemySpawner: + var spawnerClient = objects.Client.GetComponent(); + var spawnerHost = objects.Host.GetComponent(); + + return new EnemySpawnerComponent(netClient, entityId, objects, new HostClientPair { + Client = spawnerClient, + Host = spawnerHost + }); default: throw new ArgumentOutOfRangeException(nameof(type), type, $"Could not instantiate entity component for type: {type}"); } diff --git a/HKMP/Game/Client/Entity/Component/EnemySpawnerComponent.cs b/HKMP/Game/Client/Entity/Component/EnemySpawnerComponent.cs new file mode 100644 index 00000000..4a858ef0 --- /dev/null +++ b/HKMP/Game/Client/Entity/Component/EnemySpawnerComponent.cs @@ -0,0 +1,87 @@ +using System.Collections; +using Hkmp.Game.Client.Entity.Action; +using Hkmp.Networking.Client; +using Hkmp.Networking.Packet.Data; +using UnityEngine; + +namespace Hkmp.Game.Client.Entity.Component; + +/// +/// This component manages the EnemySpawner behaviour of the entity. +internal class EnemySpawnerComponent : EntityComponent { + /// + /// The unity component of the entity. + /// + private readonly HostClientPair _spawner; + + public EnemySpawnerComponent( + NetClient netClient, + byte entityId, + HostClientPair gameObject, + HostClientPair spawner + ) : base(netClient, entityId, gameObject) { + _spawner = spawner; + spawner.Client.enabled = false; + + On.EnemySpawner.Start += EnemySpawnerOnStart; + spawner.Host.OnEnemySpawned += OnEnemySpawned; + } + + /// + /// Hook for when the EnemySpawner starts to check whether the interpolation move happens. + /// + private void EnemySpawnerOnStart(On.EnemySpawner.orig_Start orig, EnemySpawner self) { + orig(self); + + if (self != _spawner.Host) { + return; + } + + // If the game object is still active after this method, we know that the interpolation was + // initiated + if (self.gameObject.activeSelf) { + var data = new EntityNetworkData { + Type = EntityComponentType.EnemySpawner + }; + SendData(data); + } + } + + /// + /// Hook for when the enemy object is spawned from the spawner so we can call the spawn event. + /// + /// The spawned game object. + private void OnEnemySpawned(GameObject obj) { + EntityFsmActions.CallEntitySpawnEvent(new EntitySpawnDetails { + Type = EntitySpawnType.SpawnerComponent, + GameObject = obj + }); + } + + /// + public override void InitializeHost() { + } + + /// + public override void Update(EntityNetworkData data) { + iTween.MoveBy(_spawner.Client.gameObject, new Hashtable { + { + "amount", + _spawner.Client.moveBy + }, { + "time", + _spawner.Client.easeTime + }, { + "easetype", + _spawner.Client.easeType + }, { + "space", + Space.World + } + }); + } + + /// + public override void Destroy() { + } +} \ No newline at end of file diff --git a/HKMP/Game/Client/Entity/Component/EntityComponent.cs b/HKMP/Game/Client/Entity/Component/EntityComponent.cs index 9c98bdbb..2f8bd606 100644 --- a/HKMP/Game/Client/Entity/Component/EntityComponent.cs +++ b/HKMP/Game/Client/Entity/Component/EntityComponent.cs @@ -80,5 +80,6 @@ internal enum EntityComponentType : byte { Velocity, GravityScale, ZPosition, - Climber + Climber, + EnemySpawner, } \ No newline at end of file diff --git a/HKMP/Game/Client/Entity/EntityManager.cs b/HKMP/Game/Client/Entity/EntityManager.cs index f1cbbf2f..f4a4f2fe 100644 --- a/HKMP/Game/Client/Entity/EntityManager.cs +++ b/HKMP/Game/Client/Entity/EntityManager.cs @@ -4,7 +4,6 @@ using Hkmp.Networking.Client; using Hkmp.Networking.Packet.Data; using Hkmp.Util; -using HutongGames.PlayMaker; using UnityEngine; using UnityEngine.SceneManagement; using Logger = Hkmp.Logging.Logger; @@ -99,19 +98,23 @@ public void SpawnEntity(byte id, EntityType spawningType, EntityType spawnedType return; } - // Find the list of client FSMs that correspond to an entity with the given type in our current scene - // Doesn't matter which instance of entity it is, because the FSMs will be the same - var clientFsms = _entities.Values.FirstOrDefault( + // Find an entity that has the same type as the spawning type. Doesn't matter if it is the correct instance, + // because the FSMs and components will be identical + var spawningEntity = _entities.Values.FirstOrDefault( e => e.Type == spawningType - )?.GetClientFsms(); - - // If no such FSMs exist we return again, because we can't spawn the new entity - if (clientFsms == null) { - Logger.Warn($"Could not find entity with same type for spawning"); + ); + + if (spawningEntity == null) { + Logger.Warn("Could not find entity with same type for spawning"); return; } - var gameObject = EntitySpawner.SpawnEntityGameObject(spawningType, spawnedType, clientFsms); + var gameObject = EntitySpawner.SpawnEntityGameObject( + spawningType, + spawnedType, + spawningEntity.Object.Client, + spawningEntity.GetClientFsms() + ); var processor = new EntityProcessor { GameObject = gameObject, @@ -174,16 +177,15 @@ public void HandleEntityUpdate(EntityUpdate entityUpdate, bool alreadyInSceneUpd /// /// Callback method for when a game object is spawned from an existing entity. /// - /// The action from which the game object was spawned. - /// The game object that was spawned. - private void OnGameObjectSpawned(FsmStateAction action, GameObject gameObject) { - if (_entities.Values.Any(existingEntity => existingEntity.Object.Host == gameObject)) { + /// The entity spawn details containing how the entity was spawned. + private void OnGameObjectSpawned(EntitySpawnDetails details) { + if (_entities.Values.Any(existingEntity => existingEntity.Object.Host == details.GameObject)) { Logger.Debug("Spawned object was already a registered entity"); return; } var processor = new EntityProcessor { - GameObject = gameObject, + GameObject = details.GameObject, IsSceneHost = _isSceneHost, LateLoad = true }.Process(); @@ -197,19 +199,33 @@ private void OnGameObjectSpawned(FsmStateAction action, GameObject gameObject) { return; } - // Since an entity was created and we are the scene host, we need to notify the server - var spawningObjectName = action.Fsm.GameObject.name; - if (EntityRegistry.TryGetEntry(action.Fsm.GameObject, out var entry)) { - var topLevelEntity = processor.Entities[0]; - - Logger.Info( - $"Notifying server of entity ({spawningObjectName}, {entry.Type}) spawning entity ({gameObject.name}, {topLevelEntity.Type}) with ID {topLevelEntity.Id}"); - _netClient.UpdateManager.SetEntitySpawn( - topLevelEntity.Id, - entry.Type, - topLevelEntity.Type - ); + string spawningObjectName; + EntityType spawningType; + var topLevelEntity = processor.Entities[0]; + + if (details.Type == EntitySpawnType.FsmAction) { + spawningObjectName = details.Action.Fsm.GameObject.name; + if (EntityRegistry.TryGetEntry(details.Action.Fsm.GameObject, out var entry)) { + spawningType = entry.Type; + } else { + Logger.Warn("Could not find registry entry for spawning type of object"); + return; + } + } else if (details.Type == EntitySpawnType.SpawnerComponent) { + spawningObjectName = "Vengefly Summon"; + spawningType = EntityType.VengeflySummon; + } else { + Logger.Error($"Invalid EntitySpawnDetails type: {details.Type}"); + return; } + + Logger.Info( + $"Notifying server of entity ({spawningObjectName}, {spawningType}) spawning entity ({details.GameObject.name}, {topLevelEntity.Type}) with ID {topLevelEntity.Id}"); + _netClient.UpdateManager.SetEntitySpawn( + topLevelEntity.Id, + spawningType, + topLevelEntity.Type + ); } /// diff --git a/HKMP/Game/Client/Entity/EntityRegistry.cs b/HKMP/Game/Client/Entity/EntityRegistry.cs index ba3e1f3c..7817868f 100644 --- a/HKMP/Game/Client/Entity/EntityRegistry.cs +++ b/HKMP/Game/Client/Entity/EntityRegistry.cs @@ -82,12 +82,16 @@ out EntityRegistryEntry foundEntry } } - // Specifically check if for the entries of type Tiktik, the game object has a Climber component - // Otherwise we might run into game objects that contain "Climber" in their name that aren't actually Tiktiks + // Specifically check for entries that don't have a defined FSM whether they contain the + // correct component(s) if (entry.Type == EntityType.Tiktik) { if (gameObject.GetComponent() == null) { continue; } + } else if (entry.Type == EntityType.VengeflySummon) { + if (gameObject.GetComponent() == null) { + continue; + } } foundEntry = entry; diff --git a/HKMP/Game/Client/Entity/EntitySpawnDetails.cs b/HKMP/Game/Client/Entity/EntitySpawnDetails.cs new file mode 100644 index 00000000..c97ae7f2 --- /dev/null +++ b/HKMP/Game/Client/Entity/EntitySpawnDetails.cs @@ -0,0 +1,32 @@ +using HutongGames.PlayMaker; +using UnityEngine; + +namespace Hkmp.Game.Client.Entity; + +/// +/// Class that holds the details for how an entity was spawned on the host client. +/// +internal class EntitySpawnDetails { + /// + /// The type of the spawn. + /// + public EntitySpawnType Type { get; init; } + + /// + /// The FSM action responsible for spawning the entity. + /// + public FsmStateAction Action { get; init; } + + /// + /// The game object that was spawned. + /// + public GameObject GameObject { get; init; } +} + +/// +/// Enumeration of types of possible spawns for the entity. +/// +internal enum EntitySpawnType { + FsmAction, + SpawnerComponent +} diff --git a/HKMP/Game/Client/Entity/EntitySpawner.cs b/HKMP/Game/Client/Entity/EntitySpawner.cs index 9941ef30..39933879 100644 --- a/HKMP/Game/Client/Entity/EntitySpawner.cs +++ b/HKMP/Game/Client/Entity/EntitySpawner.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using Hkmp.Util; using HutongGames.PlayMaker.Actions; +using Modding; using UnityEngine; using Logger = Hkmp.Logging.Logger; @@ -15,25 +16,35 @@ internal static class EntitySpawner { /// /// The type of the entity that spawns the new entity. /// The type of the spawned entity. - /// The list of client FSMs to spawn the game object. + /// The client game object from the spawning entity. + /// The list of client FSMs from the spawning entity. /// The game object for the spawned entity. public static GameObject SpawnEntityGameObject( EntityType spawningType, EntityType spawnedType, + GameObject clientObject, List clientFsms ) { Logger.Info($"Trying to spawn entity game object for: {spawningType}, {spawnedType}"); if (spawningType == EntityType.ElderBaldur && spawnedType == EntityType.Baldur) { - return SpawnBaldurGameObject(clientFsms); + var baldurFsm = clientFsms[0]; + return SpawnBaldurGameObject(baldurFsm); + } + + if (spawningType == EntityType.VengeflyKing && spawnedType == EntityType.VengeflySummon) { + var vengeflyFsm = clientFsms[0]; + return SpawnVengeflySummonObject(vengeflyFsm); + } + + if (spawningType == EntityType.VengeflySummon && spawnedType == EntityType.Vengefly) { + return SpawnVengeflyObjectFromSummon(clientObject); } return null; } - private static GameObject SpawnBaldurGameObject(List clientFsms) { - var fsm = clientFsms[0]; - + private static GameObject SpawnBaldurGameObject(PlayMakerFSM fsm) { var setGameObjectAction = fsm.GetFirstAction("Roller"); var spawnAction = fsm.GetFirstAction("Fire"); @@ -67,5 +78,58 @@ private static GameObject SpawnBaldurGameObject(List clientFsms) { return spawnedObject; } - + + private static GameObject SpawnVengeflySummonObject(PlayMakerFSM fsm) { + var action = fsm.GetFirstAction("Summon"); + + var gameObject = action.gameObject.Value; + + var position = Vector3.zero; + var euler = Vector3.zero; + if (action.spawnPoint.Value != null) { + position = action.spawnPoint.Value.transform.position; + + if (!action.position.IsNone) { + position += action.position.Value; + } + + if (!action.rotation.IsNone) { + euler = action.rotation.Value; + } else { + euler = action.spawnPoint.Value.transform.eulerAngles; + } + } else { + if (!action.position.IsNone) { + position = action.position.Value; + } + + if (!action.rotation.IsNone) { + euler = action.rotation.Value; + } + } + + var createdObject = Object.Instantiate(gameObject, position, Quaternion.Euler(euler)); + action.storeObject.Value = createdObject; + + return createdObject; + } + + private static GameObject SpawnVengeflyObjectFromSummon(GameObject spawningObject) { + var enemySpawner = spawningObject.GetComponent(); + if (enemySpawner == null) { + Logger.Error("Could not create Vengefly object from summon because EnemySpawner component is null"); + return null; + } + + // We check if the object has been created already in the Awake() of the EnemySpawner + // If not, we have to instantiate a new object and return that instead + var spawnedEnemy = ReflectionHelper.GetField(enemySpawner, "spawnedEnemy"); + if (spawnedEnemy == null) { + spawnedEnemy = Object.Instantiate(enemySpawner.enemyPrefab); + } + + spawnedEnemy.SetActive(true); + + return spawnedEnemy; + } } diff --git a/HKMP/Game/Client/Entity/EntityType.cs b/HKMP/Game/Client/Entity/EntityType.cs index fccface6..85a57182 100644 --- a/HKMP/Game/Client/Entity/EntityType.cs +++ b/HKMP/Game/Client/Entity/EntityType.cs @@ -44,5 +44,6 @@ internal enum EntityType { MossKnight, Aluba, VengeflyKing, + VengeflySummon, Hornet } diff --git a/HKMP/Resource/entity-registry.json b/HKMP/Resource/entity-registry.json index b36bc4a5..98470494 100644 --- a/HKMP/Resource/entity-registry.json +++ b/HKMP/Resource/entity-registry.json @@ -255,6 +255,13 @@ "type": "VengeflyKing", "fsm_name": "Big Buzzer" }, + { + "base_object_name": "Buzzer Summon", + "type": "VengeflySummon", + "components": [ + "EnemySpawner" + ] + }, { "base_object_name": "Hornet Boss", "type": "Hornet", From 3a59c1dc866c5bfa15dbd1bf0fa6233de5a90c67 Mon Sep 17 00:00:00 2001 From: Extremelyd1 Date: Sun, 25 Jun 2023 14:21:38 +0200 Subject: [PATCH 044/216] Add Charged Lumafly, fix collider comp --- HKMP/Game/Client/Entity/Component/ColliderComponent.cs | 6 +++--- HKMP/Game/Client/Entity/Entity.cs | 6 +++--- HKMP/Game/Client/Entity/EntityType.cs | 1 + HKMP/Resource/action-registry.json | 4 ++++ HKMP/Resource/entity-registry.json | 5 +++++ 5 files changed, 16 insertions(+), 6 deletions(-) diff --git a/HKMP/Game/Client/Entity/Component/ColliderComponent.cs b/HKMP/Game/Client/Entity/Component/ColliderComponent.cs index a9eab9d0..1aff0da5 100644 --- a/HKMP/Game/Client/Entity/Component/ColliderComponent.cs +++ b/HKMP/Game/Client/Entity/Component/ColliderComponent.cs @@ -7,12 +7,12 @@ namespace Hkmp.Game.Client.Entity.Component; /// -/// This component manages the unity component of an entity. +/// This component manages the unity component of an entity. internal class ColliderComponent : EntityComponent { /// /// Host-client pair for the box collider of the entity. /// - private readonly HostClientPair _collider; + private readonly HostClientPair _collider; /// /// Optional bool indicating whether the collider was last enabled. @@ -23,7 +23,7 @@ public ColliderComponent( NetClient netClient, byte entityId, HostClientPair gameObject, - HostClientPair collider + HostClientPair collider ) : base(netClient, entityId, gameObject) { _collider = collider; diff --git a/HKMP/Game/Client/Entity/Entity.cs b/HKMP/Game/Client/Entity/Entity.cs index dd8b37b4..66675bfd 100644 --- a/HKMP/Game/Client/Entity/Entity.cs +++ b/HKMP/Game/Client/Entity/Entity.cs @@ -354,12 +354,12 @@ private void HandleComponents(EntityComponentType[] types) { addedComponentsString += " Climber Rotation"; } - var hostCollider = Object.Host.GetComponent(); - var clientCollider = Object.Client.GetComponent(); + var hostCollider = Object.Host.GetComponent(); + var clientCollider = Object.Client.GetComponent(); if (hostCollider != null && clientCollider != null) { Logger.Info($"Adding collider component to entity: {Object.Host.name}"); - var collider = new HostClientPair { + var collider = new HostClientPair { Host = hostCollider, Client = clientCollider }; diff --git a/HKMP/Game/Client/Entity/EntityType.cs b/HKMP/Game/Client/Entity/EntityType.cs index 85a57182..99eba2df 100644 --- a/HKMP/Game/Client/Entity/EntityType.cs +++ b/HKMP/Game/Client/Entity/EntityType.cs @@ -45,5 +45,6 @@ internal enum EntityType { Aluba, VengeflyKing, VengeflySummon, + ChargedLumafly, Hornet } diff --git a/HKMP/Resource/action-registry.json b/HKMP/Resource/action-registry.json index f72ec85a..97296df1 100644 --- a/HKMP/Resource/action-registry.json +++ b/HKMP/Resource/action-registry.json @@ -15,6 +15,10 @@ "type": "FloatMultiply", "update_field": "everyFrame" }, + { + "type": "FloatAdd", + "update_field": "everyFrame" + }, { "type": "FloatMultiplyV2", "update_field": "everyFrame" diff --git a/HKMP/Resource/entity-registry.json b/HKMP/Resource/entity-registry.json index 98470494..34d0d877 100644 --- a/HKMP/Resource/entity-registry.json +++ b/HKMP/Resource/entity-registry.json @@ -262,6 +262,11 @@ "EnemySpawner" ] }, + { + "base_object_name": "Zap Cloud", + "type": "ChargedLumafly", + "fsm_name": "zap control" + }, { "base_object_name": "Hornet Boss", "type": "Hornet", From 0dd49d792102ced68933d9a0b78cae8168349f3c Mon Sep 17 00:00:00 2001 From: Extremelyd1 Date: Sun, 25 Jun 2023 23:08:34 +0200 Subject: [PATCH 045/216] Start on Fungal Wastes enemies --- .../Client/Entity/Action/EntityFsmActions.cs | 194 +++++++++++++++++- HKMP/Game/Client/Entity/EntityInitializer.cs | 21 ++ HKMP/Game/Client/Entity/EntityProcessor.cs | 8 +- HKMP/Game/Client/Entity/EntityType.cs | 18 +- HKMP/Resource/action-registry.json | 12 ++ HKMP/Resource/entity-registry.json | 101 +++++++++ 6 files changed, 346 insertions(+), 8 deletions(-) diff --git a/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs b/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs index f70bb15b..9acaf487 100644 --- a/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs +++ b/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs @@ -88,8 +88,9 @@ static EntityFsmActions() { } } - // Register the IL hook for modifying the FSM action method + // Register the IL hooks for modifying FSM action methods IL.HutongGames.PlayMaker.Actions.FlingObjectsFromGlobalPool.OnEnter += FlingObjectsFromGlobalPoolOnEnter; + IL.HutongGames.PlayMaker.Actions.FlingObjectsFromGlobalPoolVel.OnEnter += FlingObjectsFromGlobalPoolVelOnEnter; } /// @@ -210,7 +211,50 @@ void EmitInstructions() { }); } } catch (Exception e) { - Logger.Error($"Could not change FlingObjectFromGlobalPool#OnEnter IL:\n{e}"); + Logger.Error($"Could not change FlingObjectsFromGlobalPool#OnEnter IL:\n{e}"); + } + } + + /// + /// IL edit method for modifying the + /// method to store the results of the random calls. + /// + private static void FlingObjectsFromGlobalPoolVelOnEnter(ILContext il) { + try { + // Create a cursor for this context + var c = new ILCursor(il); + + // Emit instructions for Random.Range calls for 1 int and 4 floats + EmitInstructions(); + EmitInstructions(); + EmitInstructions(); + EmitInstructions(); + EmitInstructions(); + + void EmitInstructions() { + // 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 int off the stack (our random value) and + c.EmitDelegate>((value, instance) => { + if (!RandomActionValues.TryGetValue(instance, out var queue)) { + queue = new Queue(); + RandomActionValues[instance] = queue; + } + + queue.Enqueue(value); + + return value; + }); + } + } catch (Exception e) { + Logger.Error($"Could not change FlingObjectsFromGlobalPoolVel#OnEnter IL:\n{e}"); } } @@ -308,7 +352,7 @@ private static bool GetNetworkDataFromAction(EntityNetworkData data, FlingObject } if (queue.Count == 0) { - Logger.Debug("Getting data for FlingObjectFromGlobalPool has not enough items in queue 1"); + Logger.Debug("Getting data for FlingObjectsFromGlobalPool has not enough items in queue 1"); return false; } @@ -322,7 +366,7 @@ private static bool GetNetworkDataFromAction(EntityNetworkData data, FlingObject for (var i = 0; i < numSpawns; i++) { if (action.originVariationX != null) { if (queue.Count == 0) { - Logger.Debug("Getting data for FlingObjectFromGlobalPool has not enough items in queue 2"); + Logger.Debug("Getting data for FlingObjectsFromGlobalPool has not enough items in queue 2"); return false; } @@ -334,7 +378,7 @@ private static bool GetNetworkDataFromAction(EntityNetworkData data, FlingObject if (action.originVariationY != null) { if (queue.Count == 0) { - Logger.Debug("Getting data for FlingObjectFromGlobalPool has not enough items in queue 3"); + Logger.Debug("Getting data for FlingObjectsFromGlobalPool has not enough items in queue 3"); return false; } @@ -345,7 +389,7 @@ private static bool GetNetworkDataFromAction(EntityNetworkData data, FlingObject } if (queue.Count < 2) { - Logger.Debug("Getting data for FlingObjectFromGlobalPool has not enough items in queue 4"); + Logger.Debug("Getting data for FlingObjectsFromGlobalPool has not enough items in queue 4"); queue.Clear(); return false; } @@ -401,6 +445,112 @@ private static void ApplyNetworkDataFromAction(EntityNetworkData data, FlingObje #endregion + #region FlingObjectsFromGlobalPoolVel + + private static bool GetNetworkDataFromAction(EntityNetworkData data, FlingObjectsFromGlobalPoolVel action) { + var position = Vector3.zero; + + var spawnPoint = action.spawnPoint.Value; + if (spawnPoint != null) { + position = spawnPoint.transform.position; + if (!action.position.IsNone) { + position += action.position.Value; + } + } else if (!action.position.IsNone) { + position = action.position.Value; + } + + if (!RandomActionValues.TryGetValue(action, out var queue)) { + return false; + } + + if (queue.Count == 0) { + Logger.Debug("Getting data for FlingObjectsFromGlobalPoolVel has not enough items in queue 1"); + return false; + } + + data.Packet.Write(position.x); + data.Packet.Write(position.y); + data.Packet.Write(position.z); + + var numSpawns = (int) queue.Dequeue(); + data.Packet.Write((byte) numSpawns); + + for (var i = 0; i < numSpawns; i++) { + if (action.originVariationX != null) { + if (queue.Count == 0) { + Logger.Debug("Getting data for FlingObjectsFromGlobalPoolVel has not enough items in queue 2"); + return false; + } + + var originVariationX = (float) queue.Dequeue(); + data.Packet.Write(originVariationX); + } else { + data.Packet.Write(0f); + } + + if (action.originVariationY != null) { + if (queue.Count == 0) { + Logger.Debug("Getting data for FlingObjectsFromGlobalPoolVel has not enough items in queue 3"); + return false; + } + + var originVariationY = (float) queue.Dequeue(); + data.Packet.Write(originVariationY); + } else { + data.Packet.Write(0f); + } + + if (queue.Count < 2) { + Logger.Debug("Getting data for FlingObjectsFromGlobalPoolVel has not enough items in queue 4"); + queue.Clear(); + return false; + } + + var speedX = (float) queue.Dequeue(); + var speedY = (float) queue.Dequeue(); + + data.Packet.Write(speedX); + data.Packet.Write(speedY); + } + + queue.Clear(); + return true; + } + + private static void ApplyNetworkDataFromAction(EntityNetworkData data, FlingObjectsFromGlobalPoolVel action) { + var position = new Vector3( + data.Packet.ReadFloat(), + data.Packet.ReadFloat(), + data.Packet.ReadFloat() + ); + + var numSpawns = data.Packet.ReadByte(); + for (var i = 0; i < numSpawns; i++) { + var go = action.gameObject.Value.Spawn(position, Quaternion.Euler(Vector3.zero)); + + var originVariationX = data.Packet.ReadFloat(); + position.x += originVariationX; + + var originVariationY = data.Packet.ReadFloat(); + position.y += originVariationY; + + go.transform.position = position; + + var speedX = data.Packet.ReadFloat(); + var speedY = data.Packet.ReadFloat(); + + var rigidBody = go.GetComponent(); + if (rigidBody == null) { + return; + } + + rigidBody.velocity = new Vector2(speedX, speedY); + } + } + + #endregion + #region CreateObject private static bool GetNetworkDataFromAction(EntityNetworkData data, CreateObject action) { @@ -1596,4 +1746,36 @@ private static void ApplyNetworkDataFromAction(EntityNetworkData data, SetGravit } #endregion + + #region SetCollider + + private static bool GetNetworkDataFromAction(EntityNetworkData data, SetCollider action) { + var gameObject = action.Fsm.GetOwnerDefaultTarget(action.gameObject); + if (gameObject == null) { + return false; + } + + if (IsObjectInRegistry(gameObject)) { + Logger.Debug("Tried getting SetCollider network data, but entity is in registry"); + return false; + } + + return true; + } + + private static void ApplyNetworkDataFromAction(EntityNetworkData data, SetCollider action) { + var gameObject = action.Fsm.GetOwnerDefaultTarget(action.gameObject); + if (gameObject == null) { + return; + } + + var collider = gameObject.GetComponent(); + if (collider == null) { + return; + } + + collider.enabled = action.active.Value; + } + + #endregion } diff --git a/HKMP/Game/Client/Entity/EntityInitializer.cs b/HKMP/Game/Client/Entity/EntityInitializer.cs index 7aea2f2e..84fbfb90 100644 --- a/HKMP/Game/Client/Entity/EntityInitializer.cs +++ b/HKMP/Game/Client/Entity/EntityInitializer.cs @@ -1,7 +1,11 @@ using System; +using System.Collections; using System.Linq; using Hkmp.Game.Client.Entity.Action; +using Hkmp.Util; using HutongGames.PlayMaker.Actions; +using UnityEngine; +using Logger = Hkmp.Logging.Logger; namespace Hkmp.Game.Client.Entity; @@ -52,6 +56,23 @@ public static void InitializeFsm(PlayMakerFSM fsm) { } if (EntityFsmActions.SupportedActionTypes.Contains(action.GetType())) { + if (action.Fsm == null) { + Logger.Debug("Initializing FSM and action.Fsm is null, starting coroutine"); + + MonoBehaviourUtil.Instance.StartCoroutine(WaitForActionInitialization()); + IEnumerator WaitForActionInitialization() { + while (action.Fsm == null) { + yield return new WaitForSeconds(0.1f); + } + + Logger.Debug("Initializing FSM action completed"); + + EntityFsmActions.ApplyNetworkDataFromAction(null, action); + } + + continue; + } + EntityFsmActions.ApplyNetworkDataFromAction(null, action); } } diff --git a/HKMP/Game/Client/Entity/EntityProcessor.cs b/HKMP/Game/Client/Entity/EntityProcessor.cs index 76a76fbe..e748fa57 100644 --- a/HKMP/Game/Client/Entity/EntityProcessor.cs +++ b/HKMP/Game/Client/Entity/EntityProcessor.cs @@ -153,7 +153,13 @@ private void Process( // Find the correct child of the client object of the parent entity var clientObject = parentClientObject.GetChildren() - .FirstOrDefault(c => c.name.Contains(foundEntry.BaseObjectName)); + .FirstOrDefault(c => { + if (Entities.Any(processedEntity => processedEntity.Object.Client == c)) { + return false; + } + + return c.name.Contains(foundEntry.BaseObjectName); + }); if (clientObject == null) { Logger.Warn("Could not find child of client object of parent entity"); return; diff --git a/HKMP/Game/Client/Entity/EntityType.cs b/HKMP/Game/Client/Entity/EntityType.cs index 99eba2df..5d692407 100644 --- a/HKMP/Game/Client/Entity/EntityType.cs +++ b/HKMP/Game/Client/Entity/EntityType.cs @@ -46,5 +46,21 @@ internal enum EntityType { VengeflyKing, VengeflySummon, ChargedLumafly, - Hornet + Hornet, + Uoma, + Ooma, + Ambloom, + Fungling, + Fungoon, + Sporg, + FungifiedHusk, + Shrumeling, + ShrumalWarrior, + MantisYouth, + MantisWarrior, + MantisThrone, + MantisLordBattle, + MantisLord, + MantisLordCage, + MantisLordFloor } diff --git a/HKMP/Resource/action-registry.json b/HKMP/Resource/action-registry.json index 97296df1..cf2d76a1 100644 --- a/HKMP/Resource/action-registry.json +++ b/HKMP/Resource/action-registry.json @@ -164,6 +164,10 @@ "type": "FloatInRange", "update_field": "everyFrame" }, + { + "type": "AddForce2d", + "update_field": "everyFrame" + }, { "type": "AddForce2dV2", "update_field": "everyFrame" @@ -195,5 +199,13 @@ { "type": "FaceDirection", "update_field": "everyFrame" + }, + { + "type": "FaceObject", + "update_field": "everyFrame" + }, + { + "type": "Translate", + "update_field": "everyFrame" } ] diff --git a/HKMP/Resource/entity-registry.json b/HKMP/Resource/entity-registry.json index 34d0d877..3bd198ed 100644 --- a/HKMP/Resource/entity-registry.json +++ b/HKMP/Resource/entity-registry.json @@ -274,5 +274,106 @@ "components": [ "GravityScale", "Rotation" ] + }, + { + "base_object_name": "Jellyfish Baby", + "type": "Uoma", + "fsm_name": "Jellyfish Baby" + }, + { + "base_object_name": "Jellyfish", + "type": "Ooma", + "fsm_name": "Jellyfish" + }, + { + "base_object_name": "Fung Crawler", + "type": "Ambloom" + }, + { + "base_object_name": "Fungoon Baby", + "type": "Fungling", + "fsm_name": "Fungoon baby" + }, + { + "base_object_name": "Fungus Flyer", + "type": "Fungoon", + "fsm_name": "Fungus Flyer" + }, + { + "base_object_name": "Mushroom Turret", + "type": "Sporg", + "fsm_name": "Shroom Turret" + }, + { + "base_object_name": "Zombie Fungus", + "type": "FungifiedHusk", + "fsm_name": "Fungus Zombie Attack" + }, + { + "base_object_name": "Mushroom Baby", + "type": "Shrumeling", + "fsm_name": "Mushroom Mini" + }, + { + "base_object_name": "Mushroom Roller", + "type": "ShrumalWarrior", + "fsm_name": "Mush Roller" + }, + { + "base_object_name": "Mantis Flyer Child", + "type": "MantisYouth", + "fsm_name": "Mantis Flyer", + "components": [ + "Rotation" + ] + }, + { + "base_object_name": "Mantis", + "type": "MantisWarrior", + "fsm_name": "Mantis" + }, + { + "base_object_name": "Mantis Lord Throne", + "type": "MantisThrone", + "fsm_name": "Mantis Throne Sub" + }, + { + "base_object_name": "Mantis Lord Throne", + "type": "MantisThrone", + "fsm_name": "Mantis Throne Main" + }, + { + "base_object_name": "Battle Main", + "type": "MantisLordBattle", + "fsm_name": "Start", + "children": [ + { + "base_object_name": "Mantis Lord", + "type": "MantisLord", + "fsm_name": "Mantis Lord" + } + ] + }, + { + "base_object_name": "Battle Sub", + "type": "MantisLordBattle", + "fsm_name": "Start", + "children": [ + { + "base_object_name": "Mantis Lord S", + "type": "MantisLord", + "fsm_name": "Mantis Lord" + } + ] + }, + { + "base_object_name": "mantis_cage_down", + "type": "MantisLordCage", + "fsm_name": "Cage Control" + }, + { + "base_object_name": "mantis_lord_opening_floors", + "type": "MantisLordFloor", + "fsm_name": "Floor Control" } ] From f2b43249c4646457a31c362ab3503b39308632a6 Mon Sep 17 00:00:00 2001 From: Extremelyd1 Date: Mon, 26 Jun 2023 20:09:02 +0200 Subject: [PATCH 046/216] Fixes for Mantis Lords and Ooma corpses --- .../Client/Entity/Action/EntityFsmActions.cs | 51 +++++++----- .../Component/ChildrenActivationComponent.cs | 83 +++++++++++++++++++ .../Entity/Component/ComponentFactory.cs | 2 + .../Entity/Component/EntityComponent.cs | 1 + HKMP/Game/Client/Entity/Entity.cs | 20 ++++- HKMP/Game/Client/Entity/EntityManager.cs | 24 ++++-- HKMP/Game/Client/Entity/EntitySpawner.cs | 80 ++++++++++-------- HKMP/Game/Client/Entity/EntityType.cs | 2 + HKMP/Resource/entity-registry.json | 26 +++++- HKMP/Util/GameObjectExtensions.cs | 2 +- 10 files changed, 226 insertions(+), 65 deletions(-) create mode 100644 HKMP/Game/Client/Entity/Component/ChildrenActivationComponent.cs diff --git a/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs b/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs index 9acaf487..016ce92d 100644 --- a/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs +++ b/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs @@ -44,7 +44,7 @@ internal static class EntityFsmActions { /// /// Event that is called when an entity is spawned from an object. /// - public static event Action EntitySpawnEvent; + 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. @@ -168,8 +168,9 @@ private static bool IsObjectInRegistry(GameObject gameObject) { /// Method to call the spawn event externally. TODO: refactor this into something more appropriate /// /// The spawn details for the event. - public static void CallEntitySpawnEvent(EntitySpawnDetails details) { - EntitySpawnEvent?.Invoke(details); + /// Whether an entity was registered from this spawn. + public static bool CallEntitySpawnEvent(EntitySpawnDetails details) { + return EntitySpawnEvent != null && EntitySpawnEvent.Invoke(details); } /// @@ -261,21 +262,18 @@ void EmitInstructions() { #region SpawnObjectFromGlobalPool private static bool GetNetworkDataFromAction(EntityNetworkData data, SpawnObjectFromGlobalPool action) { - EntitySpawnEvent?.Invoke(new EntitySpawnDetails { - Type = EntitySpawnType.FsmAction, - Action = action, - GameObject = action.storeObject.Value - }); - // We first check whether this action results in the spawning of an entity that is managed by the // system. Because if so, it would already be handled by an EntitySpawn packet instead, and this will only // duplicate the spawning and leave it uncontrolled. So we don't send the data at all - var toSpawnObject = action.storeObject.Value; - if (IsObjectInRegistry(toSpawnObject)) { + if (EntitySpawnEvent != null && EntitySpawnEvent.Invoke(new EntitySpawnDetails { + Type = EntitySpawnType.FsmAction, + Action = action, + GameObject = action.storeObject.Value + })) { Logger.Debug($"Tried getting SpawnObjectFromGlobalPool network data, but spawned object is entity"); return false; } - + var position = Vector3.zero; var euler = Vector3.up; @@ -554,17 +552,14 @@ private static void ApplyNetworkDataFromAction(EntityNetworkData data, FlingObje #region CreateObject private static bool GetNetworkDataFromAction(EntityNetworkData data, CreateObject action) { - EntitySpawnEvent?.Invoke(new EntitySpawnDetails { - Type = EntitySpawnType.FsmAction, - Action = action, - GameObject = action.storeObject.Value - }); - // We first check whether this action results in the spawning of an entity that is managed by the // system. Because if so, it would already be handled by an EntitySpawn packet instead, and this will only // duplicate the spawning and leave it uncontrolled. So we don't send the data at all - var toSpawnObject = action.storeObject.Value; - if (IsObjectInRegistry(toSpawnObject)) { + if (EntitySpawnEvent != null && EntitySpawnEvent.Invoke(new EntitySpawnDetails { + Type = EntitySpawnType.FsmAction, + Action = action, + GameObject = action.storeObject.Value + })) { Logger.Debug($"Tried getting CreateObject network data, but spawned object is entity"); return false; } @@ -1778,4 +1773,20 @@ private static void ApplyNetworkDataFromAction(EntityNetworkData data, SetCollid } #endregion + + #region SetStringValue + + private static bool GetNetworkDataFromAction(EntityNetworkData data, SetStringValue action) { + if (action.stringVariable == null || action.stringValue == null) { + return false; + } + + return true; + } + + private static void ApplyNetworkDataFromAction(EntityNetworkData data, SetStringValue action) { + action.stringVariable.Value = action.stringValue.Value; + } + + #endregion } diff --git a/HKMP/Game/Client/Entity/Component/ChildrenActivationComponent.cs b/HKMP/Game/Client/Entity/Component/ChildrenActivationComponent.cs new file mode 100644 index 00000000..d1da1a30 --- /dev/null +++ b/HKMP/Game/Client/Entity/Component/ChildrenActivationComponent.cs @@ -0,0 +1,83 @@ +using System.Collections.Generic; +using Hkmp.Networking.Client; +using Hkmp.Networking.Packet.Data; +using Hkmp.Util; +using UnityEngine; + +namespace Hkmp.Game.Client.Entity.Component; + +/// +/// This component manages the gravity scale of an entity. +internal class ChildrenActivationComponent : EntityComponent { + private readonly List _hostChildren; + private readonly List _clientChildren; + + private bool _lastActive; + + public ChildrenActivationComponent( + NetClient netClient, + byte entityId, + HostClientPair gameObject + ) : base(netClient, entityId, gameObject) { + _hostChildren = gameObject.Host.GetChildren(); + if (_hostChildren.Count == 0) { + return; + } + + _lastActive = _hostChildren[0].activeSelf; + + _clientChildren = gameObject.Client.GetChildren(); + + MonoBehaviourUtil.Instance.OnUpdateEvent += OnUpdate; + } + + /// + /// Callback for checking the gravity scale each update. + /// + private void OnUpdate() { + if (IsControlled) { + return; + } + + if (GameObject.Host == null) { + return; + } + + var newActive = _hostChildren[0].activeSelf; + if (newActive != _lastActive) { + _lastActive = newActive; + + var data = new EntityNetworkData { + Type = EntityComponentType.ChildrenActivation + }; + data.Packet.Write(newActive); + + SendData(data); + } + } + + /// + public override void InitializeHost() { + } + + /// + public override void Update(EntityNetworkData data) { + if (!IsControlled) { + return; + } + + var newActive = data.Packet.ReadBool(); + + foreach (var child in _hostChildren) { + child.SetActive(newActive); + } + foreach (var child in _clientChildren) { + child.SetActive(newActive); + } + } + + /// + public override void Destroy() { + MonoBehaviourUtil.Instance.OnUpdateEvent -= OnUpdate; + } +} \ No newline at end of file diff --git a/HKMP/Game/Client/Entity/Component/ComponentFactory.cs b/HKMP/Game/Client/Entity/Component/ComponentFactory.cs index c526e8c8..d64c1277 100644 --- a/HKMP/Game/Client/Entity/Component/ComponentFactory.cs +++ b/HKMP/Game/Client/Entity/Component/ComponentFactory.cs @@ -45,6 +45,8 @@ HostClientPair objects Client = spawnerClient, Host = spawnerHost }); + case EntityComponentType.ChildrenActivation: + return new ChildrenActivationComponent(netClient, entityId, objects); default: throw new ArgumentOutOfRangeException(nameof(type), type, $"Could not instantiate entity component for type: {type}"); } diff --git a/HKMP/Game/Client/Entity/Component/EntityComponent.cs b/HKMP/Game/Client/Entity/Component/EntityComponent.cs index 2f8bd606..77da03eb 100644 --- a/HKMP/Game/Client/Entity/Component/EntityComponent.cs +++ b/HKMP/Game/Client/Entity/Component/EntityComponent.cs @@ -82,4 +82,5 @@ internal enum EntityComponentType : byte { ZPosition, Climber, EnemySpawner, + ChildrenActivation } \ No newline at end of file diff --git a/HKMP/Game/Client/Entity/Entity.cs b/HKMP/Game/Client/Entity/Entity.cs index 66675bfd..e8e1c2a4 100644 --- a/HKMP/Game/Client/Entity/Entity.cs +++ b/HKMP/Game/Client/Entity/Entity.cs @@ -219,6 +219,24 @@ params EntityComponentType[] types _components = new Dictionary(); HandleComponents(types); + + // Specific handling of Oomas, since the corpse of the entity will be handled by the system as well, we + // need to remove it from this entity. Otherwise we have duplicate corpses on the client-side + if (Type == EntityType.Ooma) { + Logger.Debug("Entity is Ooma, deleting death effects and corpse from client entity"); + + var enemyDeathEffects = Object.Client.GetComponent(); + if (enemyDeathEffects == null) { + Logger.Debug(" EnemyDeathEffects is null, cannot remove"); + } + UnityEngine.Object.Destroy(enemyDeathEffects); + + var corpse = Object.Client.FindGameObjectInChildren("Corpse Jellyfish(Clone)"); + if (corpse == null) { + Logger.Debug(" Could not find corpse in children"); + } + UnityEngine.Object.Destroy(corpse); + } Object.Host.SetActive(false); Object.Client.SetActive(false); @@ -800,7 +818,7 @@ public void MakeHost() { if (clientActive) { var rigidBody = Object.Host.GetComponent(); - if (rigidBody != null) { + if (rigidBody != null && Type != EntityType.MantisLord) { rigidBody.isKinematic = false; } } diff --git a/HKMP/Game/Client/Entity/EntityManager.cs b/HKMP/Game/Client/Entity/EntityManager.cs index f4a4f2fe..18cef9bd 100644 --- a/HKMP/Game/Client/Entity/EntityManager.cs +++ b/HKMP/Game/Client/Entity/EntityManager.cs @@ -178,10 +178,11 @@ public void HandleEntityUpdate(EntityUpdate entityUpdate, bool alreadyInSceneUpd /// Callback method for when a game object is spawned from an existing entity. /// /// The entity spawn details containing how the entity was spawned. - private void OnGameObjectSpawned(EntitySpawnDetails details) { + /// Whether an entity was registered from this spawn. + private bool OnGameObjectSpawned(EntitySpawnDetails details) { if (_entities.Values.Any(existingEntity => existingEntity.Object.Host == details.GameObject)) { Logger.Debug("Spawned object was already a registered entity"); - return; + return false; } var processor = new EntityProcessor { @@ -191,12 +192,12 @@ private void OnGameObjectSpawned(EntitySpawnDetails details) { }.Process(); if (!processor.Success) { - return; + return false; } if (!_isSceneHost) { Logger.Warn("Game object was spawned while not scene host, this shouldn't happen"); - return; + return false; } string spawningObjectName; @@ -209,14 +210,14 @@ private void OnGameObjectSpawned(EntitySpawnDetails details) { spawningType = entry.Type; } else { Logger.Warn("Could not find registry entry for spawning type of object"); - return; + return false; } } else if (details.Type == EntitySpawnType.SpawnerComponent) { spawningObjectName = "Vengefly Summon"; spawningType = EntityType.VengeflySummon; } else { Logger.Error($"Invalid EntitySpawnDetails type: {details.Type}"); - return; + return false; } Logger.Info( @@ -226,6 +227,8 @@ private void OnGameObjectSpawned(EntitySpawnDetails details) { spawningType, topLevelEntity.Type ); + + return true; } /// @@ -305,7 +308,14 @@ private void FindEntitiesInScene(Scene scene, bool lateLoad) { // Filter out GameObjects not in the current scene var objectsToCheck = Object.FindObjectsOfType() .Where(fsm => fsm.gameObject.scene == scene) - .Select(fsm => fsm.gameObject) + .Select(fsm => { + var enemyDeathEffects = fsm.gameObject.GetComponent(); + if (enemyDeathEffects != null) { + enemyDeathEffects.PreInstantiate(); + } + + return fsm.gameObject; + }) .SelectMany(obj => obj.GetChildren().Prepend(obj)) .Concat(Object.FindObjectsOfType().Select(climber => climber.gameObject)) .Where(obj => obj.scene == scene) diff --git a/HKMP/Game/Client/Entity/EntitySpawner.cs b/HKMP/Game/Client/Entity/EntitySpawner.cs index 39933879..8f59b98d 100644 --- a/HKMP/Game/Client/Entity/EntitySpawner.cs +++ b/HKMP/Game/Client/Entity/EntitySpawner.cs @@ -28,22 +28,57 @@ List clientFsms Logger.Info($"Trying to spawn entity game object for: {spawningType}, {spawnedType}"); if (spawningType == EntityType.ElderBaldur && spawnedType == EntityType.Baldur) { - var baldurFsm = clientFsms[0]; - return SpawnBaldurGameObject(baldurFsm); + return SpawnBaldurGameObject(clientFsms[0]); } if (spawningType == EntityType.VengeflyKing && spawnedType == EntityType.VengeflySummon) { - var vengeflyFsm = clientFsms[0]; - return SpawnVengeflySummonObject(vengeflyFsm); + return SpawnVengeflySummonObject(clientFsms[0]); } if (spawningType == EntityType.VengeflySummon && spawnedType == EntityType.Vengefly) { return SpawnVengeflyObjectFromSummon(clientObject); } + if (spawningType == EntityType.OomaCorpse && spawnedType == EntityType.OomaCore) { + return SpawnOomaCoreObject(clientFsms[0]); + } + return null; } + private static GameObject SpawnFromCreateObject(CreateObject action) { + var gameObject = action.gameObject.Value; + + var position = Vector3.zero; + var euler = Vector3.zero; + if (action.spawnPoint.Value != null) { + position = action.spawnPoint.Value.transform.position; + + if (!action.position.IsNone) { + position += action.position.Value; + } + + if (!action.rotation.IsNone) { + euler = action.rotation.Value; + } else { + euler = action.spawnPoint.Value.transform.eulerAngles; + } + } else { + if (!action.position.IsNone) { + position = action.position.Value; + } + + if (!action.rotation.IsNone) { + euler = action.rotation.Value; + } + } + + var createdObject = Object.Instantiate(gameObject, position, Quaternion.Euler(euler)); + action.storeObject.Value = createdObject; + + return createdObject; + } + private static GameObject SpawnBaldurGameObject(PlayMakerFSM fsm) { var setGameObjectAction = fsm.GetFirstAction("Roller"); var spawnAction = fsm.GetFirstAction("Fire"); @@ -82,36 +117,7 @@ private static GameObject SpawnBaldurGameObject(PlayMakerFSM fsm) { private static GameObject SpawnVengeflySummonObject(PlayMakerFSM fsm) { var action = fsm.GetFirstAction("Summon"); - var gameObject = action.gameObject.Value; - - var position = Vector3.zero; - var euler = Vector3.zero; - if (action.spawnPoint.Value != null) { - position = action.spawnPoint.Value.transform.position; - - if (!action.position.IsNone) { - position += action.position.Value; - } - - if (!action.rotation.IsNone) { - euler = action.rotation.Value; - } else { - euler = action.spawnPoint.Value.transform.eulerAngles; - } - } else { - if (!action.position.IsNone) { - position = action.position.Value; - } - - if (!action.rotation.IsNone) { - euler = action.rotation.Value; - } - } - - var createdObject = Object.Instantiate(gameObject, position, Quaternion.Euler(euler)); - action.storeObject.Value = createdObject; - - return createdObject; + return SpawnFromCreateObject(action); } private static GameObject SpawnVengeflyObjectFromSummon(GameObject spawningObject) { @@ -132,4 +138,10 @@ private static GameObject SpawnVengeflyObjectFromSummon(GameObject spawningObjec return spawnedEnemy; } + + private static GameObject SpawnOomaCoreObject(PlayMakerFSM fsm) { + var action = fsm.GetAction("Explode", 3); + + return SpawnFromCreateObject(action); + } } diff --git a/HKMP/Game/Client/Entity/EntityType.cs b/HKMP/Game/Client/Entity/EntityType.cs index 5d692407..15d19fd7 100644 --- a/HKMP/Game/Client/Entity/EntityType.cs +++ b/HKMP/Game/Client/Entity/EntityType.cs @@ -49,6 +49,8 @@ internal enum EntityType { Hornet, Uoma, Ooma, + OomaCorpse, + OomaCore, Ambloom, Fungling, Fungoon, diff --git a/HKMP/Resource/entity-registry.json b/HKMP/Resource/entity-registry.json index 3bd198ed..d9c6e895 100644 --- a/HKMP/Resource/entity-registry.json +++ b/HKMP/Resource/entity-registry.json @@ -285,6 +285,19 @@ "type": "Ooma", "fsm_name": "Jellyfish" }, + { + "base_object_name": "Corpse Jellyfish", + "type": "OomaCorpse", + "fsm_name": "corpse" + }, + { + "base_object_name": "Lil Jellyfish", + "type": "OomaCore", + "fsm_name": "Lil Jelly", + "components": [ + "Rotation" + ] + }, { "base_object_name": "Fung Crawler", "type": "Ambloom" @@ -369,11 +382,20 @@ { "base_object_name": "mantis_cage_down", "type": "MantisLordCage", - "fsm_name": "Cage Control" + "fsm_name": "Cage Control", + "components": [ + "ChildrenActivation" + ] }, { "base_object_name": "mantis_lord_opening_floors", "type": "MantisLordFloor", - "fsm_name": "Floor Control" + "fsm_name": "Floor Control", + "children": [ + { + "base_object_name": "Floor", + "type": "MantisLordFloor" + } + ] } ] diff --git a/HKMP/Util/GameObjectExtensions.cs b/HKMP/Util/GameObjectExtensions.cs index 0d988e06..292b8adc 100644 --- a/HKMP/Util/GameObjectExtensions.cs +++ b/HKMP/Util/GameObjectExtensions.cs @@ -30,7 +30,7 @@ string name return null; } - public static IEnumerable GetChildren(this GameObject gameObject) { + public static List GetChildren(this GameObject gameObject) { var children = new List(); for (var i = 0; i < gameObject.transform.childCount; i++) { children.Add(gameObject.transform.GetChild(i).gameObject); From 026216d93f3cf4a2762fc47deac3c53b04937516 Mon Sep 17 00:00:00 2001 From: Extremelyd1 Date: Mon, 26 Jun 2023 22:54:12 +0200 Subject: [PATCH 047/216] Fix Sporg, Ambloom, add Uumuu --- .../Client/Entity/Action/EntityFsmActions.cs | 225 +++++++++++++----- HKMP/Game/Client/Entity/EntityManager.cs | 8 +- HKMP/Game/Client/Entity/EntitySpawner.cs | 51 ++-- HKMP/Game/Client/Entity/EntityType.cs | 3 + HKMP/Resource/entity-registry.json | 15 ++ 5 files changed, 224 insertions(+), 78 deletions(-) diff --git a/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs b/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs index 016ce92d..c885eee8 100644 --- a/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs +++ b/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs @@ -91,6 +91,7 @@ static EntityFsmActions() { // 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.GetRandomChild.DoGetRandomChild += GetRandomChildOnDoGetRandomChild; } /// @@ -173,6 +174,35 @@ 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 int off the stack (our random value) and + c.EmitDelegate>((value, instance) => { + if (!RandomActionValues.TryGetValue(instance, out var queue)) { + queue = new Queue(); + RandomActionValues[instance] = queue; + } + + queue.Enqueue(value); + + return value; + }); + } + /// /// IL edit method for modifying the /// method to store the results of the random calls. @@ -183,39 +213,16 @@ private static void FlingObjectsFromGlobalPoolOnEnter(ILContext il) { var c = new ILCursor(il); // Emit instructions for Random.Range calls for 1 int and 4 floats - EmitInstructions(); - EmitInstructions(); - EmitInstructions(); - EmitInstructions(); - EmitInstructions(); - - void EmitInstructions() { - // 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 int off the stack (our random value) and - c.EmitDelegate>((value, instance) => { - if (!RandomActionValues.TryGetValue(instance, out var queue)) { - queue = new Queue(); - RandomActionValues[instance] = queue; - } - - queue.Enqueue(value); - - return value; - }); - } + EmitRandomInterceptInstructions(c); + EmitRandomInterceptInstructions(c); + EmitRandomInterceptInstructions(c); + EmitRandomInterceptInstructions(c); + EmitRandomInterceptInstructions(c); } catch (Exception e) { Logger.Error($"Could not change FlingObjectsFromGlobalPool#OnEnter IL:\n{e}"); } } - + /// /// IL edit method for modifying the /// method to store the results of the random calls. @@ -226,38 +233,31 @@ private static void FlingObjectsFromGlobalPoolVelOnEnter(ILContext il) { var c = new ILCursor(il); // Emit instructions for Random.Range calls for 1 int and 4 floats - EmitInstructions(); - EmitInstructions(); - EmitInstructions(); - EmitInstructions(); - EmitInstructions(); - - void EmitInstructions() { - // 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 int off the stack (our random value) and - c.EmitDelegate>((value, instance) => { - if (!RandomActionValues.TryGetValue(instance, out var queue)) { - queue = new Queue(); - RandomActionValues[instance] = queue; - } - - queue.Enqueue(value); - - return value; - }); - } + EmitRandomInterceptInstructions(c); + EmitRandomInterceptInstructions(c); + EmitRandomInterceptInstructions(c); + EmitRandomInterceptInstructions(c); + EmitRandomInterceptInstructions(c); } catch (Exception e) { Logger.Error($"Could not change FlingObjectsFromGlobalPoolVel#OnEnter IL:\n{e}"); } } + + /// + /// IL edit method for modifying the DoGetRandomChild + /// method to store the results of the random calls. + /// + private static void GetRandomChildOnDoGetRandomChild(ILContext il) { + try { + // Create a cursor for this context + var c = new ILCursor(il); + + // Emit instructions for Random.Range calls for 1 int and 4 floats + EmitRandomInterceptInstructions(c); + } catch (Exception e) { + Logger.Error($"Could not change GetRandomChild#DoGetRandomChild IL:\n{e}"); + } + } #region SpawnObjectFromGlobalPool @@ -1094,6 +1094,34 @@ private static void ApplyNetworkDataFromAction(EntityNetworkData data, SetProper #endregion + #region SetParent + + private static bool GetNetworkDataFromAction(EntityNetworkData data, SetParent action) { + return true; + } + + private static void ApplyNetworkDataFromAction(EntityNetworkData data, SetParent action) { + var gameObject = action.Fsm.GetOwnerDefaultTarget(action.gameObject); + if (gameObject == null) { + return; + } + + var parent = action.parent.Value; + if (parent != null) { + gameObject.transform.parent = parent.transform; + } + + if (action.resetLocalPosition.Value) { + gameObject.transform.localPosition = Vector3.zero; + } + + if (action.resetLocalRotation.Value) { + gameObject.transform.localRotation = Quaternion.identity; + } + } + + #endregion + #region FindAlertRange private static bool GetNetworkDataFromAction(EntityNetworkData data, FindAlertRange action) { @@ -1789,4 +1817,87 @@ private static void ApplyNetworkDataFromAction(EntityNetworkData data, SetString } #endregion + + #region GetRandomChild + + private static bool GetNetworkDataFromAction(EntityNetworkData data, GetRandomChild action) { + if (!RandomActionValues.TryGetValue(action, out var queue)) { + return false; + } + + if (queue.Count == 0) { + Logger.Debug("Getting data for GetRandomChild has not enough items in queue"); + return false; + } + + var randomIndex = (int) queue.Dequeue(); + data.Packet.Write((byte) randomIndex); + + queue.Clear(); + + return true; + } + + private static void ApplyNetworkDataFromAction(EntityNetworkData data, GetRandomChild action) { + var randomIndex = data.Packet.ReadByte(); + + var gameObject = action.Fsm.GetOwnerDefaultTarget(action.gameObject); + if (gameObject == null) { + return; + } + + var childCount = gameObject.transform.childCount; + if (childCount == 0) { + return; + } + + action.storeResult.Value = gameObject.transform.GetChild(randomIndex).gameObject; + } + + #endregion + + #region DestroyComponent + + private static bool GetNetworkDataFromAction(EntityNetworkData data, DestroyComponent action) { + return true; + } + + private static void ApplyNetworkDataFromAction(EntityNetworkData data, DestroyComponent action) { + var gameObject = action.Fsm.GetOwnerDefaultTarget(action.gameObject); + if (gameObject == null) { + return; + } + + var component = gameObject.GetComponent(ReflectionUtils.GetGlobalType(action.component.Value)); + if (component == null) { + return; + } + + Object.Destroy(component); + } + + #endregion + + #region AddComponent + + private static bool GetNetworkDataFromAction(EntityNetworkData data, AddComponent action) { + if (action.removeOnExit.Value) { + Logger.Debug("Tried getting data for AddComponent action, but removeOnExit is true"); + return false; + } + + return true; + } + + private static void ApplyNetworkDataFromAction(EntityNetworkData data, AddComponent action) { + var gameObject = action.Fsm.GetOwnerDefaultTarget(action.gameObject); + if (gameObject == null) { + return; + } + + var component = gameObject.AddComponent(ReflectionUtils.GetGlobalType(action.component.Value)); + action.storeComponent.Value = component; + } + + #endregion } diff --git a/HKMP/Game/Client/Entity/EntityManager.cs b/HKMP/Game/Client/Entity/EntityManager.cs index 18cef9bd..95be6ba1 100644 --- a/HKMP/Game/Client/Entity/EntityManager.cs +++ b/HKMP/Game/Client/Entity/EntityManager.cs @@ -182,7 +182,7 @@ public void HandleEntityUpdate(EntityUpdate entityUpdate, bool alreadyInSceneUpd private bool OnGameObjectSpawned(EntitySpawnDetails details) { if (_entities.Values.Any(existingEntity => existingEntity.Object.Host == details.GameObject)) { Logger.Debug("Spawned object was already a registered entity"); - return false; + return true; } var processor = new EntityProcessor { @@ -302,9 +302,10 @@ private void OnSceneLoaded(Scene scene, LoadSceneMode mode) { private void FindEntitiesInScene(Scene scene, bool lateLoad) { // Find all PlayMakerFSM components // Filter out FSMs with GameObjects not in the current scene - // Project each FSM to their GameObject + // Project each FSM to their GameObject and pre-instantiate the EnemyDeathEffects component is if exists // Project each GameObject into its children including itself - // Concatenate all GameObjects for Climber components + // Concatenate all GameObjects for Climber components (Tiktiks) + // Concatenate all GameObjects for Walker components (Amblooms) // Filter out GameObjects not in the current scene var objectsToCheck = Object.FindObjectsOfType() .Where(fsm => fsm.gameObject.scene == scene) @@ -318,6 +319,7 @@ private void FindEntitiesInScene(Scene scene, bool lateLoad) { }) .SelectMany(obj => obj.GetChildren().Prepend(obj)) .Concat(Object.FindObjectsOfType().Select(climber => climber.gameObject)) + .Concat(Object.FindObjectsOfType().Select(walker => walker.gameObject)) .Where(obj => obj.scene == scene) .Distinct(); diff --git a/HKMP/Game/Client/Entity/EntitySpawner.cs b/HKMP/Game/Client/Entity/EntitySpawner.cs index 8f59b98d..0deab8fd 100644 --- a/HKMP/Game/Client/Entity/EntitySpawner.cs +++ b/HKMP/Game/Client/Entity/EntitySpawner.cs @@ -39,6 +39,10 @@ List clientFsms return SpawnVengeflyObjectFromSummon(clientObject); } + if (spawningType == EntityType.Sporg && spawnedType == EntityType.SporgSpore) { + return SpawnSporgSpore(clientFsms[0]); + } + if (spawningType == EntityType.OomaCorpse && spawnedType == EntityType.OomaCore) { return SpawnOomaCoreObject(clientFsms[0]); } @@ -79,41 +83,45 @@ private static GameObject SpawnFromCreateObject(CreateObject action) { return createdObject; } - private static GameObject SpawnBaldurGameObject(PlayMakerFSM fsm) { - var setGameObjectAction = fsm.GetFirstAction("Roller"); - var spawnAction = fsm.GetFirstAction("Fire"); - - var gameObject = setGameObjectAction.gameObject.Value; - + private static GameObject SpawnFromGlobalPool(SpawnObjectFromGlobalPool action, GameObject gameObject) { var position = Vector3.zero; var euler = Vector3.up; - if (spawnAction.spawnPoint.Value != null) { - position = spawnAction.spawnPoint.Value.transform.position; - if (!spawnAction.position.IsNone) { - position += spawnAction.position.Value; + if (action.spawnPoint.Value != null) { + position = action.spawnPoint.Value.transform.position; + if (!action.position.IsNone) { + position += action.position.Value; } - if (spawnAction.rotation.IsNone) { - euler = spawnAction.spawnPoint.Value.transform.eulerAngles; + if (action.rotation.IsNone) { + euler = action.spawnPoint.Value.transform.eulerAngles; } else { - euler = spawnAction.rotation.Value; + euler = action.rotation.Value; } } else { - if (!spawnAction.position.IsNone) { - position = spawnAction.position.Value; + if (!action.position.IsNone) { + position = action.position.Value; } - if (!spawnAction.rotation.IsNone) { - euler = spawnAction.rotation.Value; + if (!action.rotation.IsNone) { + euler = action.rotation.Value; } } var spawnedObject = gameObject.Spawn(position, Quaternion.Euler(euler)); - spawnAction.storeObject.Value = spawnedObject; + action.storeObject.Value = spawnedObject; return spawnedObject; } + private static GameObject SpawnBaldurGameObject(PlayMakerFSM fsm) { + var setGameObjectAction = fsm.GetFirstAction("Roller"); + var spawnAction = fsm.GetFirstAction("Fire"); + + var gameObject = setGameObjectAction.gameObject.Value; + + return SpawnFromGlobalPool(spawnAction, gameObject); + } + private static GameObject SpawnVengeflySummonObject(PlayMakerFSM fsm) { var action = fsm.GetFirstAction("Summon"); @@ -144,4 +152,11 @@ private static GameObject SpawnOomaCoreObject(PlayMakerFSM fsm) { return SpawnFromCreateObject(action); } + + private static GameObject SpawnSporgSpore(PlayMakerFSM fsm) { + var spawnAction = fsm.GetFirstAction("Fire"); + var gameObject = spawnAction.gameObject.Value; + + return SpawnFromGlobalPool(spawnAction, gameObject); + } } diff --git a/HKMP/Game/Client/Entity/EntityType.cs b/HKMP/Game/Client/Entity/EntityType.cs index 15d19fd7..1a0a925d 100644 --- a/HKMP/Game/Client/Entity/EntityType.cs +++ b/HKMP/Game/Client/Entity/EntityType.cs @@ -51,10 +51,13 @@ internal enum EntityType { Ooma, OomaCorpse, OomaCore, + Uumuu, + UumuuQuirrel, Ambloom, Fungling, Fungoon, Sporg, + SporgSpore, FungifiedHusk, Shrumeling, ShrumalWarrior, diff --git a/HKMP/Resource/entity-registry.json b/HKMP/Resource/entity-registry.json index d9c6e895..d06035c7 100644 --- a/HKMP/Resource/entity-registry.json +++ b/HKMP/Resource/entity-registry.json @@ -298,6 +298,16 @@ "Rotation" ] }, + { + "base_object_name": "Mega Jellyfish", + "type": "Uumuu", + "fsm_name": "Mega Jellyfish" + }, + { + "base_object_name": "Quirrel Land", + "type": "UumuuQuirrel", + "fsm_name": "Watch" + }, { "base_object_name": "Fung Crawler", "type": "Ambloom" @@ -317,6 +327,11 @@ "type": "Sporg", "fsm_name": "Shroom Turret" }, + { + "base_object_name": "Spore Bomb", + "type": "SporgSpore", + "fsm_name": "Spore Bomb" + }, { "base_object_name": "Zombie Fungus", "type": "FungifiedHusk", From a6aeddc97c7b8ea55113a8675c6ea7d10e5e6198 Mon Sep 17 00:00:00 2001 From: Extremelyd1 Date: Wed, 28 Jun 2023 20:26:47 +0200 Subject: [PATCH 048/216] Refactor FSM variable snapshot and entity scaling --- .../Client/Entity/Action/EntityFsmActions.cs | 189 ++++++++++++- HKMP/Game/Client/Entity/Entity.cs | 251 +++++++++-------- HKMP/Game/Client/Entity/EntitySpawner.cs | 56 +++- HKMP/Game/Client/Entity/EntityType.cs | 16 +- HKMP/Game/Client/Entity/FsmSnapshot.cs | 24 +- HKMP/Game/Server/ServerEntityData.cs | 5 +- HKMP/Game/Server/ServerManager.cs | 6 +- HKMP/Networking/Client/ClientUpdateManager.cs | 4 +- HKMP/Networking/Packet/Data/EntityUpdate.cs | 254 +++++++++++++++++- HKMP/Networking/Server/ServerUpdateManager.cs | 4 +- HKMP/Resource/action-registry.json | 12 + HKMP/Resource/entity-registry.json | 84 ++++++ 12 files changed, 748 insertions(+), 157 deletions(-) diff --git a/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs b/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs index c885eee8..d3f69e90 100644 --- a/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs +++ b/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs @@ -1,4 +1,5 @@ using System; +using System.Collections; using System.Collections.Generic; using System.Reflection; using Hkmp.Networking.Packet.Data; @@ -641,6 +642,9 @@ private static void ApplyNetworkDataFromAction(EntityNetworkData data, FireAtTar var posY = data.Packet.ReadFloat(); var selfGameObject = action.Fsm.GetOwnerDefaultTarget(action.gameObject); + if (selfGameObject == null) { + return; + } var selfPosition = selfGameObject.transform.position; @@ -674,6 +678,10 @@ private static bool GetNetworkDataFromAction(EntityNetworkData data, SetScale ac return false; } + if (IsObjectInRegistry(gameObject)) { + return false; + } + var scale = action.vector.IsNone ? gameObject.transform.localScale : action.vector.Value; if (!action.x.IsNone) { scale.x = action.x.Value; @@ -695,15 +703,30 @@ private static bool GetNetworkDataFromAction(EntityNetworkData data, SetScale ac } private static void ApplyNetworkDataFromAction(EntityNetworkData data, SetScale action) { - var scale = new Vector3( - data.Packet.ReadFloat(), - data.Packet.ReadFloat(), - data.Packet.ReadFloat() - ); + Vector3 scale; var gameObject = action.Fsm.GetOwnerDefaultTarget(action.gameObject); - if (gameObject == action.Fsm.GameObject) { - return; + + if (data == null) { + scale = action.vector.IsNone ? gameObject.transform.localScale : action.vector.Value; + + if (!action.x.IsNone) { + scale.x = action.x.Value; + } + + if (!action.y.IsNone) { + scale.y = action.y.Value; + } + + if (!action.z.IsNone) { + scale.z = action.z.Value; + } + } else { + scale = new Vector3( + data.Packet.ReadFloat(), + data.Packet.ReadFloat(), + data.Packet.ReadFloat() + ); } gameObject.transform.localScale = scale; @@ -1326,6 +1349,80 @@ private static void ApplyNetworkDataFromAction(EntityNetworkData data, SetPositi } } + #endregion + + #region SetRotation + + private static bool GetNetworkDataFromAction(EntityNetworkData data, SetRotation action) { + var gameObject = action.Fsm.GetOwnerDefaultTarget(action.gameObject); + if (gameObject == null) { + return false; + } + + if (IsObjectInRegistry(gameObject)) { + Logger.Debug("Tried getting SetPosition network data, but entity is in registry"); + return false; + } + + Vector3 vector3; + if (action.quaternion.IsNone) { + if (action.vector.IsNone) { + if (action.space == Space.Self) { + vector3 = gameObject.transform.localEulerAngles; + } else { + vector3 = gameObject.transform.eulerAngles; + } + } else { + vector3 = action.vector.Value; + } + } else { + vector3 = action.quaternion.Value.eulerAngles; + } + + if (!action.xAngle.IsNone) { + vector3.x = action.xAngle.Value; + } + + if (!action.yAngle.IsNone) { + vector3.y = action.yAngle.Value; + } + + if (!action.zAngle.IsNone) { + vector3.z = action.zAngle.Value; + } + + data.Packet.Write(vector3.x); + data.Packet.Write(vector3.y); + data.Packet.Write(vector3.z); + + return true; + } + + private static void ApplyNetworkDataFromAction(EntityNetworkData data, SetRotation action) { + var gameObject = action.Fsm.GetOwnerDefaultTarget(action.gameObject); + + if (data == null) { + Logger.Error("No data passed for applying SetRotation action"); + return; + } + + var vector3 = new Vector3( + data.Packet.ReadFloat(), + data.Packet.ReadFloat(), + data.Packet.ReadFloat() + ); + + if (gameObject == null) { + return; + } + + if (action.space == Space.Self) { + gameObject.transform.localEulerAngles = vector3; + } else { + gameObject.transform.eulerAngles = vector3; + } + } + #endregion #region ActivateGameObject @@ -1647,9 +1744,87 @@ private static void ApplyNetworkDataFromAction(EntityNetworkData data, SetVeloci #endregion + #region iTweenMoveBy + + private static bool GetNetworkDataFromAction(EntityNetworkData data, iTweenMoveBy action) { + var gameObject = action.Fsm.GetOwnerDefaultTarget(action.gameObject); + if (gameObject == null) { + return false; + } + + if (IsObjectInRegistry(gameObject)) { + return false; + } + + return true; + } + + private static void ApplyNetworkDataFromAction(EntityNetworkData data, iTweenMoveBy action) { + var gameObject = action.Fsm.GetOwnerDefaultTarget(action.gameObject); + if (gameObject == null) { + return; + } + + var id = ReflectionHelper.GetField(action, "itweenID"); + + var args = new Hashtable { + { "amount", action.vector.IsNone ? Vector3.zero : action.vector.Value }, + { + action.speed.IsNone ? "time" : "speed", + (float) (action.speed.IsNone + ? (action.time.IsNone ? 1.0 : action.time.Value) + : (double) action.speed.Value) + }, + { "delay", (float) (action.delay.IsNone ? 0.0 : (double) action.delay.Value) }, + { "easetype", action.easeType }, + { "looptype", action.loopType }, + { "oncomplete", "iTweenOnComplete" }, + { "oncompleteparams", id }, + { "onstart", "iTweenOnStart" }, + { "onstartparams", id }, + { "ignoretimescale", !action.realTime.IsNone && action.realTime.Value }, + { "space", action.space }, + { "name", action.id.IsNone ? "" : (object) action.id.Value }, + { "axis", action.axis == iTweenFsmAction.AxisRestriction.none ? "" : (object) Enum.GetName(typeof (iTweenFsmAction.AxisRestriction), action.axis) } + }; + + if (!action.orientToPath.IsNone) { + args.Add("orienttopath", action.orientToPath.Value); + } + + if (!action.lookAtObject.IsNone) { + args.Add("looktarget", + action.lookAtVector.IsNone + ? action.lookAtObject.Value.transform.position + : action.lookAtObject.Value.transform.position + action.lookAtVector.Value + ); + } else if (!action.lookAtVector.IsNone) { + args.Add("looktarget", action.lookAtVector.Value); + } + + if (!action.lookAtObject.IsNone || !action.lookAtVector.IsNone) { + args.Add("looktime", (float) (action.lookTime.IsNone ? 0.0 : (double) action.lookTime.Value)); + } + + ReflectionHelper.SetField(action, "itweenType", "move"); + + iTween.MoveBy(gameObject, args); + } + + #endregion + #region iTweenScaleTo private static bool GetNetworkDataFromAction(EntityNetworkData data, iTweenScaleTo action) { + var gameObject = action.Fsm.GetOwnerDefaultTarget(action.gameObject); + if (gameObject == null) { + return false; + } + + if (IsObjectInRegistry(gameObject)) { + return false; + } + return action.loopType == iTween.LoopType.none; } diff --git a/HKMP/Game/Client/Entity/Entity.cs b/HKMP/Game/Client/Entity/Entity.cs index e8e1c2a4..c9b89053 100644 --- a/HKMP/Game/Client/Entity/Entity.cs +++ b/HKMP/Game/Client/Entity/Entity.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using Hkmp.Collection; using Hkmp.Fsm; @@ -143,7 +144,7 @@ params EntityComponentType[] types _hasParent = true; } - Object.Client.transform.localScale = _hasParent + Object.Client.transform.localScale = _lastScale = _hasParent ? Object.Host.transform.localScale : Object.Host.transform.lossyScale; @@ -297,25 +298,13 @@ private void ProcessHostFsm(PlayMakerFSM fsm) { CurrentState = fsm.ActiveStateName }; - foreach (var f in fsm.FsmVariables.FloatVariables) { - snapshot.Floats.Add(f.Name, f.Value); - } - foreach (var i in fsm.FsmVariables.IntVariables) { - snapshot.Ints.Add(i.Name, i.Value); - } - foreach (var b in fsm.FsmVariables.BoolVariables) { - snapshot.Bools.Add(b.Name, b.Value); - } - foreach (var s in fsm.FsmVariables.StringVariables) { - snapshot.Strings.Add(s.Name, s.Value); - } - foreach (var vec2 in fsm.FsmVariables.Vector2Variables) { - snapshot.Vector2s.Add(vec2.Name, vec2.Value); - } - foreach (var vec3 in fsm.FsmVariables.Vector3Variables) { - snapshot.Vector3s.Add(vec3.Name, vec3.Value); - } - + snapshot.Floats = fsm.FsmVariables.FloatVariables.Select(f => f.Value).ToArray(); + snapshot.Ints = fsm.FsmVariables.IntVariables.Select(i => i.Value).ToArray(); + snapshot.Bools = fsm.FsmVariables.BoolVariables.Select(b => b.Value).ToArray(); + snapshot.Strings = fsm.FsmVariables.StringVariables.Select(s => s.Value).ToArray(); + snapshot.Vector2s = fsm.FsmVariables.Vector2Variables.Select(v => v.Value).ToArray(); + snapshot.Vector3s = fsm.FsmVariables.Vector3Variables.Select(v => v.Value).ToArray(); + _fsmSnapshots.Add(snapshot); } @@ -491,6 +480,7 @@ private void OnActionEntered(FsmStateAction self) { /// /// Callback method for handling updates. /// + [SuppressMessage("ReSharper", "CompareOfFloatsByEqualityOperator")] private void OnUpdate() { if (Object.Host == null) { if (_lastIsActive) { @@ -532,27 +522,45 @@ private void OnUpdate() { ); } + const float epsilon = 0.0001f; + var newScale = _hasParent ? transform.localScale : transform.lossyScale; - var newScaleX = newScale.x > 0; - var newScaleY = newScale.y > 0; - var lastScaleX = _lastScale.x > 0; - var lastScaleY = _lastScale.y > 0; - if (newScaleX != lastScaleX || newScaleY != lastScaleY) { - _lastScale = newScale; + if (newScale != _lastScale) { + var scaleData = new EntityUpdate.ScaleData { + origin = true + }; + + if (newScale.x != _lastScale.x) { + scaleData.x = true; + scaleData.xScale = newScale.x; + + if (System.Math.Abs(newScale.x - _lastScale.x * -1) < epsilon) { + scaleData.xFlipped = true; + } + } + + if (newScale.y != _lastScale.y) { + scaleData.y = true; + scaleData.yScale = newScale.y; - byte scaleToSend = 0; - if (newScaleX) { - scaleToSend |= 1; + if (System.Math.Abs(newScale.y - _lastScale.y * -1) < epsilon) { + scaleData.yFlipped = true; + } } + + if (newScale.z != _lastScale.z) { + scaleData.z = true; + scaleData.zScale = newScale.z; - if (newScaleY) { - scaleToSend |= 2; + if (System.Math.Abs(newScale.z - _lastScale.z * -1) < epsilon) { + scaleData.zFlipped = true; + } } - _netClient.UpdateManager.UpdateEntityScale( - Id, - scaleToSend - ); + Logger.Debug($"Sending entity scale:\n{scaleData}"); + _netClient.UpdateManager.UpdateEntityScale(Id, scaleData); + + _lastScale = newScale; } var newActive = _hasParent ? Object.Host.activeSelf : Object.Host.activeInHierarchy; @@ -586,25 +594,24 @@ private void OnUpdate() { // Define a method that allows generalization of checking for changes in all FSM variables void CondAddData( VarType[] fsmVars, - Dictionary snapshotDict, - Func fsmVarName, + BaseType[] snapshotArray, Func fsmVarValue, EntityHostFsmData.Type type, Dictionary dataDict ) { for (byte i = 0; i < fsmVars.Length; i++) { var fsmVar = fsmVars[i]; + var snapshotVar = snapshotArray[i]; - var name = fsmVarName.Invoke(fsmVar); - if (!snapshotDict.TryGetValue(name, out var lastValue)) { - Logger.Warn($"No last value found for FSM var: {name}"); + if (snapshotVar == null) { + Logger.Warn("No last value found for FSM var"); continue; } var value = fsmVarValue.Invoke(fsmVar); - if (!value.Equals(lastValue)) { + if (!value.Equals(snapshotVar)) { // Update the value in the snapshot since it changed - snapshotDict[name] = value; + snapshotArray[i] = value; data.Types.Add(type); // Some funky casting here to make sure we can use this method with Vector2 and Vector3 @@ -624,7 +631,6 @@ Dictionary dataDict CondAddData( fsm.FsmVariables.FloatVariables, snapshot.Floats, - fsmFloat => fsmFloat.Name, fsmFloat => fsmFloat.Value, EntityHostFsmData.Type.Floats, data.Floats @@ -632,7 +638,6 @@ Dictionary dataDict CondAddData( fsm.FsmVariables.IntVariables, snapshot.Ints, - fsmInt => fsmInt.Name, fsmInt => fsmInt.Value, EntityHostFsmData.Type.Ints, data.Ints @@ -640,7 +645,6 @@ Dictionary dataDict CondAddData( fsm.FsmVariables.BoolVariables, snapshot.Bools, - fsmBool => fsmBool.Name, fsmBool => fsmBool.Value, EntityHostFsmData.Type.Bools, data.Bools @@ -648,7 +652,6 @@ Dictionary dataDict CondAddData( fsm.FsmVariables.StringVariables, snapshot.Strings, - fsmString => fsmString.Name, fsmString => fsmString.Value, EntityHostFsmData.Type.Strings, data.Strings @@ -656,7 +659,6 @@ Dictionary dataDict CondAddData( fsm.FsmVariables.Vector2Variables, snapshot.Vector2s, - fsmVec2 => fsmVec2.Name, fsmVec2 => fsmVec2.Value, EntityHostFsmData.Type.Vector2s, data.Vec2s @@ -664,7 +666,6 @@ Dictionary dataDict CondAddData( fsm.FsmVariables.Vector3Variables, snapshot.Vector3s, - fsmVec3 => fsmVec3.Name, fsmVec3 => fsmVec3.Value, EntityHostFsmData.Type.Vector3s, data.Vec3s @@ -851,23 +852,23 @@ public void MakeHost() { var snapshot = _fsmSnapshots[fsmIndex]; - foreach (var pair in snapshot.Floats) { - fsm.FsmVariables.GetFsmFloat(pair.Key).Value = pair.Value; + for (var i = 0; i < snapshot.Floats.Length; i++) { + fsm.FsmVariables.FloatVariables[i].Value = snapshot.Floats[i]; } - foreach (var pair in snapshot.Ints) { - fsm.FsmVariables.GetFsmInt(pair.Key).Value = pair.Value; + for (var i = 0; i < snapshot.Ints.Length; i++) { + fsm.FsmVariables.IntVariables[i].Value = snapshot.Ints[i]; } - foreach (var pair in snapshot.Bools) { - fsm.FsmVariables.GetFsmBool(pair.Key).Value = pair.Value; + for (var i = 0; i < snapshot.Bools.Length; i++) { + fsm.FsmVariables.BoolVariables[i].Value = snapshot.Bools[i]; } - foreach (var pair in snapshot.Strings) { - fsm.FsmVariables.GetFsmString(pair.Key).Value = pair.Value; + for (var i = 0; i < snapshot.Strings.Length; i++) { + fsm.FsmVariables.StringVariables[i].Value = snapshot.Strings[i]; } - foreach (var pair in snapshot.Vector2s) { - fsm.FsmVariables.GetFsmVector2(pair.Key).Value = pair.Value; + for (var i = 0; i < snapshot.Vector2s.Length; i++) { + fsm.FsmVariables.Vector2Variables[i].Value = snapshot.Vector2s[i]; } - foreach (var pair in snapshot.Vector3s) { - fsm.FsmVariables.GetFsmVector3(pair.Key).Value = pair.Value; + for (var i = 0; i < snapshot.Vector3s.Length; i++) { + fsm.FsmVariables.Vector3Variables[i].Value = snapshot.Vector3s[i]; } // Before setting the state, we replace the actions of the to-be state to only include the ones that @@ -917,42 +918,54 @@ public void UpdatePosition(Vector2 position) { /// /// Updates the scale of the client entity. /// - /// The new scale. - public void UpdateScale(byte scale) { + /// The new scale data. + public void UpdateScale(EntityUpdate.ScaleData scale) { var transform = Object.Client.transform; var localScale = transform.localScale; - var currentScaleX = localScale.x; - var currentScaleY = localScale.y; + + if (scale.x) { + if (scale.xFlipped) { + var currentScaleX = localScale.x; + + if (currentScaleX > 0 != scale.xPos) { + currentScaleX *= -1; - var scaleXPos = (scale & 1) != 0; - var scaleYPos = (scale & 2) != 0; + localScale.x = currentScaleX; + } + } else { + localScale.x = scale.xScale; + } + } - // We use the host scale as reference, either lossy or local depending on if we have a parent - var hostScale = _hasParent ? Object.Host.transform.localScale : Object.Host.transform.lossyScale; - var hostScaleX = hostScale.x; - var hostScaleY = hostScale.y; - - // Check whether the sign of the scale is equal to that of the received update - // Otherwise, construct the new scale by using the host scale and correct setting the sign - if (currentScaleX > 0 != scaleXPos) { - currentScaleX = System.Math.Abs(hostScaleX); - if (!scaleXPos) { - currentScaleX *= -1; + if (scale.y) { + if (scale.yFlipped) { + var currentScaleY = localScale.y; + + if (currentScaleY > 0 != scale.yPos) { + currentScaleY *= -1; + + localScale.y = currentScaleY; + } + } else { + localScale.y = scale.yScale; } } - if (currentScaleY > 0 != scaleYPos) { - currentScaleY = System.Math.Abs(hostScaleY); - if (!scaleYPos) { - currentScaleY *= -1; + if (scale.z) { + if (scale.zFlipped) { + var currentScaleZ = localScale.z; + + if (currentScaleZ > 0 != scale.zPos) { + currentScaleZ *= -1; + + localScale.z = currentScaleZ; + } + } else { + localScale.z = scale.zScale; } } - - transform.localScale = new Vector3( - currentScaleX, - currentScaleY, - hostScale.z - ); + + transform.localScale = localScale; } /// @@ -1125,14 +1138,14 @@ void CondUpdateVars( EntityHostFsmData.Type type, Dictionary dataDict, FsmType[] fsmVarArray, - Action setValueAction + Action 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}"); } else { - setValueAction.Invoke(fsmVarArray[pair.Key], pair.Value); + setValueAction.Invoke(pair.Key, fsmVarArray[pair.Key], pair.Value); } } } @@ -1142,50 +1155,56 @@ Action setValueAction EntityHostFsmData.Type.Floats, data.Floats, fsm.FsmVariables.FloatVariables, - (fsmVar, value) => { - fsmVar.Value = (float) value; - snapshot.Floats[fsmVar.Name] = (float) value; - }); + (index, fsmVar, value) => { + fsmVar.Value = value; + snapshot.Floats[index] = value; + } + ); CondUpdateVars( EntityHostFsmData.Type.Ints, data.Ints, fsm.FsmVariables.IntVariables, - (fsmVar, value) => { - fsmVar.Value = (int) value; - snapshot.Ints[fsmVar.Name] = (int) value; - }); + (index, fsmVar, value) => { + fsmVar.Value = value; + snapshot.Ints[index] = value; + } + ); CondUpdateVars( EntityHostFsmData.Type.Bools, - data.Bools, + data.Bools, fsm.FsmVariables.BoolVariables, - (fsmVar, value) => { - fsmVar.Value = (bool) value; - snapshot.Bools[fsmVar.Name] = (bool) value; - }); + (index, fsmVar, value) => { + fsmVar.Value = value; + snapshot.Bools[index] = value; + } + ); CondUpdateVars( EntityHostFsmData.Type.Strings, - data.Strings, + data.Strings, fsm.FsmVariables.StringVariables, - (fsmVar, value) => { - fsmVar.Value = (string) value; - snapshot.Strings[fsmVar.Name] = (string) value; - }); + (index, fsmVar, value) => { + fsmVar.Value = value; + snapshot.Strings[index] = value; + } + ); CondUpdateVars( EntityHostFsmData.Type.Vector2s, - data.Vec2s, + data.Vec2s, fsm.FsmVariables.Vector2Variables, - (fsmVar, value) => { - fsmVar.Value = (UnityEngine.Vector2) (Vector2) value; - snapshot.Vector2s[fsmVar.Name] = (UnityEngine.Vector2) (Vector2) value; - }); + (index, fsmVar, value) => { + fsmVar.Value = (UnityEngine.Vector2) value; + snapshot.Vector2s[index] = (UnityEngine.Vector2) value; + } + ); CondUpdateVars( EntityHostFsmData.Type.Vector3s, data.Vec3s, fsm.FsmVariables.Vector3Variables, - (fsmVar, value) => { - fsmVar.Value = (Vector3) (Hkmp.Math.Vector3) value; - snapshot.Vector3s[fsmVar.Name] = (Vector3) (Hkmp.Math.Vector3) value; - }); + (index, fsmVar, value) => { + fsmVar.Value = (Vector3) value; + snapshot.Vector3s[index] = (Vector3) value; + } + ); } } diff --git a/HKMP/Game/Client/Entity/EntitySpawner.cs b/HKMP/Game/Client/Entity/EntitySpawner.cs index 0deab8fd..06bc8d0e 100644 --- a/HKMP/Game/Client/Entity/EntitySpawner.cs +++ b/HKMP/Game/Client/Entity/EntitySpawner.cs @@ -47,6 +47,25 @@ List clientFsms return SpawnOomaCoreObject(clientFsms[0]); } + if (spawnedType == EntityType.SoulOrb) { + if (spawningType == EntityType.SoulTwister) { + return SpawnSoulTwisterOrbObject(clientFsms[0]); + } + if (spawningType == EntityType.SoulWarrior) { + return SpawnSoulWarriorOrbObject(clientFsms[0]); + } + if (spawningType == EntityType.SoulMaster) { + return SpawnSoulMasterOrbObject(clientFsms[0]); + } + if (spawningType == EntityType.SoulMasterOrbSpinner) { + return SpawnOrbSpinnerOrbObject(clientFsms[2]); + } + + if (spawningType == EntityType.SoulMasterPhase2) { + return SpawnSoulMaster2OrbObject(clientFsms[0]); + } + } + return null; } @@ -78,7 +97,6 @@ private static GameObject SpawnFromCreateObject(CreateObject action) { } var createdObject = Object.Instantiate(gameObject, position, Quaternion.Euler(euler)); - action.storeObject.Value = createdObject; return createdObject; } @@ -108,7 +126,6 @@ private static GameObject SpawnFromGlobalPool(SpawnObjectFromGlobalPool action, } var spawnedObject = gameObject.Spawn(position, Quaternion.Euler(euler)); - action.storeObject.Value = spawnedObject; return spawnedObject; } @@ -159,4 +176,39 @@ private static GameObject SpawnSporgSpore(PlayMakerFSM fsm) { return SpawnFromGlobalPool(spawnAction, gameObject); } + + private static GameObject SpawnSoulTwisterOrbObject(PlayMakerFSM fsm) { + var spawnAction = fsm.GetFirstAction("Fire"); + var gameObject = spawnAction.gameObject.Value; + + return SpawnFromGlobalPool(spawnAction, gameObject); + } + + private static GameObject SpawnSoulWarriorOrbObject(PlayMakerFSM fsm) { + var spawnAction = fsm.GetFirstAction("Shoot"); + var gameObject = spawnAction.gameObject.Value; + + return SpawnFromGlobalPool(spawnAction, gameObject); + } + + private static GameObject SpawnSoulMasterOrbObject(PlayMakerFSM fsm) { + var spawnAction = fsm.GetFirstAction("Shot"); + var gameObject = spawnAction.gameObject.Value; + + return SpawnFromGlobalPool(spawnAction, gameObject); + } + + private static GameObject SpawnOrbSpinnerOrbObject(PlayMakerFSM fsm) { + var spawnAction = fsm.GetFirstAction("Spawn"); + var gameObject = spawnAction.gameObject.Value; + + return SpawnFromGlobalPool(spawnAction, gameObject); + } + + private static GameObject SpawnSoulMaster2OrbObject(PlayMakerFSM fsm) { + var spawnAction = fsm.GetFirstAction("Spawn Fireball"); + var gameObject = spawnAction.gameObject.Value; + + return SpawnFromGlobalPool(spawnAction, gameObject); + } } diff --git a/HKMP/Game/Client/Entity/EntityType.cs b/HKMP/Game/Client/Entity/EntityType.cs index 1a0a925d..011aabcb 100644 --- a/HKMP/Game/Client/Entity/EntityType.cs +++ b/HKMP/Game/Client/Entity/EntityType.cs @@ -67,5 +67,19 @@ internal enum EntityType { MantisLordBattle, MantisLord, MantisLordCage, - MantisLordFloor + MantisLordFloor, + HuskSentry, + HeavySentry, + WingedSentry, + LanceSentry, + Mistake, + Folly, + SoulTwister, + SoulOrb, + SoulWarrior, + SoulMaster, + SoulMasterOrbSpinner, + SoulMasterFakeQuake, + SoulMasterWindow, + SoulMasterPhase2, } diff --git a/HKMP/Game/Client/Entity/FsmSnapshot.cs b/HKMP/Game/Client/Entity/FsmSnapshot.cs index 89f41107..b60b126f 100644 --- a/HKMP/Game/Client/Entity/FsmSnapshot.cs +++ b/HKMP/Game/Client/Entity/FsmSnapshot.cs @@ -16,37 +16,25 @@ internal class FsmSnapshot { /// /// Dictionary of names of float variables and corresponding (current/last) value. /// - public Dictionary Floats { get; } + public float[] Floats { set; get; } /// /// Dictionary of names of int variables and corresponding (current/last) value. /// - public Dictionary Ints { get; } + public int[] Ints { set; get; } /// /// Dictionary of names of bool variables and corresponding (current/last) value. /// - public Dictionary Bools { get; } + public bool[] Bools { set; get; } /// /// Dictionary of names of string variables and corresponding (current/last) value. /// - public Dictionary Strings { get; } + public string[] Strings { set; get; } /// /// Dictionary of names of vector2 variables and corresponding (current/last) value. /// - public Dictionary Vector2s { get; } + public Vector2[] Vector2s { set; get; } /// /// Dictionary of names of vector3 variables and corresponding (current/last) value. /// - public Dictionary Vector3s { get; } - - /// - /// Construct the snapshot by initializing all dictionaries. - /// - public FsmSnapshot() { - Floats = new Dictionary(); - Ints = new Dictionary(); - Bools = new Dictionary(); - Strings = new Dictionary(); - Vector2s = new Dictionary(); - Vector3s = new Dictionary(); - } + public Vector3[] Vector3s { set; get; } } diff --git a/HKMP/Game/Server/ServerEntityData.cs b/HKMP/Game/Server/ServerEntityData.cs index 8e29f213..24c2b58f 100644 --- a/HKMP/Game/Server/ServerEntityData.cs +++ b/HKMP/Game/Server/ServerEntityData.cs @@ -29,9 +29,9 @@ internal class ServerEntityData { [CanBeNull] public Vector2 Position { get; set; } /// - /// The last scale of the entity. + /// The last scale data of the entity. /// - public byte? Scale { get; set; } + public EntityUpdate.ScaleData Scale { get; set; } /// /// The ID of the last played animation. /// @@ -56,6 +56,7 @@ internal class ServerEntityData { public Dictionary HostFsmData { get; } public ServerEntityData() { + Scale = new EntityUpdate.ScaleData(); GenericData = new List(); HostFsmData = new Dictionary(); } diff --git a/HKMP/Game/Server/ServerManager.cs b/HKMP/Game/Server/ServerManager.cs index aa8c7f93..282d30b2 100644 --- a/HKMP/Game/Server/ServerManager.cs +++ b/HKMP/Game/Server/ServerManager.cs @@ -402,9 +402,9 @@ private void OnClientEnterScene(ServerPlayerData playerData) { entityUpdate.Position = entityData.Position; } - if (entityData.Scale.HasValue) { + if (!entityData.Scale.IsEmpty) { entityUpdate.UpdateTypes.Add(EntityUpdateType.Scale); - entityUpdate.Scale = entityData.Scale.Value; + entityUpdate.Scale = entityData.Scale; } if (entityData.AnimationId.HasValue) { @@ -682,7 +682,7 @@ private void OnEntityUpdate(ushort id, EntityUpdate entityUpdate) { } ); - entityData.Scale = entityUpdate.Scale; + entityData.Scale.Merge(entityUpdate.Scale); } if (entityUpdate.UpdateTypes.Contains(EntityUpdateType.Animation)) { diff --git a/HKMP/Networking/Client/ClientUpdateManager.cs b/HKMP/Networking/Client/ClientUpdateManager.cs index 2c73bbcf..f6f18d30 100644 --- a/HKMP/Networking/Client/ClientUpdateManager.cs +++ b/HKMP/Networking/Client/ClientUpdateManager.cs @@ -233,8 +233,8 @@ public void UpdateEntityPosition(byte entityId, Vector2 position) { /// Update an entity's scale in the current packet. /// /// The ID of the entity. - /// The new scale of the entity. - public void UpdateEntityScale(byte entityId, byte scale) { + /// The scale data of the entity. + public void UpdateEntityScale(byte entityId, EntityUpdate.ScaleData scale) { lock (Lock) { var entityUpdate = FindOrCreateEntityUpdate(entityId); diff --git a/HKMP/Networking/Packet/Data/EntityUpdate.cs b/HKMP/Networking/Packet/Data/EntityUpdate.cs index 7c95e00f..52721315 100644 --- a/HKMP/Networking/Packet/Data/EntityUpdate.cs +++ b/HKMP/Networking/Packet/Data/EntityUpdate.cs @@ -32,9 +32,9 @@ internal class EntityUpdate : IPacketData { public Vector2 Position { get; set; } /// - /// The boolean representation of the scale of the entity. + /// The scale data of the entity. /// - public byte Scale { get; set; } + public ScaleData Scale { get; set; } /// /// The ID of the animation of the entity. @@ -59,6 +59,7 @@ internal class EntityUpdate : IPacketData { /// public EntityUpdate() { UpdateTypes = new HashSet(); + Scale = new ScaleData(); GenericData = new List(); HostFsmData = new Dictionary(); } @@ -91,7 +92,7 @@ public void WriteData(IPacket packet) { } if (UpdateTypes.Contains(EntityUpdateType.Scale)) { - packet.Write(Scale); + Scale.WriteData(packet); } if (UpdateTypes.Contains(EntityUpdateType.Animation)) { @@ -153,7 +154,7 @@ public void ReadData(IPacket packet) { } if (UpdateTypes.Contains(EntityUpdateType.Scale)) { - Scale = packet.ReadByte(); + Scale.ReadData(packet); } if (UpdateTypes.Contains(EntityUpdateType.Animation)) { @@ -189,6 +190,251 @@ public void ReadData(IPacket packet) { } } } + + /// + /// Data class containing compact information about an entity's scale, which can be more efficiently networked. + /// + public class ScaleData { + /// + /// Whether this instance originates from a client. This influences how to write certain data. + /// + public bool origin { private get; init; } + + /// + /// Whether the x of the scale is defined. + /// + public bool x { get; set; } + /// + /// Whether the y of the scale is defined. + /// + public bool y { get; set; } + /// + /// Whether the z of the scale is defined. + /// + public bool z { get; set; } + + /// + /// Whether the x of the scale is only flipped from positive to negative or vice versa. + /// + public bool xFlipped { get; set; } + /// + /// Whether the y of the scale is only flipped from positive to negative or vice versa. + /// + public bool yFlipped { get; set; } + /// + /// Whether the z of the scale is only flipped from positive to negative or vice versa. + /// + public bool zFlipped { get; set; } + + /// + /// The float value for the x of the scale. + /// + public float xScale { get; set; } + /// + /// The float value for the y of the scale. + /// + public float yScale { get; set; } + /// + /// The float value for the z of the scale. + /// + public float zScale { get; set; } + + /// + /// Whether the x of the scale is positive if it was only flipped. + /// + public bool xPos { get; private set; } + /// + /// Whether the y of the scale is positive if it was only flipped. + /// + public bool yPos { get; private set; } + /// + /// Whether the z of the scale is positive if it was only flipped. + /// + public bool zPos { get; private set; } + + /// + /// Whether this data instance is empty (no x, y and z defined). + /// + public bool IsEmpty => !x && !y && !z; + + /// + public void WriteData(IPacket packet) { + // Logger.Debug($"ScaleData.WriteData x: {x}, y: {y}, z: {z}, xFlipped: {xFlipped}, yFlipped: {yFlipped}, zFlipped: {zFlipped}, xScale: {xScale}, yScale: {yScale}, zScale: {zScale}"); + + // 0 0 0 0 0 0 0 0 + byte flagByte = 0; + + if (x && !y && !z) { + // Only x defined + // ( 1 0 ) 0 0 0 0 0 0 + flagByte |= 1; + } else if (!x && y && !z) { + // Only y defined + // ( 0 1 ) 0 0 0 0 0 0 + flagByte |= 2; + } else if (x && y && !z) { + // Only x and y defined + // ( 1 1 ) 0 0 0 0 0 0 + flagByte |= 3; + } + + if (xFlipped) { + // 1 x ( 1 ) 0 0 0 0 0 + flagByte |= 4; + + if ((origin && xScale > 0) || (!origin && xPos)) { + // 1 x 1 ( 1 ) 0 0 0 0 + flagByte |= 8; + } + } + + if (yFlipped) { + // 1 x x x ( 1 ) 0 0 0 + flagByte |= 16; + + if ((origin && yScale > 0) || (!origin && yPos)) { + // 1 x x x 1 ( 1 ) 0 0 + flagByte |= 32; + } + } + + if (zFlipped) { + // 1 x x x x x ( 1 ) 0 + flagByte |= 64; + + if ((origin && zScale > 0) || (!origin && zPos)) { + // 1 x x x x x 1 ( 1 ) + flagByte |= 128; + } + } + + // Logger.Debug($" Flag: {flagByte}"); + packet.Write(flagByte); + + if (x && !xFlipped) { + // Logger.Debug($" xScale: {xScale}"); + packet.Write(xScale); + } + + if (y && !yFlipped) { + // Logger.Debug($" yScale: {yScale}"); + packet.Write(yScale); + } + + if (z && !zFlipped) { + // Logger.Debug($" zScale: {zScale}"); + packet.Write(zScale); + } + } + + /// + public void ReadData(IPacket packet) { + var flagByte = packet.ReadByte(); + // Logger.Debug($"ScaleData.ReadData flag: {flagByte}"); + + var firstBit = (flagByte & 1) != 0; + var secondBit = (flagByte & 2) != 0; + + if (firstBit) { + x = true; + } + + if (secondBit) { + y = true; + } + + if (!firstBit && !secondBit) { + x = y = z = true; + } + + if ((flagByte & 4) != 0) { + xFlipped = true; + + if ((flagByte & 8) != 0) { + xPos = true; + } + } + + if ((flagByte & 16) != 0) { + yFlipped = true; + + if ((flagByte & 32) != 0) { + yPos = true; + } + } + + if ((flagByte & 64) != 0) { + zFlipped = true; + + if ((flagByte & 128) != 0) { + zPos = true; + } + } + + if (x && !xFlipped) { + xScale = packet.ReadFloat(); + // Logger.Debug($" xScale: {xScale}"); + } + + if (y && !yFlipped) { + yScale = packet.ReadFloat(); + // Logger.Debug($" yScale: {yScale}"); + } + + if (z && !zFlipped) { + zScale = packet.ReadFloat(); + // Logger.Debug($" zScale: {zScale}"); + } + + // Logger.Debug($" x: {x}, y: {y}, z: {z}, xFlipped: {xFlipped}, yFlipped: {yFlipped}, zFlipped: {zFlipped}, xPos: {xPos}, yPos: {yPos}, zPos: {zPos}"); + } + + /// + /// Merge the given data into this instance. + /// + /// Another instance of ScaleData. + public void Merge(ScaleData data) { + x |= data.x; + y |= data.y; + z |= data.z; + + if (data.x) { + xFlipped = data.xFlipped; + + if (data.xFlipped) { + xPos = data.xPos; + } else { + xScale = data.xScale; + } + } + + if (data.y) { + yFlipped = data.yFlipped; + + if (data.yFlipped) { + yPos = data.yPos; + } else { + yScale = data.yScale; + } + } + + if (data.z) { + zFlipped = data.zFlipped; + + if (data.zFlipped) { + zPos = data.zPos; + } else { + zScale = data.zScale; + } + } + } + + /// + public override string ToString() { + return + $"ScaleData: x: {x}, y: {y}, z: {z}, xFlipped: {xFlipped}, yFlipped: {yFlipped}, zFlipped: {zFlipped}, xPos: {xPos}, yPos: {yPos}, zPos: {zPos}, xScale: {xScale}, yScale: {yScale}, zScale: {zScale}"; + } + } } /// diff --git a/HKMP/Networking/Server/ServerUpdateManager.cs b/HKMP/Networking/Server/ServerUpdateManager.cs index e8a559a9..623d3a0f 100644 --- a/HKMP/Networking/Server/ServerUpdateManager.cs +++ b/HKMP/Networking/Server/ServerUpdateManager.cs @@ -359,8 +359,8 @@ public void UpdateEntityPosition(byte entityId, Vector2 position) { /// Update an entity's scale in the packet. /// /// The ID of the entity. - /// The boolean representation of the scale of the entity. - public void UpdateEntityScale(byte entityId, byte scale) { + /// The scale data of the entity. + public void UpdateEntityScale(byte entityId, EntityUpdate.ScaleData scale) { lock (Lock) { var entityUpdate = FindOrCreateEntityUpdate(entityId); diff --git a/HKMP/Resource/action-registry.json b/HKMP/Resource/action-registry.json index cf2d76a1..12b0acc1 100644 --- a/HKMP/Resource/action-registry.json +++ b/HKMP/Resource/action-registry.json @@ -207,5 +207,17 @@ { "type": "Translate", "update_field": "everyFrame" + }, + { + "type": "ChaseObjectV2" + }, + { + "type": "DistanceFly" + }, + { + "type": "DistanceFlyV2" + }, + { + "type": "DistanceFlySmooth" } ] diff --git a/HKMP/Resource/entity-registry.json b/HKMP/Resource/entity-registry.json index d06035c7..3fa813b0 100644 --- a/HKMP/Resource/entity-registry.json +++ b/HKMP/Resource/entity-registry.json @@ -412,5 +412,89 @@ "type": "MantisLordFloor" } ] + }, + { + "base_object_name": "Ruins Sentry", + "type": "HuskSentry", + "fsm_name": "Ruins Sentry" + }, + { + "base_object_name": "Ruins Sentry Fat", + "type": "HeavySentry", + "fsm_name": "Ruins Sentry Fat" + }, + { + "base_object_name": "Ruins Flying Sentry", + "type": "WingedSentry", + "fsm_name": "Flying Sentry Nail" + }, + { + "base_object_name": "Ruins Flying Sentry Javelin", + "type": "LanceSentry", + "fsm_name": "Flying Sentry Javelin" + }, + { + "base_object_name": "Mage Blob", + "type": "Mistake", + "fsm_name": "Blob" + }, + { + "base_object_name": "Mage Balloon", + "type": "Folly", + "fsm_name": "Control" + }, + { + "base_object_name": "Mage Balloon Spawner", + "type": "Folly", + "fsm_name": "Control" + }, + { + "base_object_name": "Mage", + "type": "SoulTwister", + "fsm_name": "Mage" + }, + { + "base_object_name": "Mage Orb", + "type": "SoulOrb", + "fsm_name": "Orb Control", + "components": [ + "Rotation" + ] + }, + { + "base_object_name": "Mage Knight", + "type": "SoulWarrior", + "fsm_name": "Mage Knight" + }, + { + "base_object_name": "Mage Lord", + "type": "SoulMaster", + "fsm_name": "Mage Lord" + }, + { + "base_object_name": "Orb Spinner", + "type": "SoulMasterOrbSpinner", + "fsm_name": "Summon Orbs" + }, + { + "base_object_name": "Mage Lord Phase2", + "type": "SoulMasterPhase2", + "fsm_name": "Mage Lord 2" + }, + { + "base_object_name": "Knight Get Fake Quake", + "type": "SoulMasterFakeQuake", + "fsm_name": "Get Quake" + }, + { + "base_object_name": "mage_window", + "type": "SoulMasterWindow", + "fsm_name": "Break", + "children": [ + { + "base_object_name": "break_ground", + "type": "SoulMasterWindow" + } + ] } ] From b8861cafc390612fabc39a0703ffa421bfe3d58e Mon Sep 17 00:00:00 2001 From: Extremelyd1 Date: Fri, 30 Jun 2023 14:56:37 +0200 Subject: [PATCH 049/216] Add City of Tears enemies, including The Collector --- .../Entity/Component/ComponentFactory.cs | 8 + .../Entity/Component/EnemySpawnerComponent.cs | 2 +- .../Entity/Component/EntityComponent.cs | 3 +- .../Entity/Component/SpawnJarComponent.cs | 169 ++++++++++++++++++ HKMP/Game/Client/Entity/EntityManager.cs | 5 +- HKMP/Game/Client/Entity/EntityRegistry.cs | 4 + HKMP/Game/Client/Entity/EntitySpawnDetails.cs | 3 +- HKMP/Game/Client/Entity/EntitySpawner.cs | 74 ++++++++ HKMP/Game/Client/Entity/EntityType.cs | 12 +- HKMP/Resource/action-registry.json | 4 + HKMP/Resource/entity-registry.json | 52 +++++- 11 files changed, 325 insertions(+), 11 deletions(-) create mode 100644 HKMP/Game/Client/Entity/Component/SpawnJarComponent.cs diff --git a/HKMP/Game/Client/Entity/Component/ComponentFactory.cs b/HKMP/Game/Client/Entity/Component/ComponentFactory.cs index d64c1277..3d474071 100644 --- a/HKMP/Game/Client/Entity/Component/ComponentFactory.cs +++ b/HKMP/Game/Client/Entity/Component/ComponentFactory.cs @@ -47,6 +47,14 @@ HostClientPair objects }); case EntityComponentType.ChildrenActivation: return new ChildrenActivationComponent(netClient, entityId, objects); + case EntityComponentType.SpawnJar: + var spawnJarClient = objects.Client.GetComponent(); + var spawnJarHost = objects.Host.GetComponent(); + + return new SpawnJarComponent(netClient, entityId, objects, new HostClientPair { + Client = spawnJarClient, + Host = spawnJarHost + }); default: throw new ArgumentOutOfRangeException(nameof(type), type, $"Could not instantiate entity component for type: {type}"); } diff --git a/HKMP/Game/Client/Entity/Component/EnemySpawnerComponent.cs b/HKMP/Game/Client/Entity/Component/EnemySpawnerComponent.cs index 4a858ef0..8a5a7c3c 100644 --- a/HKMP/Game/Client/Entity/Component/EnemySpawnerComponent.cs +++ b/HKMP/Game/Client/Entity/Component/EnemySpawnerComponent.cs @@ -53,7 +53,7 @@ private void EnemySpawnerOnStart(On.EnemySpawner.orig_Start orig, EnemySpawner s /// The spawned game object. private void OnEnemySpawned(GameObject obj) { EntityFsmActions.CallEntitySpawnEvent(new EntitySpawnDetails { - Type = EntitySpawnType.SpawnerComponent, + Type = EntitySpawnType.EnemySpawnerComponent, GameObject = obj }); } diff --git a/HKMP/Game/Client/Entity/Component/EntityComponent.cs b/HKMP/Game/Client/Entity/Component/EntityComponent.cs index 77da03eb..98a2f029 100644 --- a/HKMP/Game/Client/Entity/Component/EntityComponent.cs +++ b/HKMP/Game/Client/Entity/Component/EntityComponent.cs @@ -82,5 +82,6 @@ internal enum EntityComponentType : byte { ZPosition, Climber, EnemySpawner, - ChildrenActivation + ChildrenActivation, + SpawnJar, } \ No newline at end of file diff --git a/HKMP/Game/Client/Entity/Component/SpawnJarComponent.cs b/HKMP/Game/Client/Entity/Component/SpawnJarComponent.cs new file mode 100644 index 00000000..8e9cd045 --- /dev/null +++ b/HKMP/Game/Client/Entity/Component/SpawnJarComponent.cs @@ -0,0 +1,169 @@ +using System; +using System.Collections; +using Hkmp.Game.Client.Entity.Action; +using Hkmp.Networking.Client; +using Hkmp.Networking.Packet.Data; +using Hkmp.Util; +using Modding; +using MonoMod.Cil; +using MonoMod.RuntimeDetour.HookGen; +using UnityEngine; +using Logger = Hkmp.Logging.Logger; +using Random = UnityEngine.Random; + +namespace Hkmp.Game.Client.Entity.Component; + +/// +/// This component manages the SpawnJarControl behaviour of the entity. +internal class SpawnJarComponent : EntityComponent { + /// + /// The unity component of the entity. + /// + private readonly HostClientPair _spawnJar; + + /// + /// Whether this is the first spawn for the jar. + /// + private bool _firstSpawn; + + public SpawnJarComponent( + NetClient netClient, + byte entityId, + HostClientPair gameObject, + HostClientPair spawnJar + ) : base(netClient, entityId, gameObject) { + _spawnJar = spawnJar; + spawnJar.Client.enabled = false; + + On.SpawnJarControl.OnEnable += SpawnJarControlOnOnEnable; + + // We can't simply hook the Behaviour method itself because it returns a state machine for the IEnumerator + // Instead we get the state machine target with MonoMod and get the hook that way + HookEndpointManager.Modify( + MonoMod.Utils.Extensions.GetStateMachineTarget( + ReflectionHelper.GetMethodInfo( + typeof(SpawnJarControl), + "Behaviour" + ) + ), + SpawnJarControlOnBehaviour + ); + + _firstSpawn = true; + } + + /// + /// Hook on the OnEnable method of the SpawnJarControl to network that it should start on the client-side. + /// + private void SpawnJarControlOnOnEnable(On.SpawnJarControl.orig_OnEnable orig, SpawnJarControl self) { + orig(self); + + if (self != _spawnJar.Host) { + return; + } + + if (_firstSpawn) { + return; + } + + var data = new EntityNetworkData { + Type = EntityComponentType.SpawnJar + }; + SendData(data); + + Logger.Debug("Sending SpawnJarComponent data OnEnable"); + } + + /// + /// IL hook for modifying the Behaviour method to grab the game object that is spawned from the jar. + /// + /// The IL context for the method. + private void SpawnJarControlOnBehaviour(ILContext il) { + try { + // Create a cursor for this context + var c = new ILCursor(il); + + // Goto the next call instruction Spawn twice, the first one is a the spawning of a nail strike + c.GotoNext(i => i.MatchCall(typeof(ObjectPoolExtensions), "Spawn")); + c.GotoNext(i => i.MatchCall(typeof(ObjectPoolExtensions), "Spawn")); + + // Move the cursor after the call instruction + c.Index++; + + // Emit a delegate that pops the current game object off the stack (our spawned object) and uses it + // before putting it back on the stack again + c.EmitDelegate>(gameObject => { + Logger.Debug($"SpawnJarControl spawned entity: {gameObject.name}"); + EntityFsmActions.CallEntitySpawnEvent(new EntitySpawnDetails { + Type = EntitySpawnType.SpawnJarComponent, + GameObject = gameObject + }); + + return gameObject; + }); + } catch (Exception e) { + Logger.Error($"Could not change SpawnJarControl#Behaviour IL:\n{e}"); + } + } + + /// + public override void InitializeHost() { + var data = new EntityNetworkData { + Type = EntityComponentType.SpawnJar + }; + SendData(data); + + Logger.Debug("Sending SpawnJarComponent data InitializeHost"); + + _firstSpawn = false; + } + + /// + public override void Update(EntityNetworkData data) { + Logger.Debug("Received SpawnJarComponent data"); + MonoBehaviourUtil.Instance.StartCoroutine(Behaviour()); + IEnumerator Behaviour() { + var jar = _spawnJar.Client; + + jar.transform.SetPositionZ(0.01f); + + jar.readyDust.Play(); + + yield return new WaitForSeconds(0.5f); + + var body = jar.GetComponent(); + body.angularVelocity = Random.Range(0, 2) > 0 ? -300f : 300f; + + jar.readyDust.Stop(); + jar.dustTrail.Play(); + + var sprite = jar.GetComponent(); + sprite.enabled = true; + + // The same while loop with a slightly higher threshold for breaking, because sometimes it doesn't reach + // that position quite yet due to network instability + while (jar.transform.position.y > jar.breakY + 0.1f) { + yield return null; + } + + GameCameras.instance.cameraShakeFSM.SendEvent("EnemyKillShake"); + + var position = jar.transform.position; + + jar.dustTrail.Stop(); + jar.ptBreakS.Play(); + jar.ptBreakL.Play(); + jar.strikeNailR.Spawn(position); + + body.angularVelocity = 0.0f; + + sprite.enabled = false; + + jar.breakSound.SpawnAndPlayOneShot(jar.audioSourcePrefab, position); + } + } + + /// + public override void Destroy() { + } +} \ No newline at end of file diff --git a/HKMP/Game/Client/Entity/EntityManager.cs b/HKMP/Game/Client/Entity/EntityManager.cs index 95be6ba1..23167207 100644 --- a/HKMP/Game/Client/Entity/EntityManager.cs +++ b/HKMP/Game/Client/Entity/EntityManager.cs @@ -212,9 +212,12 @@ private bool OnGameObjectSpawned(EntitySpawnDetails details) { Logger.Warn("Could not find registry entry for spawning type of object"); return false; } - } else if (details.Type == EntitySpawnType.SpawnerComponent) { + } else if (details.Type == EntitySpawnType.EnemySpawnerComponent) { spawningObjectName = "Vengefly Summon"; spawningType = EntityType.VengeflySummon; + } else if (details.Type == EntitySpawnType.SpawnJarComponent) { + spawningObjectName = "Spawn Jar"; + spawningType = EntityType.CollectorJar; } else { Logger.Error($"Invalid EntitySpawnDetails type: {details.Type}"); return false; diff --git a/HKMP/Game/Client/Entity/EntityRegistry.cs b/HKMP/Game/Client/Entity/EntityRegistry.cs index 7817868f..da38137d 100644 --- a/HKMP/Game/Client/Entity/EntityRegistry.cs +++ b/HKMP/Game/Client/Entity/EntityRegistry.cs @@ -92,6 +92,10 @@ out EntityRegistryEntry foundEntry if (gameObject.GetComponent() == null) { continue; } + } else if (entry.Type == EntityType.CollectorJar) { + if (gameObject.GetComponent() == null) { + continue; + } } foundEntry = entry; diff --git a/HKMP/Game/Client/Entity/EntitySpawnDetails.cs b/HKMP/Game/Client/Entity/EntitySpawnDetails.cs index c97ae7f2..01db24ae 100644 --- a/HKMP/Game/Client/Entity/EntitySpawnDetails.cs +++ b/HKMP/Game/Client/Entity/EntitySpawnDetails.cs @@ -28,5 +28,6 @@ internal class EntitySpawnDetails { /// internal enum EntitySpawnType { FsmAction, - SpawnerComponent + EnemySpawnerComponent, + SpawnJarComponent } diff --git a/HKMP/Game/Client/Entity/EntitySpawner.cs b/HKMP/Game/Client/Entity/EntitySpawner.cs index 06bc8d0e..094f27f6 100644 --- a/HKMP/Game/Client/Entity/EntitySpawner.cs +++ b/HKMP/Game/Client/Entity/EntitySpawner.cs @@ -11,6 +11,19 @@ namespace Hkmp.Game.Client.Entity; /// Static class that has implementations for spawning entities that are usually spawned by other entities in-game. /// internal static class EntitySpawner { + /// + /// Prefab for the Vengefly that can be summoned from a jar in The Collector boss fight. + /// + private static GameObject _collectorVengeflyPrefab; + /// + /// Prefab for the Aspid Hunter that can be summoned from a jar in The Collector boss fight. + /// + private static GameObject _collectorAspidPrefab; + /// + /// Prefab for the Baldur that can be summoned from a jar in The Collector boss fight. + /// + private static GameObject _collectorBaldurPrefab; + /// /// Spawn the game object for an entity with the given type that is spawned from the other given type. /// @@ -66,6 +79,14 @@ List clientFsms } } + if (spawningType == EntityType.TheCollector && spawnedType == EntityType.CollectorJar) { + return SpawnCollectorJarObject(clientFsms[0]); + } + + if (spawningType == EntityType.CollectorJar) { + return SpawnCollectorJarContents(clientObject, spawnedType); + } + return null; } @@ -211,4 +232,57 @@ private static GameObject SpawnSoulMaster2OrbObject(PlayMakerFSM fsm) { return SpawnFromGlobalPool(spawnAction, gameObject); } + + private static GameObject SpawnCollectorJarObject(PlayMakerFSM fsm) { + var setContents = fsm.GetFirstAction("Buzzer"); + _collectorVengeflyPrefab = setContents.enemyPrefab.Value; + setContents = fsm.GetFirstAction("Spitter"); + _collectorAspidPrefab = setContents.enemyPrefab.Value; + setContents = fsm.GetFirstAction("Roller"); + _collectorBaldurPrefab = setContents.enemyPrefab.Value; + + var spawnAction = fsm.GetFirstAction("Spawn"); + var gameObject = spawnAction.gameObject.Value; + + return SpawnFromGlobalPool(spawnAction, gameObject); + } + + private static GameObject SpawnCollectorJarContents(GameObject spawningObject, EntityType spawnedType) { + var spawnJarControl = spawningObject.GetComponent(); + if (spawnJarControl == null) { + Logger.Error("Could not find SpawnJarControl behaviour on spawning object"); + return null; + } + + GameObject gameObject; + int health; + var position = spawnJarControl.transform.position; + + switch (spawnedType) { + case EntityType.Vengefly: + gameObject = _collectorVengeflyPrefab.Spawn(position); + health = 8; + break; + case EntityType.AspidHunter: + gameObject = _collectorAspidPrefab.Spawn(position); + health = 15; + break; + case EntityType.Baldur: + gameObject = _collectorBaldurPrefab.Spawn(position); + health = 15; + break; + default: + Logger.Error($"Could not spawn object from collector jar: {spawnedType}"); + return null; + } + + var healthManager = gameObject.GetComponent(); + if (healthManager != null) { + healthManager.hp = health; + } + + gameObject.tag = "Boss"; + + return gameObject; + } } diff --git a/HKMP/Game/Client/Entity/EntityType.cs b/HKMP/Game/Client/Entity/EntityType.cs index 011aabcb..a966e9e8 100644 --- a/HKMP/Game/Client/Entity/EntityType.cs +++ b/HKMP/Game/Client/Entity/EntityType.cs @@ -4,14 +4,14 @@ namespace Hkmp.Game.Client.Entity; /// Enumeration of entity types. The entries more closely resemble the canonical name rather than the internal naming. /// internal enum EntityType { - Crawlid = 0, + BattleGate = 0, + Crawlid, Tiktik, Vengefly, WanderingHusk, HuskBully, HuskHornhead, Gruzzer, - BattleGate, AspidHunter, HuskGuard, LeapingHusk, @@ -82,4 +82,12 @@ internal enum EntityType { SoulMasterFakeQuake, SoulMasterWindow, SoulMasterPhase2, + HuskDandy, + CowardlyHusk, + GluttonousHusk, + GorgeousHusk, + GreatHuskSentry, + WatcherKnight, + TheCollector, + CollectorJar } diff --git a/HKMP/Resource/action-registry.json b/HKMP/Resource/action-registry.json index 12b0acc1..a0db5eb2 100644 --- a/HKMP/Resource/action-registry.json +++ b/HKMP/Resource/action-registry.json @@ -55,6 +55,10 @@ "type": "IntAdd", "update_field": "everyFrame" }, + { + "type": "IntAddV2", + "update_field": "everyFrame" + }, { "type": "IntOperator", "update_field": "everyFrame" diff --git a/HKMP/Resource/entity-registry.json b/HKMP/Resource/entity-registry.json index 3fa813b0..52d4ac7e 100644 --- a/HKMP/Resource/entity-registry.json +++ b/HKMP/Resource/entity-registry.json @@ -1,4 +1,9 @@ [ + { + "base_object_name": "Battle Gate", + "type": "BattleGate", + "fsm_name": "BG Control" + }, { "base_object_name": "Crawler", "type": "Crawlid", @@ -33,11 +38,6 @@ "type": "Gruzzer", "fsm_name": "Bouncer Control" }, - { - "base_object_name": "Battle Gate", - "type": "BattleGate", - "fsm_name": "BG Control" - }, { "base_object_name": "Spitter", "type": "AspidHunter", @@ -496,5 +496,47 @@ "type": "SoulMasterWindow" } ] + }, + { + "base_object_name": "Royal Zombie", + "type": "HuskDandy", + "fsm_name": "Attack" + }, + { + "base_object_name": "Royal Zombie Coward", + "type": "CowardlyHusk", + "fsm_name": "Coward Swipe" + }, + { + "base_object_name": "Royal Zombie Fat", + "type": "GluttonousHusk", + "fsm_name": "Attack" + }, + { + "base_object_name": "Gorgeous Husk", + "type": "GorgeousHusk", + "fsm_name": "Attack" + }, + { + "base_object_name": "Great Shield Zombie", + "type": "GreatHuskSentry", + "fsm_name": "ZombieShieldControl" + }, + { + "base_object_name": "Black Knight", + "type": "WatcherKnight", + "fsm_name": "Black Knight" + }, + { + "base_object_name": "Jar Collector", + "type": "TheCollector", + "fsm_name": "Control" + }, + { + "base_object_name": "Spawn Jar", + "type": "CollectorJar", + "components": [ + "SpawnJar" + ] } ] From 57aba40d6a42dbdc0879a0d2c92fd7458211c00e Mon Sep 17 00:00:00 2001 From: Extremelyd1 Date: Sat, 1 Jul 2023 00:00:43 +0200 Subject: [PATCH 050/216] Add Waterways enemies, including Dung Defender and Flukemarm --- .../Client/Entity/Action/EntityFsmActions.cs | 51 ++++++++++++ HKMP/Game/Client/Entity/Entity.cs | 9 ++- HKMP/Game/Client/Entity/EntityManager.cs | 23 ++++-- HKMP/Game/Client/Entity/EntitySpawner.cs | 23 ++++++ HKMP/Game/Client/Entity/EntityType.cs | 16 +++- HKMP/Resource/action-registry.json | 23 ++++++ HKMP/Resource/entity-registry.json | 81 +++++++++++++++++++ 7 files changed, 215 insertions(+), 11 deletions(-) diff --git a/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs b/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs index d3f69e90..e2f68f46 100644 --- a/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs +++ b/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs @@ -1631,6 +1631,30 @@ private static void ApplyNetworkDataFromAction(EntityNetworkData data, SendEvent #endregion + #region SendEventByNameV2 + + private static bool GetNetworkDataFromAction(EntityNetworkData data, SendEventByNameV2 action) { + if (action.eventTarget.gameObject.GameObject.Value == action.Fsm.GameObject.gameObject) { + return false; + } + + return true; + } + + private static void ApplyNetworkDataFromAction(EntityNetworkData data, SendEventByNameV2 action) { + if (action.delay.Value < 1.0 / 1000.0) { + action.Fsm.Event(action.eventTarget, action.sendEvent.Value); + } else { + action.Fsm.DelayedEvent( + action.eventTarget, + FsmEvent.GetFsmEvent(action.sendEvent.Value), + action.delay.Value + ); + } + } + + #endregion + #region SendHealthManagerDeathEvent private static bool GetNetworkDataFromAction(EntityNetworkData data, SendHealthManagerDeathEvent action) { @@ -2075,4 +2099,31 @@ private static void ApplyNetworkDataFromAction(EntityNetworkData data, AddCompon } #endregion + + #region PreBuildTK2DSprites + + private static bool GetNetworkDataFromAction(EntityNetworkData data, PreBuildTK2DSprites action) { + return true; + } + + private static void ApplyNetworkDataFromAction(EntityNetworkData data, PreBuildTK2DSprites action) { + var gameObject = action.gameObject.Value; + if (gameObject == null) { + return; + } + + tk2dSprite[] sprites; + + if (action.useChildren) { + sprites = gameObject.GetComponentsInChildren(true); + } else { + sprites = gameObject.GetComponents(); + } + + foreach (var sprite in sprites) { + sprite.ForceBuild(); + } + } + + #endregion } diff --git a/HKMP/Game/Client/Entity/Entity.cs b/HKMP/Game/Client/Entity/Entity.cs index c9b89053..a034359f 100644 --- a/HKMP/Game/Client/Entity/Entity.cs +++ b/HKMP/Game/Client/Entity/Entity.cs @@ -223,16 +223,17 @@ params EntityComponentType[] types // Specific handling of Oomas, since the corpse of the entity will be handled by the system as well, we // need to remove it from this entity. Otherwise we have duplicate corpses on the client-side - if (Type == EntityType.Ooma) { - Logger.Debug("Entity is Ooma, deleting death effects and corpse from client entity"); + if (Type is EntityType.Ooma or EntityType.Flukemon) { + Logger.Debug("Entity is Ooma or Flukemon, deleting death effects and corpse from client entity"); var enemyDeathEffects = Object.Client.GetComponent(); if (enemyDeathEffects == null) { Logger.Debug(" EnemyDeathEffects is null, cannot remove"); } UnityEngine.Object.Destroy(enemyDeathEffects); - - var corpse = Object.Client.FindGameObjectInChildren("Corpse Jellyfish(Clone)"); + + var corpseName = Type == EntityType.Ooma ? "Corpse Jellyfish(Clone)" : "Corpse Flukeman(Clone)"; + var corpse = Object.Client.FindGameObjectInChildren(corpseName); if (corpse == null) { Logger.Debug(" Could not find corpse in children"); } diff --git a/HKMP/Game/Client/Entity/EntityManager.cs b/HKMP/Game/Client/Entity/EntityManager.cs index 23167207..4bd46ebd 100644 --- a/HKMP/Game/Client/Entity/EntityManager.cs +++ b/HKMP/Game/Client/Entity/EntityManager.cs @@ -1,12 +1,15 @@ +using System; using System.Collections.Generic; using System.Linq; using Hkmp.Game.Client.Entity.Action; using Hkmp.Networking.Client; using Hkmp.Networking.Packet.Data; using Hkmp.Util; +using Modding; using UnityEngine; using UnityEngine.SceneManagement; using Logger = Hkmp.Logging.Logger; +using Object = UnityEngine.Object; namespace Hkmp.Game.Client.Entity; @@ -305,22 +308,30 @@ private void OnSceneLoaded(Scene scene, LoadSceneMode mode) { private void FindEntitiesInScene(Scene scene, bool lateLoad) { // Find all PlayMakerFSM components // Filter out FSMs with GameObjects not in the current scene - // Project each FSM to their GameObject and pre-instantiate the EnemyDeathEffects component is if exists + // Project each FSM to their GameObject and the corpse of the pre-instantiated EnemyDeathEffects component + // if it exists // Project each GameObject into its children including itself // Concatenate all GameObjects for Climber components (Tiktiks) // Concatenate all GameObjects for Walker components (Amblooms) // Filter out GameObjects not in the current scene var objectsToCheck = Object.FindObjectsOfType() .Where(fsm => fsm.gameObject.scene == scene) - .Select(fsm => { + .SelectMany(fsm => { var enemyDeathEffects = fsm.gameObject.GetComponent(); - if (enemyDeathEffects != null) { - enemyDeathEffects.PreInstantiate(); + if (enemyDeathEffects == null) { + return new[] { fsm.gameObject }; } - return fsm.gameObject; + enemyDeathEffects.PreInstantiate(); + + var corpse = ReflectionHelper.GetField( + enemyDeathEffects, + "corpse" + ); + + return new[] { fsm.gameObject, corpse }; }) - .SelectMany(obj => obj.GetChildren().Prepend(obj)) + .SelectMany(obj => obj == null ? Array.Empty() : obj.GetChildren().Prepend(obj)) .Concat(Object.FindObjectsOfType().Select(climber => climber.gameObject)) .Concat(Object.FindObjectsOfType().Select(walker => walker.gameObject)) .Where(obj => obj.scene == scene) diff --git a/HKMP/Game/Client/Entity/EntitySpawner.cs b/HKMP/Game/Client/Entity/EntitySpawner.cs index 094f27f6..dea88c9b 100644 --- a/HKMP/Game/Client/Entity/EntitySpawner.cs +++ b/HKMP/Game/Client/Entity/EntitySpawner.cs @@ -1,9 +1,11 @@ +using System; using System.Collections.Generic; using Hkmp.Util; using HutongGames.PlayMaker.Actions; using Modding; using UnityEngine; using Logger = Hkmp.Logging.Logger; +using Object = UnityEngine.Object; namespace Hkmp.Game.Client.Entity; @@ -87,6 +89,10 @@ List clientFsms return SpawnCollectorJarContents(clientObject, spawnedType); } + if (spawningType == EntityType.DungDefender) { + return SpawnDungBallObject(clientFsms[0], spawnedType); + } + return null; } @@ -285,4 +291,21 @@ private static GameObject SpawnCollectorJarContents(GameObject spawningObject, E return gameObject; } + + private static GameObject SpawnDungBallObject(PlayMakerFSM fsm, EntityType spawnedType) { + SpawnObjectFromGlobalPool action; + GameObject gameObject; + + if (spawnedType == EntityType.LargeDungBall) { + action = fsm.GetFirstAction("Throw 1"); + gameObject = action.gameObject.Value; + } else if (spawnedType == EntityType.SmallDungBall) { + action = fsm.GetFirstAction("Erupt Out"); + gameObject = action.gameObject.Value; + } else { + throw new InvalidOperationException($"Could not spawn spawned type from Dung Defender: {spawnedType}"); + } + + return SpawnFromGlobalPool(action, gameObject); + } } diff --git a/HKMP/Game/Client/Entity/EntityType.cs b/HKMP/Game/Client/Entity/EntityType.cs index a966e9e8..f8d8f8e3 100644 --- a/HKMP/Game/Client/Entity/EntityType.cs +++ b/HKMP/Game/Client/Entity/EntityType.cs @@ -89,5 +89,19 @@ internal enum EntityType { GreatHuskSentry, WatcherKnight, TheCollector, - CollectorJar + CollectorJar, + Belfly, + Pilflip, + Hwurmp, + HwurmpChild, + Bluggsac, + Flukefey, + Flukemon, + FlukemonBot, + FlukemonTop, + DungDefender, + LargeDungBall, + SmallDungBall, + DungDefenderBurrow, + Flukemarm, } diff --git a/HKMP/Resource/action-registry.json b/HKMP/Resource/action-registry.json index a0db5eb2..a1ad9800 100644 --- a/HKMP/Resource/action-registry.json +++ b/HKMP/Resource/action-registry.json @@ -208,6 +208,10 @@ "type": "FaceObject", "update_field": "everyFrame" }, + { + "type": "FaceAngle", + "update_field": "everyFrame" + }, { "type": "Translate", "update_field": "everyFrame" @@ -223,5 +227,24 @@ }, { "type": "DistanceFlySmooth" + }, + { + "type": "Collision2dEventLayer" + }, + { + "type": "Trigger2dEvent" + }, + { + "type": "IdleBuzz" + }, + { + "type": "ReceivedDamage" + }, + { + "type": "GetSpeed2d", + "update_field": "everyFrame" + }, + { + "type": "ChaseObjectGround" } ] diff --git a/HKMP/Resource/entity-registry.json b/HKMP/Resource/entity-registry.json index 52d4ac7e..76e849a3 100644 --- a/HKMP/Resource/entity-registry.json +++ b/HKMP/Resource/entity-registry.json @@ -538,5 +538,86 @@ "components": [ "SpawnJar" ] + }, + { + "base_object_name": "Ceiling Dropper", + "type": "Belfly", + "fsm_name": "Ceiling Dropper" + }, + { + "base_object_name": "Flip Hopper", + "type": "Pilflip", + "fsm_name": "Attack" + }, + { + "base_object_name": "Inflater", + "type": "Hwurmp", + "fsm_name": "Inflater", + "children": [ + { + "base_object_name": "Enemy Inflater", + "type": "HwurmpChild", + "fsm_name": "Send Inflate Event" + }, + { + "base_object_name": "Bouncer", + "type": "HwurmpChild" + } + ] + }, + { + "base_object_name": "Egg Sac", + "type": "Bluggsac" + }, + { + "base_object_name": "Fluke Fly", + "type": "Flukefey", + "fsm_name": "Fluke Fly", + "components": [ + "Rotation" + ] + }, + { + "base_object_name": "Flukeman", + "type": "Flukemon", + "fsm_name": "Flukeman" + }, + { + "base_object_name": "Flukeman Bot", + "type": "FlukemonBot", + "fsm_name": "Flukeman Bot" + }, + { + "base_object_name": "Flukeman Top", + "type": "FlukemonTop", + "fsm_name": "Flukeman Top" + }, + { + "base_object_name": "Dung Defender", + "type": "DungDefender", + "fsm_name": "Dung Defender", + "components": [ + "GravityScale" + ] + }, + { + "base_object_name": "Dung Ball Large", + "type": "LargeDungBall", + "fsm_name": "Ball Control" + }, + { + "base_object_name": "Dung Ball Small", + "type": "SmallDungBall", + "fsm_name": "Ball Control" + }, + { + "base_object_name": "Burrow Effect", + "type": "DungDefenderBurrow", + "fsm_name": "Burrow Effect" + }, + { + "base_object_name": "Fluke Mother", + "type": "Flukemarm", + "fsm_name": "Fluke Mother" } ] From 8bada3e36156459b695925d6b99e824811129856 Mon Sep 17 00:00:00 2001 From: Extremelyd1 Date: Sun, 2 Jul 2023 16:27:21 +0200 Subject: [PATCH 051/216] Add Crystal Peak enemies, fix position error --- .../Client/Entity/Action/EntityFsmActions.cs | 16 +++++-- HKMP/Game/Client/Entity/Entity.cs | 6 +++ HKMP/Game/Client/Entity/EntityInitializer.cs | 34 ++++++++++++-- HKMP/Game/Client/Entity/EntityManager.cs | 24 ++++++---- HKMP/Game/Client/Entity/EntityType.cs | 8 ++++ HKMP/Resource/entity-registry.json | 46 +++++++++++++++++++ 6 files changed, 116 insertions(+), 18 deletions(-) diff --git a/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs b/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs index e2f68f46..232775d6 100644 --- a/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs +++ b/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs @@ -1130,9 +1130,7 @@ private static void ApplyNetworkDataFromAction(EntityNetworkData data, SetParent } var parent = action.parent.Value; - if (parent != null) { - gameObject.transform.parent = parent.transform; - } + gameObject.transform.parent = parent != null ? parent.transform : null; if (action.resetLocalPosition.Value) { gameObject.transform.localPosition = Vector3.zero; @@ -1141,6 +1139,17 @@ private static void ApplyNetworkDataFromAction(EntityNetworkData data, SetParent if (action.resetLocalRotation.Value) { gameObject.transform.localRotation = Quaternion.identity; } + + if (parent == null) { + var fsms = gameObject.GetComponents(); + foreach (var fsm in fsms) { + if (fsm.Fsm.Name.Equals("destroy_if_gameobject_null")) { + Object.Destroy(fsm); + + Logger.Debug($"De-parented object contained \"{fsm.Fsm.Name}\" FSM, removing it"); + } + } + } } #endregion @@ -1333,6 +1342,7 @@ private static void ApplyNetworkDataFromAction(EntityNetworkData data, SetPositi } } else { vector3 = new Vector3( + data.Packet.ReadFloat(), data.Packet.ReadFloat(), data.Packet.ReadFloat() ); diff --git a/HKMP/Game/Client/Entity/Entity.cs b/HKMP/Game/Client/Entity/Entity.cs index a034359f..f7237272 100644 --- a/HKMP/Game/Client/Entity/Entity.cs +++ b/HKMP/Game/Client/Entity/Entity.cs @@ -134,6 +134,12 @@ params EntityComponentType[] types ) }; + if (Object.Host.scene != Object.Client.scene) { + Logger.Debug($"Entity client object instantiated in other scene: \"{Object.Host.scene.name}\", \"{Object.Client.scene.name}\", moving client"); + + UnityEngine.SceneManagement.SceneManager.MoveGameObjectToScene(Object.Client, Object.Host.scene); + } + _hasParent = false; } else { Object = new HostClientPair { diff --git a/HKMP/Game/Client/Entity/EntityInitializer.cs b/HKMP/Game/Client/Entity/EntityInitializer.cs index 84fbfb90..9901829d 100644 --- a/HKMP/Game/Client/Entity/EntityInitializer.cs +++ b/HKMP/Game/Client/Entity/EntityInitializer.cs @@ -1,8 +1,10 @@ using System; using System.Collections; +using System.Collections.Generic; using System.Linq; using Hkmp.Game.Client.Entity.Action; using Hkmp.Util; +using HutongGames.PlayMaker; using HutongGames.PlayMaker.Actions; using UnityEngine; using Logger = Hkmp.Logging.Logger; @@ -24,7 +26,8 @@ internal static class EntityInitializer { "initialize", "dormant", "pause", - "init pause" + "init pause", + "deparents" }; /// @@ -39,12 +42,32 @@ internal static class EntityInitializer { /// /// The FSM to initialize. public static void InitializeFsm(PlayMakerFSM fsm) { - // Check for all states whether they are initialize states + // Create a list of states to initialize later + var statesToInit = new List(); + // Keep track of the indices where the individual initialization states begin in our final list + var indices = new int[InitStateNames.Length]; + + // Go over each state in the FSM foreach (var state in fsm.FsmStates) { - if (!InitStateNames.Contains(state.Name.ToLower())) { + var stateName = state.Name.ToLower(); + var index = Array.IndexOf(InitStateNames, stateName); + // Check if it is a "init" state + if (index == -1) { continue; } + // Then insert it at the correct index according to our tracked indices + statesToInit.Insert(indices[index], state); + // Increase all indices that come after, since we inserted something before + for (var i = index; i < indices.Length; i++) { + indices[i]++; + } + } + + // Now we can loop over the states in the same order as our "InitStateNames" array + foreach (var state in statesToInit) { + Logger.Debug($"Found initialization state: {state.Name}, executing actions"); + // Go over each action and try to execute it by applying empty data to it foreach (var action in state.Actions) { if (!action.Enabled) { @@ -54,7 +77,7 @@ public static void InitializeFsm(PlayMakerFSM fsm) { if (ToSkipTypes.Contains(action.GetType())) { continue; } - + if (EntityFsmActions.SupportedActionTypes.Contains(action.GetType())) { if (action.Fsm == null) { Logger.Debug("Initializing FSM and action.Fsm is null, starting coroutine"); @@ -72,7 +95,8 @@ IEnumerator WaitForActionInitialization() { continue; } - + Logger.Debug($" Executing action {action.GetType()} for initialization"); + EntityFsmActions.ApplyNetworkDataFromAction(null, action); } } diff --git a/HKMP/Game/Client/Entity/EntityManager.cs b/HKMP/Game/Client/Entity/EntityManager.cs index 4bd46ebd..ba9598a2 100644 --- a/HKMP/Game/Client/Entity/EntityManager.cs +++ b/HKMP/Game/Client/Entity/EntityManager.cs @@ -306,20 +306,20 @@ private void OnSceneLoaded(Scene scene, LoadSceneMode mode) { /// The scene to find entities in. /// Whether this scene was loaded late. private void FindEntitiesInScene(Scene scene, bool lateLoad) { - // Find all PlayMakerFSM components - // Filter out FSMs with GameObjects not in the current scene - // Project each FSM to their GameObject and the corpse of the pre-instantiated EnemyDeathEffects component - // if it exists + // Find all EnemyDeathEffects components + // Filter out EnemyDeathEffects components not in the current scene + // Project each death effect to their GameObject and the corpse of the pre-instantiated EnemyDeathEffects + // component + // Concatenate all GameObjects for PlayMakerFSM components in the current scene // Project each GameObject into its children including itself // Concatenate all GameObjects for Climber components (Tiktiks) // Concatenate all GameObjects for Walker components (Amblooms) // Filter out GameObjects not in the current scene - var objectsToCheck = Object.FindObjectsOfType() - .Where(fsm => fsm.gameObject.scene == scene) - .SelectMany(fsm => { - var enemyDeathEffects = fsm.gameObject.GetComponent(); + var objectsToCheck = Object.FindObjectsOfType() + .Where(e => e.gameObject.scene == scene) + .SelectMany(enemyDeathEffects => { if (enemyDeathEffects == null) { - return new[] { fsm.gameObject }; + return new[] { enemyDeathEffects.gameObject }; } enemyDeathEffects.PreInstantiate(); @@ -329,8 +329,12 @@ private void FindEntitiesInScene(Scene scene, bool lateLoad) { "corpse" ); - return new[] { fsm.gameObject, corpse }; + return new[] { enemyDeathEffects.gameObject, corpse }; }) + .Concat(Object.FindObjectsOfType() + .Where(fsm => fsm.gameObject.scene == scene) + .Select(fsm => fsm.gameObject) + ) .SelectMany(obj => obj == null ? Array.Empty() : obj.GetChildren().Prepend(obj)) .Concat(Object.FindObjectsOfType().Select(climber => climber.gameObject)) .Concat(Object.FindObjectsOfType().Select(walker => walker.gameObject)) diff --git a/HKMP/Game/Client/Entity/EntityType.cs b/HKMP/Game/Client/Entity/EntityType.cs index f8d8f8e3..4388eb45 100644 --- a/HKMP/Game/Client/Entity/EntityType.cs +++ b/HKMP/Game/Client/Entity/EntityType.cs @@ -104,4 +104,12 @@ internal enum EntityType { SmallDungBall, DungDefenderBurrow, Flukemarm, + Shardmite, + Glimback, + CrystalHunter, + CrystalCrawler, + HuskMiner, + CrystallisedHusk, + CrystalGuardian, + CrystalGuardianLaser } diff --git a/HKMP/Resource/entity-registry.json b/HKMP/Resource/entity-registry.json index 76e849a3..e2803efb 100644 --- a/HKMP/Resource/entity-registry.json +++ b/HKMP/Resource/entity-registry.json @@ -619,5 +619,51 @@ "base_object_name": "Fluke Mother", "type": "Flukemarm", "fsm_name": "Fluke Mother" + }, + { + "base_object_name": "Zombie Miner", + "type": "HuskMiner", + "fsm_name": "Zombie Miner" + }, + { + "base_object_name": "Zombie Beam Miner", + "type": "CrystallisedHusk", + "fsm_name": "Beam Miner" + }, + { + "base_object_name": "Mines Crawler", + "type": "Shardmite", + "fsm_name": "Mines Crawler", + "components": [ + "Rotation" + ] + }, + { + "base_object_name": "Crystal Crawler", + "type": "Glimback", + "fsm_name": "Crawler" + }, + { + "base_object_name": "Crystal Flyer", + "type": "CrystalHunter", + "fsm_name": "Crystal Flyer" + }, + { + "base_object_name": "Crystallised Lazer Bug", + "type": "CrystalCrawler", + "fsm_name": "Laser Bug" + }, + { + "base_object_name": "Mega Zombie Beam Miner", + "type": "CrystalGuardian", + "fsm_name": "Beam Miner" + }, + { + "base_object_name": "Laser Turret Mega", + "type": "CrystalGuardianLaser", + "fsm_name": "Laser Bug Mega", + "components": [ + "Rotation" + ] } ] From ee83c771a3ee45e7a2486166f56ed2f24f05d9ad Mon Sep 17 00:00:00 2001 From: Extremelyd1 Date: Sun, 2 Jul 2023 17:03:38 +0200 Subject: [PATCH 052/216] Add Infected Crossroads enemies --- HKMP/Game/Client/Entity/EntityType.cs | 6 +++++- HKMP/Resource/entity-registry.json | 20 ++++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/HKMP/Game/Client/Entity/EntityType.cs b/HKMP/Game/Client/Entity/EntityType.cs index 4388eb45..bc136ff3 100644 --- a/HKMP/Game/Client/Entity/EntityType.cs +++ b/HKMP/Game/Client/Entity/EntityType.cs @@ -111,5 +111,9 @@ internal enum EntityType { HuskMiner, CrystallisedHusk, CrystalGuardian, - CrystalGuardianLaser + CrystalGuardianLaser, + FuriousVengefly, + VolatileGruzzer, + ViolentHusk, + SlobberingHusk } diff --git a/HKMP/Resource/entity-registry.json b/HKMP/Resource/entity-registry.json index e2803efb..993ab2e9 100644 --- a/HKMP/Resource/entity-registry.json +++ b/HKMP/Resource/entity-registry.json @@ -665,5 +665,25 @@ "components": [ "Rotation" ] + }, + { + "base_object_name": "AngryBuzzer", + "type": "FuriousVengefly", + "fsm_name": "Control" + }, + { + "base_object_name": "Bursting Bouncer", + "type": "VolatileGruzzer", + "fsm_name": "Bouncer Control" + }, + { + "base_object_name": "Bursting Zombie", + "type": "ViolentHusk", + "fsm_name": "Attack" + }, + { + "base_object_name": "Spitting Zombie", + "type": "SlobberingHusk", + "fsm_name": "Spit" } ] From b186a51ea71d9067a0d5f21bca5459d91229baf4 Mon Sep 17 00:00:00 2001 From: Extremelyd1 Date: Sun, 2 Jul 2023 19:45:24 +0200 Subject: [PATCH 053/216] Add Deepnest enemies --- .../Client/Entity/Action/EntityFsmActions.cs | 34 +++++++++++ HKMP/Game/Client/Entity/EntityManager.cs | 1 + HKMP/Game/Client/Entity/EntityRegistry.cs | 4 ++ HKMP/Game/Client/Entity/EntitySpawner.cs | 32 ++++++++++ HKMP/Game/Client/Entity/EntityType.cs | 12 +++- HKMP/Resource/entity-registry.json | 58 +++++++++++++++++++ 6 files changed, 140 insertions(+), 1 deletion(-) diff --git a/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs b/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs index 232775d6..665d6cda 100644 --- a/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs +++ b/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs @@ -92,6 +92,7 @@ static EntityFsmActions() { // 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; } @@ -244,6 +245,39 @@ private static void FlingObjectsFromGlobalPoolVelOnEnter(ILContext il) { } } + /// + /// IL edit method for modifying the + /// method to network the repeated spawning of objects. + /// + private static void FlingObjectsFromGlobalPoolTimeOnUpdate(ILContext il) { + try { + // Create a cursor for this context + var c = new ILCursor(il); + + // Goto the next call instruction for Random.Range() + c.GotoNext(i => i.MatchCall(typeof(ObjectPoolExtensions), "Spawn")); + + // 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 spawned object off the stack and pushes it onto it again + c.EmitDelegate>((gameObject, action) => { + EntitySpawnEvent?.Invoke(new EntitySpawnDetails { + Type = EntitySpawnType.FsmAction, + Action = action, + GameObject = gameObject + }); + + return gameObject; + }); + } catch (Exception e) { + Logger.Error($"Could not change FlingObjectsFromGlobalPoolTime#OnUpdate IL:\n{e}"); + } + } + /// /// IL edit method for modifying the DoGetRandomChild /// method to store the results of the random calls. diff --git a/HKMP/Game/Client/Entity/EntityManager.cs b/HKMP/Game/Client/Entity/EntityManager.cs index ba9598a2..0e8a6c44 100644 --- a/HKMP/Game/Client/Entity/EntityManager.cs +++ b/HKMP/Game/Client/Entity/EntityManager.cs @@ -338,6 +338,7 @@ private void FindEntitiesInScene(Scene scene, bool lateLoad) { .SelectMany(obj => obj == null ? Array.Empty() : obj.GetChildren().Prepend(obj)) .Concat(Object.FindObjectsOfType().Select(climber => climber.gameObject)) .Concat(Object.FindObjectsOfType().Select(walker => walker.gameObject)) + .Concat(Object.FindObjectsOfType().Select(centipede => centipede.gameObject)) .Where(obj => obj.scene == scene) .Distinct(); diff --git a/HKMP/Game/Client/Entity/EntityRegistry.cs b/HKMP/Game/Client/Entity/EntityRegistry.cs index da38137d..518efa0c 100644 --- a/HKMP/Game/Client/Entity/EntityRegistry.cs +++ b/HKMP/Game/Client/Entity/EntityRegistry.cs @@ -96,6 +96,10 @@ out EntityRegistryEntry foundEntry if (gameObject.GetComponent() == null) { continue; } + } else if (entry.Type == EntityType.Garpede) { + if (gameObject.GetComponent() == null) { + continue; + } } foundEntry = entry; diff --git a/HKMP/Game/Client/Entity/EntitySpawner.cs b/HKMP/Game/Client/Entity/EntitySpawner.cs index dea88c9b..dc6b6eca 100644 --- a/HKMP/Game/Client/Entity/EntitySpawner.cs +++ b/HKMP/Game/Client/Entity/EntitySpawner.cs @@ -93,6 +93,10 @@ List clientFsms return SpawnDungBallObject(clientFsms[0], spawnedType); } + if (spawningType == EntityType.Nosk && spawnedType == EntityType.NoskBlob) { + return SpawnNoskBlobObject(clientFsms[0]); + } + return null; } @@ -156,6 +160,27 @@ private static GameObject SpawnFromGlobalPool(SpawnObjectFromGlobalPool action, return spawnedObject; } + + private static GameObject SpawnFromFlingGlobalPoolTime( + FlingObjectsFromGlobalPoolTime action, + GameObject gameObject + ) { + var position = Vector3.zero; + var zero = Vector3.zero; + if (action.spawnPoint.Value != null) { + position = action.spawnPoint.Value.transform.position; + if (!action.position.IsNone) { + position += action.position.Value; + } + } else { + if (!action.position.IsNone) { + position = action.position.Value; + } + } + + var spawnedObject = gameObject.Spawn(position, Quaternion.Euler(zero)); + return spawnedObject; + } private static GameObject SpawnBaldurGameObject(PlayMakerFSM fsm) { var setGameObjectAction = fsm.GetFirstAction("Roller"); @@ -308,4 +333,11 @@ private static GameObject SpawnDungBallObject(PlayMakerFSM fsm, EntityType spawn return SpawnFromGlobalPool(action, gameObject); } + + private static GameObject SpawnNoskBlobObject(PlayMakerFSM fsm) { + var action = fsm.GetFirstAction("Roof Drop"); + var gameObject = action.gameObject.Value; + + return SpawnFromFlingGlobalPoolTime(action, gameObject); + } } diff --git a/HKMP/Game/Client/Entity/EntityType.cs b/HKMP/Game/Client/Entity/EntityType.cs index bc136ff3..094eea8e 100644 --- a/HKMP/Game/Client/Entity/EntityType.cs +++ b/HKMP/Game/Client/Entity/EntityType.cs @@ -115,5 +115,15 @@ internal enum EntityType { FuriousVengefly, VolatileGruzzer, ViolentHusk, - SlobberingHusk + SlobberingHusk, + Dirtcarver, + CarverHatcher, + Garpede, + CorpseCreeper, + Deepling, + Deephunter, + LittleWeaver, + StalkingDevout, + Nosk, + NoskBlob } diff --git a/HKMP/Resource/entity-registry.json b/HKMP/Resource/entity-registry.json index 993ab2e9..c5e7f6b0 100644 --- a/HKMP/Resource/entity-registry.json +++ b/HKMP/Resource/entity-registry.json @@ -685,5 +685,63 @@ "base_object_name": "Spitting Zombie", "type": "SlobberingHusk", "fsm_name": "Spit" + }, + { + "base_object_name": "Baby Centipede", + "type": "Dirtcarver", + "fsm_name": "Centipede" + }, + { + "base_object_name": "Centipede Hatcher", + "type": "CarverHatcher", + "fsm_name": "Centipede Hatcher" + }, + { + "base_object_name": "Big Centipede", + "type": "Garpede" + }, + { + "base_object_name": "Zombie Spider", + "type": "CorpseCreeper", + "fsm_name": "Chase" + }, + { + "base_object_name": "Tiny Spider", + "type": "Deepling", + "fsm_name": "Spawn", + "components": [ + "Rotation" + ] + }, + { + "base_object_name": "Spider Mini", + "type": "Deephunter", + "fsm_name": "Spider", + "components": [ + "Rotation" + ] + }, + { + "base_object_name": "Spider Flyer", + "type": "LittleWeaver", + "fsm_name": "Control" + }, + { + "base_object_name": "Slash Spider", + "type": "StalkingDevout", + "fsm_name": "Slash Spider" + }, + { + "base_object_name": "Mimic Spider", + "type": "Nosk", + "fsm_name": "Mimic Spider", + "components": [ + "GravityScale" + ] + }, + { + "base_object_name": "Vomit Glob Nosk", + "type": "NoskBlob", + "fsm_name": "Vomit Glob" } ] From 69a4ed02c8952952d0cd2017a1fa6455c3510a21 Mon Sep 17 00:00:00 2001 From: Extremelyd1 Date: Sun, 2 Jul 2023 20:09:22 +0200 Subject: [PATCH 054/216] Fix some Deepnest enemies --- HKMP/Game/Client/Entity/Entity.cs | 58 ++++++++++++++++++++---------- HKMP/Resource/entity-registry.json | 15 ++++++-- 2 files changed, 51 insertions(+), 22 deletions(-) diff --git a/HKMP/Game/Client/Entity/Entity.cs b/HKMP/Game/Client/Entity/Entity.cs index f7237272..ca6b3bbd 100644 --- a/HKMP/Game/Client/Entity/Entity.cs +++ b/HKMP/Game/Client/Entity/Entity.cs @@ -226,26 +226,9 @@ params EntityComponentType[] types _components = new Dictionary(); HandleComponents(types); - - // Specific handling of Oomas, since the corpse of the entity will be handled by the system as well, we - // need to remove it from this entity. Otherwise we have duplicate corpses on the client-side - if (Type is EntityType.Ooma or EntityType.Flukemon) { - Logger.Debug("Entity is Ooma or Flukemon, deleting death effects and corpse from client entity"); - - var enemyDeathEffects = Object.Client.GetComponent(); - if (enemyDeathEffects == null) { - Logger.Debug(" EnemyDeathEffects is null, cannot remove"); - } - UnityEngine.Object.Destroy(enemyDeathEffects); - - var corpseName = Type == EntityType.Ooma ? "Corpse Jellyfish(Clone)" : "Corpse Flukeman(Clone)"; - var corpse = Object.Client.FindGameObjectInChildren(corpseName); - if (corpse == null) { - Logger.Debug(" Could not find corpse in children"); - } - UnityEngine.Object.Destroy(corpse); - } + HandleEnemyDeathEffects(); + Object.Host.SetActive(false); Object.Client.SetActive(false); @@ -450,6 +433,43 @@ private void HandleComponents(EntityComponentType[] types) { Logger.Debug(addedComponentsString); } + /// + /// Handle specifics for a set of enemies that rely on EnemyDeathEffects for additional enemies. + /// + private void HandleEnemyDeathEffects() { + string corpseName; + switch (Type) { + case EntityType.Ooma: + corpseName = "Corpse Jellyfish(Clone)"; + break; + case EntityType.Flukemon: + corpseName = "Corpse Flukeman(Clone)"; + break; + case EntityType.HuskHornhead: + corpseName = "Zombie Spider 2(Clone)"; + break; + case EntityType.WanderingHusk: + corpseName = "Zombie Spider 1(Clone)"; + break; + default: + return; + } + + Logger.Debug("Entity has corpse that is also enemy, deleting death effects and corpse from client entity"); + + var enemyDeathEffects = Object.Client.GetComponent(); + if (enemyDeathEffects == null) { + Logger.Debug(" EnemyDeathEffects is null, cannot remove"); + } + UnityEngine.Object.Destroy(enemyDeathEffects); + + var corpse = Object.Client.FindGameObjectInChildren(corpseName); + if (corpse == null) { + Logger.Debug(" Could not find corpse in children"); + } + UnityEngine.Object.Destroy(corpse); + } + /// /// Callback method for entering a hooked FSM action. /// diff --git a/HKMP/Resource/entity-registry.json b/HKMP/Resource/entity-registry.json index c5e7f6b0..e80affeb 100644 --- a/HKMP/Resource/entity-registry.json +++ b/HKMP/Resource/entity-registry.json @@ -689,16 +689,25 @@ { "base_object_name": "Baby Centipede", "type": "Dirtcarver", - "fsm_name": "Centipede" + "fsm_name": "Centipede", + "components": [ + "ZPosition" + ] }, { "base_object_name": "Centipede Hatcher", "type": "CarverHatcher", - "fsm_name": "Centipede Hatcher" + "fsm_name": "Centipede Hatcher", + "components": [ + "ZPosition" + ] }, { "base_object_name": "Big Centipede", - "type": "Garpede" + "type": "Garpede", + "components": [ + "ZPosition" + ] }, { "base_object_name": "Zombie Spider", From 1336cfb64b14b720bfbe9ec1d4f4cca86a993d2e Mon Sep 17 00:00:00 2001 From: Extremelyd1 Date: Sun, 2 Jul 2023 22:53:56 +0200 Subject: [PATCH 055/216] Add Abyss, Kingdom's Edge, Hive, Queen's Gardens and Colosseum enemies Largely untested yet --- HKMP/Game/Client/Entity/EntityRegistry.cs | 4 + HKMP/Game/Client/Entity/EntityType.cs | 39 ++++- HKMP/Resource/action-registry.json | 6 + HKMP/Resource/entity-registry.json | 178 ++++++++++++++++++++++ 4 files changed, 226 insertions(+), 1 deletion(-) diff --git a/HKMP/Game/Client/Entity/EntityRegistry.cs b/HKMP/Game/Client/Entity/EntityRegistry.cs index 518efa0c..6947f5c7 100644 --- a/HKMP/Game/Client/Entity/EntityRegistry.cs +++ b/HKMP/Game/Client/Entity/EntityRegistry.cs @@ -100,6 +100,10 @@ out EntityRegistryEntry foundEntry if (gameObject.GetComponent() == null) { continue; } + } else if (entry.Type == EntityType.ShadowCreeper) { + if (gameObject.GetComponent() == null) { + continue; + } } foundEntry = entry; diff --git a/HKMP/Game/Client/Entity/EntityType.cs b/HKMP/Game/Client/Entity/EntityType.cs index 094eea8e..b3aa6a86 100644 --- a/HKMP/Game/Client/Entity/EntityType.cs +++ b/HKMP/Game/Client/Entity/EntityType.cs @@ -31,6 +31,7 @@ internal enum EntityType { BroodingMawlekHead, BroodingMawlekDummy, Mosscreep, + Mossfly, Mosskin, VolatileMosskin, FoolEater, @@ -125,5 +126,41 @@ internal enum EntityType { LittleWeaver, StalkingDevout, Nosk, - NoskBlob + NoskBlob, + ShadowCreeper, + LesserMawlek, + Mawlurk, + InfectedBalloon, + BrokenVessel, + Boofly, + PrimalAspid, + Hopper, + GreatHopper, + GrubMimic, + Hiveling, + HiveSoldier, + HiveGuardian, + HuskHive, + SpinyHusk, + Loodle, + MantisPetra, + MantisTraitor, + TraitorLord, + ColosseumCageSmall, + ColosseumCageLarge, + ColosseumPlatform, + ColosseumSpike, + SharpBaldur, + ArmouredSquit, + BattleObble, + Oblobble, + ShieldedFool, + SturdyFool, + WingedFool, + HeavyFool, + //DeathLoodle, + VoltTwister, + Zote, + Tamer, + Beast } diff --git a/HKMP/Resource/action-registry.json b/HKMP/Resource/action-registry.json index a1ad9800..6c4a4088 100644 --- a/HKMP/Resource/action-registry.json +++ b/HKMP/Resource/action-registry.json @@ -237,6 +237,12 @@ { "type": "IdleBuzz" }, + { + "type": "IdleBuzzV2" + }, + { + "type": "IdleBuzzV3" + }, { "type": "ReceivedDamage" }, diff --git a/HKMP/Resource/entity-registry.json b/HKMP/Resource/entity-registry.json index e80affeb..6781cba5 100644 --- a/HKMP/Resource/entity-registry.json +++ b/HKMP/Resource/entity-registry.json @@ -175,6 +175,11 @@ "Rotation" ] }, + { + "base_object_name": "Moss Flyer", + "type": "Mossfly", + "fsm_name": "Moss Flyer" + }, { "base_object_name": "Mossman_Runner", "type": "Mosskin", @@ -752,5 +757,178 @@ "base_object_name": "Vomit Glob Nosk", "type": "NoskBlob", "fsm_name": "Vomit Glob" + }, + { + "base_object_name": "Abyss Crawler", + "type": "ShadowCreeper", + "components": [ + "Rotation" + ] + }, + { + "base_object_name": "Lesser Mawlek", + "type": "LesserMawlek", + "fsm_name": "Lesser Mawlek" + }, + { + "base_object_name": "Mawlek Turret", + "type": "Mawlurk", + "fsm_name": "Mawlek Turret" + }, + { + "base_object_name": "Parasite Balloon", + "type": "InfectedBalloon", + "fsm_name": "Control" + }, + { + "base_object_name": "Infected Knight", + "type": "BrokenVessel", + "fsm_name": "IK Control" + }, + { + "base_object_name": "Blow Fly", + "type": "Boofly", + "fsm_name": "Blow Fly" + }, + { + "base_object_name": "Super Spitter", + "type": "PrimalAspid", + "fsm_name": "spitter" + }, + { + "base_object_name": "Giant Hopper", + "type": "GreatHopper", + "fsm_name": "Hopper" + }, + { + "base_object_name": "Grub Mimic", + "type": "GrubMimic", + "fsm_name": "Grub Mimic" + }, + { + "base_object_name": "Bee Hatchling", + "type": "Hiveling", + "fsm_name": "Bee" + }, + { + "base_object_name": "Hiveling Spawner", + "type": "Hiveling", + "fsm_name": "Control" + }, + { + "base_object_name": "Bee Stinger", + "type": "HiveSoldier", + "fsm_name": "Bee Stinger", + "components": [ + "Rotation" + ] + }, + { + "base_object_name": "Big Bee", + "type": "HiveGuardian", + "fsm_name": "Big Bee" + }, + { + "base_object_name": "Zombie Hive", + "type": "HuskHive", + "fsm_name": "Hive Zombie" + }, + { + "base_object_name": "Grass Hopper", + "type": "Loodle", + "fsm_name": "Crazy Hopper", + "components": [ + "Rotation" + ] + }, + { + "base_object_name": "Mantis Heavy Flyer", + "type": "MantisPetra", + "fsm_name": "Heavy Flyer" + }, + { + "base_object_name": "Mantis Heavy", + "type": "MantisTraitor", + "fsm_name": "Mantis" + }, + { + "base_object_name": "Mantis Traitor Lord", + "type": "TraitorLord", + "fsm_name": "Mantis" + }, + { + "base_object_name": "Colosseum Cage Small", + "type": "ColosseumCageSmall", + "fsm_name": "Spawn" + }, + { + "base_object_name": "Colosseum Cage Large", + "type": "ColosseumCageLarge", + "fsm_name": "Spawn" + }, + { + "base_object_name": "Colosseum Platform", + "type": "ColosseumPlatform", + "fsm_name": "Control" + }, + { + "base_object_name": "Colosseum Spike", + "type": "ColosseumSpike", + "fsm_name": "Control" + }, + { + "base_object_name": "Colosseum_Armored_Roller", + "type": "SharpBaldur", + "fsm_name": "Roller" + }, + { + "base_object_name": "Colosseum_Armored_Mosquito", + "type": "ArmoredSquit", + "fsm_name": "Mozzie2" + }, + { + "base_object_name": "Colosseum_Shield_Zombie", + "type": "ShieldedFool", + "fsm_name": "ZombieShieldControl" + }, + { + "base_object_name": "Colosseum_Miner", + "type": "SturdyFool", + "fsm_name": "Zombie Miner" + }, + { + "base_object_name": "Colosseum_Worm", + "type": "HeavyFool", + "fsm_name": "Ruins Sentry Fat" + }, + { + "base_object_name": "Colosseum_Flying_Sentry", + "type": "WingedFool", + "fsm_name": "Flying Sentry Nail" + }, + { + "base_object_name": "Blobble", + "type": "BattleObble", + "fsm_name": "Fatty Fly Attack" + }, + { + "base_object_name": "Mega Fat Bee", + "type": "Oblobble", + "fsm_name": "Fatty Fly Attack" + }, + { + "base_object_name": "Electric Mage", + "type": "VoltTwister", + "fsm_name": "Electric Mage" + }, + { + "base_object_name": "Lancer", + "type": "Tamer", + "fsm_name": "Control" + }, + { + "base_object_name": "Lobster", + "type": "Beast", + "fsm_name": "Control" } ] From 42723b31c473b7434e7c73c7e3f2d48a1d78dbc7 Mon Sep 17 00:00:00 2001 From: Extremelyd1 Date: Tue, 4 Jul 2023 07:52:14 +0200 Subject: [PATCH 056/216] Fix issues in Colosseum trials --- .../Client/Entity/Action/EntityFsmActions.cs | 88 +++++++++++++++++ .../Entity/Component/ComponentFactory.cs | 8 ++ .../Entity/Component/EntityComponent.cs | 1 + .../Component/SpriteRendererComponent.cs | 73 ++++++++++++++ HKMP/Game/Client/Entity/Entity.cs | 18 ++++ HKMP/Game/Client/Entity/EntityManager.cs | 98 +++++++++++++++---- HKMP/Game/Client/Entity/EntitySpawner.cs | 53 ++++++++++ HKMP/Game/Client/Entity/EntityType.cs | 2 + HKMP/Resource/entity-registry.json | 39 +++++++- 9 files changed, 358 insertions(+), 22 deletions(-) create mode 100644 HKMP/Game/Client/Entity/Component/SpriteRendererComponent.cs diff --git a/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs b/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs index 665d6cda..c3e206ee 100644 --- a/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs +++ b/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs @@ -637,6 +637,11 @@ private static bool GetNetworkDataFromAction(EntityNetworkData data, CreateObjec private static void ApplyNetworkDataFromAction(EntityNetworkData data, CreateObject action) { Logger.Debug("ApplyNetworkDataFromAction CreateObject"); + + if (data == null) { + return; + } + var position = new Vector3( data.Packet.ReadFloat(), data.Packet.ReadFloat(), @@ -2170,4 +2175,87 @@ private static void ApplyNetworkDataFromAction(EntityNetworkData data, PreBuildT } #endregion + + #region GetPosition + + private static bool GetNetworkDataFromAction(EntityNetworkData data, GetPosition action) { + Logger.Debug($"Getting network data for GetPosition: {action.Fsm.GameObject.name}, {action.Fsm.Name}"); + + return action.Fsm.GameObject.name.StartsWith("Colosseum Manager") && action.Fsm.Name.Equals("Battle Control"); + } + + private static void ApplyNetworkDataFromAction(EntityNetworkData data, GetPosition action) { + var gameObject = action.Fsm.GetOwnerDefaultTarget(action.gameObject); + if (gameObject == null) { + return; + } + + var vector3 = action.space == Space.World ? gameObject.transform.position : gameObject.transform.localPosition; + action.vector.Value = vector3; + action.x.Value = vector3.x; + action.y.Value = vector3.y; + action.z.Value = vector3.z; + } + + #endregion + + #region CallMethodProper + + private static bool GetNetworkDataFromAction(EntityNetworkData data, CallMethodProper action) { + Logger.Debug($"Getting network data for CallMethodProper: {action.Fsm.GameObject.name}, {action.Fsm.Name}"); + + return action.Fsm.GameObject.name.StartsWith("Colosseum Manager") && action.Fsm.Name.Equals("Battle Control"); + } + + private static void ApplyNetworkDataFromAction(EntityNetworkData data, CallMethodProper action) { + if (action.behaviour.Value == null) { + return; + } + + var gameObject = action.Fsm.GetOwnerDefaultTarget(action.gameObject); + if (gameObject == null) { + return; + } + + var component = gameObject.GetComponent(action.behaviour.Value) as MonoBehaviour; + if (component == null) { + return; + } + + var type = component.GetType(); + var methodInfo = type.GetMethod(action.methodName.Value); + if (methodInfo == null) { + return; + } + + var parameterInfo = methodInfo.GetParameters(); + + object obj; + if (parameterInfo.Length == 0) { + obj = methodInfo.Invoke(component, null); + } else { + var paramArray = new object[action.parameters.Length]; + + for (var i = 0; i < action.parameters.Length; i++) { + var fsmVar = action.parameters[i]; + fsmVar.UpdateValue(); + paramArray[i] = fsmVar.GetValue(); + } + + try { + obj = methodInfo.Invoke(component, paramArray); + } catch (Exception e) { + Logger.Error($"Error applying CallMethodProper:\n{e}"); + return; + } + } + + if (action.storeResult.Type == VariableType.Unknown) { + return; + } + + action.storeResult.SetValue(obj); + } + + #endregion } diff --git a/HKMP/Game/Client/Entity/Component/ComponentFactory.cs b/HKMP/Game/Client/Entity/Component/ComponentFactory.cs index 3d474071..36753016 100644 --- a/HKMP/Game/Client/Entity/Component/ComponentFactory.cs +++ b/HKMP/Game/Client/Entity/Component/ComponentFactory.cs @@ -55,6 +55,14 @@ HostClientPair objects Client = spawnJarClient, Host = spawnJarHost }); + case EntityComponentType.SpriteRenderer: + var spriteRendererClient = objects.Client.GetComponent(); + var spriteRendererHost = objects.Host.GetComponent(); + + return new SpriteRendererComponent(netClient, entityId, objects, new HostClientPair { + Client = spriteRendererClient, + Host = spriteRendererHost + }); default: throw new ArgumentOutOfRangeException(nameof(type), type, $"Could not instantiate entity component for type: {type}"); } diff --git a/HKMP/Game/Client/Entity/Component/EntityComponent.cs b/HKMP/Game/Client/Entity/Component/EntityComponent.cs index 98a2f029..5843cc97 100644 --- a/HKMP/Game/Client/Entity/Component/EntityComponent.cs +++ b/HKMP/Game/Client/Entity/Component/EntityComponent.cs @@ -84,4 +84,5 @@ internal enum EntityComponentType : byte { EnemySpawner, ChildrenActivation, SpawnJar, + SpriteRenderer, } \ No newline at end of file diff --git a/HKMP/Game/Client/Entity/Component/SpriteRendererComponent.cs b/HKMP/Game/Client/Entity/Component/SpriteRendererComponent.cs new file mode 100644 index 00000000..bce105b0 --- /dev/null +++ b/HKMP/Game/Client/Entity/Component/SpriteRendererComponent.cs @@ -0,0 +1,73 @@ +using Hkmp.Networking.Client; +using Hkmp.Networking.Packet.Data; +using Hkmp.Util; +using UnityEngine; + +namespace Hkmp.Game.Client.Entity.Component; + +/// +/// This component manages the sprite renderer of the entity. +internal class SpriteRendererComponent : EntityComponent { + /// + /// The host-client pair of unity components of the entity. + /// + private readonly HostClientPair _spriteRenderer; + + /// + /// The last value of 'enabled' for the sprite renderer. + /// + private bool _lastEnabled; + + public SpriteRendererComponent( + NetClient netClient, + byte entityId, + HostClientPair gameObject, + HostClientPair spriteRenderer + ) : base(netClient, entityId, gameObject) { + _spriteRenderer = spriteRenderer; + _lastEnabled = spriteRenderer.Host.enabled; + + MonoBehaviourUtil.Instance.OnUpdateEvent += OnUpdate; + } + + /// + /// Callback method to check for sprite renderer updates. + /// + private void OnUpdate() { + if (IsControlled) { + return; + } + + if (GameObject.Host == null) { + return; + } + + var newEnabled = _spriteRenderer.Host.enabled; + if (newEnabled != _lastEnabled) { + _lastEnabled = newEnabled; + + var data = new EntityNetworkData { + Type = EntityComponentType.SpriteRenderer + }; + data.Packet.Write(newEnabled); + + SendData(data); + } + } + + /// + public override void InitializeHost() { + } + + /// + public override void Update(EntityNetworkData data) { + var enabled = data.Packet.ReadBool(); + _spriteRenderer.Host.enabled = enabled; + _spriteRenderer.Client.enabled = enabled; + } + + /// + public override void Destroy() { + MonoBehaviourUtil.Instance.OnUpdateEvent -= OnUpdate; + } +} \ No newline at end of file diff --git a/HKMP/Game/Client/Entity/Entity.cs b/HKMP/Game/Client/Entity/Entity.cs index ca6b3bbd..d6e4078e 100644 --- a/HKMP/Game/Client/Entity/Entity.cs +++ b/HKMP/Game/Client/Entity/Entity.cs @@ -140,6 +140,8 @@ params EntityComponentType[] types UnityEngine.SceneManagement.SceneManager.MoveGameObjectToScene(Object.Client, Object.Host.scene); } + DestroyManagedChildren(Object.Client); + _hasParent = false; } else { Object = new HostClientPair { @@ -248,6 +250,22 @@ params EntityComponentType[] types // } } + /// + /// Destroy the children of the given game object that are registered entities in the system themselves. + /// Recursively go through the non-registered children as well. + /// + /// The root game object to start searching for children. + private void DestroyManagedChildren(GameObject root) { + foreach (var child in root.GetChildren()) { + if (EntityRegistry.TryGetEntry(child, out var entry)) { + Logger.Debug($"Found managed child: {child.name}, {entry.Type}, destroying it"); + UnityEngine.Object.Destroy(child); + } else { + DestroyManagedChildren(child); + } + } + } + /// /// Processes the given FSM for the host entity by hooking supported FSM actions. /// diff --git a/HKMP/Game/Client/Entity/EntityManager.cs b/HKMP/Game/Client/Entity/EntityManager.cs index 0e8a6c44..2931b6d7 100644 --- a/HKMP/Game/Client/Entity/EntityManager.cs +++ b/HKMP/Game/Client/Entity/EntityManager.cs @@ -101,33 +101,54 @@ public void SpawnEntity(byte id, EntityType spawningType, EntityType spawnedType return; } - // Find an entity that has the same type as the spawning type. Doesn't matter if it is the correct instance, - // because the FSMs and components will be identical - var spawningEntity = _entities.Values.FirstOrDefault( - e => e.Type == spawningType - ); + GameObject spawnedObject; + + if (spawningType is EntityType.ColosseumCageSmall or EntityType.ColosseumCageLarge) { + // Special handling for when the spawning type is a colosseum cage, because those have the same type + // while they can spawn a variety of enemies, which is different from the case below + var possibleSpawningEntities = _entities.Values.Where( + e => e.Type == spawningType + ).ToArray(); + + if (possibleSpawningEntities.Length == 0) { + Logger.Warn("Could not find any entities with same type for spawning"); + return; + } - if (spawningEntity == null) { - Logger.Warn("Could not find entity with same type for spawning"); - return; - } + spawnedObject = EntitySpawner.SpawnEntityGameObjectFromColosseum( + spawningType, + spawnedType, + possibleSpawningEntities + ); + } else { + // Find an entity that has the same type as the spawning type. Doesn't matter if it is the correct instance, + // because the FSMs and components will be identical + var spawningEntity = _entities.Values.FirstOrDefault( + e => e.Type == spawningType + ); - var gameObject = EntitySpawner.SpawnEntityGameObject( - spawningType, - spawnedType, - spawningEntity.Object.Client, - spawningEntity.GetClientFsms() - ); + if (spawningEntity == null) { + Logger.Warn("Could not find entity with same type for spawning"); + return; + } + + spawnedObject = EntitySpawner.SpawnEntityGameObject( + spawningType, + spawnedType, + spawningEntity.Object.Client, + spawningEntity.GetClientFsms() + ); + } var processor = new EntityProcessor { - GameObject = gameObject, + GameObject = spawnedObject, IsSceneHost = _isSceneHost, LateLoad = true, SpawnedId = id }.Process(); if (!processor.Success) { - Logger.Warn($"Could not process game object of spawned entity: {gameObject.name}"); + Logger.Warn($"Could not process game object of spawned entity: {spawnedObject.name}"); } } @@ -331,7 +352,11 @@ private void FindEntitiesInScene(Scene scene, bool lateLoad) { return new[] { enemyDeathEffects.gameObject, corpse }; }) - .Concat(Object.FindObjectsOfType() + .Concat(Object.FindObjectsOfType( + scene.name.Equals("Room_Colosseum_Bronze") || + scene.name.Equals("Room_Colosseum_Silver") || + scene.name.Equals("Room_Colosseum_Gold") + ) .Where(fsm => fsm.gameObject.scene == scene) .Select(fsm => fsm.gameObject) ) @@ -343,6 +368,43 @@ private void FindEntitiesInScene(Scene scene, bool lateLoad) { .Distinct(); foreach (var obj in objectsToCheck) { + // Logger.Debug($"Checking obj: {obj.name}, active: {obj.activeSelf}, {obj.activeInHierarchy}"); + // if (obj.name == "Colosseum Manager") { + // var fsms = obj.GetComponents(); + // foreach (var fsm in fsms) { + // Logger.Debug($" FSM: {fsm.Fsm.Name}"); + // } + // + // foreach (var child in obj.GetChildren()) { + // Logger.Debug($" Child: {child.name}, active: {child.activeSelf}, {child.activeInHierarchy}"); + // + // fsms = child.GetComponents(); + // foreach (var fsm in fsms) { + // Logger.Debug($" FSM: {fsm.Fsm.Name}"); + // } + // + // foreach (var child2 in child.GetChildren()) { + // Logger.Debug($" Child: {child2.name}, active: {child2.activeSelf}, {child2.activeInHierarchy}"); + // + // fsms = child2.GetComponents(); + // foreach (var fsm in fsms) { + // Logger.Debug($" FSM: {fsm.Fsm.Name}"); + // } + // + // foreach (var child3 in child2.GetChildren()) { + // Logger.Debug($" Child: {child3.name}, active: {child3.activeSelf}, {child3.activeInHierarchy}"); + // + // fsms = child3.GetComponents(); + // foreach (var fsm in fsms) { + // Logger.Debug($" FSM: {fsm.Fsm.Name}"); + // } + // + // + // } + // } + // } + // } + new EntityProcessor { GameObject = obj, IsSceneHost = _isSceneHost, diff --git a/HKMP/Game/Client/Entity/EntitySpawner.cs b/HKMP/Game/Client/Entity/EntitySpawner.cs index dc6b6eca..ee990e9a 100644 --- a/HKMP/Game/Client/Entity/EntitySpawner.cs +++ b/HKMP/Game/Client/Entity/EntitySpawner.cs @@ -100,6 +100,59 @@ List clientFsms return null; } + /// + /// Spawn the game object for an entity with the given type that is spawned from the other given type. + /// The spawning type must be a Colosseum cage type. + /// + /// The type of the entity that spawns the new entity. + /// The type of the spawned entity. + /// An enumerable of entities that are Colosseum cages and have the + /// potential of spawning the correct type. + /// The game object for the spawned entity. + /// Thrown if no such type can be spawned given the + /// constraints. + public static GameObject SpawnEntityGameObjectFromColosseum( + EntityType spawningType, + EntityType spawnedType, + IEnumerable possibleSpawningEntities + ) { + Logger.Debug($"Trying to spawn entity from colosseum: {spawningType}, {spawnedType}"); + + foreach (var spawningEntity in possibleSpawningEntities) { + Logger.Debug($" Candidate: {spawningEntity.Type}, {spawningEntity.Id}, {spawningEntity.Object.Client.name}"); + + var fsm = spawningEntity.GetClientFsms()[0]; + var action = fsm.GetFirstAction("Init"); + + var prefab = action.gameObject.Value; + if (!EntityRegistry.TryGetEntry(prefab, out var entry)) { + Logger.Debug($" Could not find registry entry for prefab: {prefab.name}"); + continue; + } + + if (entry.Type != spawnedType) { + Logger.Debug($" Type of prefab does not match spawned type: {entry.Type}"); + continue; + } + + Logger.Debug(" Found matching type, spawning object"); + + var spawnedObject = SpawnFromCreateObject(action); + + var healthManager = spawnedObject.GetComponent(); + if (healthManager != null) { + healthManager.SetGeoSmall(0); + healthManager.SetGeoMedium(0); + healthManager.SetGeoLarge(0); + } + + return spawnedObject; + } + + Logger.Debug(" Exhausted all possible spawning entities, no corresponding type found"); + throw new InvalidOperationException(); + } + private static GameObject SpawnFromCreateObject(CreateObject action) { var gameObject = action.gameObject.Value; diff --git a/HKMP/Game/Client/Entity/EntityType.cs b/HKMP/Game/Client/Entity/EntityType.cs index b3aa6a86..b6c14d09 100644 --- a/HKMP/Game/Client/Entity/EntityType.cs +++ b/HKMP/Game/Client/Entity/EntityType.cs @@ -146,10 +146,12 @@ internal enum EntityType { MantisPetra, MantisTraitor, TraitorLord, + ColosseumManager, ColosseumCageSmall, ColosseumCageLarge, ColosseumPlatform, ColosseumSpike, + ColosseumWall, SharpBaldur, ArmouredSquit, BattleObble, diff --git a/HKMP/Resource/entity-registry.json b/HKMP/Resource/entity-registry.json index 6781cba5..9c695d72 100644 --- a/HKMP/Resource/entity-registry.json +++ b/HKMP/Resource/entity-registry.json @@ -856,26 +856,57 @@ "type": "TraitorLord", "fsm_name": "Mantis" }, + { + "base_object_name": "Colosseum Manager", + "type": "ColosseumManager", + "fsm_name": "Battle Control" + }, { "base_object_name": "Colosseum Cage Small", "type": "ColosseumCageSmall", - "fsm_name": "Spawn" + "fsm_name": "Spawn", + "components": [ + "SpriteRenderer" + ] }, { "base_object_name": "Colosseum Cage Large", "type": "ColosseumCageLarge", - "fsm_name": "Spawn" + "fsm_name": "Spawn", + "components": [ + "SpriteRenderer" + ] }, { "base_object_name": "Colosseum Platform", "type": "ColosseumPlatform", - "fsm_name": "Control" + "fsm_name": "Control", + "components": [ + "SpriteRenderer" + ], + "children": [ + { + "base_object_name": "Platform", + "type": "ColosseumPlatform" + } + ] }, { "base_object_name": "Colosseum Spike", "type": "ColosseumSpike", "fsm_name": "Control" }, + { + "base_object_name": "Colosseum Wall", + "type": "ColosseumWall", + "fsm_name": "Control", + "children": [ + { + "base_object_name": "Wall", + "type": "ColosseumWall" + } + ] + }, { "base_object_name": "Colosseum_Armored_Roller", "type": "SharpBaldur", @@ -883,7 +914,7 @@ }, { "base_object_name": "Colosseum_Armored_Mosquito", - "type": "ArmoredSquit", + "type": "ArmouredSquit", "fsm_name": "Mozzie2" }, { From 11f17d6fe2fa6383837855500222a6d9d5a4b285 Mon Sep 17 00:00:00 2001 From: Extremelyd1 Date: Tue, 4 Jul 2023 20:53:43 +0200 Subject: [PATCH 057/216] Fix Hopper, Spiny Husk and Mantis Petra --- HKMP/Game/Client/Entity/EntityManager.cs | 12 +++------ HKMP/Game/Client/Entity/EntitySpawner.cs | 22 ++++++++++++++++ HKMP/Game/Client/Entity/EntityType.cs | 2 ++ HKMP/Resource/entity-registry.json | 33 +++++++++++++++++++++--- 4 files changed, 58 insertions(+), 11 deletions(-) diff --git a/HKMP/Game/Client/Entity/EntityManager.cs b/HKMP/Game/Client/Entity/EntityManager.cs index 2931b6d7..e23d3dec 100644 --- a/HKMP/Game/Client/Entity/EntityManager.cs +++ b/HKMP/Game/Client/Entity/EntityManager.cs @@ -352,18 +352,14 @@ private void FindEntitiesInScene(Scene scene, bool lateLoad) { return new[] { enemyDeathEffects.gameObject, corpse }; }) - .Concat(Object.FindObjectsOfType( - scene.name.Equals("Room_Colosseum_Bronze") || - scene.name.Equals("Room_Colosseum_Silver") || - scene.name.Equals("Room_Colosseum_Gold") - ) + .Concat(Object.FindObjectsOfType(true) .Where(fsm => fsm.gameObject.scene == scene) .Select(fsm => fsm.gameObject) ) .SelectMany(obj => obj == null ? Array.Empty() : obj.GetChildren().Prepend(obj)) - .Concat(Object.FindObjectsOfType().Select(climber => climber.gameObject)) - .Concat(Object.FindObjectsOfType().Select(walker => walker.gameObject)) - .Concat(Object.FindObjectsOfType().Select(centipede => centipede.gameObject)) + .Concat(Object.FindObjectsOfType(true).Select(climber => climber.gameObject)) + .Concat(Object.FindObjectsOfType(true).Select(walker => walker.gameObject)) + .Concat(Object.FindObjectsOfType(true).Select(centipede => centipede.gameObject)) .Where(obj => obj.scene == scene) .Distinct(); diff --git a/HKMP/Game/Client/Entity/EntitySpawner.cs b/HKMP/Game/Client/Entity/EntitySpawner.cs index ee990e9a..5a88df34 100644 --- a/HKMP/Game/Client/Entity/EntitySpawner.cs +++ b/HKMP/Game/Client/Entity/EntitySpawner.cs @@ -97,6 +97,14 @@ List clientFsms return SpawnNoskBlobObject(clientFsms[0]); } + if (spawningType == EntityType.BrokenVessel && spawnedType == EntityType.InfectedBalloon) { + return SpawnBrokenVesselBalloonObject(clientFsms[7]); + } + + if (spawningType == EntityType.MantisPetra && spawnedType == EntityType.MantisPetraScythe) { + return SpawnMantisPetraScytheObject(clientFsms[0]); + } + return null; } @@ -393,4 +401,18 @@ private static GameObject SpawnNoskBlobObject(PlayMakerFSM fsm) { return SpawnFromFlingGlobalPoolTime(action, gameObject); } + + private static GameObject SpawnBrokenVesselBalloonObject(PlayMakerFSM fsm) { + var action = fsm.GetFirstAction("Spawn"); + var gameObject = action.gameObject.Value; + + return SpawnFromGlobalPool(action, gameObject); + } + + private static GameObject SpawnMantisPetraScytheObject(PlayMakerFSM fsm) { + var action = fsm.GetFirstAction("Shoot"); + var gameObject = action.gameObject.Value; + + return SpawnFromGlobalPool(action, gameObject); + } } diff --git a/HKMP/Game/Client/Entity/EntityType.cs b/HKMP/Game/Client/Entity/EntityType.cs index b6c14d09..fbe43c14 100644 --- a/HKMP/Game/Client/Entity/EntityType.cs +++ b/HKMP/Game/Client/Entity/EntityType.cs @@ -144,6 +144,8 @@ internal enum EntityType { SpinyHusk, Loodle, MantisPetra, + MantisPetraScythe, + MantisPetraSummon, MantisTraitor, TraitorLord, ColosseumManager, diff --git a/HKMP/Resource/entity-registry.json b/HKMP/Resource/entity-registry.json index 9c695d72..0e047581 100644 --- a/HKMP/Resource/entity-registry.json +++ b/HKMP/Resource/entity-registry.json @@ -800,6 +800,11 @@ "type": "GreatHopper", "fsm_name": "Hopper" }, + { + "base_object_name": "Hopper", + "type": "Hopper", + "fsm_name": "Hopper" + }, { "base_object_name": "Grub Mimic", "type": "GrubMimic", @@ -833,18 +838,40 @@ "type": "HuskHive", "fsm_name": "Hive Zombie" }, + { + "base_object_name": "Garden Zombie", + "type": "SpinyHusk", + "fsm_name": "Attack" + }, { "base_object_name": "Grass Hopper", "type": "Loodle", "fsm_name": "Crazy Hopper", - "components": [ - "Rotation" + "children": [ + { + "base_object_name": "Sprite", + "type": "Loodle", + "fsm_name": "Damage Flash" + } ] }, { "base_object_name": "Mantis Heavy Flyer", "type": "MantisPetra", - "fsm_name": "Heavy Flyer" + "fsm_name": "Heavy Flyer", + "components": [ + "ZPosition" + ] + }, + { + "base_object_name": "Shot Mantis", + "type": "MantisPetraScythe", + "fsm_name": "Shot Mantis" + }, + { + "base_object_name": "Dragonfly Summon", + "type": "MantisPetraSummon", + "fsm_name": "summon" }, { "base_object_name": "Mantis Heavy", From 78e7a3a8919c7a086409ec6c293a19c5165ed4f4 Mon Sep 17 00:00:00 2001 From: Extremelyd1 Date: Wed, 5 Jul 2023 21:41:46 +0200 Subject: [PATCH 058/216] Increase networking limits for entities, fix Colosseum trials --- HKMP/Game/Client/ClientManager.cs | 2 +- .../Client/Entity/Action/EntityFsmActions.cs | 47 +++++-- .../Component/ChildrenActivationComponent.cs | 2 +- .../Entity/Component/ClimberComponent.cs | 2 +- .../Entity/Component/ColliderComponent.cs | 2 +- .../Entity/Component/ComponentFactory.cs | 2 +- .../Entity/Component/DamageHeroComponent.cs | 2 +- .../Entity/Component/EnemySpawnerComponent.cs | 2 +- .../Entity/Component/EntityComponent.cs | 4 +- .../Entity/Component/GravityScaleComponent.cs | 2 +- .../Component/HealthManagerComponent.cs | 2 +- .../Entity/Component/MeshRendererComponent.cs | 2 +- .../Entity/Component/RotationComponent.cs | 2 +- .../Entity/Component/SpawnJarComponent.cs | 2 +- .../Component/SpriteRendererComponent.cs | 2 +- .../Entity/Component/VelocityComponent.cs | 2 +- .../Entity/Component/ZPositionComponent.cs | 2 +- HKMP/Game/Client/Entity/Entity.cs | 6 +- HKMP/Game/Client/Entity/EntityManager.cs | 132 +++++++----------- HKMP/Game/Client/Entity/EntityProcessor.cs | 16 +-- HKMP/Game/Client/Entity/EntitySpawner.cs | 53 ------- HKMP/Game/Server/ServerEntityKey.cs | 4 +- HKMP/Game/Server/ServerManager.cs | 2 + HKMP/Networking/Client/ClientUpdateManager.cs | 16 +-- HKMP/Networking/Packet/Data/EntitySpawn.cs | 4 +- HKMP/Networking/Packet/Data/EntityUpdate.cs | 4 +- .../Packet/Data/PacketDataCollection.cs | 6 +- .../Packet/Data/PlayerEnterScene.cs | 16 +-- HKMP/Networking/Server/ServerUpdateManager.cs | 16 +-- HKMP/Resource/entity-registry.json | 21 ++- 30 files changed, 166 insertions(+), 211 deletions(-) diff --git a/HKMP/Game/Client/ClientManager.cs b/HKMP/Game/Client/ClientManager.cs index 6db64542..de00188b 100644 --- a/HKMP/Game/Client/ClientManager.cs +++ b/HKMP/Game/Client/ClientManager.cs @@ -613,7 +613,7 @@ private void OnPlayerAlreadyInScene(ClientPlayerAlreadyInScene alreadyInScene) { } foreach (var entityUpdate in alreadyInScene.EntityUpdateList) { - Logger.Info($"Updating already in scene entity with ID: {entityUpdate.Id}"); + Logger.Info($"Updating already in scene entity with ID: {entityUpdate.Id}, {entityUpdate.UpdateTypes.Contains(EntityUpdateType.Active)}, {entityUpdate.IsActive}"); _entityManager.HandleEntityUpdate(entityUpdate, true); } diff --git a/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs b/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs index c3e206ee..30901db0 100644 --- a/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs +++ b/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs @@ -638,20 +638,43 @@ private static bool GetNetworkDataFromAction(EntityNetworkData data, CreateObjec private static void ApplyNetworkDataFromAction(EntityNetworkData data, CreateObject action) { Logger.Debug("ApplyNetworkDataFromAction CreateObject"); + Vector3 position; + Vector3 euler; + if (data == null) { - return; + position = Vector3.zero; + euler = Vector3.zero; + if (action.spawnPoint.Value != null) { + position = action.spawnPoint.Value.transform.position; + + if (!action.position.IsNone) { + position += action.position.Value; + } + + euler = !action.rotation.IsNone ? + action.rotation.Value : + action.spawnPoint.Value.transform.eulerAngles; + } else { + if (!action.position.IsNone) { + position = action.position.Value; + } + + if (!action.rotation.IsNone) { + euler = action.rotation.Value; + } + } + } else { + position = new Vector3( + data.Packet.ReadFloat(), + data.Packet.ReadFloat(), + data.Packet.ReadFloat() + ); + euler = new Vector3( + data.Packet.ReadFloat(), + data.Packet.ReadFloat(), + data.Packet.ReadFloat() + ); } - - var position = new Vector3( - data.Packet.ReadFloat(), - data.Packet.ReadFloat(), - data.Packet.ReadFloat() - ); - var euler = new Vector3( - data.Packet.ReadFloat(), - data.Packet.ReadFloat(), - data.Packet.ReadFloat() - ); var original = action.gameObject.Value; if (original == null) { diff --git a/HKMP/Game/Client/Entity/Component/ChildrenActivationComponent.cs b/HKMP/Game/Client/Entity/Component/ChildrenActivationComponent.cs index d1da1a30..1020786e 100644 --- a/HKMP/Game/Client/Entity/Component/ChildrenActivationComponent.cs +++ b/HKMP/Game/Client/Entity/Component/ChildrenActivationComponent.cs @@ -16,7 +16,7 @@ internal class ChildrenActivationComponent : EntityComponent { public ChildrenActivationComponent( NetClient netClient, - byte entityId, + ushort entityId, HostClientPair gameObject ) : base(netClient, entityId, gameObject) { _hostChildren = gameObject.Host.GetChildren(); diff --git a/HKMP/Game/Client/Entity/Component/ClimberComponent.cs b/HKMP/Game/Client/Entity/Component/ClimberComponent.cs index 7d3a78fc..08840c8d 100644 --- a/HKMP/Game/Client/Entity/Component/ClimberComponent.cs +++ b/HKMP/Game/Client/Entity/Component/ClimberComponent.cs @@ -14,7 +14,7 @@ internal class ClimberComponent : EntityComponent { public ClimberComponent( NetClient netClient, - byte entityId, + ushort entityId, HostClientPair gameObject, Climber climber ) : base(netClient, entityId, gameObject) { diff --git a/HKMP/Game/Client/Entity/Component/ColliderComponent.cs b/HKMP/Game/Client/Entity/Component/ColliderComponent.cs index 1aff0da5..26b85458 100644 --- a/HKMP/Game/Client/Entity/Component/ColliderComponent.cs +++ b/HKMP/Game/Client/Entity/Component/ColliderComponent.cs @@ -21,7 +21,7 @@ internal class ColliderComponent : EntityComponent { public ColliderComponent( NetClient netClient, - byte entityId, + ushort entityId, HostClientPair gameObject, HostClientPair collider ) : base(netClient, entityId, gameObject) { diff --git a/HKMP/Game/Client/Entity/Component/ComponentFactory.cs b/HKMP/Game/Client/Entity/Component/ComponentFactory.cs index 36753016..497a8506 100644 --- a/HKMP/Game/Client/Entity/Component/ComponentFactory.cs +++ b/HKMP/Game/Client/Entity/Component/ComponentFactory.cs @@ -21,7 +21,7 @@ internal static class ComponentFactory { public static EntityComponent InstantiateByType( EntityComponentType type, NetClient netClient, - byte entityId, + ushort entityId, HostClientPair objects ) { Rigidbody2D rigidBody; diff --git a/HKMP/Game/Client/Entity/Component/DamageHeroComponent.cs b/HKMP/Game/Client/Entity/Component/DamageHeroComponent.cs index fcf99319..2ff7bcf7 100644 --- a/HKMP/Game/Client/Entity/Component/DamageHeroComponent.cs +++ b/HKMP/Game/Client/Entity/Component/DamageHeroComponent.cs @@ -20,7 +20,7 @@ internal class DamageHeroComponent : EntityComponent { public DamageHeroComponent( NetClient netClient, - byte entityId, + ushort entityId, HostClientPair gameObject, HostClientPair damageHero ) : base(netClient, entityId, gameObject) { diff --git a/HKMP/Game/Client/Entity/Component/EnemySpawnerComponent.cs b/HKMP/Game/Client/Entity/Component/EnemySpawnerComponent.cs index 8a5a7c3c..ff7f5aec 100644 --- a/HKMP/Game/Client/Entity/Component/EnemySpawnerComponent.cs +++ b/HKMP/Game/Client/Entity/Component/EnemySpawnerComponent.cs @@ -16,7 +16,7 @@ internal class EnemySpawnerComponent : EntityComponent { public EnemySpawnerComponent( NetClient netClient, - byte entityId, + ushort entityId, HostClientPair gameObject, HostClientPair spawner ) : base(netClient, entityId, gameObject) { diff --git a/HKMP/Game/Client/Entity/Component/EntityComponent.cs b/HKMP/Game/Client/Entity/Component/EntityComponent.cs index 5843cc97..8b72e63a 100644 --- a/HKMP/Game/Client/Entity/Component/EntityComponent.cs +++ b/HKMP/Game/Client/Entity/Component/EntityComponent.cs @@ -17,7 +17,7 @@ internal abstract class EntityComponent { /// /// The ID of the entity. /// - private readonly byte _entityId; + private readonly ushort _entityId; /// /// Host-client pair of the game objects of the entity. @@ -31,7 +31,7 @@ internal abstract class EntityComponent { protected EntityComponent( NetClient netClient, - byte entityId, + ushort entityId, HostClientPair gameObject ) { _netClient = netClient; diff --git a/HKMP/Game/Client/Entity/Component/GravityScaleComponent.cs b/HKMP/Game/Client/Entity/Component/GravityScaleComponent.cs index 53527c42..4d0b8627 100644 --- a/HKMP/Game/Client/Entity/Component/GravityScaleComponent.cs +++ b/HKMP/Game/Client/Entity/Component/GravityScaleComponent.cs @@ -26,7 +26,7 @@ internal class GravityScaleComponent : EntityComponent { public GravityScaleComponent( NetClient netClient, - byte entityId, + ushort entityId, HostClientPair gameObject, Rigidbody2D rigidbody ) : base(netClient, entityId, gameObject) { diff --git a/HKMP/Game/Client/Entity/Component/HealthManagerComponent.cs b/HKMP/Game/Client/Entity/Component/HealthManagerComponent.cs index bb88fe47..31eab343 100644 --- a/HKMP/Game/Client/Entity/Component/HealthManagerComponent.cs +++ b/HKMP/Game/Client/Entity/Component/HealthManagerComponent.cs @@ -34,7 +34,7 @@ internal class HealthManagerComponent : EntityComponent { public HealthManagerComponent( NetClient netClient, - byte entityId, + ushort entityId, HostClientPair gameObject, HostClientPair healthManager ) : base(netClient, entityId, gameObject) { diff --git a/HKMP/Game/Client/Entity/Component/MeshRendererComponent.cs b/HKMP/Game/Client/Entity/Component/MeshRendererComponent.cs index 63a04bfd..db80132f 100644 --- a/HKMP/Game/Client/Entity/Component/MeshRendererComponent.cs +++ b/HKMP/Game/Client/Entity/Component/MeshRendererComponent.cs @@ -23,7 +23,7 @@ internal class MeshRendererComponent : EntityComponent { public MeshRendererComponent( NetClient netClient, - byte entityId, + ushort entityId, HostClientPair gameObject, HostClientPair meshRenderer ) : base(netClient, entityId, gameObject) { diff --git a/HKMP/Game/Client/Entity/Component/RotationComponent.cs b/HKMP/Game/Client/Entity/Component/RotationComponent.cs index a849f476..b9114809 100644 --- a/HKMP/Game/Client/Entity/Component/RotationComponent.cs +++ b/HKMP/Game/Client/Entity/Component/RotationComponent.cs @@ -15,7 +15,7 @@ internal class RotationComponent : EntityComponent { public RotationComponent( NetClient netClient, - byte entityId, + ushort entityId, HostClientPair gameObject ) : base(netClient, entityId, gameObject) { MonoBehaviourUtil.Instance.OnUpdateEvent += OnUpdateRotation; diff --git a/HKMP/Game/Client/Entity/Component/SpawnJarComponent.cs b/HKMP/Game/Client/Entity/Component/SpawnJarComponent.cs index 8e9cd045..9d60d7f0 100644 --- a/HKMP/Game/Client/Entity/Component/SpawnJarComponent.cs +++ b/HKMP/Game/Client/Entity/Component/SpawnJarComponent.cs @@ -28,7 +28,7 @@ internal class SpawnJarComponent : EntityComponent { public SpawnJarComponent( NetClient netClient, - byte entityId, + ushort entityId, HostClientPair gameObject, HostClientPair spawnJar ) : base(netClient, entityId, gameObject) { diff --git a/HKMP/Game/Client/Entity/Component/SpriteRendererComponent.cs b/HKMP/Game/Client/Entity/Component/SpriteRendererComponent.cs index bce105b0..17d1c21b 100644 --- a/HKMP/Game/Client/Entity/Component/SpriteRendererComponent.cs +++ b/HKMP/Game/Client/Entity/Component/SpriteRendererComponent.cs @@ -20,7 +20,7 @@ internal class SpriteRendererComponent : EntityComponent { public SpriteRendererComponent( NetClient netClient, - byte entityId, + ushort entityId, HostClientPair gameObject, HostClientPair spriteRenderer ) : base(netClient, entityId, gameObject) { diff --git a/HKMP/Game/Client/Entity/Component/VelocityComponent.cs b/HKMP/Game/Client/Entity/Component/VelocityComponent.cs index e77b9367..00039891 100644 --- a/HKMP/Game/Client/Entity/Component/VelocityComponent.cs +++ b/HKMP/Game/Client/Entity/Component/VelocityComponent.cs @@ -26,7 +26,7 @@ internal class VelocityComponent : EntityComponent { public VelocityComponent( NetClient netClient, - byte entityId, + ushort entityId, HostClientPair gameObject, Rigidbody2D rigidbody ) : base(netClient, entityId, gameObject) { diff --git a/HKMP/Game/Client/Entity/Component/ZPositionComponent.cs b/HKMP/Game/Client/Entity/Component/ZPositionComponent.cs index c9012582..deeff4f5 100644 --- a/HKMP/Game/Client/Entity/Component/ZPositionComponent.cs +++ b/HKMP/Game/Client/Entity/Component/ZPositionComponent.cs @@ -16,7 +16,7 @@ internal class ZPositionComponent : EntityComponent { public ZPositionComponent( NetClient netClient, - byte entityId, + ushort entityId, HostClientPair gameObject ) : base(netClient, entityId, gameObject) { _lastZ = gameObject.Host.transform.position.z; diff --git a/HKMP/Game/Client/Entity/Entity.cs b/HKMP/Game/Client/Entity/Entity.cs index d6e4078e..62f289b9 100644 --- a/HKMP/Game/Client/Entity/Entity.cs +++ b/HKMP/Game/Client/Entity/Entity.cs @@ -34,7 +34,7 @@ internal class Entity { /// /// The ID of the entity. /// - public byte Id { get; } + public ushort Id { get; } /// /// The type of the entity. @@ -111,7 +111,7 @@ internal class Entity { public Entity( NetClient netClient, - byte id, + ushort id, EntityType type, GameObject hostObject, GameObject clientObject = null, @@ -1095,7 +1095,7 @@ tk2dSpriteAnimationClip.WrapMode wrapMode /// /// The new value for active. public void UpdateIsActive(bool active) { - // Logger.Info($"Entity '{Object.Client.name}' received active: {active}"); + Logger.Info($"Entity '{Object.Client.name}' received active: {active}"); if (Object.Client != null) { Object.Client.SetActive(active); } else { diff --git a/HKMP/Game/Client/Entity/EntityManager.cs b/HKMP/Game/Client/Entity/EntityManager.cs index e23d3dec..8b0c2755 100644 --- a/HKMP/Game/Client/Entity/EntityManager.cs +++ b/HKMP/Game/Client/Entity/EntityManager.cs @@ -5,6 +5,7 @@ using Hkmp.Networking.Client; using Hkmp.Networking.Packet.Data; using Hkmp.Util; +using HutongGames.PlayMaker.Actions; using Modding; using UnityEngine; using UnityEngine.SceneManagement; @@ -25,7 +26,7 @@ internal class EntityManager { /// /// Dictionary mapping entity IDs to their respective entity instances. /// - private readonly Dictionary _entities; + private readonly Dictionary _entities; /// /// Whether the client user is the scene host. @@ -41,7 +42,7 @@ internal class EntityManager { public EntityManager(NetClient netClient) { _netClient = netClient; - _entities = new Dictionary(); + _entities = new Dictionary(); _receivedUpdates = new Queue(); EntityProcessor.Initialize(_entities, netClient); @@ -92,7 +93,7 @@ public void BecomeSceneHost() { /// The ID of the entity. /// The type of the entity that spawned the new entity. /// The type of the spawned entity. - public void SpawnEntity(byte id, EntityType spawningType, EntityType spawnedType) { + public void SpawnEntity(ushort id, EntityType spawningType, EntityType spawnedType) { Logger.Info($"Trying to spawn entity with ID {id} with types: {spawningType}, {spawnedType}"); // If an entity with the new ID already exists, we return @@ -101,45 +102,24 @@ public void SpawnEntity(byte id, EntityType spawningType, EntityType spawnedType return; } - GameObject spawnedObject; - - if (spawningType is EntityType.ColosseumCageSmall or EntityType.ColosseumCageLarge) { - // Special handling for when the spawning type is a colosseum cage, because those have the same type - // while they can spawn a variety of enemies, which is different from the case below - var possibleSpawningEntities = _entities.Values.Where( - e => e.Type == spawningType - ).ToArray(); - - if (possibleSpawningEntities.Length == 0) { - Logger.Warn("Could not find any entities with same type for spawning"); - return; - } - - spawnedObject = EntitySpawner.SpawnEntityGameObjectFromColosseum( - spawningType, - spawnedType, - possibleSpawningEntities - ); - } else { - // Find an entity that has the same type as the spawning type. Doesn't matter if it is the correct instance, - // because the FSMs and components will be identical - var spawningEntity = _entities.Values.FirstOrDefault( - e => e.Type == spawningType - ); - - if (spawningEntity == null) { - Logger.Warn("Could not find entity with same type for spawning"); - return; - } + // Find an entity that has the same type as the spawning type. Doesn't matter if it is the correct instance, + // because the FSMs and components will be identical + var spawningEntity = _entities.Values.FirstOrDefault( + e => e.Type == spawningType + ); - spawnedObject = EntitySpawner.SpawnEntityGameObject( - spawningType, - spawnedType, - spawningEntity.Object.Client, - spawningEntity.GetClientFsms() - ); + if (spawningEntity == null) { + Logger.Warn("Could not find entity with same type for spawning"); + return; } + var spawnedObject = EntitySpawner.SpawnEntityGameObject( + spawningType, + spawnedType, + spawningEntity.Object.Client, + spawningEntity.GetClientFsms() + ); + var processor = new EntityProcessor { GameObject = spawnedObject, IsSceneHost = _isSceneHost, @@ -331,7 +311,8 @@ private void FindEntitiesInScene(Scene scene, bool lateLoad) { // Filter out EnemyDeathEffects components not in the current scene // Project each death effect to their GameObject and the corpse of the pre-instantiated EnemyDeathEffects // component - // Concatenate all GameObjects for PlayMakerFSM components in the current scene + // Concatenate all GameObjects for PlayMakerFSM components in the current scene, and check whether it is the + // FSM for a Colosseum Cage, in which case we pre-instantiate the enemy inside and concatenate it as well // Project each GameObject into its children including itself // Concatenate all GameObjects for Climber components (Tiktiks) // Concatenate all GameObjects for Walker components (Amblooms) @@ -354,7 +335,39 @@ private void FindEntitiesInScene(Scene scene, bool lateLoad) { }) .Concat(Object.FindObjectsOfType(true) .Where(fsm => fsm.gameObject.scene == scene) - .Select(fsm => fsm.gameObject) + .SelectMany(fsm => { + if (!fsm.name.StartsWith("Colosseum Cage Small") && + !fsm.name.StartsWith("Colosseum Cage Large") || + !fsm.Fsm.Name.Equals("Spawn") + ) { + return new[] { fsm.gameObject }; + } + + var createAction = fsm.GetFirstAction("Init"); + EntityFsmActions.ApplyNetworkDataFromAction(null, createAction); + + createAction.Enabled = false; + + var createdObject = createAction.storeObject.Value; + if (createdObject == null) { + return new[] { fsm.gameObject }; + } + + var fsmTransform = fsm.gameObject.transform; + + createdObject.transform.position = fsmTransform.position; + createdObject.transform.rotation = Quaternion.Euler(fsmTransform.eulerAngles); + createdObject.SetActive(false); + + var healthManager = createdObject.GetComponent(); + if (healthManager != null) { + healthManager.SetGeoSmall(0); + healthManager.SetGeoMedium(0); + healthManager.SetGeoLarge(0); + } + + return new[] { fsm.gameObject, createdObject }; + }) ) .SelectMany(obj => obj == null ? Array.Empty() : obj.GetChildren().Prepend(obj)) .Concat(Object.FindObjectsOfType(true).Select(climber => climber.gameObject)) @@ -364,43 +377,6 @@ private void FindEntitiesInScene(Scene scene, bool lateLoad) { .Distinct(); foreach (var obj in objectsToCheck) { - // Logger.Debug($"Checking obj: {obj.name}, active: {obj.activeSelf}, {obj.activeInHierarchy}"); - // if (obj.name == "Colosseum Manager") { - // var fsms = obj.GetComponents(); - // foreach (var fsm in fsms) { - // Logger.Debug($" FSM: {fsm.Fsm.Name}"); - // } - // - // foreach (var child in obj.GetChildren()) { - // Logger.Debug($" Child: {child.name}, active: {child.activeSelf}, {child.activeInHierarchy}"); - // - // fsms = child.GetComponents(); - // foreach (var fsm in fsms) { - // Logger.Debug($" FSM: {fsm.Fsm.Name}"); - // } - // - // foreach (var child2 in child.GetChildren()) { - // Logger.Debug($" Child: {child2.name}, active: {child2.activeSelf}, {child2.activeInHierarchy}"); - // - // fsms = child2.GetComponents(); - // foreach (var fsm in fsms) { - // Logger.Debug($" FSM: {fsm.Fsm.Name}"); - // } - // - // foreach (var child3 in child2.GetChildren()) { - // Logger.Debug($" Child: {child3.name}, active: {child3.activeSelf}, {child3.activeInHierarchy}"); - // - // fsms = child3.GetComponents(); - // foreach (var fsm in fsms) { - // Logger.Debug($" FSM: {fsm.Fsm.Name}"); - // } - // - // - // } - // } - // } - // } - new EntityProcessor { GameObject = obj, IsSceneHost = _isSceneHost, diff --git a/HKMP/Game/Client/Entity/EntityProcessor.cs b/HKMP/Game/Client/Entity/EntityProcessor.cs index e748fa57..6053f724 100644 --- a/HKMP/Game/Client/Entity/EntityProcessor.cs +++ b/HKMP/Game/Client/Entity/EntityProcessor.cs @@ -18,7 +18,7 @@ internal class EntityProcessor { /// /// Reference to the dictionary of entities from the entity manager. /// - private static Dictionary _entities; + private static Dictionary _entities; /// /// The net client used to pass onto constructed entities. /// @@ -27,7 +27,7 @@ internal class EntityProcessor { /// /// The last used entity ID. /// - private static byte _lastId; + private static ushort _lastId; /// /// The game object to process. @@ -45,7 +45,7 @@ internal class EntityProcessor { /// /// Whether the game object was spawned and should have the designated ID. /// - public byte? SpawnedId { get; init; } + public ushort? SpawnedId { get; init; } /// /// The list of entities that were created during the processing. @@ -61,7 +61,7 @@ internal class EntityProcessor { /// /// A reference to the dictionary of entities from the entity manager. /// The net client instance to pass onto constructed entities. - public static void Initialize(Dictionary entities, NetClient netClient) { + public static void Initialize(Dictionary entities, NetClient netClient) { _entities = entities; _netClient = netClient; @@ -107,7 +107,7 @@ private void Process( } } - byte id; + ushort id; // If a spawned ID is defined we check whether an entity with the given ID already exists // Otherwise we find a new ID that isn't used yet @@ -119,7 +119,7 @@ private void Process( return; } } else { - if (_entities.Count >= byte.MaxValue) { + if (_entities.Count >= ushort.MaxValue) { Logger.Error("Could not register entity because ID space is full!"); return; } @@ -139,7 +139,7 @@ private void Process( // Depending on whether a parent object was given we create the entity with this parent object Entity entity; if (parentClientObject == null) { - Logger.Info($"Registering entity ({foundEntry.Type}) '{gameObject.name}' with ID '{_lastId}'"); + Logger.Info($"Registering entity ({foundEntry.Type}) '{gameObject.name}' with ID '{id}'"); entity = new Entity( _netClient, @@ -149,7 +149,7 @@ private void Process( types: componentTypes ); } else { - Logger.Info($"Registering entity ({foundEntry.Type}) '{gameObject.name}' with ID '{_lastId}' with parent: {parentClientObject.name}"); + Logger.Info($"Registering entity ({foundEntry.Type}) '{gameObject.name}' with ID '{id}' with parent: {parentClientObject.name}"); // Find the correct child of the client object of the parent entity var clientObject = parentClientObject.GetChildren() diff --git a/HKMP/Game/Client/Entity/EntitySpawner.cs b/HKMP/Game/Client/Entity/EntitySpawner.cs index 5a88df34..abd66a62 100644 --- a/HKMP/Game/Client/Entity/EntitySpawner.cs +++ b/HKMP/Game/Client/Entity/EntitySpawner.cs @@ -108,59 +108,6 @@ List clientFsms return null; } - /// - /// Spawn the game object for an entity with the given type that is spawned from the other given type. - /// The spawning type must be a Colosseum cage type. - /// - /// The type of the entity that spawns the new entity. - /// The type of the spawned entity. - /// An enumerable of entities that are Colosseum cages and have the - /// potential of spawning the correct type. - /// The game object for the spawned entity. - /// Thrown if no such type can be spawned given the - /// constraints. - public static GameObject SpawnEntityGameObjectFromColosseum( - EntityType spawningType, - EntityType spawnedType, - IEnumerable possibleSpawningEntities - ) { - Logger.Debug($"Trying to spawn entity from colosseum: {spawningType}, {spawnedType}"); - - foreach (var spawningEntity in possibleSpawningEntities) { - Logger.Debug($" Candidate: {spawningEntity.Type}, {spawningEntity.Id}, {spawningEntity.Object.Client.name}"); - - var fsm = spawningEntity.GetClientFsms()[0]; - var action = fsm.GetFirstAction("Init"); - - var prefab = action.gameObject.Value; - if (!EntityRegistry.TryGetEntry(prefab, out var entry)) { - Logger.Debug($" Could not find registry entry for prefab: {prefab.name}"); - continue; - } - - if (entry.Type != spawnedType) { - Logger.Debug($" Type of prefab does not match spawned type: {entry.Type}"); - continue; - } - - Logger.Debug(" Found matching type, spawning object"); - - var spawnedObject = SpawnFromCreateObject(action); - - var healthManager = spawnedObject.GetComponent(); - if (healthManager != null) { - healthManager.SetGeoSmall(0); - healthManager.SetGeoMedium(0); - healthManager.SetGeoLarge(0); - } - - return spawnedObject; - } - - Logger.Debug(" Exhausted all possible spawning entities, no corresponding type found"); - throw new InvalidOperationException(); - } - private static GameObject SpawnFromCreateObject(CreateObject action) { var gameObject = action.gameObject.Value; diff --git a/HKMP/Game/Server/ServerEntityKey.cs b/HKMP/Game/Server/ServerEntityKey.cs index da85f1b3..51db9ad9 100644 --- a/HKMP/Game/Server/ServerEntityKey.cs +++ b/HKMP/Game/Server/ServerEntityKey.cs @@ -13,9 +13,9 @@ internal class ServerEntityKey : IEquatable { /// /// The ID of the entity. /// - public byte EntityId { get; } + public ushort EntityId { get; } - public ServerEntityKey(string scene, byte entityId) { + public ServerEntityKey(string scene, ushort entityId) { Scene = scene; EntityId = entityId; } diff --git a/HKMP/Game/Server/ServerManager.cs b/HKMP/Game/Server/ServerManager.cs index 282d30b2..50075936 100644 --- a/HKMP/Game/Server/ServerManager.cs +++ b/HKMP/Game/Server/ServerManager.cs @@ -641,6 +641,8 @@ private void OnEntityUpdate(ushort id, EntityUpdate entityUpdate) { Logger.Warn($"Received EntityUpdate data, but player with ID {id} is not in mapping"); return; } + + Logger.Debug($"Server received EntityUpdate: {entityUpdate.Id}, {entityUpdate.UpdateTypes.Contains(EntityUpdateType.Active)}, {entityUpdate.IsActive}"); // Create the key for the entity data var serverEntityKey = new ServerEntityKey( diff --git a/HKMP/Networking/Client/ClientUpdateManager.cs b/HKMP/Networking/Client/ClientUpdateManager.cs index f6f18d30..a9c92521 100644 --- a/HKMP/Networking/Client/ClientUpdateManager.cs +++ b/HKMP/Networking/Client/ClientUpdateManager.cs @@ -153,7 +153,7 @@ public void UpdatePlayerAnimation(AnimationClip clip, int frame = 0, bool[] effe /// The ID of the entity. /// The type of the entity that spawned the new entity. /// The type of the entity that was spawned. - public void SetEntitySpawn(byte id, EntityType spawningType, EntityType spawnedType) { + public void SetEntitySpawn(ushort id, EntityType spawningType, EntityType spawnedType) { lock (Lock) { PacketDataCollection entitySpawnCollection; @@ -178,7 +178,7 @@ public void SetEntitySpawn(byte id, EntityType spawningType, EntityType spawnedT /// /// The ID of the entity. /// The existing or new EntityUpdate instance. - private EntityUpdate FindOrCreateEntityUpdate(byte entityId) { + private EntityUpdate FindOrCreateEntityUpdate(ushort entityId) { EntityUpdate entityUpdate = null; PacketDataCollection entityUpdateCollection; @@ -220,7 +220,7 @@ out var packetData /// /// The ID of the entity. /// The new position of the entity. - public void UpdateEntityPosition(byte entityId, Vector2 position) { + public void UpdateEntityPosition(ushort entityId, Vector2 position) { lock (Lock) { var entityUpdate = FindOrCreateEntityUpdate(entityId); @@ -234,7 +234,7 @@ public void UpdateEntityPosition(byte entityId, Vector2 position) { /// /// The ID of the entity. /// The scale data of the entity. - public void UpdateEntityScale(byte entityId, EntityUpdate.ScaleData scale) { + public void UpdateEntityScale(ushort entityId, EntityUpdate.ScaleData scale) { lock (Lock) { var entityUpdate = FindOrCreateEntityUpdate(entityId); @@ -249,7 +249,7 @@ public void UpdateEntityScale(byte entityId, EntityUpdate.ScaleData scale) { /// The ID of the entity. /// The new animation ID of the entity. /// The wrap mode of the animation of the entity. - public void UpdateEntityAnimation(byte entityId, byte animationId, byte animationWrapMode) { + public void UpdateEntityAnimation(ushort entityId, byte animationId, byte animationWrapMode) { lock (Lock) { var entityUpdate = FindOrCreateEntityUpdate(entityId); @@ -264,7 +264,7 @@ public void UpdateEntityAnimation(byte entityId, byte animationId, byte animatio /// /// The ID of the entity. /// Whether the entity is active or not. - public void UpdateEntityIsActive(byte entityId, bool isActive) { + public void UpdateEntityIsActive(ushort entityId, bool isActive) { lock (Lock) { var entityUpdate = FindOrCreateEntityUpdate(entityId); @@ -278,7 +278,7 @@ public void UpdateEntityIsActive(byte entityId, bool isActive) { /// /// The ID of the entity. /// The entity network data to add. - public void AddEntityData(byte entityId, EntityNetworkData data) { + public void AddEntityData(ushort entityId, EntityNetworkData data) { lock (Lock) { var entityUpdate = FindOrCreateEntityUpdate(entityId); @@ -293,7 +293,7 @@ public void AddEntityData(byte entityId, EntityNetworkData data) { /// The ID of the entity. /// The index of the FSM of the entity. /// The host FSM data to add. - public void AddEntityHostFsmData(byte entityId, byte fsmIndex, EntityHostFsmData data) { + public void AddEntityHostFsmData(ushort entityId, byte fsmIndex, EntityHostFsmData data) { lock (Lock) { var entityUpdate = FindOrCreateEntityUpdate(entityId); diff --git a/HKMP/Networking/Packet/Data/EntitySpawn.cs b/HKMP/Networking/Packet/Data/EntitySpawn.cs index b8143919..ea339895 100644 --- a/HKMP/Networking/Packet/Data/EntitySpawn.cs +++ b/HKMP/Networking/Packet/Data/EntitySpawn.cs @@ -15,7 +15,7 @@ internal class EntitySpawn : IPacketData { /// /// The ID of the spawned entity. /// - public byte Id { get; set; } + public ushort Id { get; set; } /// /// The type of the entity that spawned the new entity. @@ -36,7 +36,7 @@ public void WriteData(IPacket packet) { /// public void ReadData(IPacket packet) { - Id = packet.ReadByte(); + Id = packet.ReadUShort(); SpawningType = (EntityType) packet.ReadUShort(); SpawnedType = (EntityType) packet.ReadUShort(); } diff --git a/HKMP/Networking/Packet/Data/EntityUpdate.cs b/HKMP/Networking/Packet/Data/EntityUpdate.cs index 52721315..1366d922 100644 --- a/HKMP/Networking/Packet/Data/EntityUpdate.cs +++ b/HKMP/Networking/Packet/Data/EntityUpdate.cs @@ -19,7 +19,7 @@ internal class EntityUpdate : IPacketData { /// /// The ID of the entity. /// - public byte Id { get; set; } + public ushort Id { get; set; } /// /// A set containing the types of updates contained in this packet. @@ -131,7 +131,7 @@ public void WriteData(IPacket packet) { /// public void ReadData(IPacket packet) { - Id = packet.ReadByte(); + Id = packet.ReadUShort(); // Read the byte flag representing update types and reconstruct it var updateTypeFlag = packet.ReadByte(); diff --git a/HKMP/Networking/Packet/Data/PacketDataCollection.cs b/HKMP/Networking/Packet/Data/PacketDataCollection.cs index d691db1e..b1d49114 100644 --- a/HKMP/Networking/Packet/Data/PacketDataCollection.cs +++ b/HKMP/Networking/Packet/Data/PacketDataCollection.cs @@ -1,7 +1,5 @@ namespace Hkmp.Networking.Packet.Data; -// TODO: extend this to allow a larger/customizable number of instances in the list -// It is now limited by the size of a byte /// /// Packet data for a collection of individual packet data instances. /// @@ -9,7 +7,7 @@ namespace Hkmp.Networking.Packet.Data; public class PacketDataCollection : RawPacketDataCollection, IPacketData where T : IPacketData, new() { /// public void WriteData(IPacket packet) { - var length = (byte) System.Math.Min(byte.MaxValue, DataInstances.Count); + var length = (ushort) System.Math.Min(ushort.MaxValue, DataInstances.Count); packet.Write(length); @@ -20,7 +18,7 @@ public void WriteData(IPacket packet) { /// public void ReadData(IPacket packet) { - var length = packet.ReadByte(); + var length = packet.ReadUShort(); for (var i = 0; i < length; i++) { // Create new instance of generic type diff --git a/HKMP/Networking/Packet/Data/PlayerEnterScene.cs b/HKMP/Networking/Packet/Data/PlayerEnterScene.cs index 4d7d8a01..95ca9f41 100644 --- a/HKMP/Networking/Packet/Data/PlayerEnterScene.cs +++ b/HKMP/Networking/Packet/Data/PlayerEnterScene.cs @@ -113,25 +113,25 @@ public ClientPlayerAlreadyInScene() { /// public void WriteData(IPacket packet) { - var length = (byte) System.Math.Min(byte.MaxValue, PlayerEnterSceneList.Count); + var length = System.Math.Min(byte.MaxValue, PlayerEnterSceneList.Count); - packet.Write(length); + packet.Write((byte) length); for (var i = 0; i < length; i++) { PlayerEnterSceneList[i].WriteData(packet); } - length = (byte) System.Math.Min(byte.MaxValue, EntitySpawnList.Count); + length = System.Math.Min(byte.MaxValue, EntitySpawnList.Count); - packet.Write(length); + packet.Write((byte) length); for (var i = 0; i < length; i++) { EntitySpawnList[i].WriteData(packet); } - length = (byte) System.Math.Min(byte.MaxValue, EntityUpdateList.Count); + length = System.Math.Min(ushort.MaxValue, EntityUpdateList.Count); - packet.Write(length); + packet.Write((ushort) length); for (var i = 0; i < length; i++) { EntityUpdateList[i].WriteData(packet); @@ -142,7 +142,7 @@ public void WriteData(IPacket packet) { /// public void ReadData(IPacket packet) { - var length = packet.ReadByte(); + int length = packet.ReadByte(); for (var i = 0; i < length; i++) { // Create new instance of generic type var instance = new ClientPlayerEnterScene(); @@ -166,7 +166,7 @@ public void ReadData(IPacket packet) { EntitySpawnList.Add(instance); } - length = packet.ReadByte(); + length = packet.ReadUShort(); for (var i = 0; i < length; i++) { // Create new instance of entity update var instance = new EntityUpdate(); diff --git a/HKMP/Networking/Server/ServerUpdateManager.cs b/HKMP/Networking/Server/ServerUpdateManager.cs index 623d3a0f..9a3f63df 100644 --- a/HKMP/Networking/Server/ServerUpdateManager.cs +++ b/HKMP/Networking/Server/ServerUpdateManager.cs @@ -280,7 +280,7 @@ public void UpdatePlayerAnimation(ushort id, ushort clipId, byte frame, bool[] e /// The ID of the entity. /// The type of the entity that spawned the new entity. /// The type of the entity that was spawned. - public void SetEntitySpawn(byte id, EntityType spawningType, EntityType spawnedType) { + public void SetEntitySpawn(ushort id, EntityType spawningType, EntityType spawnedType) { lock (Lock) { PacketDataCollection entitySpawnCollection; @@ -304,7 +304,7 @@ public void SetEntitySpawn(byte id, EntityType spawningType, EntityType spawnedT /// /// The ID of the entity. /// An instance of the entity update in the packet. - private EntityUpdate FindOrCreateEntityUpdate(byte entityId) { + private EntityUpdate FindOrCreateEntityUpdate(ushort entityId) { EntityUpdate entityUpdate = null; PacketDataCollection entityUpdateCollection; @@ -346,7 +346,7 @@ private EntityUpdate FindOrCreateEntityUpdate(byte entityId) { /// /// The ID of the entity. /// The position of the entity. - public void UpdateEntityPosition(byte entityId, Vector2 position) { + public void UpdateEntityPosition(ushort entityId, Vector2 position) { lock (Lock) { var entityUpdate = FindOrCreateEntityUpdate(entityId); @@ -360,7 +360,7 @@ public void UpdateEntityPosition(byte entityId, Vector2 position) { /// /// The ID of the entity. /// The scale data of the entity. - public void UpdateEntityScale(byte entityId, EntityUpdate.ScaleData scale) { + public void UpdateEntityScale(ushort entityId, EntityUpdate.ScaleData scale) { lock (Lock) { var entityUpdate = FindOrCreateEntityUpdate(entityId); @@ -375,7 +375,7 @@ public void UpdateEntityScale(byte entityId, EntityUpdate.ScaleData scale) { /// The ID of the entity. /// The animation ID of the entity. /// The wrap mode of the animation of the entity. - public void UpdateEntityAnimation(byte entityId, byte animationId, byte animationWrapMode) { + public void UpdateEntityAnimation(ushort entityId, byte animationId, byte animationWrapMode) { lock (Lock) { var entityUpdate = FindOrCreateEntityUpdate(entityId); @@ -390,7 +390,7 @@ public void UpdateEntityAnimation(byte entityId, byte animationId, byte animatio /// /// The ID of the entity. /// Whether the entity is active or not. - public void UpdateEntityIsActive(byte entityId, bool isActive) { + public void UpdateEntityIsActive(ushort entityId, bool isActive) { lock (Lock) { var entityUpdate = FindOrCreateEntityUpdate(entityId); @@ -404,7 +404,7 @@ public void UpdateEntityIsActive(byte entityId, bool isActive) { /// /// The ID of the entity. /// The list of entity network data to add. - public void AddEntityData(byte entityId, List data) { + public void AddEntityData(ushort entityId, List data) { lock (Lock) { var entityUpdate = FindOrCreateEntityUpdate(entityId); @@ -419,7 +419,7 @@ public void AddEntityData(byte entityId, List data) { /// The ID of the entity. /// The index of the FSM of the entity. /// The host FSM data to add. - public void AddEntityHostFsmData(byte entityId, byte fsmIndex, EntityHostFsmData data) { + public void AddEntityHostFsmData(ushort entityId, byte fsmIndex, EntityHostFsmData data) { lock (Lock) { var entityUpdate = FindOrCreateEntityUpdate(entityId); diff --git a/HKMP/Resource/entity-registry.json b/HKMP/Resource/entity-registry.json index 0e047581..ffe3bc9f 100644 --- a/HKMP/Resource/entity-registry.json +++ b/HKMP/Resource/entity-registry.json @@ -547,7 +547,10 @@ { "base_object_name": "Ceiling Dropper", "type": "Belfly", - "fsm_name": "Ceiling Dropper" + "fsm_name": "Ceiling Dropper", + "components": [ + "Rotation" + ] }, { "base_object_name": "Flip Hopper", @@ -672,7 +675,7 @@ ] }, { - "base_object_name": "AngryBuzzer", + "base_object_name": "Angry Buzzer", "type": "FuriousVengefly", "fsm_name": "Control" }, @@ -851,7 +854,10 @@ { "base_object_name": "Sprite", "type": "Loodle", - "fsm_name": "Damage Flash" + "fsm_name": "Damage Flash", + "components": [ + "Rotation" + ] } ] }, @@ -935,14 +941,17 @@ ] }, { - "base_object_name": "Colosseum_Armored_Roller", + "base_object_name": "Colosseum_Armoured_Roller", "type": "SharpBaldur", "fsm_name": "Roller" }, { - "base_object_name": "Colosseum_Armored_Mosquito", + "base_object_name": "Colosseum_Armoured_Mosquito", "type": "ArmouredSquit", - "fsm_name": "Mozzie2" + "fsm_name": "Mozzie2", + "components": [ + "Rotation" + ] }, { "base_object_name": "Colosseum_Shield_Zombie", From fe16310572efbc2a657e74b9f2ddcfc8c16b9d5e Mon Sep 17 00:00:00 2001 From: Extremelyd1 Date: Thu, 6 Jul 2023 20:56:22 +0200 Subject: [PATCH 059/216] Add some dream bosses, Hornet Sentinel and the final bosses --- HKMP/Game/Client/Entity/Entity.cs | 6 -- HKMP/Game/Client/Entity/EntitySpawner.cs | 41 +++++++++ HKMP/Game/Client/Entity/EntityType.cs | 21 ++++- HKMP/Game/Server/ServerManager.cs | 2 - HKMP/Resource/entity-registry.json | 112 +++++++++++++++++++++++ 5 files changed, 173 insertions(+), 9 deletions(-) diff --git a/HKMP/Game/Client/Entity/Entity.cs b/HKMP/Game/Client/Entity/Entity.cs index 62f289b9..ef5ff671 100644 --- a/HKMP/Game/Client/Entity/Entity.cs +++ b/HKMP/Game/Client/Entity/Entity.cs @@ -134,12 +134,6 @@ params EntityComponentType[] types ) }; - if (Object.Host.scene != Object.Client.scene) { - Logger.Debug($"Entity client object instantiated in other scene: \"{Object.Host.scene.name}\", \"{Object.Client.scene.name}\", moving client"); - - UnityEngine.SceneManagement.SceneManager.MoveGameObjectToScene(Object.Client, Object.Host.scene); - } - DestroyManagedChildren(Object.Client); _hasParent = false; diff --git a/HKMP/Game/Client/Entity/EntitySpawner.cs b/HKMP/Game/Client/Entity/EntitySpawner.cs index abd66a62..5d25db06 100644 --- a/HKMP/Game/Client/Entity/EntitySpawner.cs +++ b/HKMP/Game/Client/Entity/EntitySpawner.cs @@ -105,6 +105,22 @@ List clientFsms return SpawnMantisPetraScytheObject(clientFsms[0]); } + if (spawningType == EntityType.Galien && spawnedType == EntityType.GalienMiniScythe) { + return SpawnGalienMiniScytheObject(clientFsms[2]); + } + + if (spawningType == EntityType.Markoth && spawnedType == EntityType.MarkothShield) { + return SpawnMarkothShieldObject(clientFsms[3]); + } + + if (spawningType == EntityType.Kingsmould && spawnedType == EntityType.KingsmouldBlade) { + return SpawnKingsmouldBladeObject(clientFsms[0]); + } + + if (spawningType == EntityType.HornetSentinelSpikes && spawnedType == EntityType.HornetSentinelSpike) { + return SpawnHornetSentinelSpikeObject(clientFsms[0]); + } + return null; } @@ -362,4 +378,29 @@ private static GameObject SpawnMantisPetraScytheObject(PlayMakerFSM fsm) { return SpawnFromGlobalPool(action, gameObject); } + + private static GameObject SpawnGalienMiniScytheObject(PlayMakerFSM fsm) { + var action = fsm.GetFirstAction("Summon"); + + return SpawnFromCreateObject(action); + } + + private static GameObject SpawnMarkothShieldObject(PlayMakerFSM fsm) { + var action = fsm.GetFirstAction("Init"); + + return SpawnFromCreateObject(action); + } + + private static GameObject SpawnKingsmouldBladeObject(PlayMakerFSM fsm) { + var action = fsm.GetFirstAction("Throw"); + + return SpawnFromCreateObject(action); + } + + private static GameObject SpawnHornetSentinelSpikeObject(PlayMakerFSM fsm) { + var action = fsm.GetFirstAction("Spawn 1"); + var gameObject = action.gameObject.Value; + + return SpawnFromGlobalPool(action, gameObject); + } } diff --git a/HKMP/Game/Client/Entity/EntityType.cs b/HKMP/Game/Client/Entity/EntityType.cs index fbe43c14..2b7e4cc3 100644 --- a/HKMP/Game/Client/Entity/EntityType.cs +++ b/HKMP/Game/Client/Entity/EntityType.cs @@ -166,5 +166,24 @@ internal enum EntityType { VoltTwister, Zote, Tamer, - Beast + Beast, + Xero, + XeroNail, + Gorb, + ElderHu, + Marmu, + NoEyes, + Galien, + GalienScythe, + GalienMiniScythe, + Markoth, + MarkothShield, + Wingmould, + Kingsmould, + KingsmouldBlade, + Sibling, + HornetSentinelSpikes, + HornetSentinelSpike, + HollowKnight, + Radiance } diff --git a/HKMP/Game/Server/ServerManager.cs b/HKMP/Game/Server/ServerManager.cs index 50075936..7fc4b6e0 100644 --- a/HKMP/Game/Server/ServerManager.cs +++ b/HKMP/Game/Server/ServerManager.cs @@ -642,8 +642,6 @@ private void OnEntityUpdate(ushort id, EntityUpdate entityUpdate) { return; } - Logger.Debug($"Server received EntityUpdate: {entityUpdate.Id}, {entityUpdate.UpdateTypes.Contains(EntityUpdateType.Active)}, {entityUpdate.IsActive}"); - // Create the key for the entity data var serverEntityKey = new ServerEntityKey( playerData.CurrentScene, diff --git a/HKMP/Resource/entity-registry.json b/HKMP/Resource/entity-registry.json index ffe3bc9f..a93fbc52 100644 --- a/HKMP/Resource/entity-registry.json +++ b/HKMP/Resource/entity-registry.json @@ -4,6 +4,11 @@ "type": "BattleGate", "fsm_name": "BG Control" }, + { + "base_object_name": "Dream Gate", + "type": "BattleGate", + "fsm_name": "Control" + }, { "base_object_name": "Crawler", "type": "Crawlid", @@ -997,5 +1002,112 @@ "base_object_name": "Lobster", "type": "Beast", "fsm_name": "Control" + }, + { + "base_object_name": "Ghost Warrior Xero", + "type": "Xero", + "fsm_name": "FSM" + }, + { + "base_object_name": "Sword", + "type": "XeroNail", + "fsm_name": "xero_nail", + "components": [ + "Rotation", "ZPosition" + ] + }, + { + "base_object_name": "Ghost Warrior Slug", + "type": "Gorb", + "fsm_name": "FSM" + }, + { + "base_object_name": "Ghost Warrior Hu", + "type": "ElderHu", + "fsm_name": "FSM" + }, + { + "base_object_name": "Ghost Warrior Marmu", + "type": "Marmu", + "fsm_name": "FSM" + }, + { + "base_object_name": "Ghost Warrior No Eyes", + "type": "NoEyes", + "fsm_name": "FSM" + }, + { + "base_object_name": "Ghost Warrior Galien", + "type": "Galien", + "fsm_name": "FSM" + }, + { + "base_object_name": "Galien Hammer", + "type": "GalienScythe", + "fsm_name": "Control", + "components": [ + "Rotation" + ] + }, + { + "base_object_name": "Galien Mini Hammer", + "type": "GalienMiniScythe", + "fsm_name": "FSM" + }, + { + "base_object_name": "Ghost Warrior Markoth", + "type": "Markoth", + "fsm_name": "FSM" + }, + { + "base_object_name": "Markoth Shield", + "type": "MarkothShield", + "fsm_name": "Control" + }, + { + "base_object_name": "White Palace Fly", + "type": "Wingmould", + "fsm_name": "Control" + }, + { + "base_object_name": "Royal Gaurd", + "type": "Kingsmould", + "fsm_name": "Guard" + }, + { + "base_object_name": "Shot Kings Guard", + "type": "KingsmouldBlade", + "fsm_name": "Spin" + }, + { + "base_object_name": "Shade Sibling", + "type": "Sibling", + "fsm_name": "Control" + }, + { + "base_object_name": "Barb Region", + "type": "HornetSentinelSpikes", + "fsm_name": "Spawn Barbs" + }, + { + "base_object_name": "Hornet Barb", + "type": "HornetSentinelSpike", + "fsm_name": "Control", + "components": [ + "Rotation" + ] + }, + { + "base_object_name": "Hollow Knight Boss", + "type": "HollowKnight", + "fsm_name": "Control", + "components": [ + "GravityScale" + ] + }, + { + "base_object_name": "Radiance", + "type": "Radiance", + "fsm_name": "Control" } ] From d278d5d5ebbf3caeca4faa29ffab12dd15bfa4eb Mon Sep 17 00:00:00 2001 From: Extremelyd1 Date: Fri, 7 Jul 2023 18:33:32 +0200 Subject: [PATCH 060/216] Add remaining non-Godhome enemies Largely untested --- HKMP/Game/Client/Entity/Entity.cs | 8 +- HKMP/Game/Client/Entity/EntityManager.cs | 9 +- HKMP/Game/Client/Entity/EntityRegistry.cs | 4 + HKMP/Game/Client/Entity/EntitySpawner.cs | 26 ++++- HKMP/Game/Client/Entity/EntityType.cs | 22 ++++- HKMP/Resource/entity-registry.json | 112 +++++++++++++++++++++- 6 files changed, 171 insertions(+), 10 deletions(-) diff --git a/HKMP/Game/Client/Entity/Entity.cs b/HKMP/Game/Client/Entity/Entity.cs index ef5ff671..2f22c925 100644 --- a/HKMP/Game/Client/Entity/Entity.cs +++ b/HKMP/Game/Client/Entity/Entity.cs @@ -429,10 +429,10 @@ private void HandleComponents(EntityComponentType[] types) { UnityEngine.Object.Destroy(walker); } - // Find RigidBody2D MonoBehaviour and set it to be kinematic so it doesn't do physics on its own + // Find RigidBody2D MonoBehaviour and set it to be not simulated so it doesn't do physics on its own var rigidBody = Object.Client.GetComponent(); if (rigidBody != null) { - rigidBody.isKinematic = true; + rigidBody.simulated = false; } // Instantiate all types defined in the entity registry, which are passed to the constructor @@ -858,8 +858,8 @@ public void MakeHost() { if (clientActive) { var rigidBody = Object.Host.GetComponent(); - if (rigidBody != null && Type != EntityType.MantisLord) { - rigidBody.isKinematic = false; + if (rigidBody != null) { + rigidBody.simulated = true; } } diff --git a/HKMP/Game/Client/Entity/EntityManager.cs b/HKMP/Game/Client/Entity/EntityManager.cs index 8b0c2755..89252027 100644 --- a/HKMP/Game/Client/Entity/EntityManager.cs +++ b/HKMP/Game/Client/Entity/EntityManager.cs @@ -337,8 +337,13 @@ private void FindEntitiesInScene(Scene scene, bool lateLoad) { .Where(fsm => fsm.gameObject.scene == scene) .SelectMany(fsm => { if (!fsm.name.StartsWith("Colosseum Cage Small") && - !fsm.name.StartsWith("Colosseum Cage Large") || - !fsm.Fsm.Name.Equals("Spawn") + !fsm.name.StartsWith("Colosseum Cage Large") && + !fsm.name.StartsWith("Colosseum Cage Zote")) { + return new[] { fsm.gameObject }; + } + + if (!fsm.Fsm.Name.Equals("Spawn") && + !fsm.Fsm.Name.Equals("Control") ) { return new[] { fsm.gameObject }; } diff --git a/HKMP/Game/Client/Entity/EntityRegistry.cs b/HKMP/Game/Client/Entity/EntityRegistry.cs index 6947f5c7..d925904f 100644 --- a/HKMP/Game/Client/Entity/EntityRegistry.cs +++ b/HKMP/Game/Client/Entity/EntityRegistry.cs @@ -104,6 +104,10 @@ out EntityRegistryEntry foundEntry if (gameObject.GetComponent() == null) { continue; } + } else if (entry.Type == EntityType.GrimmFireball) { + if (gameObject.GetComponent() == null) { + continue; + } } foundEntry = entry; diff --git a/HKMP/Game/Client/Entity/EntitySpawner.cs b/HKMP/Game/Client/Entity/EntitySpawner.cs index 5d25db06..067baadd 100644 --- a/HKMP/Game/Client/Entity/EntitySpawner.cs +++ b/HKMP/Game/Client/Entity/EntitySpawner.cs @@ -69,14 +69,14 @@ List clientFsms if (spawningType == EntityType.SoulWarrior) { return SpawnSoulWarriorOrbObject(clientFsms[0]); } - if (spawningType == EntityType.SoulMaster) { + if (spawningType is EntityType.SoulMaster or EntityType.SoulTyrant) { return SpawnSoulMasterOrbObject(clientFsms[0]); } if (spawningType == EntityType.SoulMasterOrbSpinner) { return SpawnOrbSpinnerOrbObject(clientFsms[2]); } - if (spawningType == EntityType.SoulMasterPhase2) { + if (spawningType is EntityType.SoulMasterPhase2 or EntityType.SoulTyrantPhase2) { return SpawnSoulMaster2OrbObject(clientFsms[0]); } } @@ -121,6 +121,15 @@ List clientFsms return SpawnHornetSentinelSpikeObject(clientFsms[0]); } + if (spawningType == EntityType.GrimmkinSpawner) { + return SpawnGrimmkinObject(clientFsms[0]); + } + + if (spawningType is EntityType.Grimm or EntityType.NightmareKingGrimm && + spawnedType == EntityType.GrimmFireball) { + return SpawnGrimmFireballObject(clientFsms[0]); + } + return null; } @@ -403,4 +412,17 @@ private static GameObject SpawnHornetSentinelSpikeObject(PlayMakerFSM fsm) { return SpawnFromGlobalPool(action, gameObject); } + + private static GameObject SpawnGrimmkinObject(PlayMakerFSM fsm) { + var action = fsm.GetFirstAction("Level 3"); + + return SpawnFromCreateObject(action); + } + + private static GameObject SpawnGrimmFireballObject(PlayMakerFSM fsm) { + var action = fsm.GetFirstAction("Fire Low R"); + var gameObject = action.gameObject.Value; + + return SpawnFromGlobalPool(action, gameObject); + } } diff --git a/HKMP/Game/Client/Entity/EntityType.cs b/HKMP/Game/Client/Entity/EntityType.cs index 2b7e4cc3..874361b4 100644 --- a/HKMP/Game/Client/Entity/EntityType.cs +++ b/HKMP/Game/Client/Entity/EntityType.cs @@ -164,6 +164,7 @@ internal enum EntityType { HeavyFool, //DeathLoodle, VoltTwister, + ColosseumCageZote, Zote, Tamer, Beast, @@ -185,5 +186,24 @@ internal enum EntityType { HornetSentinelSpikes, HornetSentinelSpike, HollowKnight, - Radiance + Radiance, + GreyPrinceZote, + Zoteling, + VolatileZoteling, + WhiteDefender, + GrimmkinSpawner, + GrimmkinNovice, + GrimmkinMaster, + GrimmkinNightmare, + Grimm, + NightmareKingGrimm, + GrimmFireball, + HiveKnight, + HiveKnightSpike, + HiveKnightBee, + Flukemunga, + PaleLurker, + Revek, + SoulTyrant, + SoulTyrantPhase2, } diff --git a/HKMP/Resource/entity-registry.json b/HKMP/Resource/entity-registry.json index a93fbc52..712875fa 100644 --- a/HKMP/Resource/entity-registry.json +++ b/HKMP/Resource/entity-registry.json @@ -626,7 +626,10 @@ { "base_object_name": "Burrow Effect", "type": "DungDefenderBurrow", - "fsm_name": "Burrow Effect" + "fsm_name": "Burrow Effect", + "components": [ + "GravityScale" + ] }, { "base_object_name": "Fluke Mother", @@ -993,6 +996,16 @@ "type": "VoltTwister", "fsm_name": "Electric Mage" }, + { + "base_object_name": "Colosseum Cage Zote", + "type": "ColosseumCageZote", + "fsm_name": "Control" + }, + { + "base_object_name": "Zote Boss", + "type": "Zote", + "fsm_name": "Control" + }, { "base_object_name": "Lancer", "type": "Tamer", @@ -1109,5 +1122,102 @@ "base_object_name": "Radiance", "type": "Radiance", "fsm_name": "Control" + }, + { + "base_object_name": "Grey Prince", + "type": "GreyPrinceZote", + "fsm_name": "Control" + }, + { + "base_object_name": "Zoteling", + "type": "Zoteling", + "fsm_name": "Control" + }, + { + "base_object_name": "Zote Balloon", + "type": "VolatileZoteling", + "fsm_name": "Control" + }, + { + "base_object_name": "White Defender", + "type": "WhiteDefender", + "fsm_name": "Dung Defender" + }, + { + "base_object_name": "Flamebearer Spawn", + "type": "GrimmkinSpawner", + "fsm_name": "Spawn Control" + }, + { + "base_object_name": "Flamebearer Small", + "type": "GrimmkinNovice", + "fsm_name": "Control" + }, + { + "base_object_name": "Flamebearer Med", + "type": "GrimmkinMaster", + "fsm_name": "Control" + }, + { + "base_object_name": "Flamebearer Large", + "type": "GrimmkinNightmare", + "fsm_name": "Control" + }, + { + "base_object_name": "Grimm Boss", + "type": "Grimm", + "fsm_name": "Control" + }, + { + "base_object_name": "Nightmare Grimm Boss", + "type": "NightmareKingGrimm", + "fsm_name": "Control" + }, + { + "base_object_name": "Flameball Grimmballoon", + "type": "GrimmFireball" + }, + { + "base_object_name": "Hive Knight", + "type": "HiveKnight", + "fsm_name": "Control" + }, + { + "base_object_name": "Hive Knight Glob", + "type": "HiveKnightSpike", + "fsm_name": "Control" + }, + { + "base_object_name": "Bee Dropper", + "type": "HiveKnightBee", + "fsm_name": "Control" + }, + { + "base_object_name": "Fat Fluke", + "type": "Flukemunga", + "fsm_name": "Control" + }, + { + "base_object_name": "Pale Lurker", + "type": "PaleLurker", + "fsm_name": "Lurker Control" + }, + { + "base_object_name": "Ghost Battle Revek", + "type": "Revek", + "fsm_name": "Control", + "components": [ + "Rotation" + ] + }, + { + "base_object_name": "Dream Mage Lord", + "type": "SoulTyrant", + "fsm_name": "Mage Lord" + }, + { + "base_object_name": "Dream Mage Lord Phase2", + "type": "SoulTyrantPhase2", + "fsm_name": "Mage Lord 2" } ] From cd95fcaca907f79d22390db12f81092402ef0618 Mon Sep 17 00:00:00 2001 From: Extremelyd1 Date: Sat, 8 Jul 2023 16:24:26 +0200 Subject: [PATCH 061/216] Fix abilities/spells not able to hit remote entities --- HKMP/Animation/Effects/CycloneSlash.cs | 2 +- HKMP/Animation/Effects/DashBase.cs | 32 ++++++++++++---- HKMP/Animation/Effects/DashEnd.cs | 32 +++++++++------- HKMP/Animation/Effects/DashSlash.cs | 36 ++++++++---------- HKMP/Animation/Effects/DescendingDarkLand.cs | 17 ++++++--- HKMP/Animation/Effects/DesolateDiveLand.cs | 11 ++++-- HKMP/Animation/Effects/FireballBase.cs | 8 ++-- HKMP/Animation/Effects/FocusBurst.cs | 2 +- HKMP/Animation/Effects/GreatSlash.cs | 11 +++++- HKMP/Animation/Effects/QuakeDownBase.cs | 1 + HKMP/Animation/Effects/ScreamBase.cs | 37 ++----------------- HKMP/Animation/Effects/SlashBase.cs | 15 ++++++++ .../Client/Entity/Action/EntityFsmActions.cs | 10 ++++- HKMP/Game/Client/Entity/Entity.cs | 15 ++------ 14 files changed, 127 insertions(+), 102 deletions(-) diff --git a/HKMP/Animation/Effects/CycloneSlash.cs b/HKMP/Animation/Effects/CycloneSlash.cs index 80ced94b..c55648aa 100644 --- a/HKMP/Animation/Effects/CycloneSlash.cs +++ b/HKMP/Animation/Effects/CycloneSlash.cs @@ -36,7 +36,7 @@ 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); diff --git a/HKMP/Animation/Effects/DashBase.cs b/HKMP/Animation/Effects/DashBase.cs index cde33217..12e30b62 100644 --- a/HKMP/Animation/Effects/DashBase.cs +++ b/HKMP/Animation/Effects/DashBase.cs @@ -75,7 +75,7 @@ protected void Play(GameObject playerObject, bool[] effectInfo, bool shadowDash, // Instantiate the dash effect relative to the player position var dashEffectPrefab = HeroController.instance.shadowdashBurstPrefab; var dashEffect = Object.Instantiate(dashEffectPrefab); - Transform dashEffectTransform = dashEffect.transform; + var dashEffectTransform = dashEffect.transform; dashEffectTransform.position = playerTransform.position + spawnPosition; dashEffectTransform.rotation = playerTransform.rotation; dashEffectTransform.localScale = playerTransform.localScale; @@ -123,12 +123,30 @@ 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) { - // 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; - playerObject.GetComponent().enabled = true; + } 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) { + // 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; + playerObject.GetComponent().enabled = true; + } + + // 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 59fe0bf0..5ecbd812 100644 --- a/HKMP/Animation/Effects/DashEnd.cs +++ b/HKMP/Animation/Effects/DashEnd.cs @@ -19,24 +19,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 794e4d14..65f77a3a 100644 --- a/HKMP/Animation/Effects/DashSlash.cs +++ b/HKMP/Animation/Effects/DashSlash.cs @@ -39,17 +39,26 @@ public override void Play(GameObject playerObject, bool[] effectInfo) { // 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 playerScaleX = playerObject.transform.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; + dashSlash.layer = 17; ChangeAttackTypeOfFsm(dashSlash); + + // Get the "damages_enemy" FSM from the dash slash object + var slashFsm = dashSlash.LocateMyFSM("damages_enemy"); + // Find the variable that controls the slash direction for damaging enemies + var directionVar = slashFsm.FsmVariables.GetFsmFloat("direction"); + + // Set it based on the direction the knight is facing + var facingRight = playerScaleX > 0; + directionVar.Value = facingRight ? 180f : 0f; dashSlash.SetActive(true); @@ -60,26 +69,11 @@ public override void Play(GameObject playerObject, bool[] effectInfo) { // in case the local player was already performing it dashSlash.LocateMyFSM("Control Collider").SetState("Init"); + dashSlash.GetComponent().enabled = true; + var damage = ServerSettings.DashSlashDamage; if (ServerSettings.IsPvpEnabled && ShouldDoDamage && damage != 0) { - // Somehow adding a DamageHero component 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), - typeof(DamageHero) - ), - dashSlash.transform - ); - dashSlashCollider.SetActive(true); - dashSlashCollider.layer = 22; - - // Copy over the polygon collider points - dashSlashCollider.GetComponent().points = - dashSlash.GetComponent().points; - - dashSlashCollider.GetComponent().damageDealt = damage; + dashSlash.AddComponent().damageDealt = damage; } // Get the animator, figure out the duration of the animation and destroy the object accordingly afterwards 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 1e2fc5d2..ad2895da 100644 --- a/HKMP/Animation/Effects/GreatSlash.cs +++ b/HKMP/Animation/Effects/GreatSlash.cs @@ -36,9 +36,18 @@ public override void Play(GameObject playerObject, bool[] effectInfo) { greatSlashObject, playerAttacks.transform ); - greatSlash.layer = 22; + greatSlash.layer = 17; ChangeAttackTypeOfFsm(greatSlash); + + // Get the "damages_enemy" FSM from the great slash object + var slashFsm = greatSlash.LocateMyFSM("damages_enemy"); + // Find the variable that controls the slash direction for damaging enemies + var directionVar = slashFsm.FsmVariables.GetFsmFloat("direction"); + + // Set it based on the direction the knight is facing + var facingRight = playerObject.transform.localScale.x > 0; + directionVar.Value = facingRight ? 180f : 0f; greatSlash.SetActive(true); 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 b4d36e76..1aa175e6 100644 --- a/HKMP/Animation/Effects/SlashBase.cs +++ b/HKMP/Animation/Effects/SlashBase.cs @@ -53,6 +53,21 @@ protected void Play(GameObject playerObject, bool[] effectInfo, GameObject prefa ChangeAttackTypeOfFsm(slash); + // Get the "damages_enemy" FSM from the slash object + var slashFsm = slash.LocateMyFSM("damages_enemy"); + // Find the variable that controls the slash direction for damaging enemies + var directionVar = slashFsm.FsmVariables.GetFsmFloat("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; + directionVar.Value = facingRight ? 180f : 0f; + } else if (type is SlashType.Up) { + directionVar.Value = 90f; + } else { + directionVar.Value = 270f; + } + slash.SetActive(true); // Get the slash audio source and its clip diff --git a/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs b/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs index 30901db0..2b1b2fa9 100644 --- a/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs +++ b/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs @@ -926,7 +926,11 @@ private static bool GetNetworkDataFromAction(EntityNetworkData data, SetParticle } private static void ApplyNetworkDataFromAction(EntityNetworkData data, SetParticleEmission action) { - if (action?.emission == null) { + if (action.Fsm == null) { + return; + } + + if (action.emission == null) { return; } @@ -1127,6 +1131,10 @@ private static bool GetNetworkDataFromAction(EntityNetworkData data, FindChild a } private static void ApplyNetworkDataFromAction(EntityNetworkData data, FindChild action) { + if (action.Fsm == null) { + return; + } + var gameObject = action.Fsm.GetOwnerDefaultTarget(action.gameObject); if (gameObject == null) { return; diff --git a/HKMP/Game/Client/Entity/Entity.cs b/HKMP/Game/Client/Entity/Entity.cs index 2f22c925..dcb50103 100644 --- a/HKMP/Game/Client/Entity/Entity.cs +++ b/HKMP/Game/Client/Entity/Entity.cs @@ -429,10 +429,10 @@ private void HandleComponents(EntityComponentType[] types) { UnityEngine.Object.Destroy(walker); } - // Find RigidBody2D MonoBehaviour and set it to be not simulated so it doesn't do physics on its own + // Find RigidBody2D MonoBehaviour and remove it so the object doesn't do physics on its own var rigidBody = Object.Client.GetComponent(); if (rigidBody != null) { - rigidBody.simulated = false; + UnityEngine.Object.Destroy(rigidBody); } // Instantiate all types defined in the entity registry, which are passed to the constructor @@ -856,13 +856,6 @@ public void MakeHost() { Object.Client.SetActive(false); Object.Host.SetActive(clientActive); - if (clientActive) { - var rigidBody = Object.Host.GetComponent(); - if (rigidBody != null) { - rigidBody.simulated = true; - } - } - _lastIsActive = _hasParent ? Object.Host.activeSelf : Object.Host.activeInHierarchy; _isControlled = false; @@ -1089,8 +1082,8 @@ tk2dSpriteAnimationClip.WrapMode wrapMode /// /// The new value for active. public void UpdateIsActive(bool active) { - Logger.Info($"Entity '{Object.Client.name}' received active: {active}"); if (Object.Client != null) { + Logger.Info($"Entity '{Object.Client.name}' received active: {active}"); Object.Client.SetActive(active); } else { Logger.Warn($"Entity ({Id}, {Type}) could not update active, because client object is null"); @@ -1134,7 +1127,7 @@ public void UpdateData(List entityNetworkData) { var state = fsm.FsmStates[stateIndex]; var action = state.Actions[actionIndex]; - // Logger.Info($"Received entity network data for FSM: {fsm.Fsm.Name}, {state.Name}, {actionIndex} ({action.GetType()})"); + Logger.Info($"Received entity network data for FSM: {fsm.Fsm.Name}, {state.Name}, {actionIndex} ({action.GetType()})"); EntityFsmActions.ApplyNetworkDataFromAction(data, action); From 1d80a562f533e54f9272402231d0727e7e7a1655 Mon Sep 17 00:00:00 2001 From: Extremelyd1 Date: Sat, 8 Jul 2023 23:02:15 +0200 Subject: [PATCH 062/216] Fix White Defender and Grimm --- HKMP/Game/Client/Entity/EntitySpawner.cs | 18 ++++++++++-- HKMP/Game/Client/Entity/EntityType.cs | 3 ++ HKMP/Resource/entity-registry.json | 36 ++++++++++++++++++++---- 3 files changed, 48 insertions(+), 9 deletions(-) diff --git a/HKMP/Game/Client/Entity/EntitySpawner.cs b/HKMP/Game/Client/Entity/EntitySpawner.cs index 067baadd..e1bf1311 100644 --- a/HKMP/Game/Client/Entity/EntitySpawner.cs +++ b/HKMP/Game/Client/Entity/EntitySpawner.cs @@ -125,9 +125,14 @@ List clientFsms return SpawnGrimmkinObject(clientFsms[0]); } - if (spawningType is EntityType.Grimm or EntityType.NightmareKingGrimm && - spawnedType == EntityType.GrimmFireball) { - return SpawnGrimmFireballObject(clientFsms[0]); + if (spawningType is EntityType.Grimm or EntityType.NightmareKingGrimm) { + if (spawnedType == EntityType.GrimmFireball) { + return SpawnGrimmFireballObject(clientFsms[0]); + } + + if (spawnedType is EntityType.GrimmBat or EntityType.NightmareKingGrimmBat) { + return SpawnGrimmFirebatObject(clientFsms[0]); + } } return null; @@ -425,4 +430,11 @@ private static GameObject SpawnGrimmFireballObject(PlayMakerFSM fsm) { return SpawnFromGlobalPool(action, gameObject); } + + private static GameObject SpawnGrimmFirebatObject(PlayMakerFSM fsm) { + var action = fsm.GetFirstAction("Firebat 1"); + var gameObject = action.gameObject.Value; + + return SpawnFromGlobalPool(action, gameObject); + } } diff --git a/HKMP/Game/Client/Entity/EntityType.cs b/HKMP/Game/Client/Entity/EntityType.cs index 874361b4..f46b3cc5 100644 --- a/HKMP/Game/Client/Entity/EntityType.cs +++ b/HKMP/Game/Client/Entity/EntityType.cs @@ -196,7 +196,10 @@ internal enum EntityType { GrimmkinMaster, GrimmkinNightmare, Grimm, + GrimmBat, NightmareKingGrimm, + NightmareKingGrimmBat, + GrimmSpikes, GrimmFireball, HiveKnight, HiveKnightSpike, diff --git a/HKMP/Resource/entity-registry.json b/HKMP/Resource/entity-registry.json index 712875fa..40735d83 100644 --- a/HKMP/Resource/entity-registry.json +++ b/HKMP/Resource/entity-registry.json @@ -626,10 +626,7 @@ { "base_object_name": "Burrow Effect", "type": "DungDefenderBurrow", - "fsm_name": "Burrow Effect", - "components": [ - "GravityScale" - ] + "fsm_name": "Burrow Effect" }, { "base_object_name": "Fluke Mother", @@ -1141,12 +1138,18 @@ { "base_object_name": "White Defender", "type": "WhiteDefender", - "fsm_name": "Dung Defender" + "fsm_name": "Dung Defender", + "components": [ + "GravityScale" + ] }, { "base_object_name": "Flamebearer Spawn", "type": "GrimmkinSpawner", - "fsm_name": "Spawn Control" + "fsm_name": "Spawn Control", + "components": [ + "SpriteRenderer" + ] }, { "base_object_name": "Flamebearer Small", @@ -1166,13 +1169,34 @@ { "base_object_name": "Grimm Boss", "type": "Grimm", + "fsm_name": "Control", + "components": [ + "Rotation" + ] + }, + { + "base_object_name": "Grimm Firebat", + "type": "GrimmBat", "fsm_name": "Control" }, { "base_object_name": "Nightmare Grimm Boss", "type": "NightmareKingGrimm", + "fsm_name": "Control", + "components": [ + "Rotation" + ] + }, + { + "base_object_name": "Nightmare Firebat", + "type": "NightmareKingGrimmBat", "fsm_name": "Control" }, + { + "base_object_name": "Grimm Spike Holder", + "type": "GrimmSpikes", + "fsm_name": "Spike Control" + }, { "base_object_name": "Flameball Grimmballoon", "type": "GrimmFireball" From a3a5d7eee9d31fa9740f3db7f4ef34cfec2fb355 Mon Sep 17 00:00:00 2001 From: Extremelyd1 Date: Sun, 9 Jul 2023 14:47:00 +0200 Subject: [PATCH 063/216] Various fixes Increase entity network data limit, refactor component removal, fix Hive Knight --- HKMP/Game/Client/Entity/Entity.cs | 14 ++---------- HKMP/Game/Client/Entity/EntityInitializer.cs | 23 +++++++++++++++++++- HKMP/Networking/Packet/Data/EntityUpdate.cs | 10 ++++----- HKMP/Resource/entity-registry.json | 5 ++++- 4 files changed, 33 insertions(+), 19 deletions(-) diff --git a/HKMP/Game/Client/Entity/Entity.cs b/HKMP/Game/Client/Entity/Entity.cs index dcb50103..609d0a74 100644 --- a/HKMP/Game/Client/Entity/Entity.cs +++ b/HKMP/Game/Client/Entity/Entity.cs @@ -422,18 +422,8 @@ private void HandleComponents(EntityComponentType[] types) { addedComponentsString += " MeshRenderer"; } - - // Find Walker MonoBehaviour and remove it from the client object - var walker = Object.Client.GetComponent(); - if (walker != null) { - UnityEngine.Object.Destroy(walker); - } - - // Find RigidBody2D MonoBehaviour and remove it so the object doesn't do physics on its own - var rigidBody = Object.Client.GetComponent(); - if (rigidBody != null) { - UnityEngine.Object.Destroy(rigidBody); - } + + EntityInitializer.RemoveClientTypes(Object.Client); // Instantiate all types defined in the entity registry, which are passed to the constructor foreach (var type in types) { diff --git a/HKMP/Game/Client/Entity/EntityInitializer.cs b/HKMP/Game/Client/Entity/EntityInitializer.cs index 9901829d..0072cf17 100644 --- a/HKMP/Game/Client/Entity/EntityInitializer.cs +++ b/HKMP/Game/Client/Entity/EntityInitializer.cs @@ -30,6 +30,15 @@ internal static class EntityInitializer { "deparents" }; + /// + /// Array of types that should be removed from client-side enemies so it doesn't interfere with remote behaviour. + /// + private static readonly Type[] ToRemoveTypes = { + typeof(Walker), + typeof(Rigidbody2D), + typeof(BigCentipede) + }; + /// /// Array of types of actions that should be skipped during initialization. /// @@ -102,5 +111,17 @@ IEnumerator WaitForActionInitialization() { } } } - + + /// + /// Remove all types that should be removed from a client-side entity object. + /// + /// The game object on which to remove the types. + public static void RemoveClientTypes(GameObject gameObject) { + foreach (var type in ToRemoveTypes) { + var component = gameObject.GetComponent(type); + if (component != null) { + UnityEngine.Object.Destroy(component); + } + } + } } diff --git a/HKMP/Networking/Packet/Data/EntityUpdate.cs b/HKMP/Networking/Packet/Data/EntityUpdate.cs index 1366d922..873b07f1 100644 --- a/HKMP/Networking/Packet/Data/EntityUpdate.cs +++ b/HKMP/Networking/Packet/Data/EntityUpdate.cs @@ -456,15 +456,15 @@ public EntityNetworkData() { /// public void WriteData(IPacket packet) { - packet.Write((byte)Type); + packet.Write((byte) Type); var data = Packet.ToArray(); - if (data.Length > byte.MaxValue) { - Logger.Error("Length of entity network data exceeded max value of byte"); + if (data.Length > ushort.MaxValue) { + Logger.Error("Length of entity network data exceeded max value of ushort"); } - var length = (byte)System.Math.Min(data.Length, byte.MaxValue); + var length = (ushort) System.Math.Min(data.Length, ushort.MaxValue); packet.Write(length); for (var i = 0; i < length; i++) { @@ -476,7 +476,7 @@ public void WriteData(IPacket packet) { public void ReadData(IPacket packet) { Type = (EntityComponentType) packet.ReadByte(); - var length = packet.ReadByte(); + var length = packet.ReadUShort(); var data = new byte[length]; for (var i = 0; i < length; i++) { diff --git a/HKMP/Resource/entity-registry.json b/HKMP/Resource/entity-registry.json index 40735d83..9df46faa 100644 --- a/HKMP/Resource/entity-registry.json +++ b/HKMP/Resource/entity-registry.json @@ -1214,7 +1214,10 @@ { "base_object_name": "Bee Dropper", "type": "HiveKnightBee", - "fsm_name": "Control" + "fsm_name": "Control", + "components": [ + "Rotation" + ] }, { "base_object_name": "Fat Fluke", From 177124d94a1aef86c6a3ef996e4b119de265db99 Mon Sep 17 00:00:00 2001 From: Extremelyd1 Date: Tue, 11 Jul 2023 23:05:33 +0200 Subject: [PATCH 064/216] Add audio actions and support continuous actions --- .../Client/Entity/Action/EntityFsmActions.cs | 524 ++++++++++++++++++ HKMP/Game/Client/Entity/Entity.cs | 9 +- 2 files changed, 531 insertions(+), 2 deletions(-) diff --git a/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs b/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs index 2b1b2fa9..c2a0664c 100644 --- a/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs +++ b/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Reflection; using Hkmp.Networking.Packet.Data; +using Hkmp.Util; using HutongGames.PlayMaker; using HutongGames.PlayMaker.Actions; using Modding; @@ -56,8 +57,17 @@ internal static class EntityFsmActions { /// 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. /// @@ -94,6 +104,13 @@ static EntityFsmActions() { IL.HutongGames.PlayMaker.Actions.FlingObjectsFromGlobalPoolVel.OnEnter += FlingObjectsFromGlobalPoolVelOnEnter; IL.HutongGames.PlayMaker.Actions.FlingObjectsFromGlobalPoolTime.OnUpdate += FlingObjectsFromGlobalPoolTimeOnUpdate; IL.HutongGames.PlayMaker.Actions.GetRandomChild.DoGetRandomChild += GetRandomChildOnDoGetRandomChild; + + // Register an IL hook for the OnEnter method of FlingObjectsFromGlobalPoolTime. The OnEnter method does 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 + IL.HutongGames.PlayMaker.Actions.FlingObjectsFromGlobalPoolTime.OnEnter += il => { + new ILCursor(il).Emit(OpCodes.Nop); + }; } /// @@ -294,6 +311,28 @@ private static void GetRandomChildOnDoGetRandomChild(ILContext il) { } } + /// + /// Register a state change for the given FSM. Will propagate this change to all actions that are running in + /// that state. + /// + /// The FSM that changed states. + /// The name of the state that was changed to. + public static void RegisterStateChange(HutongGames.PlayMaker.Fsm fsm, string stateName) { + Logger.Debug($"RegisterStateChange: {fsm.Name}, {stateName}"); + + for (var i = ActionsInState.Count - 1; i >= 0; i--) { + var actionInState = ActionsInState[i]; + + Logger.Debug($" Action in state: {actionInState.Fsm.Name}, {actionInState.StateName}"); + + if (actionInState.Fsm == fsm && actionInState.StateName != stateName) { + Logger.Debug("EntityFsmActions: state changed, cancelling action in state"); + actionInState.ExitState(); + ActionsInState.RemoveAt(i); + } + } + } + #region SpawnObjectFromGlobalPool private static bool GetNetworkDataFromAction(EntityNetworkData data, SpawnObjectFromGlobalPool action) { @@ -2289,4 +2328,489 @@ private static void ApplyNetworkDataFromAction(EntityNetworkData data, CallMetho } #endregion + + #region AudioPlay + + private static bool GetNetworkDataFromAction(EntityNetworkData data, AudioPlay action) { + return true; + } + + private static void ApplyNetworkDataFromAction(EntityNetworkData data, AudioPlay action) { + var gameObject = action.Fsm.GetOwnerDefaultTarget(action.gameObject); + if (gameObject == null) { + return; + } + + var audioSource = gameObject.GetComponent(); + if (audioSource == null || !audioSource.enabled) { + return; + } + + var audioClip = action.oneShotClip.Value as AudioClip; + if (audioClip == null) { + audioSource.Play(); + + if (action.volume.IsNone) { + return; + } + + audioSource.volume = action.volume.Value; + return; + } + + if (!action.volume.IsNone) { + audioSource.PlayOneShot(audioClip, action.volume.Value); + return; + } + + audioSource.PlayOneShot(audioClip); + } + + #endregion + + #region AudioPlaySimple + + private static bool GetNetworkDataFromAction(EntityNetworkData data, AudioPlaySimple action) { + return true; + } + + private static void ApplyNetworkDataFromAction(EntityNetworkData data, AudioPlaySimple action) { + var gameObject = action.Fsm.GetOwnerDefaultTarget(action.gameObject); + if (gameObject == null) { + return; + } + + var audioSource = gameObject.GetComponent(); + if (audioSource == null) { + return; + } + + var audioClip = action.oneShotClip.Value as AudioClip; + if (audioClip == null) { + if (!audioSource.isPlaying) { + audioSource.Play(); + } + + if (!action.volume.IsNone) { + audioSource.volume = action.volume.Value; + } + } else { + if (!action.volume.IsNone) { + audioSource.PlayOneShot(audioClip, action.volume.Value); + } else { + audioSource.PlayOneShot(audioClip); + } + } + } + + #endregion + + #region AudioPlayerOneShot + + private static bool GetNetworkDataFromAction(EntityNetworkData data, AudioPlayerOneShot action) { + return true; + } + + private static void ApplyNetworkDataFromAction(EntityNetworkData data, AudioPlayerOneShot action) { + // TODO: delay? + + if (action.audioClips.Length == 0) { + return; + } + + var audioPlayerPrefab = action.audioPlayer.Value; + var audioPlayer = audioPlayerPrefab.Spawn( + action.spawnPoint.Value.transform.position, Quaternion.Euler(Vector3.up) + ); + + var audioSource = audioPlayer.GetComponent(); + + action.storePlayer.Value = audioPlayer; + + var randomWeightedIndex = ActionHelpers.GetRandomWeightedIndex(action.weights); + if (randomWeightedIndex != -1) { + var audioClip = action.audioClips[randomWeightedIndex]; + if (audioClip != null) { + audioSource.pitch = Random.Range(action.pitchMin.Value, action.pitchMax.Value); + audioSource.PlayOneShot(audioClip); + } + } + + audioSource.volume = action.volume.Value; + } + + #endregion + + #region AudioPlayerOneShotSingle + + private static bool GetNetworkDataFromAction(EntityNetworkData data, AudioPlayerOneShotSingle action) { + if (action.audioPlayer.IsNone || action.spawnPoint.IsNone || action.spawnPoint.Value == null) { + return false; + } + + return true; + } + + private static void ApplyNetworkDataFromAction(EntityNetworkData data, AudioPlayerOneShotSingle action) { + // TODO: delay? + + if (action.audioPlayer.IsNone || action.spawnPoint.IsNone || action.spawnPoint.Value == null) { + return; + } + + var audioPlayer = action.audioPlayer.Value; + var position = action.spawnPoint.Value.transform.position; + var up = Vector3.up; + + if (audioPlayer == null) { + return; + } + + audioPlayer = audioPlayer.Spawn(position, Quaternion.Euler(up)); + var audioSource = audioPlayer.GetComponent(); + action.storePlayer.Value = audioPlayer; + + var audioClip = action.audioClip.Value as AudioClip; + audioSource.pitch = Random.Range(action.pitchMin.Value, action.pitchMax.Value); + audioSource.volume = action.volume.Value; + + if (audioClip == null) { + return; + } + + audioSource.PlayOneShot(audioClip); + } + + #endregion + + #region SetAudioClip + + private static bool GetNetworkDataFromAction(EntityNetworkData data, SetAudioClip action) { + return true; + } + + private static void ApplyNetworkDataFromAction(EntityNetworkData data, SetAudioClip action) { + var gameObject = action.Fsm.GetOwnerDefaultTarget(action.gameObject); + if (gameObject == null) { + return; + } + + var audioSource = gameObject.GetComponent(); + if (audioSource == null) { + return; + } + + audioSource.clip = action.audioClip.Value as AudioClip; + } + + #endregion + + #region SetAudioPitch + + private static bool GetNetworkDataFromAction(EntityNetworkData data, SetAudioPitch action) { + return true; + } + + private static void ApplyNetworkDataFromAction(EntityNetworkData data, SetAudioPitch action) { + var gameObject = action.Fsm.GetOwnerDefaultTarget(action.gameObject); + if (gameObject == null) { + return; + } + + var audioSource = gameObject.GetComponent(); + if (audioSource == null) { + return; + } + + audioSource.pitch = action.pitch.Value; + } + + #endregion + + #region AudioStop + + private static bool GetNetworkDataFromAction(EntityNetworkData data, AudioStop action) { + return true; + } + + private static void ApplyNetworkDataFromAction(EntityNetworkData data, AudioStop action) { + var gameObject = action.Fsm.GetOwnerDefaultTarget(action.gameObject); + if (gameObject == null) { + return; + } + + var audioSource = gameObject.GetComponent(); + if (audioSource == null) { + return; + } + + audioSource.Stop(); + } + + #endregion + + #region SetAudioVolume + + private static bool GetNetworkDataFromAction(EntityNetworkData data, SetAudioVolume action) { + return true; + } + + private static void ApplyNetworkDataFromAction(EntityNetworkData data, SetAudioVolume action) { + var gameObject = action.Fsm.GetOwnerDefaultTarget(action.gameObject); + if (gameObject == null) { + return; + } + + var audioSource = gameObject.GetComponent(); + if (audioSource == null) { + return; + } + + audioSource.volume = action.volume.Value; + } + + #endregion + + #region AudioPlayRandom + + private static bool GetNetworkDataFromAction(EntityNetworkData data, AudioPlayRandom action) { + if (action.audioClips.Length == 0) { + return false; + } + + return true; + } + + private static void ApplyNetworkDataFromAction(EntityNetworkData data, AudioPlayRandom action) { + if (action.audioClips.Length == 0) { + return; + } + + var audioSource = action.gameObject.Value.GetComponent(); + + var randomWeightedIndex = ActionHelpers.GetRandomWeightedIndex(action.weights); + if (randomWeightedIndex == -1) { + return; + } + + var audioClip = action.audioClips[randomWeightedIndex]; + if (audioClip == null) { + return; + } + + audioSource.pitch = Random.Range(action.pitchMin.Value, action.pitchMax.Value); + audioSource.PlayOneShot(audioClip); + } + + #endregion + + #region AudioPlayInState + + private static bool GetNetworkDataFromAction(EntityNetworkData data, AudioPlayInState action) { + return true; + } + + private static void ApplyNetworkDataFromAction(EntityNetworkData data, AudioPlayInState action) { + var gameObject = action.Fsm.GetOwnerDefaultTarget(action.gameObject); + if (gameObject == null) { + return; + } + + var audioSource = gameObject.GetComponent(); + if (audioSource == null) { + return; + } + + if (!audioSource.isPlaying) { + audioSource.Play(); + } + + if (!action.volume.IsNone) { + audioSource.volume = action.volume.Value; + } + + var exitAction = () => { + audioSource.Stop(); + }; + + new ActionInState { + Fsm = action.Fsm, + StateName = action.State.Name, + ExitAction = exitAction + }.Register(); + } + + #endregion + + #region SpawnBloodTime + + private static bool GetNetworkDataFromAction(EntityNetworkData data, SpawnBloodTime action) { + return true; + } + + private static void ApplyNetworkDataFromAction(EntityNetworkData data, SpawnBloodTime action) { + var position = action.position.Value; + if (action.spawnPoint.Value != null) { + position += action.spawnPoint.Value.transform.position; + } + + var spawnMin = (short) action.spawnMin.Value; + var spawnMax = (short) action.spawnMax.Value; + + var speedMin = action.speedMin.Value; + var speedMax = action.speedMax.Value; + + var angleMin = action.angleMin.Value; + var angleMax = action.angleMax.Value; + + var color = action.colorOverride.IsNone ? new Color?() : action.colorOverride.Value; + + var coroutine = MonoBehaviourUtil.Instance.StartCoroutine(Behaviour()); + + new ActionInState { + Fsm = action.Fsm, + StateName = action.State.Name, + Coroutine = coroutine + }.Register(); + + IEnumerator Behaviour() { + while (true) { + yield return new WaitForSeconds(action.delay.Value); + + if (GlobalPrefabDefaults.Instance == null) { + break; + } + + GlobalPrefabDefaults.Instance.SpawnBlood( + position, + spawnMin, + spawnMax, + speedMin, + speedMax, + angleMin, + angleMax, + color + ); + } + } + } + + #endregion + + #region FlingObjectsFromGlobalPoolTime + + private static bool GetNetworkDataFromAction(EntityNetworkData data, FlingObjectsFromGlobalPoolTime action) { + data.Packet.Write(action.angleMin.Value); + data.Packet.Write(action.angleMax.Value); + + return true; + } + + private static void ApplyNetworkDataFromAction(EntityNetworkData data, FlingObjectsFromGlobalPoolTime action) { + var angleMin = data.Packet.ReadFloat(); + var angleMax = data.Packet.ReadFloat(); + + var position = Vector3.zero; + + var spawnPoint = action.spawnPoint.Value; + if (spawnPoint != null) { + position = spawnPoint.transform.position; + if (!action.position.IsNone) { + position += action.position.Value; + } + } else if (!action.position.IsNone) { + position = action.position.Value; + } + + var coroutine = MonoBehaviourUtil.Instance.StartCoroutine(Behaviour()); + + new ActionInState { + Fsm = action.Fsm, + StateName = action.State.Name, + Coroutine = coroutine + }.Register(); + + IEnumerator Behaviour() { + while (true) { + yield return new WaitForSeconds(action.frequency.Value); + + if (action.gameObject.Value == null) { + break; + } + + var numSpawns = Random.Range(action.spawnMin.Value, action.spawnMax.Value + 1); + for (var i = 0; i < numSpawns; i++) { + var gameObject = action.gameObject.Value.Spawn(position, Quaternion.Euler(Vector3.zero)); + + if (action.originVariationX != null) { + position.x += Random.Range(-action.originVariationX.Value, action.originVariationX.Value); + } + + if (action.originVariationY != null) { + position.y += Random.Range(-action.originVariationY.Value, action.originVariationY.Value); + } + + gameObject.transform.position = position; + + var rigidBody = gameObject.GetComponent(); + if (rigidBody == null) { + continue; + } + + var speed = Random.Range(action.speedMin.Value, action.speedMax.Value); + var angle = Random.Range(angleMin, angleMax); + + var x = speed * Mathf.Cos(angle * ((float) System.Math.PI / 180f)); + var y = speed * Mathf.Sin(angle * ((float) System.Math.PI / 180f)); + + rigidBody.velocity = new Vector2(x, y); + } + } + } + } + + #endregion + + /// + /// Class that keeps track of an action that executes while in a certain state of the FSM. + /// + private class ActionInState { + /// + /// The FSM of the action that is executing. + /// + public HutongGames.PlayMaker.Fsm Fsm { get; init; } + + /// + /// The name of the state in which this action executes. + /// + public string StateName { get; init; } + + /// + /// The coroutine that should be stopped when the state is exited. + /// + public Coroutine Coroutine { private get; init; } + + /// + /// The action that should be executed when the state is exited. + /// + public System.Action ExitAction { private get; init; } + + /// + /// Register this action by adding it to the list. + /// + public void Register() { + ActionsInState.Add(this); + } + + /// + /// Call when the state is exited, will stop the coroutine and execute the exit action. + /// + public void ExitState() { + if (Coroutine != null) { + MonoBehaviourUtil.Instance.StopCoroutine(Coroutine); + } + + ExitAction?.Invoke(); + } + } } diff --git a/HKMP/Game/Client/Entity/Entity.cs b/HKMP/Game/Client/Entity/Entity.cs index 609d0a74..a52e6365 100644 --- a/HKMP/Game/Client/Entity/Entity.cs +++ b/HKMP/Game/Client/Entity/Entity.cs @@ -586,7 +586,6 @@ private void OnUpdate() { } } - Logger.Debug($"Sending entity scale:\n{scaleData}"); _netClient.UpdateManager.UpdateEntityScale(Id, scaleData); _lastScale = newScale; @@ -1152,7 +1151,13 @@ public void UpdateHostFsmData(Dictionary hostFsmData) { if (states.Length <= data.CurrentState) { Logger.Warn($"Tried to update host FSM state for unknown state index: {data.CurrentState}"); } else { - snapshot.CurrentState = states[data.CurrentState].Name; + var stateName = states[data.CurrentState].Name; + + snapshot.CurrentState = stateName; + + // Also propagate this state change to the EntityFsmActions class with the client FSM for the + // same index + EntityFsmActions.RegisterStateChange(_fsms.Client[fsmIndex].Fsm, stateName); } } From 2265afa8a138758b93148fe6af9af95d24801148 Mon Sep 17 00:00:00 2001 From: Extremelyd1 Date: Wed, 12 Jul 2023 22:40:45 +0200 Subject: [PATCH 065/216] Fix issues with False Knight and battle gates --- HKMP/Game/Client/Entity/Action/EntityFsmActions.cs | 9 +++++---- HKMP/Game/Client/Entity/EntityInitializer.cs | 3 ++- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs b/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs index c2a0664c..178627ac 100644 --- a/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs +++ b/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs @@ -105,12 +105,13 @@ static EntityFsmActions() { IL.HutongGames.PlayMaker.Actions.FlingObjectsFromGlobalPoolTime.OnUpdate += FlingObjectsFromGlobalPoolTimeOnUpdate; IL.HutongGames.PlayMaker.Actions.GetRandomChild.DoGetRandomChild += GetRandomChildOnDoGetRandomChild; - // Register an IL hook for the OnEnter method of FlingObjectsFromGlobalPoolTime. The OnEnter method does not + // 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 - IL.HutongGames.PlayMaker.Actions.FlingObjectsFromGlobalPoolTime.OnEnter += il => { - new ILCursor(il).Emit(OpCodes.Nop); - }; + void EmitNop(ILContext il) => new ILCursor(il).Emit(OpCodes.Nop); + + IL.HutongGames.PlayMaker.Actions.FlingObjectsFromGlobalPoolTime.OnEnter += EmitNop; + IL.SpawnBloodTime.OnEnter += EmitNop; } /// diff --git a/HKMP/Game/Client/Entity/EntityInitializer.cs b/HKMP/Game/Client/Entity/EntityInitializer.cs index 0072cf17..a730928f 100644 --- a/HKMP/Game/Client/Entity/EntityInitializer.cs +++ b/HKMP/Game/Client/Entity/EntityInitializer.cs @@ -27,7 +27,8 @@ internal static class EntityInitializer { "dormant", "pause", "init pause", - "deparents" + "deparents", + "opened" // For battle gates }; /// From e9e4c542c9a0386fa6d84c60d76710c36f566849 Mon Sep 17 00:00:00 2001 From: Extremelyd1 Date: Sun, 16 Jul 2023 12:21:19 +0200 Subject: [PATCH 066/216] Keep track of hooks and dispose properly --- .../Client/Entity/Action/FsmActionHooks.cs | 30 +++++++++++++++---- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/HKMP/Game/Client/Entity/Action/FsmActionHooks.cs b/HKMP/Game/Client/Entity/Action/FsmActionHooks.cs index b42b7c62..ff327665 100644 --- a/HKMP/Game/Client/Entity/Action/FsmActionHooks.cs +++ b/HKMP/Game/Client/Entity/Action/FsmActionHooks.cs @@ -3,6 +3,7 @@ using Hkmp.Logging; using HutongGames.PlayMaker; using MonoMod.RuntimeDetour; +using UnityEngine.SceneManagement; namespace Hkmp.Game.Client.Entity.Action; @@ -15,8 +16,16 @@ internal static class FsmActionHooks { /// private static readonly Dictionary TypeEvents; + /// + /// List of all registered hooks. Used to loop over and remove all. + /// + private static readonly List Hooks; + static FsmActionHooks() { TypeEvents = new Dictionary(); + Hooks = new List(); + + UnityEngine.SceneManagement.SceneManager.activeSceneChanged += OnSceneChanged; } /// @@ -31,12 +40,10 @@ public static void RegisterFsmStateActionType(Type type, Action var onEnterMethodInfo = type.GetMethod("OnEnter"); - // TODO: check if we need to keep track of hook - // ReSharper disable once ObjectCreationAsStatement - new Hook( + Hooks.Add(new Hook( onEnterMethodInfo, OnActionEntered - ); + )); TypeEvents.Add(type, fsmActionHook); } @@ -60,6 +67,19 @@ private static void OnActionEntered(Action orig, FsmStateAction fsmActionHook.InvokeEvent(self); } + /// + /// Callback method for when the scene changes, used to reset all hooks. + /// + /// The old scene. + /// The new scene. + private static void OnSceneChanged(Scene oldScene, Scene newScene) { + foreach (var hook in Hooks) { + hook.Dispose(); + } + + Hooks.Clear(); + } + /// /// A wrapper class containing an event for all callbacks of a hook. /// @@ -77,4 +97,4 @@ public void InvokeEvent(FsmStateAction fsmStateAction) { HookEvent?.Invoke(fsmStateAction); } } -} \ No newline at end of file +} From faff2d61254362dbb5b0aa481e27c2bed6774433 Mon Sep 17 00:00:00 2001 From: Extremelyd1 Date: Fri, 21 Jul 2023 12:15:16 +0200 Subject: [PATCH 067/216] Refactor entity networking into reliable and non-reliable --- HKMP/Game/Client/ClientManager.cs | 22 +- HKMP/Game/Client/Entity/EntityManager.cs | 28 ++- HKMP/Game/Server/ServerManager.cs | 46 +++- HKMP/Networking/Client/ClientUpdateManager.cs | 46 ++-- HKMP/Networking/Packet/Data/EntityUpdate.cs | 237 ++++++++++++------ .../Packet/Data/PlayerEnterScene.cs | 26 ++ HKMP/Networking/Packet/PacketId.cs | 14 +- HKMP/Networking/Packet/UpdatePacket.cs | 4 + HKMP/Networking/Server/ServerUpdateManager.cs | 49 ++-- 9 files changed, 352 insertions(+), 120 deletions(-) diff --git a/HKMP/Game/Client/ClientManager.cs b/HKMP/Game/Client/ClientManager.cs index de00188b..4bdd3ea4 100644 --- a/HKMP/Game/Client/ClientManager.cs +++ b/HKMP/Game/Client/ClientManager.cs @@ -219,6 +219,8 @@ ModSettings modSettings OnPlayerMapUpdate); packetManager.RegisterClientPacketHandler(ClientPacketId.EntitySpawn, OnEntitySpawn); packetManager.RegisterClientPacketHandler(ClientPacketId.EntityUpdate, OnEntityUpdate); + packetManager.RegisterClientPacketHandler(ClientPacketId.ReliableEntityUpdate, + OnReliableEntityUpdate); packetManager.RegisterClientPacketHandler(ClientPacketId.SceneHostTransfer, OnSceneHostTransfer); packetManager.RegisterClientPacketHandler(ClientPacketId.ServerSettingsUpdated, OnServerSettingsUpdated); @@ -613,9 +615,14 @@ private void OnPlayerAlreadyInScene(ClientPlayerAlreadyInScene alreadyInScene) { } foreach (var entityUpdate in alreadyInScene.EntityUpdateList) { - Logger.Info($"Updating already in scene entity with ID: {entityUpdate.Id}, {entityUpdate.UpdateTypes.Contains(EntityUpdateType.Active)}, {entityUpdate.IsActive}"); + 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 @@ -747,6 +754,19 @@ private void OnEntityUpdate(EntityUpdate entityUpdate) { _entityManager.HandleEntityUpdate(entityUpdate); } + + /// + /// Callback method for when a reliable entity update is received. + /// + /// The ReliableEntityUpdate packet data. + private void OnReliableEntityUpdate(ReliableEntityUpdate entityUpdate) { + // We only propagate entity updates to the entity manager if we have determined the scene host + if (!_sceneHostDetermined) { + return; + } + + _entityManager.HandleReliableEntityUpdate(entityUpdate); + } private void OnSceneHostTransfer() { Logger.Info("Received scene host transfer"); diff --git a/HKMP/Game/Client/Entity/EntityManager.cs b/HKMP/Game/Client/Entity/EntityManager.cs index 89252027..7fedf639 100644 --- a/HKMP/Game/Client/Entity/EntityManager.cs +++ b/HKMP/Game/Client/Entity/EntityManager.cs @@ -38,12 +38,12 @@ internal class EntityManager { /// Usually this occurs because the entities are loaded later than the updates are received when the local player /// enters a new scene. /// - private readonly Queue _receivedUpdates; + private readonly Queue _receivedUpdates; public EntityManager(NetClient netClient) { _netClient = netClient; _entities = new Dictionary(); - _receivedUpdates = new Queue(); + _receivedUpdates = new Queue(); EntityProcessor.Initialize(_entities, netClient); @@ -164,7 +164,25 @@ public void HandleEntityUpdate(EntityUpdate entityUpdate, bool alreadyInSceneUpd alreadyInSceneUpdate ); } + } + + /// + /// Method for handling received reliable entity updates. + /// + /// The reliable entity update to handle. + /// Whether this is the update from the already in scene packet. + public void HandleReliableEntityUpdate(ReliableEntityUpdate entityUpdate, bool alreadyInSceneUpdate = false) { + if (_isSceneHost) { + return; + } + if (!_entities.TryGetValue(entityUpdate.Id, out var entity)) { + Logger.Debug($"Could not find entity ({entityUpdate.Id}) to apply update for; storing update for now"); + _receivedUpdates.Enqueue(entityUpdate); + + return; + } + if (entityUpdate.UpdateTypes.Contains(EntityUpdateType.Active)) { entity.UpdateIsActive(entityUpdate.IsActive); } @@ -248,7 +266,11 @@ private void CheckReceivedUpdates() { if (_entities.TryGetValue(update.Id, out _)) { Logger.Debug("Found un-applied entity update, applying now"); - HandleEntityUpdate(update); + if (update is EntityUpdate entityUpdate) { + HandleEntityUpdate(entityUpdate); + } else if (update is ReliableEntityUpdate reliableEntityUpdate) { + HandleReliableEntityUpdate(reliableEntityUpdate); + } } } } diff --git a/HKMP/Game/Server/ServerManager.cs b/HKMP/Game/Server/ServerManager.cs index 7fc4b6e0..6fc4faf3 100644 --- a/HKMP/Game/Server/ServerManager.cs +++ b/HKMP/Game/Server/ServerManager.cs @@ -138,6 +138,8 @@ PacketManager packetManager OnPlayerMapUpdate); packetManager.RegisterServerPacketHandler(ServerPacketId.EntitySpawn, OnEntitySpawn); packetManager.RegisterServerPacketHandler(ServerPacketId.EntityUpdate, OnEntityUpdate); + packetManager.RegisterServerPacketHandler(ServerPacketId.ReliableEntityUpdate, + OnReliableEntityUpdate); packetManager.RegisterServerPacketHandler(ServerPacketId.PlayerDisconnect, OnPlayerDisconnect); packetManager.RegisterServerPacketHandler(ServerPacketId.PlayerDeath, OnPlayerDeath); packetManager.RegisterServerPacketHandler(ServerPacketId.PlayerTeamUpdate, @@ -369,6 +371,7 @@ private void OnClientEnterScene(ServerPlayerData playerData) { var entitySpawnList = new List(); var entityUpdateList = new List(); + var reliableEntityUpdateList = new List(); foreach (var keyDataPair in _entityData) { var entityKey = keyDataPair.Key; @@ -414,25 +417,30 @@ private void OnClientEnterScene(ServerPlayerData playerData) { entityUpdate.AnimationWrapMode = entityData.AnimationWrapMode; } + var reliableEntityUpdate = new ReliableEntityUpdate { + Id = entityKey.EntityId + }; + if (entityData.IsActive.HasValue) { - entityUpdate.UpdateTypes.Add(EntityUpdateType.Active); - entityUpdate.IsActive = entityData.IsActive.Value; + reliableEntityUpdate.UpdateTypes.Add(EntityUpdateType.Active); + reliableEntityUpdate.IsActive = entityData.IsActive.Value; } if (entityData.GenericData.Count > 0) { - entityUpdate.UpdateTypes.Add(EntityUpdateType.Data); - entityUpdate.GenericData.AddRange(entityData.GenericData); + reliableEntityUpdate.UpdateTypes.Add(EntityUpdateType.Data); + reliableEntityUpdate.GenericData.AddRange(entityData.GenericData); } if (entityData.HostFsmData.Count > 0) { - entityUpdate.UpdateTypes.Add(EntityUpdateType.HostFsm); + reliableEntityUpdate.UpdateTypes.Add(EntityUpdateType.HostFsm); foreach (var pair in entityData.HostFsmData) { - entityUpdate.HostFsmData[pair.Key] = pair.Value; + reliableEntityUpdate.HostFsmData[pair.Key] = pair.Value; } } entityUpdateList.Add(entityUpdate); + reliableEntityUpdateList.Add(reliableEntityUpdate); } if (!alreadyPlayersInScene) { @@ -443,6 +451,7 @@ private void OnClientEnterScene(ServerPlayerData playerData) { enterSceneList, entitySpawnList, entityUpdateList, + reliableEntityUpdateList, !alreadyPlayersInScene ); } @@ -701,7 +710,32 @@ private void OnEntityUpdate(ushort id, EntityUpdate entityUpdate) { entityData.AnimationId = entityUpdate.AnimationId; entityData.AnimationWrapMode = entityUpdate.AnimationWrapMode; } + } + + /// + /// Callback method for when a reliable entity update is received from a player. + /// + /// The ID of the player. + /// The ReliableEntityUpdate packet data. + private void OnReliableEntityUpdate(ushort id, ReliableEntityUpdate entityUpdate) { + if (!_playerData.TryGetValue(id, out var playerData)) { + Logger.Warn($"Received ReliableEntityUpdate data, but player with ID {id} is not in mapping"); + return; + } + + // Create the key for the entity data + var serverEntityKey = new ServerEntityKey( + playerData.CurrentScene, + entityUpdate.Id + ); + // Check with the created key whether we have an existing entry + if (!_entityData.TryGetValue(serverEntityKey, out var entityData)) { + // If the entry for this entity did not yet exist, we insert a new one + entityData = new ServerEntityData(); + _entityData[serverEntityKey] = entityData; + } + if (entityUpdate.UpdateTypes.Contains(EntityUpdateType.Active)) { SendDataInSameScene( id, diff --git a/HKMP/Networking/Client/ClientUpdateManager.cs b/HKMP/Networking/Client/ClientUpdateManager.cs index a9c92521..6299bfcf 100644 --- a/HKMP/Networking/Client/ClientUpdateManager.cs +++ b/HKMP/Networking/Client/ClientUpdateManager.cs @@ -177,20 +177,26 @@ public void SetEntitySpawn(ushort id, EntityType spawningType, EntityType spawne /// Find an existing or create a new EntityUpdate instance in the current update packet. /// /// The ID of the entity. + /// The type of the entity update. Either or + /// . /// The existing or new EntityUpdate instance. - private EntityUpdate FindOrCreateEntityUpdate(ushort entityId) { - EntityUpdate entityUpdate = null; - PacketDataCollection entityUpdateCollection; + private T FindOrCreateEntityUpdate(ushort entityId) where T : BaseEntityUpdate, new() { + var entityUpdate = default(T); + PacketDataCollection entityUpdateCollection; + + var packetId = typeof(T) == typeof(EntityUpdate) + ? ServerPacketId.EntityUpdate + : ServerPacketId.ReliableEntityUpdate; // First check whether there actually exists entity data at all if (CurrentUpdatePacket.TryGetSendingPacketData( - ServerPacketId.EntityUpdate, + packetId, out var packetData )) { // And if there exists data already, try to find a match for the entity type and id - entityUpdateCollection = (PacketDataCollection) packetData; + entityUpdateCollection = (PacketDataCollection) packetData; foreach (var existingPacketData in entityUpdateCollection.DataInstances) { - var existingEntityUpdate = (EntityUpdate) existingPacketData; + var existingEntityUpdate = (T) existingPacketData; if (existingEntityUpdate.Id == entityId) { entityUpdate = existingEntityUpdate; break; @@ -198,15 +204,21 @@ out var packetData } } else { // If no data exists yet, we instantiate the data collection class and put it at the respective key - entityUpdateCollection = new PacketDataCollection(); - CurrentUpdatePacket.SetSendingPacketData(ServerPacketId.EntityUpdate, entityUpdateCollection); + entityUpdateCollection = new PacketDataCollection(); + CurrentUpdatePacket.SetSendingPacketData(packetId, entityUpdateCollection); } // If no existing instance was found, create one and add it to the (newly created) collection if (entityUpdate == null) { - entityUpdate = new EntityUpdate { - Id = entityId - }; + if (typeof(T) == typeof(EntityUpdate)) { + entityUpdate = (T) (object) new EntityUpdate { + Id = entityId + }; + } else { + entityUpdate = (T) (object) new ReliableEntityUpdate { + Id = entityId + }; + } entityUpdateCollection.DataInstances.Add(entityUpdate); @@ -222,7 +234,7 @@ out var packetData /// The new position of the entity. public void UpdateEntityPosition(ushort entityId, Vector2 position) { lock (Lock) { - var entityUpdate = FindOrCreateEntityUpdate(entityId); + var entityUpdate = FindOrCreateEntityUpdate(entityId); entityUpdate.UpdateTypes.Add(EntityUpdateType.Position); entityUpdate.Position = position; @@ -236,7 +248,7 @@ public void UpdateEntityPosition(ushort entityId, Vector2 position) { /// The scale data of the entity. public void UpdateEntityScale(ushort entityId, EntityUpdate.ScaleData scale) { lock (Lock) { - var entityUpdate = FindOrCreateEntityUpdate(entityId); + var entityUpdate = FindOrCreateEntityUpdate(entityId); entityUpdate.UpdateTypes.Add(EntityUpdateType.Scale); entityUpdate.Scale = scale; @@ -251,7 +263,7 @@ public void UpdateEntityScale(ushort entityId, EntityUpdate.ScaleData scale) { /// The wrap mode of the animation of the entity. public void UpdateEntityAnimation(ushort entityId, byte animationId, byte animationWrapMode) { lock (Lock) { - var entityUpdate = FindOrCreateEntityUpdate(entityId); + var entityUpdate = FindOrCreateEntityUpdate(entityId); entityUpdate.UpdateTypes.Add(EntityUpdateType.Animation); entityUpdate.AnimationId = animationId; @@ -266,7 +278,7 @@ public void UpdateEntityAnimation(ushort entityId, byte animationId, byte animat /// Whether the entity is active or not. public void UpdateEntityIsActive(ushort entityId, bool isActive) { lock (Lock) { - var entityUpdate = FindOrCreateEntityUpdate(entityId); + var entityUpdate = FindOrCreateEntityUpdate(entityId); entityUpdate.UpdateTypes.Add(EntityUpdateType.Active); entityUpdate.IsActive = isActive; @@ -280,7 +292,7 @@ public void UpdateEntityIsActive(ushort entityId, bool isActive) { /// The entity network data to add. public void AddEntityData(ushort entityId, EntityNetworkData data) { lock (Lock) { - var entityUpdate = FindOrCreateEntityUpdate(entityId); + var entityUpdate = FindOrCreateEntityUpdate(entityId); entityUpdate.UpdateTypes.Add(EntityUpdateType.Data); entityUpdate.GenericData.Add(data); @@ -295,7 +307,7 @@ public void AddEntityData(ushort entityId, EntityNetworkData data) { /// The host FSM data to add. public void AddEntityHostFsmData(ushort entityId, byte fsmIndex, EntityHostFsmData data) { lock (Lock) { - var entityUpdate = FindOrCreateEntityUpdate(entityId); + var entityUpdate = FindOrCreateEntityUpdate(entityId); entityUpdate.UpdateTypes.Add(EntityUpdateType.HostFsm); diff --git a/HKMP/Networking/Packet/Data/EntityUpdate.cs b/HKMP/Networking/Packet/Data/EntityUpdate.cs index 873b07f1..10571035 100644 --- a/HKMP/Networking/Packet/Data/EntityUpdate.cs +++ b/HKMP/Networking/Packet/Data/EntityUpdate.cs @@ -7,20 +7,37 @@ namespace Hkmp.Networking.Packet.Data; /// -/// Packet data for an entity update. +/// Base entity update class for reliable and non-reliable entity update data. /// -internal class EntityUpdate : IPacketData { +internal abstract class BaseEntityUpdate : IPacketData { /// - public bool IsReliable => false; + public abstract bool IsReliable { get; } /// - public bool DropReliableDataIfNewerExists => false; - + public abstract bool DropReliableDataIfNewerExists { get; } + /// /// The ID of the entity. /// public ushort Id { get; set; } + /// + public abstract void WriteData(IPacket packet); + + /// + public abstract void ReadData(IPacket packet); +} + +/// +/// Packet data for the non-reliable part of an entity update. +/// +internal class EntityUpdate : BaseEntityUpdate { + /// + public override bool IsReliable => false; + + /// + public override bool DropReliableDataIfNewerExists => false; + /// /// A set containing the types of updates contained in this packet. /// @@ -45,27 +62,16 @@ internal class EntityUpdate : IPacketData { /// public byte AnimationWrapMode { get; set; } - /// - /// Whether the entity is active or not. - /// - public bool IsActive { get; set; } - - public List GenericData { get; } - - public Dictionary HostFsmData { get; } - /// /// Construct the entity update data. /// public EntityUpdate() { UpdateTypes = new HashSet(); Scale = new ScaleData(); - GenericData = new List(); - HostFsmData = new Dictionary(); } /// - public void WriteData(IPacket packet) { + public override void WriteData(IPacket packet) { packet.Write(Id); // Construct the byte flag representing update types @@ -99,38 +105,10 @@ public void WriteData(IPacket packet) { packet.Write(AnimationId); packet.Write(AnimationWrapMode); } - - if (UpdateTypes.Contains(EntityUpdateType.Active)) { - packet.Write(IsActive); - } - - if (UpdateTypes.Contains(EntityUpdateType.Data)) { - if (GenericData.Count > byte.MaxValue) { - Logger.Error("Length of entity network data instances exceeded max value of byte"); - } - - var length = (byte)System.Math.Min(GenericData.Count, byte.MaxValue); - - packet.Write(length); - for (var i = 0; i < length; i++) { - GenericData[i].WriteData(packet); - } - } - - if (UpdateTypes.Contains(EntityUpdateType.HostFsm)) { - var length = (byte) HostFsmData.Count; - packet.Write(length); - - foreach (var pair in HostFsmData) { - packet.Write(pair.Key); - - pair.Value.WriteData(packet); - } - } } /// - public void ReadData(IPacket packet) { + public override void ReadData(IPacket packet) { Id = packet.ReadUShort(); // Read the byte flag representing update types and reconstruct it @@ -161,34 +139,6 @@ public void ReadData(IPacket packet) { AnimationId = packet.ReadByte(); AnimationWrapMode = packet.ReadByte(); } - - if (UpdateTypes.Contains(EntityUpdateType.Active)) { - IsActive = packet.ReadBool(); - } - - if (UpdateTypes.Contains(EntityUpdateType.Data)) { - var length = packet.ReadByte(); - - for (var i = 0; i < length; i++) { - var entityNetworkData = new EntityNetworkData(); - entityNetworkData.ReadData(packet); - - GenericData.Add(entityNetworkData); - } - } - - if (UpdateTypes.Contains(EntityUpdateType.HostFsm)) { - var length = packet.ReadByte(); - - for (var i = 0; i < length; i++) { - var key = packet.ReadByte(); - - var data = new EntityHostFsmData(); - data.ReadData(packet); - - HostFsmData.Add(key, data); - } - } } /// @@ -437,6 +387,145 @@ public override string ToString() { } } +/// +/// Packet data for the reliable part of an entity update. +/// +internal class ReliableEntityUpdate : BaseEntityUpdate { + /// + public override bool IsReliable => true; + + /// + public override bool DropReliableDataIfNewerExists => false; + + /// + /// A set containing the types of updates contained in this packet. + /// + public HashSet UpdateTypes { get; } + + /// + /// Whether the entity is active or not. + /// + public bool IsActive { get; set; } + + /// + /// List of generic entity network data for entity components and FSM updates. + /// + public List GenericData { get; } + + /// + /// Dictionary of data for a host entity's FSMs. + /// + public Dictionary HostFsmData { get; } + + /// + /// Construct the reliable entity update data. + /// + public ReliableEntityUpdate() { + UpdateTypes = new HashSet(); + GenericData = new List(); + HostFsmData = new Dictionary(); + } + + /// + public override void WriteData(IPacket packet) { + packet.Write(Id); + + // Construct the byte flag representing update types + byte updateTypeFlag = 0; + // Keep track of value of current bit + byte currentTypeValue = 1; + + for (var i = 0; i < Enum.GetNames(typeof(EntityUpdateType)).Length; i++) { + // Cast the current index of the loop to a PlayerUpdateType and check if it is + // contained in the update type list, if so, we add the current bit to the flag + if (UpdateTypes.Contains((EntityUpdateType) i)) { + updateTypeFlag |= currentTypeValue; + } + + currentTypeValue *= 2; + } + + // Write the update type flag + packet.Write(updateTypeFlag); + + if (UpdateTypes.Contains(EntityUpdateType.Active)) { + packet.Write(IsActive); + } + + if (UpdateTypes.Contains(EntityUpdateType.Data)) { + if (GenericData.Count > byte.MaxValue) { + Logger.Error("Length of entity network data instances exceeded max value of byte"); + } + + var length = (byte)System.Math.Min(GenericData.Count, byte.MaxValue); + + packet.Write(length); + for (var i = 0; i < length; i++) { + GenericData[i].WriteData(packet); + } + } + + if (UpdateTypes.Contains(EntityUpdateType.HostFsm)) { + var length = (byte) HostFsmData.Count; + packet.Write(length); + + foreach (var pair in HostFsmData) { + packet.Write(pair.Key); + + pair.Value.WriteData(packet); + } + } + } + + /// + public override void ReadData(IPacket packet) { + Id = packet.ReadUShort(); + + // Read the byte flag representing update types and reconstruct it + var updateTypeFlag = packet.ReadByte(); + // Keep track of value of current bit + var currentTypeValue = 1; + + for (var i = 0; i < Enum.GetNames(typeof(EntityUpdateType)).Length; i++) { + // If this bit was set in our flag, we add the type to the list + if ((updateTypeFlag & currentTypeValue) != 0) { + UpdateTypes.Add((EntityUpdateType) i); + } + + // Increase the value of current bit + currentTypeValue *= 2; + } + + if (UpdateTypes.Contains(EntityUpdateType.Active)) { + IsActive = packet.ReadBool(); + } + + if (UpdateTypes.Contains(EntityUpdateType.Data)) { + var length = packet.ReadByte(); + + for (var i = 0; i < length; i++) { + var entityNetworkData = new EntityNetworkData(); + entityNetworkData.ReadData(packet); + + GenericData.Add(entityNetworkData); + } + } + + if (UpdateTypes.Contains(EntityUpdateType.HostFsm)) { + var length = packet.ReadByte(); + + for (var i = 0; i < length; i++) { + var key = packet.ReadByte(); + + var data = new EntityHostFsmData(); + data.ReadData(packet); + + HostFsmData.Add(key, data); + } + } + } +} + /// /// Generic data for a networked entity. /// diff --git a/HKMP/Networking/Packet/Data/PlayerEnterScene.cs b/HKMP/Networking/Packet/Data/PlayerEnterScene.cs index 95ca9f41..ee5105b1 100644 --- a/HKMP/Networking/Packet/Data/PlayerEnterScene.cs +++ b/HKMP/Networking/Packet/Data/PlayerEnterScene.cs @@ -96,6 +96,11 @@ internal class ClientPlayerAlreadyInScene : IPacketData { /// List of entity update instances. /// public List EntityUpdateList { get; } + + /// + /// List of entity update instances. + /// + public List ReliableEntityUpdateList { get; } /// /// Whether the receiving player is scene host. @@ -109,6 +114,7 @@ public ClientPlayerAlreadyInScene() { PlayerEnterSceneList = new List(); EntitySpawnList = new List(); EntityUpdateList = new List(); + ReliableEntityUpdateList = new List(); } /// @@ -136,6 +142,14 @@ public void WriteData(IPacket packet) { for (var i = 0; i < length; i++) { EntityUpdateList[i].WriteData(packet); } + + length = System.Math.Min(ushort.MaxValue, ReliableEntityUpdateList.Count); + + packet.Write((ushort) length); + + for (var i = 0; i < length; i++) { + ReliableEntityUpdateList[i].WriteData(packet); + } packet.Write(SceneHost); } @@ -177,6 +191,18 @@ public void ReadData(IPacket packet) { // And add it to our already initialized list EntityUpdateList.Add(instance); } + + length = packet.ReadUShort(); + for (var i = 0; i < length; i++) { + // Create new instance of reliable entity update + var instance = new ReliableEntityUpdate(); + + // Read the packet data into the instance + instance.ReadData(packet); + + // And add it to our already initialized list + ReliableEntityUpdateList.Add(instance); + } SceneHost = packet.ReadBool(); } diff --git a/HKMP/Networking/Packet/PacketId.cs b/HKMP/Networking/Packet/PacketId.cs index 91015f5f..a6568acb 100644 --- a/HKMP/Networking/Packet/PacketId.cs +++ b/HKMP/Networking/Packet/PacketId.cs @@ -63,6 +63,11 @@ internal enum ClientPacketId { /// Update of realtime entity values. /// EntityUpdate, + + /// + /// Update of realtime reliable entity values. + /// + ReliableEntityUpdate, /// /// Notify that the player becomes scene host of their current scene. @@ -92,7 +97,7 @@ internal enum ClientPacketId { /// /// Player sent chat message. /// - ChatMessage = 17 + ChatMessage = 18 } /// @@ -133,6 +138,11 @@ public enum ServerPacketId { /// Update of realtime entity values. /// EntityUpdate, + + /// + /// Update of realtime reliable entity values. + /// + ReliableEntityUpdate, /// /// Notify that the player has entered a new scene. @@ -162,5 +172,5 @@ public enum ServerPacketId { /// /// Player sent chat message. /// - ChatMessage = 12 + ChatMessage = 13 } diff --git a/HKMP/Networking/Packet/UpdatePacket.cs b/HKMP/Networking/Packet/UpdatePacket.cs index e1b561f3..5738488e 100644 --- a/HKMP/Networking/Packet/UpdatePacket.cs +++ b/HKMP/Networking/Packet/UpdatePacket.cs @@ -869,6 +869,8 @@ protected override IPacketData InstantiatePacketDataFromId(ServerPacketId packet return new PacketDataCollection(); case ServerPacketId.EntityUpdate: return new PacketDataCollection(); + case ServerPacketId.ReliableEntityUpdate: + return new PacketDataCollection(); case ServerPacketId.PlayerEnterScene: return new ServerPlayerEnterScene(); case ServerPacketId.PlayerTeamUpdate: @@ -920,6 +922,8 @@ protected override IPacketData InstantiatePacketDataFromId(ClientPacketId packet return new PacketDataCollection(); case ClientPacketId.EntityUpdate: return new PacketDataCollection(); + case ClientPacketId.ReliableEntityUpdate: + return new PacketDataCollection(); case ClientPacketId.SceneHostTransfer: return new ReliableEmptyData(); case ClientPacketId.PlayerDeath: diff --git a/HKMP/Networking/Server/ServerUpdateManager.cs b/HKMP/Networking/Server/ServerUpdateManager.cs index 9a3f63df..27099a80 100644 --- a/HKMP/Networking/Server/ServerUpdateManager.cs +++ b/HKMP/Networking/Server/ServerUpdateManager.cs @@ -171,11 +171,13 @@ ushort animationClipId /// An enumerable of ClientPlayerEnterScene instances to add. /// An enumerable of EntitySpawn instances to add. /// An enumerable of EntityUpdate instances to add. + /// An enumerable of ReliableEntityUpdate instances to add. /// Whether the player is the scene host. public void AddPlayerAlreadyInSceneData( IEnumerable playerEnterSceneList, IEnumerable entitySpawnList, IEnumerable entityUpdateList, + IEnumerable reliableEntityUpdateList, bool sceneHost ) { lock (Lock) { @@ -185,6 +187,7 @@ bool sceneHost alreadyInScene.PlayerEnterSceneList.AddRange(playerEnterSceneList); alreadyInScene.EntitySpawnList.AddRange(entitySpawnList); alreadyInScene.EntityUpdateList.AddRange(entityUpdateList); + alreadyInScene.ReliableEntityUpdateList.AddRange(reliableEntityUpdateList); CurrentUpdatePacket.SetSendingPacketData(ClientPacketId.PlayerAlreadyInScene, alreadyInScene); } @@ -303,20 +306,26 @@ public void SetEntitySpawn(ushort id, EntityType spawningType, EntityType spawne /// Find or create an entity update instance in the current packet. /// /// The ID of the entity. + /// The type of the entity update. Either or + /// . /// An instance of the entity update in the packet. - private EntityUpdate FindOrCreateEntityUpdate(ushort entityId) { - EntityUpdate entityUpdate = null; - PacketDataCollection entityUpdateCollection; + private T FindOrCreateEntityUpdate(ushort entityId) where T : BaseEntityUpdate, new() { + var entityUpdate = default(T); + PacketDataCollection entityUpdateCollection; + + var packetId = typeof(T) == typeof(EntityUpdate) + ? ClientPacketId.EntityUpdate + : ClientPacketId.ReliableEntityUpdate; // First check whether there actually exists entity data at all if (CurrentUpdatePacket.TryGetSendingPacketData( - ClientPacketId.EntityUpdate, + packetId, out var packetData) ) { // And if there exists data already, try to find a match for the entity type and id - entityUpdateCollection = (PacketDataCollection) packetData; + entityUpdateCollection = (PacketDataCollection) packetData; foreach (var existingPacketData in entityUpdateCollection.DataInstances) { - var existingEntityUpdate = (EntityUpdate) existingPacketData; + var existingEntityUpdate = (T) existingPacketData; if (existingEntityUpdate.Id == entityId) { entityUpdate = existingEntityUpdate; break; @@ -324,15 +333,21 @@ private EntityUpdate FindOrCreateEntityUpdate(ushort entityId) { } } else { // If no data exists yet, we instantiate the data collection class and put it at the respective key - entityUpdateCollection = new PacketDataCollection(); - CurrentUpdatePacket.SetSendingPacketData(ClientPacketId.EntityUpdate, entityUpdateCollection); + entityUpdateCollection = new PacketDataCollection(); + CurrentUpdatePacket.SetSendingPacketData(packetId, entityUpdateCollection); } // If no existing instance was found, create one and add it to the (newly created) collection if (entityUpdate == null) { - entityUpdate = new EntityUpdate { - Id = entityId - }; + if (typeof(T) == typeof(EntityUpdate)) { + entityUpdate = (T) (object) new EntityUpdate { + Id = entityId + }; + } else { + entityUpdate = (T) (object) new ReliableEntityUpdate { + Id = entityId + }; + } entityUpdateCollection.DataInstances.Add(entityUpdate); @@ -348,7 +363,7 @@ private EntityUpdate FindOrCreateEntityUpdate(ushort entityId) { /// The position of the entity. public void UpdateEntityPosition(ushort entityId, Vector2 position) { lock (Lock) { - var entityUpdate = FindOrCreateEntityUpdate(entityId); + var entityUpdate = FindOrCreateEntityUpdate(entityId); entityUpdate.UpdateTypes.Add(EntityUpdateType.Position); entityUpdate.Position = position; @@ -362,7 +377,7 @@ public void UpdateEntityPosition(ushort entityId, Vector2 position) { /// The scale data of the entity. public void UpdateEntityScale(ushort entityId, EntityUpdate.ScaleData scale) { lock (Lock) { - var entityUpdate = FindOrCreateEntityUpdate(entityId); + var entityUpdate = FindOrCreateEntityUpdate(entityId); entityUpdate.UpdateTypes.Add(EntityUpdateType.Scale); entityUpdate.Scale = scale; @@ -377,7 +392,7 @@ public void UpdateEntityScale(ushort entityId, EntityUpdate.ScaleData scale) { /// The wrap mode of the animation of the entity. public void UpdateEntityAnimation(ushort entityId, byte animationId, byte animationWrapMode) { lock (Lock) { - var entityUpdate = FindOrCreateEntityUpdate(entityId); + var entityUpdate = FindOrCreateEntityUpdate(entityId); entityUpdate.UpdateTypes.Add(EntityUpdateType.Animation); entityUpdate.AnimationId = animationId; @@ -392,7 +407,7 @@ public void UpdateEntityAnimation(ushort entityId, byte animationId, byte animat /// Whether the entity is active or not. public void UpdateEntityIsActive(ushort entityId, bool isActive) { lock (Lock) { - var entityUpdate = FindOrCreateEntityUpdate(entityId); + var entityUpdate = FindOrCreateEntityUpdate(entityId); entityUpdate.UpdateTypes.Add(EntityUpdateType.Active); entityUpdate.IsActive = isActive; @@ -406,7 +421,7 @@ public void UpdateEntityIsActive(ushort entityId, bool isActive) { /// The list of entity network data to add. public void AddEntityData(ushort entityId, List data) { lock (Lock) { - var entityUpdate = FindOrCreateEntityUpdate(entityId); + var entityUpdate = FindOrCreateEntityUpdate(entityId); entityUpdate.UpdateTypes.Add(EntityUpdateType.Data); entityUpdate.GenericData.AddRange(data); @@ -421,7 +436,7 @@ public void AddEntityData(ushort entityId, List data) { /// The host FSM data to add. public void AddEntityHostFsmData(ushort entityId, byte fsmIndex, EntityHostFsmData data) { lock (Lock) { - var entityUpdate = FindOrCreateEntityUpdate(entityId); + var entityUpdate = FindOrCreateEntityUpdate(entityId); entityUpdate.UpdateTypes.Add(EntityUpdateType.HostFsm); From 0e16ffac633ce9d18aa227a441fa9678aa84066c Mon Sep 17 00:00:00 2001 From: Extremelyd1 Date: Sat, 29 Jul 2023 18:06:27 +0200 Subject: [PATCH 068/216] Fix out-of-order host transfer and entity updates --- HKMP/Game/Client/ClientManager.cs | 16 ++++- HKMP/Game/Client/Entity/EntityManager.cs | 72 ++++++++++++++----- HKMP/Game/Server/ServerManager.cs | 3 +- HKMP/Networking/Packet/Data/HostTransfer.cs | 27 +++++++ HKMP/Networking/Packet/UpdatePacket.cs | 2 +- HKMP/Networking/Server/ServerUpdateManager.cs | 7 +- 6 files changed, 102 insertions(+), 25 deletions(-) create mode 100644 HKMP/Networking/Packet/Data/HostTransfer.cs diff --git a/HKMP/Game/Client/ClientManager.cs b/HKMP/Game/Client/ClientManager.cs index 4bdd3ea4..ecb17da1 100644 --- a/HKMP/Game/Client/ClientManager.cs +++ b/HKMP/Game/Client/ClientManager.cs @@ -221,7 +221,7 @@ ModSettings modSettings packetManager.RegisterClientPacketHandler(ClientPacketId.EntityUpdate, OnEntityUpdate); packetManager.RegisterClientPacketHandler(ClientPacketId.ReliableEntityUpdate, OnReliableEntityUpdate); - packetManager.RegisterClientPacketHandler(ClientPacketId.SceneHostTransfer, OnSceneHostTransfer); + packetManager.RegisterClientPacketHandler(ClientPacketId.SceneHostTransfer, OnSceneHostTransfer); packetManager.RegisterClientPacketHandler(ClientPacketId.ServerSettingsUpdated, OnServerSettingsUpdated); packetManager.RegisterClientPacketHandler(ClientPacketId.ChatMessage, OnChatMessage); @@ -768,8 +768,18 @@ private void OnReliableEntityUpdate(ReliableEntityUpdate entityUpdate) { _entityManager.HandleReliableEntityUpdate(entityUpdate); } - private void OnSceneHostTransfer() { - Logger.Info("Received scene host transfer"); + /// + /// Callback method for when a host transfer is received. + /// + /// The HostTransfer packet data. + private void OnSceneHostTransfer(HostTransfer hostTransfer) { + 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(); } diff --git a/HKMP/Game/Client/Entity/EntityManager.cs b/HKMP/Game/Client/Entity/EntityManager.cs index 7fedf639..bd5a68d2 100644 --- a/HKMP/Game/Client/Entity/EntityManager.cs +++ b/HKMP/Game/Client/Entity/EntityManager.cs @@ -28,6 +28,11 @@ internal class EntityManager { /// private readonly Dictionary _entities; + /// + /// Whether the scene host is determined for this scene locally. + /// + private bool _isSceneHostDetermined; + /// /// Whether the client user is the scene host. /// @@ -56,22 +61,30 @@ public EntityManager(NetClient netClient) { /// Initializes the entity manager if we are the scene host. /// public void InitializeSceneHost() { - Logger.Info("Releasing control of all registered entities"); + Logger.Info("We are scene host, releasing control of all registered entities"); _isSceneHost = true; foreach (var entity in _entities.Values) { entity.InitializeHost(); } + + _isSceneHostDetermined = true; + + CheckReceivedUpdates(); } /// /// Initializes the entity manager if we are a scene client. /// public void InitializeSceneClient() { - Logger.Info("Taking control of all registered entities"); + Logger.Info("We are scene client, taking control of all registered entities"); _isSceneHost = false; + + _isSceneHostDetermined = true; + + CheckReceivedUpdates(); } /// @@ -137,18 +150,23 @@ public void SpawnEntity(ushort id, EntityType spawningType, EntityType spawnedTy /// /// The entity update to handle. /// Whether this is the update from the already in scene packet. - public void HandleEntityUpdate(EntityUpdate entityUpdate, bool alreadyInSceneUpdate = false) { + public bool HandleEntityUpdate(EntityUpdate entityUpdate, bool alreadyInSceneUpdate = false) { if (_isSceneHost) { - return; + return true; } - if (!_entities.TryGetValue(entityUpdate.Id, out var entity)) { - Logger.Debug($"Could not find entity ({entityUpdate.Id}) to apply update for; storing update for now"); + if (!_entities.TryGetValue(entityUpdate.Id, out var entity) || !_isSceneHostDetermined) { + if (_isSceneHostDetermined) { + Logger.Debug($"Could not find entity ({entityUpdate.Id}) to apply update for; storing update for now"); + } else { + Logger.Debug("Scene host is not determined yet to apply update; storing update for now"); + } + _receivedUpdates.Enqueue(entityUpdate); - return; + return false; } - + if (entityUpdate.UpdateTypes.Contains(EntityUpdateType.Position)) { entity.UpdatePosition(entityUpdate.Position); } @@ -164,6 +182,8 @@ public void HandleEntityUpdate(EntityUpdate entityUpdate, bool alreadyInSceneUpd alreadyInSceneUpdate ); } + + return true; } /// @@ -171,18 +191,23 @@ public void HandleEntityUpdate(EntityUpdate entityUpdate, bool alreadyInSceneUpd /// /// The reliable entity update to handle. /// Whether this is the update from the already in scene packet. - public void HandleReliableEntityUpdate(ReliableEntityUpdate entityUpdate, bool alreadyInSceneUpdate = false) { + public bool HandleReliableEntityUpdate(ReliableEntityUpdate entityUpdate, bool alreadyInSceneUpdate = false) { if (_isSceneHost) { - return; + return true; } - - if (!_entities.TryGetValue(entityUpdate.Id, out var entity)) { - Logger.Debug($"Could not find entity ({entityUpdate.Id}) to apply update for; storing update for now"); + + if (!_entities.TryGetValue(entityUpdate.Id, out var entity) || !_isSceneHostDetermined) { + if (_isSceneHostDetermined) { + Logger.Debug($"Could not find entity ({entityUpdate.Id}) to apply update for; storing update for now"); + } else { + Logger.Debug("Scene host is not determined yet to apply update; storing update for now"); + } + _receivedUpdates.Enqueue(entityUpdate); - return; + return false; } - + if (entityUpdate.UpdateTypes.Contains(EntityUpdateType.Active)) { entity.UpdateIsActive(entityUpdate.IsActive); } @@ -194,6 +219,8 @@ public void HandleReliableEntityUpdate(ReliableEntityUpdate entityUpdate, bool a if (entityUpdate.UpdateTypes.Contains(EntityUpdateType.HostFsm)) { entity.UpdateHostFsmData(entityUpdate.HostFsmData); } + + return true; } /// @@ -261,15 +288,22 @@ private bool OnGameObjectSpawned(EntitySpawnDetails details) { /// private void CheckReceivedUpdates() { while (_receivedUpdates.Count != 0) { - var update = _receivedUpdates.Dequeue(); + var update = _receivedUpdates.Peek(); if (_entities.TryGetValue(update.Id, out _)) { Logger.Debug("Found un-applied entity update, applying now"); + bool handled; if (update is EntityUpdate entityUpdate) { - HandleEntityUpdate(entityUpdate); + handled = HandleEntityUpdate(entityUpdate); } else if (update is ReliableEntityUpdate reliableEntityUpdate) { - HandleReliableEntityUpdate(reliableEntityUpdate); + handled = HandleReliableEntityUpdate(reliableEntityUpdate); + } else { + continue; + } + + if (handled) { + _receivedUpdates.Dequeue(); } } } @@ -299,6 +333,8 @@ private void OnSceneChanged(Scene oldScene, Scene newScene) { // Since we have tried finding entities in the scene, we also check whether there are un-applied updates for // those entities CheckReceivedUpdates(); + + _isSceneHostDetermined = false; } /// diff --git a/HKMP/Game/Server/ServerManager.cs b/HKMP/Game/Server/ServerManager.cs index 6fc4faf3..c580d9a1 100644 --- a/HKMP/Game/Server/ServerManager.cs +++ b/HKMP/Game/Server/ServerManager.cs @@ -444,6 +444,7 @@ private void OnClientEnterScene(ServerPlayerData playerData) { } if (!alreadyPlayersInScene) { + Logger.Debug($"No players already in scene, making {playerData.Id} the scene host"); playerData.IsSceneHost = true; } @@ -883,7 +884,7 @@ private void HandlePlayerLeaveScene(ushort id, bool disconnected, bool timeout = if (playerData.IsSceneHost) { // If the leaving player was the scene host, we can make this player the new scene host - updateManager.SetSceneHostTransfer(); + updateManager.SetSceneHostTransfer(sceneName); // Reset the scene host variable in the leaving player, so only a single other player // becomes the scene host diff --git a/HKMP/Networking/Packet/Data/HostTransfer.cs b/HKMP/Networking/Packet/Data/HostTransfer.cs new file mode 100644 index 00000000..7c12fbde --- /dev/null +++ b/HKMP/Networking/Packet/Data/HostTransfer.cs @@ -0,0 +1,27 @@ +namespace Hkmp.Networking.Packet.Data; + +/// +/// Packet data for a host transfer. +/// +internal class HostTransfer : IPacketData { + /// + public bool IsReliable => true; + + /// + public bool DropReliableDataIfNewerExists => true; + + /// + /// The name of the scene in which the player becomes the scene host. + /// + public string SceneName { get; set; } + + /// + public void WriteData(IPacket packet) { + packet.Write(SceneName); + } + + /// + public void ReadData(IPacket packet) { + SceneName = packet.ReadString(); + } +} diff --git a/HKMP/Networking/Packet/UpdatePacket.cs b/HKMP/Networking/Packet/UpdatePacket.cs index 5738488e..ce4776b0 100644 --- a/HKMP/Networking/Packet/UpdatePacket.cs +++ b/HKMP/Networking/Packet/UpdatePacket.cs @@ -925,7 +925,7 @@ protected override IPacketData InstantiatePacketDataFromId(ClientPacketId packet case ClientPacketId.ReliableEntityUpdate: return new PacketDataCollection(); case ClientPacketId.SceneHostTransfer: - return new ReliableEmptyData(); + return new HostTransfer(); case ClientPacketId.PlayerDeath: return new PacketDataCollection(); case ClientPacketId.PlayerTeamUpdate: diff --git a/HKMP/Networking/Server/ServerUpdateManager.cs b/HKMP/Networking/Server/ServerUpdateManager.cs index 27099a80..dece3730 100644 --- a/HKMP/Networking/Server/ServerUpdateManager.cs +++ b/HKMP/Networking/Server/ServerUpdateManager.cs @@ -451,9 +451,12 @@ public void AddEntityHostFsmData(ushort entityId, byte fsmIndex, EntityHostFsmDa /// /// Set that the receiving player should become scene host of their current scene. /// - public void SetSceneHostTransfer() { + /// The name of the scene in which the player becomes scene host. + public void SetSceneHostTransfer(string sceneName) { lock (Lock) { - CurrentUpdatePacket.SetSendingPacketData(ClientPacketId.SceneHostTransfer, new ReliableEmptyData()); + CurrentUpdatePacket.SetSendingPacketData(ClientPacketId.SceneHostTransfer, new HostTransfer { + SceneName = sceneName + }); } } From 0781ec9d4fc045327f2660248fcab7038c00ca8b Mon Sep 17 00:00:00 2001 From: Extremelyd1 Date: Sat, 29 Jul 2023 19:56:19 +0200 Subject: [PATCH 069/216] Add Godhome bosses Largely untested --- HKMP/Game/Client/Entity/EntityType.cs | 15 ++++++ HKMP/Resource/entity-registry.json | 75 +++++++++++++++++++++++++++ 2 files changed, 90 insertions(+) diff --git a/HKMP/Game/Client/Entity/EntityType.cs b/HKMP/Game/Client/Entity/EntityType.cs index f46b3cc5..122ba83b 100644 --- a/HKMP/Game/Client/Entity/EntityType.cs +++ b/HKMP/Game/Client/Entity/EntityType.cs @@ -209,4 +209,19 @@ internal enum EntityType { Revek, SoulTyrant, SoulTyrantPhase2, + OroMato, + Oro, + Mato, + Sheo, + Sly, + PureVessel, + PureVesselBlast, + WingedNosk, + TurretZoteling, + LankyZoteling, + HeadOfZote, + FlukeZoteling, + ZoteCurse, + HeavyZoteling, + AbsoluteRadiance } diff --git a/HKMP/Resource/entity-registry.json b/HKMP/Resource/entity-registry.json index 9df46faa..b30a1866 100644 --- a/HKMP/Resource/entity-registry.json +++ b/HKMP/Resource/entity-registry.json @@ -1246,5 +1246,80 @@ "base_object_name": "Dream Mage Lord Phase2", "type": "SoulTyrantPhase2", "fsm_name": "Mage Lord 2" + }, + { + "base_object_name": "Brothers", + "type": "OroMato", + "fsm_name": "Combo Control" + }, + { + "base_object_name": "Oro", + "type": "Oro", + "fsm_name": "nailmaster" + }, + { + "base_object_name": "Mato", + "type": "Mato", + "fsm_name": "nailmaster" + }, + { + "base_object_name": "Sheo Boss", + "type": "Sheo", + "fsm_name": "nailmaster_sheo" + }, + { + "base_object_name": "Sly Boss", + "type": "Sly", + "fsm_name": "Control" + }, + { + "base_object_name": "HK Prime", + "type": "PureVessel", + "fsm_name": "Control" + }, + { + "base_object_name": "HK Prime Blast", + "type": "PureVesselBlast", + "fsm_name": "Control" + }, + { + "base_object_name": "Hornet Nosk", + "type": "WingedNosk", + "fsm_name": "Hornet Nosk" + }, + { + "base_object_name": "Zote Turret", + "type": "TurretZoteling", + "fsm_name": "Control" + }, + { + "base_object_name": "Zote Crew Tall", + "type": "LankyZoteling", + "fsm_name": "Control" + }, + { + "base_object_name": "Zote Thwomp", + "type": "HeadOfZote", + "fsm_name": "Control" + }, + { + "base_object_name": "Zote Fluke", + "type": "FlukeZoteling", + "fsm_name": "Control" + }, + { + "base_object_name": "Zote Salubra", + "type": "ZoteCurse", + "fsm_name": "Control" + }, + { + "base_object_name": "Zote Crew Fat", + "type": "HeavyZoteling", + "fsm_name": "Control" + }, + { + "base_object_name": "Absolute Radiance", + "type": "AbsoluteRadiance", + "fsm_name": "Control" } ] From d7261392570c588202cf3908d550cec869bf9213 Mon Sep 17 00:00:00 2001 From: Extremelyd1 Date: Sun, 30 Jul 2023 16:38:52 +0200 Subject: [PATCH 070/216] Fix memory buildup issue --- .../Client/Entity/Action/FsmActionHooks.cs | 12 ++++++++++ .../Entity/Component/EnemySpawnerComponent.cs | 5 ++++ .../Entity/Component/SpawnJarComponent.cs | 15 ++++++++++-- HKMP/Game/Client/Entity/Entity.cs | 23 ++++++++++++------- 4 files changed, 45 insertions(+), 10 deletions(-) diff --git a/HKMP/Game/Client/Entity/Action/FsmActionHooks.cs b/HKMP/Game/Client/Entity/Action/FsmActionHooks.cs index ff327665..3dcafb9b 100644 --- a/HKMP/Game/Client/Entity/Action/FsmActionHooks.cs +++ b/HKMP/Game/Client/Entity/Action/FsmActionHooks.cs @@ -78,6 +78,11 @@ private static void OnSceneChanged(Scene oldScene, Scene newScene) { } Hooks.Clear(); + + TypeEvents.Clear(); + // foreach (var actionHook in TypeEvents.Values) { + // actionHook.Clear(); + // } } /// @@ -96,5 +101,12 @@ private class FsmActionHook { public void InvokeEvent(FsmStateAction fsmStateAction) { HookEvent?.Invoke(fsmStateAction); } + + /// + /// Clear all subscriptions to the hook event. + /// + public void Clear() { + HookEvent = null; + } } } diff --git a/HKMP/Game/Client/Entity/Component/EnemySpawnerComponent.cs b/HKMP/Game/Client/Entity/Component/EnemySpawnerComponent.cs index ff7f5aec..efe7a690 100644 --- a/HKMP/Game/Client/Entity/Component/EnemySpawnerComponent.cs +++ b/HKMP/Game/Client/Entity/Component/EnemySpawnerComponent.cs @@ -83,5 +83,10 @@ public override void Update(EntityNetworkData data) { /// public override void Destroy() { + On.EnemySpawner.Start -= EnemySpawnerOnStart; + + if (_spawner.Host != null) { + _spawner.Host.OnEnemySpawned -= OnEnemySpawned; + } } } \ No newline at end of file diff --git a/HKMP/Game/Client/Entity/Component/SpawnJarComponent.cs b/HKMP/Game/Client/Entity/Component/SpawnJarComponent.cs index 9d60d7f0..e761f60e 100644 --- a/HKMP/Game/Client/Entity/Component/SpawnJarComponent.cs +++ b/HKMP/Game/Client/Entity/Component/SpawnJarComponent.cs @@ -35,7 +35,7 @@ HostClientPair spawnJar _spawnJar = spawnJar; spawnJar.Client.enabled = false; - On.SpawnJarControl.OnEnable += SpawnJarControlOnOnEnable; + On.SpawnJarControl.OnEnable += SpawnJarControlOnEnable; // We can't simply hook the Behaviour method itself because it returns a state machine for the IEnumerator // Instead we get the state machine target with MonoMod and get the hook that way @@ -55,7 +55,7 @@ HostClientPair spawnJar /// /// Hook on the OnEnable method of the SpawnJarControl to network that it should start on the client-side. /// - private void SpawnJarControlOnOnEnable(On.SpawnJarControl.orig_OnEnable orig, SpawnJarControl self) { + private void SpawnJarControlOnEnable(On.SpawnJarControl.orig_OnEnable orig, SpawnJarControl self) { orig(self); if (self != _spawnJar.Host) { @@ -165,5 +165,16 @@ IEnumerator Behaviour() { /// public override void Destroy() { + On.SpawnJarControl.OnEnable -= SpawnJarControlOnEnable; + + HookEndpointManager.Unmodify( + MonoMod.Utils.Extensions.GetStateMachineTarget( + ReflectionHelper.GetMethodInfo( + typeof(SpawnJarControl), + "Behaviour" + ) + ), + SpawnJarControlOnBehaviour + ); } } \ No newline at end of file diff --git a/HKMP/Game/Client/Entity/Entity.cs b/HKMP/Game/Client/Entity/Entity.cs index a52e6365..d5781724 100644 --- a/HKMP/Game/Client/Entity/Entity.cs +++ b/HKMP/Game/Client/Entity/Entity.cs @@ -189,14 +189,7 @@ params EntityComponentType[] types } // Always disallow the client object from being recycled, because it will simply be destroyed - On.ObjectPool.Recycle_GameObject += (orig, obj) => { - if (obj == Object.Client) { - Logger.Debug($"Client object of entity: {Id}, {type} tried to be recycled"); - return; - } - - orig(obj); - }; + On.ObjectPool.Recycle_GameObject += ObjectPoolOnRecycleGameObject; _fsms = new HostClientPair> { Host = Object.Host.GetComponents().ToList(), @@ -756,6 +749,19 @@ float overrideFps (byte)clip.wrapMode ); } + + /// + /// Callback method for when a game object is recycled. Used to prevent client objects from being recycled, which + /// shouldn't happen because they are instantiated manually instead of from a pool. + /// + private void ObjectPoolOnRecycleGameObject(On.ObjectPool.orig_Recycle_GameObject orig, GameObject obj) { + if (obj == Object.Client) { + Logger.Debug($"Client object of entity: {Id}, {Type} tried to be recycled"); + return; + } + + orig(obj); + } /// /// Initializes the entity when the client user is the scene host. @@ -1241,6 +1247,7 @@ Action setValueAction public void Destroy() { MonoBehaviourUtil.Instance.OnUpdateEvent -= OnUpdate; On.tk2dSpriteAnimator.Play_tk2dSpriteAnimationClip_float_float -= OnAnimationPlayed; + On.ObjectPool.Recycle_GameObject -= ObjectPoolOnRecycleGameObject; foreach (var component in _components.Values.Distinct()) { component.Destroy(); From b5f4693cc6f3621e08d7013a678752f978f7107c Mon Sep 17 00:00:00 2001 From: Extremelyd1 Date: Sun, 30 Jul 2023 18:26:23 +0200 Subject: [PATCH 071/216] Fix hooks not applying correctly --- .../Client/Entity/Action/FsmActionHooks.cs | 19 +++++++++---------- HKMP/Game/Client/Entity/EntityManager.cs | 3 ++- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/HKMP/Game/Client/Entity/Action/FsmActionHooks.cs b/HKMP/Game/Client/Entity/Action/FsmActionHooks.cs index 3dcafb9b..45fd476b 100644 --- a/HKMP/Game/Client/Entity/Action/FsmActionHooks.cs +++ b/HKMP/Game/Client/Entity/Action/FsmActionHooks.cs @@ -19,12 +19,18 @@ internal static class FsmActionHooks { /// /// List of all registered hooks. Used to loop over and remove all. /// + // ReSharper disable once CollectionNeverQueried.Local private static readonly List Hooks; static FsmActionHooks() { TypeEvents = new Dictionary(); Hooks = new List(); - + } + + /// + /// Initialize this class by registering the scene changed event. + /// + public static void Initialize() { UnityEngine.SceneManagement.SceneManager.activeSceneChanged += OnSceneChanged; } @@ -73,16 +79,9 @@ private static void OnActionEntered(Action orig, FsmStateAction /// The old scene. /// The new scene. private static void OnSceneChanged(Scene oldScene, Scene newScene) { - foreach (var hook in Hooks) { - hook.Dispose(); + foreach (var actionHook in TypeEvents.Values) { + actionHook.Clear(); } - - Hooks.Clear(); - - TypeEvents.Clear(); - // foreach (var actionHook in TypeEvents.Values) { - // actionHook.Clear(); - // } } /// diff --git a/HKMP/Game/Client/Entity/EntityManager.cs b/HKMP/Game/Client/Entity/EntityManager.cs index bd5a68d2..e4e7db34 100644 --- a/HKMP/Game/Client/Entity/EntityManager.cs +++ b/HKMP/Game/Client/Entity/EntityManager.cs @@ -51,6 +51,7 @@ public EntityManager(NetClient netClient) { _receivedUpdates = new Queue(); EntityProcessor.Initialize(_entities, netClient); + FsmActionHooks.Initialize(); EntityFsmActions.EntitySpawnEvent += OnGameObjectSpawned; UnityEngine.SceneManagement.SceneManager.sceneLoaded += OnSceneLoaded; @@ -316,7 +317,7 @@ private void CheckReceivedUpdates() { /// The old scene. /// The new scene. private void OnSceneChanged(Scene oldScene, Scene newScene) { - Logger.Info($"Scene changed, clearing registered entities"); + Logger.Info("Scene changed, clearing registered entities"); foreach (var entity in _entities.Values) { entity.Destroy(); From d83211b4550f1bb1f305d5aa4f83aadc584419f6 Mon Sep 17 00:00:00 2001 From: Extremelyd1 Date: Mon, 9 Oct 2023 18:48:19 +0200 Subject: [PATCH 072/216] Fix nail-based attacks not dealing damage to remote entities --- HKMP/Animation/AnimationEffect.cs | 7 ++- HKMP/Animation/Effects/CycloneSlash.cs | 4 +- HKMP/Animation/Effects/DashSlash.cs | 49 +++++++--------- HKMP/Animation/Effects/GreatSlash.cs | 26 +++++---- HKMP/Animation/Effects/SlashBase.cs | 74 ++++++++----------------- HKMP/Ui/Component/ChatInputComponent.cs | 1 + 6 files changed, 67 insertions(+), 94 deletions(-) diff --git a/HKMP/Animation/AnimationEffect.cs b/HKMP/Animation/AnimationEffect.cs index c430c28c..af434d0d 100644 --- a/HKMP/Animation/AnimationEffect.cs +++ b/HKMP/Animation/AnimationEffect.cs @@ -30,7 +30,8 @@ public void SetServerSettings(ServerSettings serverSettings) { /// player taking knock back from remote players hitting shields etc. /// /// The target GameObject to change. - protected static void ChangeAttackTypeOfFsm(GameObject targetObject) { + /// The direction in float that the damage is coming from. + protected static void ChangeAttackTypeOfFsm(GameObject targetObject, float direction) { var damageFsm = targetObject.LocateMyFSM("damages_enemy"); if (damageFsm == null) { return; @@ -42,5 +43,9 @@ protected static void ChangeAttackTypeOfFsm(GameObject targetObject) { 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/Effects/CycloneSlash.cs b/HKMP/Animation/Effects/CycloneSlash.cs index c6c356aa..f17a5d55 100644 --- a/HKMP/Animation/Effects/CycloneSlash.cs +++ b/HKMP/Animation/Effects/CycloneSlash.cs @@ -50,10 +50,10 @@ public override void Play(GameObject playerObject, bool[] effectInfo) { cycloneSlash.layer = 17; var hitLComponent = cycloneSlash.FindGameObjectInChildren("Hit L"); - ChangeAttackTypeOfFsm(hitLComponent); + ChangeAttackTypeOfFsm(hitLComponent, 0f); var hitRComponent = cycloneSlash.FindGameObjectInChildren("Hit R"); - ChangeAttackTypeOfFsm(hitRComponent); + ChangeAttackTypeOfFsm(hitRComponent, 180f); cycloneSlash.SetActive(true); diff --git a/HKMP/Animation/Effects/DashSlash.cs b/HKMP/Animation/Effects/DashSlash.cs index 34eb384d..e05b89df 100644 --- a/HKMP/Animation/Effects/DashSlash.cs +++ b/HKMP/Animation/Effects/DashSlash.cs @@ -35,11 +35,13 @@ 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 playerScaleX = playerObject.transform.localScale.x; + var localScale = playerObject.transform.localScale; + var playerScaleX = localScale.x; var dashSlashTransform = dashSlash.transform; var dashSlashScale = dashSlashTransform.localScale; dashSlashTransform.localScale = new Vector3( @@ -48,18 +50,9 @@ public override void Play(GameObject playerObject, bool[] effectInfo) { dashSlashScale.z ); - dashSlash.layer = 22; - - ChangeAttackTypeOfFsm(dashSlash); - - // // Get the "damages_enemy" FSM from the dash slash object - // var slashFsm = dashSlash.LocateMyFSM("damages_enemy"); - // // Find the variable that controls the slash direction for damaging enemies - // var directionVar = slashFsm.FsmVariables.GetFsmFloat("direction"); - // - // // Set it based on the direction the knight is facing - // var facingRight = playerScaleX > 0; - // directionVar.Value = facingRight ? 180f : 0f; + // Check which direction the knight is facing for the damages_enemy FSM + var facingRight = localScale.x > 0; + ChangeAttackTypeOfFsm(dashSlash, facingRight ? 180f : 0f); dashSlash.SetActive(true); @@ -68,38 +61,36 @@ 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"); + var controlColliderFsm = dashSlash.LocateMyFSM("Control Collider"); + 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)); + pvpCollider.transform.SetParent(dashSlash.transform); + 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/GreatSlash.cs b/HKMP/Animation/Effects/GreatSlash.cs index 4b5735d2..15f382cb 100644 --- a/HKMP/Animation/Effects/GreatSlash.cs +++ b/HKMP/Animation/Effects/GreatSlash.cs @@ -37,18 +37,11 @@ public override void Play(GameObject playerObject, bool[] effectInfo) { playerAttacks.transform ); greatSlash.layer = 17; - - ChangeAttackTypeOfFsm(greatSlash); - // Get the "damages_enemy" FSM from the great slash object - var slashFsm = greatSlash.LocateMyFSM("damages_enemy"); - // Find the variable that controls the slash direction for damaging enemies - var directionVar = slashFsm.FsmVariables.GetFsmFloat("direction"); - - // Set it based on the direction the knight is facing + // Check which direction the knight is facing for the damages_enemy FSM var facingRight = playerObject.transform.localScale.x > 0; - directionVar.Value = facingRight ? 180f : 0f; - + ChangeAttackTypeOfFsm(greatSlash, facingRight ? 180f : 0f); + greatSlash.SetActive(true); // Set the newly instantiate collider to state Init, to reset it @@ -57,12 +50,21 @@ 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)); + pvpCollider.transform.SetParent(greatSlash.transform); + 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/SlashBase.cs b/HKMP/Animation/Effects/SlashBase.cs index 516d7787..5461c248 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; + } + + ChangeAttackTypeOfFsm(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]; @@ -147,69 +160,30 @@ protected void Play(GameObject playerObject, bool[] effectInfo, GameObject prefa slash.GetComponent().enabled = true; 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); - - // Get the "damages_enemy" FSM from the slash object - var slashFsm = enemySlash.LocateMyFSM("damages_enemy"); - // Find the variable that controls the slash direction for damaging enemies - var directionVar = slashFsm.FsmVariables.GetFsmFloat("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; - directionVar.Value = facingRight ? 180f : 0f; - } else if (type is SlashType.Up) { - directionVar.Value = 90f; - } else { - directionVar.Value = 270f; - } - } - 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)); + pvpCollider.transform.SetParent(slash.transform); + 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/Ui/Component/ChatInputComponent.cs b/HKMP/Ui/Component/ChatInputComponent.cs index 1c2b98c9..026d24a3 100644 --- a/HKMP/Ui/Component/ChatInputComponent.cs +++ b/HKMP/Ui/Component/ChatInputComponent.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using Hkmp.Networking.Packet.Data; using Hkmp.Ui.Resources; using Hkmp.Util; From 13213a8f04abe1465f448bd793986ff4203906a2 Mon Sep 17 00:00:00 2001 From: Extremelyd1 Date: Sun, 15 Oct 2023 13:37:40 +0200 Subject: [PATCH 073/216] Allow slashes to interact with remote worlds --- HKMP/Animation/Effects/CycloneSlash.cs | 4 ++++ HKMP/Animation/Effects/DashSlash.cs | 26 +++++++++++++++++++++++--- HKMP/Animation/Effects/GreatSlash.cs | 11 ++++++++++- HKMP/Animation/Effects/SlashBase.cs | 11 ++++++++++- 4 files changed, 47 insertions(+), 5 deletions(-) diff --git a/HKMP/Animation/Effects/CycloneSlash.cs b/HKMP/Animation/Effects/CycloneSlash.cs index f17a5d55..67fcc713 100644 --- a/HKMP/Animation/Effects/CycloneSlash.cs +++ b/HKMP/Animation/Effects/CycloneSlash.cs @@ -55,6 +55,10 @@ public override void Play(GameObject playerObject, bool[] effectInfo) { var hitRComponent = cycloneSlash.FindGameObjectInChildren("Hit R"); ChangeAttackTypeOfFsm(hitRComponent, 180f); + // Add rigid body and set it to be kinematic so it doesn't do physics, but still counts certain collisions + var rigidBody = cycloneSlash.AddComponent(); + rigidBody.isKinematic = true; + cycloneSlash.SetActive(true); // Set a name, so we can reference it later when we need to destroy it diff --git a/HKMP/Animation/Effects/DashSlash.cs b/HKMP/Animation/Effects/DashSlash.cs index e05b89df..eefa1097 100644 --- a/HKMP/Animation/Effects/DashSlash.cs +++ b/HKMP/Animation/Effects/DashSlash.cs @@ -51,9 +51,25 @@ public override void Play(GameObject playerObject, bool[] effectInfo) { ); // Check which direction the knight is facing for the damages_enemy FSM - var facingRight = localScale.x > 0; + var facingRight = playerScaleX > 0; ChangeAttackTypeOfFsm(dashSlash, facingRight ? 180f : 0f); + // Add rigid body and set it to be kinematic so it doesn't do physics, but still counts certain collisions + var rigidBody = dashSlash.AddComponent(); + rigidBody.isKinematic = true; + + 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); // Remove audio source component that exists on the dash slash object @@ -61,7 +77,6 @@ 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 - var controlColliderFsm = dashSlash.LocateMyFSM("Control Collider"); if (controlColliderFsm.ActiveStateName != "Init") { controlColliderFsm.SetState("Init"); } @@ -70,7 +85,12 @@ public override void Play(GameObject playerObject, bool[] effectInfo) { if (ServerSettings.IsPvpEnabled && ShouldDoDamage) { // 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)); - pvpCollider.transform.SetParent(dashSlash.transform); + + 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; diff --git a/HKMP/Animation/Effects/GreatSlash.cs b/HKMP/Animation/Effects/GreatSlash.cs index 15f382cb..5db1105d 100644 --- a/HKMP/Animation/Effects/GreatSlash.cs +++ b/HKMP/Animation/Effects/GreatSlash.cs @@ -42,6 +42,10 @@ public override void Play(GameObject playerObject, bool[] effectInfo) { var facingRight = playerObject.transform.localScale.x > 0; ChangeAttackTypeOfFsm(greatSlash, facingRight ? 180f : 0f); + // Add rigid body and set it to be kinematic so it doesn't do physics, but still counts certain collisions + var rigidBody = greatSlash.AddComponent(); + rigidBody.isKinematic = true; + greatSlash.SetActive(true); // Set the newly instantiate collider to state Init, to reset it @@ -52,7 +56,12 @@ public override void Play(GameObject playerObject, bool[] effectInfo) { 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)); - pvpCollider.transform.SetParent(greatSlash.transform); + + 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; diff --git a/HKMP/Animation/Effects/SlashBase.cs b/HKMP/Animation/Effects/SlashBase.cs index 5461c248..88ceea05 100644 --- a/HKMP/Animation/Effects/SlashBase.cs +++ b/HKMP/Animation/Effects/SlashBase.cs @@ -88,6 +88,10 @@ protected void Play(GameObject playerObject, bool[] effectInfo, GameObject prefa var originalNailSlash = slash.GetComponent(); Object.Destroy(originalNailSlash); + // Add rigid body and set it to be kinematic so it doesn't do physics, but still counts certain collisions + var rigidBody = slash.AddComponent(); + rigidBody.isKinematic = true; + slash.SetActive(true); // Get the slash audio source and its clip @@ -166,7 +170,12 @@ protected void Play(GameObject playerObject, bool[] effectInfo, GameObject prefa 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)); - pvpCollider.transform.SetParent(slash.transform); + + 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; From 88ad3e1c563b486b35ebe0195cad75e88838d550 Mon Sep 17 00:00:00 2001 From: Extremelyd1 Date: Sun, 12 Nov 2023 15:52:10 +0100 Subject: [PATCH 074/216] Synchronise configurable PlayerData values --- HKMP/Game/Client/ClientManager.cs | 4 +- HKMP/Game/Client/Entity/EntityManager.cs | 36 +- HKMP/Game/Client/Save/SaveManager.cs | 255 ++++ HKMP/Game/Server/ServerManager.cs | 29 + HKMP/HKMP.csproj | 1 + HKMP/Networking/Client/ClientUpdateManager.cs | 23 + HKMP/Networking/Packet/Data/SaveUpdate.cs | 44 + HKMP/Networking/Packet/PacketId.cs | 72 +- HKMP/Networking/Packet/UpdatePacket.cs | 4 + HKMP/Networking/Server/ServerUpdateManager.cs | 23 + HKMP/Resource/save-data.json | 1276 +++++++++++++++++ 11 files changed, 1717 insertions(+), 50 deletions(-) create mode 100644 HKMP/Game/Client/Save/SaveManager.cs create mode 100644 HKMP/Networking/Packet/Data/SaveUpdate.cs create mode 100644 HKMP/Resource/save-data.json diff --git a/HKMP/Game/Client/ClientManager.cs b/HKMP/Game/Client/ClientManager.cs index a03ea541..5f2089ab 100644 --- a/HKMP/Game/Client/ClientManager.cs +++ b/HKMP/Game/Client/ClientManager.cs @@ -6,6 +6,7 @@ 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; @@ -179,7 +180,8 @@ ModSettings modSettings _mapManager = new MapManager(netClient, serverSettings); _entityManager = new EntityManager(netClient); - + + new SaveManager(netClient, packetManager, _entityManager).Initialize(); new PauseManager(netClient).RegisterHooks(); new FsmPatcher().RegisterHooks(); diff --git a/HKMP/Game/Client/Entity/EntityManager.cs b/HKMP/Game/Client/Entity/EntityManager.cs index e4e7db34..63a29ad6 100644 --- a/HKMP/Game/Client/Entity/EntityManager.cs +++ b/HKMP/Game/Client/Entity/EntityManager.cs @@ -31,12 +31,12 @@ internal class EntityManager { /// /// Whether the scene host is determined for this scene locally. /// - private bool _isSceneHostDetermined; + public bool IsSceneHostDetermined { get; private set; } /// /// Whether the client user is the scene host. /// - private bool _isSceneHost; + public bool IsSceneHost { get; private set; } /// /// Queue of entity updates that have not been applied yet because of a missing entity. @@ -64,13 +64,13 @@ public EntityManager(NetClient netClient) { public void InitializeSceneHost() { Logger.Info("We are scene host, releasing control of all registered entities"); - _isSceneHost = true; + IsSceneHost = true; foreach (var entity in _entities.Values) { entity.InitializeHost(); } - _isSceneHostDetermined = true; + IsSceneHostDetermined = true; CheckReceivedUpdates(); } @@ -81,9 +81,9 @@ public void InitializeSceneHost() { public void InitializeSceneClient() { Logger.Info("We are scene client, taking control of all registered entities"); - _isSceneHost = false; + IsSceneHost = false; - _isSceneHostDetermined = true; + IsSceneHostDetermined = true; CheckReceivedUpdates(); } @@ -94,7 +94,7 @@ public void InitializeSceneClient() { public void BecomeSceneHost() { Logger.Info("Becoming scene host"); - _isSceneHost = true; + IsSceneHost = true; foreach (var entity in _entities.Values) { entity.MakeHost(); @@ -136,7 +136,7 @@ public void SpawnEntity(ushort id, EntityType spawningType, EntityType spawnedTy var processor = new EntityProcessor { GameObject = spawnedObject, - IsSceneHost = _isSceneHost, + IsSceneHost = IsSceneHost, LateLoad = true, SpawnedId = id }.Process(); @@ -152,12 +152,12 @@ public void SpawnEntity(ushort id, EntityType spawningType, EntityType spawnedTy /// The entity update to handle. /// Whether this is the update from the already in scene packet. public bool HandleEntityUpdate(EntityUpdate entityUpdate, bool alreadyInSceneUpdate = false) { - if (_isSceneHost) { + if (IsSceneHost) { return true; } - if (!_entities.TryGetValue(entityUpdate.Id, out var entity) || !_isSceneHostDetermined) { - if (_isSceneHostDetermined) { + if (!_entities.TryGetValue(entityUpdate.Id, out var entity) || !IsSceneHostDetermined) { + if (IsSceneHostDetermined) { Logger.Debug($"Could not find entity ({entityUpdate.Id}) to apply update for; storing update for now"); } else { Logger.Debug("Scene host is not determined yet to apply update; storing update for now"); @@ -193,12 +193,12 @@ public bool HandleEntityUpdate(EntityUpdate entityUpdate, bool alreadyInSceneUpd /// The reliable entity update to handle. /// Whether this is the update from the already in scene packet. public bool HandleReliableEntityUpdate(ReliableEntityUpdate entityUpdate, bool alreadyInSceneUpdate = false) { - if (_isSceneHost) { + if (IsSceneHost) { return true; } - if (!_entities.TryGetValue(entityUpdate.Id, out var entity) || !_isSceneHostDetermined) { - if (_isSceneHostDetermined) { + if (!_entities.TryGetValue(entityUpdate.Id, out var entity) || !IsSceneHostDetermined) { + if (IsSceneHostDetermined) { Logger.Debug($"Could not find entity ({entityUpdate.Id}) to apply update for; storing update for now"); } else { Logger.Debug("Scene host is not determined yet to apply update; storing update for now"); @@ -237,7 +237,7 @@ private bool OnGameObjectSpawned(EntitySpawnDetails details) { var processor = new EntityProcessor { GameObject = details.GameObject, - IsSceneHost = _isSceneHost, + IsSceneHost = IsSceneHost, LateLoad = true }.Process(); @@ -245,7 +245,7 @@ private bool OnGameObjectSpawned(EntitySpawnDetails details) { return false; } - if (!_isSceneHost) { + if (!IsSceneHost) { Logger.Warn("Game object was spawned while not scene host, this shouldn't happen"); return false; } @@ -335,7 +335,7 @@ private void OnSceneChanged(Scene oldScene, Scene newScene) { // those entities CheckReceivedUpdates(); - _isSceneHostDetermined = false; + IsSceneHostDetermined = false; } /// @@ -443,7 +443,7 @@ private void FindEntitiesInScene(Scene scene, bool lateLoad) { foreach (var obj in objectsToCheck) { new EntityProcessor { GameObject = obj, - IsSceneHost = _isSceneHost, + IsSceneHost = IsSceneHost, LateLoad = lateLoad }.Process(); } diff --git a/HKMP/Game/Client/Save/SaveManager.cs b/HKMP/Game/Client/Save/SaveManager.cs new file mode 100644 index 00000000..f3c476e2 --- /dev/null +++ b/HKMP/Game/Client/Save/SaveManager.cs @@ -0,0 +1,255 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Hkmp.Collection; +using Hkmp.Game.Client.Entity; +using Hkmp.Networking.Client; +using Hkmp.Networking.Packet; +using Hkmp.Networking.Packet.Data; +using Hkmp.Util; +using Modding; +using UnityEngine; +using Logger = Hkmp.Logging.Logger; + +namespace Hkmp.Game.Client.Save; + +/// +/// Class that manages save data synchronisation. +/// +internal class SaveManager { + /// + /// The file path of the embedded resource file for save data. + /// + private const string SaveDataFilePath = "Hkmp.Resource.save-data.json"; + + /// + /// The net client instance to send save updates. + /// + private readonly NetClient _netClient; + /// + /// The packet manager instance to register a callback for when save updates are received. + /// + private readonly PacketManager _packetManager; + /// + /// The entity manager to check whether we are scene host. + /// + private readonly EntityManager _entityManager; + + /// + /// Dictionary mapping save data values to booleans indicating whether they should be synchronised. + /// + private Dictionary _saveDataValues; + + /// + /// Bi-directional lookup that maps save data names and their indices. + /// + private BiLookup _saveDataIndices; + + public SaveManager(NetClient netClient, PacketManager packetManager, EntityManager entityManager) { + _netClient = netClient; + _packetManager = packetManager; + _entityManager = entityManager; + } + + /// + /// Initializes the save manager by loading the save data json. + /// + public void Initialize() { + _saveDataValues = FileUtil.LoadObjectFromEmbeddedJson>(SaveDataFilePath); + if (_saveDataValues == null) { + Logger.Warn("Could not load save data json"); + return; + } + + _saveDataIndices = new BiLookup(); + ushort index = 0; + foreach (var saveDataName in _saveDataValues.Keys) { + Logger.Info($"Saving ({saveDataName}, {index}) in bi-lookup"); + _saveDataIndices.Add(saveDataName, index++); + } + + ModHooks.SetPlayerBoolHook += OnSetPlayerBoolHook; + ModHooks.SetPlayerFloatHook += OnSetPlayerFloatHook; + ModHooks.SetPlayerIntHook += OnSetPlayerIntHook; + ModHooks.SetPlayerStringHook += OnSetPlayerStringHook; + ModHooks.SetPlayerVariableHook += OnSetPlayerVariableHook; + ModHooks.SetPlayerVector3Hook += OnSetPlayerVector3Hook; + + _packetManager.RegisterClientPacketHandler(ClientPacketId.SaveUpdate, UpdateSaveWithData); + } + + /// + /// Callback method for when a boolean is set in the player data. + /// + /// Name of the boolean variable. + /// The original value of the boolean. + private bool OnSetPlayerBoolHook(string name, bool orig) { + CheckSendSaveUpdate(name, () => { + return new[] { (byte) (orig ? 0 : 1) }; + }); + + return orig; + } + + /// + /// Callback method for when a float is set in the player data. + /// + /// Name of the float variable. + /// The original value of the float. + private float OnSetPlayerFloatHook(string name, float orig) { + CheckSendSaveUpdate(name, () => BitConverter.GetBytes(orig)); + + return orig; + } + + /// + /// Callback method for when a int is set in the player data. + /// + /// Name of the int variable. + /// The original value of the int. + private int OnSetPlayerIntHook(string name, int orig) { + CheckSendSaveUpdate(name, () => BitConverter.GetBytes(orig)); + + return orig; + } + + /// + /// Callback method for when a string is set in the player data. + /// + /// Name of the string variable. + /// The original value of the boolean. + private string OnSetPlayerStringHook(string name, string res) { + CheckSendSaveUpdate(name, () => { + var byteEncodedString = Encoding.UTF8.GetBytes(res); + + if (byteEncodedString.Length > ushort.MaxValue) { + throw new Exception($"Could not encode string of length: {byteEncodedString.Length}"); + } + + var value = BitConverter.GetBytes((ushort) byteEncodedString.Length) + .Concat(byteEncodedString) + .ToArray(); + return value; + }); + + return res; + } + + /// + /// Callback method for when an object is set in the player data. + /// + /// The type of the object. + /// Name of the object variable. + /// The original value of the object. + private object OnSetPlayerVariableHook(Type type, string name, object value) { + throw new NotImplementedException($"Object with type: {value.GetType()} could not be encoded"); + } + + /// + /// Callback method for when a vector3 is set in the player data. + /// + /// Name of the vector3 variable. + /// The original value of the vector3. + private Vector3 OnSetPlayerVector3Hook(string name, Vector3 orig) { + CheckSendSaveUpdate(name, () => + BitConverter.GetBytes(orig.x) + .Concat(BitConverter.GetBytes(orig.y)) + .Concat(BitConverter.GetBytes(orig.z)) + .ToArray() + ); + + return orig; + } + + /// + /// Checks if a save update should be sent and send it using the encode function to encode the value of the + /// changed variable. + /// + /// The name of the variable that was changed. + /// Function that encodes the value of the variable into a byte array. + private void CheckSendSaveUpdate(string name, Func encodeFunc) { + if (!_entityManager.IsSceneHost) { + Logger.Info($"Not scene host, not sending save update ({name})"); + return; + } + + if (!_saveDataValues.TryGetValue(name, out var value) || !value) { + Logger.Info($"Not in save data values or false in save data values, not sending save update ({name})"); + return; + } + + if (!_saveDataIndices.TryGetValue(name, out var index)) { + Logger.Info($"Cannot find save data index, not sending save update ({name})"); + return; + } + + Logger.Info($"Sending \"{name}\" as save update"); + + _netClient.UpdateManager.SetSaveUpdate( + index, + encodeFunc.Invoke() + ); + } + + /// + /// Callback method for when a save update is received. + /// + /// The save update that was received. + private void UpdateSaveWithData(SaveUpdate saveUpdate) { + if (!_saveDataIndices.TryGetValue(saveUpdate.SaveDataIndex, out var name)) { + Logger.Warn($"Received save update with unknown index: {saveUpdate.SaveDataIndex}"); + return; + } + + Logger.Info($"Received save update for index: {saveUpdate.SaveDataIndex}"); + + var pd = PlayerData.instance; + + var fieldInfo = typeof(PlayerData).GetField(name); + var type = fieldInfo.FieldType; + var valueLength = saveUpdate.Value.Length; + + if (type == typeof(bool)) { + if (valueLength != 1) { + Logger.Warn($"Received save update with incorrect value length for bool: {valueLength}"); + } + + var value = saveUpdate.Value[0] == 1; + + pd.SetBoolInternal(name, value); + } else if (type == typeof(float)) { + if (valueLength != 4) { + Logger.Warn($"Received save update with incorrect value length for float: {valueLength}"); + } + + var value = BitConverter.ToSingle(saveUpdate.Value, 0); + + pd.SetFloatInternal(name, value); + } else if (type == typeof(int)) { + if (valueLength != 4) { + Logger.Warn($"Received save update with incorrect value length for int: {valueLength}"); + } + + var value = BitConverter.ToInt32(saveUpdate.Value, 0); + + pd.SetIntInternal(name, value); + } else if (type == typeof(string)) { + var value = Encoding.UTF8.GetString(saveUpdate.Value); + + pd.SetStringInternal(name, value); + } else if (type == typeof(Vector3)) { + if (valueLength != 12) { + Logger.Warn($"Received save update with incorrect value length for vector3: {valueLength}"); + } + + var value = new Vector3( + BitConverter.ToSingle(saveUpdate.Value, 0), + BitConverter.ToSingle(saveUpdate.Value, 4), + BitConverter.ToSingle(saveUpdate.Value, 8) + ); + + pd.SetVector3Internal(name, value); + } + } +} diff --git a/HKMP/Game/Server/ServerManager.cs b/HKMP/Game/Server/ServerManager.cs index 6b5b9e48..9dd73f0d 100644 --- a/HKMP/Game/Server/ServerManager.cs +++ b/HKMP/Game/Server/ServerManager.cs @@ -147,6 +147,7 @@ PacketManager packetManager packetManager.RegisterServerPacketHandler(ServerPacketId.PlayerSkinUpdate, OnPlayerSkinUpdate); packetManager.RegisterServerPacketHandler(ServerPacketId.ChatMessage, OnChatMessage); + packetManager.RegisterServerPacketHandler(ServerPacketId.SaveUpdate, OnSaveUpdate); // Register a timeout handler _netServer.ClientTimeoutEvent += OnClientTimeout; @@ -1277,6 +1278,34 @@ private void OnChatMessage(ushort id, ChatMessage chatMessage) { } } + /// + /// Callback method for when a save update is received from a player. + /// + /// The ID of the player. + /// The SaveUpdate packet data. + private void OnSaveUpdate(ushort id, SaveUpdate packet) { + if (!_playerData.TryGetValue(id, out var playerData)) { + Logger.Debug($"Could not process save update from unknown player ID: {id}"); + return; + } + + Logger.Info($"Save update from ({id}, {playerData.Username}), index: {packet.SaveDataIndex}"); + + if (!playerData.IsSceneHost) { + Logger.Info(" Player is not scene host, not broadcasting update"); + return; + } + + foreach (var idPlayerDataPair in _playerData) { + var otherId = idPlayerDataPair.Key; + if (id == otherId) { + continue; + } + + _netServer.GetUpdateManagerForClient(otherId).SetSaveUpdate(packet.SaveDataIndex, packet.Value); + } + } + #endregion #region IServerManager methods diff --git a/HKMP/HKMP.csproj b/HKMP/HKMP.csproj index b30b825a..700d8820 100644 --- a/HKMP/HKMP.csproj +++ b/HKMP/HKMP.csproj @@ -21,6 +21,7 @@ + diff --git a/HKMP/Networking/Client/ClientUpdateManager.cs b/HKMP/Networking/Client/ClientUpdateManager.cs index 6299bfcf..ccc8d645 100644 --- a/HKMP/Networking/Client/ClientUpdateManager.cs +++ b/HKMP/Networking/Client/ClientUpdateManager.cs @@ -438,4 +438,27 @@ public void SetChatMessage(string message) { }); } } + + /// + /// Set save update data. + /// + /// The index of the save data entry. + /// The array of bytes that represents the changed value. + public void SetSaveUpdate(ushort index, byte[] value) { + lock (Lock) { + PacketDataCollection saveUpdateCollection; + + if (CurrentUpdatePacket.TryGetSendingPacketData(ServerPacketId.SaveUpdate, out var packetData)) { + saveUpdateCollection = (PacketDataCollection) packetData; + } else { + saveUpdateCollection = new PacketDataCollection(); + CurrentUpdatePacket.SetSendingPacketData(ServerPacketId.SaveUpdate, saveUpdateCollection); + } + + saveUpdateCollection.DataInstances.Add(new SaveUpdate { + SaveDataIndex = index, + Value = value + }); + } + } } diff --git a/HKMP/Networking/Packet/Data/SaveUpdate.cs b/HKMP/Networking/Packet/Data/SaveUpdate.cs new file mode 100644 index 00000000..b0b80540 --- /dev/null +++ b/HKMP/Networking/Packet/Data/SaveUpdate.cs @@ -0,0 +1,44 @@ +namespace Hkmp.Networking.Packet.Data; + +/// +/// Packet data for when values in the save update. +/// +internal class SaveUpdate : IPacketData { + /// + public bool IsReliable => true; + + /// + public bool DropReliableDataIfNewerExists => false; + + /// + /// The index of the save data entry that got updated. + /// + public ushort SaveDataIndex { get; set; } + + /// + /// The encoded value of the save data in a byte array. + /// + public byte[] Value { get; set; } + + /// + public void WriteData(IPacket packet) { + packet.Write(SaveDataIndex); + + var length = (byte) System.Math.Min(Value.Length, byte.MaxValue); + packet.Write(length); + for (var i = 0; i < length; i++) { + packet.Write(Value[i]); + } + } + + /// + public void ReadData(IPacket packet) { + SaveDataIndex = packet.ReadUShort(); + + var length = packet.ReadByte(); + Value = new byte[length]; + for (var i = 0; i < length; i++) { + Value[i] = packet.ReadByte(); + } + } +} diff --git a/HKMP/Networking/Packet/PacketId.cs b/HKMP/Networking/Packet/PacketId.cs index a6568acb..d89e21dc 100644 --- a/HKMP/Networking/Packet/PacketId.cs +++ b/HKMP/Networking/Packet/PacketId.cs @@ -12,92 +12,97 @@ internal enum ClientPacketId { /// /// A response to the HelloServer after a succeeding login. /// - HelloClient, + HelloClient = 1, /// /// Indicating that a client has connected. /// - PlayerConnect, + PlayerConnect = 2, /// /// Indicating that a client is disconnecting. /// - PlayerDisconnect, + PlayerDisconnect = 3, /// /// Indicating the client is (forcefully) disconnected from the server. /// - ServerClientDisconnect, + ServerClientDisconnect = 4, /// /// Notify that a player has entered the current scene. /// - PlayerEnterScene, + PlayerEnterScene = 5, /// /// Notify that a player is already in the scene we just entered. /// - PlayerAlreadyInScene, + PlayerAlreadyInScene = 6, /// /// Notify that a player has left the current scene. /// - PlayerLeaveScene, + PlayerLeaveScene = 7, /// /// Update of realtime player values. /// - PlayerUpdate, + PlayerUpdate = 8, /// /// Update of player map position. /// - PlayerMapUpdate, + PlayerMapUpdate = 9, /// /// Notify that an entity has spawned. /// - EntitySpawn, + EntitySpawn = 10, /// /// Update of realtime entity values. /// - EntityUpdate, + EntityUpdate = 11, /// /// Update of realtime reliable entity values. /// - ReliableEntityUpdate, + ReliableEntityUpdate = 12, /// /// Notify that the player becomes scene host of their current scene. /// - SceneHostTransfer, + SceneHostTransfer = 13, /// /// Notify that a player has died. /// - PlayerDeath, + PlayerDeath = 14, /// /// Notify that a player has changed teams. /// - PlayerTeamUpdate, + PlayerTeamUpdate = 15, /// /// Notify that a player has changed skins. /// - PlayerSkinUpdate, + PlayerSkinUpdate = 16, /// /// Notify that the gameplay settings have updated. /// - ServerSettingsUpdated, + ServerSettingsUpdated = 17, /// /// Player sent chat message. /// - ChatMessage = 18 + ChatMessage = 18, + + /// + /// Value in the save file has updated. + /// + SaveUpdate = 19, } /// @@ -112,65 +117,70 @@ public enum ServerPacketId { /// /// Initial hello, sent when login succeeds. /// - HelloServer, + HelloServer = 1, /// /// Indicating that a client is disconnecting. /// - PlayerDisconnect, + PlayerDisconnect = 2, /// /// Update of realtime player values. /// - PlayerUpdate, + PlayerUpdate = 3, /// /// Update of player map position. /// - PlayerMapUpdate, + PlayerMapUpdate = 4, /// /// Notify that an entity has spawned. /// - EntitySpawn, + EntitySpawn = 5, /// /// Update of realtime entity values. /// - EntityUpdate, + EntityUpdate = 6, /// /// Update of realtime reliable entity values. /// - ReliableEntityUpdate, + ReliableEntityUpdate = 7, /// /// Notify that the player has entered a new scene. /// - PlayerEnterScene, + PlayerEnterScene = 8, /// /// Notify that the player has left their current scene. /// - PlayerLeaveScene, + PlayerLeaveScene = 9, /// /// Notify that a player has died. /// - PlayerDeath, + PlayerDeath = 10, /// /// Notify that a player has changed teams. /// - PlayerTeamUpdate, + PlayerTeamUpdate = 11, /// /// Notify that a player has changed skins. /// - PlayerSkinUpdate, + PlayerSkinUpdate = 12, /// /// Player sent chat message. /// - ChatMessage = 13 + ChatMessage = 13, + + /// + /// Value in the save file has updated. + /// + SaveUpdate = 14, } diff --git a/HKMP/Networking/Packet/UpdatePacket.cs b/HKMP/Networking/Packet/UpdatePacket.cs index ce4776b0..7263775c 100644 --- a/HKMP/Networking/Packet/UpdatePacket.cs +++ b/HKMP/Networking/Packet/UpdatePacket.cs @@ -879,6 +879,8 @@ protected override IPacketData InstantiatePacketDataFromId(ServerPacketId packet return new ServerPlayerSkinUpdate(); case ServerPacketId.ChatMessage: return new ChatMessage(); + case ServerPacketId.SaveUpdate: + return new PacketDataCollection(); default: return new EmptyData(); } @@ -936,6 +938,8 @@ protected override IPacketData InstantiatePacketDataFromId(ClientPacketId packet return new ServerSettingsUpdate(); case ClientPacketId.ChatMessage: return new PacketDataCollection(); + case ClientPacketId.SaveUpdate: + return new PacketDataCollection(); default: return new EmptyData(); } diff --git a/HKMP/Networking/Server/ServerUpdateManager.cs b/HKMP/Networking/Server/ServerUpdateManager.cs index dece3730..f3cddd34 100644 --- a/HKMP/Networking/Server/ServerUpdateManager.cs +++ b/HKMP/Networking/Server/ServerUpdateManager.cs @@ -552,4 +552,27 @@ public void AddChatMessage(string message) { }); } } + + /// + /// Set save update data. + /// + /// The index of the save data entry. + /// The array of bytes that represents the changed value. + public void SetSaveUpdate(ushort index, byte[] value) { + lock (Lock) { + PacketDataCollection saveUpdateCollection; + + if (CurrentUpdatePacket.TryGetSendingPacketData(ClientPacketId.SaveUpdate, out var packetData)) { + saveUpdateCollection = (PacketDataCollection) packetData; + } else { + saveUpdateCollection = new PacketDataCollection(); + CurrentUpdatePacket.SetSendingPacketData(ClientPacketId.SaveUpdate, saveUpdateCollection); + } + + saveUpdateCollection.DataInstances.Add(new SaveUpdate { + SaveDataIndex = index, + Value = value + }); + } + } } diff --git a/HKMP/Resource/save-data.json b/HKMP/Resource/save-data.json new file mode 100644 index 00000000..93269f98 --- /dev/null +++ b/HKMP/Resource/save-data.json @@ -0,0 +1,1276 @@ +{ + "heartPieces": false, + "heartPieceMax": false, + "geo": false, + "vesselFragments": false, + "vesselFragmentMax": false, + "dreamgateMapPos": false, + "geoPool": false, + "hasSpell": false, + "fireballLevel": false, + "quakeLevel": false, + "screamLevel": false, + "hasNailArt": false, + "hasCyclone": false, + "hasDashSlash": false, + "hasUpwardSlash": false, + "hasAllNailArts": false, + "hasDreamNail": false, + "hasDreamGate": false, + "dreamNailUpgraded": false, + "dreamOrbs": false, + "dreamOrbsSpent": false, + "dreamGateScene": false, + "dreamGateX": false, + "dreamGateY": false, + "hasDash": false, + "hasWalljump": false, + "hasSuperDash": false, + "hasShadowDash": false, + "hasAcidArmour": false, + "hasDoubleJump": false, + "hasLantern": false, + "hasTramPass": false, + "hasQuill": false, + "hasCityKey": false, + "hasSlykey": false, + "gaveSlykey": false, + "hasWhiteKey": false, + "usedWhiteKey": false, + "hasMenderKey": true, + "hasWaterwaysKey": false, + "hasSpaKey": false, + "hasLoveKey": false, + "hasKingsBrand": false, + "hasXunFlower": false, + "ghostCoins": false, + "ore": false, + "foundGhostCoin": false, + "trinket1": false, + "foundTrinket1": false, + "trinket2": false, + "foundTrinket2": false, + "trinket3": false, + "foundTrinket3": false, + "trinket4": false, + "foundTrinket4": false, + "noTrinket1": false, + "noTrinket2": false, + "noTrinket3": false, + "noTrinket4": false, + "soldTrinket1": false, + "soldTrinket2": false, + "soldTrinket3": false, + "soldTrinket4": false, + "simpleKeys": false, + "rancidEggs": false, + "notchShroomOgres": false, + "notchFogCanyon": false, + "gotLurkerKey": false, + "guardiansDefeated": false, + "lurienDefeated": true, + "hegemolDefeated": true, + "monomonDefeated": true, + "maskBrokenLurien": true, + "maskBrokenHegemol": true, + "maskBrokenMonomon": true, + "maskToBreak": false, + "elderbug": false, + "metElderbug": false, + "elderbugReintro": false, + "elderbugHistory": false, + "elderbugHistory1": false, + "elderbugHistory2": false, + "elderbugHistory3": false, + "elderbugSpeechSly": false, + "elderbugSpeechStation": false, + "elderbugSpeechEggTemple": false, + "elderbugSpeechMapShop": false, + "elderbugSpeechBretta": false, + "elderbugSpeechJiji": false, + "elderbugSpeechMinesLift": false, + "elderbugSpeechKingsPass": false, + "elderbugSpeechInfectedCrossroads": false, + "elderbugSpeechFinalBossDoor": false, + "elderbugRequestedFlower": false, + "elderbugGaveFlower": false, + "elderbugFirstCall": false, + "metQuirrel": true, + "quirrelEggTemple": true, + "quirrelSlugShrine": true, + "quirrelRuins": true, + "quirrelMines": true, + "quirrelLeftStation": true, + "quirrelLeftEggTemple": true, + "quirrelCityEncountered": true, + "quirrelCityLeft": true, + "quirrelMinesEncountered": true, + "quirrelMinesLeft": true, + "quirrelMantisEncountered": true, + "enteredMantisLordArea": true, + "visitedDeepnestSpa": true, + "quirrelSpaReady": true, + "quirrelSpaEncountered": true, + "quirrelArchiveEncountered": true, + "quirrelEpilogueCompleted": true, + "metRelicDealer": false, + "metRelicDealerShop": false, + "marmOutside": true, + "marmOutsideConvo": false, + "marmConvo1": false, + "marmConvo2": false, + "marmConvo3": false, + "marmConvoNailsmith": false, + "cornifer": true, + "metCornifer": false, + "corniferIntroduced": false, + "corniferAtHome": true, + "corn_crossroadsEncountered": true, + "corn_crossroadsLeft": true, + "corn_greenpathEncountered": true, + "corn_greenpathLeft": true, + "corn_fogCanyonEncountered": true, + "corn_fogCanyonLeft": true, + "corn_fungalWastesEncountered": true, + "corn_fungalWastesLeft": true, + "corn_cityEncountered": true, + "corn_cityLeft": true, + "corn_waterwaysEncountered": true, + "corn_waterwaysLeft": true, + "corn_minesEncountered": true, + "corn_minesLeft": true, + "corn_cliffsEncountered": true, + "corn_cliffsLeft": true, + "corn_deepnestEncountered": true, + "corn_deepnestLeft": true, + "corn_deepnestMet1": true, + "corn_deepnestMet2": true, + "corn_outskirtsEncountered": true, + "corn_outskirtsLeft": true, + "corn_royalGardensEncountered": true, + "corn_royalGardensLeft": true, + "corn_abyssEncountered": true, + "corn_abyssLeft": true, + "metIselda": false, + "iseldaCorniferHomeConvo": false, + "iseldaConvo1": false, + "brettaRescued": true, + "brettaPosition": true, + "brettaState": true, + "brettaSeenBench": true, + "brettaSeenBed": true, + "brettaSeenBenchDiary": true, + "brettaSeenBedDiary": true, + "brettaLeftTown": true, + "slyRescued": true, + "slyBeta": true, + "metSlyShop": false, + "gotSlyCharm": false, + "slyShellFrag1": false, + "slyShellFrag2": false, + "slyShellFrag3": false, + "slyShellFrag4": false, + "slyVesselFrag1": false, + "slyVesselFrag2": false, + "slyVesselFrag3": false, + "slyVesselFrag4": false, + "slyNotch1": false, + "slyNotch2": false, + "slySimpleKey": false, + "slyRancidEgg": false, + "slyConvoNailArt": false, + "slyConvoMapper": false, + "slyConvoNailHoned": false, + "jijiDoorUnlocked": true, + "jijiMet": false, + "jijiShadeOffered": false, + "jijiShadeCharmConvo": false, + "metJinn": false, + "jinnConvo1": false, + "jinnConvo2": false, + "jinnConvo3": false, + "jinnConvoKingBrand": false, + "jinnConvoShadeCharm": false, + "jinnEggsSold": false, + "zote": true, + "zoteRescuedBuzzer": true, + "zoteDead": true, + "zoteDeathPos": true, + "zoteSpokenCity": true, + "zoteLeftCity": true, + "zoteTrappedDeepnest": true, + "zoteRescuedDeepnest": true, + "zoteDefeated": true, + "zoteSpokenColosseum": true, + "zotePrecept": false, + "zoteTownConvo": false, + "shaman": true, + "shamanScreamConvo": false, + "shamanQuakeConvo": false, + "shamanFireball2Convo": false, + "shamanScream2Convo": false, + "shamanQuake2Convo": false, + "metMiner": false, + "miner": true, + "minerEarly": false, + "hornetGreenpath": true, + "hornetFung": true, + "hornet_f19": true, + "hornetFountainEncounter": false, + "hornetCityBridge_ready": true, + "hornetCityBridge_completed": true, + "hornetAbyssEncounter": true, + "hornetDenEncounter": true, + "metMoth": false, + "ignoredMoth": false, + "gladeDoorOpened": true, + "mothDeparted": false, + "completedRGDreamPlant": false, + "dreamReward1": false, + "dreamReward2": false, + "dreamReward3": false, + "dreamReward4": false, + "dreamReward5": false, + "dreamReward5b": false, + "dreamReward6": false, + "dreamReward7": false, + "dreamReward8": false, + "dreamReward9": false, + "dreamMothConvo1": false, + "bankerAccountPurchased": false, + "metBanker": false, + "bankerBalance": false, + "bankerDeclined": false, + "bankerTheftCheck": true, + "bankerTheft": true, + "bankerSpaMet": false, + "metGiraffe": false, + "metCharmSlug": false, + "salubraNotch1": false, + "salubraNotch2": false, + "salubraNotch3": false, + "salubraNotch4": false, + "salubraBlessing": false, + "salubraConvoCombo": false, + "salubraConvoOvercharm": false, + "salubraConvoTruth": false, + "cultistTransformed": true, + "metNailsmith": false, + "nailSmithUpgrades": false, + "honedNail": false, + "nailsmithCliff": false, + "nailsmithKilled": false, + "nailsmithSpared": false, + "nailsmithKillSpeech": false, + "nailsmithSheo": true, + "nailsmithConvoArt": true, + "metNailmasterMato": false, + "metNailmasterSheo": false, + "metNailmasterOro": false, + "matoConvoSheo": false, + "matoConvoOro": false, + "matoConvoSly": false, + "sheoConvoMato": false, + "sheoConvoOro": false, + "sheoConvoSly": false, + "sheoConvoNailsmith": false, + "oroConvoSheo": false, + "oroConvoMato": false, + "oroConvoSly": false, + "hunterRoared": true, + "metHunter": false, + "hunterRewardOffered": false, + "huntersMarkOffered": false, + "hasHuntersMark": false, + "metLegEater": false, + "paidLegEater": false, + "refusedLegEater": false, + "legEaterConvo1": false, + "legEaterConvo2": false, + "legEaterConvo3": false, + "legEaterBrokenConvo": false, + "legEaterDungConvo": false, + "legEaterInfectedCrossroadConvo": false, + "legEaterBoughtConvo": false, + "legEaterGoldConvo": false, + "legEaterLeft": true, + "tukMet": false, + "tukEggPrice": false, + "tukDungEgg": false, + "metEmilitia": false, + "emilitiaKingsBrandConvo": false, + "metCloth": false, + "clothEnteredTramRoom": true, + "savedCloth": true, + "clothEncounteredQueensGarden": true, + "clothKilled": true, + "clothInTown": true, + "clothLeftTown": true, + "clothGhostSpoken": true, + "bigCatHitTail": false, + "bigCatHitTailConvo": false, + "bigCatMeet": false, + "bigCatTalk1": false, + "bigCatTalk2": false, + "bigCatTalk3": false, + "bigCatKingsBrandConvo": false, + "bigCatShadeConvo": false, + "tisoEncounteredTown": true, + "tisoEncounteredBench": true, + "tisoEncounteredLake": true, + "tisoEncounteredColosseum": true, + "tisoDead": true, + "tisoShieldConvo": true, + "mossCultist": true, + "maskmakerMet": false, + "maskmakerConvo1": false, + "maskmakerConvo2": false, + "maskmakerUnmasked1": false, + "maskmakerUnmasked2": false, + "maskmakerShadowDash": false, + "maskmakerKingsBrand": false, + "dungDefenderConvo1": false, + "dungDefenderConvo2": false, + "dungDefenderConvo3": false, + "dungDefenderCharmConvo": false, + "dungDefenderIsmaConvo": false, + "dungDefenderAwoken": true, + "dungDefenderLeft": true, + "dungDefenderAwakeConvo": false, + "midwifeMet": false, + "midwifeConvo1": false, + "midwifeConvo2": false, + "metQueen": false, + "queenTalk1": false, + "queenTalk2": false, + "queenDung1": false, + "queenDung2": false, + "queenHornet": false, + "queenTalkExtra": false, + "gotQueenFragment": false, + "queenConvo_grimm1": false, + "queenConvo_grimm2": false, + "gotKingFragment": false, + "metXun": false, + "xunFailedConvo1": false, + "xunFailedConvo2": false, + "xunFlowerBroken": false, + "xunFlowerBrokeTimes": false, + "xunFlowerGiven": false, + "xunRewardGiven": false, + "menderState": true, + "menderSignBroken": true, + "allBelieverTabletsDestroyed": true, + "mrMushroomState": true, + "openedMapperShop": true, + "openedSlyShop": true, + "metStag": false, + "stagPosition": true, + "stationsOpened": true, + "stagConvoTram": false, + "stagConvoTiso": false, + "stagRemember1": false, + "stagRemember2": false, + "stagRemember3": false, + "stagEggInspected": false, + "stagHopeConvo": false, + "littleFoolMet": false, + "ranAway": false, + "seenColosseumTitle": false, + "colosseumBronzeOpened": false, + "colosseumBronzeCompleted": false, + "colosseumSilverOpened": false, + "colosseumSilverCompleted": false, + "colosseumGoldOpened": false, + "colosseumGoldCompleted": false, + "openedTown": true, + "openedTownBuilding": true, + "openedCrossroads": true, + "openedGreenpath": true, + "openedRuins1": true, + "openedRuins2": true, + "openedFungalWastes": true, + "openedRoyalGardens": true, + "openedRestingGrounds": true, + "openedDeepnest": true, + "openedStagNest": true, + "openedHiddenStation": true, + "charmSlots": false, + "charmsOwned": false, + "gotCharm_1": false, + "gotCharm_2": false, + "gotCharm_3": false, + "gotCharm_4": false, + "gotCharm_5": false, + "gotCharm_6": false, + "gotCharm_7": false, + "gotCharm_8": false, + "gotCharm_9": false, + "gotCharm_10": false, + "gotCharm_11": false, + "gotCharm_12": false, + "gotCharm_13": false, + "gotCharm_14": false, + "gotCharm_15": false, + "gotCharm_16": false, + "gotCharm_17": false, + "gotCharm_18": false, + "gotCharm_19": false, + "gotCharm_20": false, + "gotCharm_21": false, + "gotCharm_22": false, + "gotCharm_23": false, + "gotCharm_24": false, + "gotCharm_25": false, + "gotCharm_26": false, + "gotCharm_27": false, + "gotCharm_28": false, + "gotCharm_29": false, + "gotCharm_30": false, + "gotCharm_31": false, + "gotCharm_32": false, + "gotCharm_33": false, + "gotCharm_34": false, + "gotCharm_35": false, + "gotCharm_36": false, + "gotCharm_37": false, + "gotCharm_38": false, + "gotCharm_39": false, + "gotCharm_40": false, + "fragileHealth_unbreakable": false, + "fragileGreed_unbreakable": false, + "fragileStrength_unbreakable": false, + "royalCharmState": false, + "hasJournal": false, + "seenJournalMsg": false, + "seenHunterMsg": false, + "fillJournal": false, + "journalEntriesCompleted": true, + "journalNotesCompleted": true, + "journalEntriesTotal": true, + "killedCrawler": true, + "killsCrawler": true, + "newDataCrawler": true, + "killedBuzzer": true, + "killsBuzzer": true, + "newDataBuzzer": true, + "killedBouncer": true, + "killsBouncer": true, + "newDataBouncer": true, + "killedClimber": true, + "killsClimber": true, + "newDataClimber": true, + "killedHopper": true, + "killsHopper": true, + "newDataHopper": true, + "killedWorm": true, + "killsWorm": true, + "newDataWorm": true, + "killedSpitter": true, + "killsSpitter": true, + "newDataSpitter": true, + "killedHatcher": true, + "killsHatcher": true, + "newDataHatcher": true, + "killedHatchling": true, + "killsHatchling": true, + "newDataHatchling": true, + "killedZombieRunner": true, + "killsZombieRunner": true, + "newDataZombieRunner": true, + "killedZombieHornhead": true, + "killsZombieHornhead": true, + "newDataZombieHornhead": true, + "killedZombieLeaper": true, + "killsZombieLeaper": true, + "newDataZombieLeaper": true, + "killedZombieBarger": true, + "killsZombieBarger": true, + "newDataZombieBarger": true, + "killedZombieShield": true, + "killsZombieShield": true, + "newDataZombieShield": true, + "killedZombieGuard": true, + "killsZombieGuard": true, + "newDataZombieGuard": true, + "killedBigBuzzer": true, + "killsBigBuzzer": true, + "newDataBigBuzzer": true, + "killedBigFly": true, + "killsBigFly": true, + "newDataBigFly": true, + "killedMawlek": true, + "killsMawlek": true, + "newDataMawlek": true, + "killedFalseKnight": true, + "killsFalseKnight": true, + "newDataFalseKnight": true, + "killedRoller": true, + "killsRoller": true, + "newDataRoller": true, + "killedBlocker": true, + "killsBlocker": true, + "newDataBlocker": true, + "killedPrayerSlug": true, + "killsPrayerSlug": true, + "newDataPrayerSlug": true, + "killedMenderBug": true, + "killsMenderBug": true, + "newDataMenderBug": true, + "killedMossmanRunner": true, + "killsMossmanRunner": true, + "newDataMossmanRunner": true, + "killedMossmanShaker": true, + "killsMossmanShaker": true, + "newDataMossmanShaker": true, + "killedMosquito": true, + "killsMosquito": true, + "newDataMosquito": true, + "killedBlobFlyer": true, + "killsBlobFlyer": true, + "newDataBlobFlyer": true, + "killedFungifiedZombie": true, + "killsFungifiedZombie": true, + "newDataFungifiedZombie": true, + "killedPlantShooter": true, + "killsPlantShooter": true, + "newDataPlantShooter": true, + "killedMossCharger": true, + "killsMossCharger": true, + "newDataMossCharger": true, + "killedMegaMossCharger": true, + "killsMegaMossCharger": true, + "newDataMegaMossCharger": true, + "killedSnapperTrap": true, + "killsSnapperTrap": true, + "newDataSnapperTrap": true, + "killedMossKnight": true, + "killsMossKnight": true, + "newDataMossKnight": true, + "killedGrassHopper": true, + "killsGrassHopper": true, + "newDataGrassHopper": true, + "killedAcidFlyer": true, + "killsAcidFlyer": true, + "newDataAcidFlyer": true, + "killedAcidWalker": true, + "killsAcidWalker": true, + "newDataAcidWalker": true, + "killedMossFlyer": true, + "killsMossFlyer": true, + "newDataMossFlyer": true, + "killedMossKnightFat": true, + "killsMossKnightFat": true, + "newDataMossKnightFat": true, + "killedMossWalker": true, + "killsMossWalker": true, + "newDataMossWalker": true, + "killedInfectedKnight": true, + "killsInfectedKnight": true, + "newDataInfectedKnight": true, + "killedLazyFlyer": true, + "killsLazyFlyer": true, + "newDataLazyFlyer": true, + "killedZapBug": true, + "killsZapBug": true, + "newDataZapBug": true, + "killedJellyfish": true, + "killsJellyfish": true, + "newDataJellyfish": true, + "killedJellyCrawler": true, + "killsJellyCrawler": true, + "newDataJellyCrawler": true, + "killedMegaJellyfish": true, + "killsMegaJellyfish": true, + "newDataMegaJellyfish": true, + "killedFungoonBaby": true, + "killsFungoonBaby": true, + "newDataFungoonBaby": true, + "killedMushroomTurret": true, + "killsMushroomTurret": true, + "newDataMushroomTurret": true, + "killedMantis": true, + "killsMantis": true, + "newDataMantis": true, + "killedMushroomRoller": true, + "killsMushroomRoller": true, + "newDataMushroomRoller": true, + "killedMushroomBrawler": true, + "killsMushroomBrawler": true, + "newDataMushroomBrawler": true, + "killedMushroomBaby": true, + "killsMushroomBaby": true, + "newDataMushroomBaby": true, + "killedMantisFlyerChild": true, + "killsMantisFlyerChild": true, + "newDataMantisFlyerChild": true, + "killedFungusFlyer": true, + "killsFungusFlyer": true, + "newDataFungusFlyer": true, + "killedFungCrawler": true, + "killsFungCrawler": true, + "newDataFungCrawler": true, + "killedMantisLord": true, + "killsMantisLord": true, + "newDataMantisLord": true, + "killedBlackKnight": true, + "killsBlackKnight": true, + "newDataBlackKnight": true, + "killedElectricMage": true, + "killsElectricMage": true, + "newDataElectricMage": true, + "killedMage": true, + "killsMage": true, + "newDataMage": true, + "killedMageKnight": true, + "killsMageKnight": true, + "newDataMageKnight": true, + "killedRoyalDandy": true, + "killsRoyalDandy": true, + "newDataRoyalDandy": true, + "killedRoyalCoward": true, + "killsRoyalCoward": true, + "newDataRoyalCoward": true, + "killedRoyalPlumper": true, + "killsRoyalPlumper": true, + "newDataRoyalPlumper": true, + "killedFlyingSentrySword": true, + "killsFlyingSentrySword": true, + "newDataFlyingSentrySword": true, + "killedFlyingSentryJavelin": true, + "killsFlyingSentryJavelin": true, + "newDataFlyingSentryJavelin": true, + "killedSentry": true, + "killsSentry": true, + "newDataSentry": true, + "killedSentryFat": true, + "killsSentryFat": true, + "newDataSentryFat": true, + "killedMageBlob": true, + "killsMageBlob": true, + "newDataMageBlob": true, + "killedGreatShieldZombie": true, + "killsGreatShieldZombie": true, + "newDataGreatShieldZombie": true, + "killedJarCollector": true, + "killsJarCollector": true, + "newDataJarCollector": true, + "killedMageBalloon": true, + "killsMageBalloon": true, + "newDataMageBalloon": true, + "killedMageLord": true, + "killsMageLord": true, + "newDataMageLord": true, + "killedGorgeousHusk": true, + "killsGorgeousHusk": true, + "newDataGorgeousHusk": true, + "killedFlipHopper": true, + "killsFlipHopper": true, + "newDataFlipHopper": true, + "killedFlukeman": true, + "killsFlukeman": true, + "newDataFlukeman": true, + "killedInflater": true, + "killsInflater": true, + "newDataInflater": true, + "killedFlukefly": true, + "killsFlukefly": true, + "newDataFlukefly": true, + "killedFlukeMother": true, + "killsFlukeMother": true, + "newDataFlukeMother": true, + "killedDungDefender": true, + "killsDungDefender": true, + "newDataDungDefender": true, + "killedCrystalCrawler": true, + "killsCrystalCrawler": true, + "newDataCrystalCrawler": true, + "killedCrystalFlyer": true, + "killsCrystalFlyer": true, + "newDataCrystalFlyer": true, + "killedLaserBug": true, + "killsLaserBug": true, + "newDataLaserBug": true, + "killedBeamMiner": true, + "killsBeamMiner": true, + "newDataBeamMiner": true, + "killedZombieMiner": true, + "killsZombieMiner": true, + "newDataZombieMiner": true, + "killedMegaBeamMiner": true, + "killsMegaBeamMiner": true, + "newDataMegaBeamMiner": true, + "killedMinesCrawler": true, + "killsMinesCrawler": true, + "newDataMinesCrawler": true, + "killedAngryBuzzer": true, + "killsAngryBuzzer": true, + "newDataAngryBuzzer": true, + "killedBurstingBouncer": true, + "killsBurstingBouncer": true, + "newDataBurstingBouncer": true, + "killedBurstingZombie": true, + "killsBurstingZombie": true, + "newDataBurstingZombie": true, + "killedSpittingZombie": true, + "killsSpittingZombie": true, + "newDataSpittingZombie": true, + "killedBabyCentipede": true, + "killsBabyCentipede": true, + "newDataBabyCentipede": true, + "killedBigCentipede": true, + "killsBigCentipede": true, + "newDataBigCentipede": true, + "killedCentipedeHatcher": true, + "killsCentipedeHatcher": true, + "newDataCentipedeHatcher": true, + "killedLesserMawlek": true, + "killsLesserMawlek": true, + "newDataLesserMawlek": true, + "killedSlashSpider": true, + "killsSlashSpider": true, + "newDataSlashSpider": true, + "killedSpiderCorpse": true, + "killsSpiderCorpse": true, + "newDataSpiderCorpse": true, + "killedShootSpider": true, + "killsShootSpider": true, + "newDataShootSpider": true, + "killedMiniSpider": true, + "killsMiniSpider": true, + "newDataMiniSpider": true, + "killedSpiderFlyer": true, + "killsSpiderFlyer": true, + "newDataSpiderFlyer": true, + "killedMimicSpider": true, + "killsMimicSpider": true, + "newDataMimicSpider": true, + "killedBeeHatchling": true, + "killsBeeHatchling": true, + "newDataBeeHatchling": true, + "killedBeeStinger": true, + "killsBeeStinger": true, + "newDataBeeStinger": true, + "killedBigBee": true, + "killsBigBee": true, + "newDataBigBee": true, + "killedHiveKnight": true, + "killsHiveKnight": true, + "newDataHiveKnight": true, + "killedBlowFly": true, + "killsBlowFly": true, + "newDataBlowFly": true, + "killedCeilingDropper": true, + "killsCeilingDropper": true, + "newDataCeilingDropper": true, + "killedGiantHopper": true, + "killsGiantHopper": true, + "newDataGiantHopper": true, + "killedGrubMimic": true, + "killsGrubMimic": true, + "newDataGrubMimic": true, + "killedMawlekTurret": true, + "killsMawlekTurret": true, + "newDataMawlekTurret": true, + "killedOrangeScuttler": true, + "killsOrangeScuttler": true, + "newDataOrangeScuttler": true, + "killedHealthScuttler": true, + "killsHealthScuttler": true, + "newDataHealthScuttler": true, + "killedPigeon": true, + "killsPigeon": true, + "newDataPigeon": true, + "killedZombieHive": true, + "killsZombieHive": true, + "newDataZombieHive": true, + "killedDreamGuard": true, + "killsDreamGuard": true, + "newDataDreamGuard": true, + "killedHornet": true, + "killsHornet": true, + "newDataHornet": true, + "killedAbyssCrawler": true, + "killsAbyssCrawler": true, + "newDataAbyssCrawler": true, + "killedSuperSpitter": true, + "killsSuperSpitter": true, + "newDataSuperSpitter": true, + "killedSibling": true, + "killsSibling": true, + "newDataSibling": true, + "killedPalaceFly": true, + "killsPalaceFly": true, + "newDataPalaceFly": true, + "killedEggSac": true, + "killsEggSac": true, + "newDataEggSac": true, + "killedMummy": true, + "killsMummy": true, + "newDataMummy": true, + "killedOrangeBalloon": true, + "killsOrangeBalloon": true, + "newDataOrangeBalloon": true, + "killedAbyssTendril": true, + "killsAbyssTendril": true, + "newDataAbyssTendril": true, + "killedHeavyMantis": true, + "killsHeavyMantis": true, + "newDataHeavyMantis": true, + "killedTraitorLord": true, + "killsTraitorLord": true, + "newDataTraitorLord": true, + "killedMantisHeavyFlyer": true, + "killsMantisHeavyFlyer": true, + "newDataMantisHeavyFlyer": true, + "killedGardenZombie": true, + "killsGardenZombie": true, + "newDataGardenZombie": true, + "killedRoyalGuard": true, + "killsRoyalGuard": true, + "newDataRoyalGuard": true, + "killedWhiteRoyal": true, + "killsWhiteRoyal": true, + "newDataWhiteRoyal": true, + "openedPalaceGrounds": true, + "killedOblobble": true, + "killsOblobble": true, + "newDataOblobble": true, + "killedZote": true, + "killsZote": true, + "newDataZote": true, + "killedBlobble": true, + "killsBlobble": true, + "newDataBlobble": true, + "killedColMosquito": true, + "killsColMosquito": true, + "newDataColMosquito": true, + "killedColRoller": true, + "killsColRoller": true, + "newDataColRoller": true, + "killedColFlyingSentry": true, + "killsColFlyingSentry": true, + "newDataColFlyingSentry": true, + "killedColMiner": true, + "killsColMiner": true, + "newDataColMiner": true, + "killedColShield": true, + "killsColShield": true, + "newDataColShield": true, + "killedColWorm": true, + "killsColWorm": true, + "newDataColWorm": true, + "killedColHopper": true, + "killsColHopper": true, + "newDataColHopper": true, + "killedLobsterLancer": true, + "killsLobsterLancer": true, + "newDataLobsterLancer": true, + "killedGhostAladar": true, + "killsGhostAladar": true, + "newDataGhostAladar": true, + "killedGhostXero": true, + "killsGhostXero": true, + "newDataGhostXero": true, + "killedGhostHu": true, + "killsGhostHu": true, + "newDataGhostHu": true, + "killedGhostMarmu": true, + "killsGhostMarmu": true, + "newDataGhostMarmu": true, + "killedGhostNoEyes": true, + "killsGhostNoEyes": true, + "newDataGhostNoEyes": true, + "killedGhostMarkoth": true, + "killsGhostMarkoth": true, + "newDataGhostMarkoth": true, + "killedGhostGalien": true, + "killsGhostGalien": true, + "newDataGhostGalien": true, + "killedWhiteDefender": true, + "killsWhiteDefender": true, + "newDataWhiteDefender": true, + "killedGreyPrince": true, + "killsGreyPrince": true, + "newDataGreyPrince": true, + "killedZotelingBalloon": true, + "killsZotelingBalloon": true, + "newDataZotelingBalloon": true, + "killedZotelingHopper": true, + "killsZotelingHopper": true, + "newDataZotelingHopper": true, + "killedZotelingBuzzer": true, + "killsZotelingBuzzer": true, + "newDataZotelingBuzzer": true, + "killedHollowKnight": true, + "killsHollowKnight": true, + "newDataHollowKnight": true, + "killedFinalBoss": true, + "killsFinalBoss": true, + "newDataFinalBoss": true, + "killedHunterMark": true, + "killsHunterMark": true, + "newDataHunterMark": true, + "killedFlameBearerSmall": true, + "killsFlameBearerSmall": true, + "newDataFlameBearerSmall": true, + "killedFlameBearerMed": true, + "killsFlameBearerMed": true, + "newDataFlameBearerMed": true, + "killedFlameBearerLarge": true, + "killsFlameBearerLarge": true, + "newDataFlameBearerLarge": true, + "killedGrimm": true, + "killsGrimm": true, + "newDataGrimm": true, + "killedNightmareGrimm": true, + "killsNightmareGrimm": true, + "newDataNightmareGrimm": true, + "killedBindingSeal": true, + "killsBindingSeal": true, + "newDataBindingSeal": true, + "killedFatFluke": true, + "killsFatFluke": true, + "newDataFatFluke": true, + "killedPaleLurker": true, + "killsPaleLurker": true, + "newDataPaleLurker": true, + "killedNailBros": true, + "killsNailBros": true, + "newDataNailBros": true, + "killedPaintmaster": true, + "killsPaintmaster": true, + "newDataPaintmaster": true, + "killedNailsage": true, + "killsNailsage": true, + "newDataNailsage": true, + "killedHollowKnightPrime": true, + "killsHollowKnightPrime": true, + "newDataHollowKnightPrime": true, + "killedGodseekerMask": true, + "killsGodseekerMask": true, + "newDataGodseekerMask": true, + "killedVoidIdol_1": true, + "killsVoidIdol_1": true, + "newDataVoidIdol_1": true, + "killedVoidIdol_2": true, + "killsVoidIdol_2": true, + "newDataVoidIdol_2": true, + "killedVoidIdol_3": true, + "killsVoidIdol_3": true, + "newDataVoidIdol_3": true, + "grubsCollected": true, + "grubRewards": false, + "finalGrubRewardCollected": false, + "fatGrubKing": false, + "falseKnightDefeated": true, + "falseKnightDreamDefeated": true, + "falseKnightOrbsCollected": false, + "mawlekDefeated": true, + "giantBuzzerDefeated": true, + "giantFlyDefeated": true, + "blocker1Defeated": true, + "blocker2Defeated": true, + "hornet1Defeated": true, + "collectorDefeated": true, + "hornetOutskirtsDefeated": true, + "mageLordDreamDefeated": true, + "mageLordOrbsCollected": false, + "infectedKnightDreamDefeated": true, + "infectedKnightOrbsCollected": false, + "whiteDefenderDefeated": true, + "whiteDefenderOrbsCollected": false, + "whiteDefenderDefeats": true, + "greyPrinceDefeats": true, + "greyPrinceDefeated": true, + "greyPrinceOrbsCollected": false, + "aladarSlugDefeated": true, + "xeroDefeated": true, + "elderHuDefeated": true, + "mumCaterpillarDefeated": true, + "noEyesDefeated": true, + "markothDefeated": true, + "galienDefeated": true, + "XERO_encountered": false, + "ALADAR_encountered": false, + "HU_encountered": false, + "MUMCAT_encountered": false, + "NOEYES_encountered": false, + "MARKOTH_encountered": false, + "GALIEN_encountered": false, + "xeroPinned": true, + "aladarPinned": true, + "huPinned": true, + "mumCaterpillarPinned": true, + "noEyesPinned": true, + "markothPinned": true, + "galienPinned": true, + "scenesVisited": true, + "scenesMapped": true, + "scenesEncounteredBench": true, + "scenesGrubRescued": true, + "scenesFlameCollected": true, + "scenesEncounteredCocoon": true, + "scenesEncounteredDreamPlant": true, + "scenesEncounteredDreamPlantC": false, + "hasMap": false, + "mapDirtmouth": false, + "mapCrossroads": false, + "mapGreenpath": false, + "mapFogCanyon": false, + "mapRoyalGardens": false, + "mapFungalWastes": false, + "mapCity": false, + "mapWaterways": false, + "mapMines": false, + "mapDeepnest": false, + "mapCliffs": false, + "mapOutskirts": false, + "mapRestingGrounds": false, + "mapAbyss": false, + "mapZoneBools": true, + "hasPin": false, + "hasPinBench": false, + "hasPinCocoon": false, + "hasPinDreamPlant": false, + "hasPinGuardian": false, + "hasPinBlackEgg": false, + "hasPinShop": false, + "hasPinSpa": false, + "hasPinStag": false, + "hasPinTram": false, + "hasPinGhost": false, + "hasPinGrub": false, + "hasMarker": false, + "hasMarker_r": false, + "hasMarker_b": false, + "hasMarker_y": false, + "hasMarker_w": false, + "openedTramLower": false, + "openedTramRestingGrounds": false, + "tramLowerPosition": true, + "tramRestingGroundsPosition": true, + "mineLiftOpened": true, + "menderDoorOpened": true, + "vesselFragStagNest": false, + "shamanPillar": true, + "crossroadsMawlekWall": true, + "eggTempleVisited": false, + "crossroadsInfected": true, + "falseKnightFirstPlop": true, + "falseKnightWallRepaired": true, + "falseKnightWallBroken": true, + "falseKnightGhostDeparted": true, + "spaBugsEncountered": true, + "hornheadVinePlat": true, + "infectedKnightEncountered": true, + "megaMossChargerEncountered": true, + "megaMossChargerDefeated": true, + "dreamerScene1": true, + "slugEncounterComplete": true, + "defeatedDoubleBlockers": true, + "oneWayArchive": true, + "defeatedMegaJelly": true, + "summonedMonomon": true, + "sawWoundedQuirrel": true, + "encounteredMegaJelly": true, + "defeatedMantisLords": true, + "encounteredGatekeeper": true, + "deepnestWall": true, + "queensStationNonDisplay": true, + "cityBridge1": true, + "cityBridge2": true, + "cityLift1": true, + "cityLift1_isUp": true, + "liftArrival": true, + "openedMageDoor": true, + "openedMageDoor_v2": true, + "brokenMageWindow": true, + "brokenMageWindowGlass": true, + "mageLordEncountered": true, + "mageLordEncountered_2": true, + "mageLordDefeated": true, + "ruins1_5_tripleDoor": true, + "openedCityGate": true, + "cityGateClosed": true, + "bathHouseOpened": true, + "bathHouseWall": true, + "cityLift2": true, + "cityLift2_isUp": true, + "city2_sewerDoor": true, + "openedLoveDoor": true, + "watcherChandelier": true, + "completedQuakeArea": true, + "kingsStationNonDisplay": true, + "tollBenchCity": true, + "waterwaysGate": true, + "defeatedDungDefender": true, + "dungDefenderEncounterReady": true, + "flukeMotherEncountered": true, + "flukeMotherDefeated": true, + "openedWaterwaysManhole": true, + "waterwaysAcidDrained": true, + "dungDefenderWallBroken": true, + "dungDefenderSleeping": true, + "defeatedMegaBeamMiner": true, + "defeatedMegaBeamMiner2": true, + "brokeMinersWall": true, + "encounteredMimicSpider": true, + "steppedBeyondBridge": true, + "deepnestBridgeCollapsed": true, + "spiderCapture": false, + "deepnest26b_switch": true, + "openedRestingGrounds02": true, + "restingGroundsCryptWall": true, + "dreamNailConvo": false, + "gladeGhostsKilled": true, + "openedGardensStagStation": true, + "extendedGramophone": true, + "tollBenchQueensGardens": true, + "blizzardEnded": true, + "encounteredHornet": true, + "savedByHornet": true, + "outskirtsWall": true, + "abyssGateOpened": true, + "abyssLighthouse": true, + "blueVineDoor": true, + "gotShadeCharm": true, + "tollBenchAbyss": true, + "fountainGeo": false, + "fountainVesselSummoned": false, + "openedBlackEggPath": true, + "enteredDreamWorld": false, + "duskKnightDefeated": true, + "whitePalaceOrb_1": true, + "whitePalaceOrb_2": true, + "whitePalaceOrb_3": true, + "whitePalace05_lever": true, + "whitePalaceMidWarp": true, + "whitePalaceSecretRoomVisited": true, + "tramOpenedDeepnest": true, + "tramOpenedCrossroads": true, + "openedBlackEggDoor": true, + "unchainedHollowKnight": true, + "flamesCollected": true, + "flamesRequired": true, + "nightmareLanternAppeared": true, + "nightmareLanternLit": true, + "troupeInTown": true, + "divineInTown": true, + "grimmChildLevel": true, + "elderbugConvoGrimm": false, + "slyConvoGrimm": false, + "iseldaConvoGrimm": false, + "midwifeWeaverlingConvo": false, + "metGrimm": true, + "foughtGrimm": true, + "metBrum": false, + "defeatedNightmareGrimm": true, + "grimmchildAwoken": true, + "gotBrummsFlame": true, + "brummBrokeBrazier": true, + "destroyedNightmareLantern": true, + "gotGrimmNotch": false, + "nymmInTown": true, + "nymmSpoken": false, + "nymmCharmConvo": false, + "nymmFinalConvo": false, + "elderbugNymmConvo": false, + "slyNymmConvo": false, + "iseldaNymmConvo": false, + "nymmMissedEggOpen": false, + "elderbugTroupeLeftConvo": false, + "elderbugBrettaLeft": false, + "jijiGrimmConvo": false, + "metDivine": false, + "divineFinalConvo": false, + "gaveFragileHeart": false, + "gaveFragileGreed": false, + "gaveFragileStrength": false, + "divineEatenConvos": false, + "pooedFragileHeart": false, + "pooedFragileGreed": false, + "pooedFragileStrength": false, + "completionPercentage": false, + "unlockedCompletionRate": false, + "newDatTraitorLord": true, + "bossDoorStateTier1": true, + "bossDoorStateTier2": true, + "bossDoorStateTier3": true, + "bossDoorStateTier4": true, + "bossDoorStateTier5": true, + "bossStatueTargetLevel": false, + "statueStateGruzMother": true, + "statueStateVengefly": true, + "statueStateBroodingMawlek": true, + "statueStateFalseKnight": true, + "statueStateFailedChampion": true, + "statueStateHornet1": true, + "statueStateHornet2": true, + "statueStateMegaMossCharger": true, + "statueStateMantisLords": true, + "statueStateOblobbles": true, + "statueStateGreyPrince": true, + "statueStateBrokenVessel": true, + "statueStateLostKin": true, + "statueStateNosk": true, + "statueStateFlukemarm": true, + "statueStateCollector": true, + "statueStateWatcherKnights": true, + "statueStateSoulMaster": true, + "statueStateSoulTyrant": true, + "statueStateGodTamer": true, + "statueStateCrystalGuardian1": true, + "statueStateCrystalGuardian2": true, + "statueStateUumuu": true, + "statueStateDungDefender": true, + "statueStateWhiteDefender": true, + "statueStateHiveKnight": true, + "statueStateTraitorLord": true, + "statueStateGrimm": true, + "statueStateNightmareGrimm": true, + "statueStateHollowKnight": true, + "statueStateElderHu": true, + "statueStateGalien": true, + "statueStateMarkoth": true, + "statueStateMarmu": true, + "statueStateNoEyes": true, + "statueStateXero": true, + "statueStateGorb": true, + "statueStateRadiance": true, + "statueStateSly": true, + "statueStateNailmasters": true, + "statueStateMageKnight": true, + "statueStatePaintmaster": true, + "statueStateZote": true, + "statueStateNoskHornet": true, + "statueStateMantisLordsExtra": true, + "godseekerUnlocked": true, + "bossDoorCageUnlocked": true, + "blueRoomDoorUnlocked": true, + "blueRoomActivated": true, + "finalBossDoorUnlocked": true, + "hasGodfinder": false, + "unlockedNewBossStatue": true, + "scaredFlukeHermitEncountered": false, + "scaredFlukeHermitReturned": false, + "enteredGGAtrium": false, + "extraFlowerAppear": true, + "givenGodseekerFlower": true, + "givenOroFlower": true, + "givenWhiteLadyFlower": true, + "givenEmilitiaFlower": true, + "unlockedBossScenes": false, + "queuedGodfinderIcon": false, + "godseekerSpokenAwake": false, + "nailsmithCorpseAppeared": true, + "godseekerWaterwaysSeenState": true, + "godseekerWaterwaysSpoken1": false, + "godseekerWaterwaysSpoken2": false, + "godseekerWaterwaysSpoken3": false, + "bossDoorEntranceTextSeen": false, + "seenDoor4Finale": true, + "zoteStatueWallBroken": true, + "seenGGWastes": false, + "ordealAchieved": true +} \ No newline at end of file From 6894cd07b1fba8a298fa763184dab5f97d1e6ef1 Mon Sep 17 00:00:00 2001 From: Extremelyd1 Date: Fri, 24 Nov 2023 20:18:39 +0100 Subject: [PATCH 075/216] Synchronise geo rocks and persistent items --- HKMP/Collection/BiLookup.cs | 8 + HKMP/Game/Client/Save/PersistentFsmData.cs | 36 + HKMP/Game/Client/Save/PersistentItemData.cs | 67 + HKMP/Game/Client/Save/SaveData.cs | 133 + HKMP/Game/Client/Save/SaveManager.cs | 330 +- HKMP/Resource/save-data.json | 20441 ++++++++++++++++-- 6 files changed, 19682 insertions(+), 1333 deletions(-) create mode 100644 HKMP/Game/Client/Save/PersistentFsmData.cs create mode 100644 HKMP/Game/Client/Save/PersistentItemData.cs create mode 100644 HKMP/Game/Client/Save/SaveData.cs 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/Game/Client/Save/PersistentFsmData.cs b/HKMP/Game/Client/Save/PersistentFsmData.cs new file mode 100644 index 00000000..d862556e --- /dev/null +++ b/HKMP/Game/Client/Save/PersistentFsmData.cs @@ -0,0 +1,36 @@ +using HutongGames.PlayMaker; + +namespace Hkmp.Game.Client.Save; + +/// +/// Data class for a persistent item in the scene with the corresponding FsmInt/FsmBool. +/// +internal class PersistentFsmData { + /// + /// The persistent item data with the ID and scene name. + /// + public PersistentItemData PersistentItemData { get; set; } + + /// + /// The FSM variable for an integer. Could be null if a boolean is used instead. + /// + public FsmInt FsmInt { get; set; } + /// + /// The FSM variable for a boolean. Could be null if an integer is used instead. + /// + public FsmBool FsmBool { get; set; } + + /// + /// The last value for the integer if used. + /// + public int LastIntValue { get; set; } + /// + /// The last value for the boolean if used. + /// + public bool LastBoolValue { get; set; } + + /// + /// Whether an int is stored for this data. + /// + public bool IsInt => FsmInt != null; +} diff --git a/HKMP/Game/Client/Save/PersistentItemData.cs b/HKMP/Game/Client/Save/PersistentItemData.cs new file mode 100644 index 00000000..42ff3a50 --- /dev/null +++ b/HKMP/Game/Client/Save/PersistentItemData.cs @@ -0,0 +1,67 @@ +using System; + +namespace Hkmp.Game.Client.Save; + +/// +/// Data class to identify a persistent item. +/// +internal class PersistentItemData : IEquatable { + /// + /// The ID of the item. + /// + public string Id { get; init; } + /// + /// The name of the scene of the item. + /// + public string SceneName { get; init; } + + /// + public bool Equals(PersistentItemData other) { + if (ReferenceEquals(null, other)) { + return false; + } + + if (ReferenceEquals(this, other)) { + return true; + } + + return Id == other.Id && SceneName == other.SceneName; + } + + /// + public override bool Equals(object obj) { + if (ReferenceEquals(null, obj)) { + return false; + } + + if (ReferenceEquals(this, obj)) { + return true; + } + + if (obj.GetType() != GetType()) { + return false; + } + + return Equals((PersistentItemData) obj); + } + + /// + public override int GetHashCode() { + unchecked { + return ((Id != null ? Id.GetHashCode() : 0) * 397) ^ (SceneName != null ? SceneName.GetHashCode() : 0); + } + } + + public static bool operator ==(PersistentItemData left, PersistentItemData right) { + return Equals(left, right); + } + + public static bool operator !=(PersistentItemData left, PersistentItemData right) { + return !Equals(left, right); + } + + /// + public override string ToString() { + return $"({Id}, {SceneName})"; + } +} diff --git a/HKMP/Game/Client/Save/SaveData.cs b/HKMP/Game/Client/Save/SaveData.cs new file mode 100644 index 00000000..5b0565f4 --- /dev/null +++ b/HKMP/Game/Client/Save/SaveData.cs @@ -0,0 +1,133 @@ +using System.Collections.Generic; +using System.Linq; +using Hkmp.Collection; +using Hkmp.Logging; +using Newtonsoft.Json; + +namespace Hkmp.Game.Client.Save; + +/// +/// Serializable data class that stores mappings for what scene data should be synchronised and their indices used for networking. +/// +internal class SaveData { + /// + /// Dictionary mapping player data values to booleans indicating whether they should be synchronised. + /// + [JsonProperty("playerData")] + public Dictionary PlayerDataBools { get; private set; } + + /// + /// Bi-directional lookup that maps save data names and their indices. + /// + [JsonIgnore] + public BiLookup PlayerDataIndices { get; private set; } + + /// + /// Deserialized key-value pairs for the geo rock data in the JSON. + /// +#pragma warning disable 0649 + [JsonProperty("geoRocks")] + private readonly List> _geoRockDataValues; +#pragma warning restore 0649 + + /// + /// Dictionary mapping geo rock data values to booleans indicating whether they should be synchronised. + /// + [JsonIgnore] + public Dictionary GeoRockDataBools { get; private set; } + + /// + /// Bi-directional lookup that maps geo rock names and their indices. + /// + [JsonIgnore] + public BiLookup GeoRockDataIndices { get; private set; } + + /// + /// Deserialized key-value pairs for the persistent bool data in the JSON. + /// +#pragma warning disable 0649 + [JsonProperty("persistentBoolItems")] + private readonly List> _persistentBoolsDataValues; +#pragma warning restore 0649 + + /// + /// Dictionary mapping persistent bool data values to booleans indicating whether they should be synchronised. + /// + [JsonIgnore] + public Dictionary PersistentBoolDataBools { get; private set; } + + /// + /// Bi-directional lookup that maps persistent bool names and their indices. + /// + [JsonIgnore] + public BiLookup PersistentBoolDataIndices { get; private set; } + + /// + /// Deserialized key-value pairs for the persistent int data in the JSON. + /// +#pragma warning disable 0649 + [JsonProperty("persistentIntItems")] + private readonly List> _persistentIntDataValues; +#pragma warning restore 0649 + + /// + /// Dictionary mapping persistent int data values to booleans indicating whether they should be synchronised. + /// + [JsonIgnore] + public Dictionary PersistentIntDataBools { get; private set; } + + /// + /// Bi-directional lookup that maps persistent int names and their indices. + /// + [JsonIgnore] + public BiLookup PersistentIntDataIndices { get; private set; } + + /// + /// Initializes the class by converting the deserialized data fields into the various dictionaries and lookups. + /// + public void Initialize() { + if (PlayerDataBools == null) { + Logger.Warn("Player data bools for save data is null"); + return; + } + + if (_geoRockDataValues == null) { + Logger.Warn("Geo rock data values for save data is null"); + return; + } + + if (_persistentBoolsDataValues == null) { + Logger.Warn("Persistent bools data values for save data is null"); + return; + } + + if (_persistentIntDataValues == null) { + Logger.Warn("Persistent int data values for save data is null"); + return; + } + + PlayerDataIndices = new BiLookup(); + ushort index = 0; + foreach (var playerDataBool in PlayerDataBools.Keys) { + PlayerDataIndices.Add(playerDataBool, index++); + } + + GeoRockDataBools = _geoRockDataValues.ToDictionary(kv => kv.Key, kv => kv.Value); + GeoRockDataIndices = new BiLookup(); + foreach (var geoRockData in GeoRockDataBools.Keys) { + GeoRockDataIndices.Add(geoRockData, index++); + } + + PersistentBoolDataBools = _persistentBoolsDataValues.ToDictionary(kv => kv.Key, kv => kv.Value); + PersistentBoolDataIndices = new BiLookup(); + foreach (var persistentBoolData in PersistentBoolDataBools.Keys) { + PersistentBoolDataIndices.Add(persistentBoolData, index++); + } + + PersistentIntDataBools = _persistentIntDataValues.ToDictionary(kv => kv.Key, kv => kv.Value); + PersistentIntDataIndices = new BiLookup(); + foreach (var persistentIntData in PersistentIntDataBools.Keys) { + PersistentIntDataIndices.Add(persistentIntData, index++); + } + } +} diff --git a/HKMP/Game/Client/Save/SaveManager.cs b/HKMP/Game/Client/Save/SaveManager.cs index f3c476e2..4dd3ab73 100644 --- a/HKMP/Game/Client/Save/SaveManager.cs +++ b/HKMP/Game/Client/Save/SaveManager.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.Linq; using System.Text; -using Hkmp.Collection; using Hkmp.Game.Client.Entity; using Hkmp.Networking.Client; using Hkmp.Networking.Packet; @@ -10,7 +9,9 @@ using Hkmp.Util; using Modding; using UnityEngine; +using UnityEngine.SceneManagement; using Logger = Hkmp.Logging.Logger; +using Object = UnityEngine.Object; namespace Hkmp.Game.Client.Save; @@ -37,38 +38,30 @@ internal class SaveManager { private readonly EntityManager _entityManager; /// - /// Dictionary mapping save data values to booleans indicating whether they should be synchronised. + /// List of data classes for each FSM that has a persistent int/bool or geo rock attached to it. /// - private Dictionary _saveDataValues; + private readonly List _persistentFsmData; /// - /// Bi-directional lookup that maps save data names and their indices. + /// The save data instances that contains mappings for what to sync and their indices. /// - private BiLookup _saveDataIndices; + private SaveData _saveData; public SaveManager(NetClient netClient, PacketManager packetManager, EntityManager entityManager) { _netClient = netClient; _packetManager = packetManager; _entityManager = entityManager; + + _persistentFsmData = new List(); } /// /// Initializes the save manager by loading the save data json. /// public void Initialize() { - _saveDataValues = FileUtil.LoadObjectFromEmbeddedJson>(SaveDataFilePath); - if (_saveDataValues == null) { - Logger.Warn("Could not load save data json"); - return; - } + _saveData = FileUtil.LoadObjectFromEmbeddedJson(SaveDataFilePath); + _saveData.Initialize(); - _saveDataIndices = new BiLookup(); - ushort index = 0; - foreach (var saveDataName in _saveDataValues.Keys) { - Logger.Info($"Saving ({saveDataName}, {index}) in bi-lookup"); - _saveDataIndices.Add(saveDataName, index++); - } - ModHooks.SetPlayerBoolHook += OnSetPlayerBoolHook; ModHooks.SetPlayerFloatHook += OnSetPlayerFloatHook; ModHooks.SetPlayerIntHook += OnSetPlayerIntHook; @@ -76,9 +69,12 @@ public void Initialize() { ModHooks.SetPlayerVariableHook += OnSetPlayerVariableHook; ModHooks.SetPlayerVector3Hook += OnSetPlayerVector3Hook; + UnityEngine.SceneManagement.SceneManager.activeSceneChanged += OnSceneChanged; + MonoBehaviourUtil.Instance.OnUpdateEvent += OnUpdate; + _packetManager.RegisterClientPacketHandler(ClientPacketId.SaveUpdate, UpdateSaveWithData); } - + /// /// Callback method for when a boolean is set in the player data. /// @@ -174,12 +170,12 @@ private void CheckSendSaveUpdate(string name, Func encodeFunc) { return; } - if (!_saveDataValues.TryGetValue(name, out var value) || !value) { + if (!_saveData.PlayerDataBools.TryGetValue(name, out var value) || !value) { Logger.Info($"Not in save data values or false in save data values, not sending save update ({name})"); return; } - if (!_saveDataIndices.TryGetValue(name, out var index)) { + if (!_saveData.PlayerDataIndices.TryGetValue(name, out var index)) { Logger.Info($"Cannot find save data index, not sending save update ({name})"); return; } @@ -193,63 +189,279 @@ private void CheckSendSaveUpdate(string name, Func encodeFunc) { } /// - /// Callback method for when a save update is received. + /// Callback method for when the scene changes. Used to check for GeoRock, PersistentInt and PersistentBool + /// instances in the scene. /// - /// The save update that was received. - private void UpdateSaveWithData(SaveUpdate saveUpdate) { - if (!_saveDataIndices.TryGetValue(saveUpdate.SaveDataIndex, out var name)) { - Logger.Warn($"Received save update with unknown index: {saveUpdate.SaveDataIndex}"); - return; - } + /// The old scene. + /// The new scene. + private void OnSceneChanged(Scene oldScene, Scene newScene) { + _persistentFsmData.Clear(); - Logger.Info($"Received save update for index: {saveUpdate.SaveDataIndex}"); + foreach (var geoRock in Object.FindObjectsOfType()) { + var geoRockObject = geoRock.gameObject; + + if (geoRockObject.scene != newScene) { + continue; + } - var pd = PlayerData.instance; + var persistentItemData = new PersistentItemData { + Id = geoRockObject.name, + SceneName = global::GameManager.GetBaseSceneName(geoRockObject.scene.name) + }; + + Logger.Info($"Found Geo Rock in scene: {persistentItemData}"); - var fieldInfo = typeof(PlayerData).GetField(name); - var type = fieldInfo.FieldType; - var valueLength = saveUpdate.Value.Length; + var fsm = geoRock.GetComponent(); + var fsmInt = fsm.FsmVariables.GetFsmInt("Hits"); - if (type == typeof(bool)) { - if (valueLength != 1) { - Logger.Warn($"Received save update with incorrect value length for bool: {valueLength}"); + var persistentFsmData = new PersistentFsmData { + PersistentItemData = persistentItemData, + FsmInt = fsmInt, + LastIntValue = fsmInt.Value + }; + + _persistentFsmData.Add(persistentFsmData); + } + + foreach (var persistentBoolItem in Object.FindObjectsOfType()) { + var itemObject = persistentBoolItem.gameObject; + + if (itemObject.scene != newScene) { + continue; } + + var persistentItemData = new PersistentItemData { + Id = itemObject.name, + SceneName = global::GameManager.GetBaseSceneName(itemObject.scene.name) + }; - var value = saveUpdate.Value[0] == 1; + Logger.Info($"Found persistent bool in scene: {persistentItemData}"); + + var fsm = FSMUtility.FindFSMWithPersistentBool(itemObject.GetComponents()); + var fsmBool = fsm.FsmVariables.GetFsmBool("Activated"); - pd.SetBoolInternal(name, value); - } else if (type == typeof(float)) { - if (valueLength != 4) { - Logger.Warn($"Received save update with incorrect value length for float: {valueLength}"); + var persistentFsmData = new PersistentFsmData { + PersistentItemData = persistentItemData, + FsmBool = fsmBool, + LastBoolValue = fsmBool.Value + }; + + _persistentFsmData.Add(persistentFsmData); + } + + foreach (var persistentIntItem in Object.FindObjectsOfType()) { + var itemObject = persistentIntItem.gameObject; + + if (itemObject.scene != newScene) { + continue; } + + var persistentItemData = new PersistentItemData { + Id = itemObject.name, + SceneName = global::GameManager.GetBaseSceneName(itemObject.scene.name) + }; - var value = BitConverter.ToSingle(saveUpdate.Value, 0); + Logger.Info($"Found persistent int in scene: {persistentItemData}"); - pd.SetFloatInternal(name, value); - } else if (type == typeof(int)) { - if (valueLength != 4) { - Logger.Warn($"Received save update with incorrect value length for int: {valueLength}"); + var fsm = FSMUtility.FindFSMWithPersistentBool(itemObject.GetComponents()); + var fsmInt = fsm.FsmVariables.GetFsmInt("Value"); + + var persistentFsmData = new PersistentFsmData { + PersistentItemData = persistentItemData, + FsmInt = fsmInt, + LastIntValue = fsmInt.Value + }; + + _persistentFsmData.Add(persistentFsmData); + } + } + + /// + /// Called every unity update. Used to check for changes in the GeoRock/PersistentInt/PersistentBool FSMs. + /// + private void OnUpdate() { + using var enumerator = _persistentFsmData.GetEnumerator(); + + while (enumerator.MoveNext()) { + var persistentFsmData = enumerator.Current; + if (persistentFsmData == null) { + continue; } + + if (persistentFsmData.IsInt) { + var value = persistentFsmData.FsmInt.Value; + if (value == persistentFsmData.LastIntValue) { + continue; + } + + persistentFsmData.LastIntValue = value; + + var itemData = persistentFsmData.PersistentItemData; + + Logger.Info($"Value for {itemData} changed to: {value}"); + + if (!_entityManager.IsSceneHost) { + Logger.Info($"Not scene host, not sending persistent int/geo rock save update ({itemData.Id}, {itemData.SceneName})"); + continue; + } + + if (_saveData.GeoRockDataBools.TryGetValue(itemData, out var shouldSync) && shouldSync) { + if (!_saveData.GeoRockDataIndices.TryGetValue(itemData, out var index)) { + Logger.Info( + $"Cannot find geo rock save data index, not sending save update ({itemData.Id}, {itemData.SceneName})"); + continue; + } + + Logger.Info($"Sending geo rock ({itemData.Id}, {itemData.SceneName}) as save update"); + + _netClient.UpdateManager.SetSaveUpdate( + index, + new[] { (byte) value } + ); + } else if (_saveData.PersistentIntDataBools.TryGetValue(itemData, out shouldSync) && shouldSync) { + if (!_saveData.PersistentIntDataIndices.TryGetValue(itemData, out var index)) { + Logger.Info( + $"Cannot find persistent int save data index, not sending save update ({itemData.Id}, {itemData.SceneName})"); + continue; + } + + Logger.Info($"Sending persistent int ({itemData.Id}, {itemData.SceneName}) as save update"); + + _netClient.UpdateManager.SetSaveUpdate( + index, + new[] { (byte) value } + ); + } else { + Logger.Info("Cannot find persistent int/geo rock data bool, not sending save update"); + } + } else { + var value = persistentFsmData.FsmBool.Value; + if (value == persistentFsmData.LastBoolValue) { + continue; + } + + persistentFsmData.LastBoolValue = value; + + var itemData = persistentFsmData.PersistentItemData; + + Logger.Info($"Value for {itemData} changed to: {value}"); + + if (!_entityManager.IsSceneHost) { + Logger.Info($"Not scene host, not sending geo rock save update ({itemData.Id}, {itemData.SceneName})"); + continue; + } + + if (!_saveData.PersistentBoolDataBools.TryGetValue(itemData, out var shouldSync) || !shouldSync) { + Logger.Info($"Not in persistent bool save data values or false in save data values, not sending save update ({itemData.Id}, {itemData.SceneName})"); + continue; + } + + if (!_saveData.PersistentBoolDataIndices.TryGetValue(itemData, out var index)) { + Logger.Info($"Cannot find persistent bool save data index, not sending save update ({itemData.Id}, {itemData.SceneName})"); + continue; + } + + Logger.Info($"Sending persistent bool ({itemData.Id}, {itemData.SceneName}) as save update"); + + _netClient.UpdateManager.SetSaveUpdate( + index, + BitConverter.GetBytes(value) + ); + } + } + } + + /// + /// Callback method for when a save update is received. + /// + /// The save update that was received. + private void UpdateSaveWithData(SaveUpdate saveUpdate) { + Logger.Info($"Received save update for index: {saveUpdate.SaveDataIndex}"); + + var pd = PlayerData.instance; + var sceneData = SceneData.instance; + + if (_saveData.PlayerDataIndices.TryGetValue(saveUpdate.SaveDataIndex, out var name)) { + var fieldInfo = typeof(PlayerData).GetField(name); + var type = fieldInfo.FieldType; + var valueLength = saveUpdate.Value.Length; + + if (type == typeof(bool)) { + if (valueLength != 1) { + Logger.Warn($"Received save update with incorrect value length for bool: {valueLength}"); + } + + var value = saveUpdate.Value[0] == 1; + + pd.SetBoolInternal(name, value); + } else if (type == typeof(float)) { + if (valueLength != 4) { + Logger.Warn($"Received save update with incorrect value length for float: {valueLength}"); + } + + var value = BitConverter.ToSingle(saveUpdate.Value, 0); + + pd.SetFloatInternal(name, value); + } else if (type == typeof(int)) { + if (valueLength != 4) { + Logger.Warn($"Received save update with incorrect value length for int: {valueLength}"); + } + + var value = BitConverter.ToInt32(saveUpdate.Value, 0); + + pd.SetIntInternal(name, value); + } else if (type == typeof(string)) { + var value = Encoding.UTF8.GetString(saveUpdate.Value); + + pd.SetStringInternal(name, value); + } else if (type == typeof(Vector3)) { + if (valueLength != 12) { + Logger.Warn($"Received save update with incorrect value length for vector3: {valueLength}"); + } + + var value = new Vector3( + BitConverter.ToSingle(saveUpdate.Value, 0), + BitConverter.ToSingle(saveUpdate.Value, 4), + BitConverter.ToSingle(saveUpdate.Value, 8) + ); + + pd.SetVector3Internal(name, value); + } + } else if (_saveData.GeoRockDataIndices.TryGetValue(saveUpdate.SaveDataIndex, out var itemData)) { + var value = saveUpdate.Value[0]; - var value = BitConverter.ToInt32(saveUpdate.Value, 0); + Logger.Info($"Received geo rock save update: {itemData.Id}, {itemData.SceneName}, {value}"); - pd.SetIntInternal(name, value); - } else if (type == typeof(string)) { - var value = Encoding.UTF8.GetString(saveUpdate.Value); + sceneData.SaveMyState(new GeoRockData { + id = itemData.Id, + sceneName = itemData.SceneName, + hitsLeft = value + }); + } else if (_saveData.PersistentBoolDataIndices.TryGetValue(saveUpdate.SaveDataIndex, out itemData)) { + var value = saveUpdate.Value[0] == 1; + + Logger.Info($"Received persistent bool save update: {itemData.Id}, {itemData.SceneName}, {value}"); - pd.SetStringInternal(name, value); - } else if (type == typeof(Vector3)) { - if (valueLength != 12) { - Logger.Warn($"Received save update with incorrect value length for vector3: {valueLength}"); + sceneData.SaveMyState(new PersistentBoolData { + id = itemData.Id, + sceneName = itemData.SceneName, + activated = value + }); + } else if (_saveData.PersistentIntDataIndices.TryGetValue(saveUpdate.SaveDataIndex, out itemData)) { + var value = (int) saveUpdate.Value[0]; + // Add a special case for the -1 value that some persistent ints might have + // 255 is never used in the byte space, so we use it for compact networking + if (value == 255) { + value = -1; } - var value = new Vector3( - BitConverter.ToSingle(saveUpdate.Value, 0), - BitConverter.ToSingle(saveUpdate.Value, 4), - BitConverter.ToSingle(saveUpdate.Value, 8) - ); + Logger.Info($"Received persistent int save update: {itemData.Id}, {itemData.SceneName}, {value}"); - pd.SetVector3Internal(name, value); + sceneData.SaveMyState(new PersistentIntData { + id = itemData.Id, + sceneName = itemData.SceneName, + value = value + }); } } } diff --git a/HKMP/Resource/save-data.json b/HKMP/Resource/save-data.json index 93269f98..aaad02f7 100644 --- a/HKMP/Resource/save-data.json +++ b/HKMP/Resource/save-data.json @@ -1,1276 +1,19169 @@ { - "heartPieces": false, - "heartPieceMax": false, - "geo": false, - "vesselFragments": false, - "vesselFragmentMax": false, - "dreamgateMapPos": false, - "geoPool": false, - "hasSpell": false, - "fireballLevel": false, - "quakeLevel": false, - "screamLevel": false, - "hasNailArt": false, - "hasCyclone": false, - "hasDashSlash": false, - "hasUpwardSlash": false, - "hasAllNailArts": false, - "hasDreamNail": false, - "hasDreamGate": false, - "dreamNailUpgraded": false, - "dreamOrbs": false, - "dreamOrbsSpent": false, - "dreamGateScene": false, - "dreamGateX": false, - "dreamGateY": false, - "hasDash": false, - "hasWalljump": false, - "hasSuperDash": false, - "hasShadowDash": false, - "hasAcidArmour": false, - "hasDoubleJump": false, - "hasLantern": false, - "hasTramPass": false, - "hasQuill": false, - "hasCityKey": false, - "hasSlykey": false, - "gaveSlykey": false, - "hasWhiteKey": false, - "usedWhiteKey": false, - "hasMenderKey": true, - "hasWaterwaysKey": false, - "hasSpaKey": false, - "hasLoveKey": false, - "hasKingsBrand": false, - "hasXunFlower": false, - "ghostCoins": false, - "ore": false, - "foundGhostCoin": false, - "trinket1": false, - "foundTrinket1": false, - "trinket2": false, - "foundTrinket2": false, - "trinket3": false, - "foundTrinket3": false, - "trinket4": false, - "foundTrinket4": false, - "noTrinket1": false, - "noTrinket2": false, - "noTrinket3": false, - "noTrinket4": false, - "soldTrinket1": false, - "soldTrinket2": false, - "soldTrinket3": false, - "soldTrinket4": false, - "simpleKeys": false, - "rancidEggs": false, - "notchShroomOgres": false, - "notchFogCanyon": false, - "gotLurkerKey": false, - "guardiansDefeated": false, - "lurienDefeated": true, - "hegemolDefeated": true, - "monomonDefeated": true, - "maskBrokenLurien": true, - "maskBrokenHegemol": true, - "maskBrokenMonomon": true, - "maskToBreak": false, - "elderbug": false, - "metElderbug": false, - "elderbugReintro": false, - "elderbugHistory": false, - "elderbugHistory1": false, - "elderbugHistory2": false, - "elderbugHistory3": false, - "elderbugSpeechSly": false, - "elderbugSpeechStation": false, - "elderbugSpeechEggTemple": false, - "elderbugSpeechMapShop": false, - "elderbugSpeechBretta": false, - "elderbugSpeechJiji": false, - "elderbugSpeechMinesLift": false, - "elderbugSpeechKingsPass": false, - "elderbugSpeechInfectedCrossroads": false, - "elderbugSpeechFinalBossDoor": false, - "elderbugRequestedFlower": false, - "elderbugGaveFlower": false, - "elderbugFirstCall": false, - "metQuirrel": true, - "quirrelEggTemple": true, - "quirrelSlugShrine": true, - "quirrelRuins": true, - "quirrelMines": true, - "quirrelLeftStation": true, - "quirrelLeftEggTemple": true, - "quirrelCityEncountered": true, - "quirrelCityLeft": true, - "quirrelMinesEncountered": true, - "quirrelMinesLeft": true, - "quirrelMantisEncountered": true, - "enteredMantisLordArea": true, - "visitedDeepnestSpa": true, - "quirrelSpaReady": true, - "quirrelSpaEncountered": true, - "quirrelArchiveEncountered": true, - "quirrelEpilogueCompleted": true, - "metRelicDealer": false, - "metRelicDealerShop": false, - "marmOutside": true, - "marmOutsideConvo": false, - "marmConvo1": false, - "marmConvo2": false, - "marmConvo3": false, - "marmConvoNailsmith": false, - "cornifer": true, - "metCornifer": false, - "corniferIntroduced": false, - "corniferAtHome": true, - "corn_crossroadsEncountered": true, - "corn_crossroadsLeft": true, - "corn_greenpathEncountered": true, - "corn_greenpathLeft": true, - "corn_fogCanyonEncountered": true, - "corn_fogCanyonLeft": true, - "corn_fungalWastesEncountered": true, - "corn_fungalWastesLeft": true, - "corn_cityEncountered": true, - "corn_cityLeft": true, - "corn_waterwaysEncountered": true, - "corn_waterwaysLeft": true, - "corn_minesEncountered": true, - "corn_minesLeft": true, - "corn_cliffsEncountered": true, - "corn_cliffsLeft": true, - "corn_deepnestEncountered": true, - "corn_deepnestLeft": true, - "corn_deepnestMet1": true, - "corn_deepnestMet2": true, - "corn_outskirtsEncountered": true, - "corn_outskirtsLeft": true, - "corn_royalGardensEncountered": true, - "corn_royalGardensLeft": true, - "corn_abyssEncountered": true, - "corn_abyssLeft": true, - "metIselda": false, - "iseldaCorniferHomeConvo": false, - "iseldaConvo1": false, - "brettaRescued": true, - "brettaPosition": true, - "brettaState": true, - "brettaSeenBench": true, - "brettaSeenBed": true, - "brettaSeenBenchDiary": true, - "brettaSeenBedDiary": true, - "brettaLeftTown": true, - "slyRescued": true, - "slyBeta": true, - "metSlyShop": false, - "gotSlyCharm": false, - "slyShellFrag1": false, - "slyShellFrag2": false, - "slyShellFrag3": false, - "slyShellFrag4": false, - "slyVesselFrag1": false, - "slyVesselFrag2": false, - "slyVesselFrag3": false, - "slyVesselFrag4": false, - "slyNotch1": false, - "slyNotch2": false, - "slySimpleKey": false, - "slyRancidEgg": false, - "slyConvoNailArt": false, - "slyConvoMapper": false, - "slyConvoNailHoned": false, - "jijiDoorUnlocked": true, - "jijiMet": false, - "jijiShadeOffered": false, - "jijiShadeCharmConvo": false, - "metJinn": false, - "jinnConvo1": false, - "jinnConvo2": false, - "jinnConvo3": false, - "jinnConvoKingBrand": false, - "jinnConvoShadeCharm": false, - "jinnEggsSold": false, - "zote": true, - "zoteRescuedBuzzer": true, - "zoteDead": true, - "zoteDeathPos": true, - "zoteSpokenCity": true, - "zoteLeftCity": true, - "zoteTrappedDeepnest": true, - "zoteRescuedDeepnest": true, - "zoteDefeated": true, - "zoteSpokenColosseum": true, - "zotePrecept": false, - "zoteTownConvo": false, - "shaman": true, - "shamanScreamConvo": false, - "shamanQuakeConvo": false, - "shamanFireball2Convo": false, - "shamanScream2Convo": false, - "shamanQuake2Convo": false, - "metMiner": false, - "miner": true, - "minerEarly": false, - "hornetGreenpath": true, - "hornetFung": true, - "hornet_f19": true, - "hornetFountainEncounter": false, - "hornetCityBridge_ready": true, - "hornetCityBridge_completed": true, - "hornetAbyssEncounter": true, - "hornetDenEncounter": true, - "metMoth": false, - "ignoredMoth": false, - "gladeDoorOpened": true, - "mothDeparted": false, - "completedRGDreamPlant": false, - "dreamReward1": false, - "dreamReward2": false, - "dreamReward3": false, - "dreamReward4": false, - "dreamReward5": false, - "dreamReward5b": false, - "dreamReward6": false, - "dreamReward7": false, - "dreamReward8": false, - "dreamReward9": false, - "dreamMothConvo1": false, - "bankerAccountPurchased": false, - "metBanker": false, - "bankerBalance": false, - "bankerDeclined": false, - "bankerTheftCheck": true, - "bankerTheft": true, - "bankerSpaMet": false, - "metGiraffe": false, - "metCharmSlug": false, - "salubraNotch1": false, - "salubraNotch2": false, - "salubraNotch3": false, - "salubraNotch4": false, - "salubraBlessing": false, - "salubraConvoCombo": false, - "salubraConvoOvercharm": false, - "salubraConvoTruth": false, - "cultistTransformed": true, - "metNailsmith": false, - "nailSmithUpgrades": false, - "honedNail": false, - "nailsmithCliff": false, - "nailsmithKilled": false, - "nailsmithSpared": false, - "nailsmithKillSpeech": false, - "nailsmithSheo": true, - "nailsmithConvoArt": true, - "metNailmasterMato": false, - "metNailmasterSheo": false, - "metNailmasterOro": false, - "matoConvoSheo": false, - "matoConvoOro": false, - "matoConvoSly": false, - "sheoConvoMato": false, - "sheoConvoOro": false, - "sheoConvoSly": false, - "sheoConvoNailsmith": false, - "oroConvoSheo": false, - "oroConvoMato": false, - "oroConvoSly": false, - "hunterRoared": true, - "metHunter": false, - "hunterRewardOffered": false, - "huntersMarkOffered": false, - "hasHuntersMark": false, - "metLegEater": false, - "paidLegEater": false, - "refusedLegEater": false, - "legEaterConvo1": false, - "legEaterConvo2": false, - "legEaterConvo3": false, - "legEaterBrokenConvo": false, - "legEaterDungConvo": false, - "legEaterInfectedCrossroadConvo": false, - "legEaterBoughtConvo": false, - "legEaterGoldConvo": false, - "legEaterLeft": true, - "tukMet": false, - "tukEggPrice": false, - "tukDungEgg": false, - "metEmilitia": false, - "emilitiaKingsBrandConvo": false, - "metCloth": false, - "clothEnteredTramRoom": true, - "savedCloth": true, - "clothEncounteredQueensGarden": true, - "clothKilled": true, - "clothInTown": true, - "clothLeftTown": true, - "clothGhostSpoken": true, - "bigCatHitTail": false, - "bigCatHitTailConvo": false, - "bigCatMeet": false, - "bigCatTalk1": false, - "bigCatTalk2": false, - "bigCatTalk3": false, - "bigCatKingsBrandConvo": false, - "bigCatShadeConvo": false, - "tisoEncounteredTown": true, - "tisoEncounteredBench": true, - "tisoEncounteredLake": true, - "tisoEncounteredColosseum": true, - "tisoDead": true, - "tisoShieldConvo": true, - "mossCultist": true, - "maskmakerMet": false, - "maskmakerConvo1": false, - "maskmakerConvo2": false, - "maskmakerUnmasked1": false, - "maskmakerUnmasked2": false, - "maskmakerShadowDash": false, - "maskmakerKingsBrand": false, - "dungDefenderConvo1": false, - "dungDefenderConvo2": false, - "dungDefenderConvo3": false, - "dungDefenderCharmConvo": false, - "dungDefenderIsmaConvo": false, - "dungDefenderAwoken": true, - "dungDefenderLeft": true, - "dungDefenderAwakeConvo": false, - "midwifeMet": false, - "midwifeConvo1": false, - "midwifeConvo2": false, - "metQueen": false, - "queenTalk1": false, - "queenTalk2": false, - "queenDung1": false, - "queenDung2": false, - "queenHornet": false, - "queenTalkExtra": false, - "gotQueenFragment": false, - "queenConvo_grimm1": false, - "queenConvo_grimm2": false, - "gotKingFragment": false, - "metXun": false, - "xunFailedConvo1": false, - "xunFailedConvo2": false, - "xunFlowerBroken": false, - "xunFlowerBrokeTimes": false, - "xunFlowerGiven": false, - "xunRewardGiven": false, - "menderState": true, - "menderSignBroken": true, - "allBelieverTabletsDestroyed": true, - "mrMushroomState": true, - "openedMapperShop": true, - "openedSlyShop": true, - "metStag": false, - "stagPosition": true, - "stationsOpened": true, - "stagConvoTram": false, - "stagConvoTiso": false, - "stagRemember1": false, - "stagRemember2": false, - "stagRemember3": false, - "stagEggInspected": false, - "stagHopeConvo": false, - "littleFoolMet": false, - "ranAway": false, - "seenColosseumTitle": false, - "colosseumBronzeOpened": false, - "colosseumBronzeCompleted": false, - "colosseumSilverOpened": false, - "colosseumSilverCompleted": false, - "colosseumGoldOpened": false, - "colosseumGoldCompleted": false, - "openedTown": true, - "openedTownBuilding": true, - "openedCrossroads": true, - "openedGreenpath": true, - "openedRuins1": true, - "openedRuins2": true, - "openedFungalWastes": true, - "openedRoyalGardens": true, - "openedRestingGrounds": true, - "openedDeepnest": true, - "openedStagNest": true, - "openedHiddenStation": true, - "charmSlots": false, - "charmsOwned": false, - "gotCharm_1": false, - "gotCharm_2": false, - "gotCharm_3": false, - "gotCharm_4": false, - "gotCharm_5": false, - "gotCharm_6": false, - "gotCharm_7": false, - "gotCharm_8": false, - "gotCharm_9": false, - "gotCharm_10": false, - "gotCharm_11": false, - "gotCharm_12": false, - "gotCharm_13": false, - "gotCharm_14": false, - "gotCharm_15": false, - "gotCharm_16": false, - "gotCharm_17": false, - "gotCharm_18": false, - "gotCharm_19": false, - "gotCharm_20": false, - "gotCharm_21": false, - "gotCharm_22": false, - "gotCharm_23": false, - "gotCharm_24": false, - "gotCharm_25": false, - "gotCharm_26": false, - "gotCharm_27": false, - "gotCharm_28": false, - "gotCharm_29": false, - "gotCharm_30": false, - "gotCharm_31": false, - "gotCharm_32": false, - "gotCharm_33": false, - "gotCharm_34": false, - "gotCharm_35": false, - "gotCharm_36": false, - "gotCharm_37": false, - "gotCharm_38": false, - "gotCharm_39": false, - "gotCharm_40": false, - "fragileHealth_unbreakable": false, - "fragileGreed_unbreakable": false, - "fragileStrength_unbreakable": false, - "royalCharmState": false, - "hasJournal": false, - "seenJournalMsg": false, - "seenHunterMsg": false, - "fillJournal": false, - "journalEntriesCompleted": true, - "journalNotesCompleted": true, - "journalEntriesTotal": true, - "killedCrawler": true, - "killsCrawler": true, - "newDataCrawler": true, - "killedBuzzer": true, - "killsBuzzer": true, - "newDataBuzzer": true, - "killedBouncer": true, - "killsBouncer": true, - "newDataBouncer": true, - "killedClimber": true, - "killsClimber": true, - "newDataClimber": true, - "killedHopper": true, - "killsHopper": true, - "newDataHopper": true, - "killedWorm": true, - "killsWorm": true, - "newDataWorm": true, - "killedSpitter": true, - "killsSpitter": true, - "newDataSpitter": true, - "killedHatcher": true, - "killsHatcher": true, - "newDataHatcher": true, - "killedHatchling": true, - "killsHatchling": true, - "newDataHatchling": true, - "killedZombieRunner": true, - "killsZombieRunner": true, - "newDataZombieRunner": true, - "killedZombieHornhead": true, - "killsZombieHornhead": true, - "newDataZombieHornhead": true, - "killedZombieLeaper": true, - "killsZombieLeaper": true, - "newDataZombieLeaper": true, - "killedZombieBarger": true, - "killsZombieBarger": true, - "newDataZombieBarger": true, - "killedZombieShield": true, - "killsZombieShield": true, - "newDataZombieShield": true, - "killedZombieGuard": true, - "killsZombieGuard": true, - "newDataZombieGuard": true, - "killedBigBuzzer": true, - "killsBigBuzzer": true, - "newDataBigBuzzer": true, - "killedBigFly": true, - "killsBigFly": true, - "newDataBigFly": true, - "killedMawlek": true, - "killsMawlek": true, - "newDataMawlek": true, - "killedFalseKnight": true, - "killsFalseKnight": true, - "newDataFalseKnight": true, - "killedRoller": true, - "killsRoller": true, - "newDataRoller": true, - "killedBlocker": true, - "killsBlocker": true, - "newDataBlocker": true, - "killedPrayerSlug": true, - "killsPrayerSlug": true, - "newDataPrayerSlug": true, - "killedMenderBug": true, - "killsMenderBug": true, - "newDataMenderBug": true, - "killedMossmanRunner": true, - "killsMossmanRunner": true, - "newDataMossmanRunner": true, - "killedMossmanShaker": true, - "killsMossmanShaker": true, - "newDataMossmanShaker": true, - "killedMosquito": true, - "killsMosquito": true, - "newDataMosquito": true, - "killedBlobFlyer": true, - "killsBlobFlyer": true, - "newDataBlobFlyer": true, - "killedFungifiedZombie": true, - "killsFungifiedZombie": true, - "newDataFungifiedZombie": true, - "killedPlantShooter": true, - "killsPlantShooter": true, - "newDataPlantShooter": true, - "killedMossCharger": true, - "killsMossCharger": true, - "newDataMossCharger": true, - "killedMegaMossCharger": true, - "killsMegaMossCharger": true, - "newDataMegaMossCharger": true, - "killedSnapperTrap": true, - "killsSnapperTrap": true, - "newDataSnapperTrap": true, - "killedMossKnight": true, - "killsMossKnight": true, - "newDataMossKnight": true, - "killedGrassHopper": true, - "killsGrassHopper": true, - "newDataGrassHopper": true, - "killedAcidFlyer": true, - "killsAcidFlyer": true, - "newDataAcidFlyer": true, - "killedAcidWalker": true, - "killsAcidWalker": true, - "newDataAcidWalker": true, - "killedMossFlyer": true, - "killsMossFlyer": true, - "newDataMossFlyer": true, - "killedMossKnightFat": true, - "killsMossKnightFat": true, - "newDataMossKnightFat": true, - "killedMossWalker": true, - "killsMossWalker": true, - "newDataMossWalker": true, - "killedInfectedKnight": true, - "killsInfectedKnight": true, - "newDataInfectedKnight": true, - "killedLazyFlyer": true, - "killsLazyFlyer": true, - "newDataLazyFlyer": true, - "killedZapBug": true, - "killsZapBug": true, - "newDataZapBug": true, - "killedJellyfish": true, - "killsJellyfish": true, - "newDataJellyfish": true, - "killedJellyCrawler": true, - "killsJellyCrawler": true, - "newDataJellyCrawler": true, - "killedMegaJellyfish": true, - "killsMegaJellyfish": true, - "newDataMegaJellyfish": true, - "killedFungoonBaby": true, - "killsFungoonBaby": true, - "newDataFungoonBaby": true, - "killedMushroomTurret": true, - "killsMushroomTurret": true, - "newDataMushroomTurret": true, - "killedMantis": true, - "killsMantis": true, - "newDataMantis": true, - "killedMushroomRoller": true, - "killsMushroomRoller": true, - "newDataMushroomRoller": true, - "killedMushroomBrawler": true, - "killsMushroomBrawler": true, - "newDataMushroomBrawler": true, - "killedMushroomBaby": true, - "killsMushroomBaby": true, - "newDataMushroomBaby": true, - "killedMantisFlyerChild": true, - "killsMantisFlyerChild": true, - "newDataMantisFlyerChild": true, - "killedFungusFlyer": true, - "killsFungusFlyer": true, - "newDataFungusFlyer": true, - "killedFungCrawler": true, - "killsFungCrawler": true, - "newDataFungCrawler": true, - "killedMantisLord": true, - "killsMantisLord": true, - "newDataMantisLord": true, - "killedBlackKnight": true, - "killsBlackKnight": true, - "newDataBlackKnight": true, - "killedElectricMage": true, - "killsElectricMage": true, - "newDataElectricMage": true, - "killedMage": true, - "killsMage": true, - "newDataMage": true, - "killedMageKnight": true, - "killsMageKnight": true, - "newDataMageKnight": true, - "killedRoyalDandy": true, - "killsRoyalDandy": true, - "newDataRoyalDandy": true, - "killedRoyalCoward": true, - "killsRoyalCoward": true, - "newDataRoyalCoward": true, - "killedRoyalPlumper": true, - "killsRoyalPlumper": true, - "newDataRoyalPlumper": true, - "killedFlyingSentrySword": true, - "killsFlyingSentrySword": true, - "newDataFlyingSentrySword": true, - "killedFlyingSentryJavelin": true, - "killsFlyingSentryJavelin": true, - "newDataFlyingSentryJavelin": true, - "killedSentry": true, - "killsSentry": true, - "newDataSentry": true, - "killedSentryFat": true, - "killsSentryFat": true, - "newDataSentryFat": true, - "killedMageBlob": true, - "killsMageBlob": true, - "newDataMageBlob": true, - "killedGreatShieldZombie": true, - "killsGreatShieldZombie": true, - "newDataGreatShieldZombie": true, - "killedJarCollector": true, - "killsJarCollector": true, - "newDataJarCollector": true, - "killedMageBalloon": true, - "killsMageBalloon": true, - "newDataMageBalloon": true, - "killedMageLord": true, - "killsMageLord": true, - "newDataMageLord": true, - "killedGorgeousHusk": true, - "killsGorgeousHusk": true, - "newDataGorgeousHusk": true, - "killedFlipHopper": true, - "killsFlipHopper": true, - "newDataFlipHopper": true, - "killedFlukeman": true, - "killsFlukeman": true, - "newDataFlukeman": true, - "killedInflater": true, - "killsInflater": true, - "newDataInflater": true, - "killedFlukefly": true, - "killsFlukefly": true, - "newDataFlukefly": true, - "killedFlukeMother": true, - "killsFlukeMother": true, - "newDataFlukeMother": true, - "killedDungDefender": true, - "killsDungDefender": true, - "newDataDungDefender": true, - "killedCrystalCrawler": true, - "killsCrystalCrawler": true, - "newDataCrystalCrawler": true, - "killedCrystalFlyer": true, - "killsCrystalFlyer": true, - "newDataCrystalFlyer": true, - "killedLaserBug": true, - "killsLaserBug": true, - "newDataLaserBug": true, - "killedBeamMiner": true, - "killsBeamMiner": true, - "newDataBeamMiner": true, - "killedZombieMiner": true, - "killsZombieMiner": true, - "newDataZombieMiner": true, - "killedMegaBeamMiner": true, - "killsMegaBeamMiner": true, - "newDataMegaBeamMiner": true, - "killedMinesCrawler": true, - "killsMinesCrawler": true, - "newDataMinesCrawler": true, - "killedAngryBuzzer": true, - "killsAngryBuzzer": true, - "newDataAngryBuzzer": true, - "killedBurstingBouncer": true, - "killsBurstingBouncer": true, - "newDataBurstingBouncer": true, - "killedBurstingZombie": true, - "killsBurstingZombie": true, - "newDataBurstingZombie": true, - "killedSpittingZombie": true, - "killsSpittingZombie": true, - "newDataSpittingZombie": true, - "killedBabyCentipede": true, - "killsBabyCentipede": true, - "newDataBabyCentipede": true, - "killedBigCentipede": true, - "killsBigCentipede": true, - "newDataBigCentipede": true, - "killedCentipedeHatcher": true, - "killsCentipedeHatcher": true, - "newDataCentipedeHatcher": true, - "killedLesserMawlek": true, - "killsLesserMawlek": true, - "newDataLesserMawlek": true, - "killedSlashSpider": true, - "killsSlashSpider": true, - "newDataSlashSpider": true, - "killedSpiderCorpse": true, - "killsSpiderCorpse": true, - "newDataSpiderCorpse": true, - "killedShootSpider": true, - "killsShootSpider": true, - "newDataShootSpider": true, - "killedMiniSpider": true, - "killsMiniSpider": true, - "newDataMiniSpider": true, - "killedSpiderFlyer": true, - "killsSpiderFlyer": true, - "newDataSpiderFlyer": true, - "killedMimicSpider": true, - "killsMimicSpider": true, - "newDataMimicSpider": true, - "killedBeeHatchling": true, - "killsBeeHatchling": true, - "newDataBeeHatchling": true, - "killedBeeStinger": true, - "killsBeeStinger": true, - "newDataBeeStinger": true, - "killedBigBee": true, - "killsBigBee": true, - "newDataBigBee": true, - "killedHiveKnight": true, - "killsHiveKnight": true, - "newDataHiveKnight": true, - "killedBlowFly": true, - "killsBlowFly": true, - "newDataBlowFly": true, - "killedCeilingDropper": true, - "killsCeilingDropper": true, - "newDataCeilingDropper": true, - "killedGiantHopper": true, - "killsGiantHopper": true, - "newDataGiantHopper": true, - "killedGrubMimic": true, - "killsGrubMimic": true, - "newDataGrubMimic": true, - "killedMawlekTurret": true, - "killsMawlekTurret": true, - "newDataMawlekTurret": true, - "killedOrangeScuttler": true, - "killsOrangeScuttler": true, - "newDataOrangeScuttler": true, - "killedHealthScuttler": true, - "killsHealthScuttler": true, - "newDataHealthScuttler": true, - "killedPigeon": true, - "killsPigeon": true, - "newDataPigeon": true, - "killedZombieHive": true, - "killsZombieHive": true, - "newDataZombieHive": true, - "killedDreamGuard": true, - "killsDreamGuard": true, - "newDataDreamGuard": true, - "killedHornet": true, - "killsHornet": true, - "newDataHornet": true, - "killedAbyssCrawler": true, - "killsAbyssCrawler": true, - "newDataAbyssCrawler": true, - "killedSuperSpitter": true, - "killsSuperSpitter": true, - "newDataSuperSpitter": true, - "killedSibling": true, - "killsSibling": true, - "newDataSibling": true, - "killedPalaceFly": true, - "killsPalaceFly": true, - "newDataPalaceFly": true, - "killedEggSac": true, - "killsEggSac": true, - "newDataEggSac": true, - "killedMummy": true, - "killsMummy": true, - "newDataMummy": true, - "killedOrangeBalloon": true, - "killsOrangeBalloon": true, - "newDataOrangeBalloon": true, - "killedAbyssTendril": true, - "killsAbyssTendril": true, - "newDataAbyssTendril": true, - "killedHeavyMantis": true, - "killsHeavyMantis": true, - "newDataHeavyMantis": true, - "killedTraitorLord": true, - "killsTraitorLord": true, - "newDataTraitorLord": true, - "killedMantisHeavyFlyer": true, - "killsMantisHeavyFlyer": true, - "newDataMantisHeavyFlyer": true, - "killedGardenZombie": true, - "killsGardenZombie": true, - "newDataGardenZombie": true, - "killedRoyalGuard": true, - "killsRoyalGuard": true, - "newDataRoyalGuard": true, - "killedWhiteRoyal": true, - "killsWhiteRoyal": true, - "newDataWhiteRoyal": true, - "openedPalaceGrounds": true, - "killedOblobble": true, - "killsOblobble": true, - "newDataOblobble": true, - "killedZote": true, - "killsZote": true, - "newDataZote": true, - "killedBlobble": true, - "killsBlobble": true, - "newDataBlobble": true, - "killedColMosquito": true, - "killsColMosquito": true, - "newDataColMosquito": true, - "killedColRoller": true, - "killsColRoller": true, - "newDataColRoller": true, - "killedColFlyingSentry": true, - "killsColFlyingSentry": true, - "newDataColFlyingSentry": true, - "killedColMiner": true, - "killsColMiner": true, - "newDataColMiner": true, - "killedColShield": true, - "killsColShield": true, - "newDataColShield": true, - "killedColWorm": true, - "killsColWorm": true, - "newDataColWorm": true, - "killedColHopper": true, - "killsColHopper": true, - "newDataColHopper": true, - "killedLobsterLancer": true, - "killsLobsterLancer": true, - "newDataLobsterLancer": true, - "killedGhostAladar": true, - "killsGhostAladar": true, - "newDataGhostAladar": true, - "killedGhostXero": true, - "killsGhostXero": true, - "newDataGhostXero": true, - "killedGhostHu": true, - "killsGhostHu": true, - "newDataGhostHu": true, - "killedGhostMarmu": true, - "killsGhostMarmu": true, - "newDataGhostMarmu": true, - "killedGhostNoEyes": true, - "killsGhostNoEyes": true, - "newDataGhostNoEyes": true, - "killedGhostMarkoth": true, - "killsGhostMarkoth": true, - "newDataGhostMarkoth": true, - "killedGhostGalien": true, - "killsGhostGalien": true, - "newDataGhostGalien": true, - "killedWhiteDefender": true, - "killsWhiteDefender": true, - "newDataWhiteDefender": true, - "killedGreyPrince": true, - "killsGreyPrince": true, - "newDataGreyPrince": true, - "killedZotelingBalloon": true, - "killsZotelingBalloon": true, - "newDataZotelingBalloon": true, - "killedZotelingHopper": true, - "killsZotelingHopper": true, - "newDataZotelingHopper": true, - "killedZotelingBuzzer": true, - "killsZotelingBuzzer": true, - "newDataZotelingBuzzer": true, - "killedHollowKnight": true, - "killsHollowKnight": true, - "newDataHollowKnight": true, - "killedFinalBoss": true, - "killsFinalBoss": true, - "newDataFinalBoss": true, - "killedHunterMark": true, - "killsHunterMark": true, - "newDataHunterMark": true, - "killedFlameBearerSmall": true, - "killsFlameBearerSmall": true, - "newDataFlameBearerSmall": true, - "killedFlameBearerMed": true, - "killsFlameBearerMed": true, - "newDataFlameBearerMed": true, - "killedFlameBearerLarge": true, - "killsFlameBearerLarge": true, - "newDataFlameBearerLarge": true, - "killedGrimm": true, - "killsGrimm": true, - "newDataGrimm": true, - "killedNightmareGrimm": true, - "killsNightmareGrimm": true, - "newDataNightmareGrimm": true, - "killedBindingSeal": true, - "killsBindingSeal": true, - "newDataBindingSeal": true, - "killedFatFluke": true, - "killsFatFluke": true, - "newDataFatFluke": true, - "killedPaleLurker": true, - "killsPaleLurker": true, - "newDataPaleLurker": true, - "killedNailBros": true, - "killsNailBros": true, - "newDataNailBros": true, - "killedPaintmaster": true, - "killsPaintmaster": true, - "newDataPaintmaster": true, - "killedNailsage": true, - "killsNailsage": true, - "newDataNailsage": true, - "killedHollowKnightPrime": true, - "killsHollowKnightPrime": true, - "newDataHollowKnightPrime": true, - "killedGodseekerMask": true, - "killsGodseekerMask": true, - "newDataGodseekerMask": true, - "killedVoidIdol_1": true, - "killsVoidIdol_1": true, - "newDataVoidIdol_1": true, - "killedVoidIdol_2": true, - "killsVoidIdol_2": true, - "newDataVoidIdol_2": true, - "killedVoidIdol_3": true, - "killsVoidIdol_3": true, - "newDataVoidIdol_3": true, - "grubsCollected": true, - "grubRewards": false, - "finalGrubRewardCollected": false, - "fatGrubKing": false, - "falseKnightDefeated": true, - "falseKnightDreamDefeated": true, - "falseKnightOrbsCollected": false, - "mawlekDefeated": true, - "giantBuzzerDefeated": true, - "giantFlyDefeated": true, - "blocker1Defeated": true, - "blocker2Defeated": true, - "hornet1Defeated": true, - "collectorDefeated": true, - "hornetOutskirtsDefeated": true, - "mageLordDreamDefeated": true, - "mageLordOrbsCollected": false, - "infectedKnightDreamDefeated": true, - "infectedKnightOrbsCollected": false, - "whiteDefenderDefeated": true, - "whiteDefenderOrbsCollected": false, - "whiteDefenderDefeats": true, - "greyPrinceDefeats": true, - "greyPrinceDefeated": true, - "greyPrinceOrbsCollected": false, - "aladarSlugDefeated": true, - "xeroDefeated": true, - "elderHuDefeated": true, - "mumCaterpillarDefeated": true, - "noEyesDefeated": true, - "markothDefeated": true, - "galienDefeated": true, - "XERO_encountered": false, - "ALADAR_encountered": false, - "HU_encountered": false, - "MUMCAT_encountered": false, - "NOEYES_encountered": false, - "MARKOTH_encountered": false, - "GALIEN_encountered": false, - "xeroPinned": true, - "aladarPinned": true, - "huPinned": true, - "mumCaterpillarPinned": true, - "noEyesPinned": true, - "markothPinned": true, - "galienPinned": true, - "scenesVisited": true, - "scenesMapped": true, - "scenesEncounteredBench": true, - "scenesGrubRescued": true, - "scenesFlameCollected": true, - "scenesEncounteredCocoon": true, - "scenesEncounteredDreamPlant": true, - "scenesEncounteredDreamPlantC": false, - "hasMap": false, - "mapDirtmouth": false, - "mapCrossroads": false, - "mapGreenpath": false, - "mapFogCanyon": false, - "mapRoyalGardens": false, - "mapFungalWastes": false, - "mapCity": false, - "mapWaterways": false, - "mapMines": false, - "mapDeepnest": false, - "mapCliffs": false, - "mapOutskirts": false, - "mapRestingGrounds": false, - "mapAbyss": false, - "mapZoneBools": true, - "hasPin": false, - "hasPinBench": false, - "hasPinCocoon": false, - "hasPinDreamPlant": false, - "hasPinGuardian": false, - "hasPinBlackEgg": false, - "hasPinShop": false, - "hasPinSpa": false, - "hasPinStag": false, - "hasPinTram": false, - "hasPinGhost": false, - "hasPinGrub": false, - "hasMarker": false, - "hasMarker_r": false, - "hasMarker_b": false, - "hasMarker_y": false, - "hasMarker_w": false, - "openedTramLower": false, - "openedTramRestingGrounds": false, - "tramLowerPosition": true, - "tramRestingGroundsPosition": true, - "mineLiftOpened": true, - "menderDoorOpened": true, - "vesselFragStagNest": false, - "shamanPillar": true, - "crossroadsMawlekWall": true, - "eggTempleVisited": false, - "crossroadsInfected": true, - "falseKnightFirstPlop": true, - "falseKnightWallRepaired": true, - "falseKnightWallBroken": true, - "falseKnightGhostDeparted": true, - "spaBugsEncountered": true, - "hornheadVinePlat": true, - "infectedKnightEncountered": true, - "megaMossChargerEncountered": true, - "megaMossChargerDefeated": true, - "dreamerScene1": true, - "slugEncounterComplete": true, - "defeatedDoubleBlockers": true, - "oneWayArchive": true, - "defeatedMegaJelly": true, - "summonedMonomon": true, - "sawWoundedQuirrel": true, - "encounteredMegaJelly": true, - "defeatedMantisLords": true, - "encounteredGatekeeper": true, - "deepnestWall": true, - "queensStationNonDisplay": true, - "cityBridge1": true, - "cityBridge2": true, - "cityLift1": true, - "cityLift1_isUp": true, - "liftArrival": true, - "openedMageDoor": true, - "openedMageDoor_v2": true, - "brokenMageWindow": true, - "brokenMageWindowGlass": true, - "mageLordEncountered": true, - "mageLordEncountered_2": true, - "mageLordDefeated": true, - "ruins1_5_tripleDoor": true, - "openedCityGate": true, - "cityGateClosed": true, - "bathHouseOpened": true, - "bathHouseWall": true, - "cityLift2": true, - "cityLift2_isUp": true, - "city2_sewerDoor": true, - "openedLoveDoor": true, - "watcherChandelier": true, - "completedQuakeArea": true, - "kingsStationNonDisplay": true, - "tollBenchCity": true, - "waterwaysGate": true, - "defeatedDungDefender": true, - "dungDefenderEncounterReady": true, - "flukeMotherEncountered": true, - "flukeMotherDefeated": true, - "openedWaterwaysManhole": true, - "waterwaysAcidDrained": true, - "dungDefenderWallBroken": true, - "dungDefenderSleeping": true, - "defeatedMegaBeamMiner": true, - "defeatedMegaBeamMiner2": true, - "brokeMinersWall": true, - "encounteredMimicSpider": true, - "steppedBeyondBridge": true, - "deepnestBridgeCollapsed": true, - "spiderCapture": false, - "deepnest26b_switch": true, - "openedRestingGrounds02": true, - "restingGroundsCryptWall": true, - "dreamNailConvo": false, - "gladeGhostsKilled": true, - "openedGardensStagStation": true, - "extendedGramophone": true, - "tollBenchQueensGardens": true, - "blizzardEnded": true, - "encounteredHornet": true, - "savedByHornet": true, - "outskirtsWall": true, - "abyssGateOpened": true, - "abyssLighthouse": true, - "blueVineDoor": true, - "gotShadeCharm": true, - "tollBenchAbyss": true, - "fountainGeo": false, - "fountainVesselSummoned": false, - "openedBlackEggPath": true, - "enteredDreamWorld": false, - "duskKnightDefeated": true, - "whitePalaceOrb_1": true, - "whitePalaceOrb_2": true, - "whitePalaceOrb_3": true, - "whitePalace05_lever": true, - "whitePalaceMidWarp": true, - "whitePalaceSecretRoomVisited": true, - "tramOpenedDeepnest": true, - "tramOpenedCrossroads": true, - "openedBlackEggDoor": true, - "unchainedHollowKnight": true, - "flamesCollected": true, - "flamesRequired": true, - "nightmareLanternAppeared": true, - "nightmareLanternLit": true, - "troupeInTown": true, - "divineInTown": true, - "grimmChildLevel": true, - "elderbugConvoGrimm": false, - "slyConvoGrimm": false, - "iseldaConvoGrimm": false, - "midwifeWeaverlingConvo": false, - "metGrimm": true, - "foughtGrimm": true, - "metBrum": false, - "defeatedNightmareGrimm": true, - "grimmchildAwoken": true, - "gotBrummsFlame": true, - "brummBrokeBrazier": true, - "destroyedNightmareLantern": true, - "gotGrimmNotch": false, - "nymmInTown": true, - "nymmSpoken": false, - "nymmCharmConvo": false, - "nymmFinalConvo": false, - "elderbugNymmConvo": false, - "slyNymmConvo": false, - "iseldaNymmConvo": false, - "nymmMissedEggOpen": false, - "elderbugTroupeLeftConvo": false, - "elderbugBrettaLeft": false, - "jijiGrimmConvo": false, - "metDivine": false, - "divineFinalConvo": false, - "gaveFragileHeart": false, - "gaveFragileGreed": false, - "gaveFragileStrength": false, - "divineEatenConvos": false, - "pooedFragileHeart": false, - "pooedFragileGreed": false, - "pooedFragileStrength": false, - "completionPercentage": false, - "unlockedCompletionRate": false, - "newDatTraitorLord": true, - "bossDoorStateTier1": true, - "bossDoorStateTier2": true, - "bossDoorStateTier3": true, - "bossDoorStateTier4": true, - "bossDoorStateTier5": true, - "bossStatueTargetLevel": false, - "statueStateGruzMother": true, - "statueStateVengefly": true, - "statueStateBroodingMawlek": true, - "statueStateFalseKnight": true, - "statueStateFailedChampion": true, - "statueStateHornet1": true, - "statueStateHornet2": true, - "statueStateMegaMossCharger": true, - "statueStateMantisLords": true, - "statueStateOblobbles": true, - "statueStateGreyPrince": true, - "statueStateBrokenVessel": true, - "statueStateLostKin": true, - "statueStateNosk": true, - "statueStateFlukemarm": true, - "statueStateCollector": true, - "statueStateWatcherKnights": true, - "statueStateSoulMaster": true, - "statueStateSoulTyrant": true, - "statueStateGodTamer": true, - "statueStateCrystalGuardian1": true, - "statueStateCrystalGuardian2": true, - "statueStateUumuu": true, - "statueStateDungDefender": true, - "statueStateWhiteDefender": true, - "statueStateHiveKnight": true, - "statueStateTraitorLord": true, - "statueStateGrimm": true, - "statueStateNightmareGrimm": true, - "statueStateHollowKnight": true, - "statueStateElderHu": true, - "statueStateGalien": true, - "statueStateMarkoth": true, - "statueStateMarmu": true, - "statueStateNoEyes": true, - "statueStateXero": true, - "statueStateGorb": true, - "statueStateRadiance": true, - "statueStateSly": true, - "statueStateNailmasters": true, - "statueStateMageKnight": true, - "statueStatePaintmaster": true, - "statueStateZote": true, - "statueStateNoskHornet": true, - "statueStateMantisLordsExtra": true, - "godseekerUnlocked": true, - "bossDoorCageUnlocked": true, - "blueRoomDoorUnlocked": true, - "blueRoomActivated": true, - "finalBossDoorUnlocked": true, - "hasGodfinder": false, - "unlockedNewBossStatue": true, - "scaredFlukeHermitEncountered": false, - "scaredFlukeHermitReturned": false, - "enteredGGAtrium": false, - "extraFlowerAppear": true, - "givenGodseekerFlower": true, - "givenOroFlower": true, - "givenWhiteLadyFlower": true, - "givenEmilitiaFlower": true, - "unlockedBossScenes": false, - "queuedGodfinderIcon": false, - "godseekerSpokenAwake": false, - "nailsmithCorpseAppeared": true, - "godseekerWaterwaysSeenState": true, - "godseekerWaterwaysSpoken1": false, - "godseekerWaterwaysSpoken2": false, - "godseekerWaterwaysSpoken3": false, - "bossDoorEntranceTextSeen": false, - "seenDoor4Finale": true, - "zoteStatueWallBroken": true, - "seenGGWastes": false, - "ordealAchieved": true + "playerData": { + "heartPieces": false, + "heartPieceMax": false, + "geo": false, + "vesselFragments": false, + "vesselFragmentMax": false, + "dreamgateMapPos": false, + "geoPool": false, + "hasSpell": false, + "fireballLevel": false, + "quakeLevel": false, + "screamLevel": false, + "hasNailArt": false, + "hasCyclone": false, + "hasDashSlash": false, + "hasUpwardSlash": false, + "hasAllNailArts": false, + "hasDreamNail": false, + "hasDreamGate": false, + "dreamNailUpgraded": false, + "dreamOrbs": false, + "dreamOrbsSpent": false, + "dreamGateScene": false, + "dreamGateX": false, + "dreamGateY": false, + "hasDash": false, + "hasWalljump": false, + "hasSuperDash": false, + "hasShadowDash": false, + "hasAcidArmour": false, + "hasDoubleJump": false, + "hasLantern": false, + "hasTramPass": false, + "hasQuill": false, + "hasCityKey": false, + "hasSlykey": false, + "gaveSlykey": false, + "hasWhiteKey": false, + "usedWhiteKey": false, + "hasMenderKey": true, + "hasWaterwaysKey": false, + "hasSpaKey": false, + "hasLoveKey": false, + "hasKingsBrand": false, + "hasXunFlower": false, + "ghostCoins": false, + "ore": false, + "foundGhostCoin": false, + "trinket1": false, + "foundTrinket1": false, + "trinket2": false, + "foundTrinket2": false, + "trinket3": false, + "foundTrinket3": false, + "trinket4": false, + "foundTrinket4": false, + "noTrinket1": false, + "noTrinket2": false, + "noTrinket3": false, + "noTrinket4": false, + "soldTrinket1": false, + "soldTrinket2": false, + "soldTrinket3": false, + "soldTrinket4": false, + "simpleKeys": false, + "rancidEggs": false, + "notchShroomOgres": false, + "notchFogCanyon": false, + "gotLurkerKey": false, + "guardiansDefeated": false, + "lurienDefeated": true, + "hegemolDefeated": true, + "monomonDefeated": true, + "maskBrokenLurien": true, + "maskBrokenHegemol": true, + "maskBrokenMonomon": true, + "maskToBreak": false, + "elderbug": false, + "metElderbug": false, + "elderbugReintro": false, + "elderbugHistory": false, + "elderbugHistory1": false, + "elderbugHistory2": false, + "elderbugHistory3": false, + "elderbugSpeechSly": false, + "elderbugSpeechStation": false, + "elderbugSpeechEggTemple": false, + "elderbugSpeechMapShop": false, + "elderbugSpeechBretta": false, + "elderbugSpeechJiji": false, + "elderbugSpeechMinesLift": false, + "elderbugSpeechKingsPass": false, + "elderbugSpeechInfectedCrossroads": false, + "elderbugSpeechFinalBossDoor": false, + "elderbugRequestedFlower": false, + "elderbugGaveFlower": false, + "elderbugFirstCall": false, + "metQuirrel": true, + "quirrelEggTemple": true, + "quirrelSlugShrine": true, + "quirrelRuins": true, + "quirrelMines": true, + "quirrelLeftStation": true, + "quirrelLeftEggTemple": true, + "quirrelCityEncountered": true, + "quirrelCityLeft": true, + "quirrelMinesEncountered": true, + "quirrelMinesLeft": true, + "quirrelMantisEncountered": true, + "enteredMantisLordArea": true, + "visitedDeepnestSpa": true, + "quirrelSpaReady": true, + "quirrelSpaEncountered": true, + "quirrelArchiveEncountered": true, + "quirrelEpilogueCompleted": true, + "metRelicDealer": false, + "metRelicDealerShop": false, + "marmOutside": true, + "marmOutsideConvo": false, + "marmConvo1": false, + "marmConvo2": false, + "marmConvo3": false, + "marmConvoNailsmith": false, + "cornifer": true, + "metCornifer": false, + "corniferIntroduced": false, + "corniferAtHome": true, + "corn_crossroadsEncountered": true, + "corn_crossroadsLeft": true, + "corn_greenpathEncountered": true, + "corn_greenpathLeft": true, + "corn_fogCanyonEncountered": true, + "corn_fogCanyonLeft": true, + "corn_fungalWastesEncountered": true, + "corn_fungalWastesLeft": true, + "corn_cityEncountered": true, + "corn_cityLeft": true, + "corn_waterwaysEncountered": true, + "corn_waterwaysLeft": true, + "corn_minesEncountered": true, + "corn_minesLeft": true, + "corn_cliffsEncountered": true, + "corn_cliffsLeft": true, + "corn_deepnestEncountered": true, + "corn_deepnestLeft": true, + "corn_deepnestMet1": true, + "corn_deepnestMet2": true, + "corn_outskirtsEncountered": true, + "corn_outskirtsLeft": true, + "corn_royalGardensEncountered": true, + "corn_royalGardensLeft": true, + "corn_abyssEncountered": true, + "corn_abyssLeft": true, + "metIselda": false, + "iseldaCorniferHomeConvo": false, + "iseldaConvo1": false, + "brettaRescued": true, + "brettaPosition": true, + "brettaState": true, + "brettaSeenBench": true, + "brettaSeenBed": true, + "brettaSeenBenchDiary": true, + "brettaSeenBedDiary": true, + "brettaLeftTown": true, + "slyRescued": true, + "slyBeta": true, + "metSlyShop": false, + "gotSlyCharm": false, + "slyShellFrag1": false, + "slyShellFrag2": false, + "slyShellFrag3": false, + "slyShellFrag4": false, + "slyVesselFrag1": false, + "slyVesselFrag2": false, + "slyVesselFrag3": false, + "slyVesselFrag4": false, + "slyNotch1": false, + "slyNotch2": false, + "slySimpleKey": false, + "slyRancidEgg": false, + "slyConvoNailArt": false, + "slyConvoMapper": false, + "slyConvoNailHoned": false, + "jijiDoorUnlocked": true, + "jijiMet": false, + "jijiShadeOffered": false, + "jijiShadeCharmConvo": false, + "metJinn": false, + "jinnConvo1": false, + "jinnConvo2": false, + "jinnConvo3": false, + "jinnConvoKingBrand": false, + "jinnConvoShadeCharm": false, + "jinnEggsSold": false, + "zote": true, + "zoteRescuedBuzzer": true, + "zoteDead": true, + "zoteDeathPos": true, + "zoteSpokenCity": true, + "zoteLeftCity": true, + "zoteTrappedDeepnest": true, + "zoteRescuedDeepnest": true, + "zoteDefeated": true, + "zoteSpokenColosseum": true, + "zotePrecept": false, + "zoteTownConvo": false, + "shaman": true, + "shamanScreamConvo": false, + "shamanQuakeConvo": false, + "shamanFireball2Convo": false, + "shamanScream2Convo": false, + "shamanQuake2Convo": false, + "metMiner": false, + "miner": true, + "minerEarly": false, + "hornetGreenpath": true, + "hornetFung": true, + "hornet_f19": true, + "hornetFountainEncounter": false, + "hornetCityBridge_ready": true, + "hornetCityBridge_completed": true, + "hornetAbyssEncounter": true, + "hornetDenEncounter": true, + "metMoth": false, + "ignoredMoth": false, + "gladeDoorOpened": true, + "mothDeparted": false, + "completedRGDreamPlant": false, + "dreamReward1": false, + "dreamReward2": false, + "dreamReward3": false, + "dreamReward4": false, + "dreamReward5": false, + "dreamReward5b": false, + "dreamReward6": false, + "dreamReward7": false, + "dreamReward8": false, + "dreamReward9": false, + "dreamMothConvo1": false, + "bankerAccountPurchased": false, + "metBanker": false, + "bankerBalance": false, + "bankerDeclined": false, + "bankerTheftCheck": true, + "bankerTheft": true, + "bankerSpaMet": false, + "metGiraffe": false, + "metCharmSlug": false, + "salubraNotch1": false, + "salubraNotch2": false, + "salubraNotch3": false, + "salubraNotch4": false, + "salubraBlessing": false, + "salubraConvoCombo": false, + "salubraConvoOvercharm": false, + "salubraConvoTruth": false, + "cultistTransformed": true, + "metNailsmith": false, + "nailSmithUpgrades": false, + "honedNail": false, + "nailsmithCliff": false, + "nailsmithKilled": false, + "nailsmithSpared": false, + "nailsmithKillSpeech": false, + "nailsmithSheo": true, + "nailsmithConvoArt": true, + "metNailmasterMato": false, + "metNailmasterSheo": false, + "metNailmasterOro": false, + "matoConvoSheo": false, + "matoConvoOro": false, + "matoConvoSly": false, + "sheoConvoMato": false, + "sheoConvoOro": false, + "sheoConvoSly": false, + "sheoConvoNailsmith": false, + "oroConvoSheo": false, + "oroConvoMato": false, + "oroConvoSly": false, + "hunterRoared": true, + "metHunter": false, + "hunterRewardOffered": false, + "huntersMarkOffered": false, + "hasHuntersMark": false, + "metLegEater": false, + "paidLegEater": false, + "refusedLegEater": false, + "legEaterConvo1": false, + "legEaterConvo2": false, + "legEaterConvo3": false, + "legEaterBrokenConvo": false, + "legEaterDungConvo": false, + "legEaterInfectedCrossroadConvo": false, + "legEaterBoughtConvo": false, + "legEaterGoldConvo": false, + "legEaterLeft": true, + "tukMet": false, + "tukEggPrice": false, + "tukDungEgg": false, + "metEmilitia": false, + "emilitiaKingsBrandConvo": false, + "metCloth": false, + "clothEnteredTramRoom": true, + "savedCloth": true, + "clothEncounteredQueensGarden": true, + "clothKilled": true, + "clothInTown": true, + "clothLeftTown": true, + "clothGhostSpoken": true, + "bigCatHitTail": false, + "bigCatHitTailConvo": false, + "bigCatMeet": false, + "bigCatTalk1": false, + "bigCatTalk2": false, + "bigCatTalk3": false, + "bigCatKingsBrandConvo": false, + "bigCatShadeConvo": false, + "tisoEncounteredTown": true, + "tisoEncounteredBench": true, + "tisoEncounteredLake": true, + "tisoEncounteredColosseum": true, + "tisoDead": true, + "tisoShieldConvo": true, + "mossCultist": true, + "maskmakerMet": false, + "maskmakerConvo1": false, + "maskmakerConvo2": false, + "maskmakerUnmasked1": false, + "maskmakerUnmasked2": false, + "maskmakerShadowDash": false, + "maskmakerKingsBrand": false, + "dungDefenderConvo1": false, + "dungDefenderConvo2": false, + "dungDefenderConvo3": false, + "dungDefenderCharmConvo": false, + "dungDefenderIsmaConvo": false, + "dungDefenderAwoken": true, + "dungDefenderLeft": true, + "dungDefenderAwakeConvo": false, + "midwifeMet": false, + "midwifeConvo1": false, + "midwifeConvo2": false, + "metQueen": false, + "queenTalk1": false, + "queenTalk2": false, + "queenDung1": false, + "queenDung2": false, + "queenHornet": false, + "queenTalkExtra": false, + "gotQueenFragment": false, + "queenConvo_grimm1": false, + "queenConvo_grimm2": false, + "gotKingFragment": false, + "metXun": false, + "xunFailedConvo1": false, + "xunFailedConvo2": false, + "xunFlowerBroken": false, + "xunFlowerBrokeTimes": false, + "xunFlowerGiven": false, + "xunRewardGiven": false, + "menderState": true, + "menderSignBroken": true, + "allBelieverTabletsDestroyed": true, + "mrMushroomState": true, + "openedMapperShop": true, + "openedSlyShop": true, + "metStag": false, + "stagPosition": true, + "stationsOpened": true, + "stagConvoTram": false, + "stagConvoTiso": false, + "stagRemember1": false, + "stagRemember2": false, + "stagRemember3": false, + "stagEggInspected": false, + "stagHopeConvo": false, + "littleFoolMet": false, + "ranAway": false, + "seenColosseumTitle": false, + "colosseumBronzeOpened": false, + "colosseumBronzeCompleted": false, + "colosseumSilverOpened": false, + "colosseumSilverCompleted": false, + "colosseumGoldOpened": false, + "colosseumGoldCompleted": false, + "openedTown": true, + "openedTownBuilding": true, + "openedCrossroads": true, + "openedGreenpath": true, + "openedRuins1": true, + "openedRuins2": true, + "openedFungalWastes": true, + "openedRoyalGardens": true, + "openedRestingGrounds": true, + "openedDeepnest": true, + "openedStagNest": true, + "openedHiddenStation": true, + "charmSlots": false, + "charmsOwned": false, + "gotCharm_1": false, + "gotCharm_2": false, + "gotCharm_3": false, + "gotCharm_4": false, + "gotCharm_5": false, + "gotCharm_6": false, + "gotCharm_7": false, + "gotCharm_8": false, + "gotCharm_9": false, + "gotCharm_10": false, + "gotCharm_11": false, + "gotCharm_12": false, + "gotCharm_13": false, + "gotCharm_14": false, + "gotCharm_15": false, + "gotCharm_16": false, + "gotCharm_17": false, + "gotCharm_18": false, + "gotCharm_19": false, + "gotCharm_20": false, + "gotCharm_21": false, + "gotCharm_22": false, + "gotCharm_23": false, + "gotCharm_24": false, + "gotCharm_25": false, + "gotCharm_26": false, + "gotCharm_27": false, + "gotCharm_28": false, + "gotCharm_29": false, + "gotCharm_30": false, + "gotCharm_31": false, + "gotCharm_32": false, + "gotCharm_33": false, + "gotCharm_34": false, + "gotCharm_35": false, + "gotCharm_36": false, + "gotCharm_37": false, + "gotCharm_38": false, + "gotCharm_39": false, + "gotCharm_40": false, + "fragileHealth_unbreakable": false, + "fragileGreed_unbreakable": false, + "fragileStrength_unbreakable": false, + "royalCharmState": false, + "hasJournal": false, + "seenJournalMsg": false, + "seenHunterMsg": false, + "fillJournal": false, + "journalEntriesCompleted": true, + "journalNotesCompleted": true, + "journalEntriesTotal": true, + "killedCrawler": true, + "killsCrawler": true, + "newDataCrawler": true, + "killedBuzzer": true, + "killsBuzzer": true, + "newDataBuzzer": true, + "killedBouncer": true, + "killsBouncer": true, + "newDataBouncer": true, + "killedClimber": true, + "killsClimber": true, + "newDataClimber": true, + "killedHopper": true, + "killsHopper": true, + "newDataHopper": true, + "killedWorm": true, + "killsWorm": true, + "newDataWorm": true, + "killedSpitter": true, + "killsSpitter": true, + "newDataSpitter": true, + "killedHatcher": true, + "killsHatcher": true, + "newDataHatcher": true, + "killedHatchling": true, + "killsHatchling": true, + "newDataHatchling": true, + "killedZombieRunner": true, + "killsZombieRunner": true, + "newDataZombieRunner": true, + "killedZombieHornhead": true, + "killsZombieHornhead": true, + "newDataZombieHornhead": true, + "killedZombieLeaper": true, + "killsZombieLeaper": true, + "newDataZombieLeaper": true, + "killedZombieBarger": true, + "killsZombieBarger": true, + "newDataZombieBarger": true, + "killedZombieShield": true, + "killsZombieShield": true, + "newDataZombieShield": true, + "killedZombieGuard": true, + "killsZombieGuard": true, + "newDataZombieGuard": true, + "killedBigBuzzer": true, + "killsBigBuzzer": true, + "newDataBigBuzzer": true, + "killedBigFly": true, + "killsBigFly": true, + "newDataBigFly": true, + "killedMawlek": true, + "killsMawlek": true, + "newDataMawlek": true, + "killedFalseKnight": true, + "killsFalseKnight": true, + "newDataFalseKnight": true, + "killedRoller": true, + "killsRoller": true, + "newDataRoller": true, + "killedBlocker": true, + "killsBlocker": true, + "newDataBlocker": true, + "killedPrayerSlug": true, + "killsPrayerSlug": true, + "newDataPrayerSlug": true, + "killedMenderBug": true, + "killsMenderBug": true, + "newDataMenderBug": true, + "killedMossmanRunner": true, + "killsMossmanRunner": true, + "newDataMossmanRunner": true, + "killedMossmanShaker": true, + "killsMossmanShaker": true, + "newDataMossmanShaker": true, + "killedMosquito": true, + "killsMosquito": true, + "newDataMosquito": true, + "killedBlobFlyer": true, + "killsBlobFlyer": true, + "newDataBlobFlyer": true, + "killedFungifiedZombie": true, + "killsFungifiedZombie": true, + "newDataFungifiedZombie": true, + "killedPlantShooter": true, + "killsPlantShooter": true, + "newDataPlantShooter": true, + "killedMossCharger": true, + "killsMossCharger": true, + "newDataMossCharger": true, + "killedMegaMossCharger": true, + "killsMegaMossCharger": true, + "newDataMegaMossCharger": true, + "killedSnapperTrap": true, + "killsSnapperTrap": true, + "newDataSnapperTrap": true, + "killedMossKnight": true, + "killsMossKnight": true, + "newDataMossKnight": true, + "killedGrassHopper": true, + "killsGrassHopper": true, + "newDataGrassHopper": true, + "killedAcidFlyer": true, + "killsAcidFlyer": true, + "newDataAcidFlyer": true, + "killedAcidWalker": true, + "killsAcidWalker": true, + "newDataAcidWalker": true, + "killedMossFlyer": true, + "killsMossFlyer": true, + "newDataMossFlyer": true, + "killedMossKnightFat": true, + "killsMossKnightFat": true, + "newDataMossKnightFat": true, + "killedMossWalker": true, + "killsMossWalker": true, + "newDataMossWalker": true, + "killedInfectedKnight": true, + "killsInfectedKnight": true, + "newDataInfectedKnight": true, + "killedLazyFlyer": true, + "killsLazyFlyer": true, + "newDataLazyFlyer": true, + "killedZapBug": true, + "killsZapBug": true, + "newDataZapBug": true, + "killedJellyfish": true, + "killsJellyfish": true, + "newDataJellyfish": true, + "killedJellyCrawler": true, + "killsJellyCrawler": true, + "newDataJellyCrawler": true, + "killedMegaJellyfish": true, + "killsMegaJellyfish": true, + "newDataMegaJellyfish": true, + "killedFungoonBaby": true, + "killsFungoonBaby": true, + "newDataFungoonBaby": true, + "killedMushroomTurret": true, + "killsMushroomTurret": true, + "newDataMushroomTurret": true, + "killedMantis": true, + "killsMantis": true, + "newDataMantis": true, + "killedMushroomRoller": true, + "killsMushroomRoller": true, + "newDataMushroomRoller": true, + "killedMushroomBrawler": true, + "killsMushroomBrawler": true, + "newDataMushroomBrawler": true, + "killedMushroomBaby": true, + "killsMushroomBaby": true, + "newDataMushroomBaby": true, + "killedMantisFlyerChild": true, + "killsMantisFlyerChild": true, + "newDataMantisFlyerChild": true, + "killedFungusFlyer": true, + "killsFungusFlyer": true, + "newDataFungusFlyer": true, + "killedFungCrawler": true, + "killsFungCrawler": true, + "newDataFungCrawler": true, + "killedMantisLord": true, + "killsMantisLord": true, + "newDataMantisLord": true, + "killedBlackKnight": true, + "killsBlackKnight": true, + "newDataBlackKnight": true, + "killedElectricMage": true, + "killsElectricMage": true, + "newDataElectricMage": true, + "killedMage": true, + "killsMage": true, + "newDataMage": true, + "killedMageKnight": true, + "killsMageKnight": true, + "newDataMageKnight": true, + "killedRoyalDandy": true, + "killsRoyalDandy": true, + "newDataRoyalDandy": true, + "killedRoyalCoward": true, + "killsRoyalCoward": true, + "newDataRoyalCoward": true, + "killedRoyalPlumper": true, + "killsRoyalPlumper": true, + "newDataRoyalPlumper": true, + "killedFlyingSentrySword": true, + "killsFlyingSentrySword": true, + "newDataFlyingSentrySword": true, + "killedFlyingSentryJavelin": true, + "killsFlyingSentryJavelin": true, + "newDataFlyingSentryJavelin": true, + "killedSentry": true, + "killsSentry": true, + "newDataSentry": true, + "killedSentryFat": true, + "killsSentryFat": true, + "newDataSentryFat": true, + "killedMageBlob": true, + "killsMageBlob": true, + "newDataMageBlob": true, + "killedGreatShieldZombie": true, + "killsGreatShieldZombie": true, + "newDataGreatShieldZombie": true, + "killedJarCollector": true, + "killsJarCollector": true, + "newDataJarCollector": true, + "killedMageBalloon": true, + "killsMageBalloon": true, + "newDataMageBalloon": true, + "killedMageLord": true, + "killsMageLord": true, + "newDataMageLord": true, + "killedGorgeousHusk": true, + "killsGorgeousHusk": true, + "newDataGorgeousHusk": true, + "killedFlipHopper": true, + "killsFlipHopper": true, + "newDataFlipHopper": true, + "killedFlukeman": true, + "killsFlukeman": true, + "newDataFlukeman": true, + "killedInflater": true, + "killsInflater": true, + "newDataInflater": true, + "killedFlukefly": true, + "killsFlukefly": true, + "newDataFlukefly": true, + "killedFlukeMother": true, + "killsFlukeMother": true, + "newDataFlukeMother": true, + "killedDungDefender": true, + "killsDungDefender": true, + "newDataDungDefender": true, + "killedCrystalCrawler": true, + "killsCrystalCrawler": true, + "newDataCrystalCrawler": true, + "killedCrystalFlyer": true, + "killsCrystalFlyer": true, + "newDataCrystalFlyer": true, + "killedLaserBug": true, + "killsLaserBug": true, + "newDataLaserBug": true, + "killedBeamMiner": true, + "killsBeamMiner": true, + "newDataBeamMiner": true, + "killedZombieMiner": true, + "killsZombieMiner": true, + "newDataZombieMiner": true, + "killedMegaBeamMiner": true, + "killsMegaBeamMiner": true, + "newDataMegaBeamMiner": true, + "killedMinesCrawler": true, + "killsMinesCrawler": true, + "newDataMinesCrawler": true, + "killedAngryBuzzer": true, + "killsAngryBuzzer": true, + "newDataAngryBuzzer": true, + "killedBurstingBouncer": true, + "killsBurstingBouncer": true, + "newDataBurstingBouncer": true, + "killedBurstingZombie": true, + "killsBurstingZombie": true, + "newDataBurstingZombie": true, + "killedSpittingZombie": true, + "killsSpittingZombie": true, + "newDataSpittingZombie": true, + "killedBabyCentipede": true, + "killsBabyCentipede": true, + "newDataBabyCentipede": true, + "killedBigCentipede": true, + "killsBigCentipede": true, + "newDataBigCentipede": true, + "killedCentipedeHatcher": true, + "killsCentipedeHatcher": true, + "newDataCentipedeHatcher": true, + "killedLesserMawlek": true, + "killsLesserMawlek": true, + "newDataLesserMawlek": true, + "killedSlashSpider": true, + "killsSlashSpider": true, + "newDataSlashSpider": true, + "killedSpiderCorpse": true, + "killsSpiderCorpse": true, + "newDataSpiderCorpse": true, + "killedShootSpider": true, + "killsShootSpider": true, + "newDataShootSpider": true, + "killedMiniSpider": true, + "killsMiniSpider": true, + "newDataMiniSpider": true, + "killedSpiderFlyer": true, + "killsSpiderFlyer": true, + "newDataSpiderFlyer": true, + "killedMimicSpider": true, + "killsMimicSpider": true, + "newDataMimicSpider": true, + "killedBeeHatchling": true, + "killsBeeHatchling": true, + "newDataBeeHatchling": true, + "killedBeeStinger": true, + "killsBeeStinger": true, + "newDataBeeStinger": true, + "killedBigBee": true, + "killsBigBee": true, + "newDataBigBee": true, + "killedHiveKnight": true, + "killsHiveKnight": true, + "newDataHiveKnight": true, + "killedBlowFly": true, + "killsBlowFly": true, + "newDataBlowFly": true, + "killedCeilingDropper": true, + "killsCeilingDropper": true, + "newDataCeilingDropper": true, + "killedGiantHopper": true, + "killsGiantHopper": true, + "newDataGiantHopper": true, + "killedGrubMimic": true, + "killsGrubMimic": true, + "newDataGrubMimic": true, + "killedMawlekTurret": true, + "killsMawlekTurret": true, + "newDataMawlekTurret": true, + "killedOrangeScuttler": true, + "killsOrangeScuttler": true, + "newDataOrangeScuttler": true, + "killedHealthScuttler": true, + "killsHealthScuttler": true, + "newDataHealthScuttler": true, + "killedPigeon": true, + "killsPigeon": true, + "newDataPigeon": true, + "killedZombieHive": true, + "killsZombieHive": true, + "newDataZombieHive": true, + "killedDreamGuard": true, + "killsDreamGuard": true, + "newDataDreamGuard": true, + "killedHornet": true, + "killsHornet": true, + "newDataHornet": true, + "killedAbyssCrawler": true, + "killsAbyssCrawler": true, + "newDataAbyssCrawler": true, + "killedSuperSpitter": true, + "killsSuperSpitter": true, + "newDataSuperSpitter": true, + "killedSibling": true, + "killsSibling": true, + "newDataSibling": true, + "killedPalaceFly": true, + "killsPalaceFly": true, + "newDataPalaceFly": true, + "killedEggSac": true, + "killsEggSac": true, + "newDataEggSac": true, + "killedMummy": true, + "killsMummy": true, + "newDataMummy": true, + "killedOrangeBalloon": true, + "killsOrangeBalloon": true, + "newDataOrangeBalloon": true, + "killedAbyssTendril": true, + "killsAbyssTendril": true, + "newDataAbyssTendril": true, + "killedHeavyMantis": true, + "killsHeavyMantis": true, + "newDataHeavyMantis": true, + "killedTraitorLord": true, + "killsTraitorLord": true, + "newDataTraitorLord": true, + "killedMantisHeavyFlyer": true, + "killsMantisHeavyFlyer": true, + "newDataMantisHeavyFlyer": true, + "killedGardenZombie": true, + "killsGardenZombie": true, + "newDataGardenZombie": true, + "killedRoyalGuard": true, + "killsRoyalGuard": true, + "newDataRoyalGuard": true, + "killedWhiteRoyal": true, + "killsWhiteRoyal": true, + "newDataWhiteRoyal": true, + "openedPalaceGrounds": true, + "killedOblobble": true, + "killsOblobble": true, + "newDataOblobble": true, + "killedZote": true, + "killsZote": true, + "newDataZote": true, + "killedBlobble": true, + "killsBlobble": true, + "newDataBlobble": true, + "killedColMosquito": true, + "killsColMosquito": true, + "newDataColMosquito": true, + "killedColRoller": true, + "killsColRoller": true, + "newDataColRoller": true, + "killedColFlyingSentry": true, + "killsColFlyingSentry": true, + "newDataColFlyingSentry": true, + "killedColMiner": true, + "killsColMiner": true, + "newDataColMiner": true, + "killedColShield": true, + "killsColShield": true, + "newDataColShield": true, + "killedColWorm": true, + "killsColWorm": true, + "newDataColWorm": true, + "killedColHopper": true, + "killsColHopper": true, + "newDataColHopper": true, + "killedLobsterLancer": true, + "killsLobsterLancer": true, + "newDataLobsterLancer": true, + "killedGhostAladar": true, + "killsGhostAladar": true, + "newDataGhostAladar": true, + "killedGhostXero": true, + "killsGhostXero": true, + "newDataGhostXero": true, + "killedGhostHu": true, + "killsGhostHu": true, + "newDataGhostHu": true, + "killedGhostMarmu": true, + "killsGhostMarmu": true, + "newDataGhostMarmu": true, + "killedGhostNoEyes": true, + "killsGhostNoEyes": true, + "newDataGhostNoEyes": true, + "killedGhostMarkoth": true, + "killsGhostMarkoth": true, + "newDataGhostMarkoth": true, + "killedGhostGalien": true, + "killsGhostGalien": true, + "newDataGhostGalien": true, + "killedWhiteDefender": true, + "killsWhiteDefender": true, + "newDataWhiteDefender": true, + "killedGreyPrince": true, + "killsGreyPrince": true, + "newDataGreyPrince": true, + "killedZotelingBalloon": true, + "killsZotelingBalloon": true, + "newDataZotelingBalloon": true, + "killedZotelingHopper": true, + "killsZotelingHopper": true, + "newDataZotelingHopper": true, + "killedZotelingBuzzer": true, + "killsZotelingBuzzer": true, + "newDataZotelingBuzzer": true, + "killedHollowKnight": true, + "killsHollowKnight": true, + "newDataHollowKnight": true, + "killedFinalBoss": true, + "killsFinalBoss": true, + "newDataFinalBoss": true, + "killedHunterMark": true, + "killsHunterMark": true, + "newDataHunterMark": true, + "killedFlameBearerSmall": true, + "killsFlameBearerSmall": true, + "newDataFlameBearerSmall": true, + "killedFlameBearerMed": true, + "killsFlameBearerMed": true, + "newDataFlameBearerMed": true, + "killedFlameBearerLarge": true, + "killsFlameBearerLarge": true, + "newDataFlameBearerLarge": true, + "killedGrimm": true, + "killsGrimm": true, + "newDataGrimm": true, + "killedNightmareGrimm": true, + "killsNightmareGrimm": true, + "newDataNightmareGrimm": true, + "killedBindingSeal": true, + "killsBindingSeal": true, + "newDataBindingSeal": true, + "killedFatFluke": true, + "killsFatFluke": true, + "newDataFatFluke": true, + "killedPaleLurker": true, + "killsPaleLurker": true, + "newDataPaleLurker": true, + "killedNailBros": true, + "killsNailBros": true, + "newDataNailBros": true, + "killedPaintmaster": true, + "killsPaintmaster": true, + "newDataPaintmaster": true, + "killedNailsage": true, + "killsNailsage": true, + "newDataNailsage": true, + "killedHollowKnightPrime": true, + "killsHollowKnightPrime": true, + "newDataHollowKnightPrime": true, + "killedGodseekerMask": true, + "killsGodseekerMask": true, + "newDataGodseekerMask": true, + "killedVoidIdol_1": true, + "killsVoidIdol_1": true, + "newDataVoidIdol_1": true, + "killedVoidIdol_2": true, + "killsVoidIdol_2": true, + "newDataVoidIdol_2": true, + "killedVoidIdol_3": true, + "killsVoidIdol_3": true, + "newDataVoidIdol_3": true, + "grubsCollected": true, + "grubRewards": false, + "finalGrubRewardCollected": false, + "fatGrubKing": false, + "falseKnightDefeated": true, + "falseKnightDreamDefeated": true, + "falseKnightOrbsCollected": false, + "mawlekDefeated": true, + "giantBuzzerDefeated": true, + "giantFlyDefeated": true, + "blocker1Defeated": true, + "blocker2Defeated": true, + "hornet1Defeated": true, + "collectorDefeated": true, + "hornetOutskirtsDefeated": true, + "mageLordDreamDefeated": true, + "mageLordOrbsCollected": false, + "infectedKnightDreamDefeated": true, + "infectedKnightOrbsCollected": false, + "whiteDefenderDefeated": true, + "whiteDefenderOrbsCollected": false, + "whiteDefenderDefeats": true, + "greyPrinceDefeats": true, + "greyPrinceDefeated": true, + "greyPrinceOrbsCollected": false, + "aladarSlugDefeated": true, + "xeroDefeated": true, + "elderHuDefeated": true, + "mumCaterpillarDefeated": true, + "noEyesDefeated": true, + "markothDefeated": true, + "galienDefeated": true, + "XERO_encountered": false, + "ALADAR_encountered": false, + "HU_encountered": false, + "MUMCAT_encountered": false, + "NOEYES_encountered": false, + "MARKOTH_encountered": false, + "GALIEN_encountered": false, + "xeroPinned": true, + "aladarPinned": true, + "huPinned": true, + "mumCaterpillarPinned": true, + "noEyesPinned": true, + "markothPinned": true, + "galienPinned": true, + "scenesVisited": true, + "scenesMapped": true, + "scenesEncounteredBench": true, + "scenesGrubRescued": true, + "scenesFlameCollected": true, + "scenesEncounteredCocoon": true, + "scenesEncounteredDreamPlant": true, + "scenesEncounteredDreamPlantC": false, + "hasMap": false, + "mapDirtmouth": false, + "mapCrossroads": false, + "mapGreenpath": false, + "mapFogCanyon": false, + "mapRoyalGardens": false, + "mapFungalWastes": false, + "mapCity": false, + "mapWaterways": false, + "mapMines": false, + "mapDeepnest": false, + "mapCliffs": false, + "mapOutskirts": false, + "mapRestingGrounds": false, + "mapAbyss": false, + "mapZoneBools": true, + "hasPin": false, + "hasPinBench": false, + "hasPinCocoon": false, + "hasPinDreamPlant": false, + "hasPinGuardian": false, + "hasPinBlackEgg": false, + "hasPinShop": false, + "hasPinSpa": false, + "hasPinStag": false, + "hasPinTram": false, + "hasPinGhost": false, + "hasPinGrub": false, + "hasMarker": false, + "hasMarker_r": false, + "hasMarker_b": false, + "hasMarker_y": false, + "hasMarker_w": false, + "openedTramLower": false, + "openedTramRestingGrounds": false, + "tramLowerPosition": true, + "tramRestingGroundsPosition": true, + "mineLiftOpened": true, + "menderDoorOpened": true, + "vesselFragStagNest": false, + "shamanPillar": true, + "crossroadsMawlekWall": true, + "eggTempleVisited": false, + "crossroadsInfected": true, + "falseKnightFirstPlop": true, + "falseKnightWallRepaired": true, + "falseKnightWallBroken": true, + "falseKnightGhostDeparted": true, + "spaBugsEncountered": true, + "hornheadVinePlat": true, + "infectedKnightEncountered": true, + "megaMossChargerEncountered": true, + "megaMossChargerDefeated": true, + "dreamerScene1": true, + "slugEncounterComplete": true, + "defeatedDoubleBlockers": true, + "oneWayArchive": true, + "defeatedMegaJelly": true, + "summonedMonomon": true, + "sawWoundedQuirrel": true, + "encounteredMegaJelly": true, + "defeatedMantisLords": true, + "encounteredGatekeeper": true, + "deepnestWall": true, + "queensStationNonDisplay": true, + "cityBridge1": true, + "cityBridge2": true, + "cityLift1": true, + "cityLift1_isUp": true, + "liftArrival": true, + "openedMageDoor": true, + "openedMageDoor_v2": true, + "brokenMageWindow": true, + "brokenMageWindowGlass": true, + "mageLordEncountered": true, + "mageLordEncountered_2": true, + "mageLordDefeated": true, + "ruins1_5_tripleDoor": true, + "openedCityGate": true, + "cityGateClosed": true, + "bathHouseOpened": true, + "bathHouseWall": true, + "cityLift2": true, + "cityLift2_isUp": true, + "city2_sewerDoor": true, + "openedLoveDoor": true, + "watcherChandelier": true, + "completedQuakeArea": true, + "kingsStationNonDisplay": true, + "tollBenchCity": true, + "waterwaysGate": true, + "defeatedDungDefender": true, + "dungDefenderEncounterReady": true, + "flukeMotherEncountered": true, + "flukeMotherDefeated": true, + "openedWaterwaysManhole": true, + "waterwaysAcidDrained": true, + "dungDefenderWallBroken": true, + "dungDefenderSleeping": true, + "defeatedMegaBeamMiner": true, + "defeatedMegaBeamMiner2": true, + "brokeMinersWall": true, + "encounteredMimicSpider": true, + "steppedBeyondBridge": true, + "deepnestBridgeCollapsed": true, + "spiderCapture": false, + "deepnest26b_switch": true, + "openedRestingGrounds02": true, + "restingGroundsCryptWall": true, + "dreamNailConvo": false, + "gladeGhostsKilled": true, + "openedGardensStagStation": true, + "extendedGramophone": true, + "tollBenchQueensGardens": true, + "blizzardEnded": true, + "encounteredHornet": true, + "savedByHornet": true, + "outskirtsWall": true, + "abyssGateOpened": true, + "abyssLighthouse": true, + "blueVineDoor": true, + "gotShadeCharm": true, + "tollBenchAbyss": true, + "fountainGeo": false, + "fountainVesselSummoned": false, + "openedBlackEggPath": true, + "enteredDreamWorld": false, + "duskKnightDefeated": true, + "whitePalaceOrb_1": true, + "whitePalaceOrb_2": true, + "whitePalaceOrb_3": true, + "whitePalace05_lever": true, + "whitePalaceMidWarp": true, + "whitePalaceSecretRoomVisited": true, + "tramOpenedDeepnest": true, + "tramOpenedCrossroads": true, + "openedBlackEggDoor": true, + "unchainedHollowKnight": true, + "flamesCollected": true, + "flamesRequired": true, + "nightmareLanternAppeared": true, + "nightmareLanternLit": true, + "troupeInTown": true, + "divineInTown": true, + "grimmChildLevel": true, + "elderbugConvoGrimm": false, + "slyConvoGrimm": false, + "iseldaConvoGrimm": false, + "midwifeWeaverlingConvo": false, + "metGrimm": true, + "foughtGrimm": true, + "metBrum": false, + "defeatedNightmareGrimm": true, + "grimmchildAwoken": true, + "gotBrummsFlame": true, + "brummBrokeBrazier": true, + "destroyedNightmareLantern": true, + "gotGrimmNotch": false, + "nymmInTown": true, + "nymmSpoken": false, + "nymmCharmConvo": false, + "nymmFinalConvo": false, + "elderbugNymmConvo": false, + "slyNymmConvo": false, + "iseldaNymmConvo": false, + "nymmMissedEggOpen": false, + "elderbugTroupeLeftConvo": false, + "elderbugBrettaLeft": false, + "jijiGrimmConvo": false, + "metDivine": false, + "divineFinalConvo": false, + "gaveFragileHeart": false, + "gaveFragileGreed": false, + "gaveFragileStrength": false, + "divineEatenConvos": false, + "pooedFragileHeart": false, + "pooedFragileGreed": false, + "pooedFragileStrength": false, + "completionPercentage": false, + "unlockedCompletionRate": false, + "newDatTraitorLord": true, + "bossDoorStateTier1": true, + "bossDoorStateTier2": true, + "bossDoorStateTier3": true, + "bossDoorStateTier4": true, + "bossDoorStateTier5": true, + "bossStatueTargetLevel": false, + "statueStateGruzMother": true, + "statueStateVengefly": true, + "statueStateBroodingMawlek": true, + "statueStateFalseKnight": true, + "statueStateFailedChampion": true, + "statueStateHornet1": true, + "statueStateHornet2": true, + "statueStateMegaMossCharger": true, + "statueStateMantisLords": true, + "statueStateOblobbles": true, + "statueStateGreyPrince": true, + "statueStateBrokenVessel": true, + "statueStateLostKin": true, + "statueStateNosk": true, + "statueStateFlukemarm": true, + "statueStateCollector": true, + "statueStateWatcherKnights": true, + "statueStateSoulMaster": true, + "statueStateSoulTyrant": true, + "statueStateGodTamer": true, + "statueStateCrystalGuardian1": true, + "statueStateCrystalGuardian2": true, + "statueStateUumuu": true, + "statueStateDungDefender": true, + "statueStateWhiteDefender": true, + "statueStateHiveKnight": true, + "statueStateTraitorLord": true, + "statueStateGrimm": true, + "statueStateNightmareGrimm": true, + "statueStateHollowKnight": true, + "statueStateElderHu": true, + "statueStateGalien": true, + "statueStateMarkoth": true, + "statueStateMarmu": true, + "statueStateNoEyes": true, + "statueStateXero": true, + "statueStateGorb": true, + "statueStateRadiance": true, + "statueStateSly": true, + "statueStateNailmasters": true, + "statueStateMageKnight": true, + "statueStatePaintmaster": true, + "statueStateZote": true, + "statueStateNoskHornet": true, + "statueStateMantisLordsExtra": true, + "godseekerUnlocked": true, + "bossDoorCageUnlocked": true, + "blueRoomDoorUnlocked": true, + "blueRoomActivated": true, + "finalBossDoorUnlocked": true, + "hasGodfinder": false, + "unlockedNewBossStatue": true, + "scaredFlukeHermitEncountered": false, + "scaredFlukeHermitReturned": false, + "enteredGGAtrium": false, + "extraFlowerAppear": true, + "givenGodseekerFlower": true, + "givenOroFlower": true, + "givenWhiteLadyFlower": true, + "givenEmilitiaFlower": true, + "unlockedBossScenes": false, + "queuedGodfinderIcon": false, + "godseekerSpokenAwake": false, + "nailsmithCorpseAppeared": true, + "godseekerWaterwaysSeenState": true, + "godseekerWaterwaysSpoken1": false, + "godseekerWaterwaysSpoken2": false, + "godseekerWaterwaysSpoken3": false, + "bossDoorEntranceTextSeen": false, + "seenDoor4Finale": true, + "zoteStatueWallBroken": true, + "seenGGWastes": false, + "ordealAchieved": true + }, + "geoRocks": [ + { + "Key": { + "id": "Geo Rock 4", + "sceneName": "Tutorial_01" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock 1", + "sceneName": "Tutorial_01" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock 3", + "sceneName": "Tutorial_01" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock 2", + "sceneName": "Tutorial_01" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock 5", + "sceneName": "Tutorial_01" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock 2", + "sceneName": "Crossroads_01" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock 1 (1)", + "sceneName": "Crossroads_07" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock 1", + "sceneName": "Crossroads_07" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock 1 (2)", + "sceneName": "Crossroads_07" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock 2", + "sceneName": "Crossroads_12" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock 1 (3)", + "sceneName": "Crossroads_08" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock 1 (1)", + "sceneName": "Crossroads_08" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock 1 (2)", + "sceneName": "Crossroads_08" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock 1", + "sceneName": "Crossroads_08" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock 1", + "sceneName": "Crossroads_13" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock 2", + "sceneName": "Crossroads_13" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock 1", + "sceneName": "Crossroads_42" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock 2", + "sceneName": "Crossroads_42" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock 1", + "sceneName": "Crossroads_19" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock 1", + "sceneName": "Crossroads_21" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock 1", + "sceneName": "Crossroads_10" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock 2 (1)", + "sceneName": "Crossroads_ShamanTemple" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock 2", + "sceneName": "Crossroads_ShamanTemple" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock 1", + "sceneName": "Crossroads_ShamanTemple" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock 1", + "sceneName": "Crossroads_27" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock Green Path 01", + "sceneName": "Fungus1_01" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock Green Path 01", + "sceneName": "Fungus1_01b" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock Green Path 01", + "sceneName": "Fungus1_02" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock Green Path 01 (1)", + "sceneName": "Fungus1_02" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock Green Path 01 (2)", + "sceneName": "Fungus1_03" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock Green Path 01 (1)", + "sceneName": "Fungus1_03" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock Green Path 01", + "sceneName": "Fungus1_03" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock 2 (1)", + "sceneName": "Fungus1_31" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock 1 (1)", + "sceneName": "Fungus1_31" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock Green Path 01", + "sceneName": "Fungus1_31" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock Green Path 02 (2)", + "sceneName": "Fungus1_21" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock Green Path 02", + "sceneName": "Fungus1_21" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock Green Path 02 (1)", + "sceneName": "Fungus1_21" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock Green Path 01 (1)", + "sceneName": "Fungus1_22" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock Green Path 01", + "sceneName": "Fungus1_22" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock Green Path 01", + "sceneName": "Fungus1_04" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock 1", + "sceneName": "Crossroads_18" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock 3", + "sceneName": "Crossroads_18" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock 2", + "sceneName": "Crossroads_18" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock Fung 01", + "sceneName": "Fungus2_08" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock Fung 01", + "sceneName": "Fungus2_10" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock Fung 01", + "sceneName": "Fungus2_11" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock Fung 02", + "sceneName": "Fungus2_11" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock Fung 01 (1)", + "sceneName": "Fungus2_13" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock Fung 01", + "sceneName": "Fungus2_13" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock Fung 02", + "sceneName": "Fungus2_13" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock 1", + "sceneName": "Fungus2_14" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock 2 (2)", + "sceneName": "Fungus2_14" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock 2 (3)", + "sceneName": "Fungus2_14" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock 2 (1)", + "sceneName": "Fungus2_14" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock 2", + "sceneName": "Fungus2_14" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock 1 (1)", + "sceneName": "Fungus2_15" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock 1", + "sceneName": "Fungus2_15" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock 1", + "sceneName": "Fungus2_21" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock City 1", + "sceneName": "Ruins1_03" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock City 1", + "sceneName": "Ruins1_05c" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock City 1 (1)", + "sceneName": "Ruins1_05b" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock 2", + "sceneName": "Crossroads_37" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock 2", + "sceneName": "Crossroads_16" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock 1", + "sceneName": "Crossroads_05" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock City 1", + "sceneName": "Ruins1_32" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock Mine (1)", + "sceneName": "Mines_02" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock Mine", + "sceneName": "Mines_02" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock 1", + "sceneName": "Mines_02" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock Mine (1)", + "sceneName": "Mines_04" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock Mine", + "sceneName": "Mines_04" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock Mine", + "sceneName": "Mines_05" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock Mine (3)", + "sceneName": "Mines_20" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock Mine (4)", + "sceneName": "Mines_20" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock Mine (2)", + "sceneName": "Mines_20" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock Mine", + "sceneName": "Mines_31" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock Mine", + "sceneName": "Mines_37" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock Mine (1)", + "sceneName": "Mines_37" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock Fung 01", + "sceneName": "Fungus2_04" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock City 1 (1)", + "sceneName": "Waterways_01" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock City 1", + "sceneName": "Waterways_01" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock 2 (1)", + "sceneName": "Abyss_01" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock 1", + "sceneName": "Abyss_01" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock 2", + "sceneName": "Abyss_01" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock City 1", + "sceneName": "Waterways_07" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock City 1", + "sceneName": "Ruins2_06" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock Green Path 01", + "sceneName": "Fungus3_26" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock 1", + "sceneName": "Crossroads_52" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock 2", + "sceneName": "Crossroads_52" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock City 1", + "sceneName": "Ruins2_01" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock Fung 01", + "sceneName": "Fungus2_18" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock Fung 02 (2)", + "sceneName": "Fungus2_18" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock Fung 01 (1)", + "sceneName": "Fungus2_18" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock Fung 02 (1)", + "sceneName": "Fungus2_18" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock Deepnest (2)", + "sceneName": "Fungus2_25" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock Deepnest (1)", + "sceneName": "Fungus2_25" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock Deepnest", + "sceneName": "Fungus2_25" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock Deepnest (2)", + "sceneName": "Deepnest_16" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock Deepnest", + "sceneName": "Deepnest_16" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock Deepnest (3)", + "sceneName": "Deepnest_16" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock Deepnest (4)", + "sceneName": "Deepnest_16" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock Deepnest (1)", + "sceneName": "Deepnest_16" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock Deepnest (2)", + "sceneName": "Deepnest_03" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock Deepnest (1)", + "sceneName": "Deepnest_03" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock Deepnest", + "sceneName": "Deepnest_03" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock Deepnest (1)", + "sceneName": "Deepnest_39" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock Deepnest (2)", + "sceneName": "Deepnest_39" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock Deepnest", + "sceneName": "Deepnest_39" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock Deepnest (4)", + "sceneName": "Deepnest_Spider_Town" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock Deepnest (1)", + "sceneName": "Deepnest_Spider_Town" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock Deepnest (2)", + "sceneName": "Deepnest_Spider_Town" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock Deepnest (6)", + "sceneName": "Deepnest_Spider_Town" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock Deepnest", + "sceneName": "Deepnest_Spider_Town" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock Deepnest (7)", + "sceneName": "Deepnest_Spider_Town" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock Deepnest (5)", + "sceneName": "Deepnest_Spider_Town" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock Deepnest (3)", + "sceneName": "Deepnest_Spider_Town" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock 1 (1)", + "sceneName": "Cliffs_02" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock 1", + "sceneName": "Cliffs_02" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock 2 (1)", + "sceneName": "Cliffs_01" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock 2 (2)", + "sceneName": "Cliffs_01" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock 2 (3)", + "sceneName": "Cliffs_01" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock 2 (4)", + "sceneName": "Cliffs_01" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock Green Path 01", + "sceneName": "Fungus1_10" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock 2", + "sceneName": "Fungus1_05" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock Green Path 01", + "sceneName": "Fungus1_19" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock Green Path 01", + "sceneName": "Fungus1_07" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock Deepnest (2)", + "sceneName": "Abyss_02" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock Deepnest (1)", + "sceneName": "Abyss_02" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock Deepnest", + "sceneName": "Abyss_02" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock Abyss", + "sceneName": "Abyss_18" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock Abyss", + "sceneName": "Abyss_19" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock Abyss (1)", + "sceneName": "Abyss_19" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock Mine", + "sceneName": "Mines_16" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock Grave 01", + "sceneName": "RestingGrounds_10" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock Grave 02 (1)", + "sceneName": "RestingGrounds_10" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock Grave 02", + "sceneName": "RestingGrounds_10" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock Outskirts", + "sceneName": "Deepnest_East_06" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock Outskirts (1)", + "sceneName": "Deepnest_East_06" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock Outskirts", + "sceneName": "Deepnest_East_04" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock Abyss (1)", + "sceneName": "Abyss_06_Core" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock Abyss", + "sceneName": "Abyss_06_Core" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock 1", + "sceneName": "Crossroads_36" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock Green Path 01", + "sceneName": "Fungus3_03" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock Green Path 02", + "sceneName": "Room_Fungus_Shaman" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock 1 (2)", + "sceneName": "Fungus1_28" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock 2", + "sceneName": "Fungus1_28" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock 2", + "sceneName": "Deepnest_01" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock 1", + "sceneName": "Deepnest_01" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock Green Path 01 (1)", + "sceneName": "Fungus3_39" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock Green Path 01", + "sceneName": "Fungus3_39" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock Deepnest", + "sceneName": "Deepnest_31" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock Deepnest (1)", + "sceneName": "Deepnest_31" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock Deepnest (2)", + "sceneName": "Deepnest_31" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock Deepnest", + "sceneName": "Deepnest_35" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock Deepnest (1)", + "sceneName": "Deepnest_35" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock Deepnest", + "sceneName": "Deepnest_37" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock Deepnest (1)", + "sceneName": "Deepnest_37" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock Green Path 01", + "sceneName": "Deepnest_43" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock Green Path 01 (1)", + "sceneName": "Deepnest_43" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock Green Path 01", + "sceneName": "Fungus3_10" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock Green Path 02", + "sceneName": "Fungus3_48" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock Outskirts (1)", + "sceneName": "Deepnest_East_07" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock Outskirts", + "sceneName": "Deepnest_East_07" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock City 1", + "sceneName": "Waterways_04b" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock City 1", + "sceneName": "Waterways_08" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock Outskirts", + "sceneName": "Deepnest_East_08" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock City 1", + "sceneName": "Ruins2_05" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock Green Path 01", + "sceneName": "Fungus1_29" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock Green Path 01", + "sceneName": "Fungus1_12" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock Green Path 02", + "sceneName": "Fungus1_12" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock Green Path 01 (1)", + "sceneName": "Fungus1_12" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock Hive", + "sceneName": "Hive_01" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock Hive", + "sceneName": "Hive_02" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock Hive (1)", + "sceneName": "Hive_02" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock Hive (2)", + "sceneName": "Hive_02" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock Deepnest", + "sceneName": "Deepnest_East_01" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock Deepnest (1)", + "sceneName": "Deepnest_East_01" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock Hive (2)", + "sceneName": "Hive_03" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock Hive", + "sceneName": "Hive_03" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock Hive (1)", + "sceneName": "Hive_03" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock Hive", + "sceneName": "Hive_04" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock Hive (1)", + "sceneName": "Hive_04" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock Deepnest (1)", + "sceneName": "Deepnest_East_02" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock Deepnest", + "sceneName": "Deepnest_East_02" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock Mine (3)", + "sceneName": "Mines_25" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock Mine (1)", + "sceneName": "Mines_25" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock Mine (2)", + "sceneName": "Mines_25" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock Mine", + "sceneName": "Mines_25" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock Mine (4)", + "sceneName": "Mines_25" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock Fung 01", + "sceneName": "Fungus2_29" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock Fung 01 (1)", + "sceneName": "Fungus2_30" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock Fung 01", + "sceneName": "Fungus2_30" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock Outskirts", + "sceneName": "Deepnest_East_17" + }, + "Value": true + }, + { + "Key": { + "id": "Giant Geo Egg", + "sceneName": "Deepnest_East_17" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock Deepnest (1)", + "sceneName": "Deepnest_02" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock Deepnest", + "sceneName": "Deepnest_02" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock City 1", + "sceneName": "Ruins_Elevator" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock Outskirts", + "sceneName": "GG_Lurker" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock 2", + "sceneName": "Mines_33" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock 2 (2)", + "sceneName": "Mines_33" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock 2 (1)", + "sceneName": "Mines_33" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock 1", + "sceneName": "Crossroads_46" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock City 1", + "sceneName": "GG_Pipeway" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock Fung 01", + "sceneName": "Room_GG_Shortcut" + }, + "Value": true + }, + { + "Key": { + "id": "Geo Rock Fung 02 (1)", + "sceneName": "Room_GG_Shortcut" + }, + "Value": true + } + ], + "persistentBoolItems": [ + { + "Key": { + "id": "fury charm_remask", + "sceneName": "Tutorial_01" + }, + "Value": true + }, + { + "Key": { + "id": "inverse_remask_right", + "sceneName": "Tutorial_01" + }, + "Value": true + }, + { + "Key": { + "id": "Collapser Tute 01", + "sceneName": "Tutorial_01" + }, + "Value": true + }, + { + "Key": { + "id": "Secret Mask", + "sceneName": "Tutorial_01" + }, + "Value": true + }, + { + "Key": { + "id": "Health Cocoon", + "sceneName": "Tutorial_01" + }, + "Value": true + }, + { + "Key": { + "id": "Tute Door 2", + "sceneName": "Tutorial_01" + }, + "Value": true + }, + { + "Key": { + "id": "Tute Door 4", + "sceneName": "Tutorial_01" + }, + "Value": true + }, + { + "Key": { + "id": "Tute Door 3", + "sceneName": "Tutorial_01" + }, + "Value": true + }, + { + "Key": { + "id": "Interact Reminder", + "sceneName": "Tutorial_01" + }, + "Value": false + }, + { + "Key": { + "id": "Tute Door 6", + "sceneName": "Tutorial_01" + }, + "Value": true + }, + { + "Key": { + "id": "Chest", + "sceneName": "Tutorial_01" + }, + "Value": true + }, + { + "Key": { + "id": "Door", + "sceneName": "Tutorial_01" + }, + "Value": true + }, + { + "Key": { + "id": "Secret Sound Region", + "sceneName": "Tutorial_01" + }, + "Value": false + }, + { + "Key": { + "id": "Tute Door 1", + "sceneName": "Tutorial_01" + }, + "Value": true + }, + { + "Key": { + "id": "Inverse Remasker", + "sceneName": "Tutorial_01" + }, + "Value": true + }, + { + "Key": { + "id": "Tute Door 7", + "sceneName": "Tutorial_01" + }, + "Value": true + }, + { + "Key": { + "id": "Break Floor 1", + "sceneName": "Tutorial_01" + }, + "Value": true + }, + { + "Key": { + "id": "Secret Sound Region (1)", + "sceneName": "Tutorial_01" + }, + "Value": false + }, + { + "Key": { + "id": "Tute Door 5", + "sceneName": "Tutorial_01" + }, + "Value": true + }, + { + "Key": { + "id": "Initial Fall Impact", + "sceneName": "Tutorial_01" + }, + "Value": false + }, + { + "Key": { + "id": "Shiny Item (1)", + "sceneName": "Tutorial_01" + }, + "Value": false + }, + { + "Key": { + "id": "Death Respawn Trigger", + "sceneName": "Town" + }, + "Value": false + }, + { + "Key": { + "id": "Mines Lever", + "sceneName": "Town" + }, + "Value": true + }, + { + "Key": { + "id": "Interact Reminder", + "sceneName": "Town" + }, + "Value": false + }, + { + "Key": { + "id": "Death Respawn Trigger 1", + "sceneName": "Town" + }, + "Value": false + }, + { + "Key": { + "id": "Door Destroyer", + "sceneName": "Town" + }, + "Value": true + }, + { + "Key": { + "id": "Gravedigger NPC", + "sceneName": "Town" + }, + "Value": true + }, + { + "Key": { + "id": "Zombie Runner", + "sceneName": "Crossroads_01" + }, + "Value": true + }, + { + "Key": { + "id": "Secret Mask", + "sceneName": "Crossroads_01" + }, + "Value": true + }, + { + "Key": { + "id": "Shiny Item", + "sceneName": "Crossroads_01" + }, + "Value": true + }, + { + "Key": { + "id": "Zombie Runner 1", + "sceneName": "Crossroads_01" + }, + "Value": true + }, + { + "Key": { + "id": "Remasker", + "sceneName": "Crossroads_07" + }, + "Value": true + }, + { + "Key": { + "id": "Dream Plant Orb (14)", + "sceneName": "Crossroads_07" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (11)", + "sceneName": "Crossroads_07" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (18)", + "sceneName": "Crossroads_07" + }, + "Value": false + }, + { + "Key": { + "id": "Breakable Wall_Silhouette", + "sceneName": "Crossroads_07" + }, + "Value": true + }, + { + "Key": { + "id": "Dream Plant Orb (26)", + "sceneName": "Crossroads_07" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (2)", + "sceneName": "Crossroads_07" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (21)", + "sceneName": "Crossroads_07" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (5)", + "sceneName": "Crossroads_07" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (19)", + "sceneName": "Crossroads_07" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (28)", + "sceneName": "Crossroads_07" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (1)", + "sceneName": "Crossroads_07" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (23)", + "sceneName": "Crossroads_07" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (22)", + "sceneName": "Crossroads_07" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (25)", + "sceneName": "Crossroads_07" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (12)", + "sceneName": "Crossroads_07" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb", + "sceneName": "Crossroads_07" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant", + "sceneName": "Crossroads_07" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (9)", + "sceneName": "Crossroads_07" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (27)", + "sceneName": "Crossroads_07" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (15)", + "sceneName": "Crossroads_07" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (16)", + "sceneName": "Crossroads_07" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (4)", + "sceneName": "Crossroads_07" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (13)", + "sceneName": "Crossroads_07" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (24)", + "sceneName": "Crossroads_07" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (17)", + "sceneName": "Crossroads_07" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (7)", + "sceneName": "Crossroads_07" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (20)", + "sceneName": "Crossroads_07" + }, + "Value": false + }, + { + "Key": { + "id": "Tute Door 1", + "sceneName": "Crossroads_07" + }, + "Value": true + }, + { + "Key": { + "id": "Dream Plant Orb (8)", + "sceneName": "Crossroads_07" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (6)", + "sceneName": "Crossroads_07" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (3)", + "sceneName": "Crossroads_07" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (10)", + "sceneName": "Crossroads_07" + }, + "Value": false + }, + { + "Key": { + "id": "Break Wall 2", + "sceneName": "Crossroads_08" + }, + "Value": true + }, + { + "Key": { + "id": "Battle Scene", + "sceneName": "Crossroads_08" + }, + "Value": true + }, + { + "Key": { + "id": "break_wall_masks", + "sceneName": "Crossroads_08" + }, + "Value": true + }, + { + "Key": { + "id": "Zombie Barger", + "sceneName": "Crossroads_13" + }, + "Value": true + }, + { + "Key": { + "id": "Heart Piece", + "sceneName": "Crossroads_13" + }, + "Value": false + }, + { + "Key": { + "id": "Break Floor 1", + "sceneName": "Crossroads_13" + }, + "Value": true + }, + { + "Key": { + "id": "Hatcher", + "sceneName": "Crossroads_19" + }, + "Value": true + }, + { + "Key": { + "id": "Zombie Leaper", + "sceneName": "Crossroads_19" + }, + "Value": true + }, + { + "Key": { + "id": "Grub Bottle", + "sceneName": "Crossroads_03" + }, + "Value": true + }, + { + "Key": { + "id": "Break Wall 2", + "sceneName": "Crossroads_03" + }, + "Value": true + }, + { + "Key": { + "id": "Toll Gate Switch", + "sceneName": "Crossroads_03" + }, + "Value": true + }, + { + "Key": { + "id": "CamLock Destroyer", + "sceneName": "Crossroads_04" + }, + "Value": true + }, + { + "Key": { + "id": "Break Floor 1", + "sceneName": "Crossroads_04" + }, + "Value": true + }, + { + "Key": { + "id": "Secret Mask", + "sceneName": "Crossroads_04" + }, + "Value": true + }, + { + "Key": { + "id": "Battle Scene", + "sceneName": "Crossroads_04" + }, + "Value": true + }, + { + "Key": { + "id": "Secret Mask (1)", + "sceneName": "Crossroads_04" + }, + "Value": true + }, + { + "Key": { + "id": "Zombie Hornhead", + "sceneName": "Crossroads_04" + }, + "Value": true + }, + { + "Key": { + "id": "Zombie Runner", + "sceneName": "Crossroads_04" + }, + "Value": true + }, + { + "Key": { + "id": "Zombie Barger", + "sceneName": "Crossroads_04" + }, + "Value": true + }, + { + "Key": { + "id": "Gate Switch", + "sceneName": "Room_Town_Stag_Station" + }, + "Value": true + }, + { + "Key": { + "id": "Zombie Guard", + "sceneName": "Crossroads_21" + }, + "Value": true + }, + { + "Key": { + "id": "Zombie Runner", + "sceneName": "Crossroads_21" + }, + "Value": true + }, + { + "Key": { + "id": "Zombie Leaper", + "sceneName": "Crossroads_21" + }, + "Value": true + }, + { + "Key": { + "id": "Zombie Barger", + "sceneName": "Crossroads_21" + }, + "Value": true + }, + { + "Key": { + "id": "Collapser Small", + "sceneName": "Crossroads_21" + }, + "Value": true + }, + { + "Key": { + "id": "Secret Mask", + "sceneName": "Crossroads_21" + }, + "Value": true + }, + { + "Key": { + "id": "Secret Mask (1)", + "sceneName": "Crossroads_21" + }, + "Value": true + }, + { + "Key": { + "id": "Breakable Wall", + "sceneName": "Crossroads_21" + }, + "Value": true + }, + { + "Key": { + "id": "Zombie Barger (1)", + "sceneName": "Crossroads_21" + }, + "Value": true + }, + { + "Key": { + "id": "Zombie Hornhead", + "sceneName": "Crossroads_10" + }, + "Value": true + }, + { + "Key": { + "id": "Zombie Runner", + "sceneName": "Crossroads_10" + }, + "Value": true + }, + { + "Key": { + "id": "Zombie Barger", + "sceneName": "Crossroads_10" + }, + "Value": true + }, + { + "Key": { + "id": "Chest", + "sceneName": "Crossroads_10" + }, + "Value": true + }, + { + "Key": { + "id": "Breakable Wall", + "sceneName": "Crossroads_10" + }, + "Value": true + }, + { + "Key": { + "id": "Battle Scene", + "sceneName": "Crossroads_10" + }, + "Value": true + }, + { + "Key": { + "id": "Shiny Item", + "sceneName": "Crossroads_10" + }, + "Value": false + }, + { + "Key": { + "id": "Gate Switch", + "sceneName": "Crossroads_06" + }, + "Value": true + }, + { + "Key": { + "id": "Raising Pillar", + "sceneName": "Crossroads_06" + }, + "Value": true + }, + { + "Key": { + "id": "Dream Plant Orb (33)", + "sceneName": "Crossroads_ShamanTemple" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (34)", + "sceneName": "Crossroads_ShamanTemple" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (18)", + "sceneName": "Crossroads_ShamanTemple" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (23)", + "sceneName": "Crossroads_ShamanTemple" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (14)", + "sceneName": "Crossroads_ShamanTemple" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (4)", + "sceneName": "Crossroads_ShamanTemple" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (26)", + "sceneName": "Crossroads_ShamanTemple" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (1)", + "sceneName": "Crossroads_ShamanTemple" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (36)", + "sceneName": "Crossroads_ShamanTemple" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (5)", + "sceneName": "Crossroads_ShamanTemple" + }, + "Value": false + }, + { + "Key": { + "id": "Health Cocoon", + "sceneName": "Crossroads_ShamanTemple" + }, + "Value": true + }, + { + "Key": { + "id": "Dream Plant Orb (9)", + "sceneName": "Crossroads_ShamanTemple" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (30)", + "sceneName": "Crossroads_ShamanTemple" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (31)", + "sceneName": "Crossroads_ShamanTemple" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (10)", + "sceneName": "Crossroads_ShamanTemple" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb", + "sceneName": "Crossroads_ShamanTemple" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (20)", + "sceneName": "Crossroads_ShamanTemple" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (11)", + "sceneName": "Crossroads_ShamanTemple" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (27)", + "sceneName": "Crossroads_ShamanTemple" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (22)", + "sceneName": "Crossroads_ShamanTemple" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (29)", + "sceneName": "Crossroads_ShamanTemple" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (21)", + "sceneName": "Crossroads_ShamanTemple" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (15)", + "sceneName": "Crossroads_ShamanTemple" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (32)", + "sceneName": "Crossroads_ShamanTemple" + }, + "Value": false + }, + { + "Key": { + "id": "Shiny Item", + "sceneName": "Crossroads_ShamanTemple" + }, + "Value": false + }, + { + "Key": { + "id": "Death Respawn Trigger 1", + "sceneName": "Crossroads_ShamanTemple" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (24)", + "sceneName": "Crossroads_ShamanTemple" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (25)", + "sceneName": "Crossroads_ShamanTemple" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (28)", + "sceneName": "Crossroads_ShamanTemple" + }, + "Value": false + }, + { + "Key": { + "id": "Bone Gate", + "sceneName": "Crossroads_ShamanTemple" + }, + "Value": true + }, + { + "Key": { + "id": "Secret Mask", + "sceneName": "Crossroads_ShamanTemple" + }, + "Value": true + }, + { + "Key": { + "id": "Blocker", + "sceneName": "Crossroads_ShamanTemple" + }, + "Value": true + }, + { + "Key": { + "id": "Dream Plant Orb (37)", + "sceneName": "Crossroads_ShamanTemple" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (35)", + "sceneName": "Crossroads_ShamanTemple" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (17)", + "sceneName": "Crossroads_ShamanTemple" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (38)", + "sceneName": "Crossroads_ShamanTemple" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (16)", + "sceneName": "Crossroads_ShamanTemple" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (13)", + "sceneName": "Crossroads_ShamanTemple" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (41)", + "sceneName": "Crossroads_ShamanTemple" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (6)", + "sceneName": "Crossroads_ShamanTemple" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant", + "sceneName": "Crossroads_ShamanTemple" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (40)", + "sceneName": "Crossroads_ShamanTemple" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (12)", + "sceneName": "Crossroads_ShamanTemple" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (3)", + "sceneName": "Crossroads_ShamanTemple" + }, + "Value": false + }, + { + "Key": { + "id": "Battle Scene", + "sceneName": "Crossroads_ShamanTemple" + }, + "Value": true + }, + { + "Key": { + "id": "Dream Plant Orb (39)", + "sceneName": "Crossroads_ShamanTemple" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (19)", + "sceneName": "Crossroads_ShamanTemple" + }, + "Value": false + }, + { + "Key": { + "id": "Remasker", + "sceneName": "Crossroads_ShamanTemple" + }, + "Value": true + }, + { + "Key": { + "id": "Dream Plant Orb (8)", + "sceneName": "Crossroads_ShamanTemple" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (2)", + "sceneName": "Crossroads_ShamanTemple" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (7)", + "sceneName": "Crossroads_ShamanTemple" + }, + "Value": false + }, + { + "Key": { + "id": "Reminder Cast (1)", + "sceneName": "Crossroads_ShamanTemple" + }, + "Value": false + }, + { + "Key": { + "id": "Zombie Runner", + "sceneName": "Crossroads_ShamanTemple" + }, + "Value": true + }, + { + "Key": { + "id": "Prayer Slug", + "sceneName": "Crossroads_10" + }, + "Value": true + }, + { + "Key": { + "id": "Prayer Slug (1)", + "sceneName": "Crossroads_10" + }, + "Value": true + }, + { + "Key": { + "id": "Hatcher 1", + "sceneName": "Crossroads_27" + }, + "Value": true + }, + { + "Key": { + "id": "Hatcher", + "sceneName": "Crossroads_27" + }, + "Value": true + }, + { + "Key": { + "id": "Hatcher 2", + "sceneName": "Crossroads_27" + }, + "Value": true + }, + { + "Key": { + "id": "Zombie Shield 1", + "sceneName": "Crossroads_15" + }, + "Value": true + }, + { + "Key": { + "id": "Zombie Shield", + "sceneName": "Crossroads_15" + }, + "Value": true + }, + { + "Key": { + "id": "Reward 16", + "sceneName": "Crossroads_38" + }, + "Value": false + }, + { + "Key": { + "id": "Reward 31", + "sceneName": "Crossroads_38" + }, + "Value": false + }, + { + "Key": { + "id": "Reward 46", + "sceneName": "Crossroads_38" + }, + "Value": false + }, + { + "Key": { + "id": "Reward 10", + "sceneName": "Crossroads_38" + }, + "Value": false + }, + { + "Key": { + "id": "Reward 38", + "sceneName": "Crossroads_38" + }, + "Value": false + }, + { + "Key": { + "id": "Reward 5", + "sceneName": "Crossroads_38" + }, + "Value": false + }, + { + "Key": { + "id": "Reward 23", + "sceneName": "Crossroads_38" + }, + "Value": false + }, + { + "Key": { + "id": "Blocker", + "sceneName": "Crossroads_11_alt" + }, + "Value": true + }, + { + "Key": { + "id": "Battle Scene", + "sceneName": "Crossroads_11_alt" + }, + "Value": true + }, + { + "Key": { + "id": "Mossman_Shaker", + "sceneName": "Fungus1_01" + }, + "Value": true + }, + { + "Key": { + "id": "Secret Mask", + "sceneName": "Fungus1_01" + }, + "Value": true + }, + { + "Key": { + "id": "Mossman_Runner", + "sceneName": "Fungus1_01" + }, + "Value": true + }, + { + "Key": { + "id": "Plant Trap", + "sceneName": "Fungus1_01" + }, + "Value": true + }, + { + "Key": { + "id": "Mossman_Shaker (1)", + "sceneName": "Fungus1_01" + }, + "Value": true + }, + { + "Key": { + "id": "Mossman_Shaker", + "sceneName": "Fungus1_02" + }, + "Value": true + }, + { + "Key": { + "id": "Secret Mask", + "sceneName": "Fungus1_17" + }, + "Value": true + }, + { + "Key": { + "id": "Secret Mask (1)", + "sceneName": "Fungus1_17" + }, + "Value": true + }, + { + "Key": { + "id": "Moss Charger", + "sceneName": "Fungus1_17" + }, + "Value": true + }, + { + "Key": { + "id": "Zombie Hornhead", + "sceneName": "Fungus1_03" + }, + "Value": true + }, + { + "Key": { + "id": "Zombie Runner", + "sceneName": "Fungus1_03" + }, + "Value": true + }, + { + "Key": { + "id": "Zombie Barger", + "sceneName": "Fungus1_03" + }, + "Value": true + }, + { + "Key": { + "id": "Zombie Runner (1)", + "sceneName": "Fungus1_03" + }, + "Value": true + }, + { + "Key": { + "id": "Plant Trap", + "sceneName": "Fungus1_03" + }, + "Value": true + }, + { + "Key": { + "id": "Mossman_Shaker (1)", + "sceneName": "Fungus1_31" + }, + "Value": true + }, + { + "Key": { + "id": "Mossman_Runner", + "sceneName": "Fungus1_31" + }, + "Value": true + }, + { + "Key": { + "id": "Breakable Wall", + "sceneName": "Fungus1_31" + }, + "Value": true + }, + { + "Key": { + "id": "Toll Gate Machine", + "sceneName": "Fungus1_31" + }, + "Value": true + }, + { + "Key": { + "id": "Toll Gate Machine (1)", + "sceneName": "Fungus1_31" + }, + "Value": true + }, + { + "Key": { + "id": "Mossman_Shaker", + "sceneName": "Fungus1_31" + }, + "Value": true + }, + { + "Key": { + "id": "Moss Knight B", + "sceneName": "Fungus1_32" + }, + "Value": true + }, + { + "Key": { + "id": "Health Cocoon", + "sceneName": "Fungus1_32" + }, + "Value": true + }, + { + "Key": { + "id": "Breakable Wall", + "sceneName": "Fungus1_32" + }, + "Value": true + }, + { + "Key": { + "id": "Battle Scene v2", + "sceneName": "Fungus1_32" + }, + "Value": true + }, + { + "Key": { + "id": "Moss Knight (1)", + "sceneName": "Fungus1_21" + }, + "Value": true + }, + { + "Key": { + "id": "Moss Knight", + "sceneName": "Fungus1_21" + }, + "Value": true + }, + { + "Key": { + "id": "Grub Bottle", + "sceneName": "Fungus1_21" + }, + "Value": true + }, + { + "Key": { + "id": "Battle Scene", + "sceneName": "Fungus1_21" + }, + "Value": true + }, + { + "Key": { + "id": "Secret Mask", + "sceneName": "Fungus1_21" + }, + "Value": true + }, + { + "Key": { + "id": "Vine Platform (2)", + "sceneName": "Fungus1_21" + }, + "Value": true + }, + { + "Key": { + "id": "Moss Charger", + "sceneName": "Fungus1_21" + }, + "Value": true + }, + { + "Key": { + "id": "Moss Charger (1)", + "sceneName": "Fungus1_21" + }, + "Value": true + }, + { + "Key": { + "id": "Moss Charger (2)", + "sceneName": "Fungus1_21" + }, + "Value": true + }, + { + "Key": { + "id": "Gate Switch", + "sceneName": "Fungus1_22" + }, + "Value": true + }, + { + "Key": { + "id": "Vine Platform", + "sceneName": "Fungus1_22" + }, + "Value": true + }, + { + "Key": { + "id": "Shiny Item", + "sceneName": "Fungus1_22" + }, + "Value": false + }, + { + "Key": { + "id": "Breakable Wall", + "sceneName": "Fungus1_22" + }, + "Value": true + }, + { + "Key": { + "id": "Vine Platform (1)", + "sceneName": "Fungus1_22" + }, + "Value": true + }, + { + "Key": { + "id": "Plant Trap (3)", + "sceneName": "Fungus1_22" + }, + "Value": true + }, + { + "Key": { + "id": "Plant Trap (5)", + "sceneName": "Fungus1_22" + }, + "Value": true + }, + { + "Key": { + "id": "Plant Trap (1)", + "sceneName": "Fungus1_22" + }, + "Value": true + }, + { + "Key": { + "id": "Plant Trap", + "sceneName": "Fungus1_22" + }, + "Value": true + }, + { + "Key": { + "id": "Plant Trap (4)", + "sceneName": "Fungus1_22" + }, + "Value": true + }, + { + "Key": { + "id": "Plant Trap (2)", + "sceneName": "Fungus1_22" + }, + "Value": true + }, + { + "Key": { + "id": "Mossman_Shaker (1)", + "sceneName": "Fungus1_22" + }, + "Value": true + }, + { + "Key": { + "id": "Mossman_Shaker (2)", + "sceneName": "Fungus1_22" + }, + "Value": true + }, + { + "Key": { + "id": "Mossman_Shaker", + "sceneName": "Fungus1_22" + }, + "Value": true + }, + { + "Key": { + "id": "secret mask 2", + "sceneName": "Fungus1_04" + }, + "Value": true + }, + { + "Key": { + "id": "Break Floor 1", + "sceneName": "Fungus1_04" + }, + "Value": true + }, + { + "Key": { + "id": "Camera Locks Boss", + "sceneName": "Fungus1_04" + }, + "Value": false + }, + { + "Key": { + "id": "Reminder Look Down", + "sceneName": "Fungus1_04" + }, + "Value": false + }, + { + "Key": { + "id": "Shiny Item", + "sceneName": "Fungus1_04" + }, + "Value": false + }, + { + "Key": { + "id": "Breakable Wall Waterways", + "sceneName": "Crossroads_18" + }, + "Value": true + }, + { + "Key": { + "id": "Inverse Remasker", + "sceneName": "Crossroads_18" + }, + "Value": true + }, + { + "Key": { + "id": "Fungus Flyer (1)", + "sceneName": "Fungus2_06" + }, + "Value": true + }, + { + "Key": { + "id": "Fungus Flyer", + "sceneName": "Fungus2_06" + }, + "Value": true + }, + { + "Key": { + "id": "Fungus Flyer (2)", + "sceneName": "Fungus2_06" + }, + "Value": true + }, + { + "Key": { + "id": "Fungus Flyer (3)", + "sceneName": "Fungus2_06" + }, + "Value": true + }, + { + "Key": { + "id": "Secret Mask (1)", + "sceneName": "Fungus2_07" + }, + "Value": true + }, + { + "Key": { + "id": "Secret Mask", + "sceneName": "Fungus2_07" + }, + "Value": true + }, + { + "Key": { + "id": "Mushroom Roller", + "sceneName": "Fungus2_07" + }, + "Value": true + }, + { + "Key": { + "id": "Mushroom Roller (1)", + "sceneName": "Fungus2_07" + }, + "Value": true + }, + { + "Key": { + "id": "Fungus Flyer", + "sceneName": "Fungus2_08" + }, + "Value": true + }, + { + "Key": { + "id": "Secret Mask", + "sceneName": "Fungus2_10" + }, + "Value": true + }, + { + "Key": { + "id": "One Way Wall", + "sceneName": "Fungus2_10" + }, + "Value": true + }, + { + "Key": { + "id": "Zombie Fungus B", + "sceneName": "Fungus2_10" + }, + "Value": true + }, + { + "Key": { + "id": "Zombie Fungus A", + "sceneName": "Fungus2_10" + }, + "Value": true + }, + { + "Key": { + "id": "Fungus Flyer", + "sceneName": "Fungus2_11" + }, + "Value": true + }, + { + "Key": { + "id": "Mantis (2)", + "sceneName": "Fungus2_12" + }, + "Value": true + }, + { + "Key": { + "id": "Mantis (1)", + "sceneName": "Fungus2_12" + }, + "Value": true + }, + { + "Key": { + "id": "Mantis", + "sceneName": "Fungus2_12" + }, + "Value": true + }, + { + "Key": { + "id": "Mantis (1)", + "sceneName": "Fungus2_13" + }, + "Value": true + }, + { + "Key": { + "id": "Mantis (4)", + "sceneName": "Fungus2_13" + }, + "Value": true + }, + { + "Key": { + "id": "Mantis (3)", + "sceneName": "Fungus2_13" + }, + "Value": true + }, + { + "Key": { + "id": "Mantis (2)", + "sceneName": "Fungus2_13" + }, + "Value": true + }, + { + "Key": { + "id": "Mantis", + "sceneName": "Fungus2_13" + }, + "Value": true + }, + { + "Key": { + "id": "Mantis Lever", + "sceneName": "Fungus2_14" + }, + "Value": true + }, + { + "Key": { + "id": "Mantis (2)", + "sceneName": "Fungus2_14" + }, + "Value": true + }, + { + "Key": { + "id": "Shiny Item Stand", + "sceneName": "Fungus2_14" + }, + "Value": false + }, + { + "Key": { + "id": "Mantis Lever (1)", + "sceneName": "Fungus2_14" + }, + "Value": true + }, + { + "Key": { + "id": "Mantis (1)", + "sceneName": "Fungus2_14" + }, + "Value": true + }, + { + "Key": { + "id": "Gate Mantis", + "sceneName": "Fungus2_14" + }, + "Value": true + }, + { + "Key": { + "id": "Mantis", + "sceneName": "Fungus2_15" + }, + "Value": true + }, + { + "Key": { + "id": "Mantis Lever", + "sceneName": "Fungus2_15" + }, + "Value": true + }, + { + "Key": { + "id": "Mantis (2)", + "sceneName": "Fungus2_15" + }, + "Value": true + }, + { + "Key": { + "id": "Mantis Lever (4)", + "sceneName": "Fungus2_15" + }, + "Value": true + }, + { + "Key": { + "id": "Mantis Lever (3)", + "sceneName": "Fungus2_15" + }, + "Value": true + }, + { + "Key": { + "id": "Breakable Wall", + "sceneName": "Fungus2_15" + }, + "Value": true + }, + { + "Key": { + "id": "Health Cocoon", + "sceneName": "Fungus2_15" + }, + "Value": true + }, + { + "Key": { + "id": "Mantis Lever (1)", + "sceneName": "Fungus2_15" + }, + "Value": true + }, + { + "Key": { + "id": "Mantis Lever (2)", + "sceneName": "Fungus2_15" + }, + "Value": true + }, + { + "Key": { + "id": "Mantis (1)", + "sceneName": "Fungus2_15" + }, + "Value": true + }, + { + "Key": { + "id": "Remasker", + "sceneName": "Fungus2_21" + }, + "Value": true + }, + { + "Key": { + "id": "secret sound", + "sceneName": "Fungus2_21" + }, + "Value": true + }, + { + "Key": { + "id": "Secret Mask", + "sceneName": "Fungus2_21" + }, + "Value": true + }, + { + "Key": { + "id": "Quake Floor", + "sceneName": "Fungus2_21" + }, + "Value": true + }, + { + "Key": { + "id": "Ceiling Dropper", + "sceneName": "Ruins1_01" + }, + "Value": true + }, + { + "Key": { + "id": "Zombie Barger", + "sceneName": "Ruins1_01" + }, + "Value": true + }, + { + "Key": { + "id": "Ceiling Dropper (1)", + "sceneName": "Ruins1_01" + }, + "Value": true + }, + { + "Key": { + "id": "Ruins Sentry 1", + "sceneName": "Ruins1_01" + }, + "Value": true + }, + { + "Key": { + "id": "Zombie Hornhead", + "sceneName": "Ruins1_01" + }, + "Value": true + }, + { + "Key": { + "id": "Zombie Leaper", + "sceneName": "Ruins1_01" + }, + "Value": true + }, + { + "Key": { + "id": "Secret Mask", + "sceneName": "Ruins1_02" + }, + "Value": true + }, + { + "Key": { + "id": "Ruins Sentry 1 (1)", + "sceneName": "Ruins1_02" + }, + "Value": true + }, + { + "Key": { + "id": "Ruins Sentry 1", + "sceneName": "Ruins1_02" + }, + "Value": true + }, + { + "Key": { + "id": "Shiny Item", + "sceneName": "Ruins1_03" + }, + "Value": false + }, + { + "Key": { + "id": "Ruins Sentry 1 (3)", + "sceneName": "Ruins1_03" + }, + "Value": true + }, + { + "Key": { + "id": "Ruins Flying Sentry", + "sceneName": "Ruins1_03" + }, + "Value": true + }, + { + "Key": { + "id": "Ruins Flying Sentry (3)", + "sceneName": "Ruins1_03" + }, + "Value": true + }, + { + "Key": { + "id": "Ruins Sentry 1 (2)", + "sceneName": "Ruins1_03" + }, + "Value": true + }, + { + "Key": { + "id": "Zombie Hornhead", + "sceneName": "Ruins1_03" + }, + "Value": true + }, + { + "Key": { + "id": "Ruins Sentry 1", + "sceneName": "Ruins1_03" + }, + "Value": true + }, + { + "Key": { + "id": "Zombie Hornhead (1)", + "sceneName": "Ruins1_03" + }, + "Value": true + }, + { + "Key": { + "id": "Ruins Sentry 1 (1)", + "sceneName": "Ruins1_03" + }, + "Value": true + }, + { + "Key": { + "id": "Ruins Flying Sentry (2)", + "sceneName": "Ruins1_03" + }, + "Value": true + }, + { + "Key": { + "id": "Zombie Leaper", + "sceneName": "Ruins1_03" + }, + "Value": true + }, + { + "Key": { + "id": "One Way Wall", + "sceneName": "Ruins1_04" + }, + "Value": true + }, + { + "Key": { + "id": "Secret Mask", + "sceneName": "Ruins1_04" + }, + "Value": true + }, + { + "Key": { + "id": "Inverse Remasker", + "sceneName": "Ruins1_04" + }, + "Value": true + }, + { + "Key": { + "id": "Secret Mask (1)", + "sceneName": "Ruins1_04" + }, + "Value": true + }, + { + "Key": { + "id": "Ruins Sentry Fat", + "sceneName": "Ruins1_05c" + }, + "Value": true + }, + { + "Key": { + "id": "Remasker", + "sceneName": "Ruins1_05c" + }, + "Value": true + }, + { + "Key": { + "id": "Shiny Item", + "sceneName": "Ruins1_05" + }, + "Value": false + }, + { + "Key": { + "id": "Ruins Lever 3", + "sceneName": "Ruins1_05" + }, + "Value": true + }, + { + "Key": { + "id": "Remasker (1)", + "sceneName": "Ruins1_05c" + }, + "Value": true + }, + { + "Key": { + "id": "Ruins Lever 2", + "sceneName": "Ruins1_05" + }, + "Value": true + }, + { + "Key": { + "id": "Ruins Flying Sentry Javelin (2)", + "sceneName": "Ruins1_05c" + }, + "Value": true + }, + { + "Key": { + "id": "Ruins Sentry 1 (7)", + "sceneName": "Ruins1_05c" + }, + "Value": true + }, + { + "Key": { + "id": "Ruins Sentry 1 (9)", + "sceneName": "Ruins1_05c" + }, + "Value": true + }, + { + "Key": { + "id": "Ceiling Dropper (5)", + "sceneName": "Ruins1_05c" + }, + "Value": true + }, + { + "Key": { + "id": "Ceiling Dropper (4)", + "sceneName": "Ruins1_05c" + }, + "Value": true + }, + { + "Key": { + "id": "Zombie Barger", + "sceneName": "Ruins1_05c" + }, + "Value": true + }, + { + "Key": { + "id": "Ceiling Dropper (6)", + "sceneName": "Ruins1_05c" + }, + "Value": true + }, + { + "Key": { + "id": "Ruins Sentry Fat (5)", + "sceneName": "Ruins1_05c" + }, + "Value": true + }, + { + "Key": { + "id": "Ruins Lever 1", + "sceneName": "Ruins1_05b" + }, + "Value": true + }, + { + "Key": { + "id": "Ruins Flying Sentry", + "sceneName": "Ruins1_05" + }, + "Value": true + }, + { + "Key": { + "id": "Ceiling Dropper", + "sceneName": "Ruins1_05" + }, + "Value": true + }, + { + "Key": { + "id": "Battle Scene v2", + "sceneName": "Ruins1_05" + }, + "Value": true + }, + { + "Key": { + "id": "Ruins Lever", + "sceneName": "Ruins1_05" + }, + "Value": true + }, + { + "Key": { + "id": "Grub Bottle (1)", + "sceneName": "Ruins1_05" + }, + "Value": true + }, + { + "Key": { + "id": "Ruins Flying Sentry Javelin (3)", + "sceneName": "Ruins1_05" + }, + "Value": true + }, + { + "Key": { + "id": "Zombie Hornhead", + "sceneName": "Ruins1_05" + }, + "Value": true + }, + { + "Key": { + "id": "Ruins Flying Sentry (1)", + "sceneName": "Ruins1_05" + }, + "Value": true + }, + { + "Key": { + "id": "Ruins Sentry FatB", + "sceneName": "Ruins1_05" + }, + "Value": true + }, + { + "Key": { + "id": "Ruins Lever", + "sceneName": "Ruins1_31" + }, + "Value": true + }, + { + "Key": { + "id": "Breakable Wall Ruin Lift", + "sceneName": "Ruins1_31" + }, + "Value": true + }, + { + "Key": { + "id": "Ruins Vial Empty", + "sceneName": "Ruins1_31" + }, + "Value": true + }, + { + "Key": { + "id": "Toll Machine Bench", + "sceneName": "Ruins1_31" + }, + "Value": true + }, + { + "Key": { + "id": "Ruins Flying Sentry Javelin (1)", + "sceneName": "Ruins1_17" + }, + "Value": true + }, + { + "Key": { + "id": "Ruins Flying Sentry", + "sceneName": "Ruins1_17" + }, + "Value": true + }, + { + "Key": { + "id": "Ruins Flying Sentry Javelin", + "sceneName": "Ruins1_17" + }, + "Value": true + }, + { + "Key": { + "id": "Dream Plant Orb (27)", + "sceneName": "Ruins1_17" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (17)", + "sceneName": "Ruins1_17" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (9)", + "sceneName": "Ruins1_17" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (26)", + "sceneName": "Ruins1_17" + }, + "Value": false + }, + { + "Key": { + "id": "Shiny Item", + "sceneName": "Ruins1_17" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (1)", + "sceneName": "Ruins1_17" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (20)", + "sceneName": "Ruins1_17" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (10)", + "sceneName": "Ruins1_17" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (24)", + "sceneName": "Ruins1_17" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (14)", + "sceneName": "Ruins1_17" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (19)", + "sceneName": "Ruins1_17" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (8)", + "sceneName": "Ruins1_17" + }, + "Value": false + }, + { + "Key": { + "id": "Remasker full bot", + "sceneName": "Ruins1_17" + }, + "Value": true + }, + { + "Key": { + "id": "Dream Plant Orb (15)", + "sceneName": "Ruins1_17" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (7)", + "sceneName": "Ruins1_17" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (11)", + "sceneName": "Ruins1_17" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (6)", + "sceneName": "Ruins1_17" + }, + "Value": false + }, + { + "Key": { + "id": "remask_half_mid", + "sceneName": "Ruins1_17" + }, + "Value": true + }, + { + "Key": { + "id": "Dream Plant Orb (13)", + "sceneName": "Ruins1_17" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (5)", + "sceneName": "Ruins1_17" + }, + "Value": false + }, + { + "Key": { + "id": "Remasker full mid", + "sceneName": "Ruins1_17" + }, + "Value": true + }, + { + "Key": { + "id": "Remasker full top", + "sceneName": "Ruins1_17" + }, + "Value": true + }, + { + "Key": { + "id": "remask_half_bot", + "sceneName": "Ruins1_17" + }, + "Value": true + }, + { + "Key": { + "id": "Dream Plant Orb (12)", + "sceneName": "Ruins1_17" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant", + "sceneName": "Ruins1_17" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (21)", + "sceneName": "Ruins1_17" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (25)", + "sceneName": "Ruins1_17" + }, + "Value": false + }, + { + "Key": { + "id": "Ruins Lever", + "sceneName": "Ruins1_17" + }, + "Value": true + }, + { + "Key": { + "id": "Dream Plant Orb (16)", + "sceneName": "Ruins1_17" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (2)", + "sceneName": "Ruins1_17" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (23)", + "sceneName": "Ruins1_17" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb", + "sceneName": "Ruins1_17" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (18)", + "sceneName": "Ruins1_17" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (4)", + "sceneName": "Ruins1_17" + }, + "Value": false + }, + { + "Key": { + "id": "remask_half_bot (2)", + "sceneName": "Ruins1_17" + }, + "Value": true + }, + { + "Key": { + "id": "Dream Plant Orb (22)", + "sceneName": "Ruins1_17" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (3)", + "sceneName": "Ruins1_17" + }, + "Value": false + }, + { + "Key": { + "id": "Zombie Hornhead", + "sceneName": "Ruins1_17" + }, + "Value": true + }, + { + "Key": { + "id": "Ruins Flying Sentry (1)", + "sceneName": "Ruins1_17" + }, + "Value": true + }, + { + "Key": { + "id": "Ruins Sentry 1", + "sceneName": "Ruins1_17" + }, + "Value": true + }, + { + "Key": { + "id": "Zombie Leaper", + "sceneName": "Ruins1_17" + }, + "Value": true + }, + { + "Key": { + "id": "Zombie Barger", + "sceneName": "Ruins1_28" + }, + "Value": true + }, + { + "Key": { + "id": "Zombie Runner", + "sceneName": "Ruins1_28" + }, + "Value": true + }, + { + "Key": { + "id": "Zombie Runner (1)", + "sceneName": "Ruins1_28" + }, + "Value": true + }, + { + "Key": { + "id": "Shiny Item", + "sceneName": "Ruins1_28" + }, + "Value": false + }, + { + "Key": { + "id": "Ruins Flying Sentry", + "sceneName": "Ruins1_28" + }, + "Value": true + }, + { + "Key": { + "id": "Chain Platform", + "sceneName": "Ruins1_28" + }, + "Value": true + }, + { + "Key": { + "id": "Zombie Barger 1", + "sceneName": "Crossroads_37" + }, + "Value": true + }, + { + "Key": { + "id": "Zombie Hornhead", + "sceneName": "Crossroads_37" + }, + "Value": true + }, + { + "Key": { + "id": "Zombie Hornhead 1", + "sceneName": "Crossroads_37" + }, + "Value": true + }, + { + "Key": { + "id": "Zombie Runner 1", + "sceneName": "Crossroads_37" + }, + "Value": true + }, + { + "Key": { + "id": "Zombie Barger (1)", + "sceneName": "Crossroads_37" + }, + "Value": true + }, + { + "Key": { + "id": "Zombie Barger", + "sceneName": "Crossroads_37" + }, + "Value": true + }, + { + "Key": { + "id": "Zombie Leaper", + "sceneName": "Crossroads_37" + }, + "Value": true + }, + { + "Key": { + "id": "Remasker", + "sceneName": "Crossroads_37" + }, + "Value": true + }, + { + "Key": { + "id": "Mask Bottom", + "sceneName": "Crossroads_37" + }, + "Value": true + }, + { + "Key": { + "id": "Break Floor 1", + "sceneName": "Crossroads_37" + }, + "Value": true + }, + { + "Key": { + "id": "Vessel Fragment", + "sceneName": "Crossroads_37" + }, + "Value": false + }, + { + "Key": { + "id": "Mask Bottom 2", + "sceneName": "Crossroads_37" + }, + "Value": true + }, + { + "Key": { + "id": "Zombie Runner (1)", + "sceneName": "Crossroads_37" + }, + "Value": true + }, + { + "Key": { + "id": "Zombie Hornhead 2", + "sceneName": "Crossroads_37" + }, + "Value": true + }, + { + "Key": { + "id": "Zombie Barger 3", + "sceneName": "Crossroads_37" + }, + "Value": true + }, + { + "Key": { + "id": "Grub Bottle", + "sceneName": "Crossroads_31" + }, + "Value": true + }, + { + "Key": { + "id": "Zombie Hornhead", + "sceneName": "Crossroads_16" + }, + "Value": true + }, + { + "Key": { + "id": "Zombie Guard", + "sceneName": "Crossroads_48" + }, + "Value": true + }, + { + "Key": { + "id": "Grub Bottle", + "sceneName": "Crossroads_48" + }, + "Value": true + }, + { + "Key": { + "id": "Zombie Runner", + "sceneName": "Crossroads_39" + }, + "Value": true + }, + { + "Key": { + "id": "Zombie Hornhead 1", + "sceneName": "Crossroads_39" + }, + "Value": true + }, + { + "Key": { + "id": "Zombie Runner 1", + "sceneName": "Crossroads_39" + }, + "Value": true + }, + { + "Key": { + "id": "Shiny", + "sceneName": "Crossroads_33" + }, + "Value": true + }, + { + "Key": { + "id": "Battle Scene v2", + "sceneName": "Fungus2_05" + }, + "Value": true + }, + { + "Key": { + "id": "Shiny Item", + "sceneName": "Fungus2_05" + }, + "Value": false + }, + { + "Key": { + "id": "Zombie Runner", + "sceneName": "Crossroads_05" + }, + "Value": true + }, + { + "Key": { + "id": "Grub Bottle", + "sceneName": "Crossroads_05" + }, + "Value": true + }, + { + "Key": { + "id": "Zombie Runner 2", + "sceneName": "Crossroads_40" + }, + "Value": true + }, + { + "Key": { + "id": "Zombie Hornhead", + "sceneName": "Crossroads_40" + }, + "Value": true + }, + { + "Key": { + "id": "Zombie Leaper", + "sceneName": "Crossroads_40" + }, + "Value": true + }, + { + "Key": { + "id": "Zombie Leaper 1", + "sceneName": "Crossroads_40" + }, + "Value": true + }, + { + "Key": { + "id": "Shiny", + "sceneName": "Ruins1_31" + }, + "Value": true + }, + { + "Key": { + "id": "Ruins Sentry Fat B", + "sceneName": "Ruins1_05" + }, + "Value": true + }, + { + "Key": { + "id": "Mage", + "sceneName": "Ruins1_09" + }, + "Value": true + }, + { + "Key": { + "id": "Battle Scene", + "sceneName": "Ruins1_09" + }, + "Value": true + }, + { + "Key": { + "id": "Ruins Lever (1)", + "sceneName": "Ruins1_23" + }, + "Value": true + }, + { + "Key": { + "id": "Ruins Lever", + "sceneName": "Ruins1_23" + }, + "Value": true + }, + { + "Key": { + "id": "Mage (1)", + "sceneName": "Ruins1_23" + }, + "Value": true + }, + { + "Key": { + "id": "Mage", + "sceneName": "Ruins1_23" + }, + "Value": true + }, + { + "Key": { + "id": "Ruins Vial Empty (1)", + "sceneName": "Ruins1_23" + }, + "Value": true + }, + { + "Key": { + "id": "Ruins Vial Empty", + "sceneName": "Ruins1_23" + }, + "Value": true + }, + { + "Key": { + "id": "Battle Scene v2", + "sceneName": "Ruins1_23" + }, + "Value": true + }, + { + "Key": { + "id": "Ruins Vial Empty (2)", + "sceneName": "Ruins1_23" + }, + "Value": true + }, + { + "Key": { + "id": "Ruins Vial Empty", + "sceneName": "Ruins1_25" + }, + "Value": true + }, + { + "Key": { + "id": "Ruins Vial Empty (1)", + "sceneName": "Ruins1_25" + }, + "Value": true + }, + { + "Key": { + "id": "Ruins Lever", + "sceneName": "Ruins1_25" + }, + "Value": true + }, + { + "Key": { + "id": "Mage", + "sceneName": "Ruins1_25" + }, + "Value": true + }, + { + "Key": { + "id": "Mage (1)", + "sceneName": "Ruins1_25" + }, + "Value": true + }, + { + "Key": { + "id": "Mage", + "sceneName": "Ruins1_30" + }, + "Value": true + }, + { + "Key": { + "id": "Mage (1)", + "sceneName": "Ruins1_30" + }, + "Value": true + }, + { + "Key": { + "id": "Quake Floor Glass", + "sceneName": "Ruins1_30" + }, + "Value": true + }, + { + "Key": { + "id": "Secret Sound Region", + "sceneName": "Ruins1_30" + }, + "Value": true + }, + { + "Key": { + "id": "Inverse Remasker", + "sceneName": "Ruins1_30" + }, + "Value": true + }, + { + "Key": { + "id": "Secret Mask (3)", + "sceneName": "Ruins1_30" + }, + "Value": true + }, + { + "Key": { + "id": "Quake Floor Glass (1)", + "sceneName": "Ruins1_30" + }, + "Value": true + }, + { + "Key": { + "id": "Secret Mask", + "sceneName": "Ruins1_30" + }, + "Value": true + }, + { + "Key": { + "id": "Secret Sounder", + "sceneName": "Ruins1_30" + }, + "Value": true + }, + { + "Key": { + "id": "Quake Floor Glass (2)", + "sceneName": "Ruins1_30" + }, + "Value": true + }, + { + "Key": { + "id": "Ruins Lever", + "sceneName": "Ruins1_30" + }, + "Value": true + }, + { + "Key": { + "id": "Shiny Item Stand", + "sceneName": "Ruins1_30" + }, + "Value": false + }, + { + "Key": { + "id": "Remasker", + "sceneName": "Ruins1_30" + }, + "Value": true + }, + { + "Key": { + "id": "Ruins Vial Empty", + "sceneName": "Ruins1_30" + }, + "Value": true + }, + { + "Key": { + "id": "Ruins Light Stand", + "sceneName": "Ruins1_30" + }, + "Value": true + }, + { + "Key": { + "id": "Secret Mask (1)", + "sceneName": "Ruins1_30" + }, + "Value": true + }, + { + "Key": { + "id": "Mage (2)", + "sceneName": "Ruins1_30" + }, + "Value": true + }, + { + "Key": { + "id": "Ruins Vial Empty (1)", + "sceneName": "Ruins1_30" + }, + "Value": true + }, + { + "Key": { + "id": "Secret Mask (2)", + "sceneName": "Ruins1_30" + }, + "Value": true + }, + { + "Key": { + "id": "Secret Mask (1)", + "sceneName": "Ruins1_24" + }, + "Value": true + }, + { + "Key": { + "id": "Ruins Vial Empty", + "sceneName": "Ruins1_24" + }, + "Value": true + }, + { + "Key": { + "id": "Ruins Vial Empty (1)", + "sceneName": "Ruins1_24" + }, + "Value": true + }, + { + "Key": { + "id": "Ruins Soul Vial", + "sceneName": "Ruins1_24" + }, + "Value": true + }, + { + "Key": { + "id": "Quake Floor Glass (2)", + "sceneName": "Ruins1_32" + }, + "Value": true + }, + { + "Key": { + "id": "Secret Mask (2)", + "sceneName": "Ruins1_32" + }, + "Value": true + }, + { + "Key": { + "id": "Quake Floor Glass (1)", + "sceneName": "Ruins1_32" + }, + "Value": true + }, + { + "Key": { + "id": "Quake Floor Glass (4)", + "sceneName": "Ruins1_32" + }, + "Value": true + }, + { + "Key": { + "id": "Secret Mask", + "sceneName": "Ruins1_32" + }, + "Value": true + }, + { + "Key": { + "id": "Quake Floor Glass", + "sceneName": "Ruins1_32" + }, + "Value": true + }, + { + "Key": { + "id": "Ruins Vial Empty (4)", + "sceneName": "Ruins1_32" + }, + "Value": true + }, + { + "Key": { + "id": "Quake Floor", + "sceneName": "Ruins1_32" + }, + "Value": true + }, + { + "Key": { + "id": "Ruins Lever (1)", + "sceneName": "Ruins1_32" + }, + "Value": true + }, + { + "Key": { + "id": "Quake Floor (1)", + "sceneName": "Ruins1_32" + }, + "Value": true + }, + { + "Key": { + "id": "Secret Mask (1)", + "sceneName": "Ruins1_32" + }, + "Value": true + }, + { + "Key": { + "id": "Chest", + "sceneName": "Ruins1_32" + }, + "Value": true + }, + { + "Key": { + "id": "Grub Bottle", + "sceneName": "Ruins1_32" + }, + "Value": true + }, + { + "Key": { + "id": "Quake Floor Glass (3)", + "sceneName": "Ruins1_32" + }, + "Value": true + }, + { + "Key": { + "id": "Ruins Lever", + "sceneName": "Ruins1_32" + }, + "Value": true + }, + { + "Key": { + "id": "Shiny Item", + "sceneName": "Ruins1_32" + }, + "Value": false + }, + { + "Key": { + "id": "Heart Piece", + "sceneName": "Crossroads_38" + }, + "Value": false + }, + { + "Key": { + "id": "mine_1_quake_floor", + "sceneName": "Mines_01" + }, + "Value": true + }, + { + "Key": { + "id": "Egg Sac", + "sceneName": "Mines_01" + }, + "Value": true + }, + { + "Key": { + "id": "Shiny Item(Clone)", + "sceneName": "Mines_01" + }, + "Value": false + }, + { + "Key": { + "id": "Zombie Miner 1", + "sceneName": "Mines_02" + }, + "Value": true + }, + { + "Key": { + "id": "Zombie Miner 1 (1)", + "sceneName": "Mines_02" + }, + "Value": true + }, + { + "Key": { + "id": "Shiny Item", + "sceneName": "Mines_29" + }, + "Value": false + }, + { + "Key": { + "id": "Crystal Flyer (1)", + "sceneName": "Mines_04" + }, + "Value": true + }, + { + "Key": { + "id": "Zombie Miner 1", + "sceneName": "Mines_04" + }, + "Value": true + }, + { + "Key": { + "id": "Zombie Miner 1 (1)", + "sceneName": "Mines_04" + }, + "Value": true + }, + { + "Key": { + "id": "Grub Bottle", + "sceneName": "Mines_04" + }, + "Value": true + }, + { + "Key": { + "id": "Mines Lever", + "sceneName": "Mines_04" + }, + "Value": true + }, + { + "Key": { + "id": "Crystal Flyer (3)", + "sceneName": "Mines_04" + }, + "Value": true + }, + { + "Key": { + "id": "Crystal Flyer (2)", + "sceneName": "Mines_04" + }, + "Value": true + }, + { + "Key": { + "id": "Crystal Flyer", + "sceneName": "Mines_04" + }, + "Value": true + }, + { + "Key": { + "id": "Crystal Flyer", + "sceneName": "Mines_07" + }, + "Value": true + }, + { + "Key": { + "id": "Crystal Flyer (2)", + "sceneName": "Mines_07" + }, + "Value": true + }, + { + "Key": { + "id": "Zombie Miner 1", + "sceneName": "Mines_17" + }, + "Value": true + }, + { + "Key": { + "id": "Zombie Miner 1 (1)", + "sceneName": "Mines_17" + }, + "Value": true + }, + { + "Key": { + "id": "Grub Bottle", + "sceneName": "Mines_03" + }, + "Value": true + }, + { + "Key": { + "id": "Mines Lever", + "sceneName": "Mines_03" + }, + "Value": true + }, + { + "Key": { + "id": "Zombie Miner 1 (2)", + "sceneName": "Mines_03" + }, + "Value": true + }, + { + "Key": { + "id": "Zombie Miner 1 (1)", + "sceneName": "Mines_03" + }, + "Value": true + }, + { + "Key": { + "id": "Zombie Miner 1 (3)", + "sceneName": "Mines_03" + }, + "Value": true + }, + { + "Key": { + "id": "Zombie Miner 1", + "sceneName": "Mines_05" + }, + "Value": true + }, + { + "Key": { + "id": "Breakable Wall", + "sceneName": "Mines_05" + }, + "Value": true + }, + { + "Key": { + "id": "Crystallised Lazer Bug (2)", + "sceneName": "Mines_05" + }, + "Value": true + }, + { + "Key": { + "id": "Crystallised Lazer Bug (11)", + "sceneName": "Mines_05" + }, + "Value": true + }, + { + "Key": { + "id": "Crystallised Lazer Bug", + "sceneName": "Mines_05" + }, + "Value": true + }, + { + "Key": { + "id": "Crystallised Lazer Bug (6)", + "sceneName": "Mines_05" + }, + "Value": true + }, + { + "Key": { + "id": "Crystallised Lazer Bug (12)", + "sceneName": "Mines_05" + }, + "Value": true + }, + { + "Key": { + "id": "Crystallised Lazer Bug (10)", + "sceneName": "Mines_05" + }, + "Value": true + }, + { + "Key": { + "id": "Crystallised Lazer Bug (3)", + "sceneName": "Mines_05" + }, + "Value": true + }, + { + "Key": { + "id": "Crystallised Lazer Bug (9)", + "sceneName": "Mines_05" + }, + "Value": true + }, + { + "Key": { + "id": "Crystallised Lazer Bug (5)", + "sceneName": "Mines_05" + }, + "Value": true + }, + { + "Key": { + "id": "Crystallised Lazer Bug (1)", + "sceneName": "Mines_05" + }, + "Value": true + }, + { + "Key": { + "id": "Shiny Item Stand", + "sceneName": "Mines_30" + }, + "Value": false + }, + { + "Key": { + "id": "Zombie Miner 1", + "sceneName": "Mines_11" + }, + "Value": true + }, + { + "Key": { + "id": "Shiny Item Stand", + "sceneName": "Mines_11" + }, + "Value": false + }, + { + "Key": { + "id": "Crystallised Lazer Bug (4)", + "sceneName": "Mines_11" + }, + "Value": true + }, + { + "Key": { + "id": "Crystallised Lazer Bug (2)", + "sceneName": "Mines_11" + }, + "Value": true + }, + { + "Key": { + "id": "Crystallised Lazer Bug (1)", + "sceneName": "Mines_11" + }, + "Value": true + }, + { + "Key": { + "id": "Crystallised Lazer Bug", + "sceneName": "Mines_11" + }, + "Value": true + }, + { + "Key": { + "id": "Crystallised Lazer Bug (3)", + "sceneName": "Mines_11" + }, + "Value": true + }, + { + "Key": { + "id": "Mega Zombie Beam Miner (1)", + "sceneName": "Mines_18" + }, + "Value": true + }, + { + "Key": { + "id": "Battle Scene", + "sceneName": "Mines_18" + }, + "Value": true + }, + { + "Key": { + "id": "Egg Sac", + "sceneName": "Mines_20" + }, + "Value": true + }, + { + "Key": { + "id": "Quake Floor", + "sceneName": "Mines_20" + }, + "Value": true + }, + { + "Key": { + "id": "Mines Lever (2)", + "sceneName": "Mines_20" + }, + "Value": true + }, + { + "Key": { + "id": "Shiny Item (1)", + "sceneName": "Mines_20" + }, + "Value": false + }, + { + "Key": { + "id": "Mines Lever (1)", + "sceneName": "Mines_20" + }, + "Value": true + }, + { + "Key": { + "id": "Mines Lever", + "sceneName": "Mines_20" + }, + "Value": true + }, + { + "Key": { + "id": "Crystallised Lazer Bug (7)", + "sceneName": "Mines_20" + }, + "Value": true + }, + { + "Key": { + "id": "Crystallised Lazer Bug (5)", + "sceneName": "Mines_20" + }, + "Value": true + }, + { + "Key": { + "id": "Crystallised Lazer Bug (3)", + "sceneName": "Mines_20" + }, + "Value": true + }, + { + "Key": { + "id": "Crystallised Lazer Bug (4)", + "sceneName": "Mines_20" + }, + "Value": true + }, + { + "Key": { + "id": "Crystallised Lazer Bug (8)", + "sceneName": "Mines_20" + }, + "Value": true + }, + { + "Key": { + "id": "Crystallised Lazer Bug (9)", + "sceneName": "Mines_20" + }, + "Value": true + }, + { + "Key": { + "id": "Crystallised Lazer Bug (6)", + "sceneName": "Mines_20" + }, + "Value": true + }, + { + "Key": { + "id": "Zombie Miner 1", + "sceneName": "Mines_20" + }, + "Value": true + }, + { + "Key": { + "id": "Zombie Miner 1 (9)", + "sceneName": "Mines_20" + }, + "Value": true + }, + { + "Key": { + "id": "Zombie Miner 1 (1)", + "sceneName": "Mines_20" + }, + "Value": true + }, + { + "Key": { + "id": "Crystal Flyer (3)", + "sceneName": "Mines_20" + }, + "Value": true + }, + { + "Key": { + "id": "Crystal Flyer", + "sceneName": "Mines_20" + }, + "Value": true + }, + { + "Key": { + "id": "Crystal Flyer (1)", + "sceneName": "Mines_20" + }, + "Value": true + }, + { + "Key": { + "id": "Crystal Flyer (2)", + "sceneName": "Mines_20" + }, + "Value": true + }, + { + "Key": { + "id": "Mines Lever", + "sceneName": "Mines_19" + }, + "Value": true + }, + { + "Key": { + "id": "Grub Bottle", + "sceneName": "Mines_19" + }, + "Value": true + }, + { + "Key": { + "id": "Mines Lever New", + "sceneName": "Mines_19" + }, + "Value": true + }, + { + "Key": { + "id": "Grub Bottle", + "sceneName": "Mines_31" + }, + "Value": true + }, + { + "Key": { + "id": "Reminder Superdash", + "sceneName": "Mines_31" + }, + "Value": false + }, + { + "Key": { + "id": "Remasker", + "sceneName": "Mines_31" + }, + "Value": true + }, + { + "Key": { + "id": "Zombie Miner 1 (2)", + "sceneName": "Mines_37" + }, + "Value": true + }, + { + "Key": { + "id": "Zombie Miner 1 (5)", + "sceneName": "Mines_37" + }, + "Value": true + }, + { + "Key": { + "id": "Zombie Miner 1 (4)", + "sceneName": "Mines_37" + }, + "Value": true + }, + { + "Key": { + "id": "Zombie Miner 1 (6)", + "sceneName": "Mines_37" + }, + "Value": true + }, + { + "Key": { + "id": "Mines Lever (3)", + "sceneName": "Mines_37" + }, + "Value": true + }, + { + "Key": { + "id": "Mines Lever (4)", + "sceneName": "Mines_37" + }, + "Value": true + }, + { + "Key": { + "id": "Mines Lever New", + "sceneName": "Mines_37" + }, + "Value": true + }, + { + "Key": { + "id": "Chest", + "sceneName": "Mines_37" + }, + "Value": true + }, + { + "Key": { + "id": "Crystallised Lazer Bug", + "sceneName": "Mines_37" + }, + "Value": true + }, + { + "Key": { + "id": "Crystallised Lazer Bug (1)", + "sceneName": "Mines_37" + }, + "Value": true + }, + { + "Key": { + "id": "Zombie Miner 1 (3)", + "sceneName": "Mines_37" + }, + "Value": true + }, + { + "Key": { + "id": "Shiny", + "sceneName": "Mines_30" + }, + "Value": true + }, + { + "Key": { + "id": "Ruins Lever", + "sceneName": "Ruins1_27" + }, + "Value": true + }, + { + "Key": { + "id": "Remasker", + "sceneName": "Ruins1_27" + }, + "Value": true + }, + { + "Key": { + "id": "Royal Zombie Coward (1)", + "sceneName": "Ruins1_27" + }, + "Value": true + }, + { + "Key": { + "id": "Royal Zombie Coward", + "sceneName": "Ruins1_27" + }, + "Value": true + }, + { + "Key": { + "id": "Royal Zombie 1", + "sceneName": "Ruins1_27" + }, + "Value": true + }, + { + "Key": { + "id": "Royal Zombie 1 (1)", + "sceneName": "Ruins1_27" + }, + "Value": true + }, + { + "Key": { + "id": "Grub Bottle", + "sceneName": "Mines_35" + }, + "Value": true + }, + { + "Key": { + "id": "One Way Wall", + "sceneName": "Mines_35" + }, + "Value": true + }, + { + "Key": { + "id": "mine_1_quake_floor", + "sceneName": "Mines_35" + }, + "Value": true + }, + { + "Key": { + "id": "Secret Mask", + "sceneName": "Mines_35" + }, + "Value": true + }, + { + "Key": { + "id": "Crystal Flyer (2)", + "sceneName": "Mines_35" + }, + "Value": true + }, + { + "Key": { + "id": "Crystal Flyer", + "sceneName": "Mines_35" + }, + "Value": true + }, + { + "Key": { + "id": "Crystal Flyer (1)", + "sceneName": "Mines_35" + }, + "Value": true + }, + { + "Key": { + "id": "Remasker", + "sceneName": "RestingGrounds_07" + }, + "Value": true + }, + { + "Key": { + "id": "Dream Plant Orb (18)", + "sceneName": "RestingGrounds_05" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (11)", + "sceneName": "RestingGrounds_05" + }, + "Value": false + }, + { + "Key": { + "id": "Quake Floor", + "sceneName": "RestingGrounds_05" + }, + "Value": true + }, + { + "Key": { + "id": "Dream Plant Orb (9)", + "sceneName": "RestingGrounds_05" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (3)", + "sceneName": "RestingGrounds_05" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (10)", + "sceneName": "RestingGrounds_05" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (13)", + "sceneName": "RestingGrounds_05" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (2)", + "sceneName": "RestingGrounds_05" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (12)", + "sceneName": "RestingGrounds_05" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant", + "sceneName": "RestingGrounds_05" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (8)", + "sceneName": "RestingGrounds_05" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (19)", + "sceneName": "RestingGrounds_05" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (17)", + "sceneName": "RestingGrounds_05" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (16)", + "sceneName": "RestingGrounds_05" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (4)", + "sceneName": "RestingGrounds_05" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (5)", + "sceneName": "RestingGrounds_05" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (1)", + "sceneName": "RestingGrounds_05" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb", + "sceneName": "RestingGrounds_05" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (14)", + "sceneName": "RestingGrounds_05" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (15)", + "sceneName": "RestingGrounds_05" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (7)", + "sceneName": "RestingGrounds_05" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (6)", + "sceneName": "RestingGrounds_05" + }, + "Value": false + }, + { + "Key": { + "id": "Shiny Item Stand", + "sceneName": "RestingGrounds_17" + }, + "Value": false + }, + { + "Key": { + "id": "Ruins Lever", + "sceneName": "RestingGrounds_09" + }, + "Value": true + }, + { + "Key": { + "id": "Shiny", + "sceneName": "RestingGrounds_09" + }, + "Value": true + }, + { + "Key": { + "id": "Shiny Item Grubsong", + "sceneName": "Crossroads_38" + }, + "Value": false + }, + { + "Key": { + "id": "Hatcher", + "sceneName": "Crossroads_35" + }, + "Value": true + }, + { + "Key": { + "id": "Grub Bottle", + "sceneName": "Crossroads_35" + }, + "Value": true + }, + { + "Key": { + "id": "Remasker", + "sceneName": "Crossroads_35" + }, + "Value": true + }, + { + "Key": { + "id": "Mushroom Brawler", + "sceneName": "Fungus2_05" + }, + "Value": true + }, + { + "Key": { + "id": "Mantis Lever", + "sceneName": "Fungus2_04" + }, + "Value": true + }, + { + "Key": { + "id": "Mantis Lever (1)", + "sceneName": "Fungus2_04" + }, + "Value": true + }, + { + "Key": { + "id": "Shiny Item", + "sceneName": "Fungus2_04" + }, + "Value": false + }, + { + "Key": { + "id": "Zombie Fungus B", + "sceneName": "Fungus2_03" + }, + "Value": true + }, + { + "Key": { + "id": "Shiny Item", + "sceneName": "Fungus2_03" + }, + "Value": false + }, + { + "Key": { + "id": "Fungus Flyer", + "sceneName": "Fungus2_03" + }, + "Value": true + }, + { + "Key": { + "id": "Fungus Flyer (1)", + "sceneName": "Fungus2_03" + }, + "Value": true + }, + { + "Key": { + "id": "Fungus Flyer (2)", + "sceneName": "Fungus2_03" + }, + "Value": true + }, + { + "Key": { + "id": "Zombie Hornhead", + "sceneName": "Fungus2_03" + }, + "Value": true + }, + { + "Key": { + "id": "Ruins Lever", + "sceneName": "Fungus2_01" + }, + "Value": true + }, + { + "Key": { + "id": "Remasker", + "sceneName": "Fungus2_01" + }, + "Value": true + }, + { + "Key": { + "id": "Heart Piece", + "sceneName": "Fungus2_01" + }, + "Value": false + }, + { + "Key": { + "id": "Breakable Wall Waterways", + "sceneName": "Waterways_01" + }, + "Value": true + }, + { + "Key": { + "id": "Ceiling Dropper (2)", + "sceneName": "Waterways_01" + }, + "Value": true + }, + { + "Key": { + "id": "Ceiling Dropper (3)", + "sceneName": "Waterways_01" + }, + "Value": true + }, + { + "Key": { + "id": "Ceiling Dropper", + "sceneName": "Waterways_01" + }, + "Value": true + }, + { + "Key": { + "id": "Ceiling Dropper (4)", + "sceneName": "Waterways_01" + }, + "Value": true + }, + { + "Key": { + "id": "Ceiling Dropper (1)", + "sceneName": "Waterways_01" + }, + "Value": true + }, + { + "Key": { + "id": "Quake Floor (1)", + "sceneName": "Waterways_04" + }, + "Value": true + }, + { + "Key": { + "id": "Quake Floor", + "sceneName": "Waterways_04" + }, + "Value": true + }, + { + "Key": { + "id": "Breakable Wall Waterways", + "sceneName": "Waterways_04" + }, + "Value": true + }, + { + "Key": { + "id": "Flukeman (1)", + "sceneName": "Waterways_04" + }, + "Value": true + }, + { + "Key": { + "id": "Secret Mask", + "sceneName": "Waterways_04" + }, + "Value": true + }, + { + "Key": { + "id": "Egg Sac", + "sceneName": "Waterways_04" + }, + "Value": true + }, + { + "Key": { + "id": "Grub Bottle", + "sceneName": "Waterways_04" + }, + "Value": true + }, + { + "Key": { + "id": "Flukeman", + "sceneName": "Waterways_04" + }, + "Value": true + }, + { + "Key": { + "id": "Ceiling Dropper", + "sceneName": "Waterways_04" + }, + "Value": true + }, + { + "Key": { + "id": "Flukeman", + "sceneName": "Waterways_02" + }, + "Value": true + }, + { + "Key": { + "id": "Remasker (1)", + "sceneName": "Waterways_02" + }, + "Value": true + }, + { + "Key": { + "id": "Flukeman (2)", + "sceneName": "Waterways_02" + }, + "Value": true + }, + { + "Key": { + "id": "Egg Sac", + "sceneName": "Waterways_02" + }, + "Value": true + }, + { + "Key": { + "id": "Quake Floor", + "sceneName": "Waterways_02" + }, + "Value": true + }, + { + "Key": { + "id": "One Way Wall", + "sceneName": "Waterways_02" + }, + "Value": true + }, + { + "Key": { + "id": "Quake Floor (1)", + "sceneName": "Waterways_02" + }, + "Value": true + }, + { + "Key": { + "id": "Secret Mask (1)", + "sceneName": "Waterways_02" + }, + "Value": true + }, + { + "Key": { + "id": "Remasker", + "sceneName": "Waterways_02" + }, + "Value": true + }, + { + "Key": { + "id": "Flukeman (3)", + "sceneName": "Waterways_02" + }, + "Value": true + }, + { + "Key": { + "id": "Secret Sound Region", + "sceneName": "Waterways_02" + }, + "Value": true + }, + { + "Key": { + "id": "Secret Mask", + "sceneName": "Waterways_02" + }, + "Value": true + }, + { + "Key": { + "id": "Flukeman (1)", + "sceneName": "Waterways_02" + }, + "Value": true + }, + { + "Key": { + "id": "Ceiling Dropper", + "sceneName": "Waterways_02" + }, + "Value": true + }, + { + "Key": { + "id": "Quake Floor", + "sceneName": "Waterways_05" + }, + "Value": true + }, + { + "Key": { + "id": "One Way Wall", + "sceneName": "Waterways_05" + }, + "Value": true + }, + { + "Key": { + "id": "Waterways_Crank_Lever", + "sceneName": "Waterways_05" + }, + "Value": true + }, + { + "Key": { + "id": "Shiny Item", + "sceneName": "Waterways_05" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (32)", + "sceneName": "Abyss_01" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (34)", + "sceneName": "Abyss_01" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (17)", + "sceneName": "Abyss_01" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (5)", + "sceneName": "Abyss_01" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (25)", + "sceneName": "Abyss_01" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (23)", + "sceneName": "Abyss_01" + }, + "Value": false + }, + { + "Key": { + "id": "Inverse Remasker", + "sceneName": "Abyss_01" + }, + "Value": true + }, + { + "Key": { + "id": "Dream Plant Orb (11)", + "sceneName": "Abyss_01" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (10)", + "sceneName": "Abyss_01" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (9)", + "sceneName": "Abyss_01" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (7)", + "sceneName": "Abyss_01" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (27)", + "sceneName": "Abyss_01" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb", + "sceneName": "Abyss_01" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (21)", + "sceneName": "Abyss_01" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (18)", + "sceneName": "Abyss_01" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (31)", + "sceneName": "Abyss_01" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (8)", + "sceneName": "Abyss_01" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (1)", + "sceneName": "Abyss_01" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (33)", + "sceneName": "Abyss_01" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (20)", + "sceneName": "Abyss_01" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (22)", + "sceneName": "Abyss_01" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (16)", + "sceneName": "Abyss_01" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (3)", + "sceneName": "Abyss_01" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (24)", + "sceneName": "Abyss_01" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (12)", + "sceneName": "Abyss_01" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant", + "sceneName": "Abyss_01" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (26)", + "sceneName": "Abyss_01" + }, + "Value": false + }, + { + "Key": { + "id": "Secret Sound Region", + "sceneName": "Abyss_01" + }, + "Value": true + }, + { + "Key": { + "id": "Dream Plant Orb (6)", + "sceneName": "Abyss_01" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (30)", + "sceneName": "Abyss_01" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (13)", + "sceneName": "Abyss_01" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (19)", + "sceneName": "Abyss_01" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (4)", + "sceneName": "Abyss_01" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (28)", + "sceneName": "Abyss_01" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (29)", + "sceneName": "Abyss_01" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (2)", + "sceneName": "Abyss_01" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (14)", + "sceneName": "Abyss_01" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (15)", + "sceneName": "Abyss_01" + }, + "Value": false + }, + { + "Key": { + "id": "Ruins Flying Sentry", + "sceneName": "Abyss_01" + }, + "Value": true + }, + { + "Key": { + "id": "Ruins Flying Sentry (1)", + "sceneName": "Abyss_01" + }, + "Value": true + }, + { + "Key": { + "id": "Egg Sac", + "sceneName": "Waterways_07" + }, + "Value": true + }, + { + "Key": { + "id": "One Way Wall", + "sceneName": "Waterways_07" + }, + "Value": true + }, + { + "Key": { + "id": "Inverse Remasker", + "sceneName": "Waterways_07" + }, + "Value": true + }, + { + "Key": { + "id": "Secret Mask", + "sceneName": "Waterways_07" + }, + "Value": true + }, + { + "Key": { + "id": "Ruins Sentry 1", + "sceneName": "Waterways_07" + }, + "Value": true + }, + { + "Key": { + "id": "Ruins Sentry 1 (1)", + "sceneName": "Waterways_07" + }, + "Value": true + }, + { + "Key": { + "id": "Ceiling Dropper", + "sceneName": "Waterways_07" + }, + "Value": true + }, + { + "Key": { + "id": "Shiny Item(Clone)", + "sceneName": "Waterways_07" + }, + "Value": false + }, + { + "Key": { + "id": "Grub Bottle", + "sceneName": "Waterways_13" + }, + "Value": true + }, + { + "Key": { + "id": "Shiny Item Acid", + "sceneName": "Waterways_13" + }, + "Value": false + }, + { + "Key": { + "id": "Battle Scene", + "sceneName": "Waterways_13" + }, + "Value": true + }, + { + "Key": { + "id": "Ruins Flying Sentry", + "sceneName": "Waterways_13" + }, + "Value": true + }, + { + "Key": { + "id": "Ruins Flying Sentry (1)", + "sceneName": "Waterways_13" + }, + "Value": true + }, + { + "Key": { + "id": "Ceiling Dropper", + "sceneName": "Waterways_13" + }, + "Value": true + }, + { + "Key": { + "id": "Ceiling Dropper (1)", + "sceneName": "Waterways_13" + }, + "Value": true + }, + { + "Key": { + "id": "Ruins Sentry Fat", + "sceneName": "Waterways_07" + }, + "Value": true + }, + { + "Key": { + "id": "Royal Zombie 1", + "sceneName": "Ruins2_04" + }, + "Value": true + }, + { + "Key": { + "id": "Royal Zombie Coward", + "sceneName": "Ruins2_04" + }, + "Value": true + }, + { + "Key": { + "id": "Royal Zombie Coward (1)", + "sceneName": "Ruins2_04" + }, + "Value": true + }, + { + "Key": { + "id": "Great Shield Zombie (2)", + "sceneName": "Ruins2_04" + }, + "Value": true + }, + { + "Key": { + "id": "Great Shield Zombie", + "sceneName": "Ruins2_04" + }, + "Value": true + }, + { + "Key": { + "id": "Great Shield Zombie (1)", + "sceneName": "Ruins2_04" + }, + "Value": true + }, + { + "Key": { + "id": "Great Shield Zombie (3)", + "sceneName": "Ruins2_04" + }, + "Value": true + }, + { + "Key": { + "id": "Royal Zombie 1 (1)", + "sceneName": "Ruins2_04" + }, + "Value": true + }, + { + "Key": { + "id": "Royal Zombie Fat", + "sceneName": "Ruins2_04" + }, + "Value": true + }, + { + "Key": { + "id": "Royal Zombie Fat (1)", + "sceneName": "Ruins2_04" + }, + "Value": true + }, + { + "Key": { + "id": "remask", + "sceneName": "Ruins2_06" + }, + "Value": true + }, + { + "Key": { + "id": "Secret Mask", + "sceneName": "Ruins2_06" + }, + "Value": true + }, + { + "Key": { + "id": "Ruins Flying Sentry Javelin", + "sceneName": "Ruins2_06" + }, + "Value": true + }, + { + "Key": { + "id": "Royal Zombie 1 (1)", + "sceneName": "Ruins2_06" + }, + "Value": true + }, + { + "Key": { + "id": "Zombie Hornhead", + "sceneName": "Ruins2_06" + }, + "Value": true + }, + { + "Key": { + "id": "Royal Zombie Fat", + "sceneName": "Ruins2_06" + }, + "Value": true + }, + { + "Key": { + "id": "Shiny Item", + "sceneName": "Ruins2_08" + }, + "Value": false + }, + { + "Key": { + "id": "Shiny Item", + "sceneName": "Fungus3_26" + }, + "Value": false + }, + { + "Key": { + "id": "Secret Mask", + "sceneName": "Fungus3_26" + }, + "Value": true + }, + { + "Key": { + "id": "Jellyfish (2)", + "sceneName": "Fungus3_26" + }, + "Value": true + }, + { + "Key": { + "id": "Jellyfish (4)", + "sceneName": "Fungus3_26" + }, + "Value": true + }, + { + "Key": { + "id": "Jellyfish", + "sceneName": "Fungus3_26" + }, + "Value": true + }, + { + "Key": { + "id": "Jellyfish (3)", + "sceneName": "Fungus3_26" + }, + "Value": true + }, + { + "Key": { + "id": "Jellyfish (1)", + "sceneName": "Fungus3_26" + }, + "Value": true + }, + { + "Key": { + "id": "Jellyfish (2)", + "sceneName": "Fungus3_25b" + }, + "Value": true + }, + { + "Key": { + "id": "Jellyfish (1)", + "sceneName": "Fungus3_25b" + }, + "Value": true + }, + { + "Key": { + "id": "Jellyfish", + "sceneName": "Fungus3_25b" + }, + "Value": true + }, + { + "Key": { + "id": "Jellyfish (4)", + "sceneName": "Fungus3_25" + }, + "Value": true + }, + { + "Key": { + "id": "Jellyfish", + "sceneName": "Fungus3_25" + }, + "Value": true + }, + { + "Key": { + "id": "Jellyfish (1)", + "sceneName": "Fungus3_25" + }, + "Value": true + }, + { + "Key": { + "id": "Jellyfish (5)", + "sceneName": "Fungus3_25" + }, + "Value": true + }, + { + "Key": { + "id": "Jellyfish (3)", + "sceneName": "Fungus3_25" + }, + "Value": true + }, + { + "Key": { + "id": "Jellyfish (2)", + "sceneName": "Fungus3_25" + }, + "Value": true + }, + { + "Key": { + "id": "Jellyfish (2)", + "sceneName": "Fungus3_27" + }, + "Value": true + }, + { + "Key": { + "id": "Jellyfish (1)", + "sceneName": "Fungus3_27" + }, + "Value": true + }, + { + "Key": { + "id": "Jellyfish", + "sceneName": "Fungus3_27" + }, + "Value": true + }, + { + "Key": { + "id": "Jellyfish (3)", + "sceneName": "Fungus3_27" + }, + "Value": true + }, + { + "Key": { + "id": "One Way Wall", + "sceneName": "Fungus3_47" + }, + "Value": true + }, + { + "Key": { + "id": "Secret Mask", + "sceneName": "Fungus3_47" + }, + "Value": true + }, + { + "Key": { + "id": "Grub Bottle", + "sceneName": "Fungus3_47" + }, + "Value": true + }, + { + "Key": { + "id": "Jellyfish (3)", + "sceneName": "Fungus3_archive_02" + }, + "Value": true + }, + { + "Key": { + "id": "Jellyfish (5)", + "sceneName": "Fungus3_archive_02" + }, + "Value": true + }, + { + "Key": { + "id": "Jellyfish (6)", + "sceneName": "Fungus3_archive_02" + }, + "Value": true + }, + { + "Key": { + "id": "Jellyfish (2)", + "sceneName": "Fungus3_archive_02" + }, + "Value": true + }, + { + "Key": { + "id": "Jellyfish (1)", + "sceneName": "Fungus3_archive_02" + }, + "Value": true + }, + { + "Key": { + "id": "Jellyfish", + "sceneName": "Fungus3_archive_02" + }, + "Value": true + }, + { + "Key": { + "id": "Jellyfish (4)", + "sceneName": "Fungus3_archive_02" + }, + "Value": true + }, + { + "Key": { + "id": "Collapser Small", + "sceneName": "Fungus3_28" + }, + "Value": true + }, + { + "Key": { + "id": "Jellyfish (2)", + "sceneName": "Fungus3_28" + }, + "Value": true + }, + { + "Key": { + "id": "Jellyfish (5)", + "sceneName": "Fungus3_28" + }, + "Value": true + }, + { + "Key": { + "id": "Jellyfish (4)", + "sceneName": "Fungus3_28" + }, + "Value": true + }, + { + "Key": { + "id": "Jellyfish (3)", + "sceneName": "Fungus3_28" + }, + "Value": true + }, + { + "Key": { + "id": "Jellyfish (1)", + "sceneName": "Fungus3_28" + }, + "Value": true + }, + { + "Key": { + "id": "Shiny Item", + "sceneName": "Fungus3_28" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (12)", + "sceneName": "Fungus2_33" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (18)", + "sceneName": "Fungus2_33" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (3)", + "sceneName": "Fungus2_33" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (11)", + "sceneName": "Fungus2_33" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (6)", + "sceneName": "Fungus2_33" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (16)", + "sceneName": "Fungus2_33" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (4)", + "sceneName": "Fungus2_33" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (15)", + "sceneName": "Fungus2_33" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (14)", + "sceneName": "Fungus2_33" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb", + "sceneName": "Fungus2_33" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (1)", + "sceneName": "Fungus2_33" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (19)", + "sceneName": "Fungus2_33" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (2)", + "sceneName": "Fungus2_33" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (8)", + "sceneName": "Fungus2_33" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (13)", + "sceneName": "Fungus2_33" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (7)", + "sceneName": "Fungus2_33" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (5)", + "sceneName": "Fungus2_33" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant", + "sceneName": "Fungus2_33" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (9)", + "sceneName": "Fungus2_33" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (10)", + "sceneName": "Fungus2_33" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (17)", + "sceneName": "Fungus2_33" + }, + "Value": false + }, + { + "Key": { + "id": "Fungus Flyer (1)", + "sceneName": "Fungus2_33" + }, + "Value": true + }, + { + "Key": { + "id": "Fungus Flyer", + "sceneName": "Fungus2_33" + }, + "Value": true + }, + { + "Key": { + "id": "Remasker", + "sceneName": "Crossroads_52" + }, + "Value": true + }, + { + "Key": { + "id": "Remasker (1)", + "sceneName": "Crossroads_52" + }, + "Value": true + }, + { + "Key": { + "id": "Quake Floor", + "sceneName": "Crossroads_52" + }, + "Value": true + }, + { + "Key": { + "id": "Bursting Zombie", + "sceneName": "Crossroads_08" + }, + "Value": true + }, + { + "Key": { + "id": "Spitting Zombie", + "sceneName": "Crossroads_08" + }, + "Value": true + }, + { + "Key": { + "id": "Bursting Bouncer (3)", + "sceneName": "Crossroads_07" + }, + "Value": true + }, + { + "Key": { + "id": "Bursting Bouncer (5)", + "sceneName": "Crossroads_07" + }, + "Value": true + }, + { + "Key": { + "id": "Bursting Bouncer", + "sceneName": "Crossroads_07" + }, + "Value": true + }, + { + "Key": { + "id": "Bursting Bouncer (2)", + "sceneName": "Crossroads_07" + }, + "Value": true + }, + { + "Key": { + "id": "Bursting Bouncer (4)", + "sceneName": "Crossroads_07" + }, + "Value": true + }, + { + "Key": { + "id": "Bursting Bouncer (1)", + "sceneName": "Crossroads_07" + }, + "Value": true + }, + { + "Key": { + "id": "Angry Buzzer", + "sceneName": "Crossroads_01" + }, + "Value": true + }, + { + "Key": { + "id": "Bursting Zombie", + "sceneName": "Crossroads_01" + }, + "Value": true + }, + { + "Key": { + "id": "Royal Zombie Fat", + "sceneName": "Ruins_House_02" + }, + "Value": true + }, + { + "Key": { + "id": "Royal Zombie Coward", + "sceneName": "Ruins_House_02" + }, + "Value": true + }, + { + "Key": { + "id": "Royal Zombie 1 (1)", + "sceneName": "Ruins_House_02" + }, + "Value": true + }, + { + "Key": { + "id": "Royal Zombie Coward (1)", + "sceneName": "Ruins_House_02" + }, + "Value": true + }, + { + "Key": { + "id": "Gorgeous Husk", + "sceneName": "Ruins_House_02" + }, + "Value": true + }, + { + "Key": { + "id": "Inverse Remasker", + "sceneName": "Ruins_House_02" + }, + "Value": true + }, + { + "Key": { + "id": "Inverse Remasker (2)", + "sceneName": "Ruins_House_02" + }, + "Value": true + }, + { + "Key": { + "id": "Inverse Remasker (1)", + "sceneName": "Ruins_House_02" + }, + "Value": true + }, + { + "Key": { + "id": "Secret Sound Region", + "sceneName": "Ruins_House_02" + }, + "Value": true + }, + { + "Key": { + "id": "Quake Floor Glass", + "sceneName": "Ruins2_01_b" + }, + "Value": true + }, + { + "Key": { + "id": "Battle Scene", + "sceneName": "Ruins2_01_b" + }, + "Value": true + }, + { + "Key": { + "id": "Royal Zombie Coward", + "sceneName": "Ruins2_01_b" + }, + "Value": true + }, + { + "Key": { + "id": "Royal Zombie 1", + "sceneName": "Ruins2_01_b" + }, + "Value": true + }, + { + "Key": { + "id": "Royal Zombie Fat (1)", + "sceneName": "Ruins2_01_b" + }, + "Value": true + }, + { + "Key": { + "id": "Royal Zombie Fat", + "sceneName": "Ruins2_01_b" + }, + "Value": true + }, + { + "Key": { + "id": "Royal Zombie Coward (2)", + "sceneName": "Ruins2_01" + }, + "Value": true + }, + { + "Key": { + "id": "Royal Zombie 1 (4)", + "sceneName": "Ruins2_01" + }, + "Value": true + }, + { + "Key": { + "id": "Ruins Flying Sentry", + "sceneName": "Ruins2_01" + }, + "Value": true + }, + { + "Key": { + "id": "Remasker", + "sceneName": "Ruins2_01" + }, + "Value": true + }, + { + "Key": { + "id": "Ruins Lever (1)", + "sceneName": "Ruins2_01" + }, + "Value": true + }, + { + "Key": { + "id": "Secret Mask", + "sceneName": "Ruins2_01" + }, + "Value": true + }, + { + "Key": { + "id": "Ruins Lever", + "sceneName": "Ruins2_01" + }, + "Value": true + }, + { + "Key": { + "id": "Battle Scene", + "sceneName": "Ruins2_01" + }, + "Value": true + }, + { + "Key": { + "id": "Ruins Flying Sentry Javelin", + "sceneName": "Ruins2_01" + }, + "Value": true + }, + { + "Key": { + "id": "Ruins Flying Sentry Javelin (1)", + "sceneName": "Ruins2_01" + }, + "Value": true + }, + { + "Key": { + "id": "Royal Zombie Coward (1)", + "sceneName": "Ruins2_01" + }, + "Value": true + }, + { + "Key": { + "id": "Great Shield Zombie", + "sceneName": "Ruins2_01" + }, + "Value": true + }, + { + "Key": { + "id": "Royal Zombie Coward (3)", + "sceneName": "Ruins2_01" + }, + "Value": true + }, + { + "Key": { + "id": "Royal Zombie 1 (1)", + "sceneName": "Ruins2_01" + }, + "Value": true + }, + { + "Key": { + "id": "Ruins Sentry 1", + "sceneName": "Ruins2_01" + }, + "Value": true + }, + { + "Key": { + "id": "Royal Zombie Fat (3)", + "sceneName": "Ruins2_01" + }, + "Value": true + }, + { + "Key": { + "id": "Ruins Sentry Fat", + "sceneName": "Ruins2_01" + }, + "Value": true + }, + { + "Key": { + "id": "Royal Zombie Fat (2)", + "sceneName": "Ruins2_01" + }, + "Value": true + }, + { + "Key": { + "id": "Ruins Lever", + "sceneName": "Ruins1_18" + }, + "Value": true + }, + { + "Key": { + "id": "Grub Scene", + "sceneName": "Ruins2_03b" + }, + "Value": true + }, + { + "Key": { + "id": "Royal Zombie Coward", + "sceneName": "Ruins2_03b" + }, + "Value": true + }, + { + "Key": { + "id": "Great Shield Zombie bottom", + "sceneName": "Ruins2_03b" + }, + "Value": true + }, + { + "Key": { + "id": "Ruins Flying Sentry (1)", + "sceneName": "Ruins2_03b" + }, + "Value": true + }, + { + "Key": { + "id": "Ruins Flying Sentry", + "sceneName": "Ruins2_03b" + }, + "Value": true + }, + { + "Key": { + "id": "Royal Zombie 1", + "sceneName": "Ruins2_03b" + }, + "Value": true + }, + { + "Key": { + "id": "Royal Zombie 1 (1)", + "sceneName": "Ruins2_03b" + }, + "Value": true + }, + { + "Key": { + "id": "Royal Zombie Fat", + "sceneName": "Ruins2_03b" + }, + "Value": true + }, + { + "Key": { + "id": "Shiny Item", + "sceneName": "Ruins2_03" + }, + "Value": false + }, + { + "Key": { + "id": "boss_floor_remasker", + "sceneName": "Ruins2_03" + }, + "Value": true + }, + { + "Key": { + "id": "Chest", + "sceneName": "Ruins2_03" + }, + "Value": true + }, + { + "Key": { + "id": "Breakable Wall", + "sceneName": "Ruins2_03" + }, + "Value": true + }, + { + "Key": { + "id": "Grub Scene", + "sceneName": "Ruins2_03" + }, + "Value": true + }, + { + "Key": { + "id": "Grub Bottle", + "sceneName": "Ruins2_03" + }, + "Value": true + }, + { + "Key": { + "id": "Battle Control", + "sceneName": "Ruins2_03" + }, + "Value": true + }, + { + "Key": { + "id": "Great Shield Zombie", + "sceneName": "Ruins2_03" + }, + "Value": true + }, + { + "Key": { + "id": "Ruins Flying Sentry Javelin", + "sceneName": "Ruins2_03" + }, + "Value": true + }, + { + "Key": { + "id": "Secret sound", + "sceneName": "Ruins2_Watcher_Room" + }, + "Value": true + }, + { + "Key": { + "id": "Royal Zombie Coward", + "sceneName": "Ruins2_Watcher_Room" + }, + "Value": true + }, + { + "Key": { + "id": "Inverse Remasker", + "sceneName": "Ruins2_Watcher_Room" + }, + "Value": true + }, + { + "Key": { + "id": "Secret Mask", + "sceneName": "Ruins2_Watcher_Room" + }, + "Value": true + }, + { + "Key": { + "id": "Angry Buzzer (1)", + "sceneName": "Crossroads_03" + }, + "Value": true + }, + { + "Key": { + "id": "Angry Buzzer (2)", + "sceneName": "Crossroads_03" + }, + "Value": true + }, + { + "Key": { + "id": "Angry Buzzer", + "sceneName": "Crossroads_03" + }, + "Value": true + }, + { + "Key": { + "id": "Spitting Zombie (1)", + "sceneName": "Crossroads_15" + }, + "Value": true + }, + { + "Key": { + "id": "Spitting Zombie", + "sceneName": "Crossroads_15" + }, + "Value": true + }, + { + "Key": { + "id": "Angry Buzzer", + "sceneName": "Crossroads_04" + }, + "Value": true + }, + { + "Key": { + "id": "Bursting Zombie (1)", + "sceneName": "Crossroads_04" + }, + "Value": true + }, + { + "Key": { + "id": "Spitting Zombie (1)", + "sceneName": "Crossroads_04" + }, + "Value": true + }, + { + "Key": { + "id": "Bursting Zombie", + "sceneName": "Crossroads_04" + }, + "Value": true + }, + { + "Key": { + "id": "Spitting Zombie", + "sceneName": "Crossroads_04" + }, + "Value": true + }, + { + "Key": { + "id": "Zombie Fungus A", + "sceneName": "Fungus2_18" + }, + "Value": true + }, + { + "Key": { + "id": "Mantis Lever", + "sceneName": "Fungus2_18" + }, + "Value": true + }, + { + "Key": { + "id": "Grub Bottle", + "sceneName": "Fungus2_18" + }, + "Value": true + }, + { + "Key": { + "id": "Fungus Flyer", + "sceneName": "Fungus2_18" + }, + "Value": true + }, + { + "Key": { + "id": "Shiny", + "sceneName": "Fungus2_18" + }, + "Value": true + }, + { + "Key": { + "id": "Grub Bottle", + "sceneName": "Fungus2_20" + }, + "Value": true + }, + { + "Key": { + "id": "Breakable Wall Waterways", + "sceneName": "Fungus2_20" + }, + "Value": true + }, + { + "Key": { + "id": "Secret Mask (1)", + "sceneName": "Fungus2_20" + }, + "Value": true + }, + { + "Key": { + "id": "One Way Wall", + "sceneName": "Fungus2_20" + }, + "Value": true + }, + { + "Key": { + "id": "Secret Mask", + "sceneName": "Fungus2_20" + }, + "Value": true + }, + { + "Key": { + "id": "Shiny Item Stand", + "sceneName": "Fungus2_20" + }, + "Value": false + }, + { + "Key": { + "id": "Mushroom Roller (3)", + "sceneName": "Fungus2_28" + }, + "Value": true + }, + { + "Key": { + "id": "Mushroom Roller (1)", + "sceneName": "Fungus2_28" + }, + "Value": true + }, + { + "Key": { + "id": "Mushroom Roller", + "sceneName": "Fungus2_28" + }, + "Value": true + }, + { + "Key": { + "id": "Mushroom Roller (2)", + "sceneName": "Fungus2_28" + }, + "Value": true + }, + { + "Key": { + "id": "Mushroom Roller", + "sceneName": "Fungus2_23" + }, + "Value": true + }, + { + "Key": { + "id": "Mushroom Roller (1)", + "sceneName": "Fungus2_23" + }, + "Value": true + }, + { + "Key": { + "id": "Secret Mask", + "sceneName": "Fungus2_23" + }, + "Value": true + }, + { + "Key": { + "id": "Collapser Small", + "sceneName": "Fungus2_23" + }, + "Value": true + }, + { + "Key": { + "id": "Shiny Item Stand", + "sceneName": "Fungus2_23" + }, + "Value": false + }, + { + "Key": { + "id": "Mantis", + "sceneName": "Fungus2_31" + }, + "Value": true + }, + { + "Key": { + "id": "Mantis Chest (1)", + "sceneName": "Fungus2_31" + }, + "Value": true + }, + { + "Key": { + "id": "Mantis Chest (2)", + "sceneName": "Fungus2_31" + }, + "Value": true + }, + { + "Key": { + "id": "Remasker (2)", + "sceneName": "Fungus2_31" + }, + "Value": true + }, + { + "Key": { + "id": "Inverse Remasker", + "sceneName": "Fungus2_31" + }, + "Value": true + }, + { + "Key": { + "id": "Mantis Chest", + "sceneName": "Fungus2_31" + }, + "Value": true + }, + { + "Key": { + "id": "Remasker (1)", + "sceneName": "Fungus2_31" + }, + "Value": true + }, + { + "Key": { + "id": "Shiny Item", + "sceneName": "Fungus2_31" + }, + "Value": false + }, + { + "Key": { + "id": "Shiny Item Charm", + "sceneName": "Fungus2_31" + }, + "Value": false + }, + { + "Key": { + "id": "Heart Piece", + "sceneName": "Fungus2_25" + }, + "Value": false + }, + { + "Key": { + "id": "Collapser Small (1)", + "sceneName": "Fungus2_25" + }, + "Value": true + }, + { + "Key": { + "id": "Collapser Small", + "sceneName": "Fungus2_25" + }, + "Value": true + }, + { + "Key": { + "id": "Collapser Small (1)", + "sceneName": "Deepnest_16" + }, + "Value": true + }, + { + "Key": { + "id": "Collapser Small", + "sceneName": "Deepnest_16" + }, + "Value": true + }, + { + "Key": { + "id": "Secret Mask bot left", + "sceneName": "Deepnest_16" + }, + "Value": true + }, + { + "Key": { + "id": "Secret Mask (1)", + "sceneName": "Deepnest_16" + }, + "Value": true + }, + { + "Key": { + "id": "Secret Mask", + "sceneName": "Deepnest_16" + }, + "Value": true + }, + { + "Key": { + "id": "Secret Mask (3)", + "sceneName": "Deepnest_16" + }, + "Value": true + }, + { + "Key": { + "id": "Inverse Remasker", + "sceneName": "Deepnest_16" + }, + "Value": true + }, + { + "Key": { + "id": "Shiny Item", + "sceneName": "Deepnest_16" + }, + "Value": false + }, + { + "Key": { + "id": "Secret Mask", + "sceneName": "Deepnest_01b" + }, + "Value": true + }, + { + "Key": { + "id": "Inverse Remasker", + "sceneName": "Deepnest_01b" + }, + "Value": true + }, + { + "Key": { + "id": "Inverse Remasker (1)", + "sceneName": "Deepnest_01b" + }, + "Value": true + }, + { + "Key": { + "id": "One Way Wall", + "sceneName": "Deepnest_01b" + }, + "Value": true + }, + { + "Key": { + "id": "Collapser Small top", + "sceneName": "Deepnest_30" + }, + "Value": true + }, + { + "Key": { + "id": "Mimic Spider Fake4", + "sceneName": "Deepnest_30" + }, + "Value": true + }, + { + "Key": { + "id": "Collapser Small (3)", + "sceneName": "Deepnest_30" + }, + "Value": true + }, + { + "Key": { + "id": "Secret Mask (1)", + "sceneName": "Deepnest_30" + }, + "Value": true + }, + { + "Key": { + "id": "Collapser Small", + "sceneName": "Deepnest_30" + }, + "Value": true + }, + { + "Key": { + "id": "Collapser Small (2)", + "sceneName": "Deepnest_30" + }, + "Value": true + }, + { + "Key": { + "id": "Collapser Small (1)", + "sceneName": "Deepnest_30" + }, + "Value": true + }, + { + "Key": { + "id": "Secret Mask", + "sceneName": "Deepnest_30" + }, + "Value": true + }, + { + "Key": { + "id": "Secret Mask (3)", + "sceneName": "Deepnest_30" + }, + "Value": true + }, + { + "Key": { + "id": "Collapser Small", + "sceneName": "Deepnest_03" + }, + "Value": true + }, + { + "Key": { + "id": "Breakable Wall", + "sceneName": "Deepnest_03" + }, + "Value": true + }, + { + "Key": { + "id": "Secret Mask (1)", + "sceneName": "Deepnest_03" + }, + "Value": true + }, + { + "Key": { + "id": "Grub Bottle", + "sceneName": "Deepnest_03" + }, + "Value": true + }, + { + "Key": { + "id": "Zombie Hornhead Sp (4)", + "sceneName": "Deepnest_03" + }, + "Value": true + }, + { + "Key": { + "id": "Zombie Runner Sp (5)", + "sceneName": "Deepnest_03" + }, + "Value": true + }, + { + "Key": { + "id": "Zombie Hornhead Sp (3)", + "sceneName": "Deepnest_03" + }, + "Value": true + }, + { + "Key": { + "id": "Zombie Hornhead Sp (2)", + "sceneName": "Deepnest_03" + }, + "Value": true + }, + { + "Key": { + "id": "Zombie Hornhead Sp", + "sceneName": "Deepnest_03" + }, + "Value": true + }, + { + "Key": { + "id": "Zombie Runner Sp", + "sceneName": "Deepnest_03" + }, + "Value": true + }, + { + "Key": { + "id": "Zombie Runner Sp (2)", + "sceneName": "Deepnest_03" + }, + "Value": true + }, + { + "Key": { + "id": "Secret Mask", + "sceneName": "Deepnest_34" + }, + "Value": true + }, + { + "Key": { + "id": "Zombie Hornhead Sp", + "sceneName": "Deepnest_34" + }, + "Value": true + }, + { + "Key": { + "id": "Zombie Runner Sp", + "sceneName": "Deepnest_34" + }, + "Value": true + }, + { + "Key": { + "id": "Zombie Runner Sp (1)", + "sceneName": "Deepnest_34" + }, + "Value": true + }, + { + "Key": { + "id": "Slash Spider", + "sceneName": "Deepnest_34" + }, + "Value": true + }, + { + "Key": { + "id": "Breakable Wall", + "sceneName": "Deepnest_39" + }, + "Value": true + }, + { + "Key": { + "id": "Dream Plant", + "sceneName": "Deepnest_39" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (14)", + "sceneName": "Deepnest_39" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (42)", + "sceneName": "Deepnest_39" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (18)", + "sceneName": "Deepnest_39" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (36)", + "sceneName": "Deepnest_39" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb", + "sceneName": "Deepnest_39" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (27)", + "sceneName": "Deepnest_39" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (44)", + "sceneName": "Deepnest_39" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (12)", + "sceneName": "Deepnest_39" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (15)", + "sceneName": "Deepnest_39" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (13)", + "sceneName": "Deepnest_39" + }, + "Value": false + }, + { + "Key": { + "id": "Remasker", + "sceneName": "Deepnest_39" + }, + "Value": true + }, + { + "Key": { + "id": "Collapser Small (4)", + "sceneName": "Deepnest_39" + }, + "Value": true + }, + { + "Key": { + "id": "Dream Plant Orb (25)", + "sceneName": "Deepnest_39" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (23)", + "sceneName": "Deepnest_39" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (40)", + "sceneName": "Deepnest_39" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (20)", + "sceneName": "Deepnest_39" + }, + "Value": false + }, + { + "Key": { + "id": "Collapser Small (3)", + "sceneName": "Deepnest_39" + }, + "Value": true + }, + { + "Key": { + "id": "Dream Plant Orb (6)", + "sceneName": "Deepnest_39" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (24)", + "sceneName": "Deepnest_39" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (10)", + "sceneName": "Deepnest_39" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (31)", + "sceneName": "Deepnest_39" + }, + "Value": false + }, + { + "Key": { + "id": "Secret Mask (1)", + "sceneName": "Deepnest_39" + }, + "Value": true + }, + { + "Key": { + "id": "Dream Plant Orb (34)", + "sceneName": "Deepnest_39" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (9)", + "sceneName": "Deepnest_39" + }, + "Value": false + }, + { + "Key": { + "id": "Collapser Small (6)", + "sceneName": "Deepnest_39" + }, + "Value": true + }, + { + "Key": { + "id": "Dream Plant Orb (17)", + "sceneName": "Deepnest_39" + }, + "Value": false + }, + { + "Key": { + "id": "Collapser Small", + "sceneName": "Deepnest_39" + }, + "Value": true + }, + { + "Key": { + "id": "Secret Mask", + "sceneName": "Deepnest_39" + }, + "Value": true + }, + { + "Key": { + "id": "Dream Plant Orb (35)", + "sceneName": "Deepnest_39" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (43)", + "sceneName": "Deepnest_39" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (21)", + "sceneName": "Deepnest_39" + }, + "Value": false + }, + { + "Key": { + "id": "Collapser Small (2)", + "sceneName": "Deepnest_39" + }, + "Value": true + }, + { + "Key": { + "id": "Dream Plant Orb (28)", + "sceneName": "Deepnest_39" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (38)", + "sceneName": "Deepnest_39" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (3)", + "sceneName": "Deepnest_39" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (11)", + "sceneName": "Deepnest_39" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (29)", + "sceneName": "Deepnest_39" + }, + "Value": false + }, + { + "Key": { + "id": "One Way Wall", + "sceneName": "Deepnest_39" + }, + "Value": true + }, + { + "Key": { + "id": "Egg Sac", + "sceneName": "Deepnest_39" + }, + "Value": true + }, + { + "Key": { + "id": "Dream Plant Orb (8)", + "sceneName": "Deepnest_39" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (26)", + "sceneName": "Deepnest_39" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (32)", + "sceneName": "Deepnest_39" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (4)", + "sceneName": "Deepnest_39" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (1)", + "sceneName": "Deepnest_39" + }, + "Value": false + }, + { + "Key": { + "id": "Grub Bottle", + "sceneName": "Deepnest_39" + }, + "Value": true + }, + { + "Key": { + "id": "Dream Plant Orb (2)", + "sceneName": "Deepnest_39" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (33)", + "sceneName": "Deepnest_39" + }, + "Value": false + }, + { + "Key": { + "id": "One Way Wall (1)", + "sceneName": "Deepnest_39" + }, + "Value": true + }, + { + "Key": { + "id": "Collapser Small (5)", + "sceneName": "Deepnest_39" + }, + "Value": true + }, + { + "Key": { + "id": "Dream Plant Orb (39)", + "sceneName": "Deepnest_39" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (41)", + "sceneName": "Deepnest_39" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (22)", + "sceneName": "Deepnest_39" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (5)", + "sceneName": "Deepnest_39" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (19)", + "sceneName": "Deepnest_39" + }, + "Value": false + }, + { + "Key": { + "id": "Collapser Small (7)", + "sceneName": "Deepnest_39" + }, + "Value": true + }, + { + "Key": { + "id": "Collapser Small (1)", + "sceneName": "Deepnest_39" + }, + "Value": true + }, + { + "Key": { + "id": "Dream Plant Orb (16)", + "sceneName": "Deepnest_39" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (37)", + "sceneName": "Deepnest_39" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (30)", + "sceneName": "Deepnest_39" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (7)", + "sceneName": "Deepnest_39" + }, + "Value": false + }, + { + "Key": { + "id": "Slash Spider (4)", + "sceneName": "Deepnest_39" + }, + "Value": true + }, + { + "Key": { + "id": "Slash Spider (2)", + "sceneName": "Deepnest_39" + }, + "Value": true + }, + { + "Key": { + "id": "Slash Spider", + "sceneName": "Deepnest_39" + }, + "Value": true + }, + { + "Key": { + "id": "Slash Spider (3)", + "sceneName": "Deepnest_39" + }, + "Value": true + }, + { + "Key": { + "id": "Collapser Small", + "sceneName": "Deepnest_41" + }, + "Value": true + }, + { + "Key": { + "id": "One Way Wall (2)", + "sceneName": "Deepnest_41" + }, + "Value": true + }, + { + "Key": { + "id": "Secret Mask temp", + "sceneName": "Deepnest_41" + }, + "Value": true + }, + { + "Key": { + "id": "Collapser Small (1)", + "sceneName": "Deepnest_41" + }, + "Value": true + }, + { + "Key": { + "id": "Secret Mask (2)", + "sceneName": "Deepnest_41" + }, + "Value": true + }, + { + "Key": { + "id": "One Way Wall (1)", + "sceneName": "Deepnest_41" + }, + "Value": true + }, + { + "Key": { + "id": "Breakable Wall", + "sceneName": "Deepnest_41" + }, + "Value": true + }, + { + "Key": { + "id": "Collapser Small (2)", + "sceneName": "Deepnest_41" + }, + "Value": true + }, + { + "Key": { + "id": "Collapser Small (4)", + "sceneName": "Deepnest_41" + }, + "Value": true + }, + { + "Key": { + "id": "One Way Wall", + "sceneName": "Deepnest_41" + }, + "Value": true + }, + { + "Key": { + "id": "Slash Spider", + "sceneName": "Deepnest_41" + }, + "Value": true + }, + { + "Key": { + "id": "Collapser Small (6)", + "sceneName": "Deepnest_Spider_Town" + }, + "Value": true + }, + { + "Key": { + "id": "One Way Wall", + "sceneName": "Deepnest_Spider_Town" + }, + "Value": true + }, + { + "Key": { + "id": "Inverse Remasker (1)", + "sceneName": "Deepnest_Spider_Town" + }, + "Value": true + }, + { + "Key": { + "id": "one way permanent", + "sceneName": "Deepnest_Spider_Town" + }, + "Value": true + }, + { + "Key": { + "id": "Collapser Small (9)", + "sceneName": "Deepnest_Spider_Town" + }, + "Value": true + }, + { + "Key": { + "id": "Grub Bottle", + "sceneName": "Deepnest_Spider_Town" + }, + "Value": true + }, + { + "Key": { + "id": "Inverse Remasker", + "sceneName": "Deepnest_Spider_Town" + }, + "Value": true + }, + { + "Key": { + "id": "Collapser Small (8)", + "sceneName": "Deepnest_Spider_Town" + }, + "Value": true + }, + { + "Key": { + "id": "Remasker (1)", + "sceneName": "Deepnest_Spider_Town" + }, + "Value": true + }, + { + "Key": { + "id": "Inverse Remasker (2)", + "sceneName": "Deepnest_Spider_Town" + }, + "Value": true + }, + { + "Key": { + "id": "Collapser Small (10)", + "sceneName": "Deepnest_Spider_Town" + }, + "Value": true + }, + { + "Key": { + "id": "Secret Sound Region", + "sceneName": "Deepnest_Spider_Town" + }, + "Value": true + }, + { + "Key": { + "id": "Remasker (3)", + "sceneName": "Deepnest_Spider_Town" + }, + "Value": true + }, + { + "Key": { + "id": "Remasker", + "sceneName": "Deepnest_Spider_Town" + }, + "Value": true + }, + { + "Key": { + "id": "Egg Sac", + "sceneName": "Deepnest_Spider_Town" + }, + "Value": true + }, + { + "Key": { + "id": "Collapser Small (7)", + "sceneName": "Deepnest_Spider_Town" + }, + "Value": true + }, + { + "Key": { + "id": "Remasker bar", + "sceneName": "Deepnest_Spider_Town" + }, + "Value": true + }, + { + "Key": { + "id": "Collapser Small (11)", + "sceneName": "Deepnest_Spider_Town" + }, + "Value": true + }, + { + "Key": { + "id": "Collapser Small (12)", + "sceneName": "Deepnest_Spider_Town" + }, + "Value": true + }, + { + "Key": { + "id": "hack jump secret remask", + "sceneName": "Deepnest_Spider_Town" + }, + "Value": true + }, + { + "Key": { + "id": "Collapser Small (4)", + "sceneName": "Deepnest_Spider_Town" + }, + "Value": true + }, + { + "Key": { + "id": "Remasker (2)", + "sceneName": "Deepnest_Spider_Town" + }, + "Value": true + }, + { + "Key": { + "id": "Collapser Small (5)", + "sceneName": "Deepnest_Spider_Town" + }, + "Value": true + }, + { + "Key": { + "id": "Remasker (4)", + "sceneName": "Deepnest_Spider_Town" + }, + "Value": true + }, + { + "Key": { + "id": "Collapser Small (3)", + "sceneName": "Deepnest_Spider_Town" + }, + "Value": true + }, + { + "Key": { + "id": "remask_store_room", + "sceneName": "Deepnest_Spider_Town" + }, + "Value": true + }, + { + "Key": { + "id": "Shiny Item", + "sceneName": "Deepnest_Spider_Town" + }, + "Value": false + }, + { + "Key": { + "id": "Slash Spider (3)", + "sceneName": "Deepnest_Spider_Town" + }, + "Value": true + }, + { + "Key": { + "id": "Slash Spider", + "sceneName": "Deepnest_Spider_Town" + }, + "Value": true + }, + { + "Key": { + "id": "Slash Spider (4)", + "sceneName": "Deepnest_Spider_Town" + }, + "Value": true + }, + { + "Key": { + "id": "Slash Spider (1)", + "sceneName": "Deepnest_Spider_Town" + }, + "Value": true + }, + { + "Key": { + "id": "Slash Spider (2)", + "sceneName": "Deepnest_Spider_Town" + }, + "Value": true + }, + { + "Key": { + "id": "Shiny Item(Clone)", + "sceneName": "Deepnest_Spider_Town" + }, + "Value": false + }, + { + "Key": { + "id": "Heart Piece", + "sceneName": "Room_Bretta" + }, + "Value": false + }, + { + "Key": { + "id": "Remasker", + "sceneName": "Room_temple" + }, + "Value": true + }, + { + "Key": { + "id": "Remasker (1)", + "sceneName": "Cliffs_02" + }, + "Value": true + }, + { + "Key": { + "id": "Remasker", + "sceneName": "Cliffs_02" + }, + "Value": true + }, + { + "Key": { + "id": "One Way Wall", + "sceneName": "Cliffs_02" + }, + "Value": true + }, + { + "Key": { + "id": "Remasker (2)", + "sceneName": "Cliffs_02" + }, + "Value": true + }, + { + "Key": { + "id": "Secret Mask (2)", + "sceneName": "Cliffs_02" + }, + "Value": true + }, + { + "Key": { + "id": "Inverse Remasker", + "sceneName": "Cliffs_02" + }, + "Value": true + }, + { + "Key": { + "id": "Zombie Hornhead", + "sceneName": "Cliffs_02" + }, + "Value": true + }, + { + "Key": { + "id": "Zombie Barger", + "sceneName": "Cliffs_02" + }, + "Value": true + }, + { + "Key": { + "id": "Remasker (1)", + "sceneName": "Room_nailmaster" + }, + "Value": true + }, + { + "Key": { + "id": "Remasker (2)", + "sceneName": "Room_nailmaster" + }, + "Value": true + }, + { + "Key": { + "id": "Remasker", + "sceneName": "Room_nailmaster" + }, + "Value": true + }, + { + "Key": { + "id": "Dream Plant Orb (56)", + "sceneName": "Cliffs_01" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (19)", + "sceneName": "Cliffs_01" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (22)", + "sceneName": "Cliffs_01" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (58)", + "sceneName": "Cliffs_01" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (48)", + "sceneName": "Cliffs_01" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (17)", + "sceneName": "Cliffs_01" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (27)", + "sceneName": "Cliffs_01" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (57)", + "sceneName": "Cliffs_01" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (33)", + "sceneName": "Cliffs_01" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (15)", + "sceneName": "Cliffs_01" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (24)", + "sceneName": "Cliffs_01" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (59)", + "sceneName": "Cliffs_01" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (14)", + "sceneName": "Cliffs_01" + }, + "Value": false + }, + { + "Key": { + "id": "Breakable Wall", + "sceneName": "Cliffs_01" + }, + "Value": true + }, + { + "Key": { + "id": "Dream Plant Orb (31)", + "sceneName": "Cliffs_01" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (8)", + "sceneName": "Cliffs_01" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (9)", + "sceneName": "Cliffs_01" + }, + "Value": false + }, + { + "Key": { + "id": "One Way Wall", + "sceneName": "Cliffs_01" + }, + "Value": true + }, + { + "Key": { + "id": "Dream Plant Orb (66)", + "sceneName": "Cliffs_01" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (11)", + "sceneName": "Cliffs_01" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (38)", + "sceneName": "Cliffs_01" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (63)", + "sceneName": "Cliffs_01" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (28)", + "sceneName": "Cliffs_01" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (42)", + "sceneName": "Cliffs_01" + }, + "Value": false + }, + { + "Key": { + "id": "Shiny Item", + "sceneName": "Cliffs_01" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (30)", + "sceneName": "Cliffs_01" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (13)", + "sceneName": "Cliffs_01" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (36)", + "sceneName": "Cliffs_01" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (41)", + "sceneName": "Cliffs_01" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (67)", + "sceneName": "Cliffs_01" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (18)", + "sceneName": "Cliffs_01" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (55)", + "sceneName": "Cliffs_01" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (29)", + "sceneName": "Cliffs_01" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (26)", + "sceneName": "Cliffs_01" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (2)", + "sceneName": "Cliffs_01" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (4)", + "sceneName": "Cliffs_01" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (25)", + "sceneName": "Cliffs_01" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (47)", + "sceneName": "Cliffs_01" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (61)", + "sceneName": "Cliffs_01" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (52)", + "sceneName": "Cliffs_01" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (53)", + "sceneName": "Cliffs_01" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant", + "sceneName": "Cliffs_01" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (6)", + "sceneName": "Cliffs_01" + }, + "Value": false + }, + { + "Key": { + "id": "Breakable Wall grimm", + "sceneName": "Cliffs_01" + }, + "Value": true + }, + { + "Key": { + "id": "Dream Plant Orb (45)", + "sceneName": "Cliffs_01" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (16)", + "sceneName": "Cliffs_01" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (64)", + "sceneName": "Cliffs_01" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (39)", + "sceneName": "Cliffs_01" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (35)", + "sceneName": "Cliffs_01" + }, + "Value": false + }, + { + "Key": { + "id": "Shiny Item (1)", + "sceneName": "Cliffs_01" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (3)", + "sceneName": "Cliffs_01" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (50)", + "sceneName": "Cliffs_01" + }, + "Value": false + }, + { + "Key": { + "id": "Shiny", + "sceneName": "Cliffs_01" + }, + "Value": true + }, + { + "Key": { + "id": "Secret Sound Region", + "sceneName": "Cliffs_06" + }, + "Value": true + }, + { + "Key": { + "id": "Inverse Remasker", + "sceneName": "Cliffs_06" + }, + "Value": true + }, + { + "Key": { + "id": "Quake Floor", + "sceneName": "Cliffs_04" + }, + "Value": true + }, + { + "Key": { + "id": "Secret Mask", + "sceneName": "Cliffs_04" + }, + "Value": true + }, + { + "Key": { + "id": "Zombie Hornhead", + "sceneName": "Cliffs_04" + }, + "Value": true + }, + { + "Key": { + "id": "Zombie Leaper (1)", + "sceneName": "Cliffs_04" + }, + "Value": true + }, + { + "Key": { + "id": "Zombie Leaper (2)", + "sceneName": "Cliffs_04" + }, + "Value": true + }, + { + "Key": { + "id": "Zombie Leaper", + "sceneName": "Cliffs_04" + }, + "Value": true + }, + { + "Key": { + "id": "Zombie Barger", + "sceneName": "Cliffs_04" + }, + "Value": true + }, + { + "Key": { + "id": "Shiny Item Stand", + "sceneName": "Cliffs_05" + }, + "Value": false + }, + { + "Key": { + "id": "Ghost Activator", + "sceneName": "Cliffs_05" + }, + "Value": true + }, + { + "Key": { + "id": "Remasker", + "sceneName": "Cliffs_05" + }, + "Value": true + }, + { + "Key": { + "id": "Ghost NPC Joni", + "sceneName": "Cliffs_05" + }, + "Value": true + }, + { + "Key": { + "id": "Remasker", + "sceneName": "Grimm_Main_Tent" + }, + "Value": true + }, + { + "Key": { + "id": "Secret Mask", + "sceneName": "Grimm_Main_Tent" + }, + "Value": true + }, + { + "Key": { + "id": "Inverse Remasker", + "sceneName": "Fungus1_26" + }, + "Value": true + }, + { + "Key": { + "id": "Secret Mask", + "sceneName": "Fungus1_26" + }, + "Value": true + }, + { + "Key": { + "id": "Moss Knight", + "sceneName": "Fungus1_26" + }, + "Value": true + }, + { + "Key": { + "id": "secret sound", + "sceneName": "Fungus1_26" + }, + "Value": true + }, + { + "Key": { + "id": "One Way Wall (1)", + "sceneName": "Fungus1_Slug" + }, + "Value": true + }, + { + "Key": { + "id": "Secret Mask", + "sceneName": "Fungus1_Slug" + }, + "Value": true + }, + { + "Key": { + "id": "Secret Mask (1)", + "sceneName": "Fungus1_Slug" + }, + "Value": true + }, + { + "Key": { + "id": "One Way Wall", + "sceneName": "Fungus1_Slug" + }, + "Value": true + }, + { + "Key": { + "id": "Secret Mask (2)", + "sceneName": "Fungus1_Slug" + }, + "Value": true + }, + { + "Key": { + "id": "Shiny Item", + "sceneName": "Fungus1_Slug" + }, + "Value": false + }, + { + "Key": { + "id": "Vine Platform", + "sceneName": "Fungus1_09" + }, + "Value": true + }, + { + "Key": { + "id": "Shiny Item", + "sceneName": "Fungus1_15" + }, + "Value": false + }, + { + "Key": { + "id": "Shiny Item", + "sceneName": "Fungus1_10" + }, + "Value": false + }, + { + "Key": { + "id": "Moss Charger (1)", + "sceneName": "Fungus1_10" + }, + "Value": true + }, + { + "Key": { + "id": "Moss Charger 1 (1)", + "sceneName": "Fungus1_10" + }, + "Value": true + }, + { + "Key": { + "id": "Moss Charger 1 (2)", + "sceneName": "Fungus1_10" + }, + "Value": true + }, + { + "Key": { + "id": "Moss Charger", + "sceneName": "Fungus1_10" + }, + "Value": true + }, + { + "Key": { + "id": "Mossman_Shaker", + "sceneName": "Fungus1_05" + }, + "Value": true + }, + { + "Key": { + "id": "Mossman_Runner", + "sceneName": "Fungus1_05" + }, + "Value": true + }, + { + "Key": { + "id": "Mossman_Shaker (1)", + "sceneName": "Fungus1_05" + }, + "Value": true + }, + { + "Key": { + "id": "Vine Platform", + "sceneName": "Fungus1_14" + }, + "Value": true + }, + { + "Key": { + "id": "Shiny Item", + "sceneName": "Fungus1_14" + }, + "Value": false + }, + { + "Key": { + "id": "Plant Trap", + "sceneName": "Fungus1_19" + }, + "Value": true + }, + { + "Key": { + "id": "Secret Mask", + "sceneName": "Fungus1_19" + }, + "Value": true + }, + { + "Key": { + "id": "Mossman_Shaker", + "sceneName": "Fungus1_19" + }, + "Value": true + }, + { + "Key": { + "id": "Shiny Item", + "sceneName": "Fungus1_11" + }, + "Value": false + }, + { + "Key": { + "id": "Secret Mask", + "sceneName": "Fungus1_11" + }, + "Value": true + }, + { + "Key": { + "id": "Acid Walker", + "sceneName": "Fungus1_11" + }, + "Value": true + }, + { + "Key": { + "id": "Zombie Runner", + "sceneName": "Fungus1_34" + }, + "Value": true + }, + { + "Key": { + "id": "Zombie Hornhead", + "sceneName": "Fungus1_34" + }, + "Value": true + }, + { + "Key": { + "id": "Plant Trap", + "sceneName": "Fungus1_34" + }, + "Value": true + }, + { + "Key": { + "id": "Zombie Leaper", + "sceneName": "Fungus1_34" + }, + "Value": true + }, + { + "Key": { + "id": "Remasker", + "sceneName": "Fungus1_36" + }, + "Value": true + }, + { + "Key": { + "id": "Heart Piece", + "sceneName": "Fungus1_36" + }, + "Value": false + }, + { + "Key": { + "id": "Vine Platform (3)", + "sceneName": "Fungus1_07" + }, + "Value": true + }, + { + "Key": { + "id": "Remasker", + "sceneName": "Fungus1_07" + }, + "Value": true + }, + { + "Key": { + "id": "Vine Platform (2)", + "sceneName": "Fungus1_07" + }, + "Value": true + }, + { + "Key": { + "id": "Grub Bottle", + "sceneName": "Fungus1_07" + }, + "Value": true + }, + { + "Key": { + "id": "Vine Platform (1)", + "sceneName": "Fungus1_07" + }, + "Value": true + }, + { + "Key": { + "id": "Vine Platform", + "sceneName": "Fungus1_07" + }, + "Value": true + }, + { + "Key": { + "id": "Mossman_Runner", + "sceneName": "Fungus1_07" + }, + "Value": true + }, + { + "Key": { + "id": "Mossman_Runner (1)", + "sceneName": "Fungus1_07" + }, + "Value": true + }, + { + "Key": { + "id": "Secret Mask", + "sceneName": "Fungus1_08" + }, + "Value": true + }, + { + "Key": { + "id": "Break Floor 1", + "sceneName": "Fungus1_08" + }, + "Value": true + }, + { + "Key": { + "id": "Break Floor 1 (1)", + "sceneName": "Fungus1_08" + }, + "Value": true + }, + { + "Key": { + "id": "Shiny Item", + "sceneName": "Fungus1_08" + }, + "Value": false + }, + { + "Key": { + "id": "Vine Platform (1)", + "sceneName": "Fungus1_06" + }, + "Value": true + }, + { + "Key": { + "id": "Vine Platform (4)", + "sceneName": "Fungus1_06" + }, + "Value": true + }, + { + "Key": { + "id": "Vine Platform (3)", + "sceneName": "Fungus1_06" + }, + "Value": true + }, + { + "Key": { + "id": "Secret sounder", + "sceneName": "Fungus1_06" + }, + "Value": true + }, + { + "Key": { + "id": "Vine Platform (5)", + "sceneName": "Fungus1_06" + }, + "Value": true + }, + { + "Key": { + "id": "Vine Platform (2)", + "sceneName": "Fungus1_06" + }, + "Value": true + }, + { + "Key": { + "id": "Secret Mask", + "sceneName": "Fungus1_06" + }, + "Value": true + }, + { + "Key": { + "id": "Vine Platform", + "sceneName": "Fungus1_06" + }, + "Value": true + }, + { + "Key": { + "id": "Vine Platform (6)", + "sceneName": "Fungus1_06" + }, + "Value": true + }, + { + "Key": { + "id": "Grub Bottle", + "sceneName": "Fungus1_06" + }, + "Value": true + }, + { + "Key": { + "id": "Mossman_Shaker (1)", + "sceneName": "Fungus1_06" + }, + "Value": true + }, + { + "Key": { + "id": "Mossman_Shaker", + "sceneName": "Fungus1_06" + }, + "Value": true + }, + { + "Key": { + "id": "Shiny", + "sceneName": "Fungus1_06" + }, + "Value": true + }, + { + "Key": { + "id": "Angry Buzzer (2)", + "sceneName": "Crossroads_11_alt" + }, + "Value": true + }, + { + "Key": { + "id": "Angry Buzzer (1)", + "sceneName": "Crossroads_11_alt" + }, + "Value": true + }, + { + "Key": { + "id": "Shiny Item Rancid Egg", + "sceneName": "Crossroads_38" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (27)", + "sceneName": "RestingGrounds_08" + }, + "Value": false + }, + { + "Key": { + "id": "Ghost kcin", + "sceneName": "RestingGrounds_08" + }, + "Value": true + }, + { + "Key": { + "id": "Ghost atra", + "sceneName": "RestingGrounds_08" + }, + "Value": true + }, + { + "Key": { + "id": "Ghost wyatt", + "sceneName": "RestingGrounds_08" + }, + "Value": true + }, + { + "Key": { + "id": "Dream Plant Orb (2)", + "sceneName": "RestingGrounds_08" + }, + "Value": false + }, + { + "Key": { + "id": "Shiny Item", + "sceneName": "RestingGrounds_08" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (31)", + "sceneName": "RestingGrounds_08" + }, + "Value": false + }, + { + "Key": { + "id": "Ghost chagax", + "sceneName": "RestingGrounds_08" + }, + "Value": true + }, + { + "Key": { + "id": "Ghost boss", + "sceneName": "RestingGrounds_08" + }, + "Value": true + }, + { + "Key": { + "id": "Dream Plant Orb (4)", + "sceneName": "RestingGrounds_08" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (25)", + "sceneName": "RestingGrounds_08" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (13)", + "sceneName": "RestingGrounds_08" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (10)", + "sceneName": "RestingGrounds_08" + }, + "Value": false + }, + { + "Key": { + "id": "Secret Sound Region", + "sceneName": "RestingGrounds_08" + }, + "Value": true + }, + { + "Key": { + "id": "Dream Plant", + "sceneName": "RestingGrounds_08" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (15)", + "sceneName": "RestingGrounds_08" + }, + "Value": false + }, + { + "Key": { + "id": "Ghost hex", + "sceneName": "RestingGrounds_08" + }, + "Value": true + }, + { + "Key": { + "id": "Dream Plant Orb (1)", + "sceneName": "RestingGrounds_08" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (18)", + "sceneName": "RestingGrounds_08" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (11)", + "sceneName": "RestingGrounds_08" + }, + "Value": false + }, + { + "Key": { + "id": "Ghost garro", + "sceneName": "RestingGrounds_08" + }, + "Value": true + }, + { + "Key": { + "id": "Ghost perpetos", + "sceneName": "RestingGrounds_08" + }, + "Value": true + }, + { + "Key": { + "id": "Ghost molten", + "sceneName": "RestingGrounds_08" + }, + "Value": true + }, + { + "Key": { + "id": "Dream Plant Orb (5)", + "sceneName": "RestingGrounds_08" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (28)", + "sceneName": "RestingGrounds_08" + }, + "Value": false + }, + { + "Key": { + "id": "Ghost revek", + "sceneName": "RestingGrounds_08" + }, + "Value": true + }, + { + "Key": { + "id": "Dream Plant Orb (30)", + "sceneName": "RestingGrounds_08" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (23)", + "sceneName": "RestingGrounds_08" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb", + "sceneName": "RestingGrounds_08" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (8)", + "sceneName": "RestingGrounds_08" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (17)", + "sceneName": "RestingGrounds_08" + }, + "Value": false + }, + { + "Key": { + "id": "Ghost NPC 100 nail", + "sceneName": "RestingGrounds_08" + }, + "Value": true + }, + { + "Key": { + "id": "Dream Plant Orb (22)", + "sceneName": "RestingGrounds_08" + }, + "Value": false + }, + { + "Key": { + "id": "Ghost caspian", + "sceneName": "RestingGrounds_08" + }, + "Value": true + }, + { + "Key": { + "id": "Ghost waldie", + "sceneName": "RestingGrounds_08" + }, + "Value": true + }, + { + "Key": { + "id": "Dream Plant Orb (29)", + "sceneName": "RestingGrounds_08" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (7)", + "sceneName": "RestingGrounds_08" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (3)", + "sceneName": "RestingGrounds_08" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (24)", + "sceneName": "RestingGrounds_08" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (21)", + "sceneName": "RestingGrounds_08" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (16)", + "sceneName": "RestingGrounds_08" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (9)", + "sceneName": "RestingGrounds_08" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (19)", + "sceneName": "RestingGrounds_08" + }, + "Value": false + }, + { + "Key": { + "id": "Ghost milly", + "sceneName": "RestingGrounds_08" + }, + "Value": true + }, + { + "Key": { + "id": "Ghost magnus", + "sceneName": "RestingGrounds_08" + }, + "Value": true + }, + { + "Key": { + "id": "Ghost grohac", + "sceneName": "RestingGrounds_08" + }, + "Value": true + }, + { + "Key": { + "id": "Dream Plant Orb (6)", + "sceneName": "RestingGrounds_08" + }, + "Value": false + }, + { + "Key": { + "id": "Ghost wayner", + "sceneName": "RestingGrounds_08" + }, + "Value": true + }, + { + "Key": { + "id": "Dream Plant Orb (26)", + "sceneName": "RestingGrounds_08" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (12)", + "sceneName": "RestingGrounds_08" + }, + "Value": false + }, + { + "Key": { + "id": "karina", + "sceneName": "RestingGrounds_08" + }, + "Value": true + }, + { + "Key": { + "id": "Dream Plant Orb (33)", + "sceneName": "RestingGrounds_08" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (32)", + "sceneName": "RestingGrounds_08" + }, + "Value": false + }, + { + "Key": { + "id": "Ghost thistlewind", + "sceneName": "RestingGrounds_08" + }, + "Value": true + }, + { + "Key": { + "id": "Dream Plant Orb (20)", + "sceneName": "RestingGrounds_08" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (14)", + "sceneName": "RestingGrounds_08" + }, + "Value": false + }, + { + "Key": { + "id": "Shiny Item", + "sceneName": "Abyss_02" + }, + "Value": false + }, + { + "Key": { + "id": "Ruins Flying Sentry Javelin (1)", + "sceneName": "Abyss_02" + }, + "Value": true + }, + { + "Key": { + "id": "Ruins Flying Sentry Javelin", + "sceneName": "Abyss_02" + }, + "Value": true + }, + { + "Key": { + "id": "Ruins Sentry 1", + "sceneName": "Abyss_02" + }, + "Value": true + }, + { + "Key": { + "id": "Ruins Flying Sentry", + "sceneName": "Abyss_02" + }, + "Value": true + }, + { + "Key": { + "id": "Ruins Flying Sentry (1)", + "sceneName": "Abyss_02" + }, + "Value": true + }, + { + "Key": { + "id": "Zombie Hornhead", + "sceneName": "Abyss_02" + }, + "Value": true + }, + { + "Key": { + "id": "Zombie Barger (1)", + "sceneName": "Abyss_02" + }, + "Value": true + }, + { + "Key": { + "id": "Zombie Barger", + "sceneName": "Abyss_02" + }, + "Value": true + }, + { + "Key": { + "id": "wish_secret sound", + "sceneName": "Abyss_04" + }, + "Value": true + }, + { + "Key": { + "id": "wish inverse remask", + "sceneName": "Abyss_04" + }, + "Value": true + }, + { + "Key": { + "id": "wish_remask", + "sceneName": "Abyss_04" + }, + "Value": true + }, + { + "Key": { + "id": "Shiny", + "sceneName": "Abyss_04" + }, + "Value": true + }, + { + "Key": { + "id": "Toll Machine Bench", + "sceneName": "Abyss_18" + }, + "Value": true + }, + { + "Key": { + "id": "Grub Bottle", + "sceneName": "Abyss_19" + }, + "Value": true + }, + { + "Key": { + "id": "Secret Mask", + "sceneName": "Abyss_19" + }, + "Value": true + }, + { + "Key": { + "id": "One Way Wall", + "sceneName": "Abyss_19" + }, + "Value": true + }, + { + "Key": { + "id": "Camera Locks Boss", + "sceneName": "Abyss_19" + }, + "Value": true + }, + { + "Key": { + "id": "Mawlek Turret", + "sceneName": "Abyss_20" + }, + "Value": true + }, + { + "Key": { + "id": "Remasker", + "sceneName": "Abyss_20" + }, + "Value": true + }, + { + "Key": { + "id": "Shiny Item Stand", + "sceneName": "Abyss_20" + }, + "Value": false + }, + { + "Key": { + "id": "Secret Sound Region", + "sceneName": "Abyss_20" + }, + "Value": true + }, + { + "Key": { + "id": "Remasker (1)", + "sceneName": "Abyss_20" + }, + "Value": true + }, + { + "Key": { + "id": "Remasker (2)", + "sceneName": "Abyss_20" + }, + "Value": true + }, + { + "Key": { + "id": "Mawlek Turret Ceiling (1)", + "sceneName": "Abyss_20" + }, + "Value": true + }, + { + "Key": { + "id": "Mawlek Turret Ceiling", + "sceneName": "Abyss_20" + }, + "Value": true + }, + { + "Key": { + "id": "Mawlek Turret (1)", + "sceneName": "Abyss_20" + }, + "Value": true + }, + { + "Key": { + "id": "Mawlek Turret (2)", + "sceneName": "Abyss_20" + }, + "Value": true + }, + { + "Key": { + "id": "Mawlek Turret (3)", + "sceneName": "Abyss_20" + }, + "Value": true + }, + { + "Key": { + "id": "Breakable Wall", + "sceneName": "Abyss_05" + }, + "Value": true + }, + { + "Key": { + "id": "Battle Scene v2", + "sceneName": "Ruins1_31" + }, + "Value": true + }, + { + "Key": { + "id": "Crystal Flyer (1)", + "sceneName": "Mines_16" + }, + "Value": true + }, + { + "Key": { + "id": "Crystal Flyer", + "sceneName": "Mines_16" + }, + "Value": true + }, + { + "Key": { + "id": "Grub Mimic", + "sceneName": "Mines_16" + }, + "Value": true + }, + { + "Key": { + "id": "Grub Bottle", + "sceneName": "Mines_16" + }, + "Value": true + }, + { + "Key": { + "id": "Breakable Wall", + "sceneName": "Mines_16" + }, + "Value": true + }, + { + "Key": { + "id": "Grub Mimic Bottle", + "sceneName": "Mines_16" + }, + "Value": true + }, + { + "Key": { + "id": "Zombie Beam Miner Rematch", + "sceneName": "Mines_32" + }, + "Value": true + }, + { + "Key": { + "id": "Battle Scene", + "sceneName": "Mines_32" + }, + "Value": true + }, + { + "Key": { + "id": "Heart Piece", + "sceneName": "Mines_32" + }, + "Value": false + }, + { + "Key": { + "id": "Collapser Small", + "sceneName": "Mines_06" + }, + "Value": true + }, + { + "Key": { + "id": "Remasker", + "sceneName": "Mines_06" + }, + "Value": true + }, + { + "Key": { + "id": "Shiny Item Stand", + "sceneName": "Mines_36" + }, + "Value": false + }, + { + "Key": { + "id": "Grave Zombie (2)", + "sceneName": "RestingGrounds_10" + }, + "Value": true + }, + { + "Key": { + "id": "Ceiling Dropper", + "sceneName": "RestingGrounds_10" + }, + "Value": true + }, + { + "Key": { + "id": "Ceiling Dropper (1)", + "sceneName": "RestingGrounds_10" + }, + "Value": true + }, + { + "Key": { + "id": "Ceiling Dropper (3)", + "sceneName": "RestingGrounds_10" + }, + "Value": true + }, + { + "Key": { + "id": "Secret Mask", + "sceneName": "RestingGrounds_10" + }, + "Value": true + }, + { + "Key": { + "id": "Secret Sound Region", + "sceneName": "RestingGrounds_10" + }, + "Value": true + }, + { + "Key": { + "id": "Shiny Item Stand", + "sceneName": "RestingGrounds_10" + }, + "Value": false + }, + { + "Key": { + "id": "Breakable Wall (5)", + "sceneName": "RestingGrounds_10" + }, + "Value": true + }, + { + "Key": { + "id": "Collapser Small (6)", + "sceneName": "RestingGrounds_10" + }, + "Value": true + }, + { + "Key": { + "id": "Breakable Wall", + "sceneName": "RestingGrounds_10" + }, + "Value": true + }, + { + "Key": { + "id": "Chest", + "sceneName": "RestingGrounds_10" + }, + "Value": true + }, + { + "Key": { + "id": "One Way Wall", + "sceneName": "RestingGrounds_10" + }, + "Value": true + }, + { + "Key": { + "id": "Breakable Wall (6)", + "sceneName": "RestingGrounds_10" + }, + "Value": true + }, + { + "Key": { + "id": "Collapser Small (3)", + "sceneName": "RestingGrounds_10" + }, + "Value": true + }, + { + "Key": { + "id": "Breakable Wall (4)", + "sceneName": "RestingGrounds_10" + }, + "Value": true + }, + { + "Key": { + "id": "Collapser Small (5)", + "sceneName": "RestingGrounds_10" + }, + "Value": true + }, + { + "Key": { + "id": "Collapser Small", + "sceneName": "RestingGrounds_10" + }, + "Value": true + }, + { + "Key": { + "id": "Collapser Small (7)", + "sceneName": "RestingGrounds_10" + }, + "Value": true + }, + { + "Key": { + "id": "Breakable Wall (3)", + "sceneName": "RestingGrounds_10" + }, + "Value": true + }, + { + "Key": { + "id": "Collapser Small (4)", + "sceneName": "RestingGrounds_10" + }, + "Value": true + }, + { + "Key": { + "id": "Breakable Wall (8)", + "sceneName": "RestingGrounds_10" + }, + "Value": true + }, + { + "Key": { + "id": "Collapser Small (1)", + "sceneName": "RestingGrounds_10" + }, + "Value": true + }, + { + "Key": { + "id": "Collapser Small (2)", + "sceneName": "RestingGrounds_10" + }, + "Value": true + }, + { + "Key": { + "id": "Shiny Item", + "sceneName": "RestingGrounds_10" + }, + "Value": false + }, + { + "Key": { + "id": "Grub Bottle", + "sceneName": "RestingGrounds_10" + }, + "Value": true + }, + { + "Key": { + "id": "Secret Mask (1)", + "sceneName": "RestingGrounds_10" + }, + "Value": true + }, + { + "Key": { + "id": "Breakable Wall (2)", + "sceneName": "RestingGrounds_10" + }, + "Value": true + }, + { + "Key": { + "id": "Inverse Remasker", + "sceneName": "RestingGrounds_10" + }, + "Value": true + }, + { + "Key": { + "id": "Shiny Item (1)", + "sceneName": "RestingGrounds_10" + }, + "Value": false + }, + { + "Key": { + "id": "Breakable Wall (7)", + "sceneName": "RestingGrounds_10" + }, + "Value": true + }, + { + "Key": { + "id": "Grave Zombie (4)", + "sceneName": "RestingGrounds_10" + }, + "Value": true + }, + { + "Key": { + "id": "Grave Zombie", + "sceneName": "RestingGrounds_10" + }, + "Value": true + }, + { + "Key": { + "id": "Grave Zombie (1)", + "sceneName": "RestingGrounds_10" + }, + "Value": true + }, + { + "Key": { + "id": "Ceiling Dropper (2)", + "sceneName": "RestingGrounds_10" + }, + "Value": true + }, + { + "Key": { + "id": "Death Respawn Trigger", + "sceneName": "RestingGrounds_12" + }, + "Value": false + }, + { + "Key": { + "id": "Great Shield Zombie", + "sceneName": "RestingGrounds_06" + }, + "Value": true + }, + { + "Key": { + "id": "Ruins Flying Sentry", + "sceneName": "RestingGrounds_06" + }, + "Value": true + }, + { + "Key": { + "id": "Gate Switch", + "sceneName": "RestingGrounds_06" + }, + "Value": true + }, + { + "Key": { + "id": "Breakable Wall", + "sceneName": "RestingGrounds_06" + }, + "Value": true + }, + { + "Key": { + "id": "Resting Grounds Slide Floor", + "sceneName": "RestingGrounds_06" + }, + "Value": true + }, + { + "Key": { + "id": "Egg Sac", + "sceneName": "Crossroads_50" + }, + "Value": true + }, + { + "Key": { + "id": "Shiny Item(Clone)", + "sceneName": "Crossroads_50" + }, + "Value": false + }, + { + "Key": { + "id": "Angry Buzzer (1)", + "sceneName": "Crossroads_21" + }, + "Value": true + }, + { + "Key": { + "id": "Angry Buzzer", + "sceneName": "Crossroads_21" + }, + "Value": true + }, + { + "Key": { + "id": "Spitting Zombie", + "sceneName": "Crossroads_21" + }, + "Value": true + }, + { + "Key": { + "id": "Bursting Zombie", + "sceneName": "Crossroads_21" + }, + "Value": true + }, + { + "Key": { + "id": "Hatcher", + "sceneName": "Crossroads_22" + }, + "Value": true + }, + { + "Key": { + "id": "Shiny Item", + "sceneName": "Crossroads_22" + }, + "Value": false + }, + { + "Key": { + "id": "Battle Scene", + "sceneName": "Crossroads_22" + }, + "Value": true + }, + { + "Key": { + "id": "Battle Scene", + "sceneName": "Dream_01_False_Knight" + }, + "Value": true + }, + { + "Key": { + "id": "Ceiling Dropper (1)", + "sceneName": "Ruins2_07" + }, + "Value": true + }, + { + "Key": { + "id": "Grub Bottle", + "sceneName": "Ruins2_07" + }, + "Value": true + }, + { + "Key": { + "id": "Ceiling Dropper (2)", + "sceneName": "Ruins2_07" + }, + "Value": true + }, + { + "Key": { + "id": "Ceiling Dropper", + "sceneName": "Ruins2_07" + }, + "Value": true + }, + { + "Key": { + "id": "Plank Solid 2", + "sceneName": "Deepnest_East_03" + }, + "Value": true + }, + { + "Key": { + "id": "Inverse Remasker", + "sceneName": "Deepnest_East_03" + }, + "Value": true + }, + { + "Key": { + "id": "Plank Solid 1", + "sceneName": "Deepnest_East_03" + }, + "Value": true + }, + { + "Key": { + "id": "Plank Solid 2 (1)", + "sceneName": "Deepnest_East_03" + }, + "Value": true + }, + { + "Key": { + "id": "Secret Mask", + "sceneName": "Deepnest_East_03" + }, + "Value": true + }, + { + "Key": { + "id": "Shiny", + "sceneName": "Deepnest_East_03" + }, + "Value": true + }, + { + "Key": { + "id": "Blow Fly (5)", + "sceneName": "Deepnest_East_03" + }, + "Value": true + }, + { + "Key": { + "id": "Blow Fly (1)", + "sceneName": "Deepnest_East_03" + }, + "Value": true + }, + { + "Key": { + "id": "Blow Fly (4)", + "sceneName": "Deepnest_East_03" + }, + "Value": true + }, + { + "Key": { + "id": "Blow Fly (3)", + "sceneName": "Deepnest_East_03" + }, + "Value": true + }, + { + "Key": { + "id": "Blow Fly (2)", + "sceneName": "Deepnest_East_03" + }, + "Value": true + }, + { + "Key": { + "id": "Blow Fly", + "sceneName": "Deepnest_East_03" + }, + "Value": true + }, + { + "Key": { + "id": "One Way Wall", + "sceneName": "Deepnest_East_06" + }, + "Value": true + }, + { + "Key": { + "id": "Secret Mask", + "sceneName": "Deepnest_East_06" + }, + "Value": true + }, + { + "Key": { + "id": "Super Spitter", + "sceneName": "Deepnest_East_06" + }, + "Value": true + }, + { + "Key": { + "id": "Super Spitter (2)", + "sceneName": "Deepnest_East_06" + }, + "Value": true + }, + { + "Key": { + "id": "Super Spitter (1)", + "sceneName": "Deepnest_East_06" + }, + "Value": true + }, + { + "Key": { + "id": "Giant Hopper (2)", + "sceneName": "Deepnest_East_06" + }, + "Value": true + }, + { + "Key": { + "id": "Giant Hopper (1)", + "sceneName": "Deepnest_East_06" + }, + "Value": true + }, + { + "Key": { + "id": "Giant Hopper", + "sceneName": "Deepnest_East_06" + }, + "Value": true + }, + { + "Key": { + "id": "Secret Mask", + "sceneName": "Deepnest_East_16" + }, + "Value": true + }, + { + "Key": { + "id": "Quake Floor", + "sceneName": "Deepnest_East_16" + }, + "Value": true + }, + { + "Key": { + "id": "Secret Sound Region", + "sceneName": "Deepnest_East_16" + }, + "Value": true + }, + { + "Key": { + "id": "Super Spitter (1)", + "sceneName": "Deepnest_East_14" + }, + "Value": true + }, + { + "Key": { + "id": "Super Spitter", + "sceneName": "Deepnest_East_14" + }, + "Value": true + }, + { + "Key": { + "id": "Quake Floor (2)", + "sceneName": "Deepnest_East_14" + }, + "Value": true + }, + { + "Key": { + "id": "Egg Sac", + "sceneName": "Deepnest_East_14" + }, + "Value": true + }, + { + "Key": { + "id": "Quake Floor (1)", + "sceneName": "Deepnest_East_14" + }, + "Value": true + }, + { + "Key": { + "id": "Grub Bottle", + "sceneName": "Deepnest_East_14" + }, + "Value": true + }, + { + "Key": { + "id": "Breakable Wall", + "sceneName": "Deepnest_East_14" + }, + "Value": true + }, + { + "Key": { + "id": "Hopper Spawn", + "sceneName": "Deepnest_East_14" + }, + "Value": true + }, + { + "Key": { + "id": "Secret Mask", + "sceneName": "Deepnest_East_14" + }, + "Value": true + }, + { + "Key": { + "id": "Giant Hopper", + "sceneName": "Deepnest_East_14" + }, + "Value": true + }, + { + "Key": { + "id": "Giant Hopper", + "sceneName": "Deepnest_East_14b" + }, + "Value": true + }, + { + "Key": { + "id": "Super Spitter (2)", + "sceneName": "Deepnest_East_14b" + }, + "Value": true + }, + { + "Key": { + "id": "Super Spitter (3)", + "sceneName": "Deepnest_East_14b" + }, + "Value": true + }, + { + "Key": { + "id": "Super Spitter (4)", + "sceneName": "Deepnest_East_14b" + }, + "Value": true + }, + { + "Key": { + "id": "Super Spitter (5)", + "sceneName": "Deepnest_East_14b" + }, + "Value": true + }, + { + "Key": { + "id": "remask corridor", + "sceneName": "Deepnest_East_14b" + }, + "Value": true + }, + { + "Key": { + "id": "Secret Mask (1)", + "sceneName": "Deepnest_East_14b" + }, + "Value": true + }, + { + "Key": { + "id": "Remasker", + "sceneName": "Deepnest_East_14b" + }, + "Value": true + }, + { + "Key": { + "id": "secret sound", + "sceneName": "Deepnest_East_14b" + }, + "Value": true + }, + { + "Key": { + "id": "Inverse Remasker", + "sceneName": "Deepnest_East_14b" + }, + "Value": true + }, + { + "Key": { + "id": "Shiny Item", + "sceneName": "Deepnest_East_14b" + }, + "Value": false + }, + { + "Key": { + "id": "Shiny Item", + "sceneName": "Deepnest_East_18" + }, + "Value": false + }, + { + "Key": { + "id": "Quake Floor (2)", + "sceneName": "Deepnest_East_18" + }, + "Value": true + }, + { + "Key": { + "id": "Quake Floor (1)", + "sceneName": "Deepnest_East_18" + }, + "Value": true + }, + { + "Key": { + "id": "Secret Mask", + "sceneName": "Deepnest_East_18" + }, + "Value": true + }, + { + "Key": { + "id": "Remasker", + "sceneName": "Deepnest_East_11" + }, + "Value": true + }, + { + "Key": { + "id": "Breakable Wall", + "sceneName": "Deepnest_East_11" + }, + "Value": true + }, + { + "Key": { + "id": "Breakable Wall top", + "sceneName": "Deepnest_East_11" + }, + "Value": true + }, + { + "Key": { + "id": "Grub Bottle", + "sceneName": "Deepnest_East_11" + }, + "Value": true + }, + { + "Key": { + "id": "Super Spitter (4)", + "sceneName": "Deepnest_East_11" + }, + "Value": true + }, + { + "Key": { + "id": "Super Spitter (1)", + "sceneName": "Deepnest_East_11" + }, + "Value": true + }, + { + "Key": { + "id": "Super Spitter", + "sceneName": "Deepnest_East_11" + }, + "Value": true + }, + { + "Key": { + "id": "Super Spitter (3)", + "sceneName": "Deepnest_East_11" + }, + "Value": true + }, + { + "Key": { + "id": "Shiny Item", + "sceneName": "Deepnest_East_13" + }, + "Value": false + }, + { + "Key": { + "id": "Hornet Encounter Outskirts", + "sceneName": "Deepnest_East_12" + }, + "Value": true + }, + { + "Key": { + "id": "Shiny Item Stand", + "sceneName": "Room_Wyrm" + }, + "Value": false + }, + { + "Key": { + "id": "Breakable Wall Waterways", + "sceneName": "Deepnest_East_04" + }, + "Value": true + }, + { + "Key": { + "id": "Remasker", + "sceneName": "Deepnest_East_04" + }, + "Value": true + }, + { + "Key": { + "id": "Secret Sound Region", + "sceneName": "Deepnest_East_04" + }, + "Value": true + }, + { + "Key": { + "id": "Super Spitter (1)", + "sceneName": "Deepnest_East_04" + }, + "Value": true + }, + { + "Key": { + "id": "Super Spitter (2)", + "sceneName": "Deepnest_East_04" + }, + "Value": true + }, + { + "Key": { + "id": "Super Spitter (4)", + "sceneName": "Deepnest_East_04" + }, + "Value": true + }, + { + "Key": { + "id": "Super Spitter (3)", + "sceneName": "Deepnest_East_04" + }, + "Value": true + }, + { + "Key": { + "id": "Super Spitter", + "sceneName": "Deepnest_East_04" + }, + "Value": true + }, + { + "Key": { + "id": "Blow Fly (3)", + "sceneName": "Deepnest_East_04" + }, + "Value": true + }, + { + "Key": { + "id": "Blow Fly", + "sceneName": "Deepnest_East_04" + }, + "Value": true + }, + { + "Key": { + "id": "Blow Fly (2)", + "sceneName": "Deepnest_East_04" + }, + "Value": true + }, + { + "Key": { + "id": "Remasker", + "sceneName": "Abyss_06_Core" + }, + "Value": true + }, + { + "Key": { + "id": "Remasker (1)", + "sceneName": "Abyss_06_Core" + }, + "Value": true + }, + { + "Key": { + "id": "Remasker", + "sceneName": "Abyss_09" + }, + "Value": true + }, + { + "Key": { + "id": "Ruins Soul Vial", + "sceneName": "Abyss_Lighthouse_room" + }, + "Value": true + }, + { + "Key": { + "id": "Ruins Vial Empty", + "sceneName": "Abyss_Lighthouse_room" + }, + "Value": true + }, + { + "Key": { + "id": "Ruins Lever", + "sceneName": "Abyss_Lighthouse_room" + }, + "Value": true + }, + { + "Key": { + "id": "Secret Sound Region", + "sceneName": "Abyss_10" + }, + "Value": true + }, + { + "Key": { + "id": "Shiny Item", + "sceneName": "Abyss_10" + }, + "Value": false + }, + { + "Key": { + "id": "Bursting Bouncer (1)", + "sceneName": "Crossroads_25" + }, + "Value": true + }, + { + "Key": { + "id": "Reminder Look Down", + "sceneName": "Crossroads_36" + }, + "Value": false + }, + { + "Key": { + "id": "Secret Mask", + "sceneName": "Crossroads_36" + }, + "Value": true + }, + { + "Key": { + "id": "Force Hard Landing", + "sceneName": "Crossroads_36" + }, + "Value": false + }, + { + "Key": { + "id": "Collapser Small 1", + "sceneName": "Crossroads_36" + }, + "Value": true + }, + { + "Key": { + "id": "Collapser Small", + "sceneName": "Crossroads_36" + }, + "Value": true + }, + { + "Key": { + "id": "Mask Bottom", + "sceneName": "Crossroads_36" + }, + "Value": true + }, + { + "Key": { + "id": "Mawlek Body", + "sceneName": "Crossroads_09" + }, + "Value": true + }, + { + "Key": { + "id": "Break Floor 1", + "sceneName": "Crossroads_09" + }, + "Value": true + }, + { + "Key": { + "id": "Battle Scene", + "sceneName": "Crossroads_09" + }, + "Value": true + }, + { + "Key": { + "id": "Heart Piece", + "sceneName": "Crossroads_09" + }, + "Value": false + }, + { + "Key": { + "id": "Jellyfish (3)", + "sceneName": "Fungus3_02" + }, + "Value": true + }, + { + "Key": { + "id": "Jellyfish (4)", + "sceneName": "Fungus3_02" + }, + "Value": true + }, + { + "Key": { + "id": "Jellyfish (1)", + "sceneName": "Fungus3_02" + }, + "Value": true + }, + { + "Key": { + "id": "Jellyfish (2)", + "sceneName": "Fungus3_02" + }, + "Value": true + }, + { + "Key": { + "id": "Jellyfish", + "sceneName": "Fungus3_02" + }, + "Value": true + }, + { + "Key": { + "id": "Garden Zombie", + "sceneName": "Fungus3_34" + }, + "Value": true + }, + { + "Key": { + "id": "Egg Sac", + "sceneName": "Fungus3_34" + }, + "Value": true + }, + { + "Key": { + "id": "Garden Zombie (1)", + "sceneName": "Fungus3_34" + }, + "Value": true + }, + { + "Key": { + "id": "Garden Zombie (2)", + "sceneName": "Fungus3_34" + }, + "Value": true + }, + { + "Key": { + "id": "Ruins Lever", + "sceneName": "Fungus3_44" + }, + "Value": true + }, + { + "Key": { + "id": "One Way Wall", + "sceneName": "Fungus3_44" + }, + "Value": true + }, + { + "Key": { + "id": "Secret Mask", + "sceneName": "Fungus3_44" + }, + "Value": true + }, + { + "Key": { + "id": "One Way Wall", + "sceneName": "Room_Fungus_Shaman" + }, + "Value": true + }, + { + "Key": { + "id": "Battle Scene", + "sceneName": "Room_Fungus_Shaman" + }, + "Value": true + }, + { + "Key": { + "id": "Secret Mask", + "sceneName": "Room_Fungus_Shaman" + }, + "Value": true + }, + { + "Key": { + "id": "Bursting Zombie", + "sceneName": "Crossroads_39" + }, + "Value": true + }, + { + "Key": { + "id": "Bursting Zombie (1)", + "sceneName": "Crossroads_39" + }, + "Value": true + }, + { + "Key": { + "id": "Spitting Zombie", + "sceneName": "Crossroads_39" + }, + "Value": true + }, + { + "Key": { + "id": "Angry Buzzer", + "sceneName": "Crossroads_14" + }, + "Value": true + }, + { + "Key": { + "id": "Angry Buzzer (2)", + "sceneName": "Crossroads_16" + }, + "Value": true + }, + { + "Key": { + "id": "Angry Buzzer (1)", + "sceneName": "Crossroads_16" + }, + "Value": true + }, + { + "Key": { + "id": "Angry Buzzer", + "sceneName": "Crossroads_16" + }, + "Value": true + }, + { + "Key": { + "id": "Angry Buzzer", + "sceneName": "Crossroads_42" + }, + "Value": true + }, + { + "Key": { + "id": "Angry Buzzer (1)", + "sceneName": "Crossroads_42" + }, + "Value": true + }, + { + "Key": { + "id": "Angry Buzzer (2)", + "sceneName": "Crossroads_42" + }, + "Value": true + }, + { + "Key": { + "id": "Angry Buzzer", + "sceneName": "Crossroads_13" + }, + "Value": true + }, + { + "Key": { + "id": "Angry Buzzer (1)", + "sceneName": "Crossroads_13" + }, + "Value": true + }, + { + "Key": { + "id": "Bursting Zombie", + "sceneName": "Crossroads_13" + }, + "Value": true + }, + { + "Key": { + "id": "Blocker 2", + "sceneName": "Fungus1_28" + }, + "Value": true + }, + { + "Key": { + "id": "Blocker 1", + "sceneName": "Fungus1_28" + }, + "Value": true + }, + { + "Key": { + "id": "One Way Wall", + "sceneName": "Fungus1_28" + }, + "Value": true + }, + { + "Key": { + "id": "Grub Bottle", + "sceneName": "Fungus1_28" + }, + "Value": true + }, + { + "Key": { + "id": "Chest", + "sceneName": "Fungus1_28" + }, + "Value": true + }, + { + "Key": { + "id": "Shiny Item", + "sceneName": "Fungus1_28" + }, + "Value": false + }, + { + "Key": { + "id": "mask_01", + "sceneName": "Fungus1_28" + }, + "Value": true + }, + { + "Key": { + "id": "Secret Mask", + "sceneName": "Fungus1_28" + }, + "Value": true + }, + { + "Key": { + "id": "One Way Wall (1)", + "sceneName": "Fungus1_28" + }, + "Value": true + }, + { + "Key": { + "id": "Moss Knight C", + "sceneName": "Fungus1_32" + }, + "Value": true + }, + { + "Key": { + "id": "Shiny Item Relic2", + "sceneName": "Crossroads_38" + }, + "Value": false + }, + { + "Key": { + "id": "Fungus Break Floor", + "sceneName": "Deepnest_01" + }, + "Value": true + }, + { + "Key": { + "id": "Secret Mask", + "sceneName": "Deepnest_01" + }, + "Value": true + }, + { + "Key": { + "id": "Breakable Wall", + "sceneName": "Deepnest_01" + }, + "Value": true + }, + { + "Key": { + "id": "One Way Wall", + "sceneName": "Deepnest_01" + }, + "Value": true + }, + { + "Key": { + "id": "Battle Scene", + "sceneName": "Fungus3_39" + }, + "Value": true + }, + { + "Key": { + "id": "One Way Wall", + "sceneName": "Fungus3_39" + }, + "Value": true + }, + { + "Key": { + "id": "Shiny Item Stand", + "sceneName": "Fungus3_39" + }, + "Value": false + }, + { + "Key": { + "id": "Mantis Heavy", + "sceneName": "Fungus3_39" + }, + "Value": true + }, + { + "Key": { + "id": "Mantis Heavy (1)", + "sceneName": "Fungus3_39" + }, + "Value": true + }, + { + "Key": { + "id": "Acid Walker", + "sceneName": "Fungus3_39" + }, + "Value": true + }, + { + "Key": { + "id": "Inverse Remasker", + "sceneName": "Deepnest_14" + }, + "Value": true + }, + { + "Key": { + "id": "Collapser Small", + "sceneName": "Deepnest_14" + }, + "Value": true + }, + { + "Key": { + "id": "Inverse Remasker (1)", + "sceneName": "Deepnest_14" + }, + "Value": true + }, + { + "Key": { + "id": "Breakable Wall", + "sceneName": "Deepnest_31" + }, + "Value": true + }, + { + "Key": { + "id": "Secret Mask", + "sceneName": "Deepnest_31" + }, + "Value": true + }, + { + "Key": { + "id": "Mimic Spider Fake1", + "sceneName": "Deepnest_31" + }, + "Value": true + }, + { + "Key": { + "id": "One Way Wall", + "sceneName": "Deepnest_31" + }, + "Value": true + }, + { + "Key": { + "id": "Secret Mask (1)", + "sceneName": "Deepnest_31" + }, + "Value": true + }, + { + "Key": { + "id": "Mimic Spider Fake3", + "sceneName": "Deepnest_31" + }, + "Value": true + }, + { + "Key": { + "id": "Grub Bottle", + "sceneName": "Deepnest_31" + }, + "Value": true + }, + { + "Key": { + "id": "Secret Mask (2)", + "sceneName": "Deepnest_31" + }, + "Value": true + }, + { + "Key": { + "id": "Breakable Wall (1)", + "sceneName": "Deepnest_31" + }, + "Value": true + }, + { + "Key": { + "id": "Mimic Spider Fake2", + "sceneName": "Deepnest_31" + }, + "Value": true + }, + { + "Key": { + "id": "Shiny Item Stand", + "sceneName": "Deepnest_32" + }, + "Value": false + }, + { + "Key": { + "id": "Battle Scene", + "sceneName": "Deepnest_32" + }, + "Value": true + }, + { + "Key": { + "id": "Zombie Hornhead Sp (2)", + "sceneName": "Deepnest_33" + }, + "Value": true + }, + { + "Key": { + "id": "Zombie Runner Sp (1)", + "sceneName": "Deepnest_33" + }, + "Value": true + }, + { + "Key": { + "id": "Collapser Small", + "sceneName": "Deepnest_33" + }, + "Value": true + }, + { + "Key": { + "id": "Secret Mask", + "sceneName": "Deepnest_33" + }, + "Value": true + }, + { + "Key": { + "id": "Battle Scene v2", + "sceneName": "Deepnest_33" + }, + "Value": true + }, + { + "Key": { + "id": "Shiny Item", + "sceneName": "Deepnest_33" + }, + "Value": false + }, + { + "Key": { + "id": "Zombie Hornhead Sp (1)", + "sceneName": "Deepnest_33" + }, + "Value": true + }, + { + "Key": { + "id": "Zombie Hornhead Sp", + "sceneName": "Deepnest_33" + }, + "Value": true + }, + { + "Key": { + "id": "Centipede Hatcher (2)", + "sceneName": "Deepnest_26" + }, + "Value": true + }, + { + "Key": { + "id": "Centipede Hatcher", + "sceneName": "Deepnest_26" + }, + "Value": true + }, + { + "Key": { + "id": "Health Cocoon", + "sceneName": "Deepnest_26" + }, + "Value": true + }, + { + "Key": { + "id": "Secret Mask (2)", + "sceneName": "Deepnest_26" + }, + "Value": true + }, + { + "Key": { + "id": "Secret Mask (1)", + "sceneName": "Deepnest_26" + }, + "Value": true + }, + { + "Key": { + "id": "Ruins Lever", + "sceneName": "Deepnest_26" + }, + "Value": true + }, + { + "Key": { + "id": "Inverse Remasker", + "sceneName": "Deepnest_26" + }, + "Value": true + }, + { + "Key": { + "id": "Secret Sound Region", + "sceneName": "Deepnest_26" + }, + "Value": true + }, + { + "Key": { + "id": "Centipede Hatcher (9)", + "sceneName": "Deepnest_26b" + }, + "Value": true + }, + { + "Key": { + "id": "Centipede Hatcher (7)", + "sceneName": "Deepnest_26b" + }, + "Value": true + }, + { + "Key": { + "id": "Centipede Hatcher (4)", + "sceneName": "Deepnest_26b" + }, + "Value": true + }, + { + "Key": { + "id": "Centipede Hatcher (5)", + "sceneName": "Deepnest_26b" + }, + "Value": true + }, + { + "Key": { + "id": "Ruins Lever Remade", + "sceneName": "Deepnest_26b" + }, + "Value": true + }, + { + "Key": { + "id": "tram_inverse mask", + "sceneName": "Deepnest_26b" + }, + "Value": true + }, + { + "Key": { + "id": "Shiny Item", + "sceneName": "Deepnest_26" + }, + "Value": false + }, + { + "Key": { + "id": "mask tram left front", + "sceneName": "Deepnest_26b" + }, + "Value": true + }, + { + "Key": { + "id": "Zombie Hornhead Sp (2)", + "sceneName": "Deepnest_35" + }, + "Value": true + }, + { + "Key": { + "id": "Zombie Hornhead Sp", + "sceneName": "Deepnest_35" + }, + "Value": true + }, + { + "Key": { + "id": "Zombie Runner Sp", + "sceneName": "Deepnest_35" + }, + "Value": true + }, + { + "Key": { + "id": "Health Cocoon", + "sceneName": "Deepnest_40" + }, + "Value": true + }, + { + "Key": { + "id": "Chest", + "sceneName": "Deepnest_45_v02" + }, + "Value": true + }, + { + "Key": { + "id": "Shiny Item (1)", + "sceneName": "Deepnest_45_v02" + }, + "Value": false + }, + { + "Key": { + "id": "Breakable Wall", + "sceneName": "Deepnest_45_v02" + }, + "Value": true + }, + { + "Key": { + "id": "Inverse Remasker (1)", + "sceneName": "Deepnest_45_v02" + }, + "Value": true + }, + { + "Key": { + "id": "Breakable Wall Waterways", + "sceneName": "Deepnest_45_v02" + }, + "Value": true + }, + { + "Key": { + "id": "Last Weaver", + "sceneName": "Deepnest_45_v02" + }, + "Value": true + }, + { + "Key": { + "id": "Inverse Remasker", + "sceneName": "Deepnest_45_v02" + }, + "Value": true + }, + { + "Key": { + "id": "Shiny Item", + "sceneName": "Deepnest_45_v02" + }, + "Value": false + }, + { + "Key": { + "id": "Secret Mask", + "sceneName": "Deepnest_45_v02" + }, + "Value": true + }, + { + "Key": { + "id": "Collapser Small", + "sceneName": "Deepnest_45_v02" + }, + "Value": true + }, + { + "Key": { + "id": "One Way Wall", + "sceneName": "Deepnest_44" + }, + "Value": true + }, + { + "Key": { + "id": "Inverse Remasker", + "sceneName": "Deepnest_44" + }, + "Value": true + }, + { + "Key": { + "id": "Shiny Item Stand", + "sceneName": "Deepnest_44" + }, + "Value": false + }, + { + "Key": { + "id": "Secret Sound Region", + "sceneName": "Deepnest_44" + }, + "Value": true + }, + { + "Key": { + "id": "Remasker", + "sceneName": "Deepnest_44" + }, + "Value": true + }, + { + "Key": { + "id": "Secret Mask", + "sceneName": "Deepnest_44" + }, + "Value": true + }, + { + "Key": { + "id": "gramaphone", + "sceneName": "Room_Tram" + }, + "Value": true + }, + { + "Key": { + "id": "gramaphone (1)", + "sceneName": "Room_Tram" + }, + "Value": true + }, + { + "Key": { + "id": "Secret Mask", + "sceneName": "Abyss_08" + }, + "Value": true + }, + { + "Key": { + "id": "Shiny Item (1)", + "sceneName": "Abyss_08" + }, + "Value": false + }, + { + "Key": { + "id": "One Way Wall", + "sceneName": "Abyss_08" + }, + "Value": true + }, + { + "Key": { + "id": "Shiny Item", + "sceneName": "Abyss_08" + }, + "Value": false + }, + { + "Key": { + "id": "Vessel Fragment", + "sceneName": "Abyss_04" + }, + "Value": false + }, + { + "Key": { + "id": "Collapser Small", + "sceneName": "Deepnest_38" + }, + "Value": true + }, + { + "Key": { + "id": "Vessel Fragment", + "sceneName": "Deepnest_38" + }, + "Value": true + }, + { + "Key": { + "id": "Secret Mask", + "sceneName": "Deepnest_38" + }, + "Value": true + }, + { + "Key": { + "id": "Breakable Wall", + "sceneName": "Deepnest_38" + }, + "Value": true + }, + { + "Key": { + "id": "Shiny Item(Clone)", + "sceneName": "Fungus3_34" + }, + "Value": false + }, + { + "Key": { + "id": "Mantis Heavy Flyer", + "sceneName": "Fungus3_04" + }, + "Value": true + }, + { + "Key": { + "id": "Ruins Lever", + "sceneName": "Fungus3_04" + }, + "Value": true + }, + { + "Key": { + "id": "Garden Slide Floor", + "sceneName": "Fungus3_04" + }, + "Value": true + }, + { + "Key": { + "id": "Mantis Heavy Flyer (1)", + "sceneName": "Fungus3_04" + }, + "Value": true + }, + { + "Key": { + "id": "Battle Scene", + "sceneName": "Fungus3_05" + }, + "Value": true + }, + { + "Key": { + "id": "One Way Wall", + "sceneName": "Fungus3_05" + }, + "Value": true + }, + { + "Key": { + "id": "Secret Mask (1)", + "sceneName": "Fungus3_05" + }, + "Value": true + }, + { + "Key": { + "id": "Gate Switch", + "sceneName": "Fungus3_05" + }, + "Value": true + }, + { + "Key": { + "id": "Plant Trap", + "sceneName": "Fungus3_05" + }, + "Value": true + }, + { + "Key": { + "id": "Plant Trap (2)", + "sceneName": "Fungus3_05" + }, + "Value": true + }, + { + "Key": { + "id": "Mantis Heavy Flyer", + "sceneName": "Fungus3_05" + }, + "Value": true + }, + { + "Key": { + "id": "Plant Trap (1)", + "sceneName": "Fungus3_05" + }, + "Value": true + }, + { + "Key": { + "id": "Collapser Small", + "sceneName": "Fungus1_24" + }, + "Value": true + }, + { + "Key": { + "id": "Ghost NPC", + "sceneName": "Fungus1_24" + }, + "Value": true + }, + { + "Key": { + "id": "Shiny", + "sceneName": "Fungus1_24" + }, + "Value": true + }, + { + "Key": { + "id": "Dream Plant Orb (20)", + "sceneName": "Fungus3_11" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (25)", + "sceneName": "Fungus3_11" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (7)", + "sceneName": "Fungus3_11" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (4)", + "sceneName": "Fungus3_11" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (22)", + "sceneName": "Fungus3_11" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (17)", + "sceneName": "Fungus3_11" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (6)", + "sceneName": "Fungus3_11" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (16)", + "sceneName": "Fungus3_11" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant", + "sceneName": "Fungus3_11" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (12)", + "sceneName": "Fungus3_11" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (13)", + "sceneName": "Fungus3_11" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (15)", + "sceneName": "Fungus3_11" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (18)", + "sceneName": "Fungus3_11" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (27)", + "sceneName": "Fungus3_11" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (14)", + "sceneName": "Fungus3_11" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (21)", + "sceneName": "Fungus3_11" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (1)", + "sceneName": "Fungus3_11" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (3)", + "sceneName": "Fungus3_11" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (11)", + "sceneName": "Fungus3_11" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (28)", + "sceneName": "Fungus3_11" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (9)", + "sceneName": "Fungus3_11" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb", + "sceneName": "Fungus3_11" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (2)", + "sceneName": "Fungus3_11" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (8)", + "sceneName": "Fungus3_11" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (10)", + "sceneName": "Fungus3_11" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (24)", + "sceneName": "Fungus3_11" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (26)", + "sceneName": "Fungus3_11" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (19)", + "sceneName": "Fungus3_11" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (5)", + "sceneName": "Fungus3_11" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (23)", + "sceneName": "Fungus3_11" + }, + "Value": false + }, + { + "Key": { + "id": "Mantis Heavy Flyer (1)", + "sceneName": "Fungus3_08" + }, + "Value": true + }, + { + "Key": { + "id": "Mantis Heavy Flyer (3)", + "sceneName": "Fungus3_08" + }, + "Value": true + }, + { + "Key": { + "id": "Mantis Heavy Flyer", + "sceneName": "Fungus3_08" + }, + "Value": true + }, + { + "Key": { + "id": "Mantis Heavy Flyer (2)", + "sceneName": "Fungus3_08" + }, + "Value": true + }, + { + "Key": { + "id": "Remasker", + "sceneName": "Deepnest_43" + }, + "Value": true + }, + { + "Key": { + "id": "Garden Zombie", + "sceneName": "Deepnest_43" + }, + "Value": true + }, + { + "Key": { + "id": "Garden Zombie (1)", + "sceneName": "Deepnest_43" + }, + "Value": true + }, + { + "Key": { + "id": "Mantis Heavy Flyer", + "sceneName": "Deepnest_43" + }, + "Value": true + }, + { + "Key": { + "id": "Toll Machine Bench", + "sceneName": "Fungus3_50" + }, + "Value": true + }, + { + "Key": { + "id": "gramaphone", + "sceneName": "Fungus3_50" + }, + "Value": true + }, + { + "Key": { + "id": "Garden Zombie (1)", + "sceneName": "Fungus3_10" + }, + "Value": true + }, + { + "Key": { + "id": "Garden Zombie", + "sceneName": "Fungus3_10" + }, + "Value": true + }, + { + "Key": { + "id": "Battle Scene", + "sceneName": "Fungus3_10" + }, + "Value": true + }, + { + "Key": { + "id": "Grub Bottle", + "sceneName": "Fungus3_10" + }, + "Value": true + }, + { + "Key": { + "id": "Secret Mask", + "sceneName": "Fungus3_10" + }, + "Value": true + }, + { + "Key": { + "id": "Plant Trap", + "sceneName": "Fungus3_48" + }, + "Value": true + }, + { + "Key": { + "id": "Plant Trap (2)", + "sceneName": "Fungus3_48" + }, + "Value": true + }, + { + "Key": { + "id": "Remasker", + "sceneName": "Fungus3_48" + }, + "Value": true + }, + { + "Key": { + "id": "Shiny Item", + "sceneName": "Fungus3_48" + }, + "Value": false + }, + { + "Key": { + "id": "secret sound", + "sceneName": "Fungus3_48" + }, + "Value": true + }, + { + "Key": { + "id": "Grub Bottle", + "sceneName": "Fungus3_48" + }, + "Value": true + }, + { + "Key": { + "id": "Plant Trap (8)", + "sceneName": "Fungus3_48" + }, + "Value": true + }, + { + "Key": { + "id": "Mantis Heavy Flyer (1)", + "sceneName": "Fungus3_48" + }, + "Value": true + }, + { + "Key": { + "id": "Plant Trap (5)", + "sceneName": "Fungus3_48" + }, + "Value": true + }, + { + "Key": { + "id": "Plant Trap (7)", + "sceneName": "Fungus3_48" + }, + "Value": true + }, + { + "Key": { + "id": "Plant Trap (4)", + "sceneName": "Fungus3_48" + }, + "Value": true + }, + { + "Key": { + "id": "Mantis Heavy Flyer", + "sceneName": "Fungus3_48" + }, + "Value": true + }, + { + "Key": { + "id": "Plant Trap (10)", + "sceneName": "Fungus3_48" + }, + "Value": true + }, + { + "Key": { + "id": "Plant Trap (3)", + "sceneName": "Fungus3_48" + }, + "Value": true + }, + { + "Key": { + "id": "Plant Trap (9)", + "sceneName": "Fungus3_48" + }, + "Value": true + }, + { + "Key": { + "id": "Plant Trap (11)", + "sceneName": "Fungus3_48" + }, + "Value": true + }, + { + "Key": { + "id": "Plant Trap (1)", + "sceneName": "Fungus3_48" + }, + "Value": true + }, + { + "Key": { + "id": "Plant Trap (6)", + "sceneName": "Fungus3_48" + }, + "Value": true + }, + { + "Key": { + "id": "secret sound", + "sceneName": "Fungus3_40" + }, + "Value": true + }, + { + "Key": { + "id": "Secret Sound Region", + "sceneName": "Fungus3_40" + }, + "Value": true + }, + { + "Key": { + "id": "Gate Switch", + "sceneName": "Fungus3_40" + }, + "Value": true + }, + { + "Key": { + "id": "Remasker", + "sceneName": "Fungus3_40" + }, + "Value": true + }, + { + "Key": { + "id": "Mantis Heavy Flyer", + "sceneName": "Fungus3_40" + }, + "Value": true + }, + { + "Key": { + "id": "Mantis Heavy Flyer (1)", + "sceneName": "Fungus3_40" + }, + "Value": true + }, + { + "Key": { + "id": "Lesser Mawlek", + "sceneName": "Abyss_17" + }, + "Value": true + }, + { + "Key": { + "id": "Lesser Mawlek (1)", + "sceneName": "Abyss_17" + }, + "Value": true + }, + { + "Key": { + "id": "Lesser Mawlek (2)", + "sceneName": "Abyss_17" + }, + "Value": true + }, + { + "Key": { + "id": "Lesser Mawlek (3)", + "sceneName": "Abyss_17" + }, + "Value": true + }, + { + "Key": { + "id": "Lesser Mawlek (4)", + "sceneName": "Abyss_17" + }, + "Value": true + }, + { + "Key": { + "id": "Lesser Mawlek 1", + "sceneName": "Abyss_17" + }, + "Value": true + }, + { + "Key": { + "id": "Lesser Mawlek 2", + "sceneName": "Abyss_17" + }, + "Value": true + }, + { + "Key": { + "id": "Ceiling Dropper (3)", + "sceneName": "Abyss_17" + }, + "Value": true + }, + { + "Key": { + "id": "Lesser Mawlek (8)", + "sceneName": "Abyss_17" + }, + "Value": true + }, + { + "Key": { + "id": "Ceiling Dropper (2)", + "sceneName": "Abyss_17" + }, + "Value": true + }, + { + "Key": { + "id": "Ceiling Dropper (1)", + "sceneName": "Abyss_17" + }, + "Value": true + }, + { + "Key": { + "id": "Ceiling Dropper", + "sceneName": "Abyss_17" + }, + "Value": true + }, + { + "Key": { + "id": "Secret Mask", + "sceneName": "Abyss_17" + }, + "Value": true + }, + { + "Key": { + "id": "Secret Mask (1)", + "sceneName": "Abyss_17" + }, + "Value": true + }, + { + "Key": { + "id": "Grub Bottle", + "sceneName": "Abyss_17" + }, + "Value": true + }, + { + "Key": { + "id": "Battle Scene Ore", + "sceneName": "Abyss_17" + }, + "Value": true + }, + { + "Key": { + "id": "One Way Wall", + "sceneName": "Abyss_17" + }, + "Value": true + }, + { + "Key": { + "id": "Quake Floor", + "sceneName": "Abyss_17" + }, + "Value": true + }, + { + "Key": { + "id": "Shiny Item Stand", + "sceneName": "Abyss_17" + }, + "Value": false + }, + { + "Key": { + "id": "Mantis Heavy", + "sceneName": "Fungus3_10" + }, + "Value": true + }, + { + "Key": { + "id": "Mantis Heavy Spawn", + "sceneName": "Fungus3_39" + }, + "Value": true + }, + { + "Key": { + "id": "ruind_dressing_light_01", + "sceneName": "Fungus3_22" + }, + "Value": true + }, + { + "Key": { + "id": "Grub Bottle", + "sceneName": "Fungus3_22" + }, + "Value": true + }, + { + "Key": { + "id": "Garden Zombie (2)", + "sceneName": "Fungus3_22" + }, + "Value": true + }, + { + "Key": { + "id": "Garden Zombie", + "sceneName": "Fungus3_22" + }, + "Value": true + }, + { + "Key": { + "id": "Garden Zombie (1)", + "sceneName": "Fungus3_22" + }, + "Value": true + }, + { + "Key": { + "id": "Mantis Heavy Flyer (1)", + "sceneName": "Fungus3_22" + }, + "Value": true + }, + { + "Key": { + "id": "Mantis Heavy Flyer", + "sceneName": "Fungus3_22" + }, + "Value": true + }, + { + "Key": { + "id": "Battle Scene", + "sceneName": "Fungus3_23" + }, + "Value": true + }, + { + "Key": { + "id": "Remasker", + "sceneName": "Room_Queen" + }, + "Value": true + }, + { + "Key": { + "id": "Inverse Remasker", + "sceneName": "Room_Queen" + }, + "Value": true + }, + { + "Key": { + "id": "Shiny Item RoyalCharm", + "sceneName": "Room_Queen" + }, + "Value": false + }, + { + "Key": { + "id": "Cloth Ghost NPC", + "sceneName": "Fungus3_23" + }, + "Value": true + }, + { + "Key": { + "id": "Dream Plant Orb (4)", + "sceneName": "Fungus1_13" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (15)", + "sceneName": "Fungus1_13" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (26)", + "sceneName": "Fungus1_13" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (5)", + "sceneName": "Fungus1_13" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (25)", + "sceneName": "Fungus1_13" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (36)", + "sceneName": "Fungus1_13" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (41)", + "sceneName": "Fungus1_13" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (16)", + "sceneName": "Fungus1_13" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (23)", + "sceneName": "Fungus1_13" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (42)", + "sceneName": "Fungus1_13" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (3)", + "sceneName": "Fungus1_13" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (38)", + "sceneName": "Fungus1_13" + }, + "Value": false + }, + { + "Key": { + "id": "Chest", + "sceneName": "Fungus1_13" + }, + "Value": true + }, + { + "Key": { + "id": "Dream Plant Orb (20)", + "sceneName": "Fungus1_13" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (2)", + "sceneName": "Fungus1_13" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (35)", + "sceneName": "Fungus1_13" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (27)", + "sceneName": "Fungus1_13" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (43)", + "sceneName": "Fungus1_13" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (14)", + "sceneName": "Fungus1_13" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (22)", + "sceneName": "Fungus1_13" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant", + "sceneName": "Fungus1_13" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (34)", + "sceneName": "Fungus1_13" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (17)", + "sceneName": "Fungus1_13" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (8)", + "sceneName": "Fungus1_13" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb", + "sceneName": "Fungus1_13" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (21)", + "sceneName": "Fungus1_13" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (40)", + "sceneName": "Fungus1_13" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (6)", + "sceneName": "Fungus1_13" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (12)", + "sceneName": "Fungus1_13" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (11)", + "sceneName": "Fungus1_13" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (29)", + "sceneName": "Fungus1_13" + }, + "Value": false + }, + { + "Key": { + "id": "Grub Bottle", + "sceneName": "Fungus1_13" + }, + "Value": true + }, + { + "Key": { + "id": "Dream Plant Orb (31)", + "sceneName": "Fungus1_13" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (24)", + "sceneName": "Fungus1_13" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (10)", + "sceneName": "Fungus1_13" + }, + "Value": false + }, + { + "Key": { + "id": "Vessel Fragment", + "sceneName": "Fungus1_13" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (9)", + "sceneName": "Fungus1_13" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (7)", + "sceneName": "Fungus1_13" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (28)", + "sceneName": "Fungus1_13" + }, + "Value": false + }, + { + "Key": { + "id": "Vine Platform", + "sceneName": "Fungus1_13" + }, + "Value": true + }, + { + "Key": { + "id": "Dream Plant Orb (13)", + "sceneName": "Fungus1_13" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (1)", + "sceneName": "Fungus1_13" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (18)", + "sceneName": "Fungus1_13" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (39)", + "sceneName": "Fungus1_13" + }, + "Value": false + }, + { + "Key": { + "id": "Vine Platform (2)", + "sceneName": "Fungus1_13" + }, + "Value": true + }, + { + "Key": { + "id": "Dream Plant Orb (32)", + "sceneName": "Fungus1_13" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (30)", + "sceneName": "Fungus1_13" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (19)", + "sceneName": "Fungus1_13" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (37)", + "sceneName": "Fungus1_13" + }, + "Value": false + }, + { + "Key": { + "id": "Vine Platform (1)", + "sceneName": "Fungus1_13" + }, + "Value": true + }, + { + "Key": { + "id": "Dream Plant Orb (33)", + "sceneName": "Fungus1_13" + }, + "Value": false + }, + { + "Key": { + "id": "Acid Walker (4)", + "sceneName": "Fungus1_13" + }, + "Value": true + }, + { + "Key": { + "id": "Acid Walker", + "sceneName": "Fungus1_13" + }, + "Value": true + }, + { + "Key": { + "id": "Acid Walker (5)", + "sceneName": "Fungus1_13" + }, + "Value": true + }, + { + "Key": { + "id": "Acid Walker (2)", + "sceneName": "Fungus1_13" + }, + "Value": true + }, + { + "Key": { + "id": "Acid Walker (3)", + "sceneName": "Fungus1_13" + }, + "Value": true + }, + { + "Key": { + "id": "Acid Walker (1)", + "sceneName": "Fungus1_13" + }, + "Value": true + }, + { + "Key": { + "id": "Dream Plant Orb (34)", + "sceneName": "Deepnest_East_07" + }, + "Value": false + }, + { + "Key": { + "id": "Shiny Item (1)", + "sceneName": "Deepnest_East_07" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (30)", + "sceneName": "Deepnest_East_07" + }, + "Value": false + }, + { + "Key": { + "id": "Shiny Item", + "sceneName": "Deepnest_East_07" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (41)", + "sceneName": "Deepnest_East_07" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (33)", + "sceneName": "Deepnest_East_07" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (17)", + "sceneName": "Deepnest_East_07" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (13)", + "sceneName": "Deepnest_East_07" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (48)", + "sceneName": "Deepnest_East_07" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (42)", + "sceneName": "Deepnest_East_07" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb", + "sceneName": "Deepnest_East_07" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (40)", + "sceneName": "Deepnest_East_07" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (29)", + "sceneName": "Deepnest_East_07" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (9)", + "sceneName": "Deepnest_East_07" + }, + "Value": false + }, + { + "Key": { + "id": "Secret Mask", + "sceneName": "Deepnest_East_07" + }, + "Value": true + }, + { + "Key": { + "id": "Dream Plant Orb (8)", + "sceneName": "Deepnest_East_07" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (14)", + "sceneName": "Deepnest_East_07" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (44)", + "sceneName": "Deepnest_East_07" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (47)", + "sceneName": "Deepnest_East_07" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (4)", + "sceneName": "Deepnest_East_07" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (18)", + "sceneName": "Deepnest_East_07" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (31)", + "sceneName": "Deepnest_East_07" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (46)", + "sceneName": "Deepnest_East_07" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (1)", + "sceneName": "Deepnest_East_07" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (23)", + "sceneName": "Deepnest_East_07" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (21)", + "sceneName": "Deepnest_East_07" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (6)", + "sceneName": "Deepnest_East_07" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (20)", + "sceneName": "Deepnest_East_07" + }, + "Value": false + }, + { + "Key": { + "id": "Remasker", + "sceneName": "Deepnest_East_07" + }, + "Value": true + }, + { + "Key": { + "id": "Dream Plant Orb (10)", + "sceneName": "Deepnest_East_07" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (51)", + "sceneName": "Deepnest_East_07" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (19)", + "sceneName": "Deepnest_East_07" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (45)", + "sceneName": "Deepnest_East_07" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (12)", + "sceneName": "Deepnest_East_07" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (15)", + "sceneName": "Deepnest_East_07" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (50)", + "sceneName": "Deepnest_East_07" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (25)", + "sceneName": "Deepnest_East_07" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (5)", + "sceneName": "Deepnest_East_07" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (36)", + "sceneName": "Deepnest_East_07" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (2)", + "sceneName": "Deepnest_East_07" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (24)", + "sceneName": "Deepnest_East_07" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (26)", + "sceneName": "Deepnest_East_07" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant", + "sceneName": "Deepnest_East_07" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (27)", + "sceneName": "Deepnest_East_07" + }, + "Value": false + }, + { + "Key": { + "id": "Secret Mask (1)", + "sceneName": "Deepnest_East_07" + }, + "Value": true + }, + { + "Key": { + "id": "Dream Plant Orb (49)", + "sceneName": "Deepnest_East_07" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (22)", + "sceneName": "Deepnest_East_07" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (37)", + "sceneName": "Deepnest_East_07" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (3)", + "sceneName": "Deepnest_East_07" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (32)", + "sceneName": "Deepnest_East_07" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (39)", + "sceneName": "Deepnest_East_07" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (43)", + "sceneName": "Deepnest_East_07" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (35)", + "sceneName": "Deepnest_East_07" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (28)", + "sceneName": "Deepnest_East_07" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (7)", + "sceneName": "Deepnest_East_07" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (16)", + "sceneName": "Deepnest_East_07" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (11)", + "sceneName": "Deepnest_East_07" + }, + "Value": false + }, + { + "Key": { + "id": "Super Spitter (1)", + "sceneName": "Deepnest_East_07" + }, + "Value": true + }, + { + "Key": { + "id": "Super Spitter (5)", + "sceneName": "Deepnest_East_07" + }, + "Value": true + }, + { + "Key": { + "id": "Super Spitter", + "sceneName": "Deepnest_East_07" + }, + "Value": true + }, + { + "Key": { + "id": "Super Spitter (2)", + "sceneName": "Deepnest_East_07" + }, + "Value": true + }, + { + "Key": { + "id": "Super Spitter (4)", + "sceneName": "Deepnest_East_07" + }, + "Value": true + }, + { + "Key": { + "id": "Blow Fly (1)", + "sceneName": "Deepnest_East_07" + }, + "Value": true + }, + { + "Key": { + "id": "Ceiling Dropper (5)", + "sceneName": "Deepnest_East_07" + }, + "Value": true + }, + { + "Key": { + "id": "Ceiling Dropper (7)", + "sceneName": "Deepnest_East_07" + }, + "Value": true + }, + { + "Key": { + "id": "Blow Fly (2)", + "sceneName": "Deepnest_East_07" + }, + "Value": true + }, + { + "Key": { + "id": "Ceiling Dropper", + "sceneName": "Deepnest_East_07" + }, + "Value": true + }, + { + "Key": { + "id": "Blow Fly", + "sceneName": "Deepnest_East_07" + }, + "Value": true + }, + { + "Key": { + "id": "Ceiling Dropper (6)", + "sceneName": "Deepnest_East_07" + }, + "Value": true + }, + { + "Key": { + "id": "Ceiling Dropper (4)", + "sceneName": "Deepnest_East_07" + }, + "Value": true + }, + { + "Key": { + "id": "Ceiling Dropper (2)", + "sceneName": "Deepnest_East_07" + }, + "Value": true + }, + { + "Key": { + "id": "Ceiling Dropper (1)", + "sceneName": "Deepnest_East_07" + }, + "Value": true + }, + { + "Key": { + "id": "Remasker (2)", + "sceneName": "Ruins2_11_b" + }, + "Value": true + }, + { + "Key": { + "id": "Break Jar", + "sceneName": "Ruins2_11_b" + }, + "Value": true + }, + { + "Key": { + "id": "Remasker (1)", + "sceneName": "Ruins2_11_b" + }, + "Value": true + }, + { + "Key": { + "id": "Inverse Remasker", + "sceneName": "Ruins2_11_b" + }, + "Value": true + }, + { + "Key": { + "id": "Ruins Lever", + "sceneName": "Ruins2_11_b" + }, + "Value": true + }, + { + "Key": { + "id": "Break Jar (1)", + "sceneName": "Ruins2_11_b" + }, + "Value": true + }, + { + "Key": { + "id": "Break Jar (4)", + "sceneName": "Ruins2_11" + }, + "Value": true + }, + { + "Key": { + "id": "inver remask_below second corridor", + "sceneName": "Ruins2_11" + }, + "Value": true + }, + { + "Key": { + "id": "inver remask_below second corridor (1)", + "sceneName": "Ruins2_11" + }, + "Value": true + }, + { + "Key": { + "id": "secret sound_grub room", + "sceneName": "Ruins2_11" + }, + "Value": true + }, + { + "Key": { + "id": "Secret Mask", + "sceneName": "Ruins2_11" + }, + "Value": true + }, + { + "Key": { + "id": "Break Jar (3)", + "sceneName": "Ruins2_11" + }, + "Value": true + }, + { + "Key": { + "id": "Grub Bottle", + "sceneName": "Ruins2_11" + }, + "Value": true + }, + { + "Key": { + "id": "Break Jar (5)", + "sceneName": "Ruins2_11" + }, + "Value": true + }, + { + "Key": { + "id": "Break Jar (7)", + "sceneName": "Ruins2_11" + }, + "Value": true + }, + { + "Key": { + "id": "inver remask_above boss encounter", + "sceneName": "Ruins2_11" + }, + "Value": true + }, + { + "Key": { + "id": "Break Jar (2)", + "sceneName": "Ruins2_11" + }, + "Value": true + }, + { + "Key": { + "id": "Break Jar (8)", + "sceneName": "Ruins2_11" + }, + "Value": true + }, + { + "Key": { + "id": "Shiny Item CollectorMap", + "sceneName": "Ruins2_11" + }, + "Value": false + }, + { + "Key": { + "id": "Break Jar (6)", + "sceneName": "Ruins2_11" + }, + "Value": true + }, + { + "Key": { + "id": "inver remask_above first corridor", + "sceneName": "Ruins2_11" + }, + "Value": true + }, + { + "Key": { + "id": "inver remask_below second corridor (2)", + "sceneName": "Ruins2_11" + }, + "Value": true + }, + { + "Key": { + "id": "Battle Scene", + "sceneName": "Ruins2_11" + }, + "Value": true + }, + { + "Key": { + "id": "Grub Bottle", + "sceneName": "Ruins_House_01" + }, + "Value": true + }, + { + "Key": { + "id": "Battle Scene", + "sceneName": "Ruins_House_01" + }, + "Value": true + }, + { + "Key": { + "id": "Shiny Item", + "sceneName": "Waterways_04b" + }, + "Value": false + }, + { + "Key": { + "id": "Flukeman (1)", + "sceneName": "Waterways_04b" + }, + "Value": true + }, + { + "Key": { + "id": "Secret Sound Region", + "sceneName": "Waterways_04b" + }, + "Value": true + }, + { + "Key": { + "id": "Flukeman", + "sceneName": "Waterways_04b" + }, + "Value": true + }, + { + "Key": { + "id": "Secret Mask", + "sceneName": "Waterways_04b" + }, + "Value": true + }, + { + "Key": { + "id": "Heart Piece", + "sceneName": "Waterways_04b" + }, + "Value": false + }, + { + "Key": { + "id": "Ceiling Dropper", + "sceneName": "Waterways_04b" + }, + "Value": true + }, + { + "Key": { + "id": "Ceiling Dropper (1)", + "sceneName": "Waterways_04b" + }, + "Value": true + }, + { + "Key": { + "id": "Flukeman", + "sceneName": "Waterways_08" + }, + "Value": true + }, + { + "Key": { + "id": "Flukeman (1)", + "sceneName": "Waterways_08" + }, + "Value": true + }, + { + "Key": { + "id": "Flukeman (2)", + "sceneName": "Waterways_08" + }, + "Value": true + }, + { + "Key": { + "id": "Flukeman (3)", + "sceneName": "Waterways_08" + }, + "Value": true + }, + { + "Key": { + "id": "Ceiling Dropper (2)", + "sceneName": "Waterways_08" + }, + "Value": true + }, + { + "Key": { + "id": "Flukeman (16)", + "sceneName": "Waterways_08" + }, + "Value": true + }, + { + "Key": { + "id": "Flukeman (5)", + "sceneName": "Waterways_08" + }, + "Value": true + }, + { + "Key": { + "id": "Flukeman (4)", + "sceneName": "Waterways_08" + }, + "Value": true + }, + { + "Key": { + "id": "Flukeman (13)", + "sceneName": "Waterways_08" + }, + "Value": true + }, + { + "Key": { + "id": "Flukeman (15)", + "sceneName": "Waterways_08" + }, + "Value": true + }, + { + "Key": { + "id": "Inverse Remasker", + "sceneName": "Waterways_08" + }, + "Value": true + }, + { + "Key": { + "id": "Breakable Wall Waterways", + "sceneName": "Waterways_08" + }, + "Value": true + }, + { + "Key": { + "id": "Break Floor 1", + "sceneName": "Waterways_08" + }, + "Value": true + }, + { + "Key": { + "id": "Secret Mask", + "sceneName": "Waterways_08" + }, + "Value": true + }, + { + "Key": { + "id": "Ceiling Dropper (3)", + "sceneName": "Waterways_08" + }, + "Value": true + }, + { + "Key": { + "id": "Ceiling Dropper", + "sceneName": "Waterways_08" + }, + "Value": true + }, + { + "Key": { + "id": "Ceiling Dropper (4)", + "sceneName": "Waterways_08" + }, + "Value": true + }, + { + "Key": { + "id": "Ceiling Dropper (1)", + "sceneName": "Waterways_08" + }, + "Value": true + }, + { + "Key": { + "id": "Shiny Item", + "sceneName": "Waterways_12" + }, + "Value": false + }, + { + "Key": { + "id": "Battle Scene", + "sceneName": "Waterways_09" + }, + "Value": true + }, + { + "Key": { + "id": "Ruins Lever", + "sceneName": "Waterways_09" + }, + "Value": true + }, + { + "Key": { + "id": "Collapser Small", + "sceneName": "Waterways_09" + }, + "Value": true + }, + { + "Key": { + "id": "Shiny", + "sceneName": "Waterways_09" + }, + "Value": true + }, + { + "Key": { + "id": "Shiny Item Ore", + "sceneName": "Crossroads_38" + }, + "Value": false + }, + { + "Key": { + "id": "Health Cocoon", + "sceneName": "Deepnest_East_15" + }, + "Value": true + }, + { + "Key": { + "id": "Super Spitter(Clone)", + "sceneName": "Deepnest_East_04" + }, + "Value": true + }, + { + "Key": { + "id": "Giant Hopper", + "sceneName": "Deepnest_East_08" + }, + "Value": true + }, + { + "Key": { + "id": "Ceiling Dropper", + "sceneName": "Deepnest_East_08" + }, + "Value": true + }, + { + "Key": { + "id": "Ceiling Dropper (3)", + "sceneName": "Deepnest_East_08" + }, + "Value": true + }, + { + "Key": { + "id": "Shiny Item", + "sceneName": "Deepnest_East_08" + }, + "Value": false + }, + { + "Key": { + "id": "Breakable Wall", + "sceneName": "Deepnest_East_08" + }, + "Value": true + }, + { + "Key": { + "id": "Ceiling Dropper (2)", + "sceneName": "Deepnest_East_08" + }, + "Value": true + }, + { + "Key": { + "id": "Ceiling Dropper (1)", + "sceneName": "Deepnest_East_08" + }, + "Value": true + }, + { + "Key": { + "id": "Ceiling Dropper (4)", + "sceneName": "Deepnest_East_09" + }, + "Value": true + }, + { + "Key": { + "id": "Ceiling Dropper (5)", + "sceneName": "Deepnest_East_09" + }, + "Value": true + }, + { + "Key": { + "id": "Ceiling Dropper (6)", + "sceneName": "Deepnest_East_09" + }, + "Value": true + }, + { + "Key": { + "id": "Secret Mask", + "sceneName": "Deepnest_East_09" + }, + "Value": true + }, + { + "Key": { + "id": "One Way Wall", + "sceneName": "Deepnest_East_09" + }, + "Value": true + }, + { + "Key": { + "id": "Ceiling Dropper (3)", + "sceneName": "Deepnest_East_09" + }, + "Value": true + }, + { + "Key": { + "id": "Ceiling Dropper", + "sceneName": "Deepnest_East_09" + }, + "Value": true + }, + { + "Key": { + "id": "Ceiling Dropper (2)", + "sceneName": "Deepnest_East_09" + }, + "Value": true + }, + { + "Key": { + "id": "Ceiling Dropper (1)", + "sceneName": "Deepnest_East_09" + }, + "Value": true + }, + { + "Key": { + "id": "Inverse Remasker", + "sceneName": "Room_Colosseum_02" + }, + "Value": true + }, + { + "Key": { + "id": "Breakable Wall_Silhouette", + "sceneName": "Room_Colosseum_02" + }, + "Value": true + }, + { + "Key": { + "id": "Remasker", + "sceneName": "Room_Colosseum_02" + }, + "Value": true + }, + { + "Key": { + "id": "Bursting Bouncer(Clone)", + "sceneName": "Room_Colosseum_Bronze" + }, + "Value": true + }, + { + "Key": { + "id": "Col_Glow_Remasker", + "sceneName": "Room_Colosseum_Bronze" + }, + "Value": true + }, + { + "Key": { + "id": "Shiny Item", + "sceneName": "Room_Colosseum_Bronze" + }, + "Value": false + }, + { + "Key": { + "id": "Royal Zombie 1", + "sceneName": "Ruins2_05" + }, + "Value": true + }, + { + "Key": { + "id": "Royal Zombie Fat", + "sceneName": "Ruins2_05" + }, + "Value": true + }, + { + "Key": { + "id": "Royal Zombie Coward (1)", + "sceneName": "Ruins2_05" + }, + "Value": true + }, + { + "Key": { + "id": "Ruins Sentry 1", + "sceneName": "Ruins2_05" + }, + "Value": true + }, + { + "Key": { + "id": "Ruins Sentry 1 (2)", + "sceneName": "Ruins2_05" + }, + "Value": true + }, + { + "Key": { + "id": "Ruins Sentry Fat", + "sceneName": "Ruins2_05" + }, + "Value": true + }, + { + "Key": { + "id": "Ruins Sentry 1 (1)", + "sceneName": "Ruins2_05" + }, + "Value": true + }, + { + "Key": { + "id": "Ruins Flying Sentry", + "sceneName": "Ruins2_05" + }, + "Value": true + }, + { + "Key": { + "id": "Royal Zombie Fat (1)", + "sceneName": "Ruins2_05" + }, + "Value": true + }, + { + "Key": { + "id": "Royal Zombie Coward (2)", + "sceneName": "Ruins2_05" + }, + "Value": true + }, + { + "Key": { + "id": "Ruins Sentry Fat (1)", + "sceneName": "Ruins2_05" + }, + "Value": true + }, + { + "Key": { + "id": "Royal Zombie Coward (3)", + "sceneName": "Ruins2_05" + }, + "Value": true + }, + { + "Key": { + "id": "Royal Zombie 1 (1)", + "sceneName": "Ruins2_05" + }, + "Value": true + }, + { + "Key": { + "id": "Ruins Sentry 1 (3)", + "sceneName": "Ruins2_05" + }, + "Value": true + }, + { + "Key": { + "id": "Royal Zombie Fat (2)", + "sceneName": "Ruins2_05" + }, + "Value": true + }, + { + "Key": { + "id": "Royal Zombie 1 (2)", + "sceneName": "Ruins2_05" + }, + "Value": true + }, + { + "Key": { + "id": "Shiny Item", + "sceneName": "Ruins2_05" + }, + "Value": false + }, + { + "Key": { + "id": "Royal Zombie 1", + "sceneName": "Ruins2_09" + }, + "Value": true + }, + { + "Key": { + "id": "Royal Zombie Coward", + "sceneName": "Ruins2_09" + }, + "Value": true + }, + { + "Key": { + "id": "Royal Zombie Fat", + "sceneName": "Ruins2_09" + }, + "Value": true + }, + { + "Key": { + "id": "Battle Scene", + "sceneName": "Ruins2_09" + }, + "Value": true + }, + { + "Key": { + "id": "Vessel Fragment", + "sceneName": "Ruins2_09" + }, + "Value": false + }, + { + "Key": { + "id": "secret sound", + "sceneName": "Fungus2_34" + }, + "Value": true + }, + { + "Key": { + "id": "Remasker", + "sceneName": "Fungus2_34" + }, + "Value": true + }, + { + "Key": { + "id": "Shiny Item", + "sceneName": "Fungus2_34" + }, + "Value": false + }, + { + "Key": { + "id": "Remasker (1)", + "sceneName": "Fungus2_34" + }, + "Value": true + }, + { + "Key": { + "id": "Jellyfish 1", + "sceneName": "Fungus3_01" + }, + "Value": true + }, + { + "Key": { + "id": "Jellyfish 4", + "sceneName": "Fungus3_01" + }, + "Value": true + }, + { + "Key": { + "id": "Jellyfish 3", + "sceneName": "Fungus3_01" + }, + "Value": true + }, + { + "Key": { + "id": "Jellyfish", + "sceneName": "Fungus3_01" + }, + "Value": true + }, + { + "Key": { + "id": "Jellyfish 6", + "sceneName": "Fungus3_01" + }, + "Value": true + }, + { + "Key": { + "id": "Jellyfish 2", + "sceneName": "Fungus3_01" + }, + "Value": true + }, + { + "Key": { + "id": "Shiny Item", + "sceneName": "Fungus3_30" + }, + "Value": false + }, + { + "Key": { + "id": "Health Cocoon", + "sceneName": "Fungus3_30" + }, + "Value": true + }, + { + "Key": { + "id": "Acid Walker", + "sceneName": "Fungus1_12" + }, + "Value": true + }, + { + "Key": { + "id": "Secret Mask", + "sceneName": "Fungus1_12" + }, + "Value": true + }, + { + "Key": { + "id": "Secret Mask sounder", + "sceneName": "Fungus1_12" + }, + "Value": true + }, + { + "Key": { + "id": "Breakable Wall", + "sceneName": "Abyss_03_c" + }, + "Value": true + }, + { + "Key": { + "id": "Zombie Hive", + "sceneName": "Hive_01" + }, + "Value": true + }, + { + "Key": { + "id": "Hive Breakable Pillar", + "sceneName": "Hive_01" + }, + "Value": true + }, + { + "Key": { + "id": "Hive Bench", + "sceneName": "Hive_01" + }, + "Value": true + }, + { + "Key": { + "id": "Bee Stinger", + "sceneName": "Hive_01" + }, + "Value": true + }, + { + "Key": { + "id": "Hive Breakable Pillar (1)", + "sceneName": "Hive_01" + }, + "Value": true + }, + { + "Key": { + "id": "Dream Plant Orb (1)", + "sceneName": "Hive_02" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (12)", + "sceneName": "Hive_02" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (18)", + "sceneName": "Hive_02" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (22)", + "sceneName": "Hive_02" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (11)", + "sceneName": "Hive_02" + }, + "Value": false + }, + { + "Key": { + "id": "Bee Stinger (1)", + "sceneName": "Hive_02" + }, + "Value": true + }, + { + "Key": { + "id": "Hive Breakable Pillar (2)", + "sceneName": "Hive_02" + }, + "Value": true + }, + { + "Key": { + "id": "Dream Plant Orb (6)", + "sceneName": "Hive_02" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (16)", + "sceneName": "Hive_02" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (8)", + "sceneName": "Hive_02" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (15)", + "sceneName": "Hive_02" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (13)", + "sceneName": "Hive_02" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (9)", + "sceneName": "Hive_02" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (19)", + "sceneName": "Hive_02" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (17)", + "sceneName": "Hive_02" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (14)", + "sceneName": "Hive_02" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (4)", + "sceneName": "Hive_02" + }, + "Value": false + }, + { + "Key": { + "id": "Bee Stinger (3)", + "sceneName": "Hive_02" + }, + "Value": true + }, + { + "Key": { + "id": "Dream Plant Orb (20)", + "sceneName": "Hive_02" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant", + "sceneName": "Hive_02" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (23)", + "sceneName": "Hive_02" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (21)", + "sceneName": "Hive_02" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (2)", + "sceneName": "Hive_02" + }, + "Value": false + }, + { + "Key": { + "id": "Bee Stinger (2)", + "sceneName": "Hive_02" + }, + "Value": true + }, + { + "Key": { + "id": "Dream Plant Orb (7)", + "sceneName": "Hive_02" + }, + "Value": false + }, + { + "Key": { + "id": "Zombie Hive (1)", + "sceneName": "Hive_02" + }, + "Value": true + }, + { + "Key": { + "id": "Zombie Hive (2)", + "sceneName": "Hive_02" + }, + "Value": true + }, + { + "Key": { + "id": "Zombie Hive (3)", + "sceneName": "Hive_02" + }, + "Value": true + }, + { + "Key": { + "id": "Bee Stinger (4)", + "sceneName": "Hive_03_c" + }, + "Value": true + }, + { + "Key": { + "id": "Bee Stinger (5)", + "sceneName": "Hive_03_c" + }, + "Value": true + }, + { + "Key": { + "id": "Break Floor 1", + "sceneName": "Hive_03_c" + }, + "Value": true + }, + { + "Key": { + "id": "Hive Breakable Pillar (5)", + "sceneName": "Hive_03_c" + }, + "Value": true + }, + { + "Key": { + "id": "Big Bee (1)", + "sceneName": "Hive_03_c" + }, + "Value": true + }, + { + "Key": { + "id": "Bee Stinger (6)", + "sceneName": "Hive_03_c" + }, + "Value": true + }, + { + "Key": { + "id": "Big Bee", + "sceneName": "Hive_03_c" + }, + "Value": true + }, + { + "Key": { + "id": "Secret Mask", + "sceneName": "Hive_03_c" + }, + "Value": true + }, + { + "Key": { + "id": "Secret Mask", + "sceneName": "Deepnest_East_01" + }, + "Value": true + }, + { + "Key": { + "id": "One Way Wall", + "sceneName": "Deepnest_East_01" + }, + "Value": true + }, + { + "Key": { + "id": "Blow Fly (1)", + "sceneName": "Deepnest_East_01" + }, + "Value": true + }, + { + "Key": { + "id": "Blow Fly (4)", + "sceneName": "Deepnest_East_01" + }, + "Value": true + }, + { + "Key": { + "id": "Blow Fly (2)", + "sceneName": "Deepnest_East_01" + }, + "Value": true + }, + { + "Key": { + "id": "Blow Fly (5)", + "sceneName": "Deepnest_East_01" + }, + "Value": true + }, + { + "Key": { + "id": "Blow Fly (3)", + "sceneName": "Deepnest_East_01" + }, + "Value": true + }, + { + "Key": { + "id": "Blow Fly", + "sceneName": "Deepnest_East_01" + }, + "Value": true + }, + { + "Key": { + "id": "Bee Stinger (7)", + "sceneName": "Hive_03" + }, + "Value": true + }, + { + "Key": { + "id": "Big Bee (2)", + "sceneName": "Hive_03" + }, + "Value": true + }, + { + "Key": { + "id": "Bee Stinger (8)", + "sceneName": "Hive_03" + }, + "Value": true + }, + { + "Key": { + "id": "Grub Bottle", + "sceneName": "Hive_03" + }, + "Value": true + }, + { + "Key": { + "id": "Remasker", + "sceneName": "Hive_03" + }, + "Value": true + }, + { + "Key": { + "id": "Bee Stinger (10)", + "sceneName": "Hive_04" + }, + "Value": true + }, + { + "Key": { + "id": "Zombie Hive (4)", + "sceneName": "Hive_04" + }, + "Value": true + }, + { + "Key": { + "id": "Bee Stinger (9)", + "sceneName": "Hive_04" + }, + "Value": true + }, + { + "Key": { + "id": "Bee Stinger (11)", + "sceneName": "Hive_04" + }, + "Value": true + }, + { + "Key": { + "id": "Zombie Hive (6)", + "sceneName": "Hive_04" + }, + "Value": true + }, + { + "Key": { + "id": "Hive Break Wall", + "sceneName": "Hive_04" + }, + "Value": true + }, + { + "Key": { + "id": "Grub Bottle", + "sceneName": "Hive_04" + }, + "Value": true + }, + { + "Key": { + "id": "Hive Breakable Pillar (4)", + "sceneName": "Hive_04" + }, + "Value": true + }, + { + "Key": { + "id": "Big Bee (3)", + "sceneName": "Hive_04" + }, + "Value": true + }, + { + "Key": { + "id": "Hive Breakable Pillar (3)", + "sceneName": "Hive_04" + }, + "Value": true + }, + { + "Key": { + "id": "Hive Breakable Pillar (5)", + "sceneName": "Hive_04" + }, + "Value": true + }, + { + "Key": { + "id": "Heart Piece", + "sceneName": "Hive_04" + }, + "Value": false + }, + { + "Key": { + "id": "Big Bee (4)", + "sceneName": "Hive_04" + }, + "Value": true + }, + { + "Key": { + "id": "Bee Stinger (12)", + "sceneName": "Hive_04" + }, + "Value": true + }, + { + "Key": { + "id": "Hive Breakable Pillar", + "sceneName": "Hive_05" + }, + "Value": true + }, + { + "Key": { + "id": "Hive Breakable Pillar (1)", + "sceneName": "Hive_05" + }, + "Value": true + }, + { + "Key": { + "id": "Hive Breakable Pillar (2)", + "sceneName": "Hive_05" + }, + "Value": true + }, + { + "Key": { + "id": "Shiny Item Stand", + "sceneName": "Hive_05" + }, + "Value": false + }, + { + "Key": { + "id": "Vespa NPC", + "sceneName": "Hive_05" + }, + "Value": true + }, + { + "Key": { + "id": "Quake Floor", + "sceneName": "Deepnest_East_02" + }, + "Value": true + }, + { + "Key": { + "id": "One Way Wall", + "sceneName": "Deepnest_East_02" + }, + "Value": true + }, + { + "Key": { + "id": "Super Spitter(Clone)", + "sceneName": "Room_Colosseum_Silver" + }, + "Value": true + }, + { + "Key": { + "id": "Bursting Bouncer(Clone)", + "sceneName": "Room_Colosseum_Silver" + }, + "Value": true + }, + { + "Key": { + "id": "Col_Glow_Remasker", + "sceneName": "Room_Colosseum_Silver" + }, + "Value": true + }, + { + "Key": { + "id": "Giant Hopper(Clone)", + "sceneName": "Room_Colosseum_Silver" + }, + "Value": true + }, + { + "Key": { + "id": "Shiny Item", + "sceneName": "Room_Colosseum_Silver" + }, + "Value": false + }, + { + "Key": { + "id": "Shiny Item Relic3", + "sceneName": "Crossroads_38" + }, + "Value": false + }, + { + "Key": { + "id": "Flamebearer Spawn", + "sceneName": "Mines_10" + }, + "Value": true + }, + { + "Key": { + "id": "Flamebearer Spawn", + "sceneName": "Ruins1_28" + }, + "Value": true + }, + { + "Key": { + "id": "Flamebearer Spawn", + "sceneName": "Fungus1_10" + }, + "Value": true + }, + { + "Key": { + "id": "Flamebearer Spawn", + "sceneName": "Tutorial_01" + }, + "Value": true + }, + { + "Key": { + "id": "Flamebearer Spawn", + "sceneName": "RestingGrounds_06" + }, + "Value": true + }, + { + "Key": { + "id": "Flamebearer Spawn", + "sceneName": "Deepnest_East_03" + }, + "Value": true + }, + { + "Key": { + "id": "Grimm Chest", + "sceneName": "Grimm_Main_Tent" + }, + "Value": true + }, + { + "Key": { + "id": "Shiny Item", + "sceneName": "Grimm_Main_Tent" + }, + "Value": false + }, + { + "Key": { + "id": "Royal Gaurd", + "sceneName": "White_Palace_11" + }, + "Value": true + }, + { + "Key": { + "id": "White Palace Orb Lever", + "sceneName": "White_Palace_02" + }, + "Value": true + }, + { + "Key": { + "id": "Collapser Small", + "sceneName": "White_Palace_02" + }, + "Value": true + }, + { + "Key": { + "id": "Battle Scene", + "sceneName": "White_Palace_02" + }, + "Value": true + }, + { + "Key": { + "id": "WP Lever", + "sceneName": "White_Palace_03_hub" + }, + "Value": true + }, + { + "Key": { + "id": "WP Lever", + "sceneName": "White_Palace_15" + }, + "Value": true + }, + { + "Key": { + "id": "White Palace Orb Lever", + "sceneName": "White_Palace_15" + }, + "Value": true + }, + { + "Key": { + "id": "Quake Floor", + "sceneName": "White_Palace_15" + }, + "Value": true + }, + { + "Key": { + "id": "White Palace Orb Lever", + "sceneName": "White_Palace_14" + }, + "Value": true + }, + { + "Key": { + "id": "Breakable Wall Ruin Lift", + "sceneName": "White_Palace_06" + }, + "Value": true + }, + { + "Key": { + "id": "Secret Sound Region", + "sceneName": "White_Palace_18" + }, + "Value": true + }, + { + "Key": { + "id": "Remasker", + "sceneName": "White_Palace_18" + }, + "Value": true + }, + { + "Key": { + "id": "Breakable Wall Waterways", + "sceneName": "White_Palace_12" + }, + "Value": true + }, + { + "Key": { + "id": "WP Lever", + "sceneName": "White_Palace_12" + }, + "Value": true + }, + { + "Key": { + "id": "Break Floor 1", + "sceneName": "White_Palace_12" + }, + "Value": true + }, + { + "Key": { + "id": "Breakable Wall Waterways", + "sceneName": "White_Palace_09" + }, + "Value": true + }, + { + "Key": { + "id": "Quake Floor", + "sceneName": "White_Palace_09" + }, + "Value": true + }, + { + "Key": { + "id": "Shiny Item RoyalCharm", + "sceneName": "White_Palace_09" + }, + "Value": false + }, + { + "Key": { + "id": "Shiny Item", + "sceneName": "Abyss_15" + }, + "Value": false + }, + { + "Key": { + "id": "Secret Mask", + "sceneName": "Dream_Final" + }, + "Value": true + }, + { + "Key": { + "id": "Shiny Item(Clone)", + "sceneName": "Mines_20" + }, + "Value": false + }, + { + "Key": { + "id": "Zombie Beam Miner (3)", + "sceneName": "Mines_23" + }, + "Value": true + }, + { + "Key": { + "id": "Dream Plant Orb (7)", + "sceneName": "Mines_23" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (16)", + "sceneName": "Mines_23" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb", + "sceneName": "Mines_23" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (18)", + "sceneName": "Mines_23" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant", + "sceneName": "Mines_23" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (8)", + "sceneName": "Mines_23" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (11)", + "sceneName": "Mines_23" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (10)", + "sceneName": "Mines_23" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (1)", + "sceneName": "Mines_23" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (3)", + "sceneName": "Mines_23" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (13)", + "sceneName": "Mines_23" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (20)", + "sceneName": "Mines_23" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (4)", + "sceneName": "Mines_23" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (6)", + "sceneName": "Mines_23" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (2)", + "sceneName": "Mines_23" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (17)", + "sceneName": "Mines_23" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (12)", + "sceneName": "Mines_23" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (14)", + "sceneName": "Mines_23" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (5)", + "sceneName": "Mines_23" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (9)", + "sceneName": "Mines_23" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (15)", + "sceneName": "Mines_23" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (19)", + "sceneName": "Mines_23" + }, + "Value": false + }, + { + "Key": { + "id": "Crystal Flyer (2)", + "sceneName": "Mines_23" + }, + "Value": true + }, + { + "Key": { + "id": "Zombie Beam Miner (1)", + "sceneName": "Mines_23" + }, + "Value": true + }, + { + "Key": { + "id": "Zombie Beam Miner (2)", + "sceneName": "Mines_23" + }, + "Value": true + }, + { + "Key": { + "id": "Zombie Beam Miner (4)", + "sceneName": "Mines_23" + }, + "Value": true + }, + { + "Key": { + "id": "Crystal Flyer", + "sceneName": "Mines_23" + }, + "Value": true + }, + { + "Key": { + "id": "Crystal Flyer (1)", + "sceneName": "Mines_23" + }, + "Value": true + }, + { + "Key": { + "id": "Zombie Beam Miner", + "sceneName": "Mines_23" + }, + "Value": true + }, + { + "Key": { + "id": "Zombie Beam Miner", + "sceneName": "Mines_24" + }, + "Value": true + }, + { + "Key": { + "id": "Grub Bottle", + "sceneName": "Mines_24" + }, + "Value": true + }, + { + "Key": { + "id": "Zombie Beam Miner", + "sceneName": "Mines_25" + }, + "Value": true + }, + { + "Key": { + "id": "Crystal Flyer", + "sceneName": "Mines_25" + }, + "Value": true + }, + { + "Key": { + "id": "Zombie Beam Miner (2)", + "sceneName": "Mines_25" + }, + "Value": true + }, + { + "Key": { + "id": "Crystal Flyer (2)", + "sceneName": "Mines_25" + }, + "Value": true + }, + { + "Key": { + "id": "Zombie Beam Miner (1)", + "sceneName": "Mines_25" + }, + "Value": true + }, + { + "Key": { + "id": "Crystal Flyer (1)", + "sceneName": "Mines_25" + }, + "Value": true + }, + { + "Key": { + "id": "Secret Mask", + "sceneName": "Mines_25" + }, + "Value": true + }, + { + "Key": { + "id": "Breakable Wall", + "sceneName": "Mines_25" + }, + "Value": true + }, + { + "Key": { + "id": "One Way Wall", + "sceneName": "Mines_25" + }, + "Value": true + }, + { + "Key": { + "id": "Quake Floor", + "sceneName": "Mines_25" + }, + "Value": true + }, + { + "Key": { + "id": "Zombie Beam Miner (3)", + "sceneName": "Mines_25" + }, + "Value": true + }, + { + "Key": { + "id": "Crystal Flyer", + "sceneName": "Mines_34" + }, + "Value": true + }, + { + "Key": { + "id": "Shiny Item Stand", + "sceneName": "Mines_34" + }, + "Value": false + }, + { + "Key": { + "id": "Remasker", + "sceneName": "Mines_34" + }, + "Value": true + }, + { + "Key": { + "id": "Dream Plant Orb (7)", + "sceneName": "Fungus2_17" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (16)", + "sceneName": "Fungus2_17" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (1)", + "sceneName": "Fungus2_17" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (13)", + "sceneName": "Fungus2_17" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (17)", + "sceneName": "Fungus2_17" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (15)", + "sceneName": "Fungus2_17" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (6)", + "sceneName": "Fungus2_17" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (3)", + "sceneName": "Fungus2_17" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (5)", + "sceneName": "Fungus2_17" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (14)", + "sceneName": "Fungus2_17" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (12)", + "sceneName": "Fungus2_17" + }, + "Value": false + }, + { + "Key": { + "id": "Shiny Item", + "sceneName": "Fungus2_17" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb", + "sceneName": "Fungus2_17" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (8)", + "sceneName": "Fungus2_17" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (10)", + "sceneName": "Fungus2_17" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (4)", + "sceneName": "Fungus2_17" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (9)", + "sceneName": "Fungus2_17" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (2)", + "sceneName": "Fungus2_17" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant", + "sceneName": "Fungus2_17" + }, + "Value": false + }, + { + "Key": { + "id": "Dream Plant Orb (11)", + "sceneName": "Fungus2_17" + }, + "Value": false + }, + { + "Key": { + "id": "Mushroom Roller", + "sceneName": "Fungus2_29" + }, + "Value": true + }, + { + "Key": { + "id": "Mushroom Roller (1)", + "sceneName": "Fungus2_29" + }, + "Value": true + }, + { + "Key": { + "id": "Fungus Flyer (1)", + "sceneName": "Fungus2_29" + }, + "Value": true + }, + { + "Key": { + "id": "Fungus Flyer", + "sceneName": "Fungus2_29" + }, + "Value": true + }, + { + "Key": { + "id": "Mushroom Roller (2)", + "sceneName": "Fungus2_29" + }, + "Value": true + }, + { + "Key": { + "id": "Mushroom Roller (3)", + "sceneName": "Fungus2_29" + }, + "Value": true + }, + { + "Key": { + "id": "Mushroom Brawler (1)", + "sceneName": "Fungus2_29" + }, + "Value": true + }, + { + "Key": { + "id": "Mushroom Brawler", + "sceneName": "Fungus2_29" + }, + "Value": true + }, + { + "Key": { + "id": "Secret Mask (2)", + "sceneName": "Fungus2_29" + }, + "Value": true + }, + { + "Key": { + "id": "Secret Mask", + "sceneName": "Fungus2_29" + }, + "Value": true + }, + { + "Key": { + "id": "Shiny Item", + "sceneName": "Fungus2_29" + }, + "Value": false + }, + { + "Key": { + "id": "Break Floor 1", + "sceneName": "Fungus2_29" + }, + "Value": true + }, + { + "Key": { + "id": "Secret Mask (1)", + "sceneName": "Fungus2_29" + }, + "Value": true + }, + { + "Key": { + "id": "Fungus Flyer (1)", + "sceneName": "Fungus2_30" + }, + "Value": true + }, + { + "Key": { + "id": "Mushroom Roller", + "sceneName": "Fungus2_30" + }, + "Value": true + }, + { + "Key": { + "id": "Mushroom Brawler (1)", + "sceneName": "Fungus2_30" + }, + "Value": true + }, + { + "Key": { + "id": "Mushroom Brawler", + "sceneName": "Fungus2_30" + }, + "Value": true + }, + { + "Key": { + "id": "Secret Mask", + "sceneName": "Fungus2_30" + }, + "Value": true + }, + { + "Key": { + "id": "Mushroom Roller (3)", + "sceneName": "Fungus2_30" + }, + "Value": true + }, + { + "Key": { + "id": "Flamebearer Spawn", + "sceneName": "Fungus2_30" + }, + "Value": true + }, + { + "Key": { + "id": "Mushroom Roller (2)", + "sceneName": "Fungus2_30" + }, + "Value": true + }, + { + "Key": { + "id": "Flamebearer Spawn", + "sceneName": "Abyss_02" + }, + "Value": true + }, + { + "Key": { + "id": "Angry Buzzer", + "sceneName": "Crossroads_12" + }, + "Value": true + }, + { + "Key": { + "id": "Hatcher (1)", + "sceneName": "Crossroads_35" + }, + "Value": true + }, + { + "Key": { + "id": "Heart Piece", + "sceneName": "Room_Mansion" + }, + "Value": false + }, + { + "Key": { + "id": "Super Spitter(Clone)", + "sceneName": "Deepnest_East_11" + }, + "Value": true + }, + { + "Key": { + "id": "Quake Floor (4)", + "sceneName": "Deepnest_East_17" + }, + "Value": true + }, + { + "Key": { + "id": "Quake Floor (2)", + "sceneName": "Deepnest_East_17" + }, + "Value": true + }, + { + "Key": { + "id": "Quake Floor (5)", + "sceneName": "Deepnest_East_17" + }, + "Value": true + }, + { + "Key": { + "id": "Secret Mask", + "sceneName": "Deepnest_East_17" + }, + "Value": true + }, + { + "Key": { + "id": "Quake Floor (10)", + "sceneName": "Deepnest_East_17" + }, + "Value": true + }, + { + "Key": { + "id": "Quake Floor", + "sceneName": "Deepnest_East_17" + }, + "Value": true + }, + { + "Key": { + "id": "Quake Floor (6)", + "sceneName": "Deepnest_East_17" + }, + "Value": true + }, + { + "Key": { + "id": "Quake Floor (3)", + "sceneName": "Deepnest_East_17" + }, + "Value": true + }, + { + "Key": { + "id": "Quake Floor (8)", + "sceneName": "Deepnest_East_17" + }, + "Value": true + }, + { + "Key": { + "id": "Quake Floor (9)", + "sceneName": "Deepnest_East_17" + }, + "Value": true + }, + { + "Key": { + "id": "Quake Floor (7)", + "sceneName": "Deepnest_East_17" + }, + "Value": true + }, + { + "Key": { + "id": "Quake Floor (1)", + "sceneName": "Deepnest_East_17" + }, + "Value": true + }, + { + "Key": { + "id": "Shiny Item(Clone)", + "sceneName": "Deepnest_East_14" + }, + "Value": false + }, + { + "Key": { + "id": "Grub Bottle", + "sceneName": "Waterways_14" + }, + "Value": true + }, + { + "Key": { + "id": "Collapser Small", + "sceneName": "Waterways_14" + }, + "Value": true + }, + { + "Key": { + "id": "Remasker", + "sceneName": "Waterways_14" + }, + "Value": true + }, + { + "Key": { + "id": "Remasker (1)", + "sceneName": "Waterways_15" + }, + "Value": true + }, + { + "Key": { + "id": "Remasker", + "sceneName": "Waterways_15" + }, + "Value": true + }, + { + "Key": { + "id": "Shiny Item Stand", + "sceneName": "Waterways_15" + }, + "Value": false + }, + { + "Key": { + "id": "Remasker (2)", + "sceneName": "Waterways_15" + }, + "Value": true + }, + { + "Key": { + "id": "Secret Sound Region", + "sceneName": "Waterways_15" + }, + "Value": true + }, + { + "Key": { + "id": "Secret Mask", + "sceneName": "Deepnest_42" + }, + "Value": true + }, + { + "Key": { + "id": "Plank Solid 1 (1)", + "sceneName": "Deepnest_42" + }, + "Value": true + }, + { + "Key": { + "id": "Plank Solid 1 (2)", + "sceneName": "Deepnest_42" + }, + "Value": true + }, + { + "Key": { + "id": "Plank Solid 1", + "sceneName": "Deepnest_42" + }, + "Value": true + }, + { + "Key": { + "id": "Secret Mask", + "sceneName": "Deepnest_02" + }, + "Value": true + }, + { + "Key": { + "id": "Breakable Wall", + "sceneName": "Deepnest_02" + }, + "Value": true + }, + { + "Key": { + "id": "Collapser Small", + "sceneName": "Deepnest_02" + }, + "Value": true + }, + { + "Key": { + "id": "Grub Mimic 2", + "sceneName": "Deepnest_36" + }, + "Value": true + }, + { + "Key": { + "id": "Grub Mimic 1", + "sceneName": "Deepnest_36" + }, + "Value": true + }, + { + "Key": { + "id": "Grub Mimic 3", + "sceneName": "Deepnest_36" + }, + "Value": true + }, + { + "Key": { + "id": "Grub Mimic Bottle (1)", + "sceneName": "Deepnest_36" + }, + "Value": true + }, + { + "Key": { + "id": "Grub Mimic Bottle (2)", + "sceneName": "Deepnest_36" + }, + "Value": true + }, + { + "Key": { + "id": "Grub Bottle", + "sceneName": "Deepnest_36" + }, + "Value": true + }, + { + "Key": { + "id": "Grub Mimic Bottle", + "sceneName": "Deepnest_36" + }, + "Value": true + }, + { + "Key": { + "id": "Secret Sound Region", + "sceneName": "Ruins_Elevator" + }, + "Value": true + }, + { + "Key": { + "id": "Ghost NPC", + "sceneName": "Ruins_Elevator" + }, + "Value": true + }, + { + "Key": { + "id": "Shiny Item", + "sceneName": "Ruins_Elevator" + }, + "Value": false + }, + { + "Key": { + "id": "Remasker (2)", + "sceneName": "Ruins_Elevator" + }, + "Value": true + }, + { + "Key": { + "id": "One Way Wall", + "sceneName": "Ruins_Elevator" + }, + "Value": true + }, + { + "Key": { + "id": "Secret Sound Region (1)", + "sceneName": "Ruins_Elevator" + }, + "Value": true + }, + { + "Key": { + "id": "Remasker", + "sceneName": "Ruins_Elevator" + }, + "Value": true + }, + { + "Key": { + "id": "Remasker (3)", + "sceneName": "Ruins_Elevator" + }, + "Value": true + }, + { + "Key": { + "id": "Remasker (1)", + "sceneName": "Ruins_Elevator" + }, + "Value": true + }, + { + "Key": { + "id": "Shiny Item (1)", + "sceneName": "Ruins_Elevator" + }, + "Value": false + }, + { + "Key": { + "id": "Ghost NPC", + "sceneName": "Ruins_Bathhouse" + }, + "Value": true + }, + { + "Key": { + "id": "Remasker", + "sceneName": "Ruins_Bathhouse" + }, + "Value": true + }, + { + "Key": { + "id": "Breakable Wall", + "sceneName": "Ruins_Bathhouse" + }, + "Value": true + }, + { + "Key": { + "id": "Breakable Wall_Silhouette", + "sceneName": "Room_Colosseum_Spectate" + }, + "Value": true + }, + { + "Key": { + "id": "Secret Mask (1)", + "sceneName": "GG_Lurker" + }, + "Value": true + }, + { + "Key": { + "id": "Secret Mask", + "sceneName": "GG_Lurker" + }, + "Value": true + }, + { + "Key": { + "id": "Shiny Item", + "sceneName": "GG_Lurker" + }, + "Value": false + }, + { + "Key": { + "id": "Col_Glow_Remasker", + "sceneName": "Room_Colosseum_Gold" + }, + "Value": true + }, + { + "Key": { + "id": "Mage (1)", + "sceneName": "Room_Colosseum_Gold" + }, + "Value": true + }, + { + "Key": { + "id": "Angry Buzzer(Clone)", + "sceneName": "Room_Colosseum_Gold" + }, + "Value": true + }, + { + "Key": { + "id": "Mantis Heavy(Clone)", + "sceneName": "Room_Colosseum_Gold" + }, + "Value": true + }, + { + "Key": { + "id": "Mantis Heavy Flyer(Clone)", + "sceneName": "Room_Colosseum_Gold" + }, + "Value": true + }, + { + "Key": { + "id": "Lesser Mawlek(Clone)", + "sceneName": "Room_Colosseum_Gold" + }, + "Value": true + }, + { + "Key": { + "id": "Poo Strength", + "sceneName": "Grimm_Divine" + }, + "Value": false + }, + { + "Key": { + "id": "Toll Gate Machine (1)", + "sceneName": "Mines_33" + }, + "Value": true + }, + { + "Key": { + "id": "One Way Wall", + "sceneName": "Mines_33" + }, + "Value": true + }, + { + "Key": { + "id": "Toll Gate Machine", + "sceneName": "Mines_33" + }, + "Value": true + }, + { + "Key": { + "id": "Secret Mask", + "sceneName": "Mines_33" + }, + "Value": true + }, + { + "Key": { + "id": "gramaphone", + "sceneName": "Room_Tram_RG" + }, + "Value": true + }, + { + "Key": { + "id": "gramaphone (1)", + "sceneName": "Room_Tram_RG" + }, + "Value": true + }, + { + "Key": { + "id": "Flukeman", + "sceneName": "GG_Pipeway" + }, + "Value": true + }, + { + "Key": { + "id": "Fat Fluke", + "sceneName": "GG_Pipeway" + }, + "Value": true + }, + { + "Key": { + "id": "Flukeman (1)", + "sceneName": "GG_Pipeway" + }, + "Value": true + }, + { + "Key": { + "id": "Fat Fluke (1)", + "sceneName": "GG_Pipeway" + }, + "Value": true + }, + { + "Key": { + "id": "Collapser Small", + "sceneName": "GG_Pipeway" + }, + "Value": true + }, + { + "Key": { + "id": "Fat Fluke (3)", + "sceneName": "GG_Pipeway" + }, + "Value": true + }, + { + "Key": { + "id": "Fat Fluke (2)", + "sceneName": "GG_Pipeway" + }, + "Value": true + }, + { + "Key": { + "id": "Quake Floor", + "sceneName": "GG_Pipeway" + }, + "Value": true + }, + { + "Key": { + "id": "Remasker (1)", + "sceneName": "GG_Waterways" + }, + "Value": true + }, + { + "Key": { + "id": "Chest (1)", + "sceneName": "GG_Waterways" + }, + "Value": true + }, + { + "Key": { + "id": "Chest (3)", + "sceneName": "GG_Waterways" + }, + "Value": true + }, + { + "Key": { + "id": "Remasker", + "sceneName": "GG_Waterways" + }, + "Value": true + }, + { + "Key": { + "id": "Chest", + "sceneName": "GG_Waterways" + }, + "Value": true + }, + { + "Key": { + "id": "Chest (4)", + "sceneName": "GG_Waterways" + }, + "Value": true + }, + { + "Key": { + "id": "Chest (2)", + "sceneName": "GG_Waterways" + }, + "Value": true + }, + { + "Key": { + "id": "Shiny Item Godfinder", + "sceneName": "GG_Waterways" + }, + "Value": false + }, + { + "Key": { + "id": "Remasker", + "sceneName": "Room_GG_Shortcut" + }, + "Value": true + }, + { + "Key": { + "id": "Secret Sound Region", + "sceneName": "Room_GG_Shortcut" + }, + "Value": true + }, + { + "Key": { + "id": "Breakable Grate", + "sceneName": "Room_GG_Shortcut" + }, + "Value": true + }, + { + "Key": { + "id": "Col_Glow_Remasker", + "sceneName": "GG_Atrium" + }, + "Value": true + }, + { + "Key": { + "id": "Zote_Break_wall", + "sceneName": "GG_Workshop" + }, + "Value": true + }, + { + "Key": { + "id": "Breakable Wall_Silhouette", + "sceneName": "GG_Workshop" + }, + "Value": true + }, + { + "Key": { + "id": "Col_Glow_Remasker", + "sceneName": "GG_Workshop" + }, + "Value": true + }, + { + "Key": { + "id": "Radiance Statue Cage", + "sceneName": "GG_Workshop" + }, + "Value": true + }, + { + "Key": { + "id": "Knight Statue Cage", + "sceneName": "GG_Workshop" + }, + "Value": true + }, + { + "Key": { + "id": "Collapser Small", + "sceneName": "White_Palace_17" + }, + "Value": true + }, + { + "Key": { + "id": "Secret Mask", + "sceneName": "White_Palace_17" + }, + "Value": true + }, + { + "Key": { + "id": "WP Lever", + "sceneName": "White_Palace_17" + }, + "Value": true + }, + { + "Key": { + "id": "Secret Sound Region", + "sceneName": "White_Palace_19" + }, + "Value": true + }, + { + "Key": { + "id": "Inverse Remasker (1)", + "sceneName": "White_Palace_19" + }, + "Value": true + }, + { + "Key": { + "id": "Inverse Remasker", + "sceneName": "White_Palace_20" + }, + "Value": true + }, + { + "Key": { + "id": "Secret Mask", + "sceneName": "GG_Atrium_Roof" + }, + "Value": true + }, + { + "Key": { + "id": "Remasker", + "sceneName": "GG_Atrium_Roof" + }, + "Value": true + }, + { + "Key": { + "id": "Breakable Wall_Silhouette", + "sceneName": "GG_Atrium_Roof" + }, + "Value": true + }, + { + "Key": { + "id": "Col_Glow_Remasker", + "sceneName": "GG_Atrium_Roof" + }, + "Value": true + }, + { + "Key": { + "id": "GG Fall Platform", + "sceneName": "GG_Atrium_Roof" + }, + "Value": true + }, + { + "Key": { + "id": "gg_roof_lever", + "sceneName": "GG_Atrium_Roof" + }, + "Value": true + }, + { + "Key": { + "id": "Shiny Item", + "sceneName": "GG_False_Knight" + }, + "Value": false + }, + { + "Key": { + "id": "Col_Glow_Remasker", + "sceneName": "GG_Spa" + }, + "Value": true + }, + { + "Key": { + "id": "Col_Glow_Remasker", + "sceneName": "GG_Engine" + }, + "Value": true + }, + { + "Key": { + "id": "Col_Glow_Remasker", + "sceneName": "GG_Engine_Prime" + }, + "Value": true + }, + { + "Key": { + "id": "white_scene_glow", + "sceneName": "GG_Hollow_Knight" + }, + "Value": true + }, + { + "Key": { + "id": "white_scene_glow (1)", + "sceneName": "GG_Hollow_Knight" + }, + "Value": true + }, + { + "Key": { + "id": "gg_roof_lever", + "sceneName": "GG_Atrium" + }, + "Value": true + }, + { + "Key": { + "id": "Col_Glow_Remasker", + "sceneName": "GG_Unn" + }, + "Value": true + }, + { + "Key": { + "id": "Col_Glow_Remasker", + "sceneName": "GG_Engine_Root" + }, + "Value": true + }, + { + "Key": { + "id": "Col_Glow_Remasker", + "sceneName": "GG_Wyrm" + }, + "Value": true + }, + { + "Key": { + "id": "Plaque_statue_03 (46)", + "sceneName": "Dream_Room_Believer_Shrine" + }, + "Value": true + }, + { + "Key": { + "id": "Plaque_statue_03 (18)", + "sceneName": "Dream_Room_Believer_Shrine" + }, + "Value": true + }, + { + "Key": { + "id": "Plaque_statue_03 (7)", + "sceneName": "Dream_Room_Believer_Shrine" + }, + "Value": true + }, + { + "Key": { + "id": "Plaque_statue_03 (23)", + "sceneName": "Dream_Room_Believer_Shrine" + }, + "Value": true + }, + { + "Key": { + "id": "Plaque_statue_03 (27)", + "sceneName": "Dream_Room_Believer_Shrine" + }, + "Value": true + }, + { + "Key": { + "id": "Plaque_statue_03 (36)", + "sceneName": "Dream_Room_Believer_Shrine" + }, + "Value": true + }, + { + "Key": { + "id": "Plaque_statue_03 (41)", + "sceneName": "Dream_Room_Believer_Shrine" + }, + "Value": true + }, + { + "Key": { + "id": "Plaque_statue_02 (5)", + "sceneName": "Dream_Room_Believer_Shrine" + }, + "Value": true + }, + { + "Key": { + "id": "Plaque_statue_03 (1)", + "sceneName": "Dream_Room_Believer_Shrine" + }, + "Value": true + }, + { + "Key": { + "id": "Plaque_statue_03 (10)", + "sceneName": "Dream_Room_Believer_Shrine" + }, + "Value": true + }, + { + "Key": { + "id": "Plaque_statue_01 (3)", + "sceneName": "Dream_Room_Believer_Shrine" + }, + "Value": true + }, + { + "Key": { + "id": "Plaque_statue_03 (25)", + "sceneName": "Dream_Room_Believer_Shrine" + }, + "Value": true + }, + { + "Key": { + "id": "Plaque_statue_02 (6)", + "sceneName": "Dream_Room_Believer_Shrine" + }, + "Value": true + }, + { + "Key": { + "id": "Plaque_statue_03 (15)", + "sceneName": "Dream_Room_Believer_Shrine" + }, + "Value": true + }, + { + "Key": { + "id": "Plaque_statue_03 (5)", + "sceneName": "Dream_Room_Believer_Shrine" + }, + "Value": true + }, + { + "Key": { + "id": "Plaque_statue_03 (40)", + "sceneName": "Dream_Room_Believer_Shrine" + }, + "Value": true + }, + { + "Key": { + "id": "Plaque_statue_03 (42)", + "sceneName": "Dream_Room_Believer_Shrine" + }, + "Value": true + }, + { + "Key": { + "id": "Plaque_statue_03 (17)", + "sceneName": "Dream_Room_Believer_Shrine" + }, + "Value": true + }, + { + "Key": { + "id": "Plaque_statue_02 (9)", + "sceneName": "Dream_Room_Believer_Shrine" + }, + "Value": true + }, + { + "Key": { + "id": "Plaque_statue_03 (20)", + "sceneName": "Dream_Room_Believer_Shrine" + }, + "Value": true + }, + { + "Key": { + "id": "Plaque_statue_01 (1)", + "sceneName": "Dream_Room_Believer_Shrine" + }, + "Value": true + }, + { + "Key": { + "id": "Plaque_statue_02", + "sceneName": "Dream_Room_Believer_Shrine" + }, + "Value": true + }, + { + "Key": { + "id": "Plaque_statue_03 (6)", + "sceneName": "Dream_Room_Believer_Shrine" + }, + "Value": true + }, + { + "Key": { + "id": "Plaque_statue_03 (26)", + "sceneName": "Dream_Room_Believer_Shrine" + }, + "Value": true + }, + { + "Key": { + "id": "Plaque_statue_03 (3)", + "sceneName": "Dream_Room_Believer_Shrine" + }, + "Value": true + }, + { + "Key": { + "id": "Plaque_statue_03 (16)", + "sceneName": "Dream_Room_Believer_Shrine" + }, + "Value": true + }, + { + "Key": { + "id": "Plaque_statue_03 (4)", + "sceneName": "Dream_Room_Believer_Shrine" + }, + "Value": true + }, + { + "Key": { + "id": "Plaque_statue_03 (43)", + "sceneName": "Dream_Room_Believer_Shrine" + }, + "Value": true + }, + { + "Key": { + "id": "Plaque_statue_03 (21)", + "sceneName": "Dream_Room_Believer_Shrine" + }, + "Value": true + }, + { + "Key": { + "id": "Plaque_statue_03 (34)", + "sceneName": "Dream_Room_Believer_Shrine" + }, + "Value": true + }, + { + "Key": { + "id": "Plaque_statue_02 (8)", + "sceneName": "Dream_Room_Believer_Shrine" + }, + "Value": true + }, + { + "Key": { + "id": "Plaque_statue_02 (10)", + "sceneName": "Dream_Room_Believer_Shrine" + }, + "Value": true + }, + { + "Key": { + "id": "Plaque_statue_03 (11)", + "sceneName": "Dream_Room_Believer_Shrine" + }, + "Value": true + }, + { + "Key": { + "id": "Plaque_statue_02 (2)", + "sceneName": "Dream_Room_Believer_Shrine" + }, + "Value": true + }, + { + "Key": { + "id": "Plaque_statue_02 (3)", + "sceneName": "Dream_Room_Believer_Shrine" + }, + "Value": true + }, + { + "Key": { + "id": "Plaque_statue_03 (2)", + "sceneName": "Dream_Room_Believer_Shrine" + }, + "Value": true + }, + { + "Key": { + "id": "Plaque_statue_03 (32)", + "sceneName": "Dream_Room_Believer_Shrine" + }, + "Value": true + }, + { + "Key": { + "id": "Plaque_statue_03 (28)", + "sceneName": "Dream_Room_Believer_Shrine" + }, + "Value": true + }, + { + "Key": { + "id": "Plaque_statue_03", + "sceneName": "Dream_Room_Believer_Shrine" + }, + "Value": true + }, + { + "Key": { + "id": "Plaque_statue_03 (9)", + "sceneName": "Dream_Room_Believer_Shrine" + }, + "Value": true + }, + { + "Key": { + "id": "Plaque_statue_03 (22)", + "sceneName": "Dream_Room_Believer_Shrine" + }, + "Value": true + }, + { + "Key": { + "id": "Plaque_statue_02 (4)", + "sceneName": "Dream_Room_Believer_Shrine" + }, + "Value": true + }, + { + "Key": { + "id": "Plaque_statue_03 (38)", + "sceneName": "Dream_Room_Believer_Shrine" + }, + "Value": true + }, + { + "Key": { + "id": "Plaque_statue_03 (45)", + "sceneName": "Dream_Room_Believer_Shrine" + }, + "Value": true + }, + { + "Key": { + "id": "Plaque_statue_03 (13)", + "sceneName": "Dream_Room_Believer_Shrine" + }, + "Value": true + }, + { + "Key": { + "id": "Plaque_statue_01 (2)", + "sceneName": "Dream_Room_Believer_Shrine" + }, + "Value": true + }, + { + "Key": { + "id": "Plaque_statue_03 (24)", + "sceneName": "Dream_Room_Believer_Shrine" + }, + "Value": true + }, + { + "Key": { + "id": "Plaque_statue_02 (1)", + "sceneName": "Dream_Room_Believer_Shrine" + }, + "Value": true + }, + { + "Key": { + "id": "Plaque_statue_03 (30)", + "sceneName": "Dream_Room_Believer_Shrine" + }, + "Value": true + }, + { + "Key": { + "id": "Plaque_statue_03 (14)", + "sceneName": "Dream_Room_Believer_Shrine" + }, + "Value": true + }, + { + "Key": { + "id": "Plaque_statue_01 (4)", + "sceneName": "Dream_Room_Believer_Shrine" + }, + "Value": true + }, + { + "Key": { + "id": "Plaque_statue_03 (29)", + "sceneName": "Dream_Room_Believer_Shrine" + }, + "Value": true + }, + { + "Key": { + "id": "Plaque_statue_03 (8)", + "sceneName": "Dream_Room_Believer_Shrine" + }, + "Value": true + }, + { + "Key": { + "id": "Plaque_statue_03 (31)", + "sceneName": "Dream_Room_Believer_Shrine" + }, + "Value": true + }, + { + "Key": { + "id": "Plaque_statue_02 (7)", + "sceneName": "Dream_Room_Believer_Shrine" + }, + "Value": true + }, + { + "Key": { + "id": "Plaque_statue_03 (33)", + "sceneName": "Dream_Room_Believer_Shrine" + }, + "Value": true + }, + { + "Key": { + "id": "Plaque_statue_02 (11)", + "sceneName": "Dream_Room_Believer_Shrine" + }, + "Value": true + }, + { + "Key": { + "id": "Plaque_statue_03 (37)", + "sceneName": "Dream_Room_Believer_Shrine" + }, + "Value": true + }, + { + "Key": { + "id": "Plaque_statue_03 (35)", + "sceneName": "Dream_Room_Believer_Shrine" + }, + "Value": true + }, + { + "Key": { + "id": "Plaque_statue_03 (19)", + "sceneName": "Dream_Room_Believer_Shrine" + }, + "Value": true + }, + { + "Key": { + "id": "Plaque_statue_03 (12)", + "sceneName": "Dream_Room_Believer_Shrine" + }, + "Value": true + }, + { + "Key": { + "id": "Plaque_statue_03 (39)", + "sceneName": "Dream_Room_Believer_Shrine" + }, + "Value": true + }, + { + "Key": { + "id": "Plaque_statue_03 (44)", + "sceneName": "Dream_Room_Believer_Shrine" + }, + "Value": true + }, + { + "Key": { + "id": "Ruins Lever", + "sceneName": "Ruins_House_03" + }, + "Value": true + }, + { + "Key": { + "id": "Inverse Remasker (1)", + "sceneName": "Ruins_House_03" + }, + "Value": true + }, + { + "Key": { + "id": "Inverse Remasker", + "sceneName": "Ruins_House_03" + }, + "Value": true + }, + { + "Key": { + "id": "Poo Heart", + "sceneName": "Grimm_Divine" + }, + "Value": false + }, + { + "Key": { + "id": "Shiny Item HunterMark", + "sceneName": "Fungus1_08" + }, + "Value": false + }, + { + "Key": { + "id": "Angry Buzzer (1)", + "sceneName": "Crossroads_05" + }, + "Value": true + }, + { + "Key": { + "id": "Angry Buzzer", + "sceneName": "Crossroads_05" + }, + "Value": true + }, + { + "Key": { + "id": "Bursting Zombie (1)", + "sceneName": "Crossroads_05" + }, + "Value": true + }, + { + "Key": { + "id": "Bursting Zombie", + "sceneName": "Crossroads_05" + }, + "Value": true + }, + { + "Key": { + "id": "Poo Greed", + "sceneName": "Grimm_Divine" + }, + "Value": false + }, + { + "Key": { + "id": "Health Cocoon (1)", + "sceneName": "GG_Spa" + }, + "Value": true + }, + { + "Key": { + "id": "Shiny Item(Clone)", + "sceneName": "Waterways_02" + }, + "Value": false + }, + { + "Key": { + "id": "Shiny Item GG Storms", + "sceneName": "GG_Land_of_Storms" + }, + "Value": false + }, + { + "Key": { + "id": "Shiny Item(Clone)", + "sceneName": "Deepnest_39" + }, + "Value": false + }, + { + "Key": { + "id": "Shiny Item(Clone)", + "sceneName": "Waterways_04" + }, + "Value": false + }, + { + "Key": { + "id": "Shiny", + "sceneName": "Fungus3_25" + }, + "Value": true + } + ], + "persistentIntItems": [ + { + "Key": { + "id": "Soul Totem mini_two_horned", + "sceneName": "Crossroads_19" + }, + "Value": true + }, + { + "Key": { + "id": "Soul Totem 2", + "sceneName": "Crossroads_ShamanTemple" + }, + "Value": true + }, + { + "Key": { + "id": "Soul Totem mini_horned", + "sceneName": "Crossroads_18" + }, + "Value": true + }, + { + "Key": { + "id": "Soul Totem mini_horned", + "sceneName": "Fungus2_10" + }, + "Value": true + }, + { + "Key": { + "id": "Soul Totem 5", + "sceneName": "Fungus2_21" + }, + "Value": true + }, + { + "Key": { + "id": "Ruins Lift 1", + "sceneName": "Ruins1_02" + }, + "Value": true + }, + { + "Key": { + "id": "Ruins Lift 1", + "sceneName": "Ruins1_03" + }, + "Value": true + }, + { + "Key": { + "id": "Ruins Lift 3", + "sceneName": "Ruins1_05c" + }, + "Value": true + }, + { + "Key": { + "id": "Ruins Lift 2", + "sceneName": "Ruins1_05b" + }, + "Value": true + }, + { + "Key": { + "id": "Ruins Lift 1", + "sceneName": "Ruins1_05" + }, + "Value": true + }, + { + "Key": { + "id": "Ruins Lift", + "sceneName": "Ruins1_31" + }, + "Value": true + }, + { + "Key": { + "id": "Ruins Lift 2", + "sceneName": "Ruins1_23" + }, + "Value": true + }, + { + "Key": { + "id": "Ruins Lift 1", + "sceneName": "Ruins1_23" + }, + "Value": true + }, + { + "Key": { + "id": "Ruins Lift", + "sceneName": "Ruins1_25" + }, + "Value": true + }, + { + "Key": { + "id": "Soul Totem 1", + "sceneName": "Ruins1_24" + }, + "Value": true + }, + { + "Key": { + "id": "Soul Totem 3", + "sceneName": "Ruins1_32" + }, + "Value": true + }, + { + "Key": { + "id": "Soul Totem 5", + "sceneName": "Crossroads_45" + }, + "Value": true + }, + { + "Key": { + "id": "Soul Totem 5", + "sceneName": "Mines_20" + }, + "Value": true + }, + { + "Key": { + "id": "Soul Totem mini_horned", + "sceneName": "Mines_31" + }, + "Value": true + }, + { + "Key": { + "id": "Soul Totem mini_two_horned", + "sceneName": "Mines_31" + }, + "Value": true + }, + { + "Key": { + "id": "Soul Totem 5", + "sceneName": "Mines_28" + }, + "Value": true + }, + { + "Key": { + "id": "Soul Totem mini_horned", + "sceneName": "Mines_35" + }, + "Value": true + }, + { + "Key": { + "id": "Soul Totem 4", + "sceneName": "RestingGrounds_05" + }, + "Value": true + }, + { + "Key": { + "id": "Soul Totem mini_horned", + "sceneName": "Crossroads_35" + }, + "Value": true + }, + { + "Key": { + "id": "Soul Totem mini_horned", + "sceneName": "Waterways_01" + }, + "Value": true + }, + { + "Key": { + "id": "Soul Totem mini_horned", + "sceneName": "Waterways_07" + }, + "Value": true + }, + { + "Key": { + "id": "Ruins Lift (1)", + "sceneName": "Ruins2_01_b" + }, + "Value": true + }, + { + "Key": { + "id": "Ruins Lift", + "sceneName": "Ruins2_01_b" + }, + "Value": true + }, + { + "Key": { + "id": "Ruins Lift", + "sceneName": "Ruins2_03b" + }, + "Value": true + }, + { + "Key": { + "id": "Ruins Lift (1)", + "sceneName": "Ruins2_03" + }, + "Value": true + }, + { + "Key": { + "id": "Ruins Lift", + "sceneName": "Ruins2_Watcher_Room" + }, + "Value": true + }, + { + "Key": { + "id": "Soul Totem 1", + "sceneName": "Deepnest_10" + }, + "Value": true + }, + { + "Key": { + "id": "Soul Totem 5", + "sceneName": "Deepnest_Spider_Town" + }, + "Value": true + }, + { + "Key": { + "id": "Soul Totem 5", + "sceneName": "Cliffs_02" + }, + "Value": true + }, + { + "Key": { + "id": "Soul Totem mini_two_horned", + "sceneName": "Cliffs_01" + }, + "Value": true + }, + { + "Key": { + "id": "Soul Totem mini_horned", + "sceneName": "Cliffs_04" + }, + "Value": true + }, + { + "Key": { + "id": "Soul Totem mini_horned", + "sceneName": "Fungus1_30" + }, + "Value": true + }, + { + "Key": { + "id": "Soul Totem mini_horned", + "sceneName": "Fungus1_07" + }, + "Value": true + }, + { + "Key": { + "id": "Soul Totem mini_horned", + "sceneName": "Abyss_04" + }, + "Value": true + }, + { + "Key": { + "id": "Soul Totem mini_two_horned", + "sceneName": "RestingGrounds_06" + }, + "Value": true + }, + { + "Key": { + "id": "Soul Totem 5", + "sceneName": "Deepnest_East_16" + }, + "Value": true + }, + { + "Key": { + "id": "Soul Totem mini_two_horned", + "sceneName": "Deepnest_East_14" + }, + "Value": true + }, + { + "Key": { + "id": "Soul Totem mini_horned", + "sceneName": "Deepnest_East_14" + }, + "Value": true + }, + { + "Key": { + "id": "Soul Totem 5", + "sceneName": "Deepnest_East_11" + }, + "Value": true + }, + { + "Key": { + "id": "Soul Totem mini_two_horned", + "sceneName": "Crossroads_25" + }, + "Value": true + }, + { + "Key": { + "id": "Soul Totem 4", + "sceneName": "Crossroads_36" + }, + "Value": true + }, + { + "Key": { + "id": "Soul Totem 4", + "sceneName": "Deepnest_38" + }, + "Value": true + }, + { + "Key": { + "id": "Soul Totem mini_two_horned", + "sceneName": "Fungus3_40" + }, + "Value": true + }, + { + "Key": { + "id": "Soul Totem mini_horned", + "sceneName": "Fungus3_21" + }, + "Value": true + }, + { + "Key": { + "id": "Soul Totem mini_two_horned", + "sceneName": "Deepnest_East_07" + }, + "Value": true + }, + { + "Key": { + "id": "Soul Totem mini_two_horned", + "sceneName": "Waterways_04b" + }, + "Value": true + }, + { + "Key": { + "id": "Soul Totem mini_two_horned", + "sceneName": "Waterways_08" + }, + "Value": true + }, + { + "Key": { + "id": "Ruins Lift", + "sceneName": "Ruins2_05" + }, + "Value": true + }, + { + "Key": { + "id": "Ruins Lift (1)", + "sceneName": "Ruins2_05" + }, + "Value": true + }, + { + "Key": { + "id": "Soul Totem mini_horned", + "sceneName": "Fungus1_29" + }, + "Value": true + }, + { + "Key": { + "id": "Soul Totem mini_two_horned", + "sceneName": "Deepnest_East_01" + }, + "Value": true + }, + { + "Key": { + "id": "Soul Totem mini_horned", + "sceneName": "Deepnest_East_02" + }, + "Value": true + }, + { + "Key": { + "id": "Soul Totem white", + "sceneName": "White_Palace_02" + }, + "Value": true + }, + { + "Key": { + "id": "Soul Totem white", + "sceneName": "White_Palace_03_hub" + }, + "Value": true + }, + { + "Key": { + "id": "Soul Totem white", + "sceneName": "White_Palace_15" + }, + "Value": true + }, + { + "Key": { + "id": "Soul Totem white", + "sceneName": "White_Palace_04" + }, + "Value": true + }, + { + "Key": { + "id": "Soul Totem white", + "sceneName": "White_Palace_09" + }, + "Value": true + }, + { + "Key": { + "id": "Soul Totem mini_two_horned", + "sceneName": "Mines_25" + }, + "Value": true + }, + { + "Key": { + "id": "Soul Totem mini_horned", + "sceneName": "Fungus2_29" + }, + "Value": true + }, + { + "Key": { + "id": "Soul Totem mini_two_horned", + "sceneName": "Deepnest_East_17" + }, + "Value": true + }, + { + "Key": { + "id": "Soul Totem mini_two_horned", + "sceneName": "Deepnest_42" + }, + "Value": true + }, + { + "Key": { + "id": "Ruins Lift", + "sceneName": "Ruins_Elevator" + }, + "Value": true + }, + { + "Key": { + "id": "Soul Totem 3", + "sceneName": "GG_Lurker" + }, + "Value": true + } + ] } \ No newline at end of file From 479d8f3f75a4c276550ae2eeeee6486d198c5de6 Mon Sep 17 00:00:00 2001 From: Extremelyd1 Date: Fri, 22 Dec 2023 16:23:07 +0100 Subject: [PATCH 076/216] Fix `FlingObjectsFromGlobalPoolTime` action networking and improve entities --- .../Client/Entity/Action/EntityFsmActions.cs | 32 ++--- HKMP/Game/Client/Entity/Entity.cs | 118 +++++++++--------- HKMP/Resource/action-registry.json | 3 + 3 files changed, 77 insertions(+), 76 deletions(-) diff --git a/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs b/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs index 178627ac..39be81b6 100644 --- a/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs +++ b/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs @@ -2701,28 +2701,10 @@ IEnumerator Behaviour() { #region FlingObjectsFromGlobalPoolTime private static bool GetNetworkDataFromAction(EntityNetworkData data, FlingObjectsFromGlobalPoolTime action) { - data.Packet.Write(action.angleMin.Value); - data.Packet.Write(action.angleMax.Value); - return true; } private static void ApplyNetworkDataFromAction(EntityNetworkData data, FlingObjectsFromGlobalPoolTime action) { - var angleMin = data.Packet.ReadFloat(); - var angleMax = data.Packet.ReadFloat(); - - var position = Vector3.zero; - - var spawnPoint = action.spawnPoint.Value; - if (spawnPoint != null) { - position = spawnPoint.transform.position; - if (!action.position.IsNone) { - position += action.position.Value; - } - } else if (!action.position.IsNone) { - position = action.position.Value; - } - var coroutine = MonoBehaviourUtil.Instance.StartCoroutine(Behaviour()); new ActionInState { @@ -2739,6 +2721,18 @@ IEnumerator Behaviour() { break; } + var position = Vector3.zero; + + var spawnPoint = action.spawnPoint.Value; + if (spawnPoint != null) { + position = spawnPoint.transform.position; + if (!action.position.IsNone) { + position += action.position.Value; + } + } else if (!action.position.IsNone) { + position = action.position.Value; + } + var numSpawns = Random.Range(action.spawnMin.Value, action.spawnMax.Value + 1); for (var i = 0; i < numSpawns; i++) { var gameObject = action.gameObject.Value.Spawn(position, Quaternion.Euler(Vector3.zero)); @@ -2759,7 +2753,7 @@ IEnumerator Behaviour() { } var speed = Random.Range(action.speedMin.Value, action.speedMax.Value); - var angle = Random.Range(angleMin, angleMax); + var angle = Random.Range(action.angleMin.Value, action.angleMax.Value); var x = speed * Mathf.Cos(angle * ((float) System.Math.PI / 180f)); var y = speed * Mathf.Sin(angle * ((float) System.Math.PI / 180f)); diff --git a/HKMP/Game/Client/Entity/Entity.cs b/HKMP/Game/Client/Entity/Entity.cs index d5781724..07a92800 100644 --- a/HKMP/Game/Client/Entity/Entity.cs +++ b/HKMP/Game/Client/Entity/Entity.cs @@ -1149,11 +1149,11 @@ public void UpdateHostFsmData(Dictionary hostFsmData) { continue; } - var fsm = _fsms.Host[fsmIndex]; + var hostFsm = _fsms.Host[fsmIndex]; var snapshot = _fsmSnapshots[fsmIndex]; if (data.Types.Contains(EntityHostFsmData.Type.State)) { - var states = fsm.FsmStates; + var states = hostFsm.FsmStates; if (states.Length <= data.CurrentState) { Logger.Warn($"Tried to update host FSM state for unknown state index: {data.CurrentState}"); } else { @@ -1167,6 +1167,8 @@ public void UpdateHostFsmData(Dictionary hostFsmData) { } } + var fsms = new[] { hostFsm, _fsms.Client[fsmIndex] }; + void CondUpdateVars( EntityHostFsmData.Type type, Dictionary dataDict, @@ -1183,61 +1185,63 @@ Action setValueAction } } } - - CondUpdateVars( - EntityHostFsmData.Type.Floats, - data.Floats, - fsm.FsmVariables.FloatVariables, - (index, fsmVar, value) => { - fsmVar.Value = value; - snapshot.Floats[index] = value; - } - ); - CondUpdateVars( - EntityHostFsmData.Type.Ints, - data.Ints, - fsm.FsmVariables.IntVariables, - (index, fsmVar, value) => { - fsmVar.Value = value; - snapshot.Ints[index] = value; - } - ); - CondUpdateVars( - EntityHostFsmData.Type.Bools, - data.Bools, - fsm.FsmVariables.BoolVariables, - (index, fsmVar, value) => { - fsmVar.Value = value; - snapshot.Bools[index] = value; - } - ); - CondUpdateVars( - EntityHostFsmData.Type.Strings, - data.Strings, - fsm.FsmVariables.StringVariables, - (index, fsmVar, value) => { - fsmVar.Value = value; - snapshot.Strings[index] = value; - } - ); - CondUpdateVars( - EntityHostFsmData.Type.Vector2s, - data.Vec2s, - fsm.FsmVariables.Vector2Variables, - (index, fsmVar, value) => { - fsmVar.Value = (UnityEngine.Vector2) value; - snapshot.Vector2s[index] = (UnityEngine.Vector2) value; - } - ); - CondUpdateVars( - EntityHostFsmData.Type.Vector3s, - data.Vec3s, - fsm.FsmVariables.Vector3Variables, - (index, fsmVar, value) => { - fsmVar.Value = (Vector3) value; - snapshot.Vector3s[index] = (Vector3) value; - } - ); + + foreach (var fsm in fsms) { + CondUpdateVars( + EntityHostFsmData.Type.Floats, + data.Floats, + fsm.FsmVariables.FloatVariables, + (index, fsmVar, value) => { + fsmVar.Value = value; + snapshot.Floats[index] = value; + } + ); + CondUpdateVars( + EntityHostFsmData.Type.Ints, + data.Ints, + fsm.FsmVariables.IntVariables, + (index, fsmVar, value) => { + fsmVar.Value = value; + snapshot.Ints[index] = value; + } + ); + CondUpdateVars( + EntityHostFsmData.Type.Bools, + data.Bools, + fsm.FsmVariables.BoolVariables, + (index, fsmVar, value) => { + fsmVar.Value = value; + snapshot.Bools[index] = value; + } + ); + CondUpdateVars( + EntityHostFsmData.Type.Strings, + data.Strings, + fsm.FsmVariables.StringVariables, + (index, fsmVar, value) => { + fsmVar.Value = value; + snapshot.Strings[index] = value; + } + ); + CondUpdateVars( + EntityHostFsmData.Type.Vector2s, + data.Vec2s, + fsm.FsmVariables.Vector2Variables, + (index, fsmVar, value) => { + fsmVar.Value = (UnityEngine.Vector2) value; + snapshot.Vector2s[index] = (UnityEngine.Vector2) value; + } + ); + CondUpdateVars( + EntityHostFsmData.Type.Vector3s, + data.Vec3s, + fsm.FsmVariables.Vector3Variables, + (index, fsmVar, value) => { + fsmVar.Value = (Vector3) value; + snapshot.Vector3s[index] = (Vector3) value; + } + ); + } } } diff --git a/HKMP/Resource/action-registry.json b/HKMP/Resource/action-registry.json index 6c4a4088..5fa10dbd 100644 --- a/HKMP/Resource/action-registry.json +++ b/HKMP/Resource/action-registry.json @@ -252,5 +252,8 @@ }, { "type": "ChaseObjectGround" + }, + { + "type": "FlingObjectsFromGlobalPoolTime" } ] From af1fdd8441a56aabfe623459e3720745d6c8f7f3 Mon Sep 17 00:00:00 2001 From: Extremelyd1 Date: Mon, 25 Dec 2023 13:59:24 +0100 Subject: [PATCH 077/216] Improvements to Radiance --- .../Client/Entity/Action/EntityFsmActions.cs | 41 +++++++++++++++++ HKMP/Game/Client/Entity/EntitySpawner.cs | 46 +++++++++++++++++++ HKMP/Game/Client/Entity/EntityType.cs | 3 ++ HKMP/Resource/entity-registry.json | 21 +++++++++ 4 files changed, 111 insertions(+) diff --git a/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs b/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs index 39be81b6..080e2610 100644 --- a/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs +++ b/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs @@ -875,6 +875,47 @@ private static void ApplyNetworkDataFromAction(EntityNetworkData data, SetFsmBoo fsmBool.Value = setValue; } + #endregion + + #region SetFsmInt + + private static bool GetNetworkDataFromAction(EntityNetworkData data, SetFsmInt action) { + if (action.setValue == null) { + return false; + } + + var gameObject = action.Fsm.GetOwnerDefaultTarget(action.gameObject); + if (gameObject == action.Fsm.GameObject) { + return false; + } + + var setValue = action.setValue.Value; + data.Packet.Write(setValue); + + return true; + } + + private static void ApplyNetworkDataFromAction(EntityNetworkData data, SetFsmInt action) { + var setValue = data.Packet.ReadInt(); + + var gameObject = action.Fsm.GetOwnerDefaultTarget(action.gameObject); + if (gameObject == null) { + return; + } + + var fsm = ActionHelpers.GetGameObjectFsm(gameObject, action.fsmName.Value); + if (fsm == null) { + return; + } + + var fsmInt = fsm.FsmVariables.GetFsmInt(action.variableName.Value); + if (fsmInt == null) { + return; + } + + fsmInt.Value = setValue; + } + #endregion #region SetFsmFloat diff --git a/HKMP/Game/Client/Entity/EntitySpawner.cs b/HKMP/Game/Client/Entity/EntitySpawner.cs index e1bf1311..79fbd159 100644 --- a/HKMP/Game/Client/Entity/EntitySpawner.cs +++ b/HKMP/Game/Client/Entity/EntitySpawner.cs @@ -135,6 +135,24 @@ List clientFsms } } + if (spawningType is EntityType.Radiance or EntityType.AbsoluteRadiance) { + if (spawnedType == EntityType.RadianceOrb) { + return SpawnRadianceOrb(clientFsms[3]); + } + + if (spawnedType == EntityType.RadianceNail) { + return SpawnRadianceNail(clientFsms[3]); + } + + if (spawnedType == EntityType.RadianceNailComb) { + return SpawnRadianceNailComb(clientFsms[3]); + } + } + + if (spawningType == EntityType.RadianceNailComb && spawnedType == EntityType.RadianceNail) { + return SpawnRadianceNailFromComb(clientFsms[0]); + } + return null; } @@ -437,4 +455,32 @@ private static GameObject SpawnGrimmFirebatObject(PlayMakerFSM fsm) { return SpawnFromGlobalPool(action, gameObject); } + + private static GameObject SpawnRadianceOrb(PlayMakerFSM fsm) { + var action = fsm.GetFirstAction("Spawn Fireball"); + var gameObject = action.gameObject.Value; + + return SpawnFromGlobalPool(action, gameObject); + } + + private static GameObject SpawnRadianceNail(PlayMakerFSM fsm) { + var action = fsm.GetFirstAction("CW Spawn"); + var gameObject = action.gameObject.Value; + + return SpawnFromGlobalPool(action, gameObject); + } + + private static GameObject SpawnRadianceNailComb(PlayMakerFSM fsm) { + var action = fsm.GetFirstAction("Comb Top"); + var gameObject = action.gameObject.Value; + + return SpawnFromGlobalPool(action, gameObject); + } + + private static GameObject SpawnRadianceNailFromComb(PlayMakerFSM fsm) { + var action = fsm.GetFirstAction("RG1"); + var gameObject = action.gameObject.Value; + + return SpawnFromGlobalPool(action, gameObject); + } } diff --git a/HKMP/Game/Client/Entity/EntityType.cs b/HKMP/Game/Client/Entity/EntityType.cs index 122ba83b..365b8122 100644 --- a/HKMP/Game/Client/Entity/EntityType.cs +++ b/HKMP/Game/Client/Entity/EntityType.cs @@ -187,6 +187,9 @@ internal enum EntityType { HornetSentinelSpike, HollowKnight, Radiance, + RadianceOrb, + RadianceNailComb, + RadianceNail, GreyPrinceZote, Zoteling, VolatileZoteling, diff --git a/HKMP/Resource/entity-registry.json b/HKMP/Resource/entity-registry.json index b30a1866..df05bb07 100644 --- a/HKMP/Resource/entity-registry.json +++ b/HKMP/Resource/entity-registry.json @@ -1120,6 +1120,27 @@ "type": "Radiance", "fsm_name": "Control" }, + { + "base_object_name": "Radiant Orb", + "type": "RadianceOrb", + "fsm_name": "Orb Control" + }, + { + "base_object_name": "Radiant Nail Comb", + "type": "RadianceNailComb", + "fsm_name": "Control", + "components": [ + "Rotation" + ] + }, + { + "base_object_name": "Radiant Nail", + "type": "RadianceNail", + "fsm_name": "Control", + "components": [ + "Rotation" + ] + }, { "base_object_name": "Grey Prince", "type": "GreyPrinceZote", From f4be15d4b036327f4b13415d08596022c93f6df2 Mon Sep 17 00:00:00 2001 From: Extremelyd1 Date: Sun, 3 Mar 2024 21:17:40 +0100 Subject: [PATCH 078/216] Save data synchronisation on server host/join --- HKMP/Game/Client/ClientManager.cs | 11 +- .../Save/{SaveData.cs => SaveDataMapping.cs} | 2 +- HKMP/Game/Client/Save/SaveManager.cs | 417 ++++++++++++++---- HKMP/Game/Server/ModServerManager.cs | 6 +- HKMP/Game/Server/ServerManager.cs | 7 +- HKMP/Networking/Packet/Data/HelloClient.cs | 10 + HKMP/Networking/Packet/Data/SaveUpdate.cs | 62 +++ HKMP/Networking/Server/ServerUpdateManager.cs | 8 +- HKMP/Resource/save-data.json | 3 +- HKMP/Util/EncodeUtil.cs | 36 ++ 10 files changed, 471 insertions(+), 91 deletions(-) rename HKMP/Game/Client/Save/{SaveData.cs => SaveDataMapping.cs} (99%) create mode 100644 HKMP/Util/EncodeUtil.cs diff --git a/HKMP/Game/Client/ClientManager.cs b/HKMP/Game/Client/ClientManager.cs index 5f2089ab..eb0112e9 100644 --- a/HKMP/Game/Client/ClientManager.cs +++ b/HKMP/Game/Client/ClientManager.cs @@ -75,6 +75,11 @@ internal class ClientManager : IClientManager { /// private readonly EntityManager _entityManager; + /// + /// The save manager instance. + /// + private readonly SaveManager _saveManager; + /// /// The client addon manager instance. /// @@ -181,7 +186,9 @@ ModSettings modSettings _entityManager = new EntityManager(netClient); - new SaveManager(netClient, packetManager, _entityManager).Initialize(); + _saveManager = new SaveManager(netClient, packetManager, _entityManager); + _saveManager.Initialize(); + new PauseManager(netClient).RegisterHooks(); new FsmPatcher().RegisterHooks(); @@ -504,6 +511,8 @@ private void OnClientConnect(LoginResponse loginResponse) { private void OnHelloClient(HelloClient helloClient) { Logger.Info("Received HelloClient from server"); + _saveManager.SetSaveWithData(helloClient.CurrentSave); + // Fill the player data dictionary with the info from the packet foreach (var (id, username) in helloClient.ClientInfo) { _playerData[id] = new ClientPlayerData(id, username); diff --git a/HKMP/Game/Client/Save/SaveData.cs b/HKMP/Game/Client/Save/SaveDataMapping.cs similarity index 99% rename from HKMP/Game/Client/Save/SaveData.cs rename to HKMP/Game/Client/Save/SaveDataMapping.cs index 5b0565f4..559b1498 100644 --- a/HKMP/Game/Client/Save/SaveData.cs +++ b/HKMP/Game/Client/Save/SaveDataMapping.cs @@ -9,7 +9,7 @@ namespace Hkmp.Game.Client.Save; /// /// Serializable data class that stores mappings for what scene data should be synchronised and their indices used for networking. /// -internal class SaveData { +internal class SaveDataMapping { /// /// Dictionary mapping player data values to booleans indicating whether they should be synchronised. /// diff --git a/HKMP/Game/Client/Save/SaveManager.cs b/HKMP/Game/Client/Save/SaveManager.cs index 4dd3ab73..c91baed2 100644 --- a/HKMP/Game/Client/Save/SaveManager.cs +++ b/HKMP/Game/Client/Save/SaveManager.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Text; +using Hkmp.Collection; using Hkmp.Game.Client.Entity; using Hkmp.Networking.Client; using Hkmp.Networking.Packet; @@ -24,6 +25,11 @@ internal class SaveManager { /// private const string SaveDataFilePath = "Hkmp.Resource.save-data.json"; + /// + /// The save data instances that contains mappings for what to sync and their indices. + /// + private static SaveDataMapping _saveDataMapping; + /// /// The net client instance to send save updates. /// @@ -42,11 +48,6 @@ internal class SaveManager { /// private readonly List _persistentFsmData; - /// - /// The save data instances that contains mappings for what to sync and their indices. - /// - private SaveData _saveData; - public SaveManager(NetClient netClient, PacketManager packetManager, EntityManager entityManager) { _netClient = netClient; _packetManager = packetManager; @@ -55,13 +56,18 @@ public SaveManager(NetClient netClient, PacketManager packetManager, EntityManag _persistentFsmData = new List(); } + /// + /// Static constructor to load and initialize the save data mapping. + /// + static SaveManager() { + _saveDataMapping = FileUtil.LoadObjectFromEmbeddedJson(SaveDataFilePath); + _saveDataMapping.Initialize(); + } + /// /// Initializes the save manager by loading the save data json. /// public void Initialize() { - _saveData = FileUtil.LoadObjectFromEmbeddedJson(SaveDataFilePath); - _saveData.Initialize(); - ModHooks.SetPlayerBoolHook += OnSetPlayerBoolHook; ModHooks.SetPlayerFloatHook += OnSetPlayerFloatHook; ModHooks.SetPlayerIntHook += OnSetPlayerIntHook; @@ -75,15 +81,106 @@ public void Initialize() { _packetManager.RegisterClientPacketHandler(ClientPacketId.SaveUpdate, UpdateSaveWithData); } + /// + /// Encode a given value into a byte array in the context of save data. + /// + /// The value to encode. + /// A byte array containing the encoded value. + /// Thrown when the given value is out of range to be encoded. + /// + /// Thrown when the given value has a type that cannot be encoded due to + /// missing implementation. + private static byte[] EncodeValue(object value) { + byte[] EncodeString(string stringValue) { + var byteEncodedString = Encoding.UTF8.GetBytes(stringValue); + + if (byteEncodedString.Length > ushort.MaxValue) { + throw new ArgumentOutOfRangeException($"Could not encode string of length: {byteEncodedString.Length}"); + } + + return byteEncodedString; + } + + if (value is bool bValue) { + return [(byte) (bValue ? 1 : 0)]; + } + + if (value is float fValue) { + return BitConverter.GetBytes(fValue); + } + + if (value is int iValue) { + return BitConverter.GetBytes(iValue); + } + + if (value is string sValue) { + return EncodeString(sValue); + } + + if (value is Vector3 vecValue) { + return BitConverter.GetBytes(vecValue.x) + .Concat(BitConverter.GetBytes(vecValue.y)) + .Concat(BitConverter.GetBytes(vecValue.z)) + .ToArray(); + } + + if (value is List listValue) { + if (listValue.Count > ushort.MaxValue) { + throw new ArgumentOutOfRangeException($"Could not encode string list length: {listValue.Count}"); + } + + var length = (ushort) listValue.Count; + + IEnumerable byteArray = BitConverter.GetBytes(length); + + for (var i = 0; i < length; i++) { + var encoded = EncodeString(listValue[i]); + + if (encoded.Length > ushort.MaxValue) { + throw new ArgumentOutOfRangeException($"Could not encode string in list of length: {encoded.Length}"); + } + + var encodedLen = (ushort) encoded.Length; + + byteArray = byteArray.Concat(BitConverter.GetBytes(encodedLen)).Concat(encoded); + } + + return byteArray.ToArray(); + } + + if (value is BossSequenceDoor.Completion bsdCompValue) { + // For now we only encode the bools of completion struct + var firstBools = new[] { + bsdCompValue.canUnlock, bsdCompValue.unlocked, bsdCompValue.completed, bsdCompValue.allBindings, bsdCompValue.noHits, + bsdCompValue.boundNail, bsdCompValue.boundShell, bsdCompValue.boundCharms + }; + + var byte1 = EncodeUtil.GetByte(firstBools); + + var byte2 = (byte) (bsdCompValue.boundSoul ? 1 : 0); + + return [byte1, byte2]; + } + + if (value is BossStatue.Completion bsCompValue) { + var bools = new[] { + bsCompValue.hasBeenSeen, bsCompValue.isUnlocked, bsCompValue.completedTier1, bsCompValue.completedTier2, + bsCompValue.completedTier3, bsCompValue.seenTier3Unlock, bsCompValue.usingAltVersion + }; + + return [EncodeUtil.GetByte(bools)]; + } + + throw new NotImplementedException($"No encoding implementation for type: {value.GetType()}"); + } + /// /// Callback method for when a boolean is set in the player data. /// /// Name of the boolean variable. /// The original value of the boolean. private bool OnSetPlayerBoolHook(string name, bool orig) { - CheckSendSaveUpdate(name, () => { - return new[] { (byte) (orig ? 0 : 1) }; - }); + CheckSendSaveUpdate(name, EncodeValue(orig)); return orig; } @@ -94,7 +191,7 @@ private bool OnSetPlayerBoolHook(string name, bool orig) { /// Name of the float variable. /// The original value of the float. private float OnSetPlayerFloatHook(string name, float orig) { - CheckSendSaveUpdate(name, () => BitConverter.GetBytes(orig)); + CheckSendSaveUpdate(name, EncodeValue(orig)); return orig; } @@ -105,7 +202,7 @@ private float OnSetPlayerFloatHook(string name, float orig) { /// Name of the int variable. /// The original value of the int. private int OnSetPlayerIntHook(string name, int orig) { - CheckSendSaveUpdate(name, () => BitConverter.GetBytes(orig)); + CheckSendSaveUpdate(name, EncodeValue(orig)); return orig; } @@ -116,18 +213,7 @@ private int OnSetPlayerIntHook(string name, int orig) { /// Name of the string variable. /// The original value of the boolean. private string OnSetPlayerStringHook(string name, string res) { - CheckSendSaveUpdate(name, () => { - var byteEncodedString = Encoding.UTF8.GetBytes(res); - - if (byteEncodedString.Length > ushort.MaxValue) { - throw new Exception($"Could not encode string of length: {byteEncodedString.Length}"); - } - - var value = BitConverter.GetBytes((ushort) byteEncodedString.Length) - .Concat(byteEncodedString) - .ToArray(); - return value; - }); + CheckSendSaveUpdate(name, EncodeValue(res)); return res; } @@ -148,46 +234,11 @@ private object OnSetPlayerVariableHook(Type type, string name, object value) { /// Name of the vector3 variable. /// The original value of the vector3. private Vector3 OnSetPlayerVector3Hook(string name, Vector3 orig) { - CheckSendSaveUpdate(name, () => - BitConverter.GetBytes(orig.x) - .Concat(BitConverter.GetBytes(orig.y)) - .Concat(BitConverter.GetBytes(orig.z)) - .ToArray() - ); + CheckSendSaveUpdate(name, EncodeValue(orig)); return orig; } - /// - /// Checks if a save update should be sent and send it using the encode function to encode the value of the - /// changed variable. - /// - /// The name of the variable that was changed. - /// Function that encodes the value of the variable into a byte array. - private void CheckSendSaveUpdate(string name, Func encodeFunc) { - if (!_entityManager.IsSceneHost) { - Logger.Info($"Not scene host, not sending save update ({name})"); - return; - } - - if (!_saveData.PlayerDataBools.TryGetValue(name, out var value) || !value) { - Logger.Info($"Not in save data values or false in save data values, not sending save update ({name})"); - return; - } - - if (!_saveData.PlayerDataIndices.TryGetValue(name, out var index)) { - Logger.Info($"Cannot find save data index, not sending save update ({name})"); - return; - } - - Logger.Info($"Sending \"{name}\" as save update"); - - _netClient.UpdateManager.SetSaveUpdate( - index, - encodeFunc.Invoke() - ); - } - /// /// Callback method for when the scene changes. Used to check for GeoRock, PersistentInt and PersistentBool /// instances in the scene. @@ -212,6 +263,11 @@ private void OnSceneChanged(Scene oldScene, Scene newScene) { Logger.Info($"Found Geo Rock in scene: {persistentItemData}"); var fsm = geoRock.GetComponent(); + if (fsm == null) { + Logger.Info(" Could not find FSM belonging to Geo Rock object, skipping"); + continue; + } + var fsmInt = fsm.FsmVariables.GetFsmInt("Hits"); var persistentFsmData = new PersistentFsmData { @@ -238,6 +294,11 @@ private void OnSceneChanged(Scene oldScene, Scene newScene) { Logger.Info($"Found persistent bool in scene: {persistentItemData}"); var fsm = FSMUtility.FindFSMWithPersistentBool(itemObject.GetComponents()); + if (fsm == null) { + Logger.Info(" Could not find FSM belonging to persistent bool object, skipping"); + continue; + } + var fsmBool = fsm.FsmVariables.GetFsmBool("Activated"); var persistentFsmData = new PersistentFsmData { @@ -264,6 +325,11 @@ private void OnSceneChanged(Scene oldScene, Scene newScene) { Logger.Info($"Found persistent int in scene: {persistentItemData}"); var fsm = FSMUtility.FindFSMWithPersistentBool(itemObject.GetComponents()); + if (fsm == null) { + Logger.Info(" Could not find FSM belonging to persistent int object, skipping"); + continue; + } + var fsmInt = fsm.FsmVariables.GetFsmInt("Value"); var persistentFsmData = new PersistentFsmData { @@ -276,6 +342,36 @@ private void OnSceneChanged(Scene oldScene, Scene newScene) { } } + /// + /// Checks if a save update should be sent and send it using the encode function to encode the value of the + /// changed variable. + /// + /// The name of the variable that was changed. + /// Encoded value of the variable as a byte array. + private void CheckSendSaveUpdate(string name, byte[] encodedValue) { + if (!_entityManager.IsSceneHost) { + Logger.Info($"Not scene host, not sending save update ({name})"); + return; + } + + if (!_saveDataMapping.PlayerDataBools.TryGetValue(name, out var value) || !value) { + Logger.Info($"Not in save data values or false in save data values, not sending save update ({name})"); + return; + } + + if (!_saveDataMapping.PlayerDataIndices.TryGetValue(name, out var index)) { + Logger.Info($"Cannot find save data index, not sending save update ({name})"); + return; + } + + Logger.Info($"Sending \"{name}\" as save update"); + + _netClient.UpdateManager.SetSaveUpdate( + index, + encodedValue + ); + } + /// /// Called every unity update. Used to check for changes in the GeoRock/PersistentInt/PersistentBool FSMs. /// @@ -305,8 +401,8 @@ private void OnUpdate() { continue; } - if (_saveData.GeoRockDataBools.TryGetValue(itemData, out var shouldSync) && shouldSync) { - if (!_saveData.GeoRockDataIndices.TryGetValue(itemData, out var index)) { + if (_saveDataMapping.GeoRockDataBools.TryGetValue(itemData, out var shouldSync) && shouldSync) { + if (!_saveDataMapping.GeoRockDataIndices.TryGetValue(itemData, out var index)) { Logger.Info( $"Cannot find geo rock save data index, not sending save update ({itemData.Id}, {itemData.SceneName})"); continue; @@ -318,8 +414,8 @@ private void OnUpdate() { index, new[] { (byte) value } ); - } else if (_saveData.PersistentIntDataBools.TryGetValue(itemData, out shouldSync) && shouldSync) { - if (!_saveData.PersistentIntDataIndices.TryGetValue(itemData, out var index)) { + } else if (_saveDataMapping.PersistentIntDataBools.TryGetValue(itemData, out shouldSync) && shouldSync) { + if (!_saveDataMapping.PersistentIntDataIndices.TryGetValue(itemData, out var index)) { Logger.Info( $"Cannot find persistent int save data index, not sending save update ({itemData.Id}, {itemData.SceneName})"); continue; @@ -351,12 +447,12 @@ private void OnUpdate() { continue; } - if (!_saveData.PersistentBoolDataBools.TryGetValue(itemData, out var shouldSync) || !shouldSync) { + if (!_saveDataMapping.PersistentBoolDataBools.TryGetValue(itemData, out var shouldSync) || !shouldSync) { Logger.Info($"Not in persistent bool save data values or false in save data values, not sending save update ({itemData.Id}, {itemData.SceneName})"); continue; } - if (!_saveData.PersistentBoolDataIndices.TryGetValue(itemData, out var index)) { + if (!_saveDataMapping.PersistentBoolDataIndices.TryGetValue(itemData, out var index)) { Logger.Info($"Cannot find persistent bool save data index, not sending save update ({itemData.Id}, {itemData.SceneName})"); continue; } @@ -377,21 +473,46 @@ private void OnUpdate() { /// The save update that was received. private void UpdateSaveWithData(SaveUpdate saveUpdate) { Logger.Info($"Received save update for index: {saveUpdate.SaveDataIndex}"); - + + var index = saveUpdate.SaveDataIndex; + var value = saveUpdate.Value; + + UpdateSaveWithData(index, value); + } + + public void SetSaveWithData(CurrentSave currentSave) { + Logger.Info("Received current save, updating..."); + + foreach (var keyValuePair in currentSave.SaveData) { + var index = keyValuePair.Key; + var value = keyValuePair.Value; + + UpdateSaveWithData(index, value); + } + } + + /// + /// Update the local save with the given data (index and encoded value). + /// + /// The index of the save data. + /// A byte array containing the encoded value of the save data. + /// Thrown when the type belonging to the save data cannot be decoded + /// due to a missing implementation. + private void UpdateSaveWithData(ushort index, byte[] encodedValue) { var pd = PlayerData.instance; var sceneData = SceneData.instance; - if (_saveData.PlayerDataIndices.TryGetValue(saveUpdate.SaveDataIndex, out var name)) { + if (_saveDataMapping.PlayerDataIndices.TryGetValue(index, out var name)) { var fieldInfo = typeof(PlayerData).GetField(name); var type = fieldInfo.FieldType; - var valueLength = saveUpdate.Value.Length; + var valueLength = encodedValue.Length; if (type == typeof(bool)) { if (valueLength != 1) { Logger.Warn($"Received save update with incorrect value length for bool: {valueLength}"); } - var value = saveUpdate.Value[0] == 1; + var value = encodedValue[0] == 1; pd.SetBoolInternal(name, value); } else if (type == typeof(float)) { @@ -399,7 +520,7 @@ private void UpdateSaveWithData(SaveUpdate saveUpdate) { Logger.Warn($"Received save update with incorrect value length for float: {valueLength}"); } - var value = BitConverter.ToSingle(saveUpdate.Value, 0); + var value = BitConverter.ToSingle(encodedValue, 0); pd.SetFloatInternal(name, value); } else if (type == typeof(int)) { @@ -407,11 +528,11 @@ private void UpdateSaveWithData(SaveUpdate saveUpdate) { Logger.Warn($"Received save update with incorrect value length for int: {valueLength}"); } - var value = BitConverter.ToInt32(saveUpdate.Value, 0); + var value = BitConverter.ToInt32(encodedValue, 0); pd.SetIntInternal(name, value); } else if (type == typeof(string)) { - var value = Encoding.UTF8.GetString(saveUpdate.Value); + var value = Encoding.UTF8.GetString(encodedValue); pd.SetStringInternal(name, value); } else if (type == typeof(Vector3)) { @@ -420,15 +541,67 @@ private void UpdateSaveWithData(SaveUpdate saveUpdate) { } var value = new Vector3( - BitConverter.ToSingle(saveUpdate.Value, 0), - BitConverter.ToSingle(saveUpdate.Value, 4), - BitConverter.ToSingle(saveUpdate.Value, 8) + BitConverter.ToSingle(encodedValue, 0), + BitConverter.ToSingle(encodedValue, 4), + BitConverter.ToSingle(encodedValue, 8) ); pd.SetVector3Internal(name, value); + } else if (type == typeof(List)) { + var length = BitConverter.ToUInt16(encodedValue, 0); + + var list = new List(); + var arrayIndex = 2; + for (var i = 0; i < length; i++) { + var strLen = BitConverter.ToUInt16(encodedValue, arrayIndex); + var str = Encoding.UTF8.GetString(encodedValue, arrayIndex + 2, strLen); + + list.Add(str); + + arrayIndex += 2 + strLen; + } + + pd.SetVariableInternal(name, list); + } else if (type == typeof(BossSequenceDoor.Completion)) { + var byte1 = encodedValue[0]; + var byte2 = encodedValue[1]; + + var bools = EncodeUtil.GetBoolsFromByte(byte1); + + var bsdComp = new BossSequenceDoor.Completion { + canUnlock = bools[0], + unlocked = bools[1], + completed = bools[2], + allBindings = bools[3], + noHits = bools[4], + boundNail = bools[5], + boundShell = bools[6], + boundCharms = bools[7], + boundSoul = byte2 == 1 + }; + + pd.SetVariableInternal(name, bsdComp); + } else if (type == typeof(BossStatue.Completion)) { + var bools = EncodeUtil.GetBoolsFromByte(encodedValue[0]); + + var bsComp = new BossStatue.Completion { + hasBeenSeen = bools[0], + isUnlocked = bools[1], + completedTier1 = bools[2], + completedTier2 = bools[3], + completedTier3 = bools[4], + seenTier3Unlock = bools[5], + usingAltVersion = bools[6] + }; + + pd.SetVariableInternal(name, bsComp); + } else { + throw new NotImplementedException($"Could not decode type: {type}"); } - } else if (_saveData.GeoRockDataIndices.TryGetValue(saveUpdate.SaveDataIndex, out var itemData)) { - var value = saveUpdate.Value[0]; + } + + if (_saveDataMapping.GeoRockDataIndices.TryGetValue(index, out var itemData)) { + var value = encodedValue[0]; Logger.Info($"Received geo rock save update: {itemData.Id}, {itemData.SceneName}, {value}"); @@ -437,8 +610,8 @@ private void UpdateSaveWithData(SaveUpdate saveUpdate) { sceneName = itemData.SceneName, hitsLeft = value }); - } else if (_saveData.PersistentBoolDataIndices.TryGetValue(saveUpdate.SaveDataIndex, out itemData)) { - var value = saveUpdate.Value[0] == 1; + } else if (_saveDataMapping.PersistentBoolDataIndices.TryGetValue(index, out itemData)) { + var value = encodedValue[0] == 1; Logger.Info($"Received persistent bool save update: {itemData.Id}, {itemData.SceneName}, {value}"); @@ -447,8 +620,8 @@ private void UpdateSaveWithData(SaveUpdate saveUpdate) { sceneName = itemData.SceneName, activated = value }); - } else if (_saveData.PersistentIntDataIndices.TryGetValue(saveUpdate.SaveDataIndex, out itemData)) { - var value = (int) saveUpdate.Value[0]; + } else if (_saveDataMapping.PersistentIntDataIndices.TryGetValue(index, out itemData)) { + var value = (int) encodedValue[0]; // Add a special case for the -1 value that some persistent ints might have // 255 is never used in the byte space, so we use it for compact networking if (value == 255) { @@ -464,4 +637,82 @@ private void UpdateSaveWithData(SaveUpdate saveUpdate) { }); } } + + /// + /// Get the current save data as a dictionary with mapped indices and encoded values. + /// + /// A dictionary with mapped indices and byte-encoded values. + public static Dictionary GetCurrentSaveData() { + var pd = PlayerData.instance; + var sd = SceneData.instance; + + var saveData = new Dictionary(); + + void AddToSaveData( + IEnumerable enumerable, + Func keyFunc, + Dictionary boolMapping, + BiLookup indexMapping, + Func valueFunc + ) { + foreach (var collectionValue in enumerable) { + var key = keyFunc.Invoke(collectionValue); + + if (!boolMapping.TryGetValue(key, out var shouldSync) || !shouldSync) { + continue; + } + + if (!indexMapping.TryGetValue(key, out var index)) { + continue; + } + + var value = valueFunc.Invoke(collectionValue); + + saveData.Add(index, EncodeValue(value)); + } + } + + AddToSaveData( + typeof(PlayerData).GetFields(), + fieldInfo => fieldInfo.Name, + _saveDataMapping.PlayerDataBools, + _saveDataMapping.PlayerDataIndices, + fieldInfo => fieldInfo.GetValue(pd) + ); + + AddToSaveData( + sd.geoRocks, + geoRock => new PersistentItemData { + Id = geoRock.id, + SceneName = geoRock.sceneName + }, + _saveDataMapping.GeoRockDataBools, + _saveDataMapping.GeoRockDataIndices, + geoRock => geoRock.hitsLeft + ); + + AddToSaveData( + sd.persistentBoolItems, + boolData => new PersistentItemData { + Id = boolData.id, + SceneName = boolData.sceneName + }, + _saveDataMapping.PersistentBoolDataBools, + _saveDataMapping.PersistentBoolDataIndices, + boolData => boolData.activated + ); + + AddToSaveData( + sd.persistentIntItems, + intData => new PersistentItemData { + Id = intData.id, + SceneName = intData.sceneName + }, + _saveDataMapping.PersistentIntDataBools, + _saveDataMapping.PersistentIntDataIndices, + intData => intData.value + ); + + return saveData; + } } diff --git a/HKMP/Game/Server/ModServerManager.cs b/HKMP/Game/Server/ModServerManager.cs index d1e20679..cb37e1c6 100644 --- a/HKMP/Game/Server/ModServerManager.cs +++ b/HKMP/Game/Server/ModServerManager.cs @@ -1,3 +1,4 @@ +using Hkmp.Game.Client.Save; using Hkmp.Game.Command.Server; using Hkmp.Game.Settings; using Hkmp.Networking.Packet; @@ -21,7 +22,10 @@ UiManager uiManager ModHooks.FinishedLoadingModsHook += AddonManager.LoadAddons; // Register handlers for UI events - uiManager.ConnectInterface.StartHostButtonPressed += Start; + uiManager.ConnectInterface.StartHostButtonPressed += port => { + CurrentSaveData = SaveManager.GetCurrentSaveData(); + Start(port); + }; uiManager.ConnectInterface.StopHostButtonPressed += Stop; // Register application quit handler diff --git a/HKMP/Game/Server/ServerManager.cs b/HKMP/Game/Server/ServerManager.cs index 9dd73f0d..4b3a82bd 100644 --- a/HKMP/Game/Server/ServerManager.cs +++ b/HKMP/Game/Server/ServerManager.cs @@ -74,6 +74,11 @@ internal abstract class ServerManager : IServerManager { /// protected readonly ServerAddonManager AddonManager; + /// + /// The current save data for the server. + /// + protected Dictionary CurrentSaveData; + #endregion #region IServerManager properties @@ -280,7 +285,7 @@ private void OnHelloServer(ushort id, HelloServer helloServer) { ); } - _netServer.GetUpdateManagerForClient(id).SetHelloClientData(clientInfo); + _netServer.GetUpdateManagerForClient(id).SetHelloClientData(CurrentSaveData, clientInfo); try { PlayerConnectEvent?.Invoke(playerData); diff --git a/HKMP/Networking/Packet/Data/HelloClient.cs b/HKMP/Networking/Packet/Data/HelloClient.cs index d89245f7..00dec378 100644 --- a/HKMP/Networking/Packet/Data/HelloClient.cs +++ b/HKMP/Networking/Packet/Data/HelloClient.cs @@ -11,6 +11,11 @@ internal class HelloClient : IPacketData { /// public bool DropReliableDataIfNewerExists => true; + + /// + /// The save data currently used on the server. + /// + public CurrentSave CurrentSave { get; set; } /// /// List of ID, username pairs for each connected client. @@ -21,11 +26,14 @@ internal class HelloClient : IPacketData { /// Construct the hello client data. /// public HelloClient() { + CurrentSave = new CurrentSave(); ClientInfo = new List<(ushort, string)>(); } /// public void WriteData(IPacket packet) { + CurrentSave.WriteData(packet); + packet.Write((ushort) ClientInfo.Count); foreach (var (id, username) in ClientInfo) { @@ -36,6 +44,8 @@ public void WriteData(IPacket packet) { /// public void ReadData(IPacket packet) { + CurrentSave.ReadData(packet); + var length = packet.ReadUShort(); for (var i = 0; i < length; i++) { diff --git a/HKMP/Networking/Packet/Data/SaveUpdate.cs b/HKMP/Networking/Packet/Data/SaveUpdate.cs index b0b80540..ad3520c3 100644 --- a/HKMP/Networking/Packet/Data/SaveUpdate.cs +++ b/HKMP/Networking/Packet/Data/SaveUpdate.cs @@ -1,3 +1,6 @@ +using System; +using System.Collections.Generic; + namespace Hkmp.Networking.Packet.Data; /// @@ -42,3 +45,62 @@ public void ReadData(IPacket packet) { } } } + +/// +/// Packet data for when an entire save is networked. +/// +internal class CurrentSave : IPacketData { + /// + public bool IsReliable => true; + + /// + public bool DropReliableDataIfNewerExists => false; + + public Dictionary SaveData { get; set; } + + public CurrentSave() { + SaveData = new Dictionary(); + } + + /// + public void WriteData(IPacket packet) { + var saveDataKeyCount = SaveData.Keys.Count; + if (saveDataKeyCount > ushort.MaxValue) { + throw new Exception("Number of keys in save data is too large"); + } + + var dataLength = (ushort) saveDataKeyCount; + + packet.Write(dataLength); + + foreach (var keyValuePair in SaveData) { + var saveDataIndex = keyValuePair.Key; + var value = keyValuePair.Value; + + packet.Write(saveDataIndex); + + var length = (ushort) System.Math.Min(value.Length, ushort.MaxValue); + packet.Write(length); + for (var i = 0; i < length; i++) { + packet.Write(value[i]); + } + } + } + + /// + public void ReadData(IPacket packet) { + var dataLength = packet.ReadUShort(); + + for (var i = 0; i < dataLength; i++) { + var saveDataIndex = packet.ReadUShort(); + + var length = packet.ReadUShort(); + var value = new byte[length]; + for (var j = 0; j < length; j++) { + value[j] = packet.ReadByte(); + } + + SaveData.Add(saveDataIndex, value); + } + } +} diff --git a/HKMP/Networking/Server/ServerUpdateManager.cs b/HKMP/Networking/Server/ServerUpdateManager.cs index f3cddd34..b9f1aa72 100644 --- a/HKMP/Networking/Server/ServerUpdateManager.cs +++ b/HKMP/Networking/Server/ServerUpdateManager.cs @@ -94,11 +94,15 @@ public void SetLoginResponse(LoginResponse loginResponse) { /// /// Set hello client data in the current packet. /// + /// Dictionary containing current save data of the server. /// The list of pairs of client IDs and usernames. - public void SetHelloClientData(List<(ushort, string)> clientInfo) { + public void SetHelloClientData(Dictionary currentSave, List<(ushort, string)> clientInfo) { lock (Lock) { var helloClient = new HelloClient { - ClientInfo = clientInfo + ClientInfo = clientInfo, + CurrentSave = new CurrentSave { + SaveData = currentSave + } }; CurrentUpdatePacket.SetSendingPacketData(ClientPacketId.HelloClient, helloClient); } diff --git a/HKMP/Resource/save-data.json b/HKMP/Resource/save-data.json index aaad02f7..8a474d6c 100644 --- a/HKMP/Resource/save-data.json +++ b/HKMP/Resource/save-data.json @@ -1029,7 +1029,6 @@ "mapOutskirts": false, "mapRestingGrounds": false, "mapAbyss": false, - "mapZoneBools": true, "hasPin": false, "hasPinBench": false, "hasPinCocoon": false, @@ -19166,4 +19165,4 @@ "Value": true } ] -} \ No newline at end of file +} diff --git a/HKMP/Util/EncodeUtil.cs b/HKMP/Util/EncodeUtil.cs new file mode 100644 index 00000000..ed1c5464 --- /dev/null +++ b/HKMP/Util/EncodeUtil.cs @@ -0,0 +1,36 @@ +namespace Hkmp.Util; + +/// +/// Static class to help with encoding/decoding values to/from bytes. +/// +public static class EncodeUtil { + /// + /// Get a single byte for the given array of booleans where each bit represents a boolean from the array. + /// + /// An array of booleans of at most length 8. + /// A byte representing the booleans. + public static byte GetByte(bool[] bits) { + byte result = 0; + for (var i = 0; i < bits.Length; i++) { + if (bits[i]) { + result |= (byte) (1 << i); + } + } + + return result; + } + + /// + /// Get a boolean array representing the given byte where each boolean is a bit from the byte. + /// + /// A byte that contains boolean for each bit. + /// An boolean array of length 8. + public static bool[] GetBoolsFromByte(byte b) { + var result = new bool[8]; + for (var i = 0; i < result.Length; i++) { + result[i] = (b & (1 << i)) > 0; + } + + return result; + } +} From 45246fe2b94627af8e651b10af79acecbb0fe2fa Mon Sep 17 00:00:00 2001 From: Extremelyd1 Date: Sun, 17 Mar 2024 16:43:22 +0100 Subject: [PATCH 079/216] Add support for string lists and godhome specific savedata syncing --- HKMP/Game/Client/Save/SaveDataMapping.cs | 18 + HKMP/Game/Client/Save/SaveManager.cs | 226 ++++++--- HKMP/HKMP.csproj | 1 + HKMP/Resource/save-data.json | 65 +++ HKMP/Resource/scene-data.json | 554 +++++++++++++++++++++++ HKMP/Util/EncodeUtil.cs | 46 ++ 6 files changed, 856 insertions(+), 54 deletions(-) create mode 100644 HKMP/Resource/scene-data.json diff --git a/HKMP/Game/Client/Save/SaveDataMapping.cs b/HKMP/Game/Client/Save/SaveDataMapping.cs index 559b1498..7f593c19 100644 --- a/HKMP/Game/Client/Save/SaveDataMapping.cs +++ b/HKMP/Game/Client/Save/SaveDataMapping.cs @@ -82,6 +82,24 @@ internal class SaveDataMapping { [JsonIgnore] public BiLookup PersistentIntDataIndices { get; private set; } + /// + /// Deserialized list of strings that represent variable names with the type of a string list. + /// + [JsonProperty("stringListVariables")] + public readonly List StringListVariables; + + /// + /// Deserialized list of strings that represent variable names with the type of BossSequenceDoor.Completion. + /// + [JsonProperty("bossSequenceDoorCompletionVariables")] + public readonly List BossSequenceDoorCompletionVariables; + + /// + /// Deserialized list of strings that represent variable names with the type of BossStatue.Completion. + /// + [JsonProperty("bossStatueCompletionVariables")] + public readonly List BossStatueCompletionVariables; + /// /// Initializes the class by converting the deserialized data fields into the various dictionaries and lookups. /// diff --git a/HKMP/Game/Client/Save/SaveManager.cs b/HKMP/Game/Client/Save/SaveManager.cs index c91baed2..a67687f9 100644 --- a/HKMP/Game/Client/Save/SaveManager.cs +++ b/HKMP/Game/Client/Save/SaveManager.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Text; using Hkmp.Collection; using Hkmp.Game.Client.Entity; using Hkmp.Networking.Client; @@ -28,7 +27,7 @@ internal class SaveManager { /// /// The save data instances that contains mappings for what to sync and their indices. /// - private static SaveDataMapping _saveDataMapping; + private static readonly SaveDataMapping SaveDataMapping; /// /// The net client instance to send save updates. @@ -48,20 +47,38 @@ internal class SaveManager { /// private readonly List _persistentFsmData; + /// + /// Dictionary of hash codes for string list variables in the PlayerData for comparing changes against. + /// + private readonly Dictionary _stringListHashes; + + /// + /// Dictionary of BossSequenceDoor.Completion structs in the PlayerData for comparing changes against. + /// + private readonly Dictionary _bsdCompHashes; + + /// + /// Dictionary of BossStatue.Completion structs in the PlayerData for comparing changes against. + /// + private readonly Dictionary _bsCompHashes; + public SaveManager(NetClient netClient, PacketManager packetManager, EntityManager entityManager) { _netClient = netClient; _packetManager = packetManager; _entityManager = entityManager; _persistentFsmData = new List(); + _stringListHashes = new Dictionary(); + _bsdCompHashes = new Dictionary(); + _bsCompHashes = new Dictionary(); } /// /// Static constructor to load and initialize the save data mapping. /// static SaveManager() { - _saveDataMapping = FileUtil.LoadObjectFromEmbeddedJson(SaveDataFilePath); - _saveDataMapping.Initialize(); + SaveDataMapping = FileUtil.LoadObjectFromEmbeddedJson(SaveDataFilePath); + SaveDataMapping.Initialize(); } /// @@ -76,7 +93,9 @@ public void Initialize() { ModHooks.SetPlayerVector3Hook += OnSetPlayerVector3Hook; UnityEngine.SceneManagement.SceneManager.activeSceneChanged += OnSceneChanged; - MonoBehaviourUtil.Instance.OnUpdateEvent += OnUpdate; + + MonoBehaviourUtil.Instance.OnUpdateEvent += OnUpdatePersistents; + MonoBehaviourUtil.Instance.OnUpdateEvent += OnUpdateCompounds; _packetManager.RegisterClientPacketHandler(ClientPacketId.SaveUpdate, UpdateSaveWithData); } @@ -91,14 +110,15 @@ public void Initialize() { /// Thrown when the given value has a type that cannot be encoded due to /// missing implementation. private static byte[] EncodeValue(object value) { + // Since all strings in the save data are scene names (or map scene names), we can convert them to indices byte[] EncodeString(string stringValue) { - var byteEncodedString = Encoding.UTF8.GetBytes(stringValue); - - if (byteEncodedString.Length > ushort.MaxValue) { - throw new ArgumentOutOfRangeException($"Could not encode string of length: {byteEncodedString.Length}"); + if (!EncodeUtil.GetSceneIndex(stringValue, out var index)) { + // Logger.Info($"Could not encode string value: {stringValue}"); + // return Array.Empty(); + throw new Exception($"Could not encode string value: {stringValue}"); } - return byteEncodedString; + return BitConverter.GetBytes(index); } if (value is bool bValue) { @@ -136,13 +156,7 @@ byte[] EncodeString(string stringValue) { for (var i = 0; i < length; i++) { var encoded = EncodeString(listValue[i]); - if (encoded.Length > ushort.MaxValue) { - throw new ArgumentOutOfRangeException($"Could not encode string in list of length: {encoded.Length}"); - } - - var encodedLen = (ushort) encoded.Length; - - byteArray = byteArray.Concat(BitConverter.GetBytes(encodedLen)).Concat(encoded); + byteArray = byteArray.Concat(encoded); } return byteArray.ToArray(); @@ -180,7 +194,7 @@ byte[] EncodeString(string stringValue) { /// Name of the boolean variable. /// The original value of the boolean. private bool OnSetPlayerBoolHook(string name, bool orig) { - CheckSendSaveUpdate(name, EncodeValue(orig)); + CheckSendSaveUpdate(name, () => EncodeValue(orig)); return orig; } @@ -191,7 +205,7 @@ private bool OnSetPlayerBoolHook(string name, bool orig) { /// Name of the float variable. /// The original value of the float. private float OnSetPlayerFloatHook(string name, float orig) { - CheckSendSaveUpdate(name, EncodeValue(orig)); + CheckSendSaveUpdate(name, () => EncodeValue(orig)); return orig; } @@ -202,7 +216,7 @@ private float OnSetPlayerFloatHook(string name, float orig) { /// Name of the int variable. /// The original value of the int. private int OnSetPlayerIntHook(string name, int orig) { - CheckSendSaveUpdate(name, EncodeValue(orig)); + CheckSendSaveUpdate(name, () => EncodeValue(orig)); return orig; } @@ -213,7 +227,7 @@ private int OnSetPlayerIntHook(string name, int orig) { /// Name of the string variable. /// The original value of the boolean. private string OnSetPlayerStringHook(string name, string res) { - CheckSendSaveUpdate(name, EncodeValue(res)); + CheckSendSaveUpdate(name, () => EncodeValue(res)); return res; } @@ -225,7 +239,9 @@ private string OnSetPlayerStringHook(string name, string res) { /// Name of the object variable. /// The original value of the object. private object OnSetPlayerVariableHook(Type type, string name, object value) { - throw new NotImplementedException($"Object with type: {value.GetType()} could not be encoded"); + CheckSendSaveUpdate(name, () => EncodeValue(value)); + + return value; } /// @@ -234,7 +250,7 @@ private object OnSetPlayerVariableHook(Type type, string name, object value) { /// Name of the vector3 variable. /// The original value of the vector3. private Vector3 OnSetPlayerVector3Hook(string name, Vector3 orig) { - CheckSendSaveUpdate(name, EncodeValue(orig)); + CheckSendSaveUpdate(name, () => EncodeValue(orig)); return orig; } @@ -347,19 +363,19 @@ private void OnSceneChanged(Scene oldScene, Scene newScene) { /// changed variable. /// /// The name of the variable that was changed. - /// Encoded value of the variable as a byte array. - private void CheckSendSaveUpdate(string name, byte[] encodedValue) { + /// Function to encode the value of the variable to a byte array. + private void CheckSendSaveUpdate(string name, Func encodeFunc) { if (!_entityManager.IsSceneHost) { Logger.Info($"Not scene host, not sending save update ({name})"); return; } - if (!_saveDataMapping.PlayerDataBools.TryGetValue(name, out var value) || !value) { + if (!SaveDataMapping.PlayerDataBools.TryGetValue(name, out var value) || !value) { Logger.Info($"Not in save data values or false in save data values, not sending save update ({name})"); return; } - if (!_saveDataMapping.PlayerDataIndices.TryGetValue(name, out var index)) { + if (!SaveDataMapping.PlayerDataIndices.TryGetValue(name, out var index)) { Logger.Info($"Cannot find save data index, not sending save update ({name})"); return; } @@ -368,14 +384,14 @@ private void CheckSendSaveUpdate(string name, byte[] encodedValue) { _netClient.UpdateManager.SetSaveUpdate( index, - encodedValue + encodeFunc.Invoke() ); } /// /// Called every unity update. Used to check for changes in the GeoRock/PersistentInt/PersistentBool FSMs. /// - private void OnUpdate() { + private void OnUpdatePersistents() { using var enumerator = _persistentFsmData.GetEnumerator(); while (enumerator.MoveNext()) { @@ -401,8 +417,8 @@ private void OnUpdate() { continue; } - if (_saveDataMapping.GeoRockDataBools.TryGetValue(itemData, out var shouldSync) && shouldSync) { - if (!_saveDataMapping.GeoRockDataIndices.TryGetValue(itemData, out var index)) { + if (SaveDataMapping.GeoRockDataBools.TryGetValue(itemData, out var shouldSync) && shouldSync) { + if (!SaveDataMapping.GeoRockDataIndices.TryGetValue(itemData, out var index)) { Logger.Info( $"Cannot find geo rock save data index, not sending save update ({itemData.Id}, {itemData.SceneName})"); continue; @@ -414,8 +430,8 @@ private void OnUpdate() { index, new[] { (byte) value } ); - } else if (_saveDataMapping.PersistentIntDataBools.TryGetValue(itemData, out shouldSync) && shouldSync) { - if (!_saveDataMapping.PersistentIntDataIndices.TryGetValue(itemData, out var index)) { + } else if (SaveDataMapping.PersistentIntDataBools.TryGetValue(itemData, out shouldSync) && shouldSync) { + if (!SaveDataMapping.PersistentIntDataIndices.TryGetValue(itemData, out var index)) { Logger.Info( $"Cannot find persistent int save data index, not sending save update ({itemData.Id}, {itemData.SceneName})"); continue; @@ -447,12 +463,12 @@ private void OnUpdate() { continue; } - if (!_saveDataMapping.PersistentBoolDataBools.TryGetValue(itemData, out var shouldSync) || !shouldSync) { + if (!SaveDataMapping.PersistentBoolDataBools.TryGetValue(itemData, out var shouldSync) || !shouldSync) { Logger.Info($"Not in persistent bool save data values or false in save data values, not sending save update ({itemData.Id}, {itemData.SceneName})"); continue; } - if (!_saveDataMapping.PersistentBoolDataIndices.TryGetValue(itemData, out var index)) { + if (!SaveDataMapping.PersistentBoolDataIndices.TryGetValue(itemData, out var index)) { Logger.Info($"Cannot find persistent bool save data index, not sending save update ({itemData.Id}, {itemData.SceneName})"); continue; } @@ -467,6 +483,86 @@ private void OnUpdate() { } } + /// + /// Called every unity update. Used to check for changes in non-primitive variables in the PlayerData. + /// + private void OnUpdateCompounds() { + void CheckUpdates( + List variableNames, + Dictionary checkDict, + Func newCheckFunc, + Func changeFunc + ) { + foreach (var varName in variableNames) { + if (!SaveDataMapping.PlayerDataBools.TryGetValue(varName, out var shouldSync) || !shouldSync) { + continue; + } + + if (!SaveDataMapping.PlayerDataIndices.TryGetValue(varName, out var index)) { + continue; + } + + var variable = (TVar) typeof(PlayerData).GetField(varName).GetValue(PlayerData.instance); + var newCheck = newCheckFunc.Invoke(variable); + + if (!checkDict.TryGetValue(varName, out var check)) { + checkDict[varName] = newCheck; + continue; + } + + if (changeFunc(newCheck, check)) { + Logger.Info($"Compound variable ({varName}) changed value"); + + checkDict[varName] = newCheck; + + if (_netClient.IsConnected && _entityManager.IsSceneHost) { + _netClient.UpdateManager.SetSaveUpdate( + index, + EncodeValue(variable) + ); + } + } + } + } + + CheckUpdates, int>( + SaveDataMapping.StringListVariables, + _stringListHashes, + GetStringListHashCode, + (hash1, hash2) => hash1 != hash2 + ); + + CheckUpdates( + SaveDataMapping.BossSequenceDoorCompletionVariables, + _bsdCompHashes, + bsdComp => bsdComp, + (b1, b2) => + b1.canUnlock != b2.canUnlock || + b1.unlocked != b2.unlocked || + b1.completed != b2.completed || + b1.allBindings != b2.allBindings || + b1.noHits != b2.noHits || + b1.boundNail != b2.boundNail || + b1.boundShell != b2.boundShell || + b1.boundCharms != b2.boundCharms || + b1.boundSoul != b2.boundSoul + ); + + CheckUpdates( + SaveDataMapping.BossStatueCompletionVariables, + _bsCompHashes, + bsComp => bsComp, + (b1, b2) => + b1.hasBeenSeen != b2.hasBeenSeen || + b1.isUnlocked != b2.isUnlocked || + b1.completedTier1 != b2.completedTier1 || + b1.completedTier2 != b2.completedTier2 || + b1.completedTier3 != b2.completedTier3 || + b1.seenTier3Unlock != b2.seenTier3Unlock || + b1.usingAltVersion != b2.usingAltVersion + ); + } + /// /// Callback method for when a save update is received. /// @@ -502,7 +598,9 @@ private void UpdateSaveWithData(ushort index, byte[] encodedValue) { var pd = PlayerData.instance; var sceneData = SceneData.instance; - if (_saveDataMapping.PlayerDataIndices.TryGetValue(index, out var name)) { + if (SaveDataMapping.PlayerDataIndices.TryGetValue(index, out var name)) { + Logger.Info($"Received save update ({index}, {name})"); + var fieldInfo = typeof(PlayerData).GetField(name); var type = fieldInfo.FieldType; var valueLength = encodedValue.Length; @@ -532,7 +630,11 @@ private void UpdateSaveWithData(ushort index, byte[] encodedValue) { pd.SetIntInternal(name, value); } else if (type == typeof(string)) { - var value = Encoding.UTF8.GetString(encodedValue); + var sceneIndex = BitConverter.ToUInt16(encodedValue, 0); + + if (!EncodeUtil.GetSceneName(sceneIndex, out var value)) { + throw new Exception($"Could not decode string from save update: {encodedValue}"); + } pd.SetStringInternal(name, value); } else if (type == typeof(Vector3)) { @@ -551,14 +653,14 @@ private void UpdateSaveWithData(ushort index, byte[] encodedValue) { var length = BitConverter.ToUInt16(encodedValue, 0); var list = new List(); - var arrayIndex = 2; for (var i = 0; i < length; i++) { - var strLen = BitConverter.ToUInt16(encodedValue, arrayIndex); - var str = Encoding.UTF8.GetString(encodedValue, arrayIndex + 2, strLen); - - list.Add(str); + var sceneIndex = BitConverter.ToUInt16(encodedValue, 2 + i * 2); - arrayIndex += 2 + strLen; + if (!EncodeUtil.GetSceneName(sceneIndex, out var sceneName)) { + throw new Exception($"Could not decode string in list from save update: {sceneIndex}"); + } + + list.Add(sceneName); } pd.SetVariableInternal(name, list); @@ -600,7 +702,7 @@ private void UpdateSaveWithData(ushort index, byte[] encodedValue) { } } - if (_saveDataMapping.GeoRockDataIndices.TryGetValue(index, out var itemData)) { + if (SaveDataMapping.GeoRockDataIndices.TryGetValue(index, out var itemData)) { var value = encodedValue[0]; Logger.Info($"Received geo rock save update: {itemData.Id}, {itemData.SceneName}, {value}"); @@ -610,7 +712,7 @@ private void UpdateSaveWithData(ushort index, byte[] encodedValue) { sceneName = itemData.SceneName, hitsLeft = value }); - } else if (_saveDataMapping.PersistentBoolDataIndices.TryGetValue(index, out itemData)) { + } else if (SaveDataMapping.PersistentBoolDataIndices.TryGetValue(index, out itemData)) { var value = encodedValue[0] == 1; Logger.Info($"Received persistent bool save update: {itemData.Id}, {itemData.SceneName}, {value}"); @@ -620,7 +722,7 @@ private void UpdateSaveWithData(ushort index, byte[] encodedValue) { sceneName = itemData.SceneName, activated = value }); - } else if (_saveDataMapping.PersistentIntDataIndices.TryGetValue(index, out itemData)) { + } else if (SaveDataMapping.PersistentIntDataIndices.TryGetValue(index, out itemData)) { var value = (int) encodedValue[0]; // Add a special case for the -1 value that some persistent ints might have // 255 is never used in the byte space, so we use it for compact networking @@ -675,8 +777,8 @@ Func valueFunc AddToSaveData( typeof(PlayerData).GetFields(), fieldInfo => fieldInfo.Name, - _saveDataMapping.PlayerDataBools, - _saveDataMapping.PlayerDataIndices, + SaveDataMapping.PlayerDataBools, + SaveDataMapping.PlayerDataIndices, fieldInfo => fieldInfo.GetValue(pd) ); @@ -686,8 +788,8 @@ Func valueFunc Id = geoRock.id, SceneName = geoRock.sceneName }, - _saveDataMapping.GeoRockDataBools, - _saveDataMapping.GeoRockDataIndices, + SaveDataMapping.GeoRockDataBools, + SaveDataMapping.GeoRockDataIndices, geoRock => geoRock.hitsLeft ); @@ -697,8 +799,8 @@ Func valueFunc Id = boolData.id, SceneName = boolData.sceneName }, - _saveDataMapping.PersistentBoolDataBools, - _saveDataMapping.PersistentBoolDataIndices, + SaveDataMapping.PersistentBoolDataBools, + SaveDataMapping.PersistentBoolDataIndices, boolData => boolData.activated ); @@ -708,11 +810,27 @@ Func valueFunc Id = intData.id, SceneName = intData.sceneName }, - _saveDataMapping.PersistentIntDataBools, - _saveDataMapping.PersistentIntDataIndices, + SaveDataMapping.PersistentIntDataBools, + SaveDataMapping.PersistentIntDataIndices, intData => intData.value ); return saveData; } + + /// + /// Get the hash code of the combined values in a string list. + /// + /// The list of strings to calculate the hash code for. + /// 0 if the list is empty, otherwise a hash code matching the specific order of strings in the list. + /// + private static int GetStringListHashCode(List list) { + if (list.Count == 0) { + return 0; + } + + return list + .Select(item => item.GetHashCode()) + .Aggregate((total, nextCode) => total ^ nextCode); + } } diff --git a/HKMP/HKMP.csproj b/HKMP/HKMP.csproj index 700d8820..76902f0a 100644 --- a/HKMP/HKMP.csproj +++ b/HKMP/HKMP.csproj @@ -22,6 +22,7 @@ + diff --git a/HKMP/Resource/save-data.json b/HKMP/Resource/save-data.json index 8a474d6c..276bf272 100644 --- a/HKMP/Resource/save-data.json +++ b/HKMP/Resource/save-data.json @@ -19164,5 +19164,70 @@ }, "Value": true } + ], + "stringListVariables": [ + "scenesVisited", + "scenesMapped", + "scenesEncounteredBench", + "scenesGrubRescued", + "scenesFlameCollected", + "scenesEncounteredCocoon", + "scenesEncounteredDreamPlant", + "scenesEncounteredDreamPlantC", + "unlockedBossScenes" + ], + "bossSequenceDoorCompletionVariables": [ + "bossDoorStateTier1", + "bossDoorStateTier2", + "bossDoorStateTier3", + "bossDoorStateTier4", + "bossDoorStateTier5" + ], + "bossStatueCompletionVariables": [ + "statueStateGruzMother", + "statueStateVengefly", + "statueStateBroodingMawlek", + "statueStateFalseKnight", + "statueStateFailedChampion", + "statueStateHornet1", + "statueStateHornet2", + "statueStateMegaMossCharger", + "statueStateMantisLords", + "statueStateOblobbles", + "statueStateGreyPrince", + "statueStateBrokenVessel", + "statueStateLostKin", + "statueStateNosk", + "statueStateFlukemarm", + "statueStateCollector", + "statueStateWatcherKnights", + "statueStateSoulMaster", + "statueStateSoulTyrant", + "statueStateGodTamer", + "statueStateCrystalGuardian1", + "statueStateCrystalGuardian2", + "statueStateUumuu", + "statueStateDungDefender", + "statueStateWhiteDefender", + "statueStateHiveKnight", + "statueStateTraitorLord", + "statueStateGrimm", + "statueStateNightmareGrimm", + "statueStateHollowKnight", + "statueStateElderHu", + "statueStateGalien", + "statueStateMarkoth", + "statueStateMarmu", + "statueStateNoEyes", + "statueStateXero", + "statueStateGorb", + "statueStateRadiance", + "statueStateSly", + "statueStateNailmasters", + "statueStateMageKnight", + "statueStatePaintmaster", + "statueStateZote", + "statueStateNoskHornet", + "statueStateMantisLordsExtra" ] } diff --git a/HKMP/Resource/scene-data.json b/HKMP/Resource/scene-data.json new file mode 100644 index 00000000..257ae1bf --- /dev/null +++ b/HKMP/Resource/scene-data.json @@ -0,0 +1,554 @@ +[ + "Pre_Menu_Intro", + "Menu_Title", + "Quit_To_Menu", + "BetaEnd", + "Knight_Pickup", + "Opening_Sequence", + "Tutorial_01", + "Town", + "Cinematic_Stag_travel", + "Room_Town_Stag_Station", + "Room_Charm_Shop", + "Room_Mender_House", + "Room_mapper", + "Room_nailmaster", + "Room_nailmaster_02", + "Room_nailmaster_03", + "Room_nailsmith", + "Room_shop", + "Room_Sly_Storeroom", + "Room_temple", + "Room_ruinhouse", + "Room_Mask_Maker", + "Room_Mansion", + "Room_Tram", + "Room_Tram_RG", + "Room_Bretta", + "Room_Bretta_Basement", + "Room_Fungus_Shaman", + "Room_Ouiji", + "Room_Jinn", + "Room_Colosseum_01", + "Room_Colosseum_02", + "Room_Colosseum_03", + "Room_Colosseum_Bronze", + "Room_Colosseum_Silver", + "Room_Colosseum_Gold", + "Room_Colosseum_Spectate", + "Room_Slug_Shrine", + "Crossroads_01", + "Crossroads_02", + "Crossroads_03", + "Crossroads_04", + "Crossroads_05", + "Crossroads_06", + "Crossroads_07", + "Crossroads_08", + "Crossroads_09", + "Crossroads_10", + "Crossroads_10_preload", + "Crossroads_10_boss", + "Crossroads_10_boss_defeated", + "Crossroads_11_alt", + "Crossroads_12", + "Crossroads_13", + "Crossroads_14", + "Crossroads_15", + "Crossroads_16", + "Crossroads_18", + "Crossroads_19", + "Crossroads_21", + "Crossroads_22", + "Crossroads_25", + "Crossroads_27", + "Crossroads_30", + "Crossroads_31", + "Crossroads_33", + "Crossroads_35", + "Crossroads_36", + "Crossroads_37", + "Crossroads_38", + "Crossroads_39", + "Crossroads_40", + "Crossroads_42", + "Crossroads_43", + "Crossroads_45", + "Crossroads_46", + "Crossroads_46b", + "Crossroads_ShamanTemple", + "Crossroads_47", + "Crossroads_48", + "Crossroads_49", + "Crossroads_49b", + "Crossroads_50", + "Crossroads_52", + "Ruins_House_01", + "Ruins_House_02", + "Ruins_House_03", + "Ruins_Elevator", + "Ruins_Bathhouse", + "Ruins1_01", + "Ruins1_02", + "Ruins1_03", + "Ruins1_04", + "Ruins1_05", + "Ruins1_05b", + "Ruins1_05c", + "Ruins1_06", + "Ruins1_09", + "Ruins1_17", + "Ruins1_18", + "Ruins1_23", + "Ruins1_30", + "Ruins1_24", + "Ruins1_24_boss", + "Ruins1_24_boss_defeated", + "Ruins1_25", + "Ruins1_27", + "Ruins1_28", + "Ruins1_29", + "Ruins1_31", + "Ruins1_31b", + "Ruins1_32", + "Ruins2_01", + "Ruins2_01_b", + "Ruins2_03", + "Ruins2_03b", + "Ruins2_03_boss", + "Ruins2_04", + "Ruins2_05", + "Ruins2_06", + "Ruins2_07", + "Ruins2_08", + "Ruins2_09", + "Ruins2_10", + "Ruins2_10b", + "Ruins2_11", + "Ruins2_11_b", + "Ruins2_11_boss", + "Ruins2_Watcher_Room", + "Fungus1_01", + "Fungus1_01b", + "Fungus1_02", + "Fungus1_03", + "Fungus1_04", + "Fungus1_04_boss", + "Fungus1_05", + "Fungus1_06", + "Fungus1_07", + "Fungus1_08", + "Fungus1_09", + "Fungus1_10", + "Fungus1_11", + "Fungus1_12", + "Fungus1_13", + "Fungus1_14", + "Fungus1_15", + "Fungus1_16_alt", + "Fungus1_17", + "Fungus1_19", + "Fungus1_20_v02", + "Fungus1_21", + "Fungus1_22", + "Fungus1_23", + "Fungus1_24", + "Fungus1_25", + "Fungus1_26", + "Fungus1_28", + "Fungus1_29", + "Fungus1_30", + "Fungus1_31", + "Fungus1_32", + "Fungus1_34", + "Fungus1_35", + "Fungus1_36", + "Fungus1_37", + "Fungus1_Slug", + "Fungus2_01", + "Fungus2_02", + "Fungus2_03", + "Fungus2_04", + "Fungus2_05", + "Fungus2_06", + "Fungus2_07", + "Fungus2_08", + "Fungus2_09", + "Fungus2_10", + "Fungus2_11", + "Fungus2_12", + "Fungus2_13", + "Fungus2_14", + "Fungus2_15", + "Fungus2_15_boss", + "Fungus2_15_boss_defeated", + "Fungus2_17", + "Fungus2_18", + "Fungus2_19", + "Fungus2_20", + "Fungus2_21", + "Fungus2_23", + "Fungus2_25", + "Fungus2_26", + "Fungus2_28", + "Fungus2_29", + "Fungus2_30", + "Fungus2_31", + "Fungus2_32", + "Fungus2_33", + "Fungus2_34", + "Fungus3_01", + "Fungus3_02", + "Fungus3_03", + "Fungus3_04", + "Fungus3_05", + "Fungus3_08", + "Fungus3_10", + "Fungus3_11", + "Fungus3_13", + "Fungus3_21", + "Fungus3_22", + "Fungus3_23", + "Fungus3_23_boss", + "Fungus3_24", + "Fungus3_25", + "Fungus3_25b", + "Fungus3_26", + "Fungus3_27", + "Fungus3_28", + "Fungus3_30", + "Fungus3_34", + "Fungus3_35", + "Fungus3_39", + "Fungus3_40", + "Fungus3_40_boss", + "Fungus3_44", + "Fungus3_47", + "Fungus3_48", + "Fungus3_49", + "Fungus3_50", + "Fungus3_archive", + "Fungus3_archive_02", + "Fungus3_archive_02_boss", + "Cliffs_01", + "Cliffs_02", + "Cliffs_02_boss", + "Cliffs_03", + "Cliffs_04", + "Cliffs_05", + "Cliffs_06", + "RestingGrounds_02", + "RestingGrounds_02_boss", + "RestingGrounds_04", + "RestingGrounds_05", + "RestingGrounds_06", + "RestingGrounds_07", + "RestingGrounds_08", + "RestingGrounds_09", + "RestingGrounds_10", + "RestingGrounds_12", + "RestingGrounds_17", + "Mines_01", + "Mines_02", + "Mines_03", + "Mines_04", + "Mines_05", + "Mines_06", + "Mines_07", + "Mines_10", + "Mines_11", + "Mines_13", + "Mines_16", + "Mines_17", + "Mines_18", + "Mines_18_boss", + "Mines_19", + "Mines_20", + "Mines_23", + "Mines_24", + "Mines_25", + "Mines_28", + "Mines_29", + "Mines_30", + "Mines_31", + "Mines_32", + "Mines_33", + "Mines_34", + "Mines_35", + "Mines_36", + "Mines_37", + "Deepnest_01", + "Deepnest_01b", + "Deepnest_02", + "Deepnest_03", + "Deepnest_09", + "Deepnest_10", + "Deepnest_14", + "Deepnest_16", + "Deepnest_17", + "Deepnest_26", + "Deepnest_26b", + "Deepnest_30", + "Deepnest_31", + "Deepnest_32", + "Deepnest_33", + "Deepnest_34", + "Deepnest_35", + "Deepnest_36", + "Deepnest_37", + "Deepnest_38", + "Deepnest_39", + "Deepnest_40", + "Deepnest_41", + "Deepnest_42", + "Deepnest_43", + "Deepnest_44", + "Deepnest_45_v02", + "Deepnest_Spider_Town", + "Room_spider_small", + "Deepnest_East_01", + "Deepnest_East_02", + "Deepnest_East_03", + "Deepnest_East_04", + "Deepnest_East_06", + "Deepnest_East_07", + "Deepnest_East_08", + "Deepnest_East_09", + "Deepnest_East_10", + "Deepnest_East_11", + "Deepnest_East_12", + "Deepnest_East_13", + "Deepnest_East_14", + "Deepnest_East_14b", + "Deepnest_East_15", + "Deepnest_East_16", + "Deepnest_East_17", + "Deepnest_East_18", + "Deepnest_East_Hornet", + "Deepnest_East_Hornet_boss", + "Room_Wyrm", + "Abyss_01", + "Abyss_02", + "Abyss_03", + "Abyss_03_b", + "Abyss_03_c", + "Abyss_04", + "Abyss_05", + "Abyss_06_Core", + "Abyss_08", + "Abyss_09", + "Abyss_10", + "Abyss_12", + "Abyss_15", + "Abyss_16", + "Abyss_17", + "Abyss_18", + "Abyss_19", + "Abyss_20", + "Abyss_21", + "Abyss_22", + "Abyss_Lighthouse_room", + "Room_Queen", + "Waterways_01", + "Waterways_02", + "Waterways_03", + "Waterways_04", + "Waterways_04b", + "Waterways_05", + "Waterways_05_boss", + "Waterways_06", + "Waterways_07", + "Waterways_08", + "Waterways_09", + "Waterways_12", + "Waterways_12_boss", + "Waterways_13", + "Waterways_14", + "Waterways_15", + "White_Palace_01", + "White_Palace_02", + "White_Palace_03_hub", + "White_Palace_04", + "White_Palace_05", + "White_Palace_06", + "White_Palace_07", + "White_Palace_08", + "White_Palace_09", + "White_Palace_11", + "White_Palace_12", + "White_Palace_13", + "White_Palace_14", + "White_Palace_15", + "White_Palace_16", + "White_Palace_17", + "White_Palace_18", + "White_Palace_19", + "White_Palace_20", + "Hive_01", + "Hive_02", + "Hive_03", + "Hive_03_c", + "Hive_04", + "Hive_05", + "Grimm_Divine", + "Grimm_Main_Tent", + "Grimm_Main_Tent_boss", + "Grimm_Nightmare", + "Dream_Nailcollection", + "Dream_01_False_Knight", + "Dream_02_Mage_Lord", + "Dream_03_Infected_Knight", + "Dream_04_White_Defender", + "Dream_Mighty_Zote", + "Dream_Guardian", + "Dream_Guardian_Hegemol", + "Dream_Guardian_Lurien", + "Dream_Guardian_Monomon", + "Cutscene_Boss_Door", + "Dream_Backer_Shrine", + "Dream_Room_Believer_Shrine", + "Dream_Abyss", + "Dream_Final", + "Dream_Final_Boss", + "Room_Final_Boss_Atrium", + "Room_Final_Boss_Core", + "Cinematic_Ending_A", + "Cinematic_Ending_B", + "Cinematic_Ending_C", + "Cinematic_Ending_D", + "Cinematic_Ending_E", + "End_Credits", + "Cinematic_MrMushroom", + "Menu_Credits", + "End_Game_Completion", + "PermaDeath", + "_test_cocoon_2", + "PermaDeath_Unlock", + "_test_cocoon_1", + "GG_Waterways", + "GG_Atrium", + "GG_Broken_Vessel", + "GG_Brooding_Mawlek", + "GG_Collector", + "GG_Crystal_Guardian", + "GG_Crystal_Guardian_2", + "GG_Dung_Defender", + "GG_Failed_Champion", + "GG_False_Knight", + "GG_Flukemarm", + "GG_Ghost_Galien", + "GG_Ghost_Gorb", + "GG_Ghost_Hu", + "GG_Ghost_Markoth", + "GG_Ghost_Marmu", + "GG_Ghost_No_Eyes", + "GG_Ghost_Xero", + "GG_God_Tamer", + "GG_Grey_Prince_Zote", + "GG_Grimm", + "GG_Grimm_Nightmare", + "GG_Gruz_Mother", + "GG_Hive_Knight", + "GG_Hollow_Knight", + "GG_Hornet_1", + "GG_Hornet_2", + "GG_Lost_Kin", + "GG_Lurker", + "GG_Mantis_Lords", + "GG_Mega_Moss_Charger", + "GG_Nailmasters", + "GG_Nosk", + "GG_Oblobbles", + "GG_Painter", + "GG_Pipeway", + "GG_Radiance", + "GG_Sly", + "GG_Soul_Master", + "GG_Soul_Tyrant", + "GG_Spa", + "GG_Traitor_Lord", + "GG_Unlock", + "GG_Uumuu", + "GG_Vengefly", + "GG_Watcher_Knights", + "GG_White_Defender", + "GG_Workshop", + "Room_GG_Shortcut", + "GG_End_Sequence", + "GG_Atrium_Roof", + "GG_Blue_Room", + "GG_Engine", + "GG_Engine_Prime", + "GG_Engine_Root", + "GG_Mage_Knight", + "GG_Vengefly_V", + "GG_Entrance_Cutscene", + "GG_Mighty_Zote", + "GG_Land_of_Storms", + "GG_Boss_Door_Entrance", + "GG_Gruz_Mother_V", + "GG_Brooding_Mawlek_V", + "GG_Mantis_Lords_V", + "GG_Nosk_Hornet", + "GG_Uumuu_V", + "GG_Ghost_Gorb_V", + "GG_Ghost_Markoth_V", + "GG_Ghost_Marmu_V", + "GG_Ghost_No_Eyes_V", + "GG_Ghost_Xero_V", + "GG_Mage_Knight_V", + "GG_Collector_V", + "GG_Nosk_V", + "GG_Wyrm", + "GG_Unn", + "GG_Door_5_Finale", + "GG_Unlock_Wastes", + "Abyss_06_Core_b", + "Abyss_18_b", + "Cliffs_01_b", + "Cliffs_02_b", + "Cliffs_06_b", + "Crossroads_04_b", + "Crossroads_18_b", + "Crossroads_21_b", + "Crossroads_35_b", + "Deepnest_26_b", + "Deepnest_30_b", + "Deepnest_41_b", + "Deepnest_43_b", + "Deepnest_44_b", + "Deepnest_East_14a", + "Deepnest_East_02_b", + "Deepnest_East_09_b", + "Deepnest_East_Hornet_b", + "Fungus1_09_b", + "Fungus1_14_b", + "Fungus1_28_b", + "Fungus2_14_b", + "Fungus2_14_c", + "Fungus2_29_b", + "Fungus3_22_b", + "Fungus3_23_b", + "Fungus3_48_bot", + "Fungus3_48_left", + "Fungus3_48_top", + "Hive_01_b", + "Hive_03_b", + "Hive_04_b", + "Mines_20_b", + "Mines_28_b", + "RestingGrounds_10_b", + "RestingGrounds_10_c", + "RestingGrounds_10_d", + "Ruins1_18_b", + "Ruins1_31_top", + "Ruins1_31_top_2", + "Ruins2_07_left", + "Ruins2_07_right", + "Waterways_02_b", + "Waterways_04_part_b", + "Intro_Cutscene_Prologue", + "Prologue_Excerpt", + "Intro_Cutscene", + "Dream_NailCollection" +] diff --git a/HKMP/Util/EncodeUtil.cs b/HKMP/Util/EncodeUtil.cs index ed1c5464..58b0479d 100644 --- a/HKMP/Util/EncodeUtil.cs +++ b/HKMP/Util/EncodeUtil.cs @@ -1,9 +1,35 @@ +using System.Collections.Generic; +using Hkmp.Collection; + namespace Hkmp.Util; /// /// Static class to help with encoding/decoding values to/from bytes. /// public static class EncodeUtil { + /// + /// The file path of the embedded resource file for scene data. + /// + private const string SceneDataFilePath = "Hkmp.Resource.scene-data.json"; + + /// + /// Bi-directional lookup that maps scene names to their indices. + /// + private static readonly BiLookup SceneIndices; + + /// + /// Static construct to load the scene indices. + /// + static EncodeUtil() { + SceneIndices = new BiLookup(); + + var sceneNames = FileUtil.LoadObjectFromEmbeddedJson>(SceneDataFilePath); + ushort index = 0; + foreach (var sceneName in sceneNames) { + SceneIndices.Add(sceneName, index++); + } + } + /// /// Get a single byte for the given array of booleans where each bit represents a boolean from the array. /// @@ -33,4 +59,24 @@ public static bool[] GetBoolsFromByte(byte b) { return result; } + + /// + /// Try to get the scene index corresponding to the given scene name for encoding/decoding purposes. + /// + /// The name of the scene. + /// The index of the scene or default if the scene name could not be found. + /// true if there is a corresponding index for the given scene name, false otherwise. + public static bool GetSceneIndex(string sceneName, out ushort index) { + return SceneIndices.TryGetValue(sceneName, out index); + } + + /// + /// Try to get the scene name corresponding to the given scene index for encoding/decoding purposes. + /// + /// The index of the scene. + /// The name of the scene or default if the scene index could not be found. + /// true if there is a corresponding name for the given scene index, false otherwise. + public static bool GetSceneName(ushort index, out string sceneName) { + return SceneIndices.TryGetValue(index, out sceneName); + } } From 73f62664aab9179340d284f7a1a821b4e12a828e Mon Sep 17 00:00:00 2001 From: Extremelyd1 Date: Sat, 30 Mar 2024 10:32:57 +0100 Subject: [PATCH 080/216] Fix Shroomal Ogres, various other fixes --- HKMP/Fsm/FsmPatcher.cs | 25 ++++++++++++++++- .../Client/Entity/Action/EntityFsmActions.cs | 28 ++++++++++++------- HKMP/Game/Client/Entity/EntityManager.cs | 11 +++++--- HKMP/Game/Client/Entity/EntityType.cs | 1 + HKMP/Resource/entity-registry.json | 5 ++++ ...eObjectExtensions.cs => GameObjectUtil.cs} | 27 ++++++++++++++++-- 6 files changed, 80 insertions(+), 17 deletions(-) rename HKMP/Util/{GameObjectExtensions.cs => GameObjectUtil.cs} (54%) diff --git a/HKMP/Fsm/FsmPatcher.cs b/HKMP/Fsm/FsmPatcher.cs index a34df891..9dac82d3 100644 --- a/HKMP/Fsm/FsmPatcher.cs +++ b/HKMP/Fsm/FsmPatcher.cs @@ -1,5 +1,5 @@ -using Hkmp.Logging; using Hkmp.Util; +using Hkmp.Logging; using HutongGames.PlayMaker.Actions; namespace Hkmp.Fsm; @@ -36,5 +36,28 @@ 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("Init", 6); + var findBrawler2 = self.GetAction("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; + } + }); + } } } diff --git a/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs b/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs index 080e2610..5ec9eacd 100644 --- a/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs +++ b/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs @@ -1782,11 +1782,15 @@ private static void ApplyNetworkDataFromAction(EntityNetworkData data, SendEvent if (action.delay.Value < 1.0 / 1000.0) { action.Fsm.Event(action.eventTarget, action.sendEvent.Value); } else { - action.Fsm.DelayedEvent( - action.eventTarget, - FsmEvent.GetFsmEvent(action.sendEvent.Value), - action.delay.Value - ); + // We need to delay the event sending ourselves, because the FSM that we are executing in is not enabled + // The usual implementation of SendEventByName will thus not work + MonoBehaviourUtil.Instance.StartCoroutine(DelayEvent()); + + IEnumerator DelayEvent() { + yield return new WaitForSeconds(action.delay.Value); + + action.Fsm.Event(action.eventTarget, action.sendEvent.Value); + } } } @@ -1806,11 +1810,15 @@ private static void ApplyNetworkDataFromAction(EntityNetworkData data, SendEvent if (action.delay.Value < 1.0 / 1000.0) { action.Fsm.Event(action.eventTarget, action.sendEvent.Value); } else { - action.Fsm.DelayedEvent( - action.eventTarget, - FsmEvent.GetFsmEvent(action.sendEvent.Value), - action.delay.Value - ); + // We need to delay the event sending ourselves, because the FSM that we are executing in is not enabled + // The usual implementation of SendEventByNameV2 will thus not work + MonoBehaviourUtil.Instance.StartCoroutine(DelayEvent()); + + IEnumerator DelayEvent() { + yield return new WaitForSeconds(action.delay.Value); + + action.Fsm.Event(action.eventTarget, action.sendEvent.Value); + } } } diff --git a/HKMP/Game/Client/Entity/EntityManager.cs b/HKMP/Game/Client/Entity/EntityManager.cs index 63a29ad6..e0940f9f 100644 --- a/HKMP/Game/Client/Entity/EntityManager.cs +++ b/HKMP/Game/Client/Entity/EntityManager.cs @@ -379,12 +379,15 @@ private void FindEntitiesInScene(Scene scene, bool lateLoad) { var objectsToCheck = Object.FindObjectsOfType() .Where(e => e.gameObject.scene == scene) .SelectMany(enemyDeathEffects => { - if (enemyDeathEffects == null) { + try { + enemyDeathEffects.PreInstantiate(); + } catch (Exception) { + // If we get an exception it might not be possible to pre-instantiate the enemy death effects + // This can happen when the object uses a PersonalObjectPool, which can't be pre-instantiated + // this early, so we return only the original gameobject return new[] { enemyDeathEffects.gameObject }; } - enemyDeathEffects.PreInstantiate(); - var corpse = ReflectionHelper.GetField( enemyDeathEffects, "corpse" @@ -448,4 +451,4 @@ private void FindEntitiesInScene(Scene scene, bool lateLoad) { }.Process(); } } -} \ No newline at end of file +} diff --git a/HKMP/Game/Client/Entity/EntityType.cs b/HKMP/Game/Client/Entity/EntityType.cs index 365b8122..5258ab1a 100644 --- a/HKMP/Game/Client/Entity/EntityType.cs +++ b/HKMP/Game/Client/Entity/EntityType.cs @@ -62,6 +62,7 @@ internal enum EntityType { FungifiedHusk, Shrumeling, ShrumalWarrior, + ShrumalOgre, MantisYouth, MantisWarrior, MantisThrone, diff --git a/HKMP/Resource/entity-registry.json b/HKMP/Resource/entity-registry.json index df05bb07..cbc9bef4 100644 --- a/HKMP/Resource/entity-registry.json +++ b/HKMP/Resource/entity-registry.json @@ -357,6 +357,11 @@ "type": "ShrumalWarrior", "fsm_name": "Mush Roller" }, + { + "base_object_name": "Mushroom Brawler", + "type": "ShrumalOgre", + "fsm_name": "Shroom Brawler" + }, { "base_object_name": "Mantis Flyer Child", "type": "MantisYouth", diff --git a/HKMP/Util/GameObjectExtensions.cs b/HKMP/Util/GameObjectUtil.cs similarity index 54% rename from HKMP/Util/GameObjectExtensions.cs rename to HKMP/Util/GameObjectUtil.cs index 292b8adc..116365e7 100644 --- a/HKMP/Util/GameObjectExtensions.cs +++ b/HKMP/Util/GameObjectUtil.cs @@ -4,9 +4,9 @@ namespace Hkmp.Util; /// -/// Class for GameObject extensions. +/// Class for GameObject utility methods and extensions. /// -internal static class GameObjectExtensions { +internal static class GameObjectUtil { /// /// Find a GameObject with the given name in the children of the given GameObject. /// @@ -30,6 +30,11 @@ string name return null; } + /// + /// Get a list of the children of the given GameObject. + /// + /// The GameObject to get the children for. + /// A list of the children of the GameObject. public static List GetChildren(this GameObject gameObject) { var children = new List(); for (var i = 0; i < gameObject.transform.childCount; i++) { @@ -38,4 +43,22 @@ public static List GetChildren(this GameObject gameObject) { return children; } + + /// + /// Find an inactive GameObject with the given name. + /// + /// The name of the GameObject. + /// The GameObject is it exists, null otherwise. + public static GameObject FindInactiveGameObject(string name) { + var transforms = Resources.FindObjectsOfTypeAll(); + foreach (var transform in transforms) { + if (transform.hideFlags == HideFlags.None) { + if (transform.name == name) { + return transform.gameObject; + } + } + } + + return null; + } } From 545af94b20ef4b1635390f5040ea8b5dd829b518 Mon Sep 17 00:00:00 2001 From: Extremelyd1 Date: Sat, 6 Apr 2024 09:45:13 +0200 Subject: [PATCH 081/216] Fixes for save data syncing, warp on connect --- HKMP/Game/Client/ClientManager.cs | 20 +- HKMP/Game/Client/Save/SaveManager.cs | 309 ++++++++++++++++++++------- HKMP/Resource/scene-data.json | 31 ++- HKMP/Ui/ConnectInterface.cs | 15 +- 4 files changed, 288 insertions(+), 87 deletions(-) diff --git a/HKMP/Game/Client/ClientManager.cs b/HKMP/Game/Client/ClientManager.cs index eb0112e9..a823b590 100644 --- a/HKMP/Game/Client/ClientManager.cs +++ b/HKMP/Game/Client/ClientManager.cs @@ -95,6 +95,12 @@ 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; + #endregion #region IClientManager properties @@ -236,8 +242,11 @@ ModSettings modSettings packetManager.RegisterClientPacketHandler(ClientPacketId.ChatMessage, OnChatMessage); // Register handlers for events from UI - uiManager.ConnectInterface.ConnectButtonPressed += Connect; - uiManager.ConnectInterface.DisconnectButtonPressed += () => Disconnect(); + uiManager.ConnectInterface.ConnectButtonPressed += (address, port, username, autoConnect) => { + _autoConnect = autoConnect; + Connect(address, port, username); + }; + uiManager.ConnectInterface.DisconnectButtonPressed += Disconnect; uiManager.SettingsInterface.OnTeamRadioButtonChange += InternalChangeTeam; uiManager.SettingsInterface.OnSkinIdChange += InternalChangeSkin; @@ -326,6 +335,8 @@ public void Disconnect() { /// Internal logic for disconnecting from the server. /// private void InternalDisconnect() { + _autoConnect = false; + _netClient.Disconnect(); // Let the player manager know we disconnected @@ -511,7 +522,10 @@ private void OnClientConnect(LoginResponse loginResponse) { private void OnHelloClient(HelloClient helloClient) { Logger.Info("Received HelloClient from server"); - _saveManager.SetSaveWithData(helloClient.CurrentSave); + // If this was not an auto-connect, we set save data. Otherwise, we know we already have the save data. + if (!_autoConnect) { + _saveManager.SetSaveWithData(helloClient.CurrentSave); + } // Fill the player data dictionary with the info from the packet foreach (var (id, username) in helloClient.ClientInfo) { diff --git a/HKMP/Game/Client/Save/SaveManager.cs b/HKMP/Game/Client/Save/SaveManager.cs index a67687f9..7e51b53c 100644 --- a/HKMP/Game/Client/Save/SaveManager.cs +++ b/HKMP/Game/Client/Save/SaveManager.cs @@ -1,6 +1,8 @@ using System; +using System.Collections; using System.Collections.Generic; using System.Linq; +using GlobalEnums; using Hkmp.Collection; using Hkmp.Game.Client.Entity; using Hkmp.Networking.Client; @@ -13,7 +15,7 @@ using Logger = Hkmp.Logging.Logger; using Object = UnityEngine.Object; -namespace Hkmp.Game.Client.Save; +namespace Hkmp.Game.Client.Save; /// /// Class that manages save data synchronisation. @@ -24,6 +26,11 @@ internal class SaveManager { /// private const string SaveDataFilePath = "Hkmp.Resource.save-data.json"; + /// + /// The index of the save data entry for the warp. + /// + private const ushort SaveWarpIndex = ushort.MaxValue; + /// /// The save data instances that contains mappings for what to sync and their indices. /// @@ -33,15 +40,17 @@ internal class SaveManager { /// The net client instance to send save updates. /// private readonly NetClient _netClient; + /// /// The packet manager instance to register a callback for when save updates are received. /// private readonly PacketManager _packetManager; + /// /// The entity manager to check whether we are scene host. /// private readonly EntityManager _entityManager; - + /// /// List of data classes for each FSM that has a persistent int/bool or geo rock attached to it. /// @@ -56,7 +65,7 @@ internal class SaveManager { /// Dictionary of BossSequenceDoor.Completion structs in the PlayerData for comparing changes against. /// private readonly Dictionary _bsdCompHashes; - + /// /// Dictionary of BossStatue.Completion structs in the PlayerData for comparing changes against. /// @@ -89,14 +98,13 @@ public void Initialize() { ModHooks.SetPlayerFloatHook += OnSetPlayerFloatHook; ModHooks.SetPlayerIntHook += OnSetPlayerIntHook; ModHooks.SetPlayerStringHook += OnSetPlayerStringHook; - ModHooks.SetPlayerVariableHook += OnSetPlayerVariableHook; ModHooks.SetPlayerVector3Hook += OnSetPlayerVector3Hook; - + UnityEngine.SceneManagement.SceneManager.activeSceneChanged += OnSceneChanged; MonoBehaviourUtil.Instance.OnUpdateEvent += OnUpdatePersistents; MonoBehaviourUtil.Instance.OnUpdateEvent += OnUpdateCompounds; - + _packetManager.RegisterClientPacketHandler(ClientPacketId.SaveUpdate, UpdateSaveWithData); } @@ -120,7 +128,7 @@ byte[] EncodeString(string stringValue) { return BitConverter.GetBytes(index); } - + if (value is bool bValue) { return [(byte) (bValue ? 1 : 0)]; } @@ -148,14 +156,14 @@ byte[] EncodeString(string stringValue) { if (listValue.Count > ushort.MaxValue) { throw new ArgumentOutOfRangeException($"Could not encode string list length: {listValue.Count}"); } - + var length = (ushort) listValue.Count; - + IEnumerable byteArray = BitConverter.GetBytes(length); for (var i = 0; i < length; i++) { var encoded = EncodeString(listValue[i]); - + byteArray = byteArray.Concat(encoded); } @@ -165,8 +173,8 @@ byte[] EncodeString(string stringValue) { if (value is BossSequenceDoor.Completion bsdCompValue) { // For now we only encode the bools of completion struct var firstBools = new[] { - bsdCompValue.canUnlock, bsdCompValue.unlocked, bsdCompValue.completed, bsdCompValue.allBindings, bsdCompValue.noHits, - bsdCompValue.boundNail, bsdCompValue.boundShell, bsdCompValue.boundCharms + bsdCompValue.canUnlock, bsdCompValue.unlocked, bsdCompValue.completed, bsdCompValue.allBindings, + bsdCompValue.noHits, bsdCompValue.boundNail, bsdCompValue.boundShell, bsdCompValue.boundCharms }; var byte1 = EncodeUtil.GetByte(firstBools); @@ -194,28 +202,41 @@ byte[] EncodeString(string stringValue) { /// Name of the boolean variable. /// The original value of the boolean. private bool OnSetPlayerBoolHook(string name, bool orig) { + if (PlayerData.instance.GetBool(name) == orig) { + return orig; + } + CheckSendSaveUpdate(name, () => EncodeValue(orig)); return orig; } - + /// /// Callback method for when a float is set in the player data. /// /// Name of the float variable. /// The original value of the float. private float OnSetPlayerFloatHook(string name, float orig) { + // ReSharper disable once CompareOfFloatsByEqualityOperator + if (PlayerData.instance.GetFloat(name) == orig) { + return orig; + } + CheckSendSaveUpdate(name, () => EncodeValue(orig)); return orig; } - + /// /// Callback method for when a int is set in the player data. /// /// Name of the int variable. /// The original value of the int. private int OnSetPlayerIntHook(string name, int orig) { + if (PlayerData.instance.GetInt(name) == orig) { + return orig; + } + CheckSendSaveUpdate(name, () => EncodeValue(orig)); return orig; @@ -227,31 +248,27 @@ private int OnSetPlayerIntHook(string name, int orig) { /// Name of the string variable. /// The original value of the boolean. private string OnSetPlayerStringHook(string name, string res) { + if (PlayerData.instance.GetString(name) == res) { + return res; + } + CheckSendSaveUpdate(name, () => EncodeValue(res)); return res; } - /// - /// Callback method for when an object is set in the player data. - /// - /// The type of the object. - /// Name of the object variable. - /// The original value of the object. - private object OnSetPlayerVariableHook(Type type, string name, object value) { - CheckSendSaveUpdate(name, () => EncodeValue(value)); - - return value; - } - /// /// Callback method for when a vector3 is set in the player data. /// /// Name of the vector3 variable. /// The original value of the vector3. private Vector3 OnSetPlayerVector3Hook(string name, Vector3 orig) { + if (PlayerData.instance.GetVector3(name) == orig) { + return orig; + } + CheckSendSaveUpdate(name, () => EncodeValue(orig)); - + return orig; } @@ -263,10 +280,10 @@ private Vector3 OnSetPlayerVector3Hook(string name, Vector3 orig) { /// The new scene. private void OnSceneChanged(Scene oldScene, Scene newScene) { _persistentFsmData.Clear(); - + foreach (var geoRock in Object.FindObjectsOfType()) { var geoRockObject = geoRock.gameObject; - + if (geoRockObject.scene != newScene) { continue; } @@ -275,7 +292,7 @@ private void OnSceneChanged(Scene oldScene, Scene newScene) { Id = geoRockObject.name, SceneName = global::GameManager.GetBaseSceneName(geoRockObject.scene.name) }; - + Logger.Info($"Found Geo Rock in scene: {persistentItemData}"); var fsm = geoRock.GetComponent(); @@ -283,7 +300,7 @@ private void OnSceneChanged(Scene oldScene, Scene newScene) { Logger.Info(" Could not find FSM belonging to Geo Rock object, skipping"); continue; } - + var fsmInt = fsm.FsmVariables.GetFsmInt("Hits"); var persistentFsmData = new PersistentFsmData { @@ -294,10 +311,10 @@ private void OnSceneChanged(Scene oldScene, Scene newScene) { _persistentFsmData.Add(persistentFsmData); } - + foreach (var persistentBoolItem in Object.FindObjectsOfType()) { var itemObject = persistentBoolItem.gameObject; - + if (itemObject.scene != newScene) { continue; } @@ -306,15 +323,15 @@ private void OnSceneChanged(Scene oldScene, Scene newScene) { Id = itemObject.name, SceneName = global::GameManager.GetBaseSceneName(itemObject.scene.name) }; - + Logger.Info($"Found persistent bool in scene: {persistentItemData}"); - + var fsm = FSMUtility.FindFSMWithPersistentBool(itemObject.GetComponents()); if (fsm == null) { Logger.Info(" Could not find FSM belonging to persistent bool object, skipping"); continue; } - + var fsmBool = fsm.FsmVariables.GetFsmBool("Activated"); var persistentFsmData = new PersistentFsmData { @@ -325,10 +342,10 @@ private void OnSceneChanged(Scene oldScene, Scene newScene) { _persistentFsmData.Add(persistentFsmData); } - + foreach (var persistentIntItem in Object.FindObjectsOfType()) { var itemObject = persistentIntItem.gameObject; - + if (itemObject.scene != newScene) { continue; } @@ -337,7 +354,7 @@ private void OnSceneChanged(Scene oldScene, Scene newScene) { Id = itemObject.name, SceneName = global::GameManager.GetBaseSceneName(itemObject.scene.name) }; - + Logger.Info($"Found persistent int in scene: {persistentItemData}"); var fsm = FSMUtility.FindFSMWithPersistentBool(itemObject.GetComponents()); @@ -379,9 +396,9 @@ private void CheckSendSaveUpdate(string name, Func encodeFunc) { Logger.Info($"Cannot find save data index, not sending save update ({name})"); return; } - + Logger.Info($"Sending \"{name}\" as save update"); - + _netClient.UpdateManager.SetSaveUpdate( index, encodeFunc.Invoke() @@ -409,11 +426,12 @@ private void OnUpdatePersistents() { persistentFsmData.LastIntValue = value; var itemData = persistentFsmData.PersistentItemData; - + Logger.Info($"Value for {itemData} changed to: {value}"); - + if (!_entityManager.IsSceneHost) { - Logger.Info($"Not scene host, not sending persistent int/geo rock save update ({itemData.Id}, {itemData.SceneName})"); + Logger.Info( + $"Not scene host, not sending persistent int/geo rock save update ({itemData.Id}, {itemData.SceneName})"); continue; } @@ -453,26 +471,29 @@ private void OnUpdatePersistents() { } persistentFsmData.LastBoolValue = value; - + var itemData = persistentFsmData.PersistentItemData; Logger.Info($"Value for {itemData} changed to: {value}"); - + if (!_entityManager.IsSceneHost) { - Logger.Info($"Not scene host, not sending geo rock save update ({itemData.Id}, {itemData.SceneName})"); + Logger.Info( + $"Not scene host, not sending geo rock save update ({itemData.Id}, {itemData.SceneName})"); continue; } - + if (!SaveDataMapping.PersistentBoolDataBools.TryGetValue(itemData, out var shouldSync) || !shouldSync) { - Logger.Info($"Not in persistent bool save data values or false in save data values, not sending save update ({itemData.Id}, {itemData.SceneName})"); + Logger.Info( + $"Not in persistent bool save data values or false in save data values, not sending save update ({itemData.Id}, {itemData.SceneName})"); continue; } if (!SaveDataMapping.PersistentBoolDataIndices.TryGetValue(itemData, out var index)) { - Logger.Info($"Cannot find persistent bool save data index, not sending save update ({itemData.Id}, {itemData.SceneName})"); + Logger.Info( + $"Cannot find persistent bool save data index, not sending save update ({itemData.Id}, {itemData.SceneName})"); continue; } - + Logger.Info($"Sending persistent bool ({itemData.Id}, {itemData.SceneName}) as save update"); _netClient.UpdateManager.SetSaveUpdate( @@ -512,7 +533,7 @@ Func changeFunc if (changeFunc(newCheck, check)) { Logger.Info($"Compound variable ({varName}) changed value"); - + checkDict[varName] = newCheck; if (_netClient.IsConnected && _entityManager.IsSceneHost) { @@ -524,20 +545,20 @@ Func changeFunc } } } - + CheckUpdates, int>( SaveDataMapping.StringListVariables, _stringListHashes, GetStringListHashCode, (hash1, hash2) => hash1 != hash2 ); - + CheckUpdates( SaveDataMapping.BossSequenceDoorCompletionVariables, _bsdCompHashes, bsdComp => bsdComp, - (b1, b2) => - b1.canUnlock != b2.canUnlock || + (b1, b2) => + b1.canUnlock != b2.canUnlock || b1.unlocked != b2.unlocked || b1.completed != b2.completed || b1.allBindings != b2.allBindings || @@ -600,7 +621,7 @@ private void UpdateSaveWithData(ushort index, byte[] encodedValue) { if (SaveDataMapping.PlayerDataIndices.TryGetValue(index, out var name)) { Logger.Info($"Received save update ({index}, {name})"); - + var fieldInfo = typeof(PlayerData).GetField(name); var type = fieldInfo.FieldType; var valueLength = encodedValue.Length; @@ -630,11 +651,7 @@ private void UpdateSaveWithData(ushort index, byte[] encodedValue) { pd.SetIntInternal(name, value); } else if (type == typeof(string)) { - var sceneIndex = BitConverter.ToUInt16(encodedValue, 0); - - if (!EncodeUtil.GetSceneName(sceneIndex, out var value)) { - throw new Exception($"Could not decode string from save update: {encodedValue}"); - } + var value = DecodeString(encodedValue, 0); pd.SetStringInternal(name, value); } else if (type == typeof(Vector3)) { @@ -651,7 +668,7 @@ private void UpdateSaveWithData(ushort index, byte[] encodedValue) { pd.SetVector3Internal(name, value); } else if (type == typeof(List)) { var length = BitConverter.ToUInt16(encodedValue, 0); - + var list = new List(); for (var i = 0; i < length; i++) { var sceneIndex = BitConverter.ToUInt16(encodedValue, 2 + i * 2); @@ -659,10 +676,13 @@ private void UpdateSaveWithData(ushort index, byte[] encodedValue) { if (!EncodeUtil.GetSceneName(sceneIndex, out var sceneName)) { throw new Exception($"Could not decode string in list from save update: {sceneIndex}"); } - + list.Add(sceneName); } + // First set the new string list hash so we don't trigger an update and subsequently a feedback loop + _stringListHashes[name] = GetStringListHashCode(list); + pd.SetVariableInternal(name, list); } else if (type == typeof(BossSequenceDoor.Completion)) { var byte1 = encodedValue[0]; @@ -682,6 +702,10 @@ private void UpdateSaveWithData(ushort index, byte[] encodedValue) { boundSoul = byte2 == 1 }; + // First set the new bsdComp obj in the dict so we don't trigger an update and subsequently a + // feedback loop + _bsdCompHashes[name] = bsdComp; + pd.SetVariableInternal(name, bsdComp); } else if (type == typeof(BossStatue.Completion)) { var bools = EncodeUtil.GetBoolsFromByte(encodedValue[0]); @@ -696,17 +720,30 @@ private void UpdateSaveWithData(ushort index, byte[] encodedValue) { usingAltVersion = bools[6] }; + // First set the new bsComp obj in the dict so we don't trigger an update and subsequently a + // feedback loop + _bsCompHashes[name] = bsComp; + pd.SetVariableInternal(name, bsComp); } else { throw new NotImplementedException($"Could not decode type: {type}"); } } - + if (SaveDataMapping.GeoRockDataIndices.TryGetValue(index, out var itemData)) { var value = encodedValue[0]; - + Logger.Info($"Received geo rock save update: {itemData.Id}, {itemData.SceneName}, {value}"); + // TODO: make the _persistentFsmData a dictionary for quicker lookups + foreach (var persistentFsmData in _persistentFsmData) { + var existingItemData = persistentFsmData.PersistentItemData; + + if (existingItemData.Id == itemData.Id && existingItemData.SceneName == itemData.SceneName) { + persistentFsmData.LastIntValue = value; + } + } + sceneData.SaveMyState(new GeoRockData { id = itemData.Id, sceneName = itemData.SceneName, @@ -714,9 +751,18 @@ private void UpdateSaveWithData(ushort index, byte[] encodedValue) { }); } else if (SaveDataMapping.PersistentBoolDataIndices.TryGetValue(index, out itemData)) { var value = encodedValue[0] == 1; - + Logger.Info($"Received persistent bool save update: {itemData.Id}, {itemData.SceneName}, {value}"); + // TODO: make the _persistentFsmData a dictionary for quicker lookups + foreach (var persistentFsmData in _persistentFsmData) { + var existingItemData = persistentFsmData.PersistentItemData; + + if (existingItemData.Id == itemData.Id && existingItemData.SceneName == itemData.SceneName) { + persistentFsmData.LastBoolValue = value; + } + } + sceneData.SaveMyState(new PersistentBoolData { id = itemData.Id, sceneName = itemData.SceneName, @@ -729,15 +775,48 @@ private void UpdateSaveWithData(ushort index, byte[] encodedValue) { if (value == 255) { value = -1; } - + Logger.Info($"Received persistent int save update: {itemData.Id}, {itemData.SceneName}, {value}"); + // TODO: make the _persistentFsmData a dictionary for quicker lookups + foreach (var persistentFsmData in _persistentFsmData) { + var existingItemData = persistentFsmData.PersistentItemData; + + if (existingItemData.Id == itemData.Id && existingItemData.SceneName == itemData.SceneName) { + persistentFsmData.LastIntValue = value; + } + } + sceneData.SaveMyState(new PersistentIntData { id = itemData.Id, sceneName = itemData.SceneName, value = value }); } + + if (index == SaveWarpIndex) { + // Specific handling of warp bench data + var respawnScene = DecodeString(encodedValue, 0); + var respawnMarkerName = DecodeString(encodedValue, 2); + var mapZone = (MapZone) encodedValue[4]; + + pd.respawnScene = respawnScene; + pd.respawnMarkerName = respawnMarkerName; + pd.mapZone = mapZone; + + MonoBehaviourUtil.Instance.StartCoroutine(WarpToBench()); + } + + // Decode a string from the given byte array and start index in that array using the EncodeUtil + string DecodeString(byte[] encoded, int startIndex) { + var sceneIndex = BitConverter.ToUInt16(encoded, startIndex); + + if (!EncodeUtil.GetSceneName(sceneIndex, out var value)) { + throw new Exception($"Could not decode string from save update: {encodedValue}"); + } + + return value; + } } /// @@ -759,7 +838,7 @@ Func valueFunc ) { foreach (var collectionValue in enumerable) { var key = keyFunc.Invoke(collectionValue); - + if (!boolMapping.TryGetValue(key, out var shouldSync) || !shouldSync) { continue; } @@ -769,11 +848,11 @@ Func valueFunc } var value = valueFunc.Invoke(collectionValue); - + saveData.Add(index, EncodeValue(value)); } } - + AddToSaveData( typeof(PlayerData).GetFields(), fieldInfo => fieldInfo.Name, @@ -781,7 +860,7 @@ Func valueFunc SaveDataMapping.PlayerDataIndices, fieldInfo => fieldInfo.GetValue(pd) ); - + AddToSaveData( sd.geoRocks, geoRock => new PersistentItemData { @@ -792,7 +871,7 @@ Func valueFunc SaveDataMapping.GeoRockDataIndices, geoRock => geoRock.hitsLeft ); - + AddToSaveData( sd.persistentBoolItems, boolData => new PersistentItemData { @@ -803,7 +882,7 @@ Func valueFunc SaveDataMapping.PersistentBoolDataIndices, boolData => boolData.activated ); - + AddToSaveData( sd.persistentIntItems, intData => new PersistentItemData { @@ -814,10 +893,18 @@ Func valueFunc SaveDataMapping.PersistentIntDataIndices, intData => intData.value ); + + // Specific handling of last bench data + var encodedBenchData = new List(); + encodedBenchData.AddRange(EncodeValue(pd.respawnScene)); + encodedBenchData.AddRange(EncodeValue(pd.respawnMarkerName)); + encodedBenchData.Add((byte) pd.mapZone); + + saveData.Add(SaveWarpIndex, encodedBenchData.ToArray()); return saveData; } - + /// /// Get the hash code of the combined values in a string list. /// @@ -828,9 +915,79 @@ private static int GetStringListHashCode(List list) { if (list.Count == 0) { return 0; } - + return list .Select(item => item.GetHashCode()) .Aggregate((total, nextCode) => total ^ nextCode); } + + private static IEnumerator WarpToBench() { + var gm = global::GameManager.instance; + + UIManager.instance.UIClosePauseMenu(); + + // Collection of various redundant attempts to fix the infamous soul orb bug + HeroController.instance.TakeMPQuick(PlayerData.instance.MPCharge); // actually broadcasts the event + HeroController.instance.SetMPCharge(0); + PlayerData.instance.MPReserve = 0; + HeroController.instance.ClearMP(); // useless + PlayMakerFSM.BroadcastEvent("MP DRAIN"); // This is the main fsm path for removing soul from the orb + PlayMakerFSM.BroadcastEvent("MP LOSE"); // This is an alternate path (used for bindings and other things) that actually plays an animation? + PlayMakerFSM.BroadcastEvent("MP RESERVE DOWN"); + + // Set some stuff which would normally be set by LoadSave + HeroController.instance.AffectedByGravity(false); + HeroController.instance.transitionState = HeroTransitionState.EXITING_SCENE; + if (HeroController.SilentInstance != null) { + if (HeroController.instance.cState.onConveyor || HeroController.instance.cState.onConveyorV || + HeroController.instance.cState.inConveyorZone) { + HeroController.instance.GetComponent()?.StopConveyorMove(); + HeroController.instance.cState.inConveyorZone = false; + HeroController.instance.cState.onConveyor = false; + HeroController.instance.cState.onConveyorV = false; + } + + HeroController.instance.cState.nearBench = false; + } + + gm.cameraCtrl.FadeOut(CameraFadeType.LEVEL_TRANSITION); + + yield return new WaitForSecondsRealtime(0.5f); + + // Actually respawn the character + gm.SetPlayerDataBool(nameof(PlayerData.atBench), false); + // Allow the player to have control if they warp to a non-bench while diving or cdashing + if (HeroController.SilentInstance != null) { + HeroController.instance.cState.superDashing = false; + HeroController.instance.cState.spellQuake = false; + } + + gm.ReadyForRespawn(false); + + yield return new WaitWhile(() => gm.IsInSceneTransition); + + EventRegister.SendEvent("UPDATE BLUE HEALTH"); // checks if hp is adjusted for Joni's blessing + + // Revert pause menu timescale + Time.timeScale = 1f; + gm.FadeSceneIn(); + + // We have to set the game non-paused because TogglePauseMenu sucks and UIClosePauseMenu doesn't do it for us. + gm.isPaused = false; + + // Restore various things normally handled by exiting the pause menu. None of these are necessary afaik + GameCameras.instance.ResumeCameraShake(); + if (HeroController.SilentInstance != null) { + HeroController.instance.UnPause(); + } + + MenuButtonList.ClearAllLastSelected(); + + //This allows the next pause to stop the game correctly + TimeController.GenericTimeScale = 1f; + + // Restores audio to normal levels. Unfortunately, some warps pop atm when music changes over + gm.actorSnapshotUnpaused.TransitionTo(0f); + gm.ui.AudioGoToGameplay(.2f); + } } diff --git a/HKMP/Resource/scene-data.json b/HKMP/Resource/scene-data.json index 257ae1bf..16a7002b 100644 --- a/HKMP/Resource/scene-data.json +++ b/HKMP/Resource/scene-data.json @@ -550,5 +550,34 @@ "Intro_Cutscene_Prologue", "Prologue_Excerpt", "Intro_Cutscene", - "Dream_NailCollection" + "Dream_NailCollection", + "RestBench", + "TOWN", + "CLIFFS", + "CROSSROADS", + "BoneBench", + "SHAMAN_TEMPLE", + "FINAL_BOSS", + "GREEN_PATH", + "FOG_CANYON", + "QUEENS_STATION", + "WASTES", + "CITY", + "WATERWAYS", + "GODS_GLORY", + "RestBench (1)", + "DEEPNEST", + "RestBench Return", + "BEASTS_DEN", + "ABYSS", + "OUTSKIRTS", + "COLOSSEUM", + "HIVE", + "MINES", + "RESTING_GROUNDS", + "ROYAL_GARDENS", + "WhiteBench", + "WHITE_PALACE", + "TRAM_UPPER", + "TRAM_LOWER" ] diff --git a/HKMP/Ui/ConnectInterface.cs b/HKMP/Ui/ConnectInterface.cs index bf073bdf..559b37c0 100644 --- a/HKMP/Ui/ConnectInterface.cs +++ b/HKMP/Ui/ConnectInterface.cs @@ -107,7 +107,7 @@ internal class ConnectInterface { /// /// Event that is executed when the connect button is pressed. /// - public event Action ConnectButtonPressed; + public event Action ConnectButtonPressed; /// /// Event that is executed when the disconnect button is pressed. @@ -142,7 +142,7 @@ ComponentGroup settingsGroup /// public void OnClientDisconnect() { _connectionButton.SetText(ConnectText); - _connectionButton.SetOnPress(OnConnectButtonPressed); + _connectionButton.SetOnPress(() => OnConnectButtonPressed()); _connectionButton.SetInteractable(true); } @@ -272,7 +272,7 @@ private void CreateConnectUi() { new Vector2(x, y), ConnectText ); - _connectionButton.SetOnPress(OnConnectButtonPressed); + _connectionButton.SetOnPress(() => OnConnectButtonPressed()); y -= ButtonComponent.DefaultHeight + 8f; @@ -312,7 +312,8 @@ private void CreateConnectUi() { /// /// Callback method for when the connect button is pressed. /// - private void OnConnectButtonPressed() { + /// Whether to execute this routine based on auto-connecting from hosting. + private void OnConnectButtonPressed(bool autoConnect = false) { var address = _addressInput.GetInput(); if (address.Length == 0) { @@ -357,7 +358,7 @@ private void OnConnectButtonPressed() { _connectionButton.SetText(ConnectingText); _connectionButton.SetInteractable(false); - ConnectButtonPressed?.Invoke(address, port, username); + ConnectButtonPressed?.Invoke(address, port, username, autoConnect); } /// @@ -371,7 +372,7 @@ private void OnDisconnectButtonPressed() { SetFeedbackText(Color.green, "Successfully disconnected"); _connectionButton.SetText(ConnectText); - _connectionButton.SetOnPress(OnConnectButtonPressed); + _connectionButton.SetOnPress(() => OnConnectButtonPressed()); _connectionButton.SetInteractable(true); } @@ -400,7 +401,7 @@ private void OnStartButtonPressed() { if (_modSettings.AutoConnectWhenHosting) { _addressInput.SetInput(LocalhostAddress); - OnConnectButtonPressed(); + OnConnectButtonPressed(true); // Let the user know that the server has been started SetFeedbackText(Color.green, "Successfully connected to hosted server"); From f20b2d2d4f68a82661d655252396805801c572db Mon Sep 17 00:00:00 2001 From: Extremelyd1 Date: Fri, 10 May 2024 20:21:35 +0200 Subject: [PATCH 082/216] Update UI, fix standalone server saves --- HKMP/Game/Client/ClientManager.cs | 90 ++-- HKMP/Game/Client/Save/SaveDataMapping.cs | 25 + HKMP/Game/Client/Save/SaveManager.cs | 17 +- HKMP/Game/GameManager.cs | 1 - HKMP/Game/Server/ModServerManager.cs | 4 +- HKMP/Game/Server/ServerManager.cs | 10 +- HKMP/Game/Settings/ModSettings.cs | 6 - HKMP/Networking/Client/ClientUpdateManager.cs | 18 +- HKMP/Networking/Packet/Data/HelloServer.cs | 36 +- HKMP/Ui/ConnectInterface.cs | 151 ++---- HKMP/Ui/UiManager.cs | 447 ++++++++++++++---- HKMPServer/ConsoleServerManager.cs | 118 +++++ 12 files changed, 592 insertions(+), 331 deletions(-) diff --git a/HKMP/Game/Client/ClientManager.cs b/HKMP/Game/Client/ClientManager.cs index a823b590..cef36c8c 100644 --- a/HKMP/Game/Client/ClientManager.cs +++ b/HKMP/Game/Client/ClientManager.cs @@ -242,35 +242,23 @@ ModSettings modSettings packetManager.RegisterClientPacketHandler(ClientPacketId.ChatMessage, OnChatMessage); // Register handlers for events from UI - uiManager.ConnectInterface.ConnectButtonPressed += (address, port, username, autoConnect) => { + uiManager.RequestClientConnectEvent += (address, port, username, autoConnect) => { _autoConnect = autoConnect; Connect(address, port, username); }; - uiManager.ConnectInterface.DisconnectButtonPressed += Disconnect; - uiManager.SettingsInterface.OnTeamRadioButtonChange += InternalChangeTeam; - uiManager.SettingsInterface.OnSkinIdChange += InternalChangeSkin; + uiManager.RequestClientDisconnectEvent += Disconnect; + + // uiManager.SettingsInterface.OnTeamRadioButtonChange += InternalChangeTeam; + // uiManager.SettingsInterface.OnSkinIdChange += InternalChangeSkin; 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 + // Register handlers for various things UnityEngine.SceneManagement.SceneManager.activeSceneChanged += OnSceneChange; + On.HeroController.Start += OnHeroControllerStart; On.HeroController.Update += OnPlayerUpdate; // Register client connect and timeout handler @@ -480,39 +468,22 @@ 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"); + _netClient.UpdateManager.SetHelloServerData(_username); + } + + /// + /// Callback method for when the HeroController is started so we can add the username to the player object. + /// + private void OnHeroControllerStart(On.HeroController.orig_Start orig, HeroController self) { + orig(self); - // 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; + if (_netClient.IsConnected) { + _playerManager.AddNameToPlayer( + HeroController.instance.gameObject, + _username, + _playerManager.LocalPlayerTeam + ); } - - var transform = HeroController.instance.transform; - var position = transform.position; - - Logger.Info("Sending Hello packet"); - - _netClient.UpdateManager.SetHelloServerData( - _username, - 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"); } /// @@ -525,6 +496,7 @@ private void OnHelloClient(HelloClient helloClient) { // If this was not an auto-connect, we set save data. Otherwise, we know we already have the save data. if (!_autoConnect) { _saveManager.SetSaveWithData(helloClient.CurrentSave); + _uiManager.EnterGameFromMultiplayerMenu(); } // Fill the player data dictionary with the info from the packet @@ -553,6 +525,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(); @@ -917,7 +891,7 @@ private void OnServerSettingsUpdated(ServerSettingsUpdate update) { _playerManager.ResetAllTeams(); } - _uiManager.OnTeamSettingChange(); + // _uiManager.OnTeamSettingChange(); } // If the allow skins setting changed and it is no longer allowed, we reset all existing skins @@ -952,12 +926,10 @@ private void OnSceneChange(Scene oldScene, Scene newScene) { // 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)) { - return; + // If the old scene is a gameplay scene, we need to notify the server that we left + if (!SceneUtil.IsNonGameplayScene(oldScene.name)) { + _netClient.UpdateManager.SetLeftScene(); } - - _netClient.UpdateManager.SetLeftScene(); } /// @@ -991,7 +963,6 @@ private void OnPlayerUpdate(On.HeroController.orig_Update orig, HeroController s 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; @@ -1049,7 +1020,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(); diff --git a/HKMP/Game/Client/Save/SaveDataMapping.cs b/HKMP/Game/Client/Save/SaveDataMapping.cs index 7f593c19..1eb442fb 100644 --- a/HKMP/Game/Client/Save/SaveDataMapping.cs +++ b/HKMP/Game/Client/Save/SaveDataMapping.cs @@ -2,6 +2,7 @@ using System.Linq; using Hkmp.Collection; using Hkmp.Logging; +using Hkmp.Util; using Newtonsoft.Json; namespace Hkmp.Game.Client.Save; @@ -10,6 +11,30 @@ namespace Hkmp.Game.Client.Save; /// Serializable data class that stores mappings for what scene data should be synchronised and their indices used for networking. /// internal class SaveDataMapping { + /// + /// The file path of the embedded resource file for save data. + /// + private const string SaveDataFilePath = "Hkmp.Resource.save-data.json"; + + /// + /// The static instance of the mapping. + /// + [JsonIgnore] + private static SaveDataMapping _instance; + + /// + [JsonIgnore] + public static SaveDataMapping Instance { + get { + if (_instance == null) { + _instance = FileUtil.LoadObjectFromEmbeddedJson(SaveDataFilePath); + _instance.Initialize(); + } + + return _instance; + } + } + /// /// Dictionary mapping player data values to booleans indicating whether they should be synchronised. /// diff --git a/HKMP/Game/Client/Save/SaveManager.cs b/HKMP/Game/Client/Save/SaveManager.cs index 7e51b53c..2fe42798 100644 --- a/HKMP/Game/Client/Save/SaveManager.cs +++ b/HKMP/Game/Client/Save/SaveManager.cs @@ -21,20 +21,15 @@ namespace Hkmp.Game.Client.Save; /// Class that manages save data synchronisation. /// internal class SaveManager { - /// - /// The file path of the embedded resource file for save data. - /// - private const string SaveDataFilePath = "Hkmp.Resource.save-data.json"; - /// /// The index of the save data entry for the warp. /// private const ushort SaveWarpIndex = ushort.MaxValue; /// - /// The save data instances that contains mappings for what to sync and their indices. + /// The save data instance that contains mappings for what to sync and their indices. /// - private static readonly SaveDataMapping SaveDataMapping; + private static SaveDataMapping SaveDataMapping => SaveDataMapping.Instance; /// /// The net client instance to send save updates. @@ -82,14 +77,6 @@ public SaveManager(NetClient netClient, PacketManager packetManager, EntityManag _bsCompHashes = new Dictionary(); } - /// - /// Static constructor to load and initialize the save data mapping. - /// - static SaveManager() { - SaveDataMapping = FileUtil.LoadObjectFromEmbeddedJson(SaveDataFilePath); - SaveDataMapping.Initialize(); - } - /// /// Initializes the save manager by loading the save data json. /// diff --git a/HKMP/Game/GameManager.cs b/HKMP/Game/GameManager.cs index 4d53dbf9..dd91b2e8 100644 --- a/HKMP/Game/GameManager.cs +++ b/HKMP/Game/GameManager.cs @@ -37,7 +37,6 @@ public GameManager(ModSettings modSettings) { var serverServerSettings = modSettings.ServerSettings; var uiManager = new UiManager( - clientServerSettings, modSettings, netClient ); diff --git a/HKMP/Game/Server/ModServerManager.cs b/HKMP/Game/Server/ModServerManager.cs index cb37e1c6..b855fe31 100644 --- a/HKMP/Game/Server/ModServerManager.cs +++ b/HKMP/Game/Server/ModServerManager.cs @@ -22,11 +22,11 @@ UiManager uiManager ModHooks.FinishedLoadingModsHook += AddonManager.LoadAddons; // Register handlers for UI events - uiManager.ConnectInterface.StartHostButtonPressed += port => { + uiManager.RequestServerStartHostEvent += port => { CurrentSaveData = SaveManager.GetCurrentSaveData(); Start(port); }; - uiManager.ConnectInterface.StopHostButtonPressed += Stop; + uiManager.RequestServerStopHostEvent += Stop; // Register application quit handler ModHooks.ApplicationQuitHook += Stop; diff --git a/HKMP/Game/Server/ServerManager.cs b/HKMP/Game/Server/ServerManager.cs index 4b3a82bd..2cf37c24 100644 --- a/HKMP/Game/Server/ServerManager.cs +++ b/HKMP/Game/Server/ServerManager.cs @@ -253,11 +253,6 @@ private void OnHelloServer(ushort id, HelloServer helloServer) { return; } - playerData.CurrentScene = helloServer.SceneName; - playerData.Position = helloServer.Position; - playerData.Scale = helloServer.Scale; - playerData.AnimationId = helloServer.AnimationClipId; - var clientInfo = new List<(ushort, string)>(); foreach (var idPlayerDataPair in _playerData) { @@ -1288,7 +1283,7 @@ private void OnChatMessage(ushort id, ChatMessage chatMessage) { /// /// The ID of the player. /// The SaveUpdate packet data. - private void OnSaveUpdate(ushort id, SaveUpdate packet) { + protected virtual void OnSaveUpdate(ushort id, SaveUpdate packet) { if (!_playerData.TryGetValue(id, out var playerData)) { Logger.Debug($"Could not process save update from unknown player ID: {id}"); return; @@ -1300,6 +1295,9 @@ private void OnSaveUpdate(ushort id, SaveUpdate packet) { Logger.Info(" Player is not scene host, not broadcasting update"); return; } + + // The save update is valid so we store it in our current save + CurrentSaveData[packet.SaveDataIndex] = packet.Value; foreach (var idPlayerDataPair in _playerData) { var otherId = idPlayerDataPair.Key; diff --git a/HKMP/Game/Settings/ModSettings.cs b/HKMP/Game/Settings/ModSettings.cs index fc92a93d..cff11676 100644 --- a/HKMP/Game/Settings/ModSettings.cs +++ b/HKMP/Game/Settings/ModSettings.cs @@ -14,12 +14,6 @@ internal class ModSettings { /// public string AuthKey { get; set; } = null; - /// - /// The key to hide the HKMP UI. - /// - [JsonConverter(typeof(StringEnumConverter))] - public KeyCode HideUiKey { get; set; } = KeyCode.RightAlt; - /// /// The key to open the chat. /// diff --git a/HKMP/Networking/Client/ClientUpdateManager.cs b/HKMP/Networking/Client/ClientUpdateManager.cs index ccc8d645..31a33f6d 100644 --- a/HKMP/Networking/Client/ClientUpdateManager.cs +++ b/HKMP/Networking/Client/ClientUpdateManager.cs @@ -358,26 +358,12 @@ public void SetSkinUpdate(byte skinId) { /// Set hello server data in the current packet. /// /// The username of the player. - /// The name of the current scene of the player. - /// The position of the player. - /// The scale of the player. - /// The animation clip ID of the player. - public void SetHelloServerData( - string username, - string sceneName, - Vector2 position, - bool scale, - ushort animationClipId - ) { + public void SetHelloServerData(string username) { lock (Lock) { CurrentUpdatePacket.SetSendingPacketData( ServerPacketId.HelloServer, new HelloServer { - Username = username, - SceneName = sceneName, - Position = position, - Scale = scale, - AnimationClipId = animationClipId + Username = username } ); } diff --git a/HKMP/Networking/Packet/Data/HelloServer.cs b/HKMP/Networking/Packet/Data/HelloServer.cs index eb3ea616..f3f1bb26 100644 --- a/HKMP/Networking/Packet/Data/HelloServer.cs +++ b/HKMP/Networking/Packet/Data/HelloServer.cs @@ -1,6 +1,4 @@ -using Hkmp.Math; - -namespace Hkmp.Networking.Packet.Data; +namespace Hkmp.Networking.Packet.Data; /// /// Packet data for the hello server data. @@ -17,45 +15,13 @@ internal class HelloServer : IPacketData { /// public string Username { get; set; } - /// - /// The name of the current scene of the player. - /// - public string SceneName { get; set; } - - /// - /// The position of the player. - /// - public Vector2 Position { get; set; } - - /// - /// The scale of the player. - /// - public bool Scale { get; set; } - - /// - /// The animation clip ID of the player. - /// - public ushort AnimationClipId { get; set; } - /// public void WriteData(IPacket packet) { packet.Write(Username); - packet.Write(SceneName); - - packet.Write(Position); - packet.Write(Scale); - - packet.Write(AnimationClipId); } /// public void ReadData(IPacket packet) { Username = packet.ReadString(); - SceneName = packet.ReadString(); - - Position = packet.ReadVector2(); - Scale = packet.ReadBool(); - - AnimationClipId = packet.ReadUShort(); } } diff --git a/HKMP/Ui/ConnectInterface.cs b/HKMP/Ui/ConnectInterface.cs index 559b37c0..77dfec39 100644 --- a/HKMP/Ui/ConnectInterface.cs +++ b/HKMP/Ui/ConnectInterface.cs @@ -14,11 +14,6 @@ namespace Hkmp.Ui; /// Class for creating and managing the connect interface. /// internal class ConnectInterface { - /// - /// The address to connect to the local device. - /// - private const string LocalhostAddress = "127.0.0.1"; - /// /// The indent of some text elements. /// @@ -34,21 +29,11 @@ internal class ConnectInterface { /// private const string ConnectingText = "Connecting..."; - /// - /// The text of the connection button while connected. - /// - private const string DisconnectText = "Disconnect"; - /// /// The text of the host button while not hosting. /// private const string StartHostingText = "Start Hosting"; - /// - /// The text of the host button while hosting. - /// - private const string StopHostingText = "Stop Hosting"; - /// /// The time in seconds to hide the feedback text after it appeared. /// @@ -64,10 +49,10 @@ internal class ConnectInterface { /// private readonly ComponentGroup _connectGroup; - /// - /// The component group of the client settings UI. - /// - private readonly ComponentGroup _settingsGroup; + // /// + // /// The component group of the client settings UI. + // /// + // private readonly ComponentGroup _settingsGroup; /// /// The username input component. @@ -107,32 +92,22 @@ internal class ConnectInterface { /// /// Event that is executed when the connect button is pressed. /// - public event Action ConnectButtonPressed; - - /// - /// Event that is executed when the disconnect button is pressed. - /// - public event Action DisconnectButtonPressed; + public event Action ConnectButtonPressed; /// /// Event that is executed when the start hosting button is pressed. /// - public event Action StartHostButtonPressed; - - /// - /// The event that is executed when the stop hosting button is pressed. - /// - public event Action StopHostButtonPressed; + public event Action StartHostButtonPressed; public ConnectInterface( ModSettings modSettings, - ComponentGroup connectGroup, - ComponentGroup settingsGroup + ComponentGroup connectGroup + // ComponentGroup settingsGroup ) { _modSettings = modSettings; _connectGroup = connectGroup; - _settingsGroup = settingsGroup; + // _settingsGroup = settingsGroup; CreateConnectUi(); } @@ -142,7 +117,7 @@ ComponentGroup settingsGroup /// public void OnClientDisconnect() { _connectionButton.SetText(ConnectText); - _connectionButton.SetOnPress(() => OnConnectButtonPressed()); + _connectionButton.SetOnPress(OnConnectButtonPressed); _connectionButton.SetInteractable(true); } @@ -153,9 +128,8 @@ public void OnSuccessfulConnect() { // Let the user know that the connection was successful SetFeedbackText(Color.green, "Successfully connected"); - // Reset the connection button with the disconnect text and callback - _connectionButton.SetText(DisconnectText); - _connectionButton.SetOnPress(OnDisconnectButtonPressed); + // Reset the connection button with the disconnect text + _connectionButton.SetText(ConnectText); _connectionButton.SetInteractable(true); } @@ -200,7 +174,7 @@ public void OnFailedConnect(ConnectFailedResult result) { private void CreateConnectUi() { // Now we can start adding individual components to our UI // Keep track of current x and y of objects we want to place - var x = 1920f - 210f; + var x = 1920f / 2f; var y = 1080f - 100f; const float labelHeight = 20f; @@ -285,15 +259,15 @@ private void CreateConnectUi() { y -= ButtonComponent.DefaultHeight + 8f; - var settingsButton = new ButtonComponent( - _connectGroup, - new Vector2(x, y), - "Settings" - ); - settingsButton.SetOnPress(() => { - _connectGroup.SetActive(false); - _settingsGroup.SetActive(true); - }); + // var settingsButton = new ButtonComponent( + // _connectGroup, + // new Vector2(x, y), + // "Settings" + // ); + // settingsButton.SetOnPress(() => { + // _connectGroup.SetActive(false); + // _settingsGroup.SetActive(true); + // }); y -= ButtonComponent.DefaultHeight + 8f; @@ -312,8 +286,7 @@ private void CreateConnectUi() { /// /// Callback method for when the connect button is pressed. /// - /// Whether to execute this routine based on auto-connecting from hosting. - private void OnConnectButtonPressed(bool autoConnect = false) { + private void OnConnectButtonPressed() { var address = _addressInput.GetInput(); if (address.Length == 0) { @@ -335,14 +308,7 @@ private void OnConnectButtonPressed(bool autoConnect = false) { Logger.Debug($"Connect button pressed, address: {address}:{port}"); - var username = _usernameInput.GetInput(); - if (username.Length == 0 || username.Length > 20) { - if (username.Length > 20) { - SetFeedbackText(Color.red, "Failed to connect:\nUsername is too long"); - } else if (username.Length == 0) { - SetFeedbackText(Color.red, "Failed to connect:\nYou must enter a username"); - } - + if (!ValidateUsername(out var username)) { return; } @@ -358,22 +324,7 @@ private void OnConnectButtonPressed(bool autoConnect = false) { _connectionButton.SetText(ConnectingText); _connectionButton.SetInteractable(false); - ConnectButtonPressed?.Invoke(address, port, username, autoConnect); - } - - /// - /// Callback method for when the disconnect button is pressed. - /// - private void OnDisconnectButtonPressed() { - // Disconnect the client - DisconnectButtonPressed?.Invoke(); - - // Let the user know that the connection was successful - SetFeedbackText(Color.green, "Successfully disconnected"); - - _connectionButton.SetText(ConnectText); - _connectionButton.SetOnPress(() => OnConnectButtonPressed()); - _connectionButton.SetInteractable(true); + ConnectButtonPressed?.Invoke(address, port, username); } /// @@ -389,40 +340,13 @@ private void OnStartButtonPressed() { return; } - - // Start the server in networkManager - StartHostButtonPressed?.Invoke(port); - - _serverButton.SetText(StopHostingText); - _serverButton.SetOnPress(OnStopButtonPressed); - - // If the setting for automatically connecting when hosting is enabled, - // we connect the client to itself as well - if (_modSettings.AutoConnectWhenHosting) { - _addressInput.SetInput(LocalhostAddress); - - OnConnectButtonPressed(true); - - // Let the user know that the server has been started - SetFeedbackText(Color.green, "Successfully connected to hosted server"); - } else { - // Let the user know that the server has been started - SetFeedbackText(Color.green, "Successfully started server"); + + if (!ValidateUsername(out var username)) { + return; } - } - - /// - /// Callback method for when the stop hosting button is pressed. - /// - private void OnStopButtonPressed() { - // Stop the server in networkManager - StopHostButtonPressed?.Invoke(); - - _serverButton.SetText(StartHostingText); - _serverButton.SetOnPress(OnStartButtonPressed); - // Let the user know that the server has been stopped - SetFeedbackText(Color.green, "Successfully stopped server"); + // Start the server in networkManager + StartHostButtonPressed?.Invoke(username, port); } /// @@ -451,4 +375,19 @@ private IEnumerator WaitHideFeedbackText() { _feedbackText.SetActive(false); } + + private bool ValidateUsername(out string username) { + username = _usernameInput.GetInput(); + if (username.Length == 0 || username.Length > 20) { + if (username.Length > 20) { + SetFeedbackText(Color.red, "Failed to connect:\nUsername is too long"); + } else if (username.Length == 0) { + SetFeedbackText(Color.red, "Failed to connect:\nYou must enter a username"); + } + + return false; + } + + return true; + } } diff --git a/HKMP/Ui/UiManager.cs b/HKMP/Ui/UiManager.cs index 9941af0f..920e8352 100644 --- a/HKMP/Ui/UiManager.cs +++ b/HKMP/Ui/UiManager.cs @@ -1,4 +1,7 @@ -using GlobalEnums; +using System; +using System.Collections; +using System.Collections.Generic; +using GlobalEnums; using Hkmp.Api.Client; using Hkmp.Game.Settings; using Hkmp.Networking.Client; @@ -9,6 +12,7 @@ using UnityEngine.EventSystems; using UnityEngine.UI; using Logger = Hkmp.Logging.Logger; +using Object = UnityEngine.Object; namespace Hkmp.Ui; @@ -35,6 +39,26 @@ internal class UiManager : IUiManager { /// The font size of sub text. /// public const int SubTextFontSize = 22; + + /// + /// The address to connect to the local device. + /// + private const string LocalhostAddress = "127.0.0.1"; + + /// + /// Expression for the GameManager instance. + /// + private static GameManager GM => GameManager.instance; + + /// + /// Expression for the UIManager instance. + /// + private static UIManager UM => UIManager.instance; + + /// + /// Expression for the InputHandler instance. + /// + private static InputHandler IH => InputHandler.Instance; /// /// The global GameObject in which all UI is created. @@ -45,37 +69,50 @@ internal class UiManager : IUiManager { /// The chat box instance. /// internal static ChatBox InternalChatBox; - + /// - /// The connect interface. + /// Event that is fired when a server is requested to be hosted from the UI. /// - public ConnectInterface ConnectInterface { get; } + public event Action RequestServerStartHostEvent; /// - /// The client settings interface. + /// Event that is fired when a server is requested to be stopped. /// - public ClientSettingsInterface SettingsInterface { get; } + public event Action RequestServerStopHostEvent; /// - /// The mod settings. + /// Event that is fired when a connection is requested with the given username, IP, port and whether it was a + /// connection from hosting. /// - private readonly ModSettings _modSettings; + public event Action RequestClientConnectEvent; /// - /// The ping interface. + /// Event that is fired when a disconnect is requested. /// - private readonly PingInterface _pingInterface; + public event Action RequestClientDisconnectEvent; + + // /// + // /// The client settings interface. + // /// + // public ClientSettingsInterface SettingsInterface { get; } /// - /// Whether the UI is hidden by the key-bind. + /// The connect interface. /// - private bool _isUiHiddenByKeyBind; - + private readonly ConnectInterface _connectInterface; + /// - /// Whether the game is in a state where we normally show the pause menu UI for example in a gameplay - /// scene in the HK pause menu. + /// The ping interface. /// - private bool _canShowPauseUi; + private readonly PingInterface _pingInterface; + + private readonly ComponentGroup _pauseMenuGroup; + + private GameObject _backButtonObj; + + private List _originalBackTriggers; + + private Action _hostSaveSlotSelectedAction; #endregion @@ -87,12 +124,9 @@ internal class UiManager : IUiManager { #endregion public UiManager( - ServerSettings clientServerSettings, ModSettings modSettings, NetClient netClient ) { - _modSettings = modSettings; - // First we create a gameObject that will hold all other objects of the UI UiGameObject = new GameObject(); @@ -114,6 +148,7 @@ NetClient netClient var canvasScaler = UiGameObject.AddComponent(); canvasScaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize; canvasScaler.referenceResolution = new Vector2(1920f, 1080f); + canvasScaler.screenMatchMode = CanvasScaler.ScreenMatchMode.Expand; UiGameObject.AddComponent(); @@ -122,15 +157,16 @@ NetClient netClient var uiGroup = new ComponentGroup(); var pauseMenuGroup = new ComponentGroup(false, uiGroup); + _pauseMenuGroup = pauseMenuGroup; var connectGroup = new ComponentGroup(parent: pauseMenuGroup); - var settingsGroup = new ComponentGroup(parent: pauseMenuGroup); + // var settingsGroup = new ComponentGroup(parent: pauseMenuGroup); - ConnectInterface = new ConnectInterface( + _connectInterface = new ConnectInterface( modSettings, - connectGroup, - settingsGroup + connectGroup + // settingsGroup ); var inGameGroup = new ComponentGroup(parent: uiGroup); @@ -147,32 +183,21 @@ NetClient netClient netClient ); - SettingsInterface = new ClientSettingsInterface( - modSettings, - clientServerSettings, - settingsGroup, - connectGroup, - _pingInterface - ); + // SettingsInterface = new ClientSettingsInterface( + // modSettings, + // clientServerSettings, + // settingsGroup, + // connectGroup, + // _pingInterface + // ); // Register callbacks to make sure the UI is hidden and shown at correct times On.UIManager.SetState += (orig, self, state) => { orig(self, state); if (state == UIState.PAUSED) { - // Only show UI in gameplay scenes - if (!SceneUtil.IsNonGameplayScene(SceneUtil.GetCurrentSceneName())) { - _canShowPauseUi = true; - - pauseMenuGroup.SetActive(!_isUiHiddenByKeyBind); - } - inGameGroup.SetActive(false); } else { - pauseMenuGroup.SetActive(false); - - _canShowPauseUi = false; - // Only show chat box UI in gameplay scenes if (!SceneUtil.IsNonGameplayScene(SceneUtil.GetCurrentSceneName())) { inGameGroup.SetActive(true); @@ -180,18 +205,10 @@ NetClient netClient } }; UnityEngine.SceneManagement.SceneManager.activeSceneChanged += (oldScene, newScene) => { - if (SceneUtil.IsNonGameplayScene(newScene.name)) { - eventSystem.enabled = false; - - _canShowPauseUi = false; - - pauseMenuGroup.SetActive(false); - inGameGroup.SetActive(false); - } else { - eventSystem.enabled = true; - - inGameGroup.SetActive(true); - } + var isNonGamePlayScene = SceneUtil.IsNonGameplayScene(newScene.name); + + eventSystem.enabled = !isNonGamePlayScene; + inGameGroup.SetActive(!isNonGamePlayScene); }; // The game is automatically unpaused when the knight dies, so we need @@ -199,83 +216,341 @@ NetClient netClient // TODO: this still gives issues, since it displays the cursor while we are supposed to be unpaused ModHooks.AfterPlayerDeadHook += () => { pauseMenuGroup.SetActive(false); }; - MonoBehaviourUtil.Instance.OnUpdateEvent += () => { CheckKeyBinds(uiGroup); }; + ModHooks.LanguageGetHook += (key, sheet, orig) => { + if (key == "StartMultiplayerBtn" && sheet == "MainMenu") { + return "Start Multiplayer"; + } + + if (key == "MODAL_PROGRESS" && sheet == "MainMenu" && netClient.IsConnected) { + return "You will be disconnected"; + } + + return orig; + }; + + On.UIManager.UIGoToMainMenu += (orig, self) => { + orig(self); + + TryAddMultiOption(); + }; + + TryAddMultiOption(); + + var achievementsMenuControls = UM.achievementsMenuScreen.gameObject.FindGameObjectInChildren("Controls"); + if (achievementsMenuControls == null) { + Logger.Warn("achievementsMenuControls is null"); + return; + } + + var achievementsBackBtn = achievementsMenuControls.FindGameObjectInChildren("BackButton"); + if (achievementsBackBtn == null) { + Logger.Warn("achievementsBackBtn is null"); + return; + } + + _backButtonObj = Object.Instantiate(achievementsBackBtn, UiGameObject.transform); + _backButtonObj.SetActive(false); + + var eventTrigger = _backButtonObj.GetComponent(); + eventTrigger.triggers.Clear(); + + ChangeBtnTriggers(eventTrigger, () => UIManager.instance.StartCoroutine(ReturnToMainMenu())); + + _connectInterface.StartHostButtonPressed += (username, port) => { + _hostSaveSlotSelectedAction = SaveSlotSelectedCallback; + + On.GameManager.StartNewGame += OnStartNewGame; + On.GameManager.ContinueGame += OnContinueGame; + + void SaveSlotSelectedCallback() { + RequestServerStartHostEvent?.Invoke(port); + RequestClientConnectEvent?.Invoke(LocalhostAddress, port, username, true); + + On.GameManager.StartNewGame -= OnStartNewGame; + On.GameManager.ContinueGame -= OnContinueGame; + } + + UM.StartCoroutine(GoToSaveMenu()); + }; + + _connectInterface.ConnectButtonPressed += (address, port, username) => { + RequestClientConnectEvent?.Invoke(address, port, username, false); + }; + + On.UIManager.ReturnToMainMenu += (orig, self) => { + RequestClientDisconnectEvent?.Invoke(); + RequestServerStopHostEvent?.Invoke(); + + return orig(self); + }; } + + /// + /// Enter the game with the current PlayerData from the multiplayer menu. This assumes that the PlayerData + /// instance is populated with values already. + /// + public void EnterGameFromMultiplayerMenu() { + IH.StopUIInput(); - #region Internal UI manager methods + _pauseMenuGroup.SetActive(false); + _backButtonObj.SetActive(false); + UM.uiAudioPlayer.PlayStartGame(); + if (MenuStyles.Instance) { + MenuStyles.Instance.StopAudio(); + } + + GM.ContinueGame(); + } + /// - /// Callback method for when the client successfully connects. + /// Return to the main menu from in-game. Used whenever the player disconnects from the current server. /// - public void OnSuccessfulConnect() { - ConnectInterface.OnSuccessfulConnect(); - _pingInterface.SetEnabled(true); - SettingsInterface.OnSuccessfulConnect(); + public void ReturnToMainMenuFromGame() { + IH.StopUIInput(); + + UM.StartCoroutine(GM.ReturnToMainMenu( + GameManager.ReturnToMainMenuSaveModes.DontSave, + _ => { + UM.StartCoroutine(UM.HideCurrentMenu()); + } + )); + } + + /// + /// Callback method for when a new game is started. This is used to check when to start a hosted server from + /// the save menu. + /// + private void OnStartNewGame(On.GameManager.orig_StartNewGame orig, GameManager self, bool permaDeathMode, bool bossRushMode) { + orig(self, permaDeathMode, bossRushMode); + _hostSaveSlotSelectedAction.Invoke(); } /// - /// Callback method for when the client fails to connect. + /// Callback method for when a save file is continued. This is used to check when to start a hosted server from + /// the save menu. /// - /// The result of the failed connection. - public void OnFailedConnect(ConnectFailedResult result) { - ConnectInterface.OnFailedConnect(result); + private void OnContinueGame(On.GameManager.orig_ContinueGame orig, GameManager self) { + orig(self); + _hostSaveSlotSelectedAction.Invoke(); } /// - /// Callback method for when the client disconnects. + /// Try to add the multiplayer option to the menu screen. Will not add the option if is already exists. /// - public void OnClientDisconnect() { - ConnectInterface.OnClientDisconnect(); - _pingInterface.SetEnabled(false); - SettingsInterface.OnDisconnect(); + private void TryAddMultiOption() { + Logger.Info("AddMultiOption called"); + + var btnParent = UM.mainMenuButtons.gameObject; + if (btnParent == null) { + Logger.Info("btnParent is null"); + return; + } + + if (btnParent.FindGameObjectInChildren("StartMultiplayerButton") != null) { + Logger.Info("Multiplayer button is already present"); + return; + } + + var startGameBtn = UM.mainMenuButtons.startButton.gameObject; + if (startGameBtn == null) { + Logger.Info("startGameBtn is null"); + return; + } + + var startMultiBtn = Object.Instantiate(startGameBtn, btnParent.transform); + if (startMultiBtn == null) { + Logger.Info("startMultiBtn is null"); + return; + } + + startMultiBtn.name = "StartMultiplayerButton"; + startMultiBtn.transform.SetSiblingIndex(1); + + var autoLocalize = startMultiBtn.GetComponent(); + autoLocalize.textKey = "StartMultiplayerBtn"; + autoLocalize.RefreshTextFromLocalization(); + + // Fix navigation for buttons + var startMultiBtnMenuBtn = startMultiBtn.GetComponent(); + if (startMultiBtnMenuBtn != null) { + var nav = UM.mainMenuButtons.startButton.navigation; + nav.selectOnDown = startMultiBtnMenuBtn; + UM.mainMenuButtons.startButton.navigation = nav; + + nav = UM.mainMenuButtons.optionsButton.navigation; + nav.selectOnUp = startMultiBtnMenuBtn; + UM.mainMenuButtons.optionsButton.navigation = nav; + + nav = startMultiBtnMenuBtn.navigation; + nav.selectOnUp = UM.mainMenuButtons.startButton; + startMultiBtnMenuBtn.navigation = nav; + } + + var eventTrigger = startMultiBtn.GetComponent(); + eventTrigger.triggers.Clear(); + + ChangeBtnTriggers(eventTrigger, () => UM.StartCoroutine(GoToMultiplayerMenu())); } /// - /// Callback method for when the team setting in the changes. + /// Coroutine to go to the multiplayer menu of the main menu. /// - public void OnTeamSettingChange() { - SettingsInterface.OnTeamSettingChange(); + private IEnumerator GoToMultiplayerMenu() { + IH.StopUIInput(); + + if (UM.menuState == MainMenuState.MAIN_MENU) { + UM.StartCoroutine(ReflectionHelper.CallMethod(UM, "FadeOutSprite", UM.gameTitle)); + UM.subtitleFSM.SendEvent("FADE OUT"); + yield return UM.StartCoroutine(UM.FadeOutCanvasGroup(UM.mainMenuScreen)); + } else if (UM.menuState == MainMenuState.SAVE_PROFILES) { + yield return UM.StartCoroutine(UM.HideSaveProfileMenu()); + } + + IH.StartUIInput(); + + _pauseMenuGroup.SetActive(true); + _backButtonObj.SetActive(true); } /// - /// Check key-binds to show/hide the UI. + /// Coroutine to go back to the main menu from the multiplayer menu. /// - /// The component group for the entire UI. - private void CheckKeyBinds(ComponentGroup uiGroup) { - if (Input.GetKeyDown((KeyCode) _modSettings.HideUiKey)) { - // Only allow UI toggling within the pause menu, otherwise the chat input might interfere - if (_canShowPauseUi) { - _isUiHiddenByKeyBind = !_isUiHiddenByKeyBind; + /// + private IEnumerator ReturnToMainMenu() { + IH.StopUIInput(); + + _pauseMenuGroup.SetActive(false); + _backButtonObj.SetActive(false); + + UM.gameTitle.gameObject.SetActive(true); + UM.mainMenuScreen.gameObject.SetActive(true); + + if (MenuStyles.Instance) { + MenuStyles.Instance.UpdateTitle(); + } - Logger.Debug($"UI is now {(_isUiHiddenByKeyBind ? "hidden" : "shown")}"); + UM.StartCoroutine(ReflectionHelper.CallMethod(UM, "FadeInSprite", UM.gameTitle)); + UM.subtitleFSM.SendEvent("FADE IN"); - uiGroup.SetActive(!_isUiHiddenByKeyBind); - } + yield return UM.StartCoroutine(UM.FadeInCanvasGroup(UM.mainMenuScreen)); + + UM.mainMenuScreen.interactable = true; + + IH.StartUIInput(); + + yield return null; + + UM.mainMenuButtons.HighlightDefault(); + + UM.menuState = MainMenuState.MAIN_MENU; + } + + /// + /// Coroutine to go to the saves menu from the multiplayer menu. Used whenever the user selects to host a server. + /// + private IEnumerator GoToSaveMenu() { + _pauseMenuGroup.SetActive(false); + _backButtonObj.SetActive(false); + + yield return UM.GoToProfileMenu(); + + var saveProfilesBackBtn = UM.saveProfileControls.gameObject.FindGameObjectInChildren("BackButton"); + if (saveProfilesBackBtn == null) { + Logger.Info("saveProfilesBackBtn is null"); + yield break; } + + var eventTrigger = saveProfilesBackBtn.GetComponent(); + _originalBackTriggers = eventTrigger.triggers; + + eventTrigger.triggers = new List(); + ChangeBtnTriggers(eventTrigger, () => { + On.GameManager.StartNewGame -= OnStartNewGame; + On.GameManager.ContinueGame -= OnContinueGame; + + UM.StartCoroutine(GoToMultiplayerMenu()); + + eventTrigger.triggers = _originalBackTriggers; + }); + } + + /// + /// Change the triggers on a button with the given event trigger. + /// + /// The event trigger of the button to change. + /// The action that should be executed whenever the button is triggered. + private void ChangeBtnTriggers(EventTrigger eventTrigger, Action action) { + var entry = new EventTrigger.Entry { + eventID = EventTriggerType.Submit + }; + entry.callback.AddListener(_ => action.Invoke()); + eventTrigger.triggers.Add(entry); + + var entry2 = new EventTrigger.Entry { + eventID = EventTriggerType.PointerClick + }; + entry2.callback.AddListener(_ => action.Invoke()); + eventTrigger.triggers.Add(entry2); + } + + #region Internal UI manager methods + + /// + /// Callback method for when the client successfully connects. + /// + public void OnSuccessfulConnect() { + _connectInterface.OnSuccessfulConnect(); + _pingInterface.SetEnabled(true); + // SettingsInterface.OnSuccessfulConnect(); } + /// + /// Callback method for when the client fails to connect. + /// + /// The result of the failed connection. + public void OnFailedConnect(ConnectFailedResult result) { + _connectInterface.OnFailedConnect(result); + } + + /// + /// Callback method for when the client disconnects. + /// + public void OnClientDisconnect() { + _connectInterface.OnClientDisconnect(); + _pingInterface.SetEnabled(false); + // SettingsInterface.OnDisconnect(); + } + + // /// + // /// Callback method for when the team setting in the changes. + // /// + // public void OnTeamSettingChange() { + // SettingsInterface.OnTeamSettingChange(); + // } + #endregion #region IUiManager methods /// public void DisableTeamSelection() { - SettingsInterface.OnAddonSetTeamSelection(false); + // SettingsInterface.OnAddonSetTeamSelection(false); } /// public void EnableTeamSelection() { - SettingsInterface.OnAddonSetTeamSelection(true); + // SettingsInterface.OnAddonSetTeamSelection(true); } /// public void DisableSkinSelection() { - SettingsInterface.OnAddonSetSkinSelection(false); + // SettingsInterface.OnAddonSetSkinSelection(false); } /// public void EnableSkinSelection() { - SettingsInterface.OnAddonSetSkinSelection(true); + // SettingsInterface.OnAddonSetSkinSelection(true); } #endregion diff --git a/HKMPServer/ConsoleServerManager.cs b/HKMPServer/ConsoleServerManager.cs index 6bd6c6ee..9e5aad23 100644 --- a/HKMPServer/ConsoleServerManager.cs +++ b/HKMPServer/ConsoleServerManager.cs @@ -1,7 +1,12 @@ using System; +using System.Collections.Generic; +using System.IO; +using System.Reflection; using Hkmp.Game.Server; using Hkmp.Game.Settings; +using Hkmp.Logging; using Hkmp.Networking.Packet; +using Hkmp.Networking.Packet.Data; using Hkmp.Networking.Server; using HkmpServer.Command; using HkmpServer.Logging; @@ -11,10 +16,25 @@ namespace HkmpServer { /// Specialization of the server manager for the console program. /// internal class ConsoleServerManager : ServerManager { + /// + /// Name of the file used to store save data. + /// + private const string SaveFileName = "save.dat"; + /// /// The logger class for logging to console. /// private readonly ConsoleLogger _consoleLogger; + + /// + /// Lock object for asynchronous access to the save file. + /// + private readonly object _saveFileLock = new object(); + + /// + /// The absolute file path of the save file. + /// + private string _saveFilePath; public ConsoleServerManager( NetServer netServer, @@ -35,6 +55,8 @@ ConsoleLogger consoleLogger Stop(); }; + + InitializeSaveFile(); } /// @@ -45,5 +67,101 @@ protected override void RegisterCommands() { CommandManager.RegisterCommand(new ConsoleSettingsCommand(this, InternalServerSettings)); CommandManager.RegisterCommand(new LogCommand(_consoleLogger)); } + + /// + protected override void OnSaveUpdate(ushort id, SaveUpdate packet) { + base.OnSaveUpdate(id, packet); + + // After the server manager has processed the save update, we write the current save data to file + WriteToSaveFile(CurrentSaveData); + } + + /// + /// Initialize the save file by either reading it from disk or creating a new one and writing it to disk. + /// + /// Thrown when the directory of the assembly could not be found. + private void InitializeSaveFile() { + // We first try to get the entry assembly in case the executing assembly was + // embedded in the standalone server + var assembly = Assembly.GetEntryAssembly(); + if (assembly == null) { + // If the entry assembly doesn't exist, we fall back on the executing assembly + assembly = Assembly.GetExecutingAssembly(); + } + + var currentPath = Path.GetDirectoryName(assembly.Location); + if (currentPath == null) { + throw new Exception("Could not get directory of assembly for save file"); + } + + lock (_saveFileLock) { + _saveFilePath = Path.Combine(currentPath, SaveFileName); + + // If the file exists, simply read it into the current save data for the server + // Otherwise, create an empty dictionary for save data and save it to file + if (File.Exists(_saveFilePath) && TryReadSaveFile(out var saveData)) { + CurrentSaveData = saveData; + } else { + CurrentSaveData = new Dictionary(); + + WriteToSaveFile(CurrentSaveData); + } + } + } + + /// + /// Try to read the save data in the save file into the given dictionary. + /// + /// The save data in a dictionary if it was read, otherwise null. + /// true if the save file could be read, false otherwise. + private bool TryReadSaveFile(out Dictionary saveData) { + lock (_saveFileLock) { + // Read the raw bytes from the file + var bytes = File.ReadAllBytes(_saveFilePath); + + try { + // We use the Packet class to easily read the raw bytes in the data + var packet = new Packet(bytes); + + // Then we use the CurrentSave class to parse the packet into the desired format + var currentSave = new CurrentSave(); + currentSave.ReadData(packet); + + saveData = currentSave.SaveData; + return true; + } catch (Exception e) { + Logger.Error($"Could not read the save data from file:\n{e}"); + } + + saveData = null; + return false; + } + } + + /// + /// Write the save data in the given dictionary to the save file. + /// + /// The dictionary containing the save data. + private void WriteToSaveFile(Dictionary saveData) { + lock (_saveFileLock) { + try { + // We use the Packet class to easily write the data to raw bytes + var packet = new Packet(); + + // Then we use the CurrentSave class to write the data into the packet as bytes + var currentSave = new CurrentSave { + SaveData = saveData + }; + currentSave.WriteData(packet); + + // And finally obtain the byte array to write to file + var bytes = packet.ToArray(); + + File.WriteAllBytes(_saveFilePath, bytes); + } catch (Exception e) { + Logger.Error($"Exception occurred while writing to save file:\n{e}"); + } + } + } } } From 2cb9c4a533efa43ef849179e04cd4b6874363d17 Mon Sep 17 00:00:00 2001 From: Extremelyd1 Date: Sat, 11 May 2024 10:40:17 +0200 Subject: [PATCH 083/216] Fix UI softlock and navigation inconsistencies --- HKMP/Game/Client/Save/SaveManager.cs | 4 +++ HKMP/Ui/UiManager.cs | 39 +++++++++++++++++++++------- 2 files changed, 34 insertions(+), 9 deletions(-) diff --git a/HKMP/Game/Client/Save/SaveManager.cs b/HKMP/Game/Client/Save/SaveManager.cs index 2fe42798..1ea7100a 100644 --- a/HKMP/Game/Client/Save/SaveManager.cs +++ b/HKMP/Game/Client/Save/SaveManager.cs @@ -584,6 +584,10 @@ private void UpdateSaveWithData(SaveUpdate saveUpdate) { UpdateSaveWithData(index, value); } + /// + /// Set the save data from the given CurrentSave by overriding all values. + /// + /// The save data to set. public void SetSaveWithData(CurrentSave currentSave) { Logger.Info("Received current save, updating..."); diff --git a/HKMP/Ui/UiManager.cs b/HKMP/Ui/UiManager.cs index 920e8352..606b8f1e 100644 --- a/HKMP/Ui/UiManager.cs +++ b/HKMP/Ui/UiManager.cs @@ -312,7 +312,8 @@ public void ReturnToMainMenuFromGame() { UM.StartCoroutine(GM.ReturnToMainMenu( GameManager.ReturnToMainMenuSaveModes.DontSave, _ => { - UM.StartCoroutine(UM.HideCurrentMenu()); + IH.StartUIInput(); + UM.returnMainMenuPrompt.HighlightDefault(); } )); } @@ -347,8 +348,12 @@ private void TryAddMultiOption() { return; } - if (btnParent.FindGameObjectInChildren("StartMultiplayerButton") != null) { + var startMultiBtn = btnParent.FindGameObjectInChildren("StartMultiplayerButton"); + if (startMultiBtn != null) { Logger.Info("Multiplayer button is already present"); + + FixMultiplayerButtonNavigation(startMultiBtn); + return; } @@ -358,7 +363,7 @@ private void TryAddMultiOption() { return; } - var startMultiBtn = Object.Instantiate(startGameBtn, btnParent.transform); + startMultiBtn = Object.Instantiate(startGameBtn, btnParent.transform); if (startMultiBtn == null) { Logger.Info("startMultiBtn is null"); return; @@ -370,9 +375,30 @@ private void TryAddMultiOption() { var autoLocalize = startMultiBtn.GetComponent(); autoLocalize.textKey = "StartMultiplayerBtn"; autoLocalize.RefreshTextFromLocalization(); + + var eventTrigger = startMultiBtn.GetComponent(); + eventTrigger.triggers.Clear(); + ChangeBtnTriggers(eventTrigger, () => UM.StartCoroutine(GoToMultiplayerMenu())); + + // Fix navigation now and when the IH can accept input again + FixMultiplayerButtonNavigation(startMultiBtn); + + UM.StartCoroutine(WaitForInput()); + IEnumerator WaitForInput() { + yield return new WaitUntil(() => IH.acceptingInput); + + FixMultiplayerButtonNavigation(startMultiBtn); + } + } + + /// + /// Fix the navigation for the multiplayer button that is added to the main menu. + /// + /// The game object for the multiplayer button. + private void FixMultiplayerButtonNavigation(GameObject multiBtnObject) { // Fix navigation for buttons - var startMultiBtnMenuBtn = startMultiBtn.GetComponent(); + var startMultiBtnMenuBtn = multiBtnObject.GetComponent(); if (startMultiBtnMenuBtn != null) { var nav = UM.mainMenuButtons.startButton.navigation; nav.selectOnDown = startMultiBtnMenuBtn; @@ -386,11 +412,6 @@ private void TryAddMultiOption() { nav.selectOnUp = UM.mainMenuButtons.startButton; startMultiBtnMenuBtn.navigation = nav; } - - var eventTrigger = startMultiBtn.GetComponent(); - eventTrigger.triggers.Clear(); - - ChangeBtnTriggers(eventTrigger, () => UM.StartCoroutine(GoToMultiplayerMenu())); } /// From 896103420efcb4a4182546d4e0efe4fd6285c21e Mon Sep 17 00:00:00 2001 From: Extremelyd1 Date: Sun, 19 May 2024 17:41:43 +0200 Subject: [PATCH 084/216] Refactor team selection to command --- HKMP/Api/Client/IClientManager.cs | 1 + HKMP/Api/Client/IUiManager.cs | 3 + HKMP/Game/Client/ClientManager.cs | 30 ---------- HKMP/Game/Client/PlayerManager.cs | 15 +++-- HKMP/Game/Command/Server/TeamCommand.cs | 55 +++++++++++++++++++ HKMP/Game/Server/ServerManager.cs | 35 ++++++++---- HKMP/Networking/Client/ClientUpdateManager.cs | 13 ----- .../Packet/Data/PlayerTeamUpdate.cs | 47 +++++----------- HKMP/Networking/Packet/PacketId.cs | 11 +--- HKMP/Networking/Packet/UpdatePacket.cs | 2 - HKMP/Networking/Server/ServerUpdateManager.cs | 53 +++++++++++++++--- HKMP/Ui/UiManager.cs | 27 --------- 12 files changed, 154 insertions(+), 138 deletions(-) create mode 100644 HKMP/Game/Command/Server/TeamCommand.cs diff --git a/HKMP/Api/Client/IClientManager.cs b/HKMP/Api/Client/IClientManager.cs index 53ed9f15..6cd3c77a 100644 --- a/HKMP/Api/Client/IClientManager.cs +++ b/HKMP/Api/Client/IClientManager.cs @@ -52,6 +52,7 @@ 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); /// 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/Game/Client/ClientManager.cs b/HKMP/Game/Client/ClientManager.cs index cef36c8c..f3f72e20 100644 --- a/HKMP/Game/Client/ClientManager.cs +++ b/HKMP/Game/Client/ClientManager.cs @@ -414,31 +414,6 @@ 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. /// @@ -1062,11 +1037,6 @@ 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); } /// diff --git a/HKMP/Game/Client/PlayerManager.cs b/HKMP/Game/Client/PlayerManager.cs index 32ac1565..8a21b031 100644 --- a/HKMP/Game/Client/PlayerManager.cs +++ b/HKMP/Game/Client/PlayerManager.cs @@ -492,9 +492,16 @@ public void AddNameToPlayer(GameObject playerContainer, string name, Team team = /// /// The ClientPlayerTeamUpdate packet data. private void OnPlayerTeamUpdate(ClientPlayerTeamUpdate playerTeamUpdate) { - var id = playerTeamUpdate.Id; var team = playerTeamUpdate.Team; + + if (playerTeamUpdate.Self) { + Logger.Debug($"Received PlayerTeamUpdate for local player: {Enum.GetName(typeof(Team), team)}"); + UpdateLocalPlayerTeam(team); + return; + } + + var id = playerTeamUpdate.Id; Logger.Debug($"Received PlayerTeamUpdate for ID: {id}, team: {Enum.GetName(typeof(Team), team)}"); UpdatePlayerTeam(id, team); @@ -504,7 +511,7 @@ private void OnPlayerTeamUpdate(ClientPlayerTeamUpdate playerTeamUpdate) { /// Reset the local player's team to be None and reset all existing player names and hit-boxes. /// public void ResetAllTeams() { - OnLocalPlayerTeamUpdate(Team.None); + UpdateLocalPlayerTeam(Team.None); foreach (var id in _playerData.Keys) { UpdatePlayerTeam(id, Team.None); @@ -548,10 +555,10 @@ private void UpdatePlayerTeam(ushort id, Team team) { } /// - /// Callback method for when the team of the local player updates. + /// Update the team for the local player. /// /// The new team of the local player. - public void OnLocalPlayerTeamUpdate(Team team) { + private void UpdateLocalPlayerTeam(Team team) { LocalPlayerTeam = team; var nameObject = HeroController.instance.gameObject.FindGameObjectInChildren(UsernameObjectName); diff --git a/HKMP/Game/Command/Server/TeamCommand.cs b/HKMP/Game/Command/Server/TeamCommand.cs new file mode 100644 index 00000000..232b9808 --- /dev/null +++ b/HKMP/Game/Command/Server/TeamCommand.cs @@ -0,0 +1,55 @@ +using System; +using Hkmp.Api.Command.Server; +using Hkmp.Game.Server; + +namespace Hkmp.Game.Command.Server; + +/// +/// Command for changing the team of the player. +/// +internal class TeamCommand : IServerCommand { + /// + public string Trigger => "/team"; + + /// + public string[] Aliases => Array.Empty(); + + /// + public bool AuthorizedOnly => false; + + /// + /// The server manager instance. + /// + private readonly ServerManager _serverManager; + + public TeamCommand(ServerManager serverManager) { + _serverManager = serverManager; + } + + /// + public void Execute(ICommandSender commandSender, string[] arguments) { + if (commandSender.Type == CommandSenderType.Console) { + commandSender.SendMessage("Console cannot change teams."); + return; + } + + var sender = (PlayerCommandSender) commandSender; + + if (arguments.Length != 2) { + sender.SendMessage($"Usage: {Trigger} "); + return; + } + + var teamName = arguments[1]; + if (!Enum.TryParse(teamName, true, out var team)) { + sender.SendMessage($"Unknown team name: '{teamName}'"); + return; + } + + if (_serverManager.TryUpdatePlayerTeam(sender.Id, team, out var reason)) { + sender.SendMessage($"Team changed to '{team}'"); + } else { + sender.SendMessage(reason); + } + } +} diff --git a/HKMP/Game/Server/ServerManager.cs b/HKMP/Game/Server/ServerManager.cs index 2cf37c24..b7055f96 100644 --- a/HKMP/Game/Server/ServerManager.cs +++ b/HKMP/Game/Server/ServerManager.cs @@ -147,8 +147,6 @@ PacketManager packetManager OnReliableEntityUpdate); packetManager.RegisterServerPacketHandler(ServerPacketId.PlayerDisconnect, OnPlayerDisconnect); packetManager.RegisterServerPacketHandler(ServerPacketId.PlayerDeath, OnPlayerDeath); - packetManager.RegisterServerPacketHandler(ServerPacketId.PlayerTeamUpdate, - OnPlayerTeamUpdate); packetManager.RegisterServerPacketHandler(ServerPacketId.PlayerSkinUpdate, OnPlayerSkinUpdate); packetManager.RegisterServerPacketHandler(ServerPacketId.ChatMessage, OnChatMessage); @@ -184,6 +182,7 @@ protected virtual void RegisterCommands() { CommandManager.RegisterCommand(new AnnounceCommand(_playerData, _netServer)); CommandManager.RegisterCommand(new BanCommand(_banList, this)); CommandManager.RegisterCommand(new KickCommand(this)); + CommandManager.RegisterCommand(new TeamCommand(this)); } /// @@ -985,33 +984,47 @@ private void OnPlayerDeath(ushort id) { } /// - /// Callback method for when a player updates their team. + /// Try to update the team for the player with the given ID. /// /// The ID of the player. - /// The ServerPlayerTeamUpdate packet data. - private void OnPlayerTeamUpdate(ushort id, ServerPlayerTeamUpdate teamUpdate) { + /// The team to change the player to. + /// The reason if the team could not be updated, otherwise null. + /// True if the player's team was updated, false otherwise. + public bool TryUpdatePlayerTeam(ushort id, Team team, out string reason) { if (!_playerData.TryGetValue(id, out var playerData)) { Logger.Warn($"Received PlayerTeamUpdate data, but player with ID {id} is not in mapping"); - return; + + reason = "Could not find player."; + return false; } - Logger.Info($"Received PlayerTeamUpdate data from ({id}, {playerData.Username}), new team: {teamUpdate.Team}"); + Logger.Info($"Received PlayerTeamUpdate data from ({id}, {playerData.Username}) for team: {team}"); + + if (!ServerSettings.TeamsEnabled) { + Logger.Info(" Teams are not enabled, won't update team"); + + reason = "Unable to change team."; + return false; + } // Update the team in the player data - playerData.Team = teamUpdate.Team; + playerData.Team = team; // Broadcast the packet to all players except the player we received the update from foreach (var playerId in _playerData.Keys) { if (id == playerId) { + _netServer.GetUpdateManagerForClient(playerId)?.AddPlayerTeamUpdateData(team); continue; } - _netServer.GetUpdateManagerForClient(playerId)?.AddPlayerTeamUpdateData( + _netServer.GetUpdateManagerForClient(playerId)?.AddOtherPlayerTeamUpdateData( id, - playerData.Username, - teamUpdate.Team + team ); } + + reason = null; + return true; } /// diff --git a/HKMP/Networking/Client/ClientUpdateManager.cs b/HKMP/Networking/Client/ClientUpdateManager.cs index 31a33f6d..e65d633e 100644 --- a/HKMP/Networking/Client/ClientUpdateManager.cs +++ b/HKMP/Networking/Client/ClientUpdateManager.cs @@ -328,19 +328,6 @@ public void SetPlayerDisconnect() { } } - /// - /// Set a team update in the current packet. - /// - /// The new team of the player. - public void SetTeamUpdate(Team team) { - lock (Lock) { - CurrentUpdatePacket.SetSendingPacketData( - ServerPacketId.PlayerTeamUpdate, - new ServerPlayerTeamUpdate { Team = team } - ); - } - } - /// /// Set a skin update in the current packet. /// diff --git a/HKMP/Networking/Packet/Data/PlayerTeamUpdate.cs b/HKMP/Networking/Packet/Data/PlayerTeamUpdate.cs index ffd238d4..e9542924 100644 --- a/HKMP/Networking/Packet/Data/PlayerTeamUpdate.cs +++ b/HKMP/Networking/Packet/Data/PlayerTeamUpdate.cs @@ -7,60 +7,39 @@ namespace Hkmp.Networking.Packet.Data; /// internal class ClientPlayerTeamUpdate : GenericClientData { /// - /// The username of the player. + /// Whether the team update is for the player receiving the packet. /// - public string Username { get; set; } + public bool Self { get; set; } /// /// The team of the player. /// public Team Team { get; set; } - /// - /// Construct the player team update data. - /// public ClientPlayerTeamUpdate() { IsReliable = true; - DropReliableDataIfNewerExists = true; + DropReliableDataIfNewerExists = false; } /// public override void WriteData(IPacket packet) { - packet.Write(Id); - packet.Write(Username); + packet.Write(Self); + + if (!Self) { + packet.Write(Id); + } + packet.Write((byte) Team); } /// public override void ReadData(IPacket packet) { - Id = packet.ReadUShort(); - Username = packet.ReadString(); - Team = (Team) packet.ReadByte(); - } -} - -/// -/// Packet data for the server-bound player team update. -/// -internal class ServerPlayerTeamUpdate : IPacketData { - /// - public bool IsReliable => true; + Self = packet.ReadBool(); - /// - public bool DropReliableDataIfNewerExists => true; - - /// - /// The team of the player. - /// - public Team Team { get; set; } + if (!Self) { + Id = packet.ReadUShort(); + } - /// - public void WriteData(IPacket packet) { - packet.Write((byte) Team); - } - - /// - public void ReadData(IPacket packet) { Team = (Team) packet.ReadByte(); } } diff --git a/HKMP/Networking/Packet/PacketId.cs b/HKMP/Networking/Packet/PacketId.cs index d89e21dc..1282c05e 100644 --- a/HKMP/Networking/Packet/PacketId.cs +++ b/HKMP/Networking/Packet/PacketId.cs @@ -164,23 +164,18 @@ public enum ServerPacketId { /// PlayerDeath = 10, - /// - /// Notify that a player has changed teams. - /// - PlayerTeamUpdate = 11, - /// /// Notify that a player has changed skins. /// - PlayerSkinUpdate = 12, + PlayerSkinUpdate = 11, /// /// Player sent chat message. /// - ChatMessage = 13, + ChatMessage = 12, /// /// Value in the save file has updated. /// - SaveUpdate = 14, + SaveUpdate = 13, } diff --git a/HKMP/Networking/Packet/UpdatePacket.cs b/HKMP/Networking/Packet/UpdatePacket.cs index 7263775c..6f79cc6b 100644 --- a/HKMP/Networking/Packet/UpdatePacket.cs +++ b/HKMP/Networking/Packet/UpdatePacket.cs @@ -873,8 +873,6 @@ protected override IPacketData InstantiatePacketDataFromId(ServerPacketId packet return new PacketDataCollection(); case ServerPacketId.PlayerEnterScene: return new ServerPlayerEnterScene(); - case ServerPacketId.PlayerTeamUpdate: - return new ServerPlayerTeamUpdate(); case ServerPacketId.PlayerSkinUpdate: return new ServerPlayerSkinUpdate(); case ServerPacketId.ChatMessage: diff --git a/HKMP/Networking/Server/ServerUpdateManager.cs b/HKMP/Networking/Server/ServerUpdateManager.cs index b9f1aa72..a136850a 100644 --- a/HKMP/Networking/Server/ServerUpdateManager.cs +++ b/HKMP/Networking/Server/ServerUpdateManager.cs @@ -49,6 +49,28 @@ public override void ResendReliableData(ClientUpdatePacket lostPacket) { /// The type of the generic client packet data. /// An instance of the packet data in the packet. private T FindOrCreatePacketData(ushort id, ClientPacketId packetId) where T : GenericClientData, new() { + return FindOrCreatePacketData( + packetId, + packetData => packetData.Id == id, + () => new T { + Id = id + } + ); + } + + /// + /// Find or create a packet data instance in the current packet that matches a function. + /// + /// The ID of the packet data. + /// The function to match the packet data. + /// The function to construct the packet data if it does not exist. + /// The type of the generic client packet data. + /// An instance of the packet data in the packet. + private T FindOrCreatePacketData( + ClientPacketId packetId, + Func findFunc, + Func constructFunc + ) where T : IPacketData, new() { PacketDataCollection packetDataCollection; IPacketData packetData = null; @@ -57,8 +79,8 @@ public override void ResendReliableData(ClientUpdatePacket lostPacket) { // And if so, try to find the packet data with the requested client ID packetDataCollection = (PacketDataCollection) iPacketDataAsCollection; - foreach (var existingPacketData in packetDataCollection.DataInstances) { - if (((GenericClientData) existingPacketData).Id == id) { + foreach (T existingPacketData in packetDataCollection.DataInstances) { + if (findFunc(existingPacketData)) { packetData = existingPacketData; break; } @@ -71,9 +93,7 @@ public override void ResendReliableData(ClientUpdatePacket lostPacket) { // If no existing instance was found, create one and add it to the (newly created) collection if (packetData == null) { - packetData = new T { - Id = id - }; + packetData = constructFunc.Invoke(); packetDataCollection.DataInstances.Add(packetData); } @@ -476,17 +496,32 @@ public void AddPlayerDeathData(ushort id) { } /// - /// Add a player team update to the current packet. + /// Add a team update to the current packet for the receiving player. + /// + /// The team of the player. + public void AddPlayerTeamUpdateData(Team team) { + lock (Lock) { + var playerTeamUpdate = FindOrCreatePacketData( + ClientPacketId.PlayerTeamUpdate, + packetData => packetData.Self, + () => new ClientPlayerTeamUpdate { + Self = true + } + ); + playerTeamUpdate.Team = team; + } + } + + /// + /// Add a player team update to the current packet for another player. /// /// The ID of the player. - /// The username of the player. /// The team of the player. - public void AddPlayerTeamUpdateData(ushort id, string username, Team team) { + public void AddOtherPlayerTeamUpdateData(ushort id, Team team) { lock (Lock) { var playerTeamUpdate = FindOrCreatePacketData(id, ClientPacketId.PlayerTeamUpdate); playerTeamUpdate.Id = id; - playerTeamUpdate.Username = username; playerTeamUpdate.Team = team; } } diff --git a/HKMP/Ui/UiManager.cs b/HKMP/Ui/UiManager.cs index 606b8f1e..012cb8fb 100644 --- a/HKMP/Ui/UiManager.cs +++ b/HKMP/Ui/UiManager.cs @@ -91,11 +91,6 @@ internal class UiManager : IUiManager { /// public event Action RequestClientDisconnectEvent; - // /// - // /// The client settings interface. - // /// - // public ClientSettingsInterface SettingsInterface { get; } - /// /// The connect interface. /// @@ -161,12 +156,9 @@ NetClient netClient var connectGroup = new ComponentGroup(parent: pauseMenuGroup); - // var settingsGroup = new ComponentGroup(parent: pauseMenuGroup); - _connectInterface = new ConnectInterface( modSettings, connectGroup - // settingsGroup ); var inGameGroup = new ComponentGroup(parent: uiGroup); @@ -183,14 +175,6 @@ NetClient netClient netClient ); - // SettingsInterface = new ClientSettingsInterface( - // modSettings, - // clientServerSettings, - // settingsGroup, - // connectGroup, - // _pingInterface - // ); - // Register callbacks to make sure the UI is hidden and shown at correct times On.UIManager.SetState += (orig, self, state) => { orig(self, state); @@ -523,7 +507,6 @@ private void ChangeBtnTriggers(EventTrigger eventTrigger, Action action) { public void OnSuccessfulConnect() { _connectInterface.OnSuccessfulConnect(); _pingInterface.SetEnabled(true); - // SettingsInterface.OnSuccessfulConnect(); } /// @@ -540,28 +523,18 @@ public void OnFailedConnect(ConnectFailedResult result) { public void OnClientDisconnect() { _connectInterface.OnClientDisconnect(); _pingInterface.SetEnabled(false); - // SettingsInterface.OnDisconnect(); } - // /// - // /// Callback method for when the team setting in the changes. - // /// - // public void OnTeamSettingChange() { - // SettingsInterface.OnTeamSettingChange(); - // } - #endregion #region IUiManager methods /// public void DisableTeamSelection() { - // SettingsInterface.OnAddonSetTeamSelection(false); } /// public void EnableTeamSelection() { - // SettingsInterface.OnAddonSetTeamSelection(true); } /// From ed657d7101203b10a1892e71b3cf48b0673fd80e Mon Sep 17 00:00:00 2001 From: Extremelyd1 <10898310+Extremelyd1@users.noreply.github.com> Date: Mon, 20 May 2024 12:19:30 +0200 Subject: [PATCH 085/216] Refactor skin selection to command --- HKMP/Api/Client/IClientManager.cs | 1 + HKMP/Game/Client/ClientManager.cs | 29 -------- HKMP/Game/Client/PlayerManager.cs | 26 ++++--- HKMP/Game/Command/Server/SkinCommand.cs | 55 +++++++++++++++ HKMP/Game/Server/ServerManager.cs | 67 +++++++++++++------ HKMP/Networking/Client/ClientUpdateManager.cs | 13 ---- .../Packet/Data/PlayerSkinUpdate.cs | 43 +++++------- HKMP/Networking/Packet/PacketId.cs | 9 +-- HKMP/Networking/Packet/UpdatePacket.cs | 2 - HKMP/Networking/Server/ServerUpdateManager.cs | 41 +++++++++--- 10 files changed, 169 insertions(+), 117 deletions(-) create mode 100644 HKMP/Game/Command/Server/SkinCommand.cs diff --git a/HKMP/Api/Client/IClientManager.cs b/HKMP/Api/Client/IClientManager.cs index 6cd3c77a..2ad29ab5 100644 --- a/HKMP/Api/Client/IClientManager.cs +++ b/HKMP/Api/Client/IClientManager.cs @@ -59,6 +59,7 @@ public interface IClientManager { /// 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/Game/Client/ClientManager.cs b/HKMP/Game/Client/ClientManager.cs index f3f72e20..8fbdc338 100644 --- a/HKMP/Game/Client/ClientManager.cs +++ b/HKMP/Game/Client/ClientManager.cs @@ -248,9 +248,6 @@ ModSettings modSettings }; uiManager.RequestClientDisconnectEvent += Disconnect; - // uiManager.SettingsInterface.OnTeamRadioButtonChange += InternalChangeTeam; - // uiManager.SettingsInterface.OnSkinIdChange += InternalChangeSkin; - UiManager.InternalChatBox.ChatInputEvent += OnChatInput; netClient.ConnectEvent += _ => uiManager.OnSuccessfulConnect(); @@ -414,27 +411,6 @@ private void OnChatInput(string message) { _netClient.UpdateManager.SetChatMessage(message); } - /// - /// 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. /// @@ -1041,11 +1017,6 @@ public void ChangeTeam(Team 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/PlayerManager.cs b/HKMP/Game/Client/PlayerManager.cs index 8a21b031..d59edeab 100644 --- a/HKMP/Game/Client/PlayerManager.cs +++ b/HKMP/Game/Client/PlayerManager.cs @@ -597,29 +597,35 @@ public Team GetPlayerTeam(ushort id) { return playerData.Team; } - /// - /// Update the skin of the local player. - /// - /// The ID of the skin to update to. - public void UpdateLocalPlayerSkin(byte skinId) { - _skinManager.UpdateLocalPlayerSkin(skinId); - } - /// /// Callback method for when a player updates their skin. /// /// The ClientPlayerSkinUpdate packet data. private void OnPlayerSkinUpdate(ClientPlayerSkinUpdate playerSkinUpdate) { - var id = playerSkinUpdate.Id; var skinId = playerSkinUpdate.SkinId; + + if (playerSkinUpdate.Self) { + Logger.Debug($"Received PlayerSkinUpdate for local player: {skinId}"); + + _skinManager.UpdateLocalPlayerSkin(skinId); + return; + } + + var id = playerSkinUpdate.Id; + Logger.Debug($"Received PlayerSkinUpdate for ID: {id}, skin ID: {skinId}"); if (!_playerData.TryGetValue(id, out var playerData)) { - Logger.Debug($"Received PlayerSkinUpdate for ID: {id}, skinId: {skinId}"); + Logger.Debug(" Could not find player"); return; } playerData.SkinId = skinId; + // If the player is not in the local scene, we don't have to apply the skin update to the player object + if (!playerData.IsInLocalScene) { + return; + } + _skinManager.UpdatePlayerSkin(playerData.PlayerObject, skinId); } diff --git a/HKMP/Game/Command/Server/SkinCommand.cs b/HKMP/Game/Command/Server/SkinCommand.cs new file mode 100644 index 00000000..5f0d8541 --- /dev/null +++ b/HKMP/Game/Command/Server/SkinCommand.cs @@ -0,0 +1,55 @@ +using System; +using Hkmp.Api.Command.Server; +using Hkmp.Game.Server; + +namespace Hkmp.Game.Command.Server; + +/// +/// Command for changing the skin of the player. +/// +internal class SkinCommand : IServerCommand { + /// + public string Trigger => "/skin"; + + /// + public string[] Aliases => Array.Empty(); + + /// + public bool AuthorizedOnly => false; + + /// + /// The server manager instance. + /// + private readonly ServerManager _serverManager; + + public SkinCommand(ServerManager serverManager) { + _serverManager = serverManager; + } + + /// + public void Execute(ICommandSender commandSender, string[] arguments) { + if (commandSender.Type == CommandSenderType.Console) { + commandSender.SendMessage("Console cannot change skins."); + return; + } + + var sender = (PlayerCommandSender) commandSender; + + if (arguments.Length != 2) { + sender.SendMessage($"Usage: {Trigger} "); + return; + } + + var skinIdArg = arguments[1]; + if (!byte.TryParse(skinIdArg, out var skinId)) { + sender.SendMessage($"Unknown skin ID '{skinIdArg}', please provide a value between 0-255"); + return; + } + + if (_serverManager.TryUpdatePlayerSkin(sender.Id, skinId, out var reason)) { + sender.SendMessage($"Skin ID changed to '{skinId}'"); + } else { + sender.SendMessage(reason); + } + } +} diff --git a/HKMP/Game/Server/ServerManager.cs b/HKMP/Game/Server/ServerManager.cs index b7055f96..95b6195e 100644 --- a/HKMP/Game/Server/ServerManager.cs +++ b/HKMP/Game/Server/ServerManager.cs @@ -147,8 +147,6 @@ PacketManager packetManager OnReliableEntityUpdate); packetManager.RegisterServerPacketHandler(ServerPacketId.PlayerDisconnect, OnPlayerDisconnect); packetManager.RegisterServerPacketHandler(ServerPacketId.PlayerDeath, OnPlayerDeath); - packetManager.RegisterServerPacketHandler(ServerPacketId.PlayerSkinUpdate, - OnPlayerSkinUpdate); packetManager.RegisterServerPacketHandler(ServerPacketId.ChatMessage, OnChatMessage); packetManager.RegisterServerPacketHandler(ServerPacketId.SaveUpdate, OnSaveUpdate); @@ -183,6 +181,7 @@ protected virtual void RegisterCommands() { CommandManager.RegisterCommand(new BanCommand(_banList, this)); CommandManager.RegisterCommand(new KickCommand(this)); CommandManager.RegisterCommand(new TeamCommand(this)); + CommandManager.RegisterCommand(new SkinCommand(this)); } /// @@ -994,7 +993,7 @@ public bool TryUpdatePlayerTeam(ushort id, Team team, out string reason) { if (!_playerData.TryGetValue(id, out var playerData)) { Logger.Warn($"Received PlayerTeamUpdate data, but player with ID {id} is not in mapping"); - reason = "Could not find player."; + reason = "Could not find player"; return false; } @@ -1003,7 +1002,7 @@ public bool TryUpdatePlayerTeam(ushort id, Team team, out string reason) { if (!ServerSettings.TeamsEnabled) { Logger.Info(" Teams are not enabled, won't update team"); - reason = "Unable to change team."; + reason = "Unable to change team"; return false; } @@ -1028,33 +1027,59 @@ public bool TryUpdatePlayerTeam(ushort id, Team team, out string reason) { } /// - /// Callback method for when a player updates their skin. + /// Try to update the skin for the player with the given ID. /// /// The ID of the player. - /// The ServerPlayerSkinUpdate packet data. - private void OnPlayerSkinUpdate(ushort id, ServerPlayerSkinUpdate skinUpdate) { + /// The ID of the skin to change the player to. + /// The reason if the skin could not be updated, otherwise null. + /// True if the player's team was updated, false otherwise. + public bool TryUpdatePlayerSkin(ushort id, byte skinId, out string reason) { if (!_playerData.TryGetValue(id, out var playerData)) { Logger.Warn($"Received PlayerSkinUpdate data, but player with ID {id} is not in mapping"); - return; + + reason = "Could not find player"; + return false; } - - if (playerData.SkinId == skinUpdate.SkinId) { - Logger.Debug($"Received PlayerSkinUpdate data from ({id}, {playerData.Username}), but skin was the same"); - return; + + Logger.Info($"Received PlayerSkinUpdate data from ({id}, {playerData.Username}) for skin ID: {skinId}"); + + if (!ServerSettings.AllowSkins) { + Logger.Info(" Skins are not allowed, won't update skin"); + + reason = "Unable to change skin"; + return false; } - Logger.Debug($"Received PlayerSkinUpdate data from ({id}, {playerData.Username}), new skin ID: {skinUpdate.SkinId}"); + if (playerData.SkinId == skinId) { + Logger.Info(" Skins is the same as current, won't update skin"); + + reason = "Skin is already in use"; + return false; + } // Update the skin ID in the player data - playerData.SkinId = skinUpdate.SkinId; - - SendDataInSameScene( - id, - playerData.CurrentScene, - otherId => { - _netServer.GetUpdateManagerForClient(otherId)?.AddPlayerSkinUpdateData(id, playerData.SkinId); + playerData.SkinId = skinId; + + foreach (var idPlayerDataPair in _playerData) { + var otherId = idPlayerDataPair.Key; + + if (otherId == id) { + _netServer.GetUpdateManagerForClient(id)?.AddPlayerSkinUpdateData(skinId); + continue; } - ); + + var otherPd = idPlayerDataPair.Value; + + // Skip sending skin to players not in the same scene + if (!string.Equals(otherPd.CurrentScene, playerData.CurrentScene)) { + continue; + } + + _netServer.GetUpdateManagerForClient(otherId)?.AddOtherPlayerSkinUpdateData(id, skinId); + } + + reason = null; + return true; } /// diff --git a/HKMP/Networking/Client/ClientUpdateManager.cs b/HKMP/Networking/Client/ClientUpdateManager.cs index e65d633e..575e7988 100644 --- a/HKMP/Networking/Client/ClientUpdateManager.cs +++ b/HKMP/Networking/Client/ClientUpdateManager.cs @@ -328,19 +328,6 @@ public void SetPlayerDisconnect() { } } - /// - /// Set a skin update in the current packet. - /// - /// The ID of the skin of the player. - public void SetSkinUpdate(byte skinId) { - lock (Lock) { - CurrentUpdatePacket.SetSendingPacketData( - ServerPacketId.PlayerSkinUpdate, - new ServerPlayerSkinUpdate { SkinId = skinId } - ); - } - } - /// /// Set hello server data in the current packet. /// diff --git a/HKMP/Networking/Packet/Data/PlayerSkinUpdate.cs b/HKMP/Networking/Packet/Data/PlayerSkinUpdate.cs index cb5aa992..487bfcd2 100644 --- a/HKMP/Networking/Packet/Data/PlayerSkinUpdate.cs +++ b/HKMP/Networking/Packet/Data/PlayerSkinUpdate.cs @@ -4,6 +4,11 @@ namespace Hkmp.Networking.Packet.Data; /// Packet data for client-bound player skin update. /// internal class ClientPlayerSkinUpdate : GenericClientData { + /// + /// Whether the skin update is for the player receiving this packet. + /// + public bool Self { get; set; } + /// /// The ID of the skin. /// @@ -14,44 +19,28 @@ internal class ClientPlayerSkinUpdate : GenericClientData { /// public ClientPlayerSkinUpdate() { IsReliable = true; - DropReliableDataIfNewerExists = true; + DropReliableDataIfNewerExists = false; } /// public override void WriteData(IPacket packet) { - packet.Write(Id); + packet.Write(Self); + + if (!Self) { + packet.Write(Id); + } + packet.Write(SkinId); } /// public override void ReadData(IPacket packet) { - Id = packet.ReadUShort(); - SkinId = packet.ReadByte(); - } -} + Self = packet.ReadBool(); -/// -/// Packet data for the server-bound player skin update. -/// -internal class ServerPlayerSkinUpdate : IPacketData { - /// - public bool IsReliable => true; + if (!Self) { + Id = packet.ReadUShort(); + } - /// - public bool DropReliableDataIfNewerExists => true; - - /// - /// The ID of the skin. - /// - public byte SkinId { get; set; } - - /// - public void WriteData(IPacket packet) { - packet.Write(SkinId); - } - - /// - public void ReadData(IPacket packet) { SkinId = packet.ReadByte(); } } diff --git a/HKMP/Networking/Packet/PacketId.cs b/HKMP/Networking/Packet/PacketId.cs index 1282c05e..9328e02f 100644 --- a/HKMP/Networking/Packet/PacketId.cs +++ b/HKMP/Networking/Packet/PacketId.cs @@ -164,18 +164,13 @@ public enum ServerPacketId { /// PlayerDeath = 10, - /// - /// Notify that a player has changed skins. - /// - PlayerSkinUpdate = 11, - /// /// Player sent chat message. /// - ChatMessage = 12, + ChatMessage = 11, /// /// Value in the save file has updated. /// - SaveUpdate = 13, + SaveUpdate = 12, } diff --git a/HKMP/Networking/Packet/UpdatePacket.cs b/HKMP/Networking/Packet/UpdatePacket.cs index 6f79cc6b..773bc58d 100644 --- a/HKMP/Networking/Packet/UpdatePacket.cs +++ b/HKMP/Networking/Packet/UpdatePacket.cs @@ -873,8 +873,6 @@ protected override IPacketData InstantiatePacketDataFromId(ServerPacketId packet return new PacketDataCollection(); case ServerPacketId.PlayerEnterScene: return new ServerPlayerEnterScene(); - case ServerPacketId.PlayerSkinUpdate: - return new ServerPlayerSkinUpdate(); case ServerPacketId.ChatMessage: return new ChatMessage(); case ServerPacketId.SaveUpdate: diff --git a/HKMP/Networking/Server/ServerUpdateManager.cs b/HKMP/Networking/Server/ServerUpdateManager.cs index a136850a..6e2a6bf0 100644 --- a/HKMP/Networking/Server/ServerUpdateManager.cs +++ b/HKMP/Networking/Server/ServerUpdateManager.cs @@ -519,23 +519,48 @@ public void AddPlayerTeamUpdateData(Team team) { /// The team of the player. public void AddOtherPlayerTeamUpdateData(ushort id, Team team) { lock (Lock) { - var playerTeamUpdate = - FindOrCreatePacketData(id, ClientPacketId.PlayerTeamUpdate); - playerTeamUpdate.Id = id; + var playerTeamUpdate = FindOrCreatePacketData( + ClientPacketId.PlayerTeamUpdate, + packetData => packetData.Id == id && !packetData.Self, + () => new ClientPlayerTeamUpdate { + Id = id + } + ); playerTeamUpdate.Team = team; } } /// - /// Add a player skin update to the current packet. + /// Add a skin update to the current packet for the receiving player. + /// + /// The ID of the skin of the player. + public void AddPlayerSkinUpdateData(byte skinId) { + lock (Lock) { + var playerSkinUpdate = FindOrCreatePacketData( + ClientPacketId.PlayerSkinUpdate, + packetData => packetData.Self, + () => new ClientPlayerSkinUpdate { + Self = true + } + ); + playerSkinUpdate.SkinId = skinId; + } + } + + /// + /// Add a skin update to the current packet for another player. /// /// The ID of the player. /// The ID of the skin of the player. - public void AddPlayerSkinUpdateData(ushort id, byte skinId) { + public void AddOtherPlayerSkinUpdateData(ushort id, byte skinId) { lock (Lock) { - var playerSkinUpdate = - FindOrCreatePacketData(id, ClientPacketId.PlayerSkinUpdate); - playerSkinUpdate.Id = id; + var playerSkinUpdate = FindOrCreatePacketData( + ClientPacketId.PlayerSkinUpdate, + packetData => packetData.Id == id && !packetData.Self, + () => new ClientPlayerSkinUpdate { + Id = id + } + ); playerSkinUpdate.SkinId = skinId; } } From 96a855417429ea393e3c0f6cbd3b5482c02b787d Mon Sep 17 00:00:00 2001 From: Extremelyd1 <10898310+Extremelyd1@users.noreply.github.com> Date: Sun, 26 May 2024 16:37:26 +0200 Subject: [PATCH 086/216] Fix nail-based attacks environment interaction --- HKMP/Animation/AnimationEffect.cs | 13 ++------ HKMP/Animation/AnimationManager.cs | 46 ++++++++++++++++++++++++++ HKMP/Animation/Effects/CycloneSlash.cs | 8 ++--- HKMP/Animation/Effects/DashSlash.cs | 6 +--- HKMP/Animation/Effects/GreatSlash.cs | 8 ++--- HKMP/Animation/Effects/SlashBase.cs | 10 +++--- HKMP/Fsm/FsmPatcher.cs | 8 +++++ HKMP/Game/Client/PlayerManager.cs | 8 ++++- 8 files changed, 74 insertions(+), 33 deletions(-) diff --git a/HKMP/Animation/AnimationEffect.cs b/HKMP/Animation/AnimationEffect.cs index af434d0d..2fb04664 100644 --- a/HKMP/Animation/AnimationEffect.cs +++ b/HKMP/Animation/AnimationEffect.cs @@ -26,23 +26,16 @@ 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. /// The direction in float that the damage is coming from. - protected static void ChangeAttackTypeOfFsm(GameObject targetObject, float direction) { + 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"); diff --git a/HKMP/Animation/AnimationManager.cs b/HKMP/Animation/AnimationManager.cs index 105968b7..b877f9a0 100644 --- a/HKMP/Animation/AnimationManager.cs +++ b/HKMP/Animation/AnimationManager.cs @@ -1,3 +1,4 @@ +using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; @@ -15,9 +16,13 @@ using Hkmp.Util; using HutongGames.PlayMaker.Actions; using Modding; +using MonoMod.Cil; using UnityEngine; using UnityEngine.SceneManagement; using Logger = Hkmp.Logging.Logger; +using Object = UnityEngine.Object; +using OpCodes = Mono.Cecil.Cil.OpCodes; +using Random = UnityEngine.Random; namespace Hkmp.Animation; @@ -450,6 +455,9 @@ ServerSettings serverSettings // Register when the player dies to send the animation ModHooks.BeforePlayerDeadHook += OnDeath; + + // Register IL hook for changing the behaviour of tink effects + IL.TinkEffect.OnTriggerEnter2D += TinkEffectOnTriggerEnter2D; // Set the server settings for all animation effects foreach (var effect in AnimationEffects.Values) { @@ -1126,4 +1134,42 @@ public static AnimationClip GetCurrentAnimationClip() { return 0; } + + /// + /// IL hook to change the TinkEffect OnTriggerEnter2D to not trigger on remote players. + /// This method will insert IL to check whether the player responsible for the attack is the local player. + /// + private void TinkEffectOnTriggerEnter2D(ILContext il) { + try { + // Create a cursor for this context + var c = new ILCursor(il); + + // Find the first return instruction in the method to branch to later + var retInstr = il.Instrs.First(i => i.MatchRet()); + + // Load the 'collision' argument onto the stack + c.Emit(OpCodes.Ldarg_1); + + // Emit a delegate that pops the TinkEffect from the stack, checks whether the parent + // of the effect is the knight and pushes a bool on the stack based on this + c.EmitDelegate>(collider => { + var parent = collider.transform.parent; + if (parent == null) { + return true; + } + + parent = parent.parent; + if (parent == null) { + return true; + } + + return parent.gameObject.name != "Knight"; + }); + + // Based on the bool we pushed to the stack earlier, we conditionally branch to the return instruction + c.Emit(OpCodes.Brtrue, retInstr); + } catch (Exception e) { + Logger.Error($"Could not change TinkEffect#OnTriggerEnter2D IL:\n{e}"); + } + } } diff --git a/HKMP/Animation/Effects/CycloneSlash.cs b/HKMP/Animation/Effects/CycloneSlash.cs index 67fcc713..1765ee1e 100644 --- a/HKMP/Animation/Effects/CycloneSlash.cs +++ b/HKMP/Animation/Effects/CycloneSlash.cs @@ -50,14 +50,10 @@ public override void Play(GameObject playerObject, bool[] effectInfo) { cycloneSlash.layer = 17; var hitLComponent = cycloneSlash.FindGameObjectInChildren("Hit L"); - ChangeAttackTypeOfFsm(hitLComponent, 0f); + ChangeAttackDirection(hitLComponent, 0f); var hitRComponent = cycloneSlash.FindGameObjectInChildren("Hit R"); - ChangeAttackTypeOfFsm(hitRComponent, 180f); - - // Add rigid body and set it to be kinematic so it doesn't do physics, but still counts certain collisions - var rigidBody = cycloneSlash.AddComponent(); - rigidBody.isKinematic = true; + ChangeAttackDirection(hitRComponent, 180f); cycloneSlash.SetActive(true); diff --git a/HKMP/Animation/Effects/DashSlash.cs b/HKMP/Animation/Effects/DashSlash.cs index eefa1097..c1526913 100644 --- a/HKMP/Animation/Effects/DashSlash.cs +++ b/HKMP/Animation/Effects/DashSlash.cs @@ -52,11 +52,7 @@ public override void Play(GameObject playerObject, bool[] effectInfo) { // Check which direction the knight is facing for the damages_enemy FSM var facingRight = playerScaleX > 0; - ChangeAttackTypeOfFsm(dashSlash, facingRight ? 180f : 0f); - - // Add rigid body and set it to be kinematic so it doesn't do physics, but still counts certain collisions - var rigidBody = dashSlash.AddComponent(); - rigidBody.isKinematic = true; + 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 diff --git a/HKMP/Animation/Effects/GreatSlash.cs b/HKMP/Animation/Effects/GreatSlash.cs index 5db1105d..83259bca 100644 --- a/HKMP/Animation/Effects/GreatSlash.cs +++ b/HKMP/Animation/Effects/GreatSlash.cs @@ -40,12 +40,8 @@ public override void Play(GameObject playerObject, bool[] effectInfo) { // Check which direction the knight is facing for the damages_enemy FSM var facingRight = playerObject.transform.localScale.x > 0; - ChangeAttackTypeOfFsm(greatSlash, facingRight ? 180f : 0f); - - // Add rigid body and set it to be kinematic so it doesn't do physics, but still counts certain collisions - var rigidBody = greatSlash.AddComponent(); - rigidBody.isKinematic = true; - + ChangeAttackDirection(greatSlash, facingRight ? 180f : 0f); + greatSlash.SetActive(true); // Set the newly instantiate collider to state Init, to reset it diff --git a/HKMP/Animation/Effects/SlashBase.cs b/HKMP/Animation/Effects/SlashBase.cs index 88ceea05..65de041a 100644 --- a/HKMP/Animation/Effects/SlashBase.cs +++ b/HKMP/Animation/Effects/SlashBase.cs @@ -73,7 +73,7 @@ protected void Play(GameObject playerObject, bool[] effectInfo, GameObject prefa direction = 270f; } - ChangeAttackTypeOfFsm(slash, direction); + 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 @@ -88,10 +88,6 @@ protected void Play(GameObject playerObject, bool[] effectInfo, GameObject prefa var originalNailSlash = slash.GetComponent(); Object.Destroy(originalNailSlash); - // Add rigid body and set it to be kinematic so it doesn't do physics, but still counts certain collisions - var rigidBody = slash.AddComponent(); - rigidBody.isKinematic = true; - slash.SetActive(true); // Get the slash audio source and its clip @@ -163,8 +159,12 @@ 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; + + var clashTink = slash.transform.Find("Clash Tink").GetComponent(); + clashTink.enabled = true; var damage = ServerSettings.NailDamage; if (ServerSettings.IsPvpEnabled && ShouldDoDamage) { diff --git a/HKMP/Fsm/FsmPatcher.cs b/HKMP/Fsm/FsmPatcher.cs index 9dac82d3..59026d4f 100644 --- a/HKMP/Fsm/FsmPatcher.cs +++ b/HKMP/Fsm/FsmPatcher.cs @@ -59,5 +59,13 @@ private void OnFsmEnable(On.PlayMakerFSM.orig_OnEnable orig, PlayMakerFSM self) } }); } + + // 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); + } + } } } diff --git a/HKMP/Game/Client/PlayerManager.cs b/HKMP/Game/Client/PlayerManager.cs index d59edeab..c34b414a 100644 --- a/HKMP/Game/Client/PlayerManager.cs +++ b/HKMP/Game/Client/PlayerManager.cs @@ -159,7 +159,13 @@ private void CreatePlayerPool() { nonBouncer.active = false; // Add some extra gameObjects related to animation effects - new GameObject("Attacks") { layer = 9 }.transform.SetParent(playerPrefab.transform); + var attacks = new GameObject("Attacks") { layer = 9 }; + attacks.transform.SetParent(playerPrefab.transform); + // Add rigid body to make sure collisions still work + var rigidbody = attacks.AddComponent(); + rigidbody.collisionDetectionMode = CollisionDetectionMode2D.Continuous; + rigidbody.gravityScale = 0; + new GameObject("Effects") { layer = 9 }.transform.SetParent(playerPrefab.transform); new GameObject("Spells") { layer = 9 }.transform.SetParent(playerPrefab.transform); From 6ef53c5e91ced7e748b230179fbe8ec1c923ba77 Mon Sep 17 00:00:00 2001 From: Extremelyd1 <10898310+Extremelyd1@users.noreply.github.com> Date: Sun, 26 May 2024 16:37:41 +0200 Subject: [PATCH 087/216] Fix save-sync issue with string data --- HKMP/Resource/scene-data.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/HKMP/Resource/scene-data.json b/HKMP/Resource/scene-data.json index 16a7002b..c0a76a5c 100644 --- a/HKMP/Resource/scene-data.json +++ b/HKMP/Resource/scene-data.json @@ -579,5 +579,6 @@ "WhiteBench", "WHITE_PALACE", "TRAM_UPPER", - "TRAM_LOWER" + "TRAM_LOWER", + "Death Respawn Marker" ] From b1e203a9fd3b7e3d7aa42386f35887c1276c9fef Mon Sep 17 00:00:00 2001 From: Extremelyd1 <10898310+Extremelyd1@users.noreply.github.com> Date: Sat, 29 Jun 2024 21:00:57 +0200 Subject: [PATCH 088/216] Fix duplicate enemies, rigidbody sync issues --- HKMP/Game/Client/Entity/Entity.cs | 24 +++++++++++++++++++++- HKMP/Game/Client/Entity/EntityManager.cs | 7 +++++-- HKMP/Game/Client/Entity/EntityProcessor.cs | 6 +++++- 3 files changed, 33 insertions(+), 4 deletions(-) diff --git a/HKMP/Game/Client/Entity/Entity.cs b/HKMP/Game/Client/Entity/Entity.cs index 07a92800..8a031b7d 100644 --- a/HKMP/Game/Client/Entity/Entity.cs +++ b/HKMP/Game/Client/Entity/Entity.cs @@ -804,13 +804,18 @@ public void MakeHost() { component.IsControlled = false; } + Logger.Debug(" Client object null, enabling host object and returning"); return; } if (_hasParent) { + Logger.Debug(" Entity has parent, only setting local transform"); + Object.Host.transform.localPosition = _lastPosition = Object.Client.transform.localPosition; Object.Host.transform.localScale = _lastScale = Object.Client.transform.localScale; } else { + Logger.Debug(" Entity has no parent, calculating transform"); + var clientPos = Object.Client.transform.localPosition; var parentPos = Vector3.zero; if (Object.Host.transform.parent != null) { @@ -851,6 +856,17 @@ public void MakeHost() { Object.Client.SetActive(false); Object.Host.SetActive(clientActive); + Logger.Debug($" Set Active of host object to: {clientActive}, disabling client object"); + + // We need to set the isKinematic property of rigid bodies to ensure physics work again after enabling + // the host object. In Hornet 1 this is necessary because another state sets this property normally in the + // fight. See the "Wake" or "Refight Ready" state of the "Control" FSM on Hornet 1 + var rigidBody = Object.Host.GetComponent(); + if (rigidBody != null) { + Logger.Debug(" Resetting isKinematic of Rigidbody to ensure physics work for host object"); + rigidBody.isKinematic = false; + } + _lastIsActive = _hasParent ? Object.Host.activeSelf : Object.Host.activeInHierarchy; _isControlled = false; @@ -865,15 +881,19 @@ public void MakeHost() { var clientAnimation = currentClip.name; var wrapMode = currentClip.wrapMode; - Logger.Debug($"MakeHost ({Id}, {Type}) animation: {clientAnimation}, {wrapMode}"); + Logger.Debug($" Animator and current clip present, updating animation: {clientAnimation}, {wrapMode}"); LateUpdateAnimation(_animator.Host, clientAnimation, wrapMode); } } + Logger.Debug(" Restoring FSMs from snapshots"); + for (var fsmIndex = 0; fsmIndex < _fsms.Host.Count; fsmIndex++) { var fsm = _fsms.Host[fsmIndex]; + Logger.Debug($" Restoring FSM: {fsm.Fsm.Name}"); + // Force initialize the host FSM, since it might have been disabled before initializing EntityInitializer.InitializeFsm(fsm); @@ -912,6 +932,8 @@ public void MakeHost() { var oldActions = state.Actions; var newActions = oldActions.Where(ActionRegistry.IsActionContinuous).ToArray(); + Logger.Debug($" Only using actions: {string.Join(", ", newActions.Select(a => a.GetType().ToString()))}"); + // Replace the actions, set the state and reset the actions again state.Actions = newActions; fsm.SetState(snapshot.CurrentState); diff --git a/HKMP/Game/Client/Entity/EntityManager.cs b/HKMP/Game/Client/Entity/EntityManager.cs index e0940f9f..a874b48b 100644 --- a/HKMP/Game/Client/Entity/EntityManager.cs +++ b/HKMP/Game/Client/Entity/EntityManager.cs @@ -137,6 +137,7 @@ public void SpawnEntity(ushort id, EntityType spawningType, EntityType spawnedTy var processor = new EntityProcessor { GameObject = spawnedObject, IsSceneHost = IsSceneHost, + IsSceneHostDetermined = IsSceneHostDetermined, LateLoad = true, SpawnedId = id }.Process(); @@ -238,6 +239,7 @@ private bool OnGameObjectSpawned(EntitySpawnDetails details) { var processor = new EntityProcessor { GameObject = details.GameObject, IsSceneHost = IsSceneHost, + IsSceneHostDetermined = IsSceneHostDetermined, LateLoad = true }.Process(); @@ -329,13 +331,13 @@ private void OnSceneChanged(Scene oldScene, Scene newScene) { return; } + IsSceneHostDetermined = false; + FindEntitiesInScene(newScene, false); // Since we have tried finding entities in the scene, we also check whether there are un-applied updates for // those entities CheckReceivedUpdates(); - - IsSceneHostDetermined = false; } /// @@ -447,6 +449,7 @@ private void FindEntitiesInScene(Scene scene, bool lateLoad) { new EntityProcessor { GameObject = obj, IsSceneHost = IsSceneHost, + IsSceneHostDetermined = IsSceneHostDetermined, LateLoad = lateLoad }.Process(); } diff --git a/HKMP/Game/Client/Entity/EntityProcessor.cs b/HKMP/Game/Client/Entity/EntityProcessor.cs index 6053f724..4a1af57b 100644 --- a/HKMP/Game/Client/Entity/EntityProcessor.cs +++ b/HKMP/Game/Client/Entity/EntityProcessor.cs @@ -38,6 +38,10 @@ internal class EntityProcessor { /// public bool IsSceneHost { get; init; } /// + /// Whether the scene host is determined for this scene locally. + /// + public bool IsSceneHostDetermined { get; init; } + /// /// Whether the processing of this entity should happen under the assumption that was a late load of the /// game object. /// @@ -188,7 +192,7 @@ private void Process( } } - if (LateLoad) { + if (LateLoad && IsSceneHostDetermined) { if (IsSceneHost) { // Since this is a late load it needs to be initialized as host if we are the scene host entity.InitializeHost(); From 9de4dac6a826d2bb243000eada104050f510db60 Mon Sep 17 00:00:00 2001 From: Extremelyd1 <10898310+Extremelyd1@users.noreply.github.com> Date: Sun, 30 Jun 2024 16:40:12 +0200 Subject: [PATCH 089/216] Fix certain Godhome fights not ending on scene clients --- HKMP/Game/Client/Entity/Entity.cs | 43 ++++++++++++++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/HKMP/Game/Client/Entity/Entity.cs b/HKMP/Game/Client/Entity/Entity.cs index 8a031b7d..af7ffefd 100644 --- a/HKMP/Game/Client/Entity/Entity.cs +++ b/HKMP/Game/Client/Entity/Entity.cs @@ -10,6 +10,7 @@ using Hkmp.Networking.Packet.Data; using Hkmp.Util; using HutongGames.PlayMaker; +using Modding; using UnityEngine; using Logger = Hkmp.Logging.Logger; using Vector2 = Hkmp.Math.Vector2; @@ -220,7 +221,9 @@ params EntityComponentType[] types Object.Host.SetActive(false); Object.Client.SetActive(false); - + + CheckGodhome(); + // // Debug code that logs each action's OnEnter method call // foreach (var fsm in _fsms.Host) { // foreach (var state in fsm.FsmStates) { @@ -465,6 +468,44 @@ private void HandleEnemyDeathEffects() { UnityEngine.Object.Destroy(corpse); } + /// + /// Checks whether this is an enemy in a godhome fight. If that's the case, the health manager of the client + /// object will have their death be registered as a trigger for the boss scene controller. This ensures that + /// fights will end on scene clients if the client objects die. + /// + private void CheckGodhome() { + var bossSceneController = UnityEngine.Object.FindObjectOfType(); + if (bossSceneController == null) { + return; + } + + var hostHealthManager = Object.Host.GetComponent(); + if (hostHealthManager == null) { + return; + } + + var clientHealthManager = Object.Client.GetComponent(); + if (clientHealthManager == null) { + Logger.Debug($"Entity ({Id}, {Type}) has HealthManager on host but not on client"); + return; + } + + if (!bossSceneController.bosses.Contains(hostHealthManager)) { + return; + } + + Logger.Debug($"Entity ({Id}, {Type}) is contained in the boss scene controller, registering on death"); + + clientHealthManager.OnDeath += () => { + Logger.Debug("OnDeath triggered for health manager in boss scene controller"); + + var bossesLeft = ReflectionHelper.GetField(bossSceneController, "bossesLeft"); + ReflectionHelper.SetField(bossSceneController, "bossesLeft", bossesLeft - 1); + + ReflectionHelper.CallMethod(bossSceneController, "CheckBossesDead"); + }; + } + /// /// Callback method for entering a hooked FSM action. /// From a7c2376f9299378aa22ed46f86c3a715d4a5fa59 Mon Sep 17 00:00:00 2001 From: Extremelyd1 <10898310+Extremelyd1@users.noreply.github.com> Date: Sun, 7 Jul 2024 22:44:28 +0200 Subject: [PATCH 090/216] Preliminary refactor of save synchronisation --- HKMP/Game/Client/Save/SaveDataMapping.cs | 42 +- HKMP/Game/Client/Save/SaveManager.cs | 92 +- HKMP/Game/GameManager.cs | 11 +- HKMP/Game/Server/ModServerManager.cs | 34 +- HKMP/Game/Server/ServerManager.cs | 77 +- HKMP/Game/Server/ServerSaveData.cs | 48 + HKMP/HkmpMod.cs | 19 +- HKMP/Networking/Packet/Data/SaveUpdate.cs | 42 +- HKMP/Resource/save-data.json | 19177 ++++++++++++++++---- HKMPServer/ConsoleServerManager.cs | 89 +- 10 files changed, 15926 insertions(+), 3705 deletions(-) create mode 100644 HKMP/Game/Server/ServerSaveData.cs diff --git a/HKMP/Game/Client/Save/SaveDataMapping.cs b/HKMP/Game/Client/Save/SaveDataMapping.cs index 1eb442fb..c3222d64 100644 --- a/HKMP/Game/Client/Save/SaveDataMapping.cs +++ b/HKMP/Game/Client/Save/SaveDataMapping.cs @@ -3,7 +3,9 @@ using Hkmp.Collection; using Hkmp.Logging; using Hkmp.Util; +using JetBrains.Annotations; using Newtonsoft.Json; +using Newtonsoft.Json.Converters; namespace Hkmp.Game.Client.Save; @@ -39,7 +41,7 @@ public static SaveDataMapping Instance { /// Dictionary mapping player data values to booleans indicating whether they should be synchronised. /// [JsonProperty("playerData")] - public Dictionary PlayerDataBools { get; private set; } + public Dictionary PlayerDataBools { get; private set; } /// /// Bi-directional lookup that maps save data names and their indices. @@ -72,14 +74,14 @@ public static SaveDataMapping Instance { /// #pragma warning disable 0649 [JsonProperty("persistentBoolItems")] - private readonly List> _persistentBoolsDataValues; + private readonly List> _persistentBoolsDataValues; #pragma warning restore 0649 /// /// Dictionary mapping persistent bool data values to booleans indicating whether they should be synchronised. /// [JsonIgnore] - public Dictionary PersistentBoolDataBools { get; private set; } + public Dictionary PersistentBoolDataBools { get; private set; } /// /// Bi-directional lookup that maps persistent bool names and their indices. @@ -92,14 +94,14 @@ public static SaveDataMapping Instance { /// #pragma warning disable 0649 [JsonProperty("persistentIntItems")] - private readonly List> _persistentIntDataValues; + private readonly List> _persistentIntDataValues; #pragma warning restore 0649 /// /// Dictionary mapping persistent int data values to booleans indicating whether they should be synchronised. /// [JsonIgnore] - public Dictionary PersistentIntDataBools { get; private set; } + public Dictionary PersistentIntDataBools { get; private set; } /// /// Bi-directional lookup that maps persistent int names and their indices. @@ -173,4 +175,34 @@ public void Initialize() { PersistentIntDataIndices.Add(persistentIntData, index++); } } + + /// + /// Properties that denote when to sync values. + /// + [UsedImplicitly] + internal class SyncProperties { + /// + /// Whether to sync this value. If true, the variable indicates where to store + /// the synced values. + /// + public bool Sync { get; set; } + /// + /// The sync type of this value. Type Player is used for player specific values and Server is used for + /// global world specific values. + /// + public SyncType SyncType { get; set; } + /// + /// Whether to ignore the check for scene host when sending/processing a save data for this value. + /// + public bool IgnoreSceneHost { get; set; } + } + + /// + /// The sync type for sync properties indicating whether values are global or player specific. + /// + [JsonConverter(typeof(StringEnumConverter))] + internal enum SyncType { + Player, + Server + } } diff --git a/HKMP/Game/Client/Save/SaveManager.cs b/HKMP/Game/Client/Save/SaveManager.cs index 1ea7100a..e694aaaf 100644 --- a/HKMP/Game/Client/Save/SaveManager.cs +++ b/HKMP/Game/Client/Save/SaveManager.cs @@ -369,13 +369,23 @@ private void OnSceneChanged(Scene oldScene, Scene newScene) { /// The name of the variable that was changed. /// Function to encode the value of the variable to a byte array. private void CheckSendSaveUpdate(string name, Func encodeFunc) { - if (!_entityManager.IsSceneHost) { - Logger.Info($"Not scene host, not sending save update ({name})"); + if (!_netClient.IsConnected) { + return; + } + + if (!SaveDataMapping.PlayerDataBools.TryGetValue(name, out var syncProps)) { + Logger.Info($"Not in save data values, not sending save update ({name})"); return; } - if (!SaveDataMapping.PlayerDataBools.TryGetValue(name, out var value) || !value) { - Logger.Info($"Not in save data values or false in save data values, not sending save update ({name})"); + if (!syncProps.Sync) { + Logger.Info($"Value should not sync, not sending save update ({name})"); + return; + } + + // If we should do the scene host check and the player is not scene host, skip sending + if (!syncProps.IgnoreSceneHost && !_entityManager.IsSceneHost) { + Logger.Info($"Not scene host, but required, not sending save update ({name})"); return; } @@ -416,13 +426,17 @@ private void OnUpdatePersistents() { Logger.Info($"Value for {itemData} changed to: {value}"); - if (!_entityManager.IsSceneHost) { - Logger.Info( - $"Not scene host, not sending persistent int/geo rock save update ({itemData.Id}, {itemData.SceneName})"); + if (!_netClient.IsConnected) { continue; } if (SaveDataMapping.GeoRockDataBools.TryGetValue(itemData, out var shouldSync) && shouldSync) { + if (!_entityManager.IsSceneHost) { + Logger.Info( + $"Not scene host, not sending geo rock save update ({itemData.Id}, {itemData.SceneName})"); + continue; + } + if (!SaveDataMapping.GeoRockDataIndices.TryGetValue(itemData, out var index)) { Logger.Info( $"Cannot find geo rock save data index, not sending save update ({itemData.Id}, {itemData.SceneName})"); @@ -435,7 +449,17 @@ private void OnUpdatePersistents() { index, new[] { (byte) value } ); - } else if (SaveDataMapping.PersistentIntDataBools.TryGetValue(itemData, out shouldSync) && shouldSync) { + } else if ( + SaveDataMapping.PersistentIntDataBools.TryGetValue(itemData, out var syncProps) && + syncProps.Sync + ) { + // If we should do the scene host check and the player is not scene host, skip sending + if (!syncProps.IgnoreSceneHost && !_entityManager.IsSceneHost) { + Logger.Info( + $"Not scene host, not sending geo rock save update ({itemData.Id}, {itemData.SceneName})"); + continue; + } + if (!SaveDataMapping.PersistentIntDataIndices.TryGetValue(itemData, out var index)) { Logger.Info( $"Cannot find persistent int save data index, not sending save update ({itemData.Id}, {itemData.SceneName})"); @@ -462,16 +486,22 @@ private void OnUpdatePersistents() { var itemData = persistentFsmData.PersistentItemData; Logger.Info($"Value for {itemData} changed to: {value}"); + + if (!_netClient.IsConnected) { + continue; + } - if (!_entityManager.IsSceneHost) { + if (!SaveDataMapping.PersistentBoolDataBools.TryGetValue(itemData, out var syncProps) || + !syncProps.Sync) { Logger.Info( - $"Not scene host, not sending geo rock save update ({itemData.Id}, {itemData.SceneName})"); + $"Not in persistent bool save data values or false in sync props, not sending save update ({itemData.Id}, {itemData.SceneName})"); continue; } - - if (!SaveDataMapping.PersistentBoolDataBools.TryGetValue(itemData, out var shouldSync) || !shouldSync) { + + // If we should do the scene host check and the player is not scene host, skip sending + if (!syncProps.IgnoreSceneHost && !_entityManager.IsSceneHost) { Logger.Info( - $"Not in persistent bool save data values or false in save data values, not sending save update ({itemData.Id}, {itemData.SceneName})"); + $"Not scene host, not sending persistent bool save update ({itemData.Id}, {itemData.SceneName})"); continue; } @@ -502,7 +532,15 @@ void CheckUpdates( Func changeFunc ) { foreach (var varName in variableNames) { - if (!SaveDataMapping.PlayerDataBools.TryGetValue(varName, out var shouldSync) || !shouldSync) { + if (!SaveDataMapping.PlayerDataBools.TryGetValue(varName, out var syncProps)) { + continue; + } + + if (!syncProps.Sync) { + continue; + } + + if (!syncProps.IgnoreSceneHost && !_entityManager.IsSceneHost) { continue; } @@ -523,7 +561,7 @@ Func changeFunc checkDict[varName] = newCheck; - if (_netClient.IsConnected && _entityManager.IsSceneHost) { + if (_netClient.IsConnected) { _netClient.UpdateManager.SetSaveUpdate( index, EncodeValue(variable) @@ -785,6 +823,7 @@ private void UpdateSaveWithData(ushort index, byte[] encodedValue) { }); } + // TODO: refactor this, remove probably if (index == SaveWarpIndex) { // Specific handling of warp bench data var respawnScene = DecodeString(encodedValue, 0); @@ -811,10 +850,11 @@ string DecodeString(byte[] encoded, int startIndex) { } /// - /// Get the current save data as a dictionary with mapped indices and encoded values. + /// Get the current save data as a dictionary with mapped indices and encoded values. This only returns the + /// global save data for a server. E.g. broken walls, open doors, defeated bosses. /// /// A dictionary with mapped indices and byte-encoded values. - public static Dictionary GetCurrentSaveData() { + public static Dictionary GetCurrentGlobalSaveData() { var pd = PlayerData.instance; var sd = SceneData.instance; @@ -823,15 +863,27 @@ public static Dictionary GetCurrentSaveData() { void AddToSaveData( IEnumerable enumerable, Func keyFunc, - Dictionary boolMapping, + object syncMapping, BiLookup indexMapping, Func valueFunc ) { foreach (var collectionValue in enumerable) { var key = keyFunc.Invoke(collectionValue); - if (!boolMapping.TryGetValue(key, out var shouldSync) || !shouldSync) { - continue; + if (syncMapping is Dictionary boolMapping) { + if (!boolMapping.TryGetValue(key, out var shouldSync) || !shouldSync) { + continue; + } + } else if (syncMapping is Dictionary syncPropMapping) { + if (!syncPropMapping.TryGetValue(key, out var syncProps)) { + continue; + } + + // Skip values that are not supposed to be synced, or ones that have the property that it is + // server data. Since we will not require the hosting player's save data on the server. + if (!syncProps.Sync || syncProps.SyncType != SaveDataMapping.SyncType.Server) { + continue; + } } if (!indexMapping.TryGetValue(key, out var index)) { diff --git a/HKMP/Game/GameManager.cs b/HKMP/Game/GameManager.cs index dd91b2e8..13f34cbc 100644 --- a/HKMP/Game/GameManager.cs +++ b/HKMP/Game/GameManager.cs @@ -14,6 +14,11 @@ namespace Hkmp.Game; /// Instantiates all necessary classes to start multiplayer activities. /// internal class GameManager { + /// + /// The server manager instance for the mod. + /// + public readonly ModServerManager ServerManager; + /// /// Constructs this GameManager instance by instantiating all other necessary classes. /// @@ -41,17 +46,17 @@ public GameManager(ModSettings modSettings) { netClient ); - var serverManager = new ModServerManager( + ServerManager = new ModServerManager( netServer, serverServerSettings, packetManager, uiManager ); - serverManager.Initialize(); + ServerManager.Initialize(); new ClientManager( netClient, - serverManager, + ServerManager, packetManager, uiManager, clientServerSettings, diff --git a/HKMP/Game/Server/ModServerManager.cs b/HKMP/Game/Server/ModServerManager.cs index b855fe31..174dbc10 100644 --- a/HKMP/Game/Server/ModServerManager.cs +++ b/HKMP/Game/Server/ModServerManager.cs @@ -12,6 +12,12 @@ namespace Hkmp.Game.Server; /// Specialization of that adds handlers for the mod specific things. /// internal class ModServerManager : ServerManager { + /// + /// Save data that was loaded from selecting a save file. Will be retroactively applied to a server, if one was + /// requested to be started after selecting a save file. + /// + private ServerSaveData _loadedLocalSaveData; + public ModServerManager( NetServer netServer, ServerSettings serverSettings, @@ -22,20 +28,40 @@ UiManager uiManager ModHooks.FinishedLoadingModsHook += AddonManager.LoadAddons; // Register handlers for UI events - uiManager.RequestServerStartHostEvent += port => { - CurrentSaveData = SaveManager.GetCurrentSaveData(); - Start(port); - }; + uiManager.RequestServerStartHostEvent += OnRequestServerStartHost; uiManager.RequestServerStopHostEvent += Stop; // Register application quit handler ModHooks.ApplicationQuitHook += Stop; } + private void OnRequestServerStartHost(int port) { + // Get the global save data from the save manager, which obtains the global save data from the loaded + // save file that the user selected. Then we import the player save data from the (potentially) loaded + // modded save file from the user selected save file. + ServerSaveData = new ServerSaveData { + GlobalSaveData = SaveManager.GetCurrentGlobalSaveData() + }; + + if (_loadedLocalSaveData != null) { + ServerSaveData.PlayerSaveData = _loadedLocalSaveData.PlayerSaveData; + } + + Start(port); + } + /// protected override void RegisterCommands() { base.RegisterCommands(); CommandManager.RegisterCommand(new SettingsCommand(this, InternalServerSettings)); } + + public void OnLoadLocal(ServerSaveData serverSaveData) { + _loadedLocalSaveData = serverSaveData; + } + + public ServerSaveData OnSaveLocal() { + return ServerSaveData; + } } diff --git a/HKMP/Game/Server/ServerManager.cs b/HKMP/Game/Server/ServerManager.cs index 95b6195e..c1269a16 100644 --- a/HKMP/Game/Server/ServerManager.cs +++ b/HKMP/Game/Server/ServerManager.cs @@ -10,6 +10,7 @@ using Hkmp.Eventing; using Hkmp.Eventing.ServerEvents; using Hkmp.Game.Client.Entity.Component; +using Hkmp.Game.Client.Save; using Hkmp.Game.Command.Server; using Hkmp.Game.Server.Auth; using Hkmp.Game.Settings; @@ -75,9 +76,9 @@ internal abstract class ServerManager : IServerManager { protected readonly ServerAddonManager AddonManager; /// - /// The current save data for the server. + /// The save data for the server. /// - protected Dictionary CurrentSaveData; + protected ServerSaveData ServerSaveData; #endregion @@ -128,6 +129,8 @@ PacketManager packetManager var serverApi = new ServerApi(this, CommandManager, _netServer, eventAggregator); AddonManager = new ServerAddonManager(serverApi); + ServerSaveData = new ServerSaveData(); + // Load the lists _whiteList = WhiteList.LoadFromFile(); _authorizedList = AuthKeyList.LoadFromFile(AuthorizedFileName); @@ -278,7 +281,10 @@ private void OnHelloServer(ushort id, HelloServer helloServer) { ); } - _netServer.GetUpdateManagerForClient(id).SetHelloClientData(CurrentSaveData, clientInfo); + _netServer.GetUpdateManagerForClient(id).SetHelloClientData( + ServerSaveData.GetMergedSaveData(playerData.AuthKey), + clientInfo + ); try { PlayerConnectEvent?.Invoke(playerData); @@ -1329,21 +1335,66 @@ protected virtual void OnSaveUpdate(ushort id, SaveUpdate packet) { Logger.Info($"Save update from ({id}, {playerData.Username}), index: {packet.SaveDataIndex}"); - if (!playerData.IsSceneHost) { - Logger.Info(" Player is not scene host, not broadcasting update"); + // Find the properties for syncing this save update, based on whether it is a geo rock, player data or + // persistent bool/int item + SaveDataMapping.SyncProperties syncProps; + if (SaveDataMapping.Instance.GeoRockDataIndices.TryGetValue(packet.SaveDataIndex, out var persistentItemData)) { + if (!SaveDataMapping.Instance.GeoRockDataBools.TryGetValue(persistentItemData, out _)) { + return; + } + + syncProps = new SaveDataMapping.SyncProperties { + Sync = true, + SyncType = SaveDataMapping.SyncType.Server, + IgnoreSceneHost = false + }; + } else if (SaveDataMapping.Instance.PlayerDataIndices.TryGetValue(packet.SaveDataIndex, out var name)) { + if (!SaveDataMapping.Instance.PlayerDataBools.TryGetValue(name, out syncProps)) { + return; + } + } else if (SaveDataMapping.Instance.PersistentBoolDataIndices.TryGetValue( + packet.SaveDataIndex, + out persistentItemData) + ) { + if (!SaveDataMapping.Instance.PersistentBoolDataBools.TryGetValue(persistentItemData, out syncProps)) { + return; + } + } else if (SaveDataMapping.Instance.PersistentIntDataIndices.TryGetValue( + packet.SaveDataIndex, + out persistentItemData) + ) { + if (!SaveDataMapping.Instance.PersistentIntDataBools.TryGetValue(persistentItemData, out syncProps)) { + return; + } + } else { + Logger.Info(" Could not find sync props for save update"); return; } - - // The save update is valid so we store it in our current save - CurrentSaveData[packet.SaveDataIndex] = packet.Value; - foreach (var idPlayerDataPair in _playerData) { - var otherId = idPlayerDataPair.Key; - if (id == otherId) { - continue; + // Check whether this save update requires the player to be scene host and do the check for it + if (!syncProps.IgnoreSceneHost && !playerData.IsSceneHost) { + Logger.Info(" Player is not scene host, but should be for update, not broadcasting"); + return; + } + + if (syncProps.SyncType == SaveDataMapping.SyncType.Player) { + if (!ServerSaveData.PlayerSaveData.TryGetValue(playerData.AuthKey, out var playerSaveData)) { + playerSaveData = new Dictionary(); + ServerSaveData.PlayerSaveData[playerData.AuthKey] = playerSaveData; } + + playerSaveData[packet.SaveDataIndex] = packet.Value; + } else if (syncProps.SyncType == SaveDataMapping.SyncType.Server) { + ServerSaveData.GlobalSaveData[packet.SaveDataIndex] = packet.Value; - _netServer.GetUpdateManagerForClient(otherId).SetSaveUpdate(packet.SaveDataIndex, packet.Value); + foreach (var idPlayerDataPair in _playerData) { + var otherId = idPlayerDataPair.Key; + if (id == otherId) { + continue; + } + + _netServer.GetUpdateManagerForClient(otherId).SetSaveUpdate(packet.SaveDataIndex, packet.Value); + } } } diff --git a/HKMP/Game/Server/ServerSaveData.cs b/HKMP/Game/Server/ServerSaveData.cs new file mode 100644 index 00000000..783a0bfd --- /dev/null +++ b/HKMP/Game/Server/ServerSaveData.cs @@ -0,0 +1,48 @@ +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace Hkmp.Game.Server; + +/// +/// Class that holds save data from a server. This consists of global data relating to the world and individual +/// data specific to each player. The JSON attribute are used to ensure only the player specific data is used +/// for serializing to modded save files. The global save data is already stored locally by the hosting player in +/// their normal save file. +/// +internal class ServerSaveData { + /// + /// The global save data for the server. E.g. broken walls, open doors, etc. + /// + [JsonIgnore] + public Dictionary GlobalSaveData { get; set; } + + /// + /// The player specific save data mapped to player's auth keys. + /// + [JsonProperty("player_save_data")] + public Dictionary> PlayerSaveData { get; set; } + + public ServerSaveData() { + GlobalSaveData = new Dictionary(); + PlayerSaveData = new Dictionary>(); + } + + /// + /// Get merged save data that contains global save data and player specific save data for the player with the + /// given auth key. + /// + /// The auth key that corresponds to the player for the player specific data. + /// A dictionary mapping save data indices to byte encoded values. + public Dictionary GetMergedSaveData(string authKey) { + if (!PlayerSaveData.TryGetValue(authKey, out var playerSaveData)) { + return new Dictionary(GlobalSaveData); + } + + var saveData = new Dictionary(GlobalSaveData); + foreach (var data in playerSaveData) { + saveData[data.Key] = data.Value; + } + + return saveData; + } +} diff --git a/HKMP/HkmpMod.cs b/HKMP/HkmpMod.cs index 45489cbf..85ed4096 100644 --- a/HKMP/HkmpMod.cs +++ b/HKMP/HkmpMod.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using Hkmp.Game.Server; using Hkmp.Game.Settings; using Hkmp.Logging; using Hkmp.Util; @@ -11,7 +12,7 @@ namespace Hkmp; /// /// Mod class for the HKMP mod. /// -internal class HkmpMod : Mod, IGlobalSettings { +internal class HkmpMod : Mod, IGlobalSettings, ILocalSettings { /// /// Dictionary containing preloaded objects by scene name and object path. /// @@ -22,6 +23,11 @@ internal class HkmpMod : Mod, IGlobalSettings { /// private ModSettings _modSettings = new ModSettings(); + /// + /// The game manager instance. + /// + private Game.GameManager _gameManager; + /// /// Construct the HKMP mod. /// @@ -55,7 +61,7 @@ public override void Initialize(Dictionary(); - var gameManager = new Game.GameManager(_modSettings); + _gameManager = new Game.GameManager(_modSettings); } /// @@ -67,4 +73,13 @@ public void OnLoadGlobal(ModSettings modSettings) { public ModSettings OnSaveGlobal() { return _modSettings; } + + + public void OnLoadLocal(ServerSaveData serverSaveData) { + _gameManager?.ServerManager?.OnLoadLocal(serverSaveData); + } + + public ServerSaveData OnSaveLocal() { + return _gameManager.ServerManager.OnSaveLocal(); + } } diff --git a/HKMP/Networking/Packet/Data/SaveUpdate.cs b/HKMP/Networking/Packet/Data/SaveUpdate.cs index ad3520c3..2a8ec1a2 100644 --- a/HKMP/Networking/Packet/Data/SaveUpdate.cs +++ b/HKMP/Networking/Packet/Data/SaveUpdate.cs @@ -27,7 +27,11 @@ internal class SaveUpdate : IPacketData { public void WriteData(IPacket packet) { packet.Write(SaveDataIndex); - var length = (byte) System.Math.Min(Value.Length, byte.MaxValue); + if (Value.Length > ushort.MaxValue) { + throw new Exception($"Number of bytes exceeds ushort max value: {Value.Length}"); + } + + var length = (ushort) Value.Length; packet.Write(length); for (var i = 0; i < length; i++) { packet.Write(Value[i]); @@ -38,7 +42,7 @@ public void WriteData(IPacket packet) { public void ReadData(IPacket packet) { SaveDataIndex = packet.ReadUShort(); - var length = packet.ReadByte(); + var length = packet.ReadUShort(); Value = new byte[length]; for (var i = 0; i < length; i++) { Value[i] = packet.ReadByte(); @@ -64,7 +68,23 @@ public CurrentSave() { /// public void WriteData(IPacket packet) { - var saveDataKeyCount = SaveData.Keys.Count; + WriteSaveDataDict(SaveData, packet); + } + + /// + public void ReadData(IPacket packet) { + SaveData = ReadSaveDataDict(packet); + } + + /// + /// Writes a save data dictionary to the given packet. + /// + /// The dictionary mapping indices to byte encoded values to write. + /// The packet to write the data into. + /// Thrown if the number of keys in the given save data dictionary is too large to + /// be written (> max ushort). + public static void WriteSaveDataDict(Dictionary dataDict, IPacket packet) { + var saveDataKeyCount = dataDict.Keys.Count; if (saveDataKeyCount > ushort.MaxValue) { throw new Exception("Number of keys in save data is too large"); } @@ -73,7 +93,7 @@ public void WriteData(IPacket packet) { packet.Write(dataLength); - foreach (var keyValuePair in SaveData) { + foreach (var keyValuePair in dataDict) { var saveDataIndex = keyValuePair.Key; var value = keyValuePair.Value; @@ -87,8 +107,14 @@ public void WriteData(IPacket packet) { } } - /// - public void ReadData(IPacket packet) { + /// + /// Reads a save data dictionary that maps indices to byte encoded values from the given + /// packet. + /// + /// The packet interface to read from. + /// A dictionary mapping save data indices to byte encoded values. + public static Dictionary ReadSaveDataDict(IPacket packet) { + var saveData = new Dictionary(); var dataLength = packet.ReadUShort(); for (var i = 0; i < dataLength; i++) { @@ -100,7 +126,9 @@ public void ReadData(IPacket packet) { value[j] = packet.ReadByte(); } - SaveData.Add(saveDataIndex, value); + saveData.Add(saveDataIndex, value); } + + return saveData; } } diff --git a/HKMP/Resource/save-data.json b/HKMP/Resource/save-data.json index 276bf272..89b162ee 100644 --- a/HKMP/Resource/save-data.json +++ b/HKMP/Resource/save-data.json @@ -1,1278 +1,5514 @@ { "playerData": { - "heartPieces": false, - "heartPieceMax": false, - "geo": false, - "vesselFragments": false, - "vesselFragmentMax": false, - "dreamgateMapPos": false, - "geoPool": false, - "hasSpell": false, - "fireballLevel": false, - "quakeLevel": false, - "screamLevel": false, - "hasNailArt": false, - "hasCyclone": false, - "hasDashSlash": false, - "hasUpwardSlash": false, - "hasAllNailArts": false, - "hasDreamNail": false, - "hasDreamGate": false, - "dreamNailUpgraded": false, - "dreamOrbs": false, - "dreamOrbsSpent": false, - "dreamGateScene": false, - "dreamGateX": false, - "dreamGateY": false, - "hasDash": false, - "hasWalljump": false, - "hasSuperDash": false, - "hasShadowDash": false, - "hasAcidArmour": false, - "hasDoubleJump": false, - "hasLantern": false, - "hasTramPass": false, - "hasQuill": false, - "hasCityKey": false, - "hasSlykey": false, - "gaveSlykey": false, - "hasWhiteKey": false, - "usedWhiteKey": false, - "hasMenderKey": true, - "hasWaterwaysKey": false, - "hasSpaKey": false, - "hasLoveKey": false, - "hasKingsBrand": false, - "hasXunFlower": false, - "ghostCoins": false, - "ore": false, - "foundGhostCoin": false, - "trinket1": false, - "foundTrinket1": false, - "trinket2": false, - "foundTrinket2": false, - "trinket3": false, - "foundTrinket3": false, - "trinket4": false, - "foundTrinket4": false, - "noTrinket1": false, - "noTrinket2": false, - "noTrinket3": false, - "noTrinket4": false, - "soldTrinket1": false, - "soldTrinket2": false, - "soldTrinket3": false, - "soldTrinket4": false, - "simpleKeys": false, - "rancidEggs": false, - "notchShroomOgres": false, - "notchFogCanyon": false, - "gotLurkerKey": false, - "guardiansDefeated": false, - "lurienDefeated": true, - "hegemolDefeated": true, - "monomonDefeated": true, - "maskBrokenLurien": true, - "maskBrokenHegemol": true, - "maskBrokenMonomon": true, - "maskToBreak": false, - "elderbug": false, - "metElderbug": false, - "elderbugReintro": false, - "elderbugHistory": false, - "elderbugHistory1": false, - "elderbugHistory2": false, - "elderbugHistory3": false, - "elderbugSpeechSly": false, - "elderbugSpeechStation": false, - "elderbugSpeechEggTemple": false, - "elderbugSpeechMapShop": false, - "elderbugSpeechBretta": false, - "elderbugSpeechJiji": false, - "elderbugSpeechMinesLift": false, - "elderbugSpeechKingsPass": false, - "elderbugSpeechInfectedCrossroads": false, - "elderbugSpeechFinalBossDoor": false, - "elderbugRequestedFlower": false, - "elderbugGaveFlower": false, - "elderbugFirstCall": false, - "metQuirrel": true, - "quirrelEggTemple": true, - "quirrelSlugShrine": true, - "quirrelRuins": true, - "quirrelMines": true, - "quirrelLeftStation": true, - "quirrelLeftEggTemple": true, - "quirrelCityEncountered": true, - "quirrelCityLeft": true, - "quirrelMinesEncountered": true, - "quirrelMinesLeft": true, - "quirrelMantisEncountered": true, - "enteredMantisLordArea": true, - "visitedDeepnestSpa": true, - "quirrelSpaReady": true, - "quirrelSpaEncountered": true, - "quirrelArchiveEncountered": true, - "quirrelEpilogueCompleted": true, - "metRelicDealer": false, - "metRelicDealerShop": false, - "marmOutside": true, - "marmOutsideConvo": false, - "marmConvo1": false, - "marmConvo2": false, - "marmConvo3": false, - "marmConvoNailsmith": false, - "cornifer": true, - "metCornifer": false, - "corniferIntroduced": false, - "corniferAtHome": true, - "corn_crossroadsEncountered": true, - "corn_crossroadsLeft": true, - "corn_greenpathEncountered": true, - "corn_greenpathLeft": true, - "corn_fogCanyonEncountered": true, - "corn_fogCanyonLeft": true, - "corn_fungalWastesEncountered": true, - "corn_fungalWastesLeft": true, - "corn_cityEncountered": true, - "corn_cityLeft": true, - "corn_waterwaysEncountered": true, - "corn_waterwaysLeft": true, - "corn_minesEncountered": true, - "corn_minesLeft": true, - "corn_cliffsEncountered": true, - "corn_cliffsLeft": true, - "corn_deepnestEncountered": true, - "corn_deepnestLeft": true, - "corn_deepnestMet1": true, - "corn_deepnestMet2": true, - "corn_outskirtsEncountered": true, - "corn_outskirtsLeft": true, - "corn_royalGardensEncountered": true, - "corn_royalGardensLeft": true, - "corn_abyssEncountered": true, - "corn_abyssLeft": true, - "metIselda": false, - "iseldaCorniferHomeConvo": false, - "iseldaConvo1": false, - "brettaRescued": true, - "brettaPosition": true, - "brettaState": true, - "brettaSeenBench": true, - "brettaSeenBed": true, - "brettaSeenBenchDiary": true, - "brettaSeenBedDiary": true, - "brettaLeftTown": true, - "slyRescued": true, - "slyBeta": true, - "metSlyShop": false, - "gotSlyCharm": false, - "slyShellFrag1": false, - "slyShellFrag2": false, - "slyShellFrag3": false, - "slyShellFrag4": false, - "slyVesselFrag1": false, - "slyVesselFrag2": false, - "slyVesselFrag3": false, - "slyVesselFrag4": false, - "slyNotch1": false, - "slyNotch2": false, - "slySimpleKey": false, - "slyRancidEgg": false, - "slyConvoNailArt": false, - "slyConvoMapper": false, - "slyConvoNailHoned": false, - "jijiDoorUnlocked": true, - "jijiMet": false, - "jijiShadeOffered": false, - "jijiShadeCharmConvo": false, - "metJinn": false, - "jinnConvo1": false, - "jinnConvo2": false, - "jinnConvo3": false, - "jinnConvoKingBrand": false, - "jinnConvoShadeCharm": false, - "jinnEggsSold": false, - "zote": true, - "zoteRescuedBuzzer": true, - "zoteDead": true, - "zoteDeathPos": true, - "zoteSpokenCity": true, - "zoteLeftCity": true, - "zoteTrappedDeepnest": true, - "zoteRescuedDeepnest": true, - "zoteDefeated": true, - "zoteSpokenColosseum": true, - "zotePrecept": false, - "zoteTownConvo": false, - "shaman": true, - "shamanScreamConvo": false, - "shamanQuakeConvo": false, - "shamanFireball2Convo": false, - "shamanScream2Convo": false, - "shamanQuake2Convo": false, - "metMiner": false, - "miner": true, - "minerEarly": false, - "hornetGreenpath": true, - "hornetFung": true, - "hornet_f19": true, - "hornetFountainEncounter": false, - "hornetCityBridge_ready": true, - "hornetCityBridge_completed": true, - "hornetAbyssEncounter": true, - "hornetDenEncounter": true, - "metMoth": false, - "ignoredMoth": false, - "gladeDoorOpened": true, - "mothDeparted": false, - "completedRGDreamPlant": false, - "dreamReward1": false, - "dreamReward2": false, - "dreamReward3": false, - "dreamReward4": false, - "dreamReward5": false, - "dreamReward5b": false, - "dreamReward6": false, - "dreamReward7": false, - "dreamReward8": false, - "dreamReward9": false, - "dreamMothConvo1": false, - "bankerAccountPurchased": false, - "metBanker": false, - "bankerBalance": false, - "bankerDeclined": false, - "bankerTheftCheck": true, - "bankerTheft": true, - "bankerSpaMet": false, - "metGiraffe": false, - "metCharmSlug": false, - "salubraNotch1": false, - "salubraNotch2": false, - "salubraNotch3": false, - "salubraNotch4": false, - "salubraBlessing": false, - "salubraConvoCombo": false, - "salubraConvoOvercharm": false, - "salubraConvoTruth": false, - "cultistTransformed": true, - "metNailsmith": false, - "nailSmithUpgrades": false, - "honedNail": false, - "nailsmithCliff": false, - "nailsmithKilled": false, - "nailsmithSpared": false, - "nailsmithKillSpeech": false, - "nailsmithSheo": true, - "nailsmithConvoArt": true, - "metNailmasterMato": false, - "metNailmasterSheo": false, - "metNailmasterOro": false, - "matoConvoSheo": false, - "matoConvoOro": false, - "matoConvoSly": false, - "sheoConvoMato": false, - "sheoConvoOro": false, - "sheoConvoSly": false, - "sheoConvoNailsmith": false, - "oroConvoSheo": false, - "oroConvoMato": false, - "oroConvoSly": false, - "hunterRoared": true, - "metHunter": false, - "hunterRewardOffered": false, - "huntersMarkOffered": false, - "hasHuntersMark": false, - "metLegEater": false, - "paidLegEater": false, - "refusedLegEater": false, - "legEaterConvo1": false, - "legEaterConvo2": false, - "legEaterConvo3": false, - "legEaterBrokenConvo": false, - "legEaterDungConvo": false, - "legEaterInfectedCrossroadConvo": false, - "legEaterBoughtConvo": false, - "legEaterGoldConvo": false, - "legEaterLeft": true, - "tukMet": false, - "tukEggPrice": false, - "tukDungEgg": false, - "metEmilitia": false, - "emilitiaKingsBrandConvo": false, - "metCloth": false, - "clothEnteredTramRoom": true, - "savedCloth": true, - "clothEncounteredQueensGarden": true, - "clothKilled": true, - "clothInTown": true, - "clothLeftTown": true, - "clothGhostSpoken": true, - "bigCatHitTail": false, - "bigCatHitTailConvo": false, - "bigCatMeet": false, - "bigCatTalk1": false, - "bigCatTalk2": false, - "bigCatTalk3": false, - "bigCatKingsBrandConvo": false, - "bigCatShadeConvo": false, - "tisoEncounteredTown": true, - "tisoEncounteredBench": true, - "tisoEncounteredLake": true, - "tisoEncounteredColosseum": true, - "tisoDead": true, - "tisoShieldConvo": true, - "mossCultist": true, - "maskmakerMet": false, - "maskmakerConvo1": false, - "maskmakerConvo2": false, - "maskmakerUnmasked1": false, - "maskmakerUnmasked2": false, - "maskmakerShadowDash": false, - "maskmakerKingsBrand": false, - "dungDefenderConvo1": false, - "dungDefenderConvo2": false, - "dungDefenderConvo3": false, - "dungDefenderCharmConvo": false, - "dungDefenderIsmaConvo": false, - "dungDefenderAwoken": true, - "dungDefenderLeft": true, - "dungDefenderAwakeConvo": false, - "midwifeMet": false, - "midwifeConvo1": false, - "midwifeConvo2": false, - "metQueen": false, - "queenTalk1": false, - "queenTalk2": false, - "queenDung1": false, - "queenDung2": false, - "queenHornet": false, - "queenTalkExtra": false, - "gotQueenFragment": false, - "queenConvo_grimm1": false, - "queenConvo_grimm2": false, - "gotKingFragment": false, - "metXun": false, - "xunFailedConvo1": false, - "xunFailedConvo2": false, - "xunFlowerBroken": false, - "xunFlowerBrokeTimes": false, - "xunFlowerGiven": false, - "xunRewardGiven": false, - "menderState": true, - "menderSignBroken": true, - "allBelieverTabletsDestroyed": true, - "mrMushroomState": true, - "openedMapperShop": true, - "openedSlyShop": true, - "metStag": false, - "stagPosition": true, - "stationsOpened": true, - "stagConvoTram": false, - "stagConvoTiso": false, - "stagRemember1": false, - "stagRemember2": false, - "stagRemember3": false, - "stagEggInspected": false, - "stagHopeConvo": false, - "littleFoolMet": false, - "ranAway": false, - "seenColosseumTitle": false, - "colosseumBronzeOpened": false, - "colosseumBronzeCompleted": false, - "colosseumSilverOpened": false, - "colosseumSilverCompleted": false, - "colosseumGoldOpened": false, - "colosseumGoldCompleted": false, - "openedTown": true, - "openedTownBuilding": true, - "openedCrossroads": true, - "openedGreenpath": true, - "openedRuins1": true, - "openedRuins2": true, - "openedFungalWastes": true, - "openedRoyalGardens": true, - "openedRestingGrounds": true, - "openedDeepnest": true, - "openedStagNest": true, - "openedHiddenStation": true, - "charmSlots": false, - "charmsOwned": false, - "gotCharm_1": false, - "gotCharm_2": false, - "gotCharm_3": false, - "gotCharm_4": false, - "gotCharm_5": false, - "gotCharm_6": false, - "gotCharm_7": false, - "gotCharm_8": false, - "gotCharm_9": false, - "gotCharm_10": false, - "gotCharm_11": false, - "gotCharm_12": false, - "gotCharm_13": false, - "gotCharm_14": false, - "gotCharm_15": false, - "gotCharm_16": false, - "gotCharm_17": false, - "gotCharm_18": false, - "gotCharm_19": false, - "gotCharm_20": false, - "gotCharm_21": false, - "gotCharm_22": false, - "gotCharm_23": false, - "gotCharm_24": false, - "gotCharm_25": false, - "gotCharm_26": false, - "gotCharm_27": false, - "gotCharm_28": false, - "gotCharm_29": false, - "gotCharm_30": false, - "gotCharm_31": false, - "gotCharm_32": false, - "gotCharm_33": false, - "gotCharm_34": false, - "gotCharm_35": false, - "gotCharm_36": false, - "gotCharm_37": false, - "gotCharm_38": false, - "gotCharm_39": false, - "gotCharm_40": false, - "fragileHealth_unbreakable": false, - "fragileGreed_unbreakable": false, - "fragileStrength_unbreakable": false, - "royalCharmState": false, - "hasJournal": false, - "seenJournalMsg": false, - "seenHunterMsg": false, - "fillJournal": false, - "journalEntriesCompleted": true, - "journalNotesCompleted": true, - "journalEntriesTotal": true, - "killedCrawler": true, - "killsCrawler": true, - "newDataCrawler": true, - "killedBuzzer": true, - "killsBuzzer": true, - "newDataBuzzer": true, - "killedBouncer": true, - "killsBouncer": true, - "newDataBouncer": true, - "killedClimber": true, - "killsClimber": true, - "newDataClimber": true, - "killedHopper": true, - "killsHopper": true, - "newDataHopper": true, - "killedWorm": true, - "killsWorm": true, - "newDataWorm": true, - "killedSpitter": true, - "killsSpitter": true, - "newDataSpitter": true, - "killedHatcher": true, - "killsHatcher": true, - "newDataHatcher": true, - "killedHatchling": true, - "killsHatchling": true, - "newDataHatchling": true, - "killedZombieRunner": true, - "killsZombieRunner": true, - "newDataZombieRunner": true, - "killedZombieHornhead": true, - "killsZombieHornhead": true, - "newDataZombieHornhead": true, - "killedZombieLeaper": true, - "killsZombieLeaper": true, - "newDataZombieLeaper": true, - "killedZombieBarger": true, - "killsZombieBarger": true, - "newDataZombieBarger": true, - "killedZombieShield": true, - "killsZombieShield": true, - "newDataZombieShield": true, - "killedZombieGuard": true, - "killsZombieGuard": true, - "newDataZombieGuard": true, - "killedBigBuzzer": true, - "killsBigBuzzer": true, - "newDataBigBuzzer": true, - "killedBigFly": true, - "killsBigFly": true, - "newDataBigFly": true, - "killedMawlek": true, - "killsMawlek": true, - "newDataMawlek": true, - "killedFalseKnight": true, - "killsFalseKnight": true, - "newDataFalseKnight": true, - "killedRoller": true, - "killsRoller": true, - "newDataRoller": true, - "killedBlocker": true, - "killsBlocker": true, - "newDataBlocker": true, - "killedPrayerSlug": true, - "killsPrayerSlug": true, - "newDataPrayerSlug": true, - "killedMenderBug": true, - "killsMenderBug": true, - "newDataMenderBug": true, - "killedMossmanRunner": true, - "killsMossmanRunner": true, - "newDataMossmanRunner": true, - "killedMossmanShaker": true, - "killsMossmanShaker": true, - "newDataMossmanShaker": true, - "killedMosquito": true, - "killsMosquito": true, - "newDataMosquito": true, - "killedBlobFlyer": true, - "killsBlobFlyer": true, - "newDataBlobFlyer": true, - "killedFungifiedZombie": true, - "killsFungifiedZombie": true, - "newDataFungifiedZombie": true, - "killedPlantShooter": true, - "killsPlantShooter": true, - "newDataPlantShooter": true, - "killedMossCharger": true, - "killsMossCharger": true, - "newDataMossCharger": true, - "killedMegaMossCharger": true, - "killsMegaMossCharger": true, - "newDataMegaMossCharger": true, - "killedSnapperTrap": true, - "killsSnapperTrap": true, - "newDataSnapperTrap": true, - "killedMossKnight": true, - "killsMossKnight": true, - "newDataMossKnight": true, - "killedGrassHopper": true, - "killsGrassHopper": true, - "newDataGrassHopper": true, - "killedAcidFlyer": true, - "killsAcidFlyer": true, - "newDataAcidFlyer": true, - "killedAcidWalker": true, - "killsAcidWalker": true, - "newDataAcidWalker": true, - "killedMossFlyer": true, - "killsMossFlyer": true, - "newDataMossFlyer": true, - "killedMossKnightFat": true, - "killsMossKnightFat": true, - "newDataMossKnightFat": true, - "killedMossWalker": true, - "killsMossWalker": true, - "newDataMossWalker": true, - "killedInfectedKnight": true, - "killsInfectedKnight": true, - "newDataInfectedKnight": true, - "killedLazyFlyer": true, - "killsLazyFlyer": true, - "newDataLazyFlyer": true, - "killedZapBug": true, - "killsZapBug": true, - "newDataZapBug": true, - "killedJellyfish": true, - "killsJellyfish": true, - "newDataJellyfish": true, - "killedJellyCrawler": true, - "killsJellyCrawler": true, - "newDataJellyCrawler": true, - "killedMegaJellyfish": true, - "killsMegaJellyfish": true, - "newDataMegaJellyfish": true, - "killedFungoonBaby": true, - "killsFungoonBaby": true, - "newDataFungoonBaby": true, - "killedMushroomTurret": true, - "killsMushroomTurret": true, - "newDataMushroomTurret": true, - "killedMantis": true, - "killsMantis": true, - "newDataMantis": true, - "killedMushroomRoller": true, - "killsMushroomRoller": true, - "newDataMushroomRoller": true, - "killedMushroomBrawler": true, - "killsMushroomBrawler": true, - "newDataMushroomBrawler": true, - "killedMushroomBaby": true, - "killsMushroomBaby": true, - "newDataMushroomBaby": true, - "killedMantisFlyerChild": true, - "killsMantisFlyerChild": true, - "newDataMantisFlyerChild": true, - "killedFungusFlyer": true, - "killsFungusFlyer": true, - "newDataFungusFlyer": true, - "killedFungCrawler": true, - "killsFungCrawler": true, - "newDataFungCrawler": true, - "killedMantisLord": true, - "killsMantisLord": true, - "newDataMantisLord": true, - "killedBlackKnight": true, - "killsBlackKnight": true, - "newDataBlackKnight": true, - "killedElectricMage": true, - "killsElectricMage": true, - "newDataElectricMage": true, - "killedMage": true, - "killsMage": true, - "newDataMage": true, - "killedMageKnight": true, - "killsMageKnight": true, - "newDataMageKnight": true, - "killedRoyalDandy": true, - "killsRoyalDandy": true, - "newDataRoyalDandy": true, - "killedRoyalCoward": true, - "killsRoyalCoward": true, - "newDataRoyalCoward": true, - "killedRoyalPlumper": true, - "killsRoyalPlumper": true, - "newDataRoyalPlumper": true, - "killedFlyingSentrySword": true, - "killsFlyingSentrySword": true, - "newDataFlyingSentrySword": true, - "killedFlyingSentryJavelin": true, - "killsFlyingSentryJavelin": true, - "newDataFlyingSentryJavelin": true, - "killedSentry": true, - "killsSentry": true, - "newDataSentry": true, - "killedSentryFat": true, - "killsSentryFat": true, - "newDataSentryFat": true, - "killedMageBlob": true, - "killsMageBlob": true, - "newDataMageBlob": true, - "killedGreatShieldZombie": true, - "killsGreatShieldZombie": true, - "newDataGreatShieldZombie": true, - "killedJarCollector": true, - "killsJarCollector": true, - "newDataJarCollector": true, - "killedMageBalloon": true, - "killsMageBalloon": true, - "newDataMageBalloon": true, - "killedMageLord": true, - "killsMageLord": true, - "newDataMageLord": true, - "killedGorgeousHusk": true, - "killsGorgeousHusk": true, - "newDataGorgeousHusk": true, - "killedFlipHopper": true, - "killsFlipHopper": true, - "newDataFlipHopper": true, - "killedFlukeman": true, - "killsFlukeman": true, - "newDataFlukeman": true, - "killedInflater": true, - "killsInflater": true, - "newDataInflater": true, - "killedFlukefly": true, - "killsFlukefly": true, - "newDataFlukefly": true, - "killedFlukeMother": true, - "killsFlukeMother": true, - "newDataFlukeMother": true, - "killedDungDefender": true, - "killsDungDefender": true, - "newDataDungDefender": true, - "killedCrystalCrawler": true, - "killsCrystalCrawler": true, - "newDataCrystalCrawler": true, - "killedCrystalFlyer": true, - "killsCrystalFlyer": true, - "newDataCrystalFlyer": true, - "killedLaserBug": true, - "killsLaserBug": true, - "newDataLaserBug": true, - "killedBeamMiner": true, - "killsBeamMiner": true, - "newDataBeamMiner": true, - "killedZombieMiner": true, - "killsZombieMiner": true, - "newDataZombieMiner": true, - "killedMegaBeamMiner": true, - "killsMegaBeamMiner": true, - "newDataMegaBeamMiner": true, - "killedMinesCrawler": true, - "killsMinesCrawler": true, - "newDataMinesCrawler": true, - "killedAngryBuzzer": true, - "killsAngryBuzzer": true, - "newDataAngryBuzzer": true, - "killedBurstingBouncer": true, - "killsBurstingBouncer": true, - "newDataBurstingBouncer": true, - "killedBurstingZombie": true, - "killsBurstingZombie": true, - "newDataBurstingZombie": true, - "killedSpittingZombie": true, - "killsSpittingZombie": true, - "newDataSpittingZombie": true, - "killedBabyCentipede": true, - "killsBabyCentipede": true, - "newDataBabyCentipede": true, - "killedBigCentipede": true, - "killsBigCentipede": true, - "newDataBigCentipede": true, - "killedCentipedeHatcher": true, - "killsCentipedeHatcher": true, - "newDataCentipedeHatcher": true, - "killedLesserMawlek": true, - "killsLesserMawlek": true, - "newDataLesserMawlek": true, - "killedSlashSpider": true, - "killsSlashSpider": true, - "newDataSlashSpider": true, - "killedSpiderCorpse": true, - "killsSpiderCorpse": true, - "newDataSpiderCorpse": true, - "killedShootSpider": true, - "killsShootSpider": true, - "newDataShootSpider": true, - "killedMiniSpider": true, - "killsMiniSpider": true, - "newDataMiniSpider": true, - "killedSpiderFlyer": true, - "killsSpiderFlyer": true, - "newDataSpiderFlyer": true, - "killedMimicSpider": true, - "killsMimicSpider": true, - "newDataMimicSpider": true, - "killedBeeHatchling": true, - "killsBeeHatchling": true, - "newDataBeeHatchling": true, - "killedBeeStinger": true, - "killsBeeStinger": true, - "newDataBeeStinger": true, - "killedBigBee": true, - "killsBigBee": true, - "newDataBigBee": true, - "killedHiveKnight": true, - "killsHiveKnight": true, - "newDataHiveKnight": true, - "killedBlowFly": true, - "killsBlowFly": true, - "newDataBlowFly": true, - "killedCeilingDropper": true, - "killsCeilingDropper": true, - "newDataCeilingDropper": true, - "killedGiantHopper": true, - "killsGiantHopper": true, - "newDataGiantHopper": true, - "killedGrubMimic": true, - "killsGrubMimic": true, - "newDataGrubMimic": true, - "killedMawlekTurret": true, - "killsMawlekTurret": true, - "newDataMawlekTurret": true, - "killedOrangeScuttler": true, - "killsOrangeScuttler": true, - "newDataOrangeScuttler": true, - "killedHealthScuttler": true, - "killsHealthScuttler": true, - "newDataHealthScuttler": true, - "killedPigeon": true, - "killsPigeon": true, - "newDataPigeon": true, - "killedZombieHive": true, - "killsZombieHive": true, - "newDataZombieHive": true, - "killedDreamGuard": true, - "killsDreamGuard": true, - "newDataDreamGuard": true, - "killedHornet": true, - "killsHornet": true, - "newDataHornet": true, - "killedAbyssCrawler": true, - "killsAbyssCrawler": true, - "newDataAbyssCrawler": true, - "killedSuperSpitter": true, - "killsSuperSpitter": true, - "newDataSuperSpitter": true, - "killedSibling": true, - "killsSibling": true, - "newDataSibling": true, - "killedPalaceFly": true, - "killsPalaceFly": true, - "newDataPalaceFly": true, - "killedEggSac": true, - "killsEggSac": true, - "newDataEggSac": true, - "killedMummy": true, - "killsMummy": true, - "newDataMummy": true, - "killedOrangeBalloon": true, - "killsOrangeBalloon": true, - "newDataOrangeBalloon": true, - "killedAbyssTendril": true, - "killsAbyssTendril": true, - "newDataAbyssTendril": true, - "killedHeavyMantis": true, - "killsHeavyMantis": true, - "newDataHeavyMantis": true, - "killedTraitorLord": true, - "killsTraitorLord": true, - "newDataTraitorLord": true, - "killedMantisHeavyFlyer": true, - "killsMantisHeavyFlyer": true, - "newDataMantisHeavyFlyer": true, - "killedGardenZombie": true, - "killsGardenZombie": true, - "newDataGardenZombie": true, - "killedRoyalGuard": true, - "killsRoyalGuard": true, - "newDataRoyalGuard": true, - "killedWhiteRoyal": true, - "killsWhiteRoyal": true, - "newDataWhiteRoyal": true, - "openedPalaceGrounds": true, - "killedOblobble": true, - "killsOblobble": true, - "newDataOblobble": true, - "killedZote": true, - "killsZote": true, - "newDataZote": true, - "killedBlobble": true, - "killsBlobble": true, - "newDataBlobble": true, - "killedColMosquito": true, - "killsColMosquito": true, - "newDataColMosquito": true, - "killedColRoller": true, - "killsColRoller": true, - "newDataColRoller": true, - "killedColFlyingSentry": true, - "killsColFlyingSentry": true, - "newDataColFlyingSentry": true, - "killedColMiner": true, - "killsColMiner": true, - "newDataColMiner": true, - "killedColShield": true, - "killsColShield": true, - "newDataColShield": true, - "killedColWorm": true, - "killsColWorm": true, - "newDataColWorm": true, - "killedColHopper": true, - "killsColHopper": true, - "newDataColHopper": true, - "killedLobsterLancer": true, - "killsLobsterLancer": true, - "newDataLobsterLancer": true, - "killedGhostAladar": true, - "killsGhostAladar": true, - "newDataGhostAladar": true, - "killedGhostXero": true, - "killsGhostXero": true, - "newDataGhostXero": true, - "killedGhostHu": true, - "killsGhostHu": true, - "newDataGhostHu": true, - "killedGhostMarmu": true, - "killsGhostMarmu": true, - "newDataGhostMarmu": true, - "killedGhostNoEyes": true, - "killsGhostNoEyes": true, - "newDataGhostNoEyes": true, - "killedGhostMarkoth": true, - "killsGhostMarkoth": true, - "newDataGhostMarkoth": true, - "killedGhostGalien": true, - "killsGhostGalien": true, - "newDataGhostGalien": true, - "killedWhiteDefender": true, - "killsWhiteDefender": true, - "newDataWhiteDefender": true, - "killedGreyPrince": true, - "killsGreyPrince": true, - "newDataGreyPrince": true, - "killedZotelingBalloon": true, - "killsZotelingBalloon": true, - "newDataZotelingBalloon": true, - "killedZotelingHopper": true, - "killsZotelingHopper": true, - "newDataZotelingHopper": true, - "killedZotelingBuzzer": true, - "killsZotelingBuzzer": true, - "newDataZotelingBuzzer": true, - "killedHollowKnight": true, - "killsHollowKnight": true, - "newDataHollowKnight": true, - "killedFinalBoss": true, - "killsFinalBoss": true, - "newDataFinalBoss": true, - "killedHunterMark": true, - "killsHunterMark": true, - "newDataHunterMark": true, - "killedFlameBearerSmall": true, - "killsFlameBearerSmall": true, - "newDataFlameBearerSmall": true, - "killedFlameBearerMed": true, - "killsFlameBearerMed": true, - "newDataFlameBearerMed": true, - "killedFlameBearerLarge": true, - "killsFlameBearerLarge": true, - "newDataFlameBearerLarge": true, - "killedGrimm": true, - "killsGrimm": true, - "newDataGrimm": true, - "killedNightmareGrimm": true, - "killsNightmareGrimm": true, - "newDataNightmareGrimm": true, - "killedBindingSeal": true, - "killsBindingSeal": true, - "newDataBindingSeal": true, - "killedFatFluke": true, - "killsFatFluke": true, - "newDataFatFluke": true, - "killedPaleLurker": true, - "killsPaleLurker": true, - "newDataPaleLurker": true, - "killedNailBros": true, - "killsNailBros": true, - "newDataNailBros": true, - "killedPaintmaster": true, - "killsPaintmaster": true, - "newDataPaintmaster": true, - "killedNailsage": true, - "killsNailsage": true, - "newDataNailsage": true, - "killedHollowKnightPrime": true, - "killsHollowKnightPrime": true, - "newDataHollowKnightPrime": true, - "killedGodseekerMask": true, - "killsGodseekerMask": true, - "newDataGodseekerMask": true, - "killedVoidIdol_1": true, - "killsVoidIdol_1": true, - "newDataVoidIdol_1": true, - "killedVoidIdol_2": true, - "killsVoidIdol_2": true, - "newDataVoidIdol_2": true, - "killedVoidIdol_3": true, - "killsVoidIdol_3": true, - "newDataVoidIdol_3": true, - "grubsCollected": true, - "grubRewards": false, - "finalGrubRewardCollected": false, - "fatGrubKing": false, - "falseKnightDefeated": true, - "falseKnightDreamDefeated": true, - "falseKnightOrbsCollected": false, - "mawlekDefeated": true, - "giantBuzzerDefeated": true, - "giantFlyDefeated": true, - "blocker1Defeated": true, - "blocker2Defeated": true, - "hornet1Defeated": true, - "collectorDefeated": true, - "hornetOutskirtsDefeated": true, - "mageLordDreamDefeated": true, - "mageLordOrbsCollected": false, - "infectedKnightDreamDefeated": true, - "infectedKnightOrbsCollected": false, - "whiteDefenderDefeated": true, - "whiteDefenderOrbsCollected": false, - "whiteDefenderDefeats": true, - "greyPrinceDefeats": true, - "greyPrinceDefeated": true, - "greyPrinceOrbsCollected": false, - "aladarSlugDefeated": true, - "xeroDefeated": true, - "elderHuDefeated": true, - "mumCaterpillarDefeated": true, - "noEyesDefeated": true, - "markothDefeated": true, - "galienDefeated": true, - "XERO_encountered": false, - "ALADAR_encountered": false, - "HU_encountered": false, - "MUMCAT_encountered": false, - "NOEYES_encountered": false, - "MARKOTH_encountered": false, - "GALIEN_encountered": false, - "xeroPinned": true, - "aladarPinned": true, - "huPinned": true, - "mumCaterpillarPinned": true, - "noEyesPinned": true, - "markothPinned": true, - "galienPinned": true, - "scenesVisited": true, - "scenesMapped": true, - "scenesEncounteredBench": true, - "scenesGrubRescued": true, - "scenesFlameCollected": true, - "scenesEncounteredCocoon": true, - "scenesEncounteredDreamPlant": true, - "scenesEncounteredDreamPlantC": false, - "hasMap": false, - "mapDirtmouth": false, - "mapCrossroads": false, - "mapGreenpath": false, - "mapFogCanyon": false, - "mapRoyalGardens": false, - "mapFungalWastes": false, - "mapCity": false, - "mapWaterways": false, - "mapMines": false, - "mapDeepnest": false, - "mapCliffs": false, - "mapOutskirts": false, - "mapRestingGrounds": false, - "mapAbyss": false, - "hasPin": false, - "hasPinBench": false, - "hasPinCocoon": false, - "hasPinDreamPlant": false, - "hasPinGuardian": false, - "hasPinBlackEgg": false, - "hasPinShop": false, - "hasPinSpa": false, - "hasPinStag": false, - "hasPinTram": false, - "hasPinGhost": false, - "hasPinGrub": false, - "hasMarker": false, - "hasMarker_r": false, - "hasMarker_b": false, - "hasMarker_y": false, - "hasMarker_w": false, - "openedTramLower": false, - "openedTramRestingGrounds": false, - "tramLowerPosition": true, - "tramRestingGroundsPosition": true, - "mineLiftOpened": true, - "menderDoorOpened": true, - "vesselFragStagNest": false, - "shamanPillar": true, - "crossroadsMawlekWall": true, - "eggTempleVisited": false, - "crossroadsInfected": true, - "falseKnightFirstPlop": true, - "falseKnightWallRepaired": true, - "falseKnightWallBroken": true, - "falseKnightGhostDeparted": true, - "spaBugsEncountered": true, - "hornheadVinePlat": true, - "infectedKnightEncountered": true, - "megaMossChargerEncountered": true, - "megaMossChargerDefeated": true, - "dreamerScene1": true, - "slugEncounterComplete": true, - "defeatedDoubleBlockers": true, - "oneWayArchive": true, - "defeatedMegaJelly": true, - "summonedMonomon": true, - "sawWoundedQuirrel": true, - "encounteredMegaJelly": true, - "defeatedMantisLords": true, - "encounteredGatekeeper": true, - "deepnestWall": true, - "queensStationNonDisplay": true, - "cityBridge1": true, - "cityBridge2": true, - "cityLift1": true, - "cityLift1_isUp": true, - "liftArrival": true, - "openedMageDoor": true, - "openedMageDoor_v2": true, - "brokenMageWindow": true, - "brokenMageWindowGlass": true, - "mageLordEncountered": true, - "mageLordEncountered_2": true, - "mageLordDefeated": true, - "ruins1_5_tripleDoor": true, - "openedCityGate": true, - "cityGateClosed": true, - "bathHouseOpened": true, - "bathHouseWall": true, - "cityLift2": true, - "cityLift2_isUp": true, - "city2_sewerDoor": true, - "openedLoveDoor": true, - "watcherChandelier": true, - "completedQuakeArea": true, - "kingsStationNonDisplay": true, - "tollBenchCity": true, - "waterwaysGate": true, - "defeatedDungDefender": true, - "dungDefenderEncounterReady": true, - "flukeMotherEncountered": true, - "flukeMotherDefeated": true, - "openedWaterwaysManhole": true, - "waterwaysAcidDrained": true, - "dungDefenderWallBroken": true, - "dungDefenderSleeping": true, - "defeatedMegaBeamMiner": true, - "defeatedMegaBeamMiner2": true, - "brokeMinersWall": true, - "encounteredMimicSpider": true, - "steppedBeyondBridge": true, - "deepnestBridgeCollapsed": true, - "spiderCapture": false, - "deepnest26b_switch": true, - "openedRestingGrounds02": true, - "restingGroundsCryptWall": true, - "dreamNailConvo": false, - "gladeGhostsKilled": true, - "openedGardensStagStation": true, - "extendedGramophone": true, - "tollBenchQueensGardens": true, - "blizzardEnded": true, - "encounteredHornet": true, - "savedByHornet": true, - "outskirtsWall": true, - "abyssGateOpened": true, - "abyssLighthouse": true, - "blueVineDoor": true, - "gotShadeCharm": true, - "tollBenchAbyss": true, - "fountainGeo": false, - "fountainVesselSummoned": false, - "openedBlackEggPath": true, - "enteredDreamWorld": false, - "duskKnightDefeated": true, - "whitePalaceOrb_1": true, - "whitePalaceOrb_2": true, - "whitePalaceOrb_3": true, - "whitePalace05_lever": true, - "whitePalaceMidWarp": true, - "whitePalaceSecretRoomVisited": true, - "tramOpenedDeepnest": true, - "tramOpenedCrossroads": true, - "openedBlackEggDoor": true, - "unchainedHollowKnight": true, - "flamesCollected": true, - "flamesRequired": true, - "nightmareLanternAppeared": true, - "nightmareLanternLit": true, - "troupeInTown": true, - "divineInTown": true, - "grimmChildLevel": true, - "elderbugConvoGrimm": false, - "slyConvoGrimm": false, - "iseldaConvoGrimm": false, - "midwifeWeaverlingConvo": false, - "metGrimm": true, - "foughtGrimm": true, - "metBrum": false, - "defeatedNightmareGrimm": true, - "grimmchildAwoken": true, - "gotBrummsFlame": true, - "brummBrokeBrazier": true, - "destroyedNightmareLantern": true, - "gotGrimmNotch": false, - "nymmInTown": true, - "nymmSpoken": false, - "nymmCharmConvo": false, - "nymmFinalConvo": false, - "elderbugNymmConvo": false, - "slyNymmConvo": false, - "iseldaNymmConvo": false, - "nymmMissedEggOpen": false, - "elderbugTroupeLeftConvo": false, - "elderbugBrettaLeft": false, - "jijiGrimmConvo": false, - "metDivine": false, - "divineFinalConvo": false, - "gaveFragileHeart": false, - "gaveFragileGreed": false, - "gaveFragileStrength": false, - "divineEatenConvos": false, - "pooedFragileHeart": false, - "pooedFragileGreed": false, - "pooedFragileStrength": false, - "completionPercentage": false, - "unlockedCompletionRate": false, - "newDatTraitorLord": true, - "bossDoorStateTier1": true, - "bossDoorStateTier2": true, - "bossDoorStateTier3": true, - "bossDoorStateTier4": true, - "bossDoorStateTier5": true, - "bossStatueTargetLevel": false, - "statueStateGruzMother": true, - "statueStateVengefly": true, - "statueStateBroodingMawlek": true, - "statueStateFalseKnight": true, - "statueStateFailedChampion": true, - "statueStateHornet1": true, - "statueStateHornet2": true, - "statueStateMegaMossCharger": true, - "statueStateMantisLords": true, - "statueStateOblobbles": true, - "statueStateGreyPrince": true, - "statueStateBrokenVessel": true, - "statueStateLostKin": true, - "statueStateNosk": true, - "statueStateFlukemarm": true, - "statueStateCollector": true, - "statueStateWatcherKnights": true, - "statueStateSoulMaster": true, - "statueStateSoulTyrant": true, - "statueStateGodTamer": true, - "statueStateCrystalGuardian1": true, - "statueStateCrystalGuardian2": true, - "statueStateUumuu": true, - "statueStateDungDefender": true, - "statueStateWhiteDefender": true, - "statueStateHiveKnight": true, - "statueStateTraitorLord": true, - "statueStateGrimm": true, - "statueStateNightmareGrimm": true, - "statueStateHollowKnight": true, - "statueStateElderHu": true, - "statueStateGalien": true, - "statueStateMarkoth": true, - "statueStateMarmu": true, - "statueStateNoEyes": true, - "statueStateXero": true, - "statueStateGorb": true, - "statueStateRadiance": true, - "statueStateSly": true, - "statueStateNailmasters": true, - "statueStateMageKnight": true, - "statueStatePaintmaster": true, - "statueStateZote": true, - "statueStateNoskHornet": true, - "statueStateMantisLordsExtra": true, - "godseekerUnlocked": true, - "bossDoorCageUnlocked": true, - "blueRoomDoorUnlocked": true, - "blueRoomActivated": true, - "finalBossDoorUnlocked": true, - "hasGodfinder": false, - "unlockedNewBossStatue": true, - "scaredFlukeHermitEncountered": false, - "scaredFlukeHermitReturned": false, - "enteredGGAtrium": false, - "extraFlowerAppear": true, - "givenGodseekerFlower": true, - "givenOroFlower": true, - "givenWhiteLadyFlower": true, - "givenEmilitiaFlower": true, - "unlockedBossScenes": false, - "queuedGodfinderIcon": false, - "godseekerSpokenAwake": false, - "nailsmithCorpseAppeared": true, - "godseekerWaterwaysSeenState": true, - "godseekerWaterwaysSpoken1": false, - "godseekerWaterwaysSpoken2": false, - "godseekerWaterwaysSpoken3": false, - "bossDoorEntranceTextSeen": false, - "seenDoor4Finale": true, - "zoteStatueWallBroken": true, - "seenGGWastes": false, - "ordealAchieved": true + "heartPieces": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "heartPieceMax": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "geo": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "vesselFragments": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "vesselFragmentMax": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "dreamgateMapPos": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "geoPool": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "hasSpell": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "fireballLevel": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "quakeLevel": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "screamLevel": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "hasNailArt": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "hasCyclone": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "hasDashSlash": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "hasUpwardSlash": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "hasAllNailArts": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "hasDreamNail": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "hasDreamGate": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "dreamNailUpgraded": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "dreamOrbs": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "dreamOrbsSpent": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "dreamGateScene": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "dreamGateX": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "dreamGateY": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "hasDash": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "hasWalljump": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "hasSuperDash": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "hasShadowDash": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "hasAcidArmour": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "hasDoubleJump": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "hasLantern": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "hasTramPass": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "hasQuill": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "hasCityKey": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "hasSlykey": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "gaveSlykey": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "hasWhiteKey": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "usedWhiteKey": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "hasMenderKey": { + "Sync": true, + "SyncType": "Server" + }, + "hasWaterwaysKey": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "hasSpaKey": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "hasLoveKey": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "hasKingsBrand": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "hasXunFlower": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "ghostCoins": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "ore": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "foundGhostCoin": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "trinket1": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "foundTrinket1": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "trinket2": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "foundTrinket2": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "trinket3": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "foundTrinket3": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "trinket4": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "foundTrinket4": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "noTrinket1": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "noTrinket2": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "noTrinket3": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "noTrinket4": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "soldTrinket1": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "soldTrinket2": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "soldTrinket3": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "soldTrinket4": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "simpleKeys": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "rancidEggs": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "notchShroomOgres": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "notchFogCanyon": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "gotLurkerKey": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "guardiansDefeated": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "lurienDefeated": { + "Sync": true, + "SyncType": "Server" + }, + "hegemolDefeated": { + "Sync": true, + "SyncType": "Server" + }, + "monomonDefeated": { + "Sync": true, + "SyncType": "Server" + }, + "maskBrokenLurien": { + "Sync": true, + "SyncType": "Server" + }, + "maskBrokenHegemol": { + "Sync": true, + "SyncType": "Server" + }, + "maskBrokenMonomon": { + "Sync": true, + "SyncType": "Server" + }, + "maskToBreak": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "elderbug": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "metElderbug": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "elderbugReintro": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "elderbugHistory": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "elderbugHistory1": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "elderbugHistory2": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "elderbugHistory3": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "elderbugSpeechSly": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "elderbugSpeechStation": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "elderbugSpeechEggTemple": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "elderbugSpeechMapShop": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "elderbugSpeechBretta": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "elderbugSpeechJiji": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "elderbugSpeechMinesLift": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "elderbugSpeechKingsPass": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "elderbugSpeechInfectedCrossroads": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "elderbugSpeechFinalBossDoor": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "elderbugRequestedFlower": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "elderbugGaveFlower": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "elderbugFirstCall": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "metQuirrel": { + "Sync": true, + "SyncType": "Server", + "IgnoreSceneHost": true + }, + "quirrelEggTemple": { + "Sync": true, + "SyncType": "Server" + }, + "quirrelSlugShrine": { + "Sync": true, + "SyncType": "Server" + }, + "quirrelRuins": { + "Sync": true, + "SyncType": "Server" + }, + "quirrelMines": { + "Sync": true, + "SyncType": "Server" + }, + "quirrelLeftStation": { + "Sync": true, + "SyncType": "Server" + }, + "quirrelLeftEggTemple": { + "Sync": true, + "SyncType": "Server" + }, + "quirrelCityEncountered": { + "Sync": true, + "SyncType": "Server" + }, + "quirrelCityLeft": { + "Sync": true, + "SyncType": "Server" + }, + "quirrelMinesEncountered": { + "Sync": true, + "SyncType": "Server" + }, + "quirrelMinesLeft": { + "Sync": true, + "SyncType": "Server" + }, + "quirrelMantisEncountered": { + "Sync": true, + "SyncType": "Server" + }, + "enteredMantisLordArea": { + "Sync": true, + "SyncType": "Server" + }, + "visitedDeepnestSpa": { + "Sync": true, + "SyncType": "Server" + }, + "quirrelSpaReady": { + "Sync": true, + "SyncType": "Server" + }, + "quirrelSpaEncountered": { + "Sync": true, + "SyncType": "Server" + }, + "quirrelArchiveEncountered": { + "Sync": true, + "SyncType": "Server" + }, + "quirrelEpilogueCompleted": { + "Sync": true, + "SyncType": "Server" + }, + "metRelicDealer": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "metRelicDealerShop": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "marmOutside": { + "Sync": true, + "SyncType": "Server", + "IgnoreSceneHost": true + }, + "marmOutsideConvo": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "marmConvo1": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "marmConvo2": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "marmConvo3": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "marmConvoNailsmith": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "cornifer": { + "Sync": true, + "SyncType": "Server" + }, + "metCornifer": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "corniferIntroduced": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "corniferAtHome": { + "Sync": true, + "SyncType": "Server" + }, + "corn_crossroadsEncountered": { + "Sync": true, + "SyncType": "Server" + }, + "corn_crossroadsLeft": { + "Sync": true, + "SyncType": "Server" + }, + "corn_greenpathEncountered": { + "Sync": true, + "SyncType": "Server" + }, + "corn_greenpathLeft": { + "Sync": true, + "SyncType": "Server" + }, + "corn_fogCanyonEncountered": { + "Sync": true, + "SyncType": "Server" + }, + "corn_fogCanyonLeft": { + "Sync": true, + "SyncType": "Server" + }, + "corn_fungalWastesEncountered": { + "Sync": true, + "SyncType": "Server" + }, + "corn_fungalWastesLeft": { + "Sync": true, + "SyncType": "Server" + }, + "corn_cityEncountered": { + "Sync": true, + "SyncType": "Server" + }, + "corn_cityLeft": { + "Sync": true, + "SyncType": "Server" + }, + "corn_waterwaysEncountered": { + "Sync": true, + "SyncType": "Server" + }, + "corn_waterwaysLeft": { + "Sync": true, + "SyncType": "Server" + }, + "corn_minesEncountered": { + "Sync": true, + "SyncType": "Server" + }, + "corn_minesLeft": { + "Sync": true, + "SyncType": "Server" + }, + "corn_cliffsEncountered": { + "Sync": true, + "SyncType": "Server" + }, + "corn_cliffsLeft": { + "Sync": true, + "SyncType": "Server" + }, + "corn_deepnestEncountered": { + "Sync": true, + "SyncType": "Server" + }, + "corn_deepnestLeft": { + "Sync": true, + "SyncType": "Server" + }, + "corn_deepnestMet1": { + "Sync": true, + "SyncType": "Server" + }, + "corn_deepnestMet2": { + "Sync": true, + "SyncType": "Server" + }, + "corn_outskirtsEncountered": { + "Sync": true, + "SyncType": "Server" + }, + "corn_outskirtsLeft": { + "Sync": true, + "SyncType": "Server" + }, + "corn_royalGardensEncountered": { + "Sync": true, + "SyncType": "Server" + }, + "corn_royalGardensLeft": { + "Sync": true, + "SyncType": "Server" + }, + "corn_abyssEncountered": { + "Sync": true, + "SyncType": "Server" + }, + "corn_abyssLeft": { + "Sync": true, + "SyncType": "Server" + }, + "metIselda": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "iseldaCorniferHomeConvo": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "iseldaConvo1": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "brettaRescued": { + "Sync": true, + "SyncType": "Server" + }, + "brettaPosition": { + "Sync": true, + "SyncType": "Server" + }, + "brettaState": { + "Sync": true, + "SyncType": "Server" + }, + "brettaSeenBench": { + "Sync": true, + "SyncType": "Server" + }, + "brettaSeenBed": { + "Sync": true, + "SyncType": "Server" + }, + "brettaSeenBenchDiary": { + "Sync": true, + "SyncType": "Server" + }, + "brettaSeenBedDiary": { + "Sync": true, + "SyncType": "Server" + }, + "brettaLeftTown": { + "Sync": true, + "SyncType": "Server" + }, + "slyRescued": { + "Sync": true, + "SyncType": "Server" + }, + "slyBeta": { + "Sync": true, + "SyncType": "Server" + }, + "metSlyShop": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "gotSlyCharm": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "slyShellFrag1": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "slyShellFrag2": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "slyShellFrag3": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "slyShellFrag4": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "slyVesselFrag1": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "slyVesselFrag2": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "slyVesselFrag3": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "slyVesselFrag4": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "slyNotch1": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "slyNotch2": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "slySimpleKey": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "slyRancidEgg": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "slyConvoNailArt": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "slyConvoMapper": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "slyConvoNailHoned": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "jijiDoorUnlocked": { + "Sync": true, + "SyncType": "Server" + }, + "jijiMet": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "jijiShadeOffered": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "jijiShadeCharmConvo": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "metJinn": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "jinnConvo1": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "jinnConvo2": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "jinnConvo3": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "jinnConvoKingBrand": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "jinnConvoShadeCharm": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "jinnEggsSold": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "zote": { + "Sync": true, + "SyncType": "Server" + }, + "zoteRescuedBuzzer": { + "Sync": true, + "SyncType": "Server" + }, + "zoteDead": { + "Sync": true, + "SyncType": "Server" + }, + "zoteDeathPos": { + "Sync": true, + "SyncType": "Server" + }, + "zoteSpokenCity": { + "Sync": true, + "SyncType": "Server" + }, + "zoteLeftCity": { + "Sync": true, + "SyncType": "Server" + }, + "zoteTrappedDeepnest": { + "Sync": true, + "SyncType": "Server" + }, + "zoteRescuedDeepnest": { + "Sync": true, + "SyncType": "Server" + }, + "zoteDefeated": { + "Sync": true, + "SyncType": "Server" + }, + "zoteSpokenColosseum": { + "Sync": true, + "SyncType": "Server" + }, + "zotePrecept": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "zoteTownConvo": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "shaman": { + "Sync": true, + "SyncType": "Server" + }, + "shamanScreamConvo": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "shamanQuakeConvo": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "shamanFireball2Convo": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "shamanScream2Convo": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "shamanQuake2Convo": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "metMiner": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "miner": { + "Sync": true, + "SyncType": "Server" + }, + "minerEarly": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "hornetGreenpath": { + "Sync": true, + "SyncType": "Server" + }, + "hornetFung": { + "Sync": true, + "SyncType": "Server" + }, + "hornet_f19": { + "Sync": true, + "SyncType": "Server" + }, + "hornetFountainEncounter": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "hornetCityBridge_ready": { + "Sync": true, + "SyncType": "Server" + }, + "hornetCityBridge_completed": { + "Sync": true, + "SyncType": "Server" + }, + "hornetAbyssEncounter": { + "Sync": true, + "SyncType": "Server" + }, + "hornetDenEncounter": { + "Sync": true, + "SyncType": "Server" + }, + "metMoth": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "ignoredMoth": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "gladeDoorOpened": { + "Sync": true, + "SyncType": "Server" + }, + "mothDeparted": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "completedRGDreamPlant": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "dreamReward1": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "dreamReward2": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "dreamReward3": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "dreamReward4": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "dreamReward5": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "dreamReward5b": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "dreamReward6": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "dreamReward7": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "dreamReward8": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "dreamReward9": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "dreamMothConvo1": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "bankerAccountPurchased": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "metBanker": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "bankerBalance": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "bankerDeclined": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "bankerTheftCheck": { + "Sync": true, + "SyncType": "Server" + }, + "bankerTheft": { + "Sync": true, + "SyncType": "Server" + }, + "bankerSpaMet": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "metGiraffe": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "metCharmSlug": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "salubraNotch1": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "salubraNotch2": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "salubraNotch3": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "salubraNotch4": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "salubraBlessing": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "salubraConvoCombo": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "salubraConvoOvercharm": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "salubraConvoTruth": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "cultistTransformed": { + "Sync": true, + "SyncType": "Server" + }, + "metNailsmith": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "nailSmithUpgrades": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "honedNail": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "nailsmithCliff": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "nailsmithKilled": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "nailsmithSpared": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "nailsmithKillSpeech": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "nailsmithSheo": { + "Sync": true, + "SyncType": "Server" + }, + "nailsmithConvoArt": { + "Sync": true, + "SyncType": "Server" + }, + "metNailmasterMato": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "metNailmasterSheo": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "metNailmasterOro": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "matoConvoSheo": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "matoConvoOro": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "matoConvoSly": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "sheoConvoMato": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "sheoConvoOro": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "sheoConvoSly": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "sheoConvoNailsmith": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "oroConvoSheo": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "oroConvoMato": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "oroConvoSly": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "hunterRoared": { + "Sync": true, + "SyncType": "Server" + }, + "metHunter": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "hunterRewardOffered": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "huntersMarkOffered": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "hasHuntersMark": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "metLegEater": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "paidLegEater": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "refusedLegEater": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "legEaterConvo1": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "legEaterConvo2": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "legEaterConvo3": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "legEaterBrokenConvo": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "legEaterDungConvo": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "legEaterInfectedCrossroadConvo": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "legEaterBoughtConvo": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "legEaterGoldConvo": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "legEaterLeft": { + "Sync": true, + "SyncType": "Server" + }, + "tukMet": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "tukEggPrice": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "tukDungEgg": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "metEmilitia": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "emilitiaKingsBrandConvo": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "metCloth": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "clothEnteredTramRoom": { + "Sync": true, + "SyncType": "Server" + }, + "savedCloth": { + "Sync": true, + "SyncType": "Server" + }, + "clothEncounteredQueensGarden": { + "Sync": true, + "SyncType": "Server" + }, + "clothKilled": { + "Sync": true, + "SyncType": "Server" + }, + "clothInTown": { + "Sync": true, + "SyncType": "Server" + }, + "clothLeftTown": { + "Sync": true, + "SyncType": "Server" + }, + "clothGhostSpoken": { + "Sync": true, + "SyncType": "Server" + }, + "bigCatHitTail": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "bigCatHitTailConvo": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "bigCatMeet": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "bigCatTalk1": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "bigCatTalk2": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "bigCatTalk3": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "bigCatKingsBrandConvo": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "bigCatShadeConvo": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "tisoEncounteredTown": { + "Sync": true, + "SyncType": "Server" + }, + "tisoEncounteredBench": { + "Sync": true, + "SyncType": "Server" + }, + "tisoEncounteredLake": { + "Sync": true, + "SyncType": "Server" + }, + "tisoEncounteredColosseum": { + "Sync": true, + "SyncType": "Server" + }, + "tisoDead": { + "Sync": true, + "SyncType": "Server" + }, + "tisoShieldConvo": { + "Sync": true, + "SyncType": "Server" + }, + "mossCultist": { + "Sync": true, + "SyncType": "Server" + }, + "maskmakerMet": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "maskmakerConvo1": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "maskmakerConvo2": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "maskmakerUnmasked1": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "maskmakerUnmasked2": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "maskmakerShadowDash": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "maskmakerKingsBrand": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "dungDefenderConvo1": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "dungDefenderConvo2": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "dungDefenderConvo3": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "dungDefenderCharmConvo": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "dungDefenderIsmaConvo": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "dungDefenderAwoken": { + "Sync": true, + "SyncType": "Server" + }, + "dungDefenderLeft": { + "Sync": true, + "SyncType": "Server" + }, + "dungDefenderAwakeConvo": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "midwifeMet": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "midwifeConvo1": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "midwifeConvo2": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "metQueen": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "queenTalk1": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "queenTalk2": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "queenDung1": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "queenDung2": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "queenHornet": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "queenTalkExtra": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "gotQueenFragment": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "queenConvo_grimm1": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "queenConvo_grimm2": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "gotKingFragment": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "metXun": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "xunFailedConvo1": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "xunFailedConvo2": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "xunFlowerBroken": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "xunFlowerBrokeTimes": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "xunFlowerGiven": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "xunRewardGiven": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "menderState": { + "Sync": true, + "SyncType": "Server" + }, + "menderSignBroken": { + "Sync": true, + "SyncType": "Server" + }, + "allBelieverTabletsDestroyed": { + "Sync": true, + "SyncType": "Server" + }, + "mrMushroomState": { + "Sync": true, + "SyncType": "Server" + }, + "openedMapperShop": { + "Sync": true, + "SyncType": "Server" + }, + "openedSlyShop": { + "Sync": true, + "SyncType": "Server" + }, + "metStag": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "stagPosition": { + "Sync": true, + "SyncType": "Server" + }, + "stationsOpened": { + "Sync": true, + "SyncType": "Server" + }, + "stagConvoTram": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "stagConvoTiso": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "stagRemember1": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "stagRemember2": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "stagRemember3": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "stagEggInspected": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "stagHopeConvo": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "littleFoolMet": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "ranAway": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "seenColosseumTitle": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "colosseumBronzeOpened": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "colosseumBronzeCompleted": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "colosseumSilverOpened": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "colosseumSilverCompleted": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "colosseumGoldOpened": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "colosseumGoldCompleted": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "openedTown": { + "Sync": true, + "SyncType": "Server" + }, + "openedTownBuilding": { + "Sync": true, + "SyncType": "Server" + }, + "openedCrossroads": { + "Sync": true, + "SyncType": "Server" + }, + "openedGreenpath": { + "Sync": true, + "SyncType": "Server" + }, + "openedRuins1": { + "Sync": true, + "SyncType": "Server" + }, + "openedRuins2": { + "Sync": true, + "SyncType": "Server" + }, + "openedFungalWastes": { + "Sync": true, + "SyncType": "Server" + }, + "openedRoyalGardens": { + "Sync": true, + "SyncType": "Server" + }, + "openedRestingGrounds": { + "Sync": true, + "SyncType": "Server" + }, + "openedDeepnest": { + "Sync": true, + "SyncType": "Server" + }, + "openedStagNest": { + "Sync": true, + "SyncType": "Server" + }, + "openedHiddenStation": { + "Sync": true, + "SyncType": "Server" + }, + "charmSlots": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "charmsOwned": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "gotCharm_1": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "gotCharm_2": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "gotCharm_3": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "gotCharm_4": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "gotCharm_5": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "gotCharm_6": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "gotCharm_7": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "gotCharm_8": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "gotCharm_9": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "gotCharm_10": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "gotCharm_11": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "gotCharm_12": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "gotCharm_13": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "gotCharm_14": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "gotCharm_15": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "gotCharm_16": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "gotCharm_17": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "gotCharm_18": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "gotCharm_19": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "gotCharm_20": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "gotCharm_21": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "gotCharm_22": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "gotCharm_23": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "gotCharm_24": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "gotCharm_25": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "gotCharm_26": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "gotCharm_27": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "gotCharm_28": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "gotCharm_29": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "gotCharm_30": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "gotCharm_31": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "gotCharm_32": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "gotCharm_33": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "gotCharm_34": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "gotCharm_35": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "gotCharm_36": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "gotCharm_37": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "gotCharm_38": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "gotCharm_39": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "gotCharm_40": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "fragileHealth_unbreakable": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "fragileGreed_unbreakable": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "fragileStrength_unbreakable": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "royalCharmState": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "hasJournal": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "seenJournalMsg": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "seenHunterMsg": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "fillJournal": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "journalEntriesCompleted": { + "Sync": true, + "SyncType": "Server" + }, + "journalNotesCompleted": { + "Sync": true, + "SyncType": "Server" + }, + "journalEntriesTotal": { + "Sync": true, + "SyncType": "Server" + }, + "killedCrawler": { + "Sync": true, + "SyncType": "Server" + }, + "killsCrawler": { + "Sync": true, + "SyncType": "Server" + }, + "newDataCrawler": { + "Sync": true, + "SyncType": "Server" + }, + "killedBuzzer": { + "Sync": true, + "SyncType": "Server" + }, + "killsBuzzer": { + "Sync": true, + "SyncType": "Server" + }, + "newDataBuzzer": { + "Sync": true, + "SyncType": "Server" + }, + "killedBouncer": { + "Sync": true, + "SyncType": "Server" + }, + "killsBouncer": { + "Sync": true, + "SyncType": "Server" + }, + "newDataBouncer": { + "Sync": true, + "SyncType": "Server" + }, + "killedClimber": { + "Sync": true, + "SyncType": "Server" + }, + "killsClimber": { + "Sync": true, + "SyncType": "Server" + }, + "newDataClimber": { + "Sync": true, + "SyncType": "Server" + }, + "killedHopper": { + "Sync": true, + "SyncType": "Server" + }, + "killsHopper": { + "Sync": true, + "SyncType": "Server" + }, + "newDataHopper": { + "Sync": true, + "SyncType": "Server" + }, + "killedWorm": { + "Sync": true, + "SyncType": "Server" + }, + "killsWorm": { + "Sync": true, + "SyncType": "Server" + }, + "newDataWorm": { + "Sync": true, + "SyncType": "Server" + }, + "killedSpitter": { + "Sync": true, + "SyncType": "Server" + }, + "killsSpitter": { + "Sync": true, + "SyncType": "Server" + }, + "newDataSpitter": { + "Sync": true, + "SyncType": "Server" + }, + "killedHatcher": { + "Sync": true, + "SyncType": "Server" + }, + "killsHatcher": { + "Sync": true, + "SyncType": "Server" + }, + "newDataHatcher": { + "Sync": true, + "SyncType": "Server" + }, + "killedHatchling": { + "Sync": true, + "SyncType": "Server" + }, + "killsHatchling": { + "Sync": true, + "SyncType": "Server" + }, + "newDataHatchling": { + "Sync": true, + "SyncType": "Server" + }, + "killedZombieRunner": { + "Sync": true, + "SyncType": "Server" + }, + "killsZombieRunner": { + "Sync": true, + "SyncType": "Server" + }, + "newDataZombieRunner": { + "Sync": true, + "SyncType": "Server" + }, + "killedZombieHornhead": { + "Sync": true, + "SyncType": "Server" + }, + "killsZombieHornhead": { + "Sync": true, + "SyncType": "Server" + }, + "newDataZombieHornhead": { + "Sync": true, + "SyncType": "Server" + }, + "killedZombieLeaper": { + "Sync": true, + "SyncType": "Server" + }, + "killsZombieLeaper": { + "Sync": true, + "SyncType": "Server" + }, + "newDataZombieLeaper": { + "Sync": true, + "SyncType": "Server" + }, + "killedZombieBarger": { + "Sync": true, + "SyncType": "Server" + }, + "killsZombieBarger": { + "Sync": true, + "SyncType": "Server" + }, + "newDataZombieBarger": { + "Sync": true, + "SyncType": "Server" + }, + "killedZombieShield": { + "Sync": true, + "SyncType": "Server" + }, + "killsZombieShield": { + "Sync": true, + "SyncType": "Server" + }, + "newDataZombieShield": { + "Sync": true, + "SyncType": "Server" + }, + "killedZombieGuard": { + "Sync": true, + "SyncType": "Server" + }, + "killsZombieGuard": { + "Sync": true, + "SyncType": "Server" + }, + "newDataZombieGuard": { + "Sync": true, + "SyncType": "Server" + }, + "killedBigBuzzer": { + "Sync": true, + "SyncType": "Server" + }, + "killsBigBuzzer": { + "Sync": true, + "SyncType": "Server" + }, + "newDataBigBuzzer": { + "Sync": true, + "SyncType": "Server" + }, + "killedBigFly": { + "Sync": true, + "SyncType": "Server" + }, + "killsBigFly": { + "Sync": true, + "SyncType": "Server" + }, + "newDataBigFly": { + "Sync": true, + "SyncType": "Server" + }, + "killedMawlek": { + "Sync": true, + "SyncType": "Server" + }, + "killsMawlek": { + "Sync": true, + "SyncType": "Server" + }, + "newDataMawlek": { + "Sync": true, + "SyncType": "Server" + }, + "killedFalseKnight": { + "Sync": true, + "SyncType": "Server" + }, + "killsFalseKnight": { + "Sync": true, + "SyncType": "Server" + }, + "newDataFalseKnight": { + "Sync": true, + "SyncType": "Server" + }, + "killedRoller": { + "Sync": true, + "SyncType": "Server" + }, + "killsRoller": { + "Sync": true, + "SyncType": "Server" + }, + "newDataRoller": { + "Sync": true, + "SyncType": "Server" + }, + "killedBlocker": { + "Sync": true, + "SyncType": "Server" + }, + "killsBlocker": { + "Sync": true, + "SyncType": "Server" + }, + "newDataBlocker": { + "Sync": true, + "SyncType": "Server" + }, + "killedPrayerSlug": { + "Sync": true, + "SyncType": "Server" + }, + "killsPrayerSlug": { + "Sync": true, + "SyncType": "Server" + }, + "newDataPrayerSlug": { + "Sync": true, + "SyncType": "Server" + }, + "killedMenderBug": { + "Sync": true, + "SyncType": "Server" + }, + "killsMenderBug": { + "Sync": true, + "SyncType": "Server" + }, + "newDataMenderBug": { + "Sync": true, + "SyncType": "Server" + }, + "killedMossmanRunner": { + "Sync": true, + "SyncType": "Server" + }, + "killsMossmanRunner": { + "Sync": true, + "SyncType": "Server" + }, + "newDataMossmanRunner": { + "Sync": true, + "SyncType": "Server" + }, + "killedMossmanShaker": { + "Sync": true, + "SyncType": "Server" + }, + "killsMossmanShaker": { + "Sync": true, + "SyncType": "Server" + }, + "newDataMossmanShaker": { + "Sync": true, + "SyncType": "Server" + }, + "killedMosquito": { + "Sync": true, + "SyncType": "Server" + }, + "killsMosquito": { + "Sync": true, + "SyncType": "Server" + }, + "newDataMosquito": { + "Sync": true, + "SyncType": "Server" + }, + "killedBlobFlyer": { + "Sync": true, + "SyncType": "Server" + }, + "killsBlobFlyer": { + "Sync": true, + "SyncType": "Server" + }, + "newDataBlobFlyer": { + "Sync": true, + "SyncType": "Server" + }, + "killedFungifiedZombie": { + "Sync": true, + "SyncType": "Server" + }, + "killsFungifiedZombie": { + "Sync": true, + "SyncType": "Server" + }, + "newDataFungifiedZombie": { + "Sync": true, + "SyncType": "Server" + }, + "killedPlantShooter": { + "Sync": true, + "SyncType": "Server" + }, + "killsPlantShooter": { + "Sync": true, + "SyncType": "Server" + }, + "newDataPlantShooter": { + "Sync": true, + "SyncType": "Server" + }, + "killedMossCharger": { + "Sync": true, + "SyncType": "Server" + }, + "killsMossCharger": { + "Sync": true, + "SyncType": "Server" + }, + "newDataMossCharger": { + "Sync": true, + "SyncType": "Server" + }, + "killedMegaMossCharger": { + "Sync": true, + "SyncType": "Server" + }, + "killsMegaMossCharger": { + "Sync": true, + "SyncType": "Server" + }, + "newDataMegaMossCharger": { + "Sync": true, + "SyncType": "Server" + }, + "killedSnapperTrap": { + "Sync": true, + "SyncType": "Server" + }, + "killsSnapperTrap": { + "Sync": true, + "SyncType": "Server" + }, + "newDataSnapperTrap": { + "Sync": true, + "SyncType": "Server" + }, + "killedMossKnight": { + "Sync": true, + "SyncType": "Server" + }, + "killsMossKnight": { + "Sync": true, + "SyncType": "Server" + }, + "newDataMossKnight": { + "Sync": true, + "SyncType": "Server" + }, + "killedGrassHopper": { + "Sync": true, + "SyncType": "Server" + }, + "killsGrassHopper": { + "Sync": true, + "SyncType": "Server" + }, + "newDataGrassHopper": { + "Sync": true, + "SyncType": "Server" + }, + "killedAcidFlyer": { + "Sync": true, + "SyncType": "Server" + }, + "killsAcidFlyer": { + "Sync": true, + "SyncType": "Server" + }, + "newDataAcidFlyer": { + "Sync": true, + "SyncType": "Server" + }, + "killedAcidWalker": { + "Sync": true, + "SyncType": "Server" + }, + "killsAcidWalker": { + "Sync": true, + "SyncType": "Server" + }, + "newDataAcidWalker": { + "Sync": true, + "SyncType": "Server" + }, + "killedMossFlyer": { + "Sync": true, + "SyncType": "Server" + }, + "killsMossFlyer": { + "Sync": true, + "SyncType": "Server" + }, + "newDataMossFlyer": { + "Sync": true, + "SyncType": "Server" + }, + "killedMossKnightFat": { + "Sync": true, + "SyncType": "Server" + }, + "killsMossKnightFat": { + "Sync": true, + "SyncType": "Server" + }, + "newDataMossKnightFat": { + "Sync": true, + "SyncType": "Server" + }, + "killedMossWalker": { + "Sync": true, + "SyncType": "Server" + }, + "killsMossWalker": { + "Sync": true, + "SyncType": "Server" + }, + "newDataMossWalker": { + "Sync": true, + "SyncType": "Server" + }, + "killedInfectedKnight": { + "Sync": true, + "SyncType": "Server" + }, + "killsInfectedKnight": { + "Sync": true, + "SyncType": "Server" + }, + "newDataInfectedKnight": { + "Sync": true, + "SyncType": "Server" + }, + "killedLazyFlyer": { + "Sync": true, + "SyncType": "Server" + }, + "killsLazyFlyer": { + "Sync": true, + "SyncType": "Server" + }, + "newDataLazyFlyer": { + "Sync": true, + "SyncType": "Server" + }, + "killedZapBug": { + "Sync": true, + "SyncType": "Server" + }, + "killsZapBug": { + "Sync": true, + "SyncType": "Server" + }, + "newDataZapBug": { + "Sync": true, + "SyncType": "Server" + }, + "killedJellyfish": { + "Sync": true, + "SyncType": "Server" + }, + "killsJellyfish": { + "Sync": true, + "SyncType": "Server" + }, + "newDataJellyfish": { + "Sync": true, + "SyncType": "Server" + }, + "killedJellyCrawler": { + "Sync": true, + "SyncType": "Server" + }, + "killsJellyCrawler": { + "Sync": true, + "SyncType": "Server" + }, + "newDataJellyCrawler": { + "Sync": true, + "SyncType": "Server" + }, + "killedMegaJellyfish": { + "Sync": true, + "SyncType": "Server" + }, + "killsMegaJellyfish": { + "Sync": true, + "SyncType": "Server" + }, + "newDataMegaJellyfish": { + "Sync": true, + "SyncType": "Server" + }, + "killedFungoonBaby": { + "Sync": true, + "SyncType": "Server" + }, + "killsFungoonBaby": { + "Sync": true, + "SyncType": "Server" + }, + "newDataFungoonBaby": { + "Sync": true, + "SyncType": "Server" + }, + "killedMushroomTurret": { + "Sync": true, + "SyncType": "Server" + }, + "killsMushroomTurret": { + "Sync": true, + "SyncType": "Server" + }, + "newDataMushroomTurret": { + "Sync": true, + "SyncType": "Server" + }, + "killedMantis": { + "Sync": true, + "SyncType": "Server" + }, + "killsMantis": { + "Sync": true, + "SyncType": "Server" + }, + "newDataMantis": { + "Sync": true, + "SyncType": "Server" + }, + "killedMushroomRoller": { + "Sync": true, + "SyncType": "Server" + }, + "killsMushroomRoller": { + "Sync": true, + "SyncType": "Server" + }, + "newDataMushroomRoller": { + "Sync": true, + "SyncType": "Server" + }, + "killedMushroomBrawler": { + "Sync": true, + "SyncType": "Server" + }, + "killsMushroomBrawler": { + "Sync": true, + "SyncType": "Server" + }, + "newDataMushroomBrawler": { + "Sync": true, + "SyncType": "Server" + }, + "killedMushroomBaby": { + "Sync": true, + "SyncType": "Server" + }, + "killsMushroomBaby": { + "Sync": true, + "SyncType": "Server" + }, + "newDataMushroomBaby": { + "Sync": true, + "SyncType": "Server" + }, + "killedMantisFlyerChild": { + "Sync": true, + "SyncType": "Server" + }, + "killsMantisFlyerChild": { + "Sync": true, + "SyncType": "Server" + }, + "newDataMantisFlyerChild": { + "Sync": true, + "SyncType": "Server" + }, + "killedFungusFlyer": { + "Sync": true, + "SyncType": "Server" + }, + "killsFungusFlyer": { + "Sync": true, + "SyncType": "Server" + }, + "newDataFungusFlyer": { + "Sync": true, + "SyncType": "Server" + }, + "killedFungCrawler": { + "Sync": true, + "SyncType": "Server" + }, + "killsFungCrawler": { + "Sync": true, + "SyncType": "Server" + }, + "newDataFungCrawler": { + "Sync": true, + "SyncType": "Server" + }, + "killedMantisLord": { + "Sync": true, + "SyncType": "Server" + }, + "killsMantisLord": { + "Sync": true, + "SyncType": "Server" + }, + "newDataMantisLord": { + "Sync": true, + "SyncType": "Server" + }, + "killedBlackKnight": { + "Sync": true, + "SyncType": "Server" + }, + "killsBlackKnight": { + "Sync": true, + "SyncType": "Server" + }, + "newDataBlackKnight": { + "Sync": true, + "SyncType": "Server" + }, + "killedElectricMage": { + "Sync": true, + "SyncType": "Server" + }, + "killsElectricMage": { + "Sync": true, + "SyncType": "Server" + }, + "newDataElectricMage": { + "Sync": true, + "SyncType": "Server" + }, + "killedMage": { + "Sync": true, + "SyncType": "Server" + }, + "killsMage": { + "Sync": true, + "SyncType": "Server" + }, + "newDataMage": { + "Sync": true, + "SyncType": "Server" + }, + "killedMageKnight": { + "Sync": true, + "SyncType": "Server" + }, + "killsMageKnight": { + "Sync": true, + "SyncType": "Server" + }, + "newDataMageKnight": { + "Sync": true, + "SyncType": "Server" + }, + "killedRoyalDandy": { + "Sync": true, + "SyncType": "Server" + }, + "killsRoyalDandy": { + "Sync": true, + "SyncType": "Server" + }, + "newDataRoyalDandy": { + "Sync": true, + "SyncType": "Server" + }, + "killedRoyalCoward": { + "Sync": true, + "SyncType": "Server" + }, + "killsRoyalCoward": { + "Sync": true, + "SyncType": "Server" + }, + "newDataRoyalCoward": { + "Sync": true, + "SyncType": "Server" + }, + "killedRoyalPlumper": { + "Sync": true, + "SyncType": "Server" + }, + "killsRoyalPlumper": { + "Sync": true, + "SyncType": "Server" + }, + "newDataRoyalPlumper": { + "Sync": true, + "SyncType": "Server" + }, + "killedFlyingSentrySword": { + "Sync": true, + "SyncType": "Server" + }, + "killsFlyingSentrySword": { + "Sync": true, + "SyncType": "Server" + }, + "newDataFlyingSentrySword": { + "Sync": true, + "SyncType": "Server" + }, + "killedFlyingSentryJavelin": { + "Sync": true, + "SyncType": "Server" + }, + "killsFlyingSentryJavelin": { + "Sync": true, + "SyncType": "Server" + }, + "newDataFlyingSentryJavelin": { + "Sync": true, + "SyncType": "Server" + }, + "killedSentry": { + "Sync": true, + "SyncType": "Server" + }, + "killsSentry": { + "Sync": true, + "SyncType": "Server" + }, + "newDataSentry": { + "Sync": true, + "SyncType": "Server" + }, + "killedSentryFat": { + "Sync": true, + "SyncType": "Server" + }, + "killsSentryFat": { + "Sync": true, + "SyncType": "Server" + }, + "newDataSentryFat": { + "Sync": true, + "SyncType": "Server" + }, + "killedMageBlob": { + "Sync": true, + "SyncType": "Server" + }, + "killsMageBlob": { + "Sync": true, + "SyncType": "Server" + }, + "newDataMageBlob": { + "Sync": true, + "SyncType": "Server" + }, + "killedGreatShieldZombie": { + "Sync": true, + "SyncType": "Server" + }, + "killsGreatShieldZombie": { + "Sync": true, + "SyncType": "Server" + }, + "newDataGreatShieldZombie": { + "Sync": true, + "SyncType": "Server" + }, + "killedJarCollector": { + "Sync": true, + "SyncType": "Server" + }, + "killsJarCollector": { + "Sync": true, + "SyncType": "Server" + }, + "newDataJarCollector": { + "Sync": true, + "SyncType": "Server" + }, + "killedMageBalloon": { + "Sync": true, + "SyncType": "Server" + }, + "killsMageBalloon": { + "Sync": true, + "SyncType": "Server" + }, + "newDataMageBalloon": { + "Sync": true, + "SyncType": "Server" + }, + "killedMageLord": { + "Sync": true, + "SyncType": "Server" + }, + "killsMageLord": { + "Sync": true, + "SyncType": "Server" + }, + "newDataMageLord": { + "Sync": true, + "SyncType": "Server" + }, + "killedGorgeousHusk": { + "Sync": true, + "SyncType": "Server" + }, + "killsGorgeousHusk": { + "Sync": true, + "SyncType": "Server" + }, + "newDataGorgeousHusk": { + "Sync": true, + "SyncType": "Server" + }, + "killedFlipHopper": { + "Sync": true, + "SyncType": "Server" + }, + "killsFlipHopper": { + "Sync": true, + "SyncType": "Server" + }, + "newDataFlipHopper": { + "Sync": true, + "SyncType": "Server" + }, + "killedFlukeman": { + "Sync": true, + "SyncType": "Server" + }, + "killsFlukeman": { + "Sync": true, + "SyncType": "Server" + }, + "newDataFlukeman": { + "Sync": true, + "SyncType": "Server" + }, + "killedInflater": { + "Sync": true, + "SyncType": "Server" + }, + "killsInflater": { + "Sync": true, + "SyncType": "Server" + }, + "newDataInflater": { + "Sync": true, + "SyncType": "Server" + }, + "killedFlukefly": { + "Sync": true, + "SyncType": "Server" + }, + "killsFlukefly": { + "Sync": true, + "SyncType": "Server" + }, + "newDataFlukefly": { + "Sync": true, + "SyncType": "Server" + }, + "killedFlukeMother": { + "Sync": true, + "SyncType": "Server" + }, + "killsFlukeMother": { + "Sync": true, + "SyncType": "Server" + }, + "newDataFlukeMother": { + "Sync": true, + "SyncType": "Server" + }, + "killedDungDefender": { + "Sync": true, + "SyncType": "Server" + }, + "killsDungDefender": { + "Sync": true, + "SyncType": "Server" + }, + "newDataDungDefender": { + "Sync": true, + "SyncType": "Server" + }, + "killedCrystalCrawler": { + "Sync": true, + "SyncType": "Server" + }, + "killsCrystalCrawler": { + "Sync": true, + "SyncType": "Server" + }, + "newDataCrystalCrawler": { + "Sync": true, + "SyncType": "Server" + }, + "killedCrystalFlyer": { + "Sync": true, + "SyncType": "Server" + }, + "killsCrystalFlyer": { + "Sync": true, + "SyncType": "Server" + }, + "newDataCrystalFlyer": { + "Sync": true, + "SyncType": "Server" + }, + "killedLaserBug": { + "Sync": true, + "SyncType": "Server" + }, + "killsLaserBug": { + "Sync": true, + "SyncType": "Server" + }, + "newDataLaserBug": { + "Sync": true, + "SyncType": "Server" + }, + "killedBeamMiner": { + "Sync": true, + "SyncType": "Server" + }, + "killsBeamMiner": { + "Sync": true, + "SyncType": "Server" + }, + "newDataBeamMiner": { + "Sync": true, + "SyncType": "Server" + }, + "killedZombieMiner": { + "Sync": true, + "SyncType": "Server" + }, + "killsZombieMiner": { + "Sync": true, + "SyncType": "Server" + }, + "newDataZombieMiner": { + "Sync": true, + "SyncType": "Server" + }, + "killedMegaBeamMiner": { + "Sync": true, + "SyncType": "Server" + }, + "killsMegaBeamMiner": { + "Sync": true, + "SyncType": "Server" + }, + "newDataMegaBeamMiner": { + "Sync": true, + "SyncType": "Server" + }, + "killedMinesCrawler": { + "Sync": true, + "SyncType": "Server" + }, + "killsMinesCrawler": { + "Sync": true, + "SyncType": "Server" + }, + "newDataMinesCrawler": { + "Sync": true, + "SyncType": "Server" + }, + "killedAngryBuzzer": { + "Sync": true, + "SyncType": "Server" + }, + "killsAngryBuzzer": { + "Sync": true, + "SyncType": "Server" + }, + "newDataAngryBuzzer": { + "Sync": true, + "SyncType": "Server" + }, + "killedBurstingBouncer": { + "Sync": true, + "SyncType": "Server" + }, + "killsBurstingBouncer": { + "Sync": true, + "SyncType": "Server" + }, + "newDataBurstingBouncer": { + "Sync": true, + "SyncType": "Server" + }, + "killedBurstingZombie": { + "Sync": true, + "SyncType": "Server" + }, + "killsBurstingZombie": { + "Sync": true, + "SyncType": "Server" + }, + "newDataBurstingZombie": { + "Sync": true, + "SyncType": "Server" + }, + "killedSpittingZombie": { + "Sync": true, + "SyncType": "Server" + }, + "killsSpittingZombie": { + "Sync": true, + "SyncType": "Server" + }, + "newDataSpittingZombie": { + "Sync": true, + "SyncType": "Server" + }, + "killedBabyCentipede": { + "Sync": true, + "SyncType": "Server" + }, + "killsBabyCentipede": { + "Sync": true, + "SyncType": "Server" + }, + "newDataBabyCentipede": { + "Sync": true, + "SyncType": "Server" + }, + "killedBigCentipede": { + "Sync": true, + "SyncType": "Server" + }, + "killsBigCentipede": { + "Sync": true, + "SyncType": "Server" + }, + "newDataBigCentipede": { + "Sync": true, + "SyncType": "Server" + }, + "killedCentipedeHatcher": { + "Sync": true, + "SyncType": "Server" + }, + "killsCentipedeHatcher": { + "Sync": true, + "SyncType": "Server" + }, + "newDataCentipedeHatcher": { + "Sync": true, + "SyncType": "Server" + }, + "killedLesserMawlek": { + "Sync": true, + "SyncType": "Server" + }, + "killsLesserMawlek": { + "Sync": true, + "SyncType": "Server" + }, + "newDataLesserMawlek": { + "Sync": true, + "SyncType": "Server" + }, + "killedSlashSpider": { + "Sync": true, + "SyncType": "Server" + }, + "killsSlashSpider": { + "Sync": true, + "SyncType": "Server" + }, + "newDataSlashSpider": { + "Sync": true, + "SyncType": "Server" + }, + "killedSpiderCorpse": { + "Sync": true, + "SyncType": "Server" + }, + "killsSpiderCorpse": { + "Sync": true, + "SyncType": "Server" + }, + "newDataSpiderCorpse": { + "Sync": true, + "SyncType": "Server" + }, + "killedShootSpider": { + "Sync": true, + "SyncType": "Server" + }, + "killsShootSpider": { + "Sync": true, + "SyncType": "Server" + }, + "newDataShootSpider": { + "Sync": true, + "SyncType": "Server" + }, + "killedMiniSpider": { + "Sync": true, + "SyncType": "Server" + }, + "killsMiniSpider": { + "Sync": true, + "SyncType": "Server" + }, + "newDataMiniSpider": { + "Sync": true, + "SyncType": "Server" + }, + "killedSpiderFlyer": { + "Sync": true, + "SyncType": "Server" + }, + "killsSpiderFlyer": { + "Sync": true, + "SyncType": "Server" + }, + "newDataSpiderFlyer": { + "Sync": true, + "SyncType": "Server" + }, + "killedMimicSpider": { + "Sync": true, + "SyncType": "Server" + }, + "killsMimicSpider": { + "Sync": true, + "SyncType": "Server" + }, + "newDataMimicSpider": { + "Sync": true, + "SyncType": "Server" + }, + "killedBeeHatchling": { + "Sync": true, + "SyncType": "Server" + }, + "killsBeeHatchling": { + "Sync": true, + "SyncType": "Server" + }, + "newDataBeeHatchling": { + "Sync": true, + "SyncType": "Server" + }, + "killedBeeStinger": { + "Sync": true, + "SyncType": "Server" + }, + "killsBeeStinger": { + "Sync": true, + "SyncType": "Server" + }, + "newDataBeeStinger": { + "Sync": true, + "SyncType": "Server" + }, + "killedBigBee": { + "Sync": true, + "SyncType": "Server" + }, + "killsBigBee": { + "Sync": true, + "SyncType": "Server" + }, + "newDataBigBee": { + "Sync": true, + "SyncType": "Server" + }, + "killedHiveKnight": { + "Sync": true, + "SyncType": "Server" + }, + "killsHiveKnight": { + "Sync": true, + "SyncType": "Server" + }, + "newDataHiveKnight": { + "Sync": true, + "SyncType": "Server" + }, + "killedBlowFly": { + "Sync": true, + "SyncType": "Server" + }, + "killsBlowFly": { + "Sync": true, + "SyncType": "Server" + }, + "newDataBlowFly": { + "Sync": true, + "SyncType": "Server" + }, + "killedCeilingDropper": { + "Sync": true, + "SyncType": "Server" + }, + "killsCeilingDropper": { + "Sync": true, + "SyncType": "Server" + }, + "newDataCeilingDropper": { + "Sync": true, + "SyncType": "Server" + }, + "killedGiantHopper": { + "Sync": true, + "SyncType": "Server" + }, + "killsGiantHopper": { + "Sync": true, + "SyncType": "Server" + }, + "newDataGiantHopper": { + "Sync": true, + "SyncType": "Server" + }, + "killedGrubMimic": { + "Sync": true, + "SyncType": "Server" + }, + "killsGrubMimic": { + "Sync": true, + "SyncType": "Server" + }, + "newDataGrubMimic": { + "Sync": true, + "SyncType": "Server" + }, + "killedMawlekTurret": { + "Sync": true, + "SyncType": "Server" + }, + "killsMawlekTurret": { + "Sync": true, + "SyncType": "Server" + }, + "newDataMawlekTurret": { + "Sync": true, + "SyncType": "Server" + }, + "killedOrangeScuttler": { + "Sync": true, + "SyncType": "Server" + }, + "killsOrangeScuttler": { + "Sync": true, + "SyncType": "Server" + }, + "newDataOrangeScuttler": { + "Sync": true, + "SyncType": "Server" + }, + "killedHealthScuttler": { + "Sync": true, + "SyncType": "Server" + }, + "killsHealthScuttler": { + "Sync": true, + "SyncType": "Server" + }, + "newDataHealthScuttler": { + "Sync": true, + "SyncType": "Server" + }, + "killedPigeon": { + "Sync": true, + "SyncType": "Server" + }, + "killsPigeon": { + "Sync": true, + "SyncType": "Server" + }, + "newDataPigeon": { + "Sync": true, + "SyncType": "Server" + }, + "killedZombieHive": { + "Sync": true, + "SyncType": "Server" + }, + "killsZombieHive": { + "Sync": true, + "SyncType": "Server" + }, + "newDataZombieHive": { + "Sync": true, + "SyncType": "Server" + }, + "killedDreamGuard": { + "Sync": true, + "SyncType": "Server" + }, + "killsDreamGuard": { + "Sync": true, + "SyncType": "Server" + }, + "newDataDreamGuard": { + "Sync": true, + "SyncType": "Server" + }, + "killedHornet": { + "Sync": true, + "SyncType": "Server" + }, + "killsHornet": { + "Sync": true, + "SyncType": "Server" + }, + "newDataHornet": { + "Sync": true, + "SyncType": "Server" + }, + "killedAbyssCrawler": { + "Sync": true, + "SyncType": "Server" + }, + "killsAbyssCrawler": { + "Sync": true, + "SyncType": "Server" + }, + "newDataAbyssCrawler": { + "Sync": true, + "SyncType": "Server" + }, + "killedSuperSpitter": { + "Sync": true, + "SyncType": "Server" + }, + "killsSuperSpitter": { + "Sync": true, + "SyncType": "Server" + }, + "newDataSuperSpitter": { + "Sync": true, + "SyncType": "Server" + }, + "killedSibling": { + "Sync": true, + "SyncType": "Server" + }, + "killsSibling": { + "Sync": true, + "SyncType": "Server" + }, + "newDataSibling": { + "Sync": true, + "SyncType": "Server" + }, + "killedPalaceFly": { + "Sync": true, + "SyncType": "Server" + }, + "killsPalaceFly": { + "Sync": true, + "SyncType": "Server" + }, + "newDataPalaceFly": { + "Sync": true, + "SyncType": "Server" + }, + "killedEggSac": { + "Sync": true, + "SyncType": "Server" + }, + "killsEggSac": { + "Sync": true, + "SyncType": "Server" + }, + "newDataEggSac": { + "Sync": true, + "SyncType": "Server" + }, + "killedMummy": { + "Sync": true, + "SyncType": "Server" + }, + "killsMummy": { + "Sync": true, + "SyncType": "Server" + }, + "newDataMummy": { + "Sync": true, + "SyncType": "Server" + }, + "killedOrangeBalloon": { + "Sync": true, + "SyncType": "Server" + }, + "killsOrangeBalloon": { + "Sync": true, + "SyncType": "Server" + }, + "newDataOrangeBalloon": { + "Sync": true, + "SyncType": "Server" + }, + "killedAbyssTendril": { + "Sync": true, + "SyncType": "Server" + }, + "killsAbyssTendril": { + "Sync": true, + "SyncType": "Server" + }, + "newDataAbyssTendril": { + "Sync": true, + "SyncType": "Server" + }, + "killedHeavyMantis": { + "Sync": true, + "SyncType": "Server" + }, + "killsHeavyMantis": { + "Sync": true, + "SyncType": "Server" + }, + "newDataHeavyMantis": { + "Sync": true, + "SyncType": "Server" + }, + "killedTraitorLord": { + "Sync": true, + "SyncType": "Server" + }, + "killsTraitorLord": { + "Sync": true, + "SyncType": "Server" + }, + "newDataTraitorLord": { + "Sync": true, + "SyncType": "Server" + }, + "killedMantisHeavyFlyer": { + "Sync": true, + "SyncType": "Server" + }, + "killsMantisHeavyFlyer": { + "Sync": true, + "SyncType": "Server" + }, + "newDataMantisHeavyFlyer": { + "Sync": true, + "SyncType": "Server" + }, + "killedGardenZombie": { + "Sync": true, + "SyncType": "Server" + }, + "killsGardenZombie": { + "Sync": true, + "SyncType": "Server" + }, + "newDataGardenZombie": { + "Sync": true, + "SyncType": "Server" + }, + "killedRoyalGuard": { + "Sync": true, + "SyncType": "Server" + }, + "killsRoyalGuard": { + "Sync": true, + "SyncType": "Server" + }, + "newDataRoyalGuard": { + "Sync": true, + "SyncType": "Server" + }, + "killedWhiteRoyal": { + "Sync": true, + "SyncType": "Server" + }, + "killsWhiteRoyal": { + "Sync": true, + "SyncType": "Server" + }, + "newDataWhiteRoyal": { + "Sync": true, + "SyncType": "Server" + }, + "openedPalaceGrounds": { + "Sync": true, + "SyncType": "Server" + }, + "killedOblobble": { + "Sync": true, + "SyncType": "Server" + }, + "killsOblobble": { + "Sync": true, + "SyncType": "Server" + }, + "newDataOblobble": { + "Sync": true, + "SyncType": "Server" + }, + "killedZote": { + "Sync": true, + "SyncType": "Server" + }, + "killsZote": { + "Sync": true, + "SyncType": "Server" + }, + "newDataZote": { + "Sync": true, + "SyncType": "Server" + }, + "killedBlobble": { + "Sync": true, + "SyncType": "Server" + }, + "killsBlobble": { + "Sync": true, + "SyncType": "Server" + }, + "newDataBlobble": { + "Sync": true, + "SyncType": "Server" + }, + "killedColMosquito": { + "Sync": true, + "SyncType": "Server" + }, + "killsColMosquito": { + "Sync": true, + "SyncType": "Server" + }, + "newDataColMosquito": { + "Sync": true, + "SyncType": "Server" + }, + "killedColRoller": { + "Sync": true, + "SyncType": "Server" + }, + "killsColRoller": { + "Sync": true, + "SyncType": "Server" + }, + "newDataColRoller": { + "Sync": true, + "SyncType": "Server" + }, + "killedColFlyingSentry": { + "Sync": true, + "SyncType": "Server" + }, + "killsColFlyingSentry": { + "Sync": true, + "SyncType": "Server" + }, + "newDataColFlyingSentry": { + "Sync": true, + "SyncType": "Server" + }, + "killedColMiner": { + "Sync": true, + "SyncType": "Server" + }, + "killsColMiner": { + "Sync": true, + "SyncType": "Server" + }, + "newDataColMiner": { + "Sync": true, + "SyncType": "Server" + }, + "killedColShield": { + "Sync": true, + "SyncType": "Server" + }, + "killsColShield": { + "Sync": true, + "SyncType": "Server" + }, + "newDataColShield": { + "Sync": true, + "SyncType": "Server" + }, + "killedColWorm": { + "Sync": true, + "SyncType": "Server" + }, + "killsColWorm": { + "Sync": true, + "SyncType": "Server" + }, + "newDataColWorm": { + "Sync": true, + "SyncType": "Server" + }, + "killedColHopper": { + "Sync": true, + "SyncType": "Server" + }, + "killsColHopper": { + "Sync": true, + "SyncType": "Server" + }, + "newDataColHopper": { + "Sync": true, + "SyncType": "Server" + }, + "killedLobsterLancer": { + "Sync": true, + "SyncType": "Server" + }, + "killsLobsterLancer": { + "Sync": true, + "SyncType": "Server" + }, + "newDataLobsterLancer": { + "Sync": true, + "SyncType": "Server" + }, + "killedGhostAladar": { + "Sync": true, + "SyncType": "Server" + }, + "killsGhostAladar": { + "Sync": true, + "SyncType": "Server" + }, + "newDataGhostAladar": { + "Sync": true, + "SyncType": "Server" + }, + "killedGhostXero": { + "Sync": true, + "SyncType": "Server" + }, + "killsGhostXero": { + "Sync": true, + "SyncType": "Server" + }, + "newDataGhostXero": { + "Sync": true, + "SyncType": "Server" + }, + "killedGhostHu": { + "Sync": true, + "SyncType": "Server" + }, + "killsGhostHu": { + "Sync": true, + "SyncType": "Server" + }, + "newDataGhostHu": { + "Sync": true, + "SyncType": "Server" + }, + "killedGhostMarmu": { + "Sync": true, + "SyncType": "Server" + }, + "killsGhostMarmu": { + "Sync": true, + "SyncType": "Server" + }, + "newDataGhostMarmu": { + "Sync": true, + "SyncType": "Server" + }, + "killedGhostNoEyes": { + "Sync": true, + "SyncType": "Server" + }, + "killsGhostNoEyes": { + "Sync": true, + "SyncType": "Server" + }, + "newDataGhostNoEyes": { + "Sync": true, + "SyncType": "Server" + }, + "killedGhostMarkoth": { + "Sync": true, + "SyncType": "Server" + }, + "killsGhostMarkoth": { + "Sync": true, + "SyncType": "Server" + }, + "newDataGhostMarkoth": { + "Sync": true, + "SyncType": "Server" + }, + "killedGhostGalien": { + "Sync": true, + "SyncType": "Server" + }, + "killsGhostGalien": { + "Sync": true, + "SyncType": "Server" + }, + "newDataGhostGalien": { + "Sync": true, + "SyncType": "Server" + }, + "killedWhiteDefender": { + "Sync": true, + "SyncType": "Server" + }, + "killsWhiteDefender": { + "Sync": true, + "SyncType": "Server" + }, + "newDataWhiteDefender": { + "Sync": true, + "SyncType": "Server" + }, + "killedGreyPrince": { + "Sync": true, + "SyncType": "Server" + }, + "killsGreyPrince": { + "Sync": true, + "SyncType": "Server" + }, + "newDataGreyPrince": { + "Sync": true, + "SyncType": "Server" + }, + "killedZotelingBalloon": { + "Sync": true, + "SyncType": "Server" + }, + "killsZotelingBalloon": { + "Sync": true, + "SyncType": "Server" + }, + "newDataZotelingBalloon": { + "Sync": true, + "SyncType": "Server" + }, + "killedZotelingHopper": { + "Sync": true, + "SyncType": "Server" + }, + "killsZotelingHopper": { + "Sync": true, + "SyncType": "Server" + }, + "newDataZotelingHopper": { + "Sync": true, + "SyncType": "Server" + }, + "killedZotelingBuzzer": { + "Sync": true, + "SyncType": "Server" + }, + "killsZotelingBuzzer": { + "Sync": true, + "SyncType": "Server" + }, + "newDataZotelingBuzzer": { + "Sync": true, + "SyncType": "Server" + }, + "killedHollowKnight": { + "Sync": true, + "SyncType": "Server" + }, + "killsHollowKnight": { + "Sync": true, + "SyncType": "Server" + }, + "newDataHollowKnight": { + "Sync": true, + "SyncType": "Server" + }, + "killedFinalBoss": { + "Sync": true, + "SyncType": "Server" + }, + "killsFinalBoss": { + "Sync": true, + "SyncType": "Server" + }, + "newDataFinalBoss": { + "Sync": true, + "SyncType": "Server" + }, + "killedHunterMark": { + "Sync": true, + "SyncType": "Server" + }, + "killsHunterMark": { + "Sync": true, + "SyncType": "Server" + }, + "newDataHunterMark": { + "Sync": true, + "SyncType": "Server" + }, + "killedFlameBearerSmall": { + "Sync": true, + "SyncType": "Server" + }, + "killsFlameBearerSmall": { + "Sync": true, + "SyncType": "Server" + }, + "newDataFlameBearerSmall": { + "Sync": true, + "SyncType": "Server" + }, + "killedFlameBearerMed": { + "Sync": true, + "SyncType": "Server" + }, + "killsFlameBearerMed": { + "Sync": true, + "SyncType": "Server" + }, + "newDataFlameBearerMed": { + "Sync": true, + "SyncType": "Server" + }, + "killedFlameBearerLarge": { + "Sync": true, + "SyncType": "Server" + }, + "killsFlameBearerLarge": { + "Sync": true, + "SyncType": "Server" + }, + "newDataFlameBearerLarge": { + "Sync": true, + "SyncType": "Server" + }, + "killedGrimm": { + "Sync": true, + "SyncType": "Server" + }, + "killsGrimm": { + "Sync": true, + "SyncType": "Server" + }, + "newDataGrimm": { + "Sync": true, + "SyncType": "Server" + }, + "killedNightmareGrimm": { + "Sync": true, + "SyncType": "Server" + }, + "killsNightmareGrimm": { + "Sync": true, + "SyncType": "Server" + }, + "newDataNightmareGrimm": { + "Sync": true, + "SyncType": "Server" + }, + "killedBindingSeal": { + "Sync": true, + "SyncType": "Server" + }, + "killsBindingSeal": { + "Sync": true, + "SyncType": "Server" + }, + "newDataBindingSeal": { + "Sync": true, + "SyncType": "Server" + }, + "killedFatFluke": { + "Sync": true, + "SyncType": "Server" + }, + "killsFatFluke": { + "Sync": true, + "SyncType": "Server" + }, + "newDataFatFluke": { + "Sync": true, + "SyncType": "Server" + }, + "killedPaleLurker": { + "Sync": true, + "SyncType": "Server" + }, + "killsPaleLurker": { + "Sync": true, + "SyncType": "Server" + }, + "newDataPaleLurker": { + "Sync": true, + "SyncType": "Server" + }, + "killedNailBros": { + "Sync": true, + "SyncType": "Server" + }, + "killsNailBros": { + "Sync": true, + "SyncType": "Server" + }, + "newDataNailBros": { + "Sync": true, + "SyncType": "Server" + }, + "killedPaintmaster": { + "Sync": true, + "SyncType": "Server" + }, + "killsPaintmaster": { + "Sync": true, + "SyncType": "Server" + }, + "newDataPaintmaster": { + "Sync": true, + "SyncType": "Server" + }, + "killedNailsage": { + "Sync": true, + "SyncType": "Server" + }, + "killsNailsage": { + "Sync": true, + "SyncType": "Server" + }, + "newDataNailsage": { + "Sync": true, + "SyncType": "Server" + }, + "killedHollowKnightPrime": { + "Sync": true, + "SyncType": "Server" + }, + "killsHollowKnightPrime": { + "Sync": true, + "SyncType": "Server" + }, + "newDataHollowKnightPrime": { + "Sync": true, + "SyncType": "Server" + }, + "killedGodseekerMask": { + "Sync": true, + "SyncType": "Server" + }, + "killsGodseekerMask": { + "Sync": true, + "SyncType": "Server" + }, + "newDataGodseekerMask": { + "Sync": true, + "SyncType": "Server" + }, + "killedVoidIdol_1": { + "Sync": true, + "SyncType": "Server" + }, + "killsVoidIdol_1": { + "Sync": true, + "SyncType": "Server" + }, + "newDataVoidIdol_1": { + "Sync": true, + "SyncType": "Server" + }, + "killedVoidIdol_2": { + "Sync": true, + "SyncType": "Server" + }, + "killsVoidIdol_2": { + "Sync": true, + "SyncType": "Server" + }, + "newDataVoidIdol_2": { + "Sync": true, + "SyncType": "Server" + }, + "killedVoidIdol_3": { + "Sync": true, + "SyncType": "Server" + }, + "killsVoidIdol_3": { + "Sync": true, + "SyncType": "Server" + }, + "newDataVoidIdol_3": { + "Sync": true, + "SyncType": "Server" + }, + "grubsCollected": { + "Sync": true, + "SyncType": "Server" + }, + "grubRewards": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "finalGrubRewardCollected": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "fatGrubKing": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "falseKnightDefeated": { + "Sync": true, + "SyncType": "Server" + }, + "falseKnightDreamDefeated": { + "Sync": true, + "SyncType": "Server" + }, + "falseKnightOrbsCollected": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "mawlekDefeated": { + "Sync": true, + "SyncType": "Server" + }, + "giantBuzzerDefeated": { + "Sync": true, + "SyncType": "Server" + }, + "giantFlyDefeated": { + "Sync": true, + "SyncType": "Server" + }, + "blocker1Defeated": { + "Sync": true, + "SyncType": "Server" + }, + "blocker2Defeated": { + "Sync": true, + "SyncType": "Server" + }, + "hornet1Defeated": { + "Sync": true, + "SyncType": "Server" + }, + "collectorDefeated": { + "Sync": true, + "SyncType": "Server" + }, + "hornetOutskirtsDefeated": { + "Sync": true, + "SyncType": "Server" + }, + "mageLordDreamDefeated": { + "Sync": true, + "SyncType": "Server" + }, + "mageLordOrbsCollected": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "infectedKnightDreamDefeated": { + "Sync": true, + "SyncType": "Server" + }, + "infectedKnightOrbsCollected": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "whiteDefenderDefeated": { + "Sync": true, + "SyncType": "Server" + }, + "whiteDefenderOrbsCollected": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "whiteDefenderDefeats": { + "Sync": true, + "SyncType": "Server" + }, + "greyPrinceDefeats": { + "Sync": true, + "SyncType": "Server" + }, + "greyPrinceDefeated": { + "Sync": true, + "SyncType": "Server" + }, + "greyPrinceOrbsCollected": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "aladarSlugDefeated": { + "Sync": true, + "SyncType": "Server" + }, + "xeroDefeated": { + "Sync": true, + "SyncType": "Server" + }, + "elderHuDefeated": { + "Sync": true, + "SyncType": "Server" + }, + "mumCaterpillarDefeated": { + "Sync": true, + "SyncType": "Server" + }, + "noEyesDefeated": { + "Sync": true, + "SyncType": "Server" + }, + "markothDefeated": { + "Sync": true, + "SyncType": "Server" + }, + "galienDefeated": { + "Sync": true, + "SyncType": "Server" + }, + "XERO_encountered": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "ALADAR_encountered": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "HU_encountered": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "MUMCAT_encountered": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "NOEYES_encountered": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "MARKOTH_encountered": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "GALIEN_encountered": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "xeroPinned": { + "Sync": true, + "SyncType": "Server" + }, + "aladarPinned": { + "Sync": true, + "SyncType": "Server" + }, + "huPinned": { + "Sync": true, + "SyncType": "Server" + }, + "mumCaterpillarPinned": { + "Sync": true, + "SyncType": "Server" + }, + "noEyesPinned": { + "Sync": true, + "SyncType": "Server" + }, + "markothPinned": { + "Sync": true, + "SyncType": "Server" + }, + "galienPinned": { + "Sync": true, + "SyncType": "Server" + }, + "scenesVisited": { + "Sync": true, + "SyncType": "Server" + }, + "scenesMapped": { + "Sync": true, + "SyncType": "Server" + }, + "scenesEncounteredBench": { + "Sync": true, + "SyncType": "Server" + }, + "scenesGrubRescued": { + "Sync": true, + "SyncType": "Server" + }, + "scenesFlameCollected": { + "Sync": true, + "SyncType": "Server" + }, + "scenesEncounteredCocoon": { + "Sync": true, + "SyncType": "Server" + }, + "scenesEncounteredDreamPlant": { + "Sync": true, + "SyncType": "Server" + }, + "scenesEncounteredDreamPlantC": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "hasMap": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "mapDirtmouth": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "mapCrossroads": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "mapGreenpath": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "mapFogCanyon": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "mapRoyalGardens": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "mapFungalWastes": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "mapCity": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "mapWaterways": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "mapMines": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "mapDeepnest": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "mapCliffs": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "mapOutskirts": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "mapRestingGrounds": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "mapAbyss": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "hasPin": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "hasPinBench": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "hasPinCocoon": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "hasPinDreamPlant": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "hasPinGuardian": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "hasPinBlackEgg": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "hasPinShop": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "hasPinSpa": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "hasPinStag": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "hasPinTram": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "hasPinGhost": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "hasPinGrub": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "hasMarker": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "hasMarker_r": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "hasMarker_b": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "hasMarker_y": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "hasMarker_w": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "openedTramLower": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "openedTramRestingGrounds": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "tramLowerPosition": { + "Sync": true, + "SyncType": "Server" + }, + "tramRestingGroundsPosition": { + "Sync": true, + "SyncType": "Server" + }, + "mineLiftOpened": { + "Sync": true, + "SyncType": "Server" + }, + "menderDoorOpened": { + "Sync": true, + "SyncType": "Server" + }, + "vesselFragStagNest": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "shamanPillar": { + "Sync": true, + "SyncType": "Server" + }, + "crossroadsMawlekWall": { + "Sync": true, + "SyncType": "Server" + }, + "eggTempleVisited": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "crossroadsInfected": { + "Sync": true, + "SyncType": "Server" + }, + "falseKnightFirstPlop": { + "Sync": true, + "SyncType": "Server" + }, + "falseKnightWallRepaired": { + "Sync": true, + "SyncType": "Server" + }, + "falseKnightWallBroken": { + "Sync": true, + "SyncType": "Server" + }, + "falseKnightGhostDeparted": { + "Sync": true, + "SyncType": "Server" + }, + "spaBugsEncountered": { + "Sync": true, + "SyncType": "Server" + }, + "hornheadVinePlat": { + "Sync": true, + "SyncType": "Server" + }, + "infectedKnightEncountered": { + "Sync": true, + "SyncType": "Server" + }, + "megaMossChargerEncountered": { + "Sync": true, + "SyncType": "Server" + }, + "megaMossChargerDefeated": { + "Sync": true, + "SyncType": "Server" + }, + "dreamerScene1": { + "Sync": true, + "SyncType": "Server" + }, + "slugEncounterComplete": { + "Sync": true, + "SyncType": "Server" + }, + "defeatedDoubleBlockers": { + "Sync": true, + "SyncType": "Server" + }, + "oneWayArchive": { + "Sync": true, + "SyncType": "Server" + }, + "defeatedMegaJelly": { + "Sync": true, + "SyncType": "Server" + }, + "summonedMonomon": { + "Sync": true, + "SyncType": "Server" + }, + "sawWoundedQuirrel": { + "Sync": true, + "SyncType": "Server" + }, + "encounteredMegaJelly": { + "Sync": true, + "SyncType": "Server" + }, + "defeatedMantisLords": { + "Sync": true, + "SyncType": "Server" + }, + "encounteredGatekeeper": { + "Sync": true, + "SyncType": "Server" + }, + "deepnestWall": { + "Sync": true, + "SyncType": "Server" + }, + "queensStationNonDisplay": { + "Sync": true, + "SyncType": "Server" + }, + "cityBridge1": { + "Sync": true, + "SyncType": "Server" + }, + "cityBridge2": { + "Sync": true, + "SyncType": "Server" + }, + "cityLift1": { + "Sync": true, + "SyncType": "Server" + }, + "cityLift1_isUp": { + "Sync": true, + "SyncType": "Server" + }, + "liftArrival": { + "Sync": true, + "SyncType": "Server" + }, + "openedMageDoor": { + "Sync": true, + "SyncType": "Server" + }, + "openedMageDoor_v2": { + "Sync": true, + "SyncType": "Server" + }, + "brokenMageWindow": { + "Sync": true, + "SyncType": "Server" + }, + "brokenMageWindowGlass": { + "Sync": true, + "SyncType": "Server" + }, + "mageLordEncountered": { + "Sync": true, + "SyncType": "Server" + }, + "mageLordEncountered_2": { + "Sync": true, + "SyncType": "Server" + }, + "mageLordDefeated": { + "Sync": true, + "SyncType": "Server" + }, + "ruins1_5_tripleDoor": { + "Sync": true, + "SyncType": "Server" + }, + "openedCityGate": { + "Sync": true, + "SyncType": "Server" + }, + "cityGateClosed": { + "Sync": true, + "SyncType": "Server" + }, + "bathHouseOpened": { + "Sync": true, + "SyncType": "Server" + }, + "bathHouseWall": { + "Sync": true, + "SyncType": "Server" + }, + "cityLift2": { + "Sync": true, + "SyncType": "Server" + }, + "cityLift2_isUp": { + "Sync": true, + "SyncType": "Server" + }, + "city2_sewerDoor": { + "Sync": true, + "SyncType": "Server" + }, + "openedLoveDoor": { + "Sync": true, + "SyncType": "Server" + }, + "watcherChandelier": { + "Sync": true, + "SyncType": "Server" + }, + "completedQuakeArea": { + "Sync": true, + "SyncType": "Server" + }, + "kingsStationNonDisplay": { + "Sync": true, + "SyncType": "Server" + }, + "tollBenchCity": { + "Sync": true, + "SyncType": "Server" + }, + "waterwaysGate": { + "Sync": true, + "SyncType": "Server" + }, + "defeatedDungDefender": { + "Sync": true, + "SyncType": "Server" + }, + "dungDefenderEncounterReady": { + "Sync": true, + "SyncType": "Server" + }, + "flukeMotherEncountered": { + "Sync": true, + "SyncType": "Server" + }, + "flukeMotherDefeated": { + "Sync": true, + "SyncType": "Server" + }, + "openedWaterwaysManhole": { + "Sync": true, + "SyncType": "Server" + }, + "waterwaysAcidDrained": { + "Sync": true, + "SyncType": "Server" + }, + "dungDefenderWallBroken": { + "Sync": true, + "SyncType": "Server" + }, + "dungDefenderSleeping": { + "Sync": true, + "SyncType": "Server" + }, + "defeatedMegaBeamMiner": { + "Sync": true, + "SyncType": "Server" + }, + "defeatedMegaBeamMiner2": { + "Sync": true, + "SyncType": "Server" + }, + "brokeMinersWall": { + "Sync": true, + "SyncType": "Server" + }, + "encounteredMimicSpider": { + "Sync": true, + "SyncType": "Server" + }, + "steppedBeyondBridge": { + "Sync": true, + "SyncType": "Server" + }, + "deepnestBridgeCollapsed": { + "Sync": true, + "SyncType": "Server" + }, + "spiderCapture": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "deepnest26b_switch": { + "Sync": true, + "SyncType": "Server" + }, + "openedRestingGrounds02": { + "Sync": true, + "SyncType": "Server" + }, + "restingGroundsCryptWall": { + "Sync": true, + "SyncType": "Server" + }, + "dreamNailConvo": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "gladeGhostsKilled": { + "Sync": true, + "SyncType": "Server" + }, + "openedGardensStagStation": { + "Sync": true, + "SyncType": "Server" + }, + "extendedGramophone": { + "Sync": true, + "SyncType": "Server" + }, + "tollBenchQueensGardens": { + "Sync": true, + "SyncType": "Server" + }, + "blizzardEnded": { + "Sync": true, + "SyncType": "Server" + }, + "encounteredHornet": { + "Sync": true, + "SyncType": "Server" + }, + "savedByHornet": { + "Sync": true, + "SyncType": "Server" + }, + "outskirtsWall": { + "Sync": true, + "SyncType": "Server" + }, + "abyssGateOpened": { + "Sync": true, + "SyncType": "Server" + }, + "abyssLighthouse": { + "Sync": true, + "SyncType": "Server" + }, + "blueVineDoor": { + "Sync": true, + "SyncType": "Server" + }, + "gotShadeCharm": { + "Sync": true, + "SyncType": "Server" + }, + "tollBenchAbyss": { + "Sync": true, + "SyncType": "Server" + }, + "fountainGeo": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "fountainVesselSummoned": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "openedBlackEggPath": { + "Sync": true, + "SyncType": "Server" + }, + "enteredDreamWorld": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "duskKnightDefeated": { + "Sync": true, + "SyncType": "Server" + }, + "whitePalaceOrb_1": { + "Sync": true, + "SyncType": "Server" + }, + "whitePalaceOrb_2": { + "Sync": true, + "SyncType": "Server" + }, + "whitePalaceOrb_3": { + "Sync": true, + "SyncType": "Server" + }, + "whitePalace05_lever": { + "Sync": true, + "SyncType": "Server" + }, + "whitePalaceMidWarp": { + "Sync": true, + "SyncType": "Server" + }, + "whitePalaceSecretRoomVisited": { + "Sync": true, + "SyncType": "Server" + }, + "tramOpenedDeepnest": { + "Sync": true, + "SyncType": "Server" + }, + "tramOpenedCrossroads": { + "Sync": true, + "SyncType": "Server" + }, + "openedBlackEggDoor": { + "Sync": true, + "SyncType": "Server" + }, + "unchainedHollowKnight": { + "Sync": true, + "SyncType": "Server" + }, + "flamesCollected": { + "Sync": true, + "SyncType": "Server" + }, + "flamesRequired": { + "Sync": true, + "SyncType": "Server" + }, + "nightmareLanternAppeared": { + "Sync": true, + "SyncType": "Server" + }, + "nightmareLanternLit": { + "Sync": true, + "SyncType": "Server" + }, + "troupeInTown": { + "Sync": true, + "SyncType": "Server" + }, + "divineInTown": { + "Sync": true, + "SyncType": "Server" + }, + "grimmChildLevel": { + "Sync": true, + "SyncType": "Server" + }, + "elderbugConvoGrimm": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "slyConvoGrimm": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "iseldaConvoGrimm": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "midwifeWeaverlingConvo": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "metGrimm": { + "Sync": true, + "SyncType": "Server" + }, + "foughtGrimm": { + "Sync": true, + "SyncType": "Server" + }, + "metBrum": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "defeatedNightmareGrimm": { + "Sync": true, + "SyncType": "Server" + }, + "grimmchildAwoken": { + "Sync": true, + "SyncType": "Server" + }, + "gotBrummsFlame": { + "Sync": true, + "SyncType": "Server" + }, + "brummBrokeBrazier": { + "Sync": true, + "SyncType": "Server" + }, + "destroyedNightmareLantern": { + "Sync": true, + "SyncType": "Server" + }, + "gotGrimmNotch": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "nymmInTown": { + "Sync": true, + "SyncType": "Server" + }, + "nymmSpoken": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "nymmCharmConvo": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "nymmFinalConvo": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "elderbugNymmConvo": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "slyNymmConvo": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "iseldaNymmConvo": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "nymmMissedEggOpen": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "elderbugTroupeLeftConvo": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "elderbugBrettaLeft": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "jijiGrimmConvo": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "metDivine": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "divineFinalConvo": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "gaveFragileHeart": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "gaveFragileGreed": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "gaveFragileStrength": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "divineEatenConvos": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "pooedFragileHeart": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "pooedFragileGreed": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "pooedFragileStrength": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "completionPercentage": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "unlockedCompletionRate": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "newDatTraitorLord": { + "Sync": true, + "SyncType": "Server" + }, + "bossDoorStateTier1": { + "Sync": true, + "SyncType": "Server" + }, + "bossDoorStateTier2": { + "Sync": true, + "SyncType": "Server" + }, + "bossDoorStateTier3": { + "Sync": true, + "SyncType": "Server" + }, + "bossDoorStateTier4": { + "Sync": true, + "SyncType": "Server" + }, + "bossDoorStateTier5": { + "Sync": true, + "SyncType": "Server" + }, + "bossStatueTargetLevel": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "statueStateGruzMother": { + "Sync": true, + "SyncType": "Server" + }, + "statueStateVengefly": { + "Sync": true, + "SyncType": "Server" + }, + "statueStateBroodingMawlek": { + "Sync": true, + "SyncType": "Server" + }, + "statueStateFalseKnight": { + "Sync": true, + "SyncType": "Server" + }, + "statueStateFailedChampion": { + "Sync": true, + "SyncType": "Server" + }, + "statueStateHornet1": { + "Sync": true, + "SyncType": "Server" + }, + "statueStateHornet2": { + "Sync": true, + "SyncType": "Server" + }, + "statueStateMegaMossCharger": { + "Sync": true, + "SyncType": "Server" + }, + "statueStateMantisLords": { + "Sync": true, + "SyncType": "Server" + }, + "statueStateOblobbles": { + "Sync": true, + "SyncType": "Server" + }, + "statueStateGreyPrince": { + "Sync": true, + "SyncType": "Server" + }, + "statueStateBrokenVessel": { + "Sync": true, + "SyncType": "Server" + }, + "statueStateLostKin": { + "Sync": true, + "SyncType": "Server" + }, + "statueStateNosk": { + "Sync": true, + "SyncType": "Server" + }, + "statueStateFlukemarm": { + "Sync": true, + "SyncType": "Server" + }, + "statueStateCollector": { + "Sync": true, + "SyncType": "Server" + }, + "statueStateWatcherKnights": { + "Sync": true, + "SyncType": "Server" + }, + "statueStateSoulMaster": { + "Sync": true, + "SyncType": "Server" + }, + "statueStateSoulTyrant": { + "Sync": true, + "SyncType": "Server" + }, + "statueStateGodTamer": { + "Sync": true, + "SyncType": "Server" + }, + "statueStateCrystalGuardian1": { + "Sync": true, + "SyncType": "Server" + }, + "statueStateCrystalGuardian2": { + "Sync": true, + "SyncType": "Server" + }, + "statueStateUumuu": { + "Sync": true, + "SyncType": "Server" + }, + "statueStateDungDefender": { + "Sync": true, + "SyncType": "Server" + }, + "statueStateWhiteDefender": { + "Sync": true, + "SyncType": "Server" + }, + "statueStateHiveKnight": { + "Sync": true, + "SyncType": "Server" + }, + "statueStateTraitorLord": { + "Sync": true, + "SyncType": "Server" + }, + "statueStateGrimm": { + "Sync": true, + "SyncType": "Server" + }, + "statueStateNightmareGrimm": { + "Sync": true, + "SyncType": "Server" + }, + "statueStateHollowKnight": { + "Sync": true, + "SyncType": "Server" + }, + "statueStateElderHu": { + "Sync": true, + "SyncType": "Server" + }, + "statueStateGalien": { + "Sync": true, + "SyncType": "Server" + }, + "statueStateMarkoth": { + "Sync": true, + "SyncType": "Server" + }, + "statueStateMarmu": { + "Sync": true, + "SyncType": "Server" + }, + "statueStateNoEyes": { + "Sync": true, + "SyncType": "Server" + }, + "statueStateXero": { + "Sync": true, + "SyncType": "Server" + }, + "statueStateGorb": { + "Sync": true, + "SyncType": "Server" + }, + "statueStateRadiance": { + "Sync": true, + "SyncType": "Server" + }, + "statueStateSly": { + "Sync": true, + "SyncType": "Server" + }, + "statueStateNailmasters": { + "Sync": true, + "SyncType": "Server" + }, + "statueStateMageKnight": { + "Sync": true, + "SyncType": "Server" + }, + "statueStatePaintmaster": { + "Sync": true, + "SyncType": "Server" + }, + "statueStateZote": { + "Sync": true, + "SyncType": "Server" + }, + "statueStateNoskHornet": { + "Sync": true, + "SyncType": "Server" + }, + "statueStateMantisLordsExtra": { + "Sync": true, + "SyncType": "Server" + }, + "godseekerUnlocked": { + "Sync": true, + "SyncType": "Server" + }, + "bossDoorCageUnlocked": { + "Sync": true, + "SyncType": "Server" + }, + "blueRoomDoorUnlocked": { + "Sync": true, + "SyncType": "Server" + }, + "blueRoomActivated": { + "Sync": true, + "SyncType": "Server" + }, + "finalBossDoorUnlocked": { + "Sync": true, + "SyncType": "Server" + }, + "hasGodfinder": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "unlockedNewBossStatue": { + "Sync": true, + "SyncType": "Server" + }, + "scaredFlukeHermitEncountered": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "scaredFlukeHermitReturned": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "enteredGGAtrium": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "extraFlowerAppear": { + "Sync": true, + "SyncType": "Server" + }, + "givenGodseekerFlower": { + "Sync": true, + "SyncType": "Server" + }, + "givenOroFlower": { + "Sync": true, + "SyncType": "Server" + }, + "givenWhiteLadyFlower": { + "Sync": true, + "SyncType": "Server" + }, + "givenEmilitiaFlower": { + "Sync": true, + "SyncType": "Server" + }, + "unlockedBossScenes": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "queuedGodfinderIcon": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "godseekerSpokenAwake": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "nailsmithCorpseAppeared": { + "Sync": true, + "SyncType": "Server" + }, + "godseekerWaterwaysSeenState": { + "Sync": true, + "SyncType": "Server" + }, + "godseekerWaterwaysSpoken1": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "godseekerWaterwaysSpoken2": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "godseekerWaterwaysSpoken3": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "bossDoorEntranceTextSeen": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "seenDoor4Finale": { + "Sync": true, + "SyncType": "Server" + }, + "zoteStatueWallBroken": { + "Sync": true, + "SyncType": "Server" + }, + "seenGGWastes": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "ordealAchieved": { + "Sync": true, + "SyncType": "Server" + } }, "geoRocks": [ { @@ -2717,15974 +6953,23465 @@ "id": "fury charm_remask", "sceneName": "Tutorial_01" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "inverse_remask_right", "sceneName": "Tutorial_01" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Collapser Tute 01", "sceneName": "Tutorial_01" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Secret Mask", "sceneName": "Tutorial_01" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Health Cocoon", "sceneName": "Tutorial_01" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Tute Door 2", "sceneName": "Tutorial_01" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Tute Door 4", "sceneName": "Tutorial_01" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Tute Door 3", "sceneName": "Tutorial_01" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Interact Reminder", "sceneName": "Tutorial_01" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Tute Door 6", "sceneName": "Tutorial_01" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Chest", "sceneName": "Tutorial_01" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Door", "sceneName": "Tutorial_01" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Secret Sound Region", "sceneName": "Tutorial_01" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Tute Door 1", "sceneName": "Tutorial_01" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Inverse Remasker", "sceneName": "Tutorial_01" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Tute Door 7", "sceneName": "Tutorial_01" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Break Floor 1", "sceneName": "Tutorial_01" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Secret Sound Region (1)", "sceneName": "Tutorial_01" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Tute Door 5", "sceneName": "Tutorial_01" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Initial Fall Impact", "sceneName": "Tutorial_01" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Shiny Item (1)", "sceneName": "Tutorial_01" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Death Respawn Trigger", "sceneName": "Town" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Mines Lever", "sceneName": "Town" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Interact Reminder", "sceneName": "Town" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Death Respawn Trigger 1", "sceneName": "Town" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Door Destroyer", "sceneName": "Town" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Gravedigger NPC", "sceneName": "Town" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Zombie Runner", "sceneName": "Crossroads_01" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Secret Mask", "sceneName": "Crossroads_01" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Shiny Item", "sceneName": "Crossroads_01" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Zombie Runner 1", "sceneName": "Crossroads_01" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Remasker", "sceneName": "Crossroads_07" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Dream Plant Orb (14)", "sceneName": "Crossroads_07" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (11)", "sceneName": "Crossroads_07" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (18)", "sceneName": "Crossroads_07" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Breakable Wall_Silhouette", "sceneName": "Crossroads_07" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Dream Plant Orb (26)", "sceneName": "Crossroads_07" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (2)", "sceneName": "Crossroads_07" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (21)", "sceneName": "Crossroads_07" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (5)", "sceneName": "Crossroads_07" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (19)", "sceneName": "Crossroads_07" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (28)", "sceneName": "Crossroads_07" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (1)", "sceneName": "Crossroads_07" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (23)", "sceneName": "Crossroads_07" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (22)", "sceneName": "Crossroads_07" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (25)", "sceneName": "Crossroads_07" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (12)", "sceneName": "Crossroads_07" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb", "sceneName": "Crossroads_07" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant", "sceneName": "Crossroads_07" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (9)", "sceneName": "Crossroads_07" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (27)", "sceneName": "Crossroads_07" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (15)", "sceneName": "Crossroads_07" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (16)", "sceneName": "Crossroads_07" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (4)", "sceneName": "Crossroads_07" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (13)", "sceneName": "Crossroads_07" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (24)", "sceneName": "Crossroads_07" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (17)", "sceneName": "Crossroads_07" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (7)", "sceneName": "Crossroads_07" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (20)", "sceneName": "Crossroads_07" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Tute Door 1", "sceneName": "Crossroads_07" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Dream Plant Orb (8)", "sceneName": "Crossroads_07" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (6)", "sceneName": "Crossroads_07" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (3)", "sceneName": "Crossroads_07" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (10)", "sceneName": "Crossroads_07" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Break Wall 2", "sceneName": "Crossroads_08" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Battle Scene", "sceneName": "Crossroads_08" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "break_wall_masks", "sceneName": "Crossroads_08" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Zombie Barger", "sceneName": "Crossroads_13" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Heart Piece", "sceneName": "Crossroads_13" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Break Floor 1", "sceneName": "Crossroads_13" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Hatcher", "sceneName": "Crossroads_19" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Zombie Leaper", "sceneName": "Crossroads_19" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Grub Bottle", "sceneName": "Crossroads_03" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Break Wall 2", "sceneName": "Crossroads_03" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Toll Gate Switch", "sceneName": "Crossroads_03" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "CamLock Destroyer", "sceneName": "Crossroads_04" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Break Floor 1", "sceneName": "Crossroads_04" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Secret Mask", "sceneName": "Crossroads_04" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Battle Scene", "sceneName": "Crossroads_04" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Secret Mask (1)", "sceneName": "Crossroads_04" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Zombie Hornhead", "sceneName": "Crossroads_04" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Zombie Runner", "sceneName": "Crossroads_04" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Zombie Barger", "sceneName": "Crossroads_04" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Gate Switch", "sceneName": "Room_Town_Stag_Station" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Zombie Guard", "sceneName": "Crossroads_21" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Zombie Runner", "sceneName": "Crossroads_21" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Zombie Leaper", "sceneName": "Crossroads_21" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Zombie Barger", "sceneName": "Crossroads_21" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Collapser Small", "sceneName": "Crossroads_21" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Secret Mask", "sceneName": "Crossroads_21" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Secret Mask (1)", "sceneName": "Crossroads_21" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Breakable Wall", "sceneName": "Crossroads_21" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Zombie Barger (1)", "sceneName": "Crossroads_21" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Zombie Hornhead", "sceneName": "Crossroads_10" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Zombie Runner", "sceneName": "Crossroads_10" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Zombie Barger", "sceneName": "Crossroads_10" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Chest", "sceneName": "Crossroads_10" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Breakable Wall", "sceneName": "Crossroads_10" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Battle Scene", "sceneName": "Crossroads_10" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Shiny Item", "sceneName": "Crossroads_10" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Gate Switch", "sceneName": "Crossroads_06" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Raising Pillar", "sceneName": "Crossroads_06" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Dream Plant Orb (33)", "sceneName": "Crossroads_ShamanTemple" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (34)", "sceneName": "Crossroads_ShamanTemple" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (18)", "sceneName": "Crossroads_ShamanTemple" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (23)", "sceneName": "Crossroads_ShamanTemple" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (14)", "sceneName": "Crossroads_ShamanTemple" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (4)", "sceneName": "Crossroads_ShamanTemple" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (26)", "sceneName": "Crossroads_ShamanTemple" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (1)", "sceneName": "Crossroads_ShamanTemple" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (36)", "sceneName": "Crossroads_ShamanTemple" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (5)", "sceneName": "Crossroads_ShamanTemple" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Health Cocoon", "sceneName": "Crossroads_ShamanTemple" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Dream Plant Orb (9)", "sceneName": "Crossroads_ShamanTemple" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (30)", "sceneName": "Crossroads_ShamanTemple" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (31)", "sceneName": "Crossroads_ShamanTemple" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (10)", "sceneName": "Crossroads_ShamanTemple" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb", "sceneName": "Crossroads_ShamanTemple" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (20)", "sceneName": "Crossroads_ShamanTemple" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (11)", "sceneName": "Crossroads_ShamanTemple" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (27)", "sceneName": "Crossroads_ShamanTemple" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (22)", "sceneName": "Crossroads_ShamanTemple" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (29)", "sceneName": "Crossroads_ShamanTemple" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (21)", "sceneName": "Crossroads_ShamanTemple" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (15)", "sceneName": "Crossroads_ShamanTemple" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (32)", "sceneName": "Crossroads_ShamanTemple" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Shiny Item", "sceneName": "Crossroads_ShamanTemple" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Death Respawn Trigger 1", "sceneName": "Crossroads_ShamanTemple" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (24)", "sceneName": "Crossroads_ShamanTemple" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (25)", "sceneName": "Crossroads_ShamanTemple" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (28)", "sceneName": "Crossroads_ShamanTemple" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Bone Gate", "sceneName": "Crossroads_ShamanTemple" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Secret Mask", "sceneName": "Crossroads_ShamanTemple" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Blocker", "sceneName": "Crossroads_ShamanTemple" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Dream Plant Orb (37)", "sceneName": "Crossroads_ShamanTemple" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (35)", "sceneName": "Crossroads_ShamanTemple" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (17)", "sceneName": "Crossroads_ShamanTemple" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (38)", "sceneName": "Crossroads_ShamanTemple" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (16)", "sceneName": "Crossroads_ShamanTemple" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (13)", "sceneName": "Crossroads_ShamanTemple" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (41)", "sceneName": "Crossroads_ShamanTemple" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (6)", "sceneName": "Crossroads_ShamanTemple" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant", "sceneName": "Crossroads_ShamanTemple" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (40)", "sceneName": "Crossroads_ShamanTemple" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (12)", "sceneName": "Crossroads_ShamanTemple" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (3)", "sceneName": "Crossroads_ShamanTemple" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Battle Scene", "sceneName": "Crossroads_ShamanTemple" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Dream Plant Orb (39)", "sceneName": "Crossroads_ShamanTemple" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (19)", "sceneName": "Crossroads_ShamanTemple" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Remasker", "sceneName": "Crossroads_ShamanTemple" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Dream Plant Orb (8)", "sceneName": "Crossroads_ShamanTemple" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (2)", "sceneName": "Crossroads_ShamanTemple" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (7)", "sceneName": "Crossroads_ShamanTemple" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Reminder Cast (1)", "sceneName": "Crossroads_ShamanTemple" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Zombie Runner", "sceneName": "Crossroads_ShamanTemple" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Prayer Slug", "sceneName": "Crossroads_10" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Prayer Slug (1)", "sceneName": "Crossroads_10" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Hatcher 1", "sceneName": "Crossroads_27" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Hatcher", "sceneName": "Crossroads_27" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Hatcher 2", "sceneName": "Crossroads_27" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Zombie Shield 1", "sceneName": "Crossroads_15" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Zombie Shield", "sceneName": "Crossroads_15" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Reward 16", "sceneName": "Crossroads_38" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Reward 31", "sceneName": "Crossroads_38" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Reward 46", "sceneName": "Crossroads_38" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Reward 10", "sceneName": "Crossroads_38" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Reward 38", "sceneName": "Crossroads_38" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Reward 5", "sceneName": "Crossroads_38" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Reward 23", "sceneName": "Crossroads_38" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Blocker", "sceneName": "Crossroads_11_alt" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Battle Scene", "sceneName": "Crossroads_11_alt" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Mossman_Shaker", "sceneName": "Fungus1_01" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Secret Mask", "sceneName": "Fungus1_01" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Mossman_Runner", "sceneName": "Fungus1_01" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Plant Trap", "sceneName": "Fungus1_01" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Mossman_Shaker (1)", "sceneName": "Fungus1_01" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Mossman_Shaker", "sceneName": "Fungus1_02" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Secret Mask", "sceneName": "Fungus1_17" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Secret Mask (1)", "sceneName": "Fungus1_17" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Moss Charger", "sceneName": "Fungus1_17" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Zombie Hornhead", "sceneName": "Fungus1_03" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Zombie Runner", "sceneName": "Fungus1_03" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Zombie Barger", "sceneName": "Fungus1_03" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Zombie Runner (1)", "sceneName": "Fungus1_03" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Plant Trap", "sceneName": "Fungus1_03" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Mossman_Shaker (1)", "sceneName": "Fungus1_31" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Mossman_Runner", "sceneName": "Fungus1_31" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Breakable Wall", "sceneName": "Fungus1_31" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Toll Gate Machine", "sceneName": "Fungus1_31" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Toll Gate Machine (1)", "sceneName": "Fungus1_31" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Mossman_Shaker", "sceneName": "Fungus1_31" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Moss Knight B", "sceneName": "Fungus1_32" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Health Cocoon", "sceneName": "Fungus1_32" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Breakable Wall", "sceneName": "Fungus1_32" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Battle Scene v2", "sceneName": "Fungus1_32" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Moss Knight (1)", "sceneName": "Fungus1_21" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Moss Knight", "sceneName": "Fungus1_21" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Grub Bottle", "sceneName": "Fungus1_21" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Battle Scene", "sceneName": "Fungus1_21" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Secret Mask", "sceneName": "Fungus1_21" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Vine Platform (2)", "sceneName": "Fungus1_21" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Moss Charger", "sceneName": "Fungus1_21" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Moss Charger (1)", "sceneName": "Fungus1_21" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Moss Charger (2)", "sceneName": "Fungus1_21" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Gate Switch", "sceneName": "Fungus1_22" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Vine Platform", "sceneName": "Fungus1_22" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Shiny Item", "sceneName": "Fungus1_22" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Breakable Wall", "sceneName": "Fungus1_22" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Vine Platform (1)", "sceneName": "Fungus1_22" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Plant Trap (3)", "sceneName": "Fungus1_22" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Plant Trap (5)", "sceneName": "Fungus1_22" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Plant Trap (1)", "sceneName": "Fungus1_22" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Plant Trap", "sceneName": "Fungus1_22" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Plant Trap (4)", "sceneName": "Fungus1_22" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Plant Trap (2)", "sceneName": "Fungus1_22" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Mossman_Shaker (1)", "sceneName": "Fungus1_22" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Mossman_Shaker (2)", "sceneName": "Fungus1_22" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Mossman_Shaker", "sceneName": "Fungus1_22" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "secret mask 2", "sceneName": "Fungus1_04" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Break Floor 1", "sceneName": "Fungus1_04" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Camera Locks Boss", "sceneName": "Fungus1_04" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Reminder Look Down", "sceneName": "Fungus1_04" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Shiny Item", "sceneName": "Fungus1_04" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Breakable Wall Waterways", "sceneName": "Crossroads_18" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Inverse Remasker", "sceneName": "Crossroads_18" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Fungus Flyer (1)", "sceneName": "Fungus2_06" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Fungus Flyer", "sceneName": "Fungus2_06" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Fungus Flyer (2)", "sceneName": "Fungus2_06" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Fungus Flyer (3)", "sceneName": "Fungus2_06" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Secret Mask (1)", "sceneName": "Fungus2_07" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Secret Mask", "sceneName": "Fungus2_07" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Mushroom Roller", "sceneName": "Fungus2_07" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Mushroom Roller (1)", "sceneName": "Fungus2_07" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Fungus Flyer", "sceneName": "Fungus2_08" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Secret Mask", "sceneName": "Fungus2_10" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "One Way Wall", "sceneName": "Fungus2_10" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Zombie Fungus B", "sceneName": "Fungus2_10" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Zombie Fungus A", "sceneName": "Fungus2_10" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Fungus Flyer", "sceneName": "Fungus2_11" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Mantis (2)", "sceneName": "Fungus2_12" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Mantis (1)", "sceneName": "Fungus2_12" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Mantis", "sceneName": "Fungus2_12" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Mantis (1)", "sceneName": "Fungus2_13" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Mantis (4)", "sceneName": "Fungus2_13" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Mantis (3)", "sceneName": "Fungus2_13" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Mantis (2)", "sceneName": "Fungus2_13" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Mantis", "sceneName": "Fungus2_13" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Mantis Lever", "sceneName": "Fungus2_14" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Mantis (2)", "sceneName": "Fungus2_14" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Shiny Item Stand", "sceneName": "Fungus2_14" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Mantis Lever (1)", "sceneName": "Fungus2_14" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Mantis (1)", "sceneName": "Fungus2_14" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Gate Mantis", "sceneName": "Fungus2_14" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Mantis", "sceneName": "Fungus2_15" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Mantis Lever", "sceneName": "Fungus2_15" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Mantis (2)", "sceneName": "Fungus2_15" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Mantis Lever (4)", "sceneName": "Fungus2_15" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Mantis Lever (3)", "sceneName": "Fungus2_15" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Breakable Wall", "sceneName": "Fungus2_15" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Health Cocoon", "sceneName": "Fungus2_15" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Mantis Lever (1)", "sceneName": "Fungus2_15" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Mantis Lever (2)", "sceneName": "Fungus2_15" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Mantis (1)", "sceneName": "Fungus2_15" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Remasker", "sceneName": "Fungus2_21" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "secret sound", "sceneName": "Fungus2_21" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Secret Mask", "sceneName": "Fungus2_21" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Quake Floor", "sceneName": "Fungus2_21" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Ceiling Dropper", "sceneName": "Ruins1_01" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Zombie Barger", "sceneName": "Ruins1_01" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Ceiling Dropper (1)", "sceneName": "Ruins1_01" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Ruins Sentry 1", "sceneName": "Ruins1_01" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Zombie Hornhead", "sceneName": "Ruins1_01" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Zombie Leaper", "sceneName": "Ruins1_01" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Secret Mask", "sceneName": "Ruins1_02" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Ruins Sentry 1 (1)", "sceneName": "Ruins1_02" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Ruins Sentry 1", "sceneName": "Ruins1_02" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Shiny Item", "sceneName": "Ruins1_03" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Ruins Sentry 1 (3)", "sceneName": "Ruins1_03" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Ruins Flying Sentry", "sceneName": "Ruins1_03" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Ruins Flying Sentry (3)", "sceneName": "Ruins1_03" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Ruins Sentry 1 (2)", "sceneName": "Ruins1_03" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Zombie Hornhead", "sceneName": "Ruins1_03" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Ruins Sentry 1", "sceneName": "Ruins1_03" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Zombie Hornhead (1)", "sceneName": "Ruins1_03" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Ruins Sentry 1 (1)", "sceneName": "Ruins1_03" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Ruins Flying Sentry (2)", "sceneName": "Ruins1_03" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Zombie Leaper", "sceneName": "Ruins1_03" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "One Way Wall", "sceneName": "Ruins1_04" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Secret Mask", "sceneName": "Ruins1_04" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Inverse Remasker", "sceneName": "Ruins1_04" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Secret Mask (1)", "sceneName": "Ruins1_04" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Ruins Sentry Fat", "sceneName": "Ruins1_05c" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Remasker", "sceneName": "Ruins1_05c" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Shiny Item", "sceneName": "Ruins1_05" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Ruins Lever 3", "sceneName": "Ruins1_05" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Remasker (1)", "sceneName": "Ruins1_05c" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Ruins Lever 2", "sceneName": "Ruins1_05" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Ruins Flying Sentry Javelin (2)", "sceneName": "Ruins1_05c" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Ruins Sentry 1 (7)", "sceneName": "Ruins1_05c" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Ruins Sentry 1 (9)", "sceneName": "Ruins1_05c" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Ceiling Dropper (5)", "sceneName": "Ruins1_05c" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Ceiling Dropper (4)", "sceneName": "Ruins1_05c" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Zombie Barger", "sceneName": "Ruins1_05c" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Ceiling Dropper (6)", "sceneName": "Ruins1_05c" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Ruins Sentry Fat (5)", "sceneName": "Ruins1_05c" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Ruins Lever 1", "sceneName": "Ruins1_05b" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Ruins Flying Sentry", "sceneName": "Ruins1_05" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Ceiling Dropper", "sceneName": "Ruins1_05" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Battle Scene v2", "sceneName": "Ruins1_05" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Ruins Lever", "sceneName": "Ruins1_05" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Grub Bottle (1)", "sceneName": "Ruins1_05" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Ruins Flying Sentry Javelin (3)", "sceneName": "Ruins1_05" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Zombie Hornhead", "sceneName": "Ruins1_05" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Ruins Flying Sentry (1)", "sceneName": "Ruins1_05" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Ruins Sentry FatB", "sceneName": "Ruins1_05" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Ruins Lever", "sceneName": "Ruins1_31" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Breakable Wall Ruin Lift", "sceneName": "Ruins1_31" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Ruins Vial Empty", "sceneName": "Ruins1_31" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Toll Machine Bench", "sceneName": "Ruins1_31" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Ruins Flying Sentry Javelin (1)", "sceneName": "Ruins1_17" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Ruins Flying Sentry", "sceneName": "Ruins1_17" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Ruins Flying Sentry Javelin", "sceneName": "Ruins1_17" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Dream Plant Orb (27)", "sceneName": "Ruins1_17" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (17)", "sceneName": "Ruins1_17" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (9)", "sceneName": "Ruins1_17" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (26)", "sceneName": "Ruins1_17" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Shiny Item", "sceneName": "Ruins1_17" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (1)", "sceneName": "Ruins1_17" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (20)", "sceneName": "Ruins1_17" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (10)", "sceneName": "Ruins1_17" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (24)", "sceneName": "Ruins1_17" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (14)", "sceneName": "Ruins1_17" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (19)", "sceneName": "Ruins1_17" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (8)", "sceneName": "Ruins1_17" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Remasker full bot", "sceneName": "Ruins1_17" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Dream Plant Orb (15)", "sceneName": "Ruins1_17" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (7)", "sceneName": "Ruins1_17" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (11)", "sceneName": "Ruins1_17" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (6)", "sceneName": "Ruins1_17" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "remask_half_mid", "sceneName": "Ruins1_17" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Dream Plant Orb (13)", "sceneName": "Ruins1_17" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (5)", "sceneName": "Ruins1_17" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Remasker full mid", "sceneName": "Ruins1_17" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Remasker full top", "sceneName": "Ruins1_17" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "remask_half_bot", "sceneName": "Ruins1_17" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Dream Plant Orb (12)", "sceneName": "Ruins1_17" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant", "sceneName": "Ruins1_17" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (21)", "sceneName": "Ruins1_17" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (25)", "sceneName": "Ruins1_17" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Ruins Lever", "sceneName": "Ruins1_17" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Dream Plant Orb (16)", "sceneName": "Ruins1_17" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (2)", "sceneName": "Ruins1_17" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (23)", "sceneName": "Ruins1_17" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb", "sceneName": "Ruins1_17" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (18)", "sceneName": "Ruins1_17" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (4)", "sceneName": "Ruins1_17" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "remask_half_bot (2)", "sceneName": "Ruins1_17" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Dream Plant Orb (22)", "sceneName": "Ruins1_17" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (3)", "sceneName": "Ruins1_17" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Zombie Hornhead", "sceneName": "Ruins1_17" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Ruins Flying Sentry (1)", "sceneName": "Ruins1_17" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Ruins Sentry 1", "sceneName": "Ruins1_17" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Zombie Leaper", "sceneName": "Ruins1_17" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Zombie Barger", "sceneName": "Ruins1_28" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Zombie Runner", "sceneName": "Ruins1_28" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Zombie Runner (1)", "sceneName": "Ruins1_28" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Shiny Item", "sceneName": "Ruins1_28" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Ruins Flying Sentry", "sceneName": "Ruins1_28" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Chain Platform", "sceneName": "Ruins1_28" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Zombie Barger 1", "sceneName": "Crossroads_37" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Zombie Hornhead", "sceneName": "Crossroads_37" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Zombie Hornhead 1", "sceneName": "Crossroads_37" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Zombie Runner 1", "sceneName": "Crossroads_37" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Zombie Barger (1)", "sceneName": "Crossroads_37" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Zombie Barger", "sceneName": "Crossroads_37" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Zombie Leaper", "sceneName": "Crossroads_37" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Remasker", "sceneName": "Crossroads_37" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Mask Bottom", "sceneName": "Crossroads_37" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Break Floor 1", "sceneName": "Crossroads_37" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Vessel Fragment", "sceneName": "Crossroads_37" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Mask Bottom 2", "sceneName": "Crossroads_37" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Zombie Runner (1)", "sceneName": "Crossroads_37" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Zombie Hornhead 2", "sceneName": "Crossroads_37" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Zombie Barger 3", "sceneName": "Crossroads_37" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Grub Bottle", "sceneName": "Crossroads_31" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Zombie Hornhead", "sceneName": "Crossroads_16" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Zombie Guard", "sceneName": "Crossroads_48" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Grub Bottle", "sceneName": "Crossroads_48" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Zombie Runner", "sceneName": "Crossroads_39" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Zombie Hornhead 1", "sceneName": "Crossroads_39" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Zombie Runner 1", "sceneName": "Crossroads_39" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Shiny", "sceneName": "Crossroads_33" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Battle Scene v2", "sceneName": "Fungus2_05" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Shiny Item", "sceneName": "Fungus2_05" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Zombie Runner", "sceneName": "Crossroads_05" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Grub Bottle", "sceneName": "Crossroads_05" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Zombie Runner 2", "sceneName": "Crossroads_40" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Zombie Hornhead", "sceneName": "Crossroads_40" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Zombie Leaper", "sceneName": "Crossroads_40" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Zombie Leaper 1", "sceneName": "Crossroads_40" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Shiny", "sceneName": "Ruins1_31" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Ruins Sentry Fat B", "sceneName": "Ruins1_05" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Mage", "sceneName": "Ruins1_09" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Battle Scene", "sceneName": "Ruins1_09" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Ruins Lever (1)", "sceneName": "Ruins1_23" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Ruins Lever", "sceneName": "Ruins1_23" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Mage (1)", "sceneName": "Ruins1_23" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Mage", "sceneName": "Ruins1_23" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Ruins Vial Empty (1)", "sceneName": "Ruins1_23" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Ruins Vial Empty", "sceneName": "Ruins1_23" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Battle Scene v2", "sceneName": "Ruins1_23" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Ruins Vial Empty (2)", "sceneName": "Ruins1_23" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Ruins Vial Empty", "sceneName": "Ruins1_25" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Ruins Vial Empty (1)", "sceneName": "Ruins1_25" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Ruins Lever", "sceneName": "Ruins1_25" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Mage", "sceneName": "Ruins1_25" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Mage (1)", "sceneName": "Ruins1_25" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Mage", "sceneName": "Ruins1_30" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Mage (1)", "sceneName": "Ruins1_30" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Quake Floor Glass", "sceneName": "Ruins1_30" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Secret Sound Region", "sceneName": "Ruins1_30" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Inverse Remasker", "sceneName": "Ruins1_30" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Secret Mask (3)", "sceneName": "Ruins1_30" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Quake Floor Glass (1)", "sceneName": "Ruins1_30" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Secret Mask", "sceneName": "Ruins1_30" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Secret Sounder", "sceneName": "Ruins1_30" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Quake Floor Glass (2)", "sceneName": "Ruins1_30" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Ruins Lever", "sceneName": "Ruins1_30" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Shiny Item Stand", "sceneName": "Ruins1_30" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Remasker", "sceneName": "Ruins1_30" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Ruins Vial Empty", "sceneName": "Ruins1_30" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Ruins Light Stand", "sceneName": "Ruins1_30" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Secret Mask (1)", "sceneName": "Ruins1_30" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Mage (2)", "sceneName": "Ruins1_30" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Ruins Vial Empty (1)", "sceneName": "Ruins1_30" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Secret Mask (2)", "sceneName": "Ruins1_30" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Secret Mask (1)", "sceneName": "Ruins1_24" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Ruins Vial Empty", "sceneName": "Ruins1_24" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Ruins Vial Empty (1)", "sceneName": "Ruins1_24" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Ruins Soul Vial", "sceneName": "Ruins1_24" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Quake Floor Glass (2)", "sceneName": "Ruins1_32" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Secret Mask (2)", "sceneName": "Ruins1_32" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Quake Floor Glass (1)", "sceneName": "Ruins1_32" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Quake Floor Glass (4)", "sceneName": "Ruins1_32" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Secret Mask", "sceneName": "Ruins1_32" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Quake Floor Glass", "sceneName": "Ruins1_32" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Ruins Vial Empty (4)", "sceneName": "Ruins1_32" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Quake Floor", "sceneName": "Ruins1_32" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Ruins Lever (1)", "sceneName": "Ruins1_32" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Quake Floor (1)", "sceneName": "Ruins1_32" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Secret Mask (1)", "sceneName": "Ruins1_32" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Chest", "sceneName": "Ruins1_32" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Grub Bottle", "sceneName": "Ruins1_32" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Quake Floor Glass (3)", "sceneName": "Ruins1_32" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Ruins Lever", "sceneName": "Ruins1_32" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Shiny Item", "sceneName": "Ruins1_32" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Heart Piece", "sceneName": "Crossroads_38" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "mine_1_quake_floor", "sceneName": "Mines_01" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Egg Sac", "sceneName": "Mines_01" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Shiny Item(Clone)", "sceneName": "Mines_01" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Zombie Miner 1", "sceneName": "Mines_02" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Zombie Miner 1 (1)", "sceneName": "Mines_02" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Shiny Item", "sceneName": "Mines_29" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Crystal Flyer (1)", "sceneName": "Mines_04" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Zombie Miner 1", "sceneName": "Mines_04" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Zombie Miner 1 (1)", "sceneName": "Mines_04" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Grub Bottle", "sceneName": "Mines_04" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Mines Lever", "sceneName": "Mines_04" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Crystal Flyer (3)", "sceneName": "Mines_04" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Crystal Flyer (2)", "sceneName": "Mines_04" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Crystal Flyer", "sceneName": "Mines_04" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Crystal Flyer", "sceneName": "Mines_07" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Crystal Flyer (2)", "sceneName": "Mines_07" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Zombie Miner 1", "sceneName": "Mines_17" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Zombie Miner 1 (1)", "sceneName": "Mines_17" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Grub Bottle", "sceneName": "Mines_03" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Mines Lever", "sceneName": "Mines_03" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Zombie Miner 1 (2)", "sceneName": "Mines_03" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Zombie Miner 1 (1)", "sceneName": "Mines_03" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Zombie Miner 1 (3)", "sceneName": "Mines_03" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Zombie Miner 1", "sceneName": "Mines_05" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Breakable Wall", "sceneName": "Mines_05" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Crystallised Lazer Bug (2)", "sceneName": "Mines_05" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Crystallised Lazer Bug (11)", "sceneName": "Mines_05" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Crystallised Lazer Bug", "sceneName": "Mines_05" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Crystallised Lazer Bug (6)", "sceneName": "Mines_05" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Crystallised Lazer Bug (12)", "sceneName": "Mines_05" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Crystallised Lazer Bug (10)", "sceneName": "Mines_05" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Crystallised Lazer Bug (3)", "sceneName": "Mines_05" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Crystallised Lazer Bug (9)", "sceneName": "Mines_05" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Crystallised Lazer Bug (5)", "sceneName": "Mines_05" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Crystallised Lazer Bug (1)", "sceneName": "Mines_05" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Shiny Item Stand", "sceneName": "Mines_30" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Zombie Miner 1", "sceneName": "Mines_11" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Shiny Item Stand", "sceneName": "Mines_11" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Crystallised Lazer Bug (4)", "sceneName": "Mines_11" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Crystallised Lazer Bug (2)", "sceneName": "Mines_11" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Crystallised Lazer Bug (1)", "sceneName": "Mines_11" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Crystallised Lazer Bug", "sceneName": "Mines_11" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Crystallised Lazer Bug (3)", "sceneName": "Mines_11" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Mega Zombie Beam Miner (1)", "sceneName": "Mines_18" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Battle Scene", "sceneName": "Mines_18" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Egg Sac", "sceneName": "Mines_20" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Quake Floor", "sceneName": "Mines_20" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Mines Lever (2)", "sceneName": "Mines_20" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Shiny Item (1)", "sceneName": "Mines_20" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Mines Lever (1)", "sceneName": "Mines_20" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Mines Lever", "sceneName": "Mines_20" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Crystallised Lazer Bug (7)", "sceneName": "Mines_20" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Crystallised Lazer Bug (5)", "sceneName": "Mines_20" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Crystallised Lazer Bug (3)", "sceneName": "Mines_20" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Crystallised Lazer Bug (4)", "sceneName": "Mines_20" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Crystallised Lazer Bug (8)", "sceneName": "Mines_20" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Crystallised Lazer Bug (9)", "sceneName": "Mines_20" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Crystallised Lazer Bug (6)", "sceneName": "Mines_20" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Zombie Miner 1", "sceneName": "Mines_20" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Zombie Miner 1 (9)", "sceneName": "Mines_20" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Zombie Miner 1 (1)", "sceneName": "Mines_20" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Crystal Flyer (3)", "sceneName": "Mines_20" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Crystal Flyer", "sceneName": "Mines_20" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Crystal Flyer (1)", "sceneName": "Mines_20" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Crystal Flyer (2)", "sceneName": "Mines_20" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Mines Lever", "sceneName": "Mines_19" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Grub Bottle", "sceneName": "Mines_19" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Mines Lever New", "sceneName": "Mines_19" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Grub Bottle", "sceneName": "Mines_31" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Reminder Superdash", "sceneName": "Mines_31" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Remasker", "sceneName": "Mines_31" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Zombie Miner 1 (2)", "sceneName": "Mines_37" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Zombie Miner 1 (5)", "sceneName": "Mines_37" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Zombie Miner 1 (4)", "sceneName": "Mines_37" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Zombie Miner 1 (6)", "sceneName": "Mines_37" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Mines Lever (3)", "sceneName": "Mines_37" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Mines Lever (4)", "sceneName": "Mines_37" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Mines Lever New", "sceneName": "Mines_37" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Chest", "sceneName": "Mines_37" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Crystallised Lazer Bug", "sceneName": "Mines_37" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Crystallised Lazer Bug (1)", "sceneName": "Mines_37" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Zombie Miner 1 (3)", "sceneName": "Mines_37" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Shiny", "sceneName": "Mines_30" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Ruins Lever", "sceneName": "Ruins1_27" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Remasker", "sceneName": "Ruins1_27" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Royal Zombie Coward (1)", "sceneName": "Ruins1_27" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Royal Zombie Coward", "sceneName": "Ruins1_27" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Royal Zombie 1", "sceneName": "Ruins1_27" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Royal Zombie 1 (1)", "sceneName": "Ruins1_27" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Grub Bottle", "sceneName": "Mines_35" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "One Way Wall", "sceneName": "Mines_35" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "mine_1_quake_floor", "sceneName": "Mines_35" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Secret Mask", "sceneName": "Mines_35" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Crystal Flyer (2)", "sceneName": "Mines_35" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Crystal Flyer", "sceneName": "Mines_35" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Crystal Flyer (1)", "sceneName": "Mines_35" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Remasker", "sceneName": "RestingGrounds_07" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Dream Plant Orb (18)", "sceneName": "RestingGrounds_05" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (11)", "sceneName": "RestingGrounds_05" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Quake Floor", "sceneName": "RestingGrounds_05" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Dream Plant Orb (9)", "sceneName": "RestingGrounds_05" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (3)", "sceneName": "RestingGrounds_05" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (10)", "sceneName": "RestingGrounds_05" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (13)", "sceneName": "RestingGrounds_05" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (2)", "sceneName": "RestingGrounds_05" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (12)", "sceneName": "RestingGrounds_05" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant", "sceneName": "RestingGrounds_05" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (8)", "sceneName": "RestingGrounds_05" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (19)", "sceneName": "RestingGrounds_05" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (17)", "sceneName": "RestingGrounds_05" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (16)", "sceneName": "RestingGrounds_05" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (4)", "sceneName": "RestingGrounds_05" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (5)", "sceneName": "RestingGrounds_05" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (1)", "sceneName": "RestingGrounds_05" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb", "sceneName": "RestingGrounds_05" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (14)", "sceneName": "RestingGrounds_05" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (15)", "sceneName": "RestingGrounds_05" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (7)", "sceneName": "RestingGrounds_05" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (6)", "sceneName": "RestingGrounds_05" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Shiny Item Stand", "sceneName": "RestingGrounds_17" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Ruins Lever", "sceneName": "RestingGrounds_09" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Shiny", "sceneName": "RestingGrounds_09" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Shiny Item Grubsong", "sceneName": "Crossroads_38" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Hatcher", "sceneName": "Crossroads_35" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Grub Bottle", "sceneName": "Crossroads_35" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Remasker", "sceneName": "Crossroads_35" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Mushroom Brawler", "sceneName": "Fungus2_05" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Mantis Lever", "sceneName": "Fungus2_04" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Mantis Lever (1)", "sceneName": "Fungus2_04" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Shiny Item", "sceneName": "Fungus2_04" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Zombie Fungus B", "sceneName": "Fungus2_03" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Shiny Item", "sceneName": "Fungus2_03" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Fungus Flyer", "sceneName": "Fungus2_03" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Fungus Flyer (1)", "sceneName": "Fungus2_03" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Fungus Flyer (2)", "sceneName": "Fungus2_03" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Zombie Hornhead", "sceneName": "Fungus2_03" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Ruins Lever", "sceneName": "Fungus2_01" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Remasker", "sceneName": "Fungus2_01" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Heart Piece", "sceneName": "Fungus2_01" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Breakable Wall Waterways", "sceneName": "Waterways_01" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Ceiling Dropper (2)", "sceneName": "Waterways_01" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Ceiling Dropper (3)", "sceneName": "Waterways_01" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Ceiling Dropper", "sceneName": "Waterways_01" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Ceiling Dropper (4)", "sceneName": "Waterways_01" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Ceiling Dropper (1)", "sceneName": "Waterways_01" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Quake Floor (1)", "sceneName": "Waterways_04" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Quake Floor", "sceneName": "Waterways_04" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Breakable Wall Waterways", "sceneName": "Waterways_04" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Flukeman (1)", "sceneName": "Waterways_04" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Secret Mask", "sceneName": "Waterways_04" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Egg Sac", "sceneName": "Waterways_04" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Grub Bottle", "sceneName": "Waterways_04" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Flukeman", "sceneName": "Waterways_04" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Ceiling Dropper", "sceneName": "Waterways_04" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Flukeman", "sceneName": "Waterways_02" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Remasker (1)", "sceneName": "Waterways_02" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Flukeman (2)", "sceneName": "Waterways_02" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Egg Sac", "sceneName": "Waterways_02" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Quake Floor", "sceneName": "Waterways_02" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "One Way Wall", "sceneName": "Waterways_02" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Quake Floor (1)", "sceneName": "Waterways_02" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Secret Mask (1)", "sceneName": "Waterways_02" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Remasker", "sceneName": "Waterways_02" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Flukeman (3)", "sceneName": "Waterways_02" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Secret Sound Region", "sceneName": "Waterways_02" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Secret Mask", "sceneName": "Waterways_02" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Flukeman (1)", "sceneName": "Waterways_02" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Ceiling Dropper", "sceneName": "Waterways_02" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Quake Floor", "sceneName": "Waterways_05" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "One Way Wall", "sceneName": "Waterways_05" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Waterways_Crank_Lever", "sceneName": "Waterways_05" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Shiny Item", "sceneName": "Waterways_05" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (32)", "sceneName": "Abyss_01" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (34)", "sceneName": "Abyss_01" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (17)", "sceneName": "Abyss_01" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (5)", "sceneName": "Abyss_01" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (25)", "sceneName": "Abyss_01" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (23)", "sceneName": "Abyss_01" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Inverse Remasker", "sceneName": "Abyss_01" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Dream Plant Orb (11)", "sceneName": "Abyss_01" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (10)", "sceneName": "Abyss_01" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (9)", "sceneName": "Abyss_01" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (7)", "sceneName": "Abyss_01" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (27)", "sceneName": "Abyss_01" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb", "sceneName": "Abyss_01" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (21)", "sceneName": "Abyss_01" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (18)", "sceneName": "Abyss_01" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (31)", "sceneName": "Abyss_01" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (8)", "sceneName": "Abyss_01" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (1)", "sceneName": "Abyss_01" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (33)", "sceneName": "Abyss_01" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (20)", "sceneName": "Abyss_01" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (22)", "sceneName": "Abyss_01" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (16)", "sceneName": "Abyss_01" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (3)", "sceneName": "Abyss_01" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (24)", "sceneName": "Abyss_01" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (12)", "sceneName": "Abyss_01" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant", "sceneName": "Abyss_01" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (26)", "sceneName": "Abyss_01" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Secret Sound Region", "sceneName": "Abyss_01" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Dream Plant Orb (6)", "sceneName": "Abyss_01" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (30)", "sceneName": "Abyss_01" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (13)", "sceneName": "Abyss_01" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (19)", "sceneName": "Abyss_01" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (4)", "sceneName": "Abyss_01" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (28)", "sceneName": "Abyss_01" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (29)", "sceneName": "Abyss_01" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (2)", "sceneName": "Abyss_01" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (14)", "sceneName": "Abyss_01" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (15)", "sceneName": "Abyss_01" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Ruins Flying Sentry", "sceneName": "Abyss_01" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Ruins Flying Sentry (1)", "sceneName": "Abyss_01" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Egg Sac", "sceneName": "Waterways_07" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "One Way Wall", "sceneName": "Waterways_07" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Inverse Remasker", "sceneName": "Waterways_07" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Secret Mask", "sceneName": "Waterways_07" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Ruins Sentry 1", "sceneName": "Waterways_07" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Ruins Sentry 1 (1)", "sceneName": "Waterways_07" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Ceiling Dropper", "sceneName": "Waterways_07" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Shiny Item(Clone)", "sceneName": "Waterways_07" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Grub Bottle", "sceneName": "Waterways_13" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Shiny Item Acid", "sceneName": "Waterways_13" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Battle Scene", "sceneName": "Waterways_13" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Ruins Flying Sentry", "sceneName": "Waterways_13" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Ruins Flying Sentry (1)", "sceneName": "Waterways_13" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Ceiling Dropper", "sceneName": "Waterways_13" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Ceiling Dropper (1)", "sceneName": "Waterways_13" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Ruins Sentry Fat", "sceneName": "Waterways_07" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Royal Zombie 1", "sceneName": "Ruins2_04" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Royal Zombie Coward", "sceneName": "Ruins2_04" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Royal Zombie Coward (1)", "sceneName": "Ruins2_04" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Great Shield Zombie (2)", "sceneName": "Ruins2_04" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Great Shield Zombie", "sceneName": "Ruins2_04" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Great Shield Zombie (1)", "sceneName": "Ruins2_04" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Great Shield Zombie (3)", "sceneName": "Ruins2_04" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Royal Zombie 1 (1)", "sceneName": "Ruins2_04" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Royal Zombie Fat", "sceneName": "Ruins2_04" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Royal Zombie Fat (1)", "sceneName": "Ruins2_04" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "remask", "sceneName": "Ruins2_06" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Secret Mask", "sceneName": "Ruins2_06" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Ruins Flying Sentry Javelin", "sceneName": "Ruins2_06" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Royal Zombie 1 (1)", "sceneName": "Ruins2_06" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Zombie Hornhead", "sceneName": "Ruins2_06" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Royal Zombie Fat", "sceneName": "Ruins2_06" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Shiny Item", "sceneName": "Ruins2_08" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Shiny Item", "sceneName": "Fungus3_26" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Secret Mask", "sceneName": "Fungus3_26" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Jellyfish (2)", "sceneName": "Fungus3_26" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Jellyfish (4)", "sceneName": "Fungus3_26" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Jellyfish", "sceneName": "Fungus3_26" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Jellyfish (3)", "sceneName": "Fungus3_26" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Jellyfish (1)", "sceneName": "Fungus3_26" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Jellyfish (2)", "sceneName": "Fungus3_25b" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Jellyfish (1)", "sceneName": "Fungus3_25b" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Jellyfish", "sceneName": "Fungus3_25b" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Jellyfish (4)", "sceneName": "Fungus3_25" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Jellyfish", "sceneName": "Fungus3_25" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Jellyfish (1)", "sceneName": "Fungus3_25" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Jellyfish (5)", "sceneName": "Fungus3_25" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Jellyfish (3)", "sceneName": "Fungus3_25" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Jellyfish (2)", "sceneName": "Fungus3_25" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Jellyfish (2)", "sceneName": "Fungus3_27" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Jellyfish (1)", "sceneName": "Fungus3_27" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Jellyfish", "sceneName": "Fungus3_27" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Jellyfish (3)", "sceneName": "Fungus3_27" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "One Way Wall", "sceneName": "Fungus3_47" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Secret Mask", "sceneName": "Fungus3_47" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Grub Bottle", "sceneName": "Fungus3_47" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Jellyfish (3)", "sceneName": "Fungus3_archive_02" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Jellyfish (5)", "sceneName": "Fungus3_archive_02" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Jellyfish (6)", "sceneName": "Fungus3_archive_02" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Jellyfish (2)", "sceneName": "Fungus3_archive_02" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Jellyfish (1)", "sceneName": "Fungus3_archive_02" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Jellyfish", "sceneName": "Fungus3_archive_02" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Jellyfish (4)", "sceneName": "Fungus3_archive_02" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Collapser Small", "sceneName": "Fungus3_28" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Jellyfish (2)", "sceneName": "Fungus3_28" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Jellyfish (5)", "sceneName": "Fungus3_28" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Jellyfish (4)", "sceneName": "Fungus3_28" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Jellyfish (3)", "sceneName": "Fungus3_28" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Jellyfish (1)", "sceneName": "Fungus3_28" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Shiny Item", "sceneName": "Fungus3_28" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (12)", "sceneName": "Fungus2_33" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (18)", "sceneName": "Fungus2_33" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (3)", "sceneName": "Fungus2_33" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (11)", "sceneName": "Fungus2_33" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (6)", "sceneName": "Fungus2_33" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (16)", "sceneName": "Fungus2_33" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (4)", "sceneName": "Fungus2_33" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (15)", "sceneName": "Fungus2_33" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (14)", "sceneName": "Fungus2_33" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb", "sceneName": "Fungus2_33" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (1)", "sceneName": "Fungus2_33" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (19)", "sceneName": "Fungus2_33" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (2)", "sceneName": "Fungus2_33" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (8)", "sceneName": "Fungus2_33" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (13)", "sceneName": "Fungus2_33" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (7)", "sceneName": "Fungus2_33" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (5)", "sceneName": "Fungus2_33" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant", "sceneName": "Fungus2_33" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (9)", "sceneName": "Fungus2_33" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (10)", "sceneName": "Fungus2_33" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (17)", "sceneName": "Fungus2_33" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Fungus Flyer (1)", "sceneName": "Fungus2_33" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Fungus Flyer", "sceneName": "Fungus2_33" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Remasker", "sceneName": "Crossroads_52" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Remasker (1)", "sceneName": "Crossroads_52" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Quake Floor", "sceneName": "Crossroads_52" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Bursting Zombie", "sceneName": "Crossroads_08" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Spitting Zombie", "sceneName": "Crossroads_08" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Bursting Bouncer (3)", "sceneName": "Crossroads_07" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Bursting Bouncer (5)", "sceneName": "Crossroads_07" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Bursting Bouncer", "sceneName": "Crossroads_07" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Bursting Bouncer (2)", "sceneName": "Crossroads_07" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Bursting Bouncer (4)", "sceneName": "Crossroads_07" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Bursting Bouncer (1)", "sceneName": "Crossroads_07" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Angry Buzzer", "sceneName": "Crossroads_01" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Bursting Zombie", "sceneName": "Crossroads_01" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Royal Zombie Fat", "sceneName": "Ruins_House_02" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Royal Zombie Coward", "sceneName": "Ruins_House_02" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Royal Zombie 1 (1)", "sceneName": "Ruins_House_02" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Royal Zombie Coward (1)", "sceneName": "Ruins_House_02" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Gorgeous Husk", "sceneName": "Ruins_House_02" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Inverse Remasker", "sceneName": "Ruins_House_02" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Inverse Remasker (2)", "sceneName": "Ruins_House_02" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Inverse Remasker (1)", "sceneName": "Ruins_House_02" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Secret Sound Region", "sceneName": "Ruins_House_02" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Quake Floor Glass", "sceneName": "Ruins2_01_b" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Battle Scene", "sceneName": "Ruins2_01_b" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Royal Zombie Coward", "sceneName": "Ruins2_01_b" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Royal Zombie 1", "sceneName": "Ruins2_01_b" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Royal Zombie Fat (1)", "sceneName": "Ruins2_01_b" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Royal Zombie Fat", "sceneName": "Ruins2_01_b" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Royal Zombie Coward (2)", "sceneName": "Ruins2_01" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Royal Zombie 1 (4)", "sceneName": "Ruins2_01" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Ruins Flying Sentry", "sceneName": "Ruins2_01" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Remasker", "sceneName": "Ruins2_01" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Ruins Lever (1)", "sceneName": "Ruins2_01" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Secret Mask", "sceneName": "Ruins2_01" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Ruins Lever", "sceneName": "Ruins2_01" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Battle Scene", "sceneName": "Ruins2_01" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Ruins Flying Sentry Javelin", "sceneName": "Ruins2_01" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Ruins Flying Sentry Javelin (1)", "sceneName": "Ruins2_01" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Royal Zombie Coward (1)", "sceneName": "Ruins2_01" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Great Shield Zombie", "sceneName": "Ruins2_01" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Royal Zombie Coward (3)", "sceneName": "Ruins2_01" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Royal Zombie 1 (1)", "sceneName": "Ruins2_01" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Ruins Sentry 1", "sceneName": "Ruins2_01" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Royal Zombie Fat (3)", "sceneName": "Ruins2_01" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Ruins Sentry Fat", "sceneName": "Ruins2_01" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Royal Zombie Fat (2)", "sceneName": "Ruins2_01" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Ruins Lever", "sceneName": "Ruins1_18" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Grub Scene", "sceneName": "Ruins2_03b" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Royal Zombie Coward", "sceneName": "Ruins2_03b" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Great Shield Zombie bottom", "sceneName": "Ruins2_03b" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Ruins Flying Sentry (1)", "sceneName": "Ruins2_03b" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Ruins Flying Sentry", "sceneName": "Ruins2_03b" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Royal Zombie 1", "sceneName": "Ruins2_03b" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Royal Zombie 1 (1)", "sceneName": "Ruins2_03b" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Royal Zombie Fat", "sceneName": "Ruins2_03b" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Shiny Item", "sceneName": "Ruins2_03" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "boss_floor_remasker", "sceneName": "Ruins2_03" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Chest", "sceneName": "Ruins2_03" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Breakable Wall", "sceneName": "Ruins2_03" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Grub Scene", "sceneName": "Ruins2_03" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Grub Bottle", "sceneName": "Ruins2_03" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Battle Control", "sceneName": "Ruins2_03" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Great Shield Zombie", "sceneName": "Ruins2_03" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Ruins Flying Sentry Javelin", "sceneName": "Ruins2_03" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Secret sound", "sceneName": "Ruins2_Watcher_Room" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Royal Zombie Coward", "sceneName": "Ruins2_Watcher_Room" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Inverse Remasker", "sceneName": "Ruins2_Watcher_Room" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Secret Mask", "sceneName": "Ruins2_Watcher_Room" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Angry Buzzer (1)", "sceneName": "Crossroads_03" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Angry Buzzer (2)", "sceneName": "Crossroads_03" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Angry Buzzer", "sceneName": "Crossroads_03" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Spitting Zombie (1)", "sceneName": "Crossroads_15" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Spitting Zombie", "sceneName": "Crossroads_15" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Angry Buzzer", "sceneName": "Crossroads_04" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Bursting Zombie (1)", "sceneName": "Crossroads_04" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Spitting Zombie (1)", "sceneName": "Crossroads_04" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Bursting Zombie", "sceneName": "Crossroads_04" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Spitting Zombie", "sceneName": "Crossroads_04" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Zombie Fungus A", "sceneName": "Fungus2_18" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Mantis Lever", "sceneName": "Fungus2_18" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Grub Bottle", "sceneName": "Fungus2_18" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Fungus Flyer", "sceneName": "Fungus2_18" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Shiny", "sceneName": "Fungus2_18" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Grub Bottle", "sceneName": "Fungus2_20" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Breakable Wall Waterways", "sceneName": "Fungus2_20" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Secret Mask (1)", "sceneName": "Fungus2_20" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "One Way Wall", "sceneName": "Fungus2_20" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Secret Mask", "sceneName": "Fungus2_20" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Shiny Item Stand", "sceneName": "Fungus2_20" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Mushroom Roller (3)", "sceneName": "Fungus2_28" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Mushroom Roller (1)", "sceneName": "Fungus2_28" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Mushroom Roller", "sceneName": "Fungus2_28" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Mushroom Roller (2)", "sceneName": "Fungus2_28" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Mushroom Roller", "sceneName": "Fungus2_23" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Mushroom Roller (1)", "sceneName": "Fungus2_23" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Secret Mask", "sceneName": "Fungus2_23" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Collapser Small", "sceneName": "Fungus2_23" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Shiny Item Stand", "sceneName": "Fungus2_23" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Mantis", "sceneName": "Fungus2_31" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Mantis Chest (1)", "sceneName": "Fungus2_31" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Mantis Chest (2)", "sceneName": "Fungus2_31" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Remasker (2)", "sceneName": "Fungus2_31" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Inverse Remasker", "sceneName": "Fungus2_31" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Mantis Chest", "sceneName": "Fungus2_31" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Remasker (1)", "sceneName": "Fungus2_31" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Shiny Item", "sceneName": "Fungus2_31" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Shiny Item Charm", "sceneName": "Fungus2_31" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Heart Piece", "sceneName": "Fungus2_25" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Collapser Small (1)", "sceneName": "Fungus2_25" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Collapser Small", "sceneName": "Fungus2_25" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Collapser Small (1)", "sceneName": "Deepnest_16" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Collapser Small", "sceneName": "Deepnest_16" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Secret Mask bot left", "sceneName": "Deepnest_16" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Secret Mask (1)", "sceneName": "Deepnest_16" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Secret Mask", "sceneName": "Deepnest_16" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Secret Mask (3)", "sceneName": "Deepnest_16" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Inverse Remasker", "sceneName": "Deepnest_16" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Shiny Item", "sceneName": "Deepnest_16" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Secret Mask", "sceneName": "Deepnest_01b" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Inverse Remasker", "sceneName": "Deepnest_01b" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Inverse Remasker (1)", "sceneName": "Deepnest_01b" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "One Way Wall", "sceneName": "Deepnest_01b" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Collapser Small top", "sceneName": "Deepnest_30" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Mimic Spider Fake4", "sceneName": "Deepnest_30" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Collapser Small (3)", "sceneName": "Deepnest_30" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Secret Mask (1)", "sceneName": "Deepnest_30" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Collapser Small", "sceneName": "Deepnest_30" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Collapser Small (2)", "sceneName": "Deepnest_30" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Collapser Small (1)", "sceneName": "Deepnest_30" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Secret Mask", "sceneName": "Deepnest_30" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Secret Mask (3)", "sceneName": "Deepnest_30" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Collapser Small", "sceneName": "Deepnest_03" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Breakable Wall", "sceneName": "Deepnest_03" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Secret Mask (1)", "sceneName": "Deepnest_03" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Grub Bottle", "sceneName": "Deepnest_03" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Zombie Hornhead Sp (4)", "sceneName": "Deepnest_03" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Zombie Runner Sp (5)", "sceneName": "Deepnest_03" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Zombie Hornhead Sp (3)", "sceneName": "Deepnest_03" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Zombie Hornhead Sp (2)", "sceneName": "Deepnest_03" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Zombie Hornhead Sp", "sceneName": "Deepnest_03" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Zombie Runner Sp", "sceneName": "Deepnest_03" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Zombie Runner Sp (2)", "sceneName": "Deepnest_03" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Secret Mask", "sceneName": "Deepnest_34" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Zombie Hornhead Sp", "sceneName": "Deepnest_34" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Zombie Runner Sp", "sceneName": "Deepnest_34" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Zombie Runner Sp (1)", "sceneName": "Deepnest_34" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Slash Spider", "sceneName": "Deepnest_34" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Breakable Wall", "sceneName": "Deepnest_39" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Dream Plant", "sceneName": "Deepnest_39" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (14)", "sceneName": "Deepnest_39" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (42)", "sceneName": "Deepnest_39" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (18)", "sceneName": "Deepnest_39" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (36)", "sceneName": "Deepnest_39" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb", "sceneName": "Deepnest_39" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (27)", "sceneName": "Deepnest_39" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (44)", "sceneName": "Deepnest_39" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (12)", "sceneName": "Deepnest_39" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (15)", "sceneName": "Deepnest_39" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (13)", "sceneName": "Deepnest_39" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Remasker", "sceneName": "Deepnest_39" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Collapser Small (4)", "sceneName": "Deepnest_39" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Dream Plant Orb (25)", "sceneName": "Deepnest_39" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (23)", "sceneName": "Deepnest_39" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (40)", "sceneName": "Deepnest_39" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (20)", "sceneName": "Deepnest_39" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Collapser Small (3)", "sceneName": "Deepnest_39" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Dream Plant Orb (6)", "sceneName": "Deepnest_39" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (24)", "sceneName": "Deepnest_39" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (10)", "sceneName": "Deepnest_39" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (31)", "sceneName": "Deepnest_39" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Secret Mask (1)", "sceneName": "Deepnest_39" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Dream Plant Orb (34)", "sceneName": "Deepnest_39" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (9)", "sceneName": "Deepnest_39" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Collapser Small (6)", "sceneName": "Deepnest_39" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Dream Plant Orb (17)", "sceneName": "Deepnest_39" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Collapser Small", "sceneName": "Deepnest_39" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Secret Mask", "sceneName": "Deepnest_39" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Dream Plant Orb (35)", "sceneName": "Deepnest_39" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (43)", "sceneName": "Deepnest_39" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (21)", "sceneName": "Deepnest_39" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Collapser Small (2)", "sceneName": "Deepnest_39" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Dream Plant Orb (28)", "sceneName": "Deepnest_39" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (38)", "sceneName": "Deepnest_39" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (3)", "sceneName": "Deepnest_39" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (11)", "sceneName": "Deepnest_39" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (29)", "sceneName": "Deepnest_39" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "One Way Wall", "sceneName": "Deepnest_39" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Egg Sac", "sceneName": "Deepnest_39" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Dream Plant Orb (8)", "sceneName": "Deepnest_39" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (26)", "sceneName": "Deepnest_39" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (32)", "sceneName": "Deepnest_39" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (4)", "sceneName": "Deepnest_39" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (1)", "sceneName": "Deepnest_39" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Grub Bottle", "sceneName": "Deepnest_39" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Dream Plant Orb (2)", "sceneName": "Deepnest_39" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (33)", "sceneName": "Deepnest_39" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "One Way Wall (1)", "sceneName": "Deepnest_39" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Collapser Small (5)", "sceneName": "Deepnest_39" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Dream Plant Orb (39)", "sceneName": "Deepnest_39" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (41)", "sceneName": "Deepnest_39" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (22)", "sceneName": "Deepnest_39" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (5)", "sceneName": "Deepnest_39" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (19)", "sceneName": "Deepnest_39" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Collapser Small (7)", "sceneName": "Deepnest_39" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Collapser Small (1)", "sceneName": "Deepnest_39" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Dream Plant Orb (16)", "sceneName": "Deepnest_39" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (37)", "sceneName": "Deepnest_39" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (30)", "sceneName": "Deepnest_39" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (7)", "sceneName": "Deepnest_39" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Slash Spider (4)", "sceneName": "Deepnest_39" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Slash Spider (2)", "sceneName": "Deepnest_39" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Slash Spider", "sceneName": "Deepnest_39" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Slash Spider (3)", "sceneName": "Deepnest_39" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Collapser Small", "sceneName": "Deepnest_41" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "One Way Wall (2)", "sceneName": "Deepnest_41" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Secret Mask temp", "sceneName": "Deepnest_41" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Collapser Small (1)", "sceneName": "Deepnest_41" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Secret Mask (2)", "sceneName": "Deepnest_41" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "One Way Wall (1)", "sceneName": "Deepnest_41" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Breakable Wall", "sceneName": "Deepnest_41" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Collapser Small (2)", "sceneName": "Deepnest_41" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Collapser Small (4)", "sceneName": "Deepnest_41" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "One Way Wall", "sceneName": "Deepnest_41" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Slash Spider", "sceneName": "Deepnest_41" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Collapser Small (6)", "sceneName": "Deepnest_Spider_Town" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "One Way Wall", "sceneName": "Deepnest_Spider_Town" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Inverse Remasker (1)", "sceneName": "Deepnest_Spider_Town" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "one way permanent", "sceneName": "Deepnest_Spider_Town" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Collapser Small (9)", "sceneName": "Deepnest_Spider_Town" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Grub Bottle", "sceneName": "Deepnest_Spider_Town" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Inverse Remasker", "sceneName": "Deepnest_Spider_Town" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Collapser Small (8)", "sceneName": "Deepnest_Spider_Town" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Remasker (1)", "sceneName": "Deepnest_Spider_Town" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Inverse Remasker (2)", "sceneName": "Deepnest_Spider_Town" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Collapser Small (10)", "sceneName": "Deepnest_Spider_Town" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Secret Sound Region", "sceneName": "Deepnest_Spider_Town" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Remasker (3)", "sceneName": "Deepnest_Spider_Town" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Remasker", "sceneName": "Deepnest_Spider_Town" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Egg Sac", "sceneName": "Deepnest_Spider_Town" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Collapser Small (7)", "sceneName": "Deepnest_Spider_Town" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Remasker bar", "sceneName": "Deepnest_Spider_Town" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Collapser Small (11)", "sceneName": "Deepnest_Spider_Town" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Collapser Small (12)", "sceneName": "Deepnest_Spider_Town" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "hack jump secret remask", "sceneName": "Deepnest_Spider_Town" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Collapser Small (4)", "sceneName": "Deepnest_Spider_Town" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Remasker (2)", "sceneName": "Deepnest_Spider_Town" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Collapser Small (5)", "sceneName": "Deepnest_Spider_Town" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Remasker (4)", "sceneName": "Deepnest_Spider_Town" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Collapser Small (3)", "sceneName": "Deepnest_Spider_Town" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "remask_store_room", "sceneName": "Deepnest_Spider_Town" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Shiny Item", "sceneName": "Deepnest_Spider_Town" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Slash Spider (3)", "sceneName": "Deepnest_Spider_Town" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Slash Spider", "sceneName": "Deepnest_Spider_Town" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Slash Spider (4)", "sceneName": "Deepnest_Spider_Town" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Slash Spider (1)", "sceneName": "Deepnest_Spider_Town" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Slash Spider (2)", "sceneName": "Deepnest_Spider_Town" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Shiny Item(Clone)", "sceneName": "Deepnest_Spider_Town" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Heart Piece", "sceneName": "Room_Bretta" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Remasker", "sceneName": "Room_temple" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Remasker (1)", "sceneName": "Cliffs_02" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Remasker", "sceneName": "Cliffs_02" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "One Way Wall", "sceneName": "Cliffs_02" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Remasker (2)", "sceneName": "Cliffs_02" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Secret Mask (2)", "sceneName": "Cliffs_02" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Inverse Remasker", "sceneName": "Cliffs_02" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Zombie Hornhead", "sceneName": "Cliffs_02" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Zombie Barger", "sceneName": "Cliffs_02" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Remasker (1)", "sceneName": "Room_nailmaster" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Remasker (2)", "sceneName": "Room_nailmaster" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Remasker", "sceneName": "Room_nailmaster" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Dream Plant Orb (56)", "sceneName": "Cliffs_01" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (19)", "sceneName": "Cliffs_01" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (22)", "sceneName": "Cliffs_01" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (58)", "sceneName": "Cliffs_01" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (48)", "sceneName": "Cliffs_01" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (17)", "sceneName": "Cliffs_01" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (27)", "sceneName": "Cliffs_01" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (57)", "sceneName": "Cliffs_01" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (33)", "sceneName": "Cliffs_01" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (15)", "sceneName": "Cliffs_01" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (24)", "sceneName": "Cliffs_01" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (59)", "sceneName": "Cliffs_01" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (14)", "sceneName": "Cliffs_01" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Breakable Wall", "sceneName": "Cliffs_01" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Dream Plant Orb (31)", "sceneName": "Cliffs_01" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (8)", "sceneName": "Cliffs_01" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (9)", "sceneName": "Cliffs_01" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "One Way Wall", "sceneName": "Cliffs_01" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Dream Plant Orb (66)", "sceneName": "Cliffs_01" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (11)", "sceneName": "Cliffs_01" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (38)", "sceneName": "Cliffs_01" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (63)", "sceneName": "Cliffs_01" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (28)", "sceneName": "Cliffs_01" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (42)", "sceneName": "Cliffs_01" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Shiny Item", "sceneName": "Cliffs_01" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (30)", "sceneName": "Cliffs_01" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (13)", "sceneName": "Cliffs_01" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (36)", "sceneName": "Cliffs_01" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (41)", "sceneName": "Cliffs_01" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (67)", "sceneName": "Cliffs_01" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (18)", "sceneName": "Cliffs_01" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (55)", "sceneName": "Cliffs_01" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (29)", "sceneName": "Cliffs_01" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (26)", "sceneName": "Cliffs_01" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (2)", "sceneName": "Cliffs_01" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (4)", "sceneName": "Cliffs_01" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (25)", "sceneName": "Cliffs_01" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (47)", "sceneName": "Cliffs_01" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (61)", "sceneName": "Cliffs_01" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (52)", "sceneName": "Cliffs_01" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (53)", "sceneName": "Cliffs_01" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant", "sceneName": "Cliffs_01" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (6)", "sceneName": "Cliffs_01" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Breakable Wall grimm", "sceneName": "Cliffs_01" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Dream Plant Orb (45)", "sceneName": "Cliffs_01" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (16)", "sceneName": "Cliffs_01" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (64)", "sceneName": "Cliffs_01" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (39)", "sceneName": "Cliffs_01" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (35)", "sceneName": "Cliffs_01" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Shiny Item (1)", "sceneName": "Cliffs_01" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (3)", "sceneName": "Cliffs_01" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (50)", "sceneName": "Cliffs_01" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Shiny", "sceneName": "Cliffs_01" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Secret Sound Region", "sceneName": "Cliffs_06" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Inverse Remasker", "sceneName": "Cliffs_06" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Quake Floor", "sceneName": "Cliffs_04" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Secret Mask", "sceneName": "Cliffs_04" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Zombie Hornhead", "sceneName": "Cliffs_04" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Zombie Leaper (1)", "sceneName": "Cliffs_04" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Zombie Leaper (2)", "sceneName": "Cliffs_04" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Zombie Leaper", "sceneName": "Cliffs_04" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Zombie Barger", "sceneName": "Cliffs_04" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Shiny Item Stand", "sceneName": "Cliffs_05" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Ghost Activator", "sceneName": "Cliffs_05" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Remasker", "sceneName": "Cliffs_05" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Ghost NPC Joni", "sceneName": "Cliffs_05" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Remasker", "sceneName": "Grimm_Main_Tent" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Secret Mask", "sceneName": "Grimm_Main_Tent" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Inverse Remasker", "sceneName": "Fungus1_26" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Secret Mask", "sceneName": "Fungus1_26" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Moss Knight", "sceneName": "Fungus1_26" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "secret sound", "sceneName": "Fungus1_26" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "One Way Wall (1)", "sceneName": "Fungus1_Slug" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Secret Mask", "sceneName": "Fungus1_Slug" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Secret Mask (1)", "sceneName": "Fungus1_Slug" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "One Way Wall", "sceneName": "Fungus1_Slug" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Secret Mask (2)", "sceneName": "Fungus1_Slug" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Shiny Item", "sceneName": "Fungus1_Slug" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Vine Platform", "sceneName": "Fungus1_09" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Shiny Item", "sceneName": "Fungus1_15" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Shiny Item", "sceneName": "Fungus1_10" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Moss Charger (1)", "sceneName": "Fungus1_10" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Moss Charger 1 (1)", "sceneName": "Fungus1_10" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Moss Charger 1 (2)", "sceneName": "Fungus1_10" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Moss Charger", "sceneName": "Fungus1_10" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Mossman_Shaker", "sceneName": "Fungus1_05" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Mossman_Runner", "sceneName": "Fungus1_05" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Mossman_Shaker (1)", "sceneName": "Fungus1_05" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Vine Platform", "sceneName": "Fungus1_14" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Shiny Item", "sceneName": "Fungus1_14" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Plant Trap", "sceneName": "Fungus1_19" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Secret Mask", "sceneName": "Fungus1_19" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Mossman_Shaker", "sceneName": "Fungus1_19" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Shiny Item", "sceneName": "Fungus1_11" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Secret Mask", "sceneName": "Fungus1_11" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Acid Walker", "sceneName": "Fungus1_11" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Zombie Runner", "sceneName": "Fungus1_34" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Zombie Hornhead", "sceneName": "Fungus1_34" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Plant Trap", "sceneName": "Fungus1_34" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Zombie Leaper", "sceneName": "Fungus1_34" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Remasker", "sceneName": "Fungus1_36" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Heart Piece", "sceneName": "Fungus1_36" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Vine Platform (3)", "sceneName": "Fungus1_07" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Remasker", "sceneName": "Fungus1_07" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Vine Platform (2)", "sceneName": "Fungus1_07" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Grub Bottle", "sceneName": "Fungus1_07" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Vine Platform (1)", "sceneName": "Fungus1_07" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Vine Platform", "sceneName": "Fungus1_07" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Mossman_Runner", "sceneName": "Fungus1_07" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Mossman_Runner (1)", "sceneName": "Fungus1_07" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Secret Mask", "sceneName": "Fungus1_08" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Break Floor 1", "sceneName": "Fungus1_08" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Break Floor 1 (1)", "sceneName": "Fungus1_08" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Shiny Item", "sceneName": "Fungus1_08" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Vine Platform (1)", "sceneName": "Fungus1_06" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Vine Platform (4)", "sceneName": "Fungus1_06" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Vine Platform (3)", "sceneName": "Fungus1_06" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Secret sounder", "sceneName": "Fungus1_06" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Vine Platform (5)", "sceneName": "Fungus1_06" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Vine Platform (2)", "sceneName": "Fungus1_06" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Secret Mask", "sceneName": "Fungus1_06" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Vine Platform", "sceneName": "Fungus1_06" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Vine Platform (6)", "sceneName": "Fungus1_06" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Grub Bottle", "sceneName": "Fungus1_06" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Mossman_Shaker (1)", "sceneName": "Fungus1_06" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Mossman_Shaker", "sceneName": "Fungus1_06" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Shiny", "sceneName": "Fungus1_06" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Angry Buzzer (2)", "sceneName": "Crossroads_11_alt" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Angry Buzzer (1)", "sceneName": "Crossroads_11_alt" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Shiny Item Rancid Egg", "sceneName": "Crossroads_38" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (27)", "sceneName": "RestingGrounds_08" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Ghost kcin", "sceneName": "RestingGrounds_08" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Ghost atra", "sceneName": "RestingGrounds_08" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Ghost wyatt", "sceneName": "RestingGrounds_08" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Dream Plant Orb (2)", "sceneName": "RestingGrounds_08" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Shiny Item", "sceneName": "RestingGrounds_08" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (31)", "sceneName": "RestingGrounds_08" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Ghost chagax", "sceneName": "RestingGrounds_08" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Ghost boss", "sceneName": "RestingGrounds_08" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Dream Plant Orb (4)", "sceneName": "RestingGrounds_08" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (25)", "sceneName": "RestingGrounds_08" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (13)", "sceneName": "RestingGrounds_08" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (10)", "sceneName": "RestingGrounds_08" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Secret Sound Region", "sceneName": "RestingGrounds_08" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Dream Plant", "sceneName": "RestingGrounds_08" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (15)", "sceneName": "RestingGrounds_08" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Ghost hex", "sceneName": "RestingGrounds_08" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Dream Plant Orb (1)", "sceneName": "RestingGrounds_08" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (18)", "sceneName": "RestingGrounds_08" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (11)", "sceneName": "RestingGrounds_08" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Ghost garro", "sceneName": "RestingGrounds_08" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Ghost perpetos", "sceneName": "RestingGrounds_08" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Ghost molten", "sceneName": "RestingGrounds_08" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Dream Plant Orb (5)", "sceneName": "RestingGrounds_08" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (28)", "sceneName": "RestingGrounds_08" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Ghost revek", "sceneName": "RestingGrounds_08" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Dream Plant Orb (30)", "sceneName": "RestingGrounds_08" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (23)", "sceneName": "RestingGrounds_08" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb", "sceneName": "RestingGrounds_08" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (8)", "sceneName": "RestingGrounds_08" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (17)", "sceneName": "RestingGrounds_08" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Ghost NPC 100 nail", "sceneName": "RestingGrounds_08" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Dream Plant Orb (22)", "sceneName": "RestingGrounds_08" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Ghost caspian", "sceneName": "RestingGrounds_08" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Ghost waldie", "sceneName": "RestingGrounds_08" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Dream Plant Orb (29)", "sceneName": "RestingGrounds_08" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (7)", "sceneName": "RestingGrounds_08" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (3)", "sceneName": "RestingGrounds_08" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (24)", "sceneName": "RestingGrounds_08" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (21)", "sceneName": "RestingGrounds_08" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (16)", "sceneName": "RestingGrounds_08" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (9)", "sceneName": "RestingGrounds_08" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (19)", "sceneName": "RestingGrounds_08" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Ghost milly", "sceneName": "RestingGrounds_08" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Ghost magnus", "sceneName": "RestingGrounds_08" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Ghost grohac", "sceneName": "RestingGrounds_08" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Dream Plant Orb (6)", "sceneName": "RestingGrounds_08" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Ghost wayner", "sceneName": "RestingGrounds_08" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Dream Plant Orb (26)", "sceneName": "RestingGrounds_08" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (12)", "sceneName": "RestingGrounds_08" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "karina", "sceneName": "RestingGrounds_08" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Dream Plant Orb (33)", "sceneName": "RestingGrounds_08" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (32)", "sceneName": "RestingGrounds_08" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Ghost thistlewind", "sceneName": "RestingGrounds_08" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Dream Plant Orb (20)", "sceneName": "RestingGrounds_08" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (14)", "sceneName": "RestingGrounds_08" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Shiny Item", "sceneName": "Abyss_02" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Ruins Flying Sentry Javelin (1)", "sceneName": "Abyss_02" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Ruins Flying Sentry Javelin", "sceneName": "Abyss_02" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Ruins Sentry 1", "sceneName": "Abyss_02" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Ruins Flying Sentry", "sceneName": "Abyss_02" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Ruins Flying Sentry (1)", "sceneName": "Abyss_02" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Zombie Hornhead", "sceneName": "Abyss_02" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Zombie Barger (1)", "sceneName": "Abyss_02" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Zombie Barger", "sceneName": "Abyss_02" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "wish_secret sound", "sceneName": "Abyss_04" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "wish inverse remask", "sceneName": "Abyss_04" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "wish_remask", "sceneName": "Abyss_04" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Shiny", "sceneName": "Abyss_04" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Toll Machine Bench", "sceneName": "Abyss_18" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Grub Bottle", "sceneName": "Abyss_19" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Secret Mask", "sceneName": "Abyss_19" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "One Way Wall", "sceneName": "Abyss_19" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Camera Locks Boss", "sceneName": "Abyss_19" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Mawlek Turret", "sceneName": "Abyss_20" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Remasker", "sceneName": "Abyss_20" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Shiny Item Stand", "sceneName": "Abyss_20" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Secret Sound Region", "sceneName": "Abyss_20" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Remasker (1)", "sceneName": "Abyss_20" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Remasker (2)", "sceneName": "Abyss_20" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Mawlek Turret Ceiling (1)", "sceneName": "Abyss_20" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Mawlek Turret Ceiling", "sceneName": "Abyss_20" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Mawlek Turret (1)", "sceneName": "Abyss_20" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Mawlek Turret (2)", "sceneName": "Abyss_20" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Mawlek Turret (3)", "sceneName": "Abyss_20" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Breakable Wall", "sceneName": "Abyss_05" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Battle Scene v2", "sceneName": "Ruins1_31" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Crystal Flyer (1)", "sceneName": "Mines_16" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Crystal Flyer", "sceneName": "Mines_16" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Grub Mimic", "sceneName": "Mines_16" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Grub Bottle", "sceneName": "Mines_16" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Breakable Wall", "sceneName": "Mines_16" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Grub Mimic Bottle", "sceneName": "Mines_16" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Zombie Beam Miner Rematch", "sceneName": "Mines_32" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Battle Scene", "sceneName": "Mines_32" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Heart Piece", "sceneName": "Mines_32" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Collapser Small", "sceneName": "Mines_06" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Remasker", "sceneName": "Mines_06" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Shiny Item Stand", "sceneName": "Mines_36" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Grave Zombie (2)", "sceneName": "RestingGrounds_10" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Ceiling Dropper", "sceneName": "RestingGrounds_10" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Ceiling Dropper (1)", "sceneName": "RestingGrounds_10" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Ceiling Dropper (3)", "sceneName": "RestingGrounds_10" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Secret Mask", "sceneName": "RestingGrounds_10" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Secret Sound Region", "sceneName": "RestingGrounds_10" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Shiny Item Stand", "sceneName": "RestingGrounds_10" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Breakable Wall (5)", "sceneName": "RestingGrounds_10" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Collapser Small (6)", "sceneName": "RestingGrounds_10" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Breakable Wall", "sceneName": "RestingGrounds_10" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Chest", "sceneName": "RestingGrounds_10" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "One Way Wall", "sceneName": "RestingGrounds_10" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Breakable Wall (6)", "sceneName": "RestingGrounds_10" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Collapser Small (3)", "sceneName": "RestingGrounds_10" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Breakable Wall (4)", "sceneName": "RestingGrounds_10" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Collapser Small (5)", "sceneName": "RestingGrounds_10" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Collapser Small", "sceneName": "RestingGrounds_10" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Collapser Small (7)", "sceneName": "RestingGrounds_10" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Breakable Wall (3)", "sceneName": "RestingGrounds_10" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Collapser Small (4)", "sceneName": "RestingGrounds_10" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Breakable Wall (8)", "sceneName": "RestingGrounds_10" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Collapser Small (1)", "sceneName": "RestingGrounds_10" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Collapser Small (2)", "sceneName": "RestingGrounds_10" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Shiny Item", "sceneName": "RestingGrounds_10" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Grub Bottle", "sceneName": "RestingGrounds_10" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Secret Mask (1)", "sceneName": "RestingGrounds_10" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Breakable Wall (2)", "sceneName": "RestingGrounds_10" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Inverse Remasker", "sceneName": "RestingGrounds_10" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Shiny Item (1)", "sceneName": "RestingGrounds_10" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Breakable Wall (7)", "sceneName": "RestingGrounds_10" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Grave Zombie (4)", "sceneName": "RestingGrounds_10" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Grave Zombie", "sceneName": "RestingGrounds_10" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Grave Zombie (1)", "sceneName": "RestingGrounds_10" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Ceiling Dropper (2)", "sceneName": "RestingGrounds_10" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Death Respawn Trigger", "sceneName": "RestingGrounds_12" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Great Shield Zombie", "sceneName": "RestingGrounds_06" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Ruins Flying Sentry", "sceneName": "RestingGrounds_06" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Gate Switch", "sceneName": "RestingGrounds_06" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Breakable Wall", "sceneName": "RestingGrounds_06" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Resting Grounds Slide Floor", "sceneName": "RestingGrounds_06" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Egg Sac", "sceneName": "Crossroads_50" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Shiny Item(Clone)", "sceneName": "Crossroads_50" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Angry Buzzer (1)", "sceneName": "Crossroads_21" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Angry Buzzer", "sceneName": "Crossroads_21" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Spitting Zombie", "sceneName": "Crossroads_21" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Bursting Zombie", "sceneName": "Crossroads_21" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Hatcher", "sceneName": "Crossroads_22" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Shiny Item", "sceneName": "Crossroads_22" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Battle Scene", "sceneName": "Crossroads_22" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Battle Scene", "sceneName": "Dream_01_False_Knight" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Ceiling Dropper (1)", "sceneName": "Ruins2_07" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Grub Bottle", "sceneName": "Ruins2_07" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Ceiling Dropper (2)", "sceneName": "Ruins2_07" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Ceiling Dropper", "sceneName": "Ruins2_07" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Plank Solid 2", "sceneName": "Deepnest_East_03" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Inverse Remasker", "sceneName": "Deepnest_East_03" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Plank Solid 1", "sceneName": "Deepnest_East_03" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Plank Solid 2 (1)", "sceneName": "Deepnest_East_03" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Secret Mask", "sceneName": "Deepnest_East_03" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Shiny", "sceneName": "Deepnest_East_03" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Blow Fly (5)", "sceneName": "Deepnest_East_03" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Blow Fly (1)", "sceneName": "Deepnest_East_03" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Blow Fly (4)", "sceneName": "Deepnest_East_03" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Blow Fly (3)", "sceneName": "Deepnest_East_03" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Blow Fly (2)", "sceneName": "Deepnest_East_03" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Blow Fly", "sceneName": "Deepnest_East_03" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "One Way Wall", "sceneName": "Deepnest_East_06" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Secret Mask", "sceneName": "Deepnest_East_06" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Super Spitter", "sceneName": "Deepnest_East_06" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Super Spitter (2)", "sceneName": "Deepnest_East_06" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Super Spitter (1)", "sceneName": "Deepnest_East_06" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Giant Hopper (2)", "sceneName": "Deepnest_East_06" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Giant Hopper (1)", "sceneName": "Deepnest_East_06" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Giant Hopper", "sceneName": "Deepnest_East_06" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Secret Mask", "sceneName": "Deepnest_East_16" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Quake Floor", "sceneName": "Deepnest_East_16" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Secret Sound Region", "sceneName": "Deepnest_East_16" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Super Spitter (1)", "sceneName": "Deepnest_East_14" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Super Spitter", "sceneName": "Deepnest_East_14" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Quake Floor (2)", "sceneName": "Deepnest_East_14" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Egg Sac", "sceneName": "Deepnest_East_14" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Quake Floor (1)", "sceneName": "Deepnest_East_14" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Grub Bottle", "sceneName": "Deepnest_East_14" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Breakable Wall", "sceneName": "Deepnest_East_14" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Hopper Spawn", "sceneName": "Deepnest_East_14" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Secret Mask", "sceneName": "Deepnest_East_14" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Giant Hopper", "sceneName": "Deepnest_East_14" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Giant Hopper", "sceneName": "Deepnest_East_14b" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Super Spitter (2)", "sceneName": "Deepnest_East_14b" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Super Spitter (3)", "sceneName": "Deepnest_East_14b" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Super Spitter (4)", "sceneName": "Deepnest_East_14b" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Super Spitter (5)", "sceneName": "Deepnest_East_14b" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "remask corridor", "sceneName": "Deepnest_East_14b" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Secret Mask (1)", "sceneName": "Deepnest_East_14b" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Remasker", "sceneName": "Deepnest_East_14b" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "secret sound", "sceneName": "Deepnest_East_14b" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Inverse Remasker", "sceneName": "Deepnest_East_14b" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Shiny Item", "sceneName": "Deepnest_East_14b" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Shiny Item", "sceneName": "Deepnest_East_18" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Quake Floor (2)", "sceneName": "Deepnest_East_18" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Quake Floor (1)", "sceneName": "Deepnest_East_18" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Secret Mask", "sceneName": "Deepnest_East_18" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Remasker", "sceneName": "Deepnest_East_11" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Breakable Wall", "sceneName": "Deepnest_East_11" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Breakable Wall top", "sceneName": "Deepnest_East_11" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Grub Bottle", "sceneName": "Deepnest_East_11" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Super Spitter (4)", "sceneName": "Deepnest_East_11" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Super Spitter (1)", "sceneName": "Deepnest_East_11" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Super Spitter", "sceneName": "Deepnest_East_11" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Super Spitter (3)", "sceneName": "Deepnest_East_11" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Shiny Item", "sceneName": "Deepnest_East_13" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Hornet Encounter Outskirts", "sceneName": "Deepnest_East_12" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Shiny Item Stand", "sceneName": "Room_Wyrm" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Breakable Wall Waterways", "sceneName": "Deepnest_East_04" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Remasker", "sceneName": "Deepnest_East_04" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Secret Sound Region", "sceneName": "Deepnest_East_04" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Super Spitter (1)", "sceneName": "Deepnest_East_04" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Super Spitter (2)", "sceneName": "Deepnest_East_04" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Super Spitter (4)", "sceneName": "Deepnest_East_04" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Super Spitter (3)", "sceneName": "Deepnest_East_04" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Super Spitter", "sceneName": "Deepnest_East_04" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Blow Fly (3)", "sceneName": "Deepnest_East_04" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Blow Fly", "sceneName": "Deepnest_East_04" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Blow Fly (2)", "sceneName": "Deepnest_East_04" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Remasker", "sceneName": "Abyss_06_Core" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Remasker (1)", "sceneName": "Abyss_06_Core" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Remasker", "sceneName": "Abyss_09" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Ruins Soul Vial", "sceneName": "Abyss_Lighthouse_room" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Ruins Vial Empty", "sceneName": "Abyss_Lighthouse_room" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Ruins Lever", "sceneName": "Abyss_Lighthouse_room" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Secret Sound Region", "sceneName": "Abyss_10" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Shiny Item", "sceneName": "Abyss_10" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Bursting Bouncer (1)", "sceneName": "Crossroads_25" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Reminder Look Down", "sceneName": "Crossroads_36" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Secret Mask", "sceneName": "Crossroads_36" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Force Hard Landing", "sceneName": "Crossroads_36" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Collapser Small 1", "sceneName": "Crossroads_36" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Collapser Small", "sceneName": "Crossroads_36" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Mask Bottom", "sceneName": "Crossroads_36" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Mawlek Body", "sceneName": "Crossroads_09" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Break Floor 1", "sceneName": "Crossroads_09" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Battle Scene", "sceneName": "Crossroads_09" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Heart Piece", "sceneName": "Crossroads_09" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Jellyfish (3)", "sceneName": "Fungus3_02" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Jellyfish (4)", "sceneName": "Fungus3_02" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Jellyfish (1)", "sceneName": "Fungus3_02" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Jellyfish (2)", "sceneName": "Fungus3_02" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Jellyfish", "sceneName": "Fungus3_02" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Garden Zombie", "sceneName": "Fungus3_34" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Egg Sac", "sceneName": "Fungus3_34" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Garden Zombie (1)", "sceneName": "Fungus3_34" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Garden Zombie (2)", "sceneName": "Fungus3_34" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Ruins Lever", "sceneName": "Fungus3_44" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "One Way Wall", "sceneName": "Fungus3_44" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Secret Mask", "sceneName": "Fungus3_44" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "One Way Wall", "sceneName": "Room_Fungus_Shaman" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Battle Scene", "sceneName": "Room_Fungus_Shaman" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Secret Mask", "sceneName": "Room_Fungus_Shaman" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Bursting Zombie", "sceneName": "Crossroads_39" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Bursting Zombie (1)", "sceneName": "Crossroads_39" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Spitting Zombie", "sceneName": "Crossroads_39" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Angry Buzzer", "sceneName": "Crossroads_14" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Angry Buzzer (2)", "sceneName": "Crossroads_16" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Angry Buzzer (1)", "sceneName": "Crossroads_16" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Angry Buzzer", "sceneName": "Crossroads_16" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Angry Buzzer", "sceneName": "Crossroads_42" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Angry Buzzer (1)", "sceneName": "Crossroads_42" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Angry Buzzer (2)", "sceneName": "Crossroads_42" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Angry Buzzer", "sceneName": "Crossroads_13" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Angry Buzzer (1)", "sceneName": "Crossroads_13" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Bursting Zombie", "sceneName": "Crossroads_13" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Blocker 2", "sceneName": "Fungus1_28" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Blocker 1", "sceneName": "Fungus1_28" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "One Way Wall", "sceneName": "Fungus1_28" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Grub Bottle", "sceneName": "Fungus1_28" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Chest", "sceneName": "Fungus1_28" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Shiny Item", "sceneName": "Fungus1_28" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "mask_01", "sceneName": "Fungus1_28" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Secret Mask", "sceneName": "Fungus1_28" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "One Way Wall (1)", "sceneName": "Fungus1_28" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Moss Knight C", "sceneName": "Fungus1_32" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Shiny Item Relic2", "sceneName": "Crossroads_38" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Fungus Break Floor", "sceneName": "Deepnest_01" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Secret Mask", "sceneName": "Deepnest_01" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Breakable Wall", "sceneName": "Deepnest_01" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "One Way Wall", "sceneName": "Deepnest_01" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Battle Scene", "sceneName": "Fungus3_39" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "One Way Wall", "sceneName": "Fungus3_39" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Shiny Item Stand", "sceneName": "Fungus3_39" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Mantis Heavy", "sceneName": "Fungus3_39" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Mantis Heavy (1)", "sceneName": "Fungus3_39" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Acid Walker", "sceneName": "Fungus3_39" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Inverse Remasker", "sceneName": "Deepnest_14" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Collapser Small", "sceneName": "Deepnest_14" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Inverse Remasker (1)", "sceneName": "Deepnest_14" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Breakable Wall", "sceneName": "Deepnest_31" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Secret Mask", "sceneName": "Deepnest_31" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Mimic Spider Fake1", "sceneName": "Deepnest_31" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "One Way Wall", "sceneName": "Deepnest_31" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Secret Mask (1)", "sceneName": "Deepnest_31" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Mimic Spider Fake3", "sceneName": "Deepnest_31" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Grub Bottle", "sceneName": "Deepnest_31" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Secret Mask (2)", "sceneName": "Deepnest_31" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Breakable Wall (1)", "sceneName": "Deepnest_31" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Mimic Spider Fake2", "sceneName": "Deepnest_31" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Shiny Item Stand", "sceneName": "Deepnest_32" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Battle Scene", "sceneName": "Deepnest_32" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Zombie Hornhead Sp (2)", "sceneName": "Deepnest_33" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Zombie Runner Sp (1)", "sceneName": "Deepnest_33" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Collapser Small", "sceneName": "Deepnest_33" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Secret Mask", "sceneName": "Deepnest_33" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Battle Scene v2", "sceneName": "Deepnest_33" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Shiny Item", "sceneName": "Deepnest_33" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Zombie Hornhead Sp (1)", "sceneName": "Deepnest_33" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Zombie Hornhead Sp", "sceneName": "Deepnest_33" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Centipede Hatcher (2)", "sceneName": "Deepnest_26" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Centipede Hatcher", "sceneName": "Deepnest_26" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Health Cocoon", "sceneName": "Deepnest_26" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Secret Mask (2)", "sceneName": "Deepnest_26" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Secret Mask (1)", "sceneName": "Deepnest_26" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Ruins Lever", "sceneName": "Deepnest_26" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Inverse Remasker", "sceneName": "Deepnest_26" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Secret Sound Region", "sceneName": "Deepnest_26" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Centipede Hatcher (9)", "sceneName": "Deepnest_26b" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Centipede Hatcher (7)", "sceneName": "Deepnest_26b" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Centipede Hatcher (4)", "sceneName": "Deepnest_26b" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Centipede Hatcher (5)", "sceneName": "Deepnest_26b" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Ruins Lever Remade", "sceneName": "Deepnest_26b" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "tram_inverse mask", "sceneName": "Deepnest_26b" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Shiny Item", "sceneName": "Deepnest_26" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "mask tram left front", "sceneName": "Deepnest_26b" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Zombie Hornhead Sp (2)", "sceneName": "Deepnest_35" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Zombie Hornhead Sp", "sceneName": "Deepnest_35" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Zombie Runner Sp", "sceneName": "Deepnest_35" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Health Cocoon", "sceneName": "Deepnest_40" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Chest", "sceneName": "Deepnest_45_v02" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Shiny Item (1)", "sceneName": "Deepnest_45_v02" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Breakable Wall", "sceneName": "Deepnest_45_v02" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Inverse Remasker (1)", "sceneName": "Deepnest_45_v02" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Breakable Wall Waterways", "sceneName": "Deepnest_45_v02" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Last Weaver", "sceneName": "Deepnest_45_v02" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Inverse Remasker", "sceneName": "Deepnest_45_v02" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Shiny Item", "sceneName": "Deepnest_45_v02" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Secret Mask", "sceneName": "Deepnest_45_v02" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Collapser Small", "sceneName": "Deepnest_45_v02" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "One Way Wall", "sceneName": "Deepnest_44" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Inverse Remasker", "sceneName": "Deepnest_44" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Shiny Item Stand", "sceneName": "Deepnest_44" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Secret Sound Region", "sceneName": "Deepnest_44" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Remasker", "sceneName": "Deepnest_44" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Secret Mask", "sceneName": "Deepnest_44" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "gramaphone", "sceneName": "Room_Tram" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "gramaphone (1)", "sceneName": "Room_Tram" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Secret Mask", "sceneName": "Abyss_08" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Shiny Item (1)", "sceneName": "Abyss_08" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "One Way Wall", "sceneName": "Abyss_08" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Shiny Item", "sceneName": "Abyss_08" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Vessel Fragment", "sceneName": "Abyss_04" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Collapser Small", "sceneName": "Deepnest_38" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Vessel Fragment", "sceneName": "Deepnest_38" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Secret Mask", "sceneName": "Deepnest_38" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Breakable Wall", "sceneName": "Deepnest_38" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Shiny Item(Clone)", "sceneName": "Fungus3_34" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Mantis Heavy Flyer", "sceneName": "Fungus3_04" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Ruins Lever", "sceneName": "Fungus3_04" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Garden Slide Floor", "sceneName": "Fungus3_04" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Mantis Heavy Flyer (1)", "sceneName": "Fungus3_04" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Battle Scene", "sceneName": "Fungus3_05" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "One Way Wall", "sceneName": "Fungus3_05" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Secret Mask (1)", "sceneName": "Fungus3_05" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Gate Switch", "sceneName": "Fungus3_05" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Plant Trap", "sceneName": "Fungus3_05" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Plant Trap (2)", "sceneName": "Fungus3_05" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Mantis Heavy Flyer", "sceneName": "Fungus3_05" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Plant Trap (1)", "sceneName": "Fungus3_05" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Collapser Small", "sceneName": "Fungus1_24" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Ghost NPC", "sceneName": "Fungus1_24" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Shiny", "sceneName": "Fungus1_24" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Dream Plant Orb (20)", "sceneName": "Fungus3_11" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (25)", "sceneName": "Fungus3_11" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (7)", "sceneName": "Fungus3_11" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (4)", "sceneName": "Fungus3_11" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (22)", "sceneName": "Fungus3_11" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (17)", "sceneName": "Fungus3_11" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (6)", "sceneName": "Fungus3_11" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (16)", "sceneName": "Fungus3_11" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant", "sceneName": "Fungus3_11" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (12)", "sceneName": "Fungus3_11" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (13)", "sceneName": "Fungus3_11" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (15)", "sceneName": "Fungus3_11" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (18)", "sceneName": "Fungus3_11" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (27)", "sceneName": "Fungus3_11" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (14)", "sceneName": "Fungus3_11" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (21)", "sceneName": "Fungus3_11" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (1)", "sceneName": "Fungus3_11" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (3)", "sceneName": "Fungus3_11" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (11)", "sceneName": "Fungus3_11" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (28)", "sceneName": "Fungus3_11" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (9)", "sceneName": "Fungus3_11" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb", "sceneName": "Fungus3_11" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (2)", "sceneName": "Fungus3_11" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (8)", "sceneName": "Fungus3_11" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (10)", "sceneName": "Fungus3_11" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (24)", "sceneName": "Fungus3_11" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (26)", "sceneName": "Fungus3_11" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (19)", "sceneName": "Fungus3_11" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (5)", "sceneName": "Fungus3_11" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (23)", "sceneName": "Fungus3_11" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Mantis Heavy Flyer (1)", "sceneName": "Fungus3_08" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Mantis Heavy Flyer (3)", "sceneName": "Fungus3_08" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Mantis Heavy Flyer", "sceneName": "Fungus3_08" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Mantis Heavy Flyer (2)", "sceneName": "Fungus3_08" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Remasker", "sceneName": "Deepnest_43" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Garden Zombie", "sceneName": "Deepnest_43" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Garden Zombie (1)", "sceneName": "Deepnest_43" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Mantis Heavy Flyer", "sceneName": "Deepnest_43" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Toll Machine Bench", "sceneName": "Fungus3_50" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "gramaphone", "sceneName": "Fungus3_50" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Garden Zombie (1)", "sceneName": "Fungus3_10" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Garden Zombie", "sceneName": "Fungus3_10" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Battle Scene", "sceneName": "Fungus3_10" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Grub Bottle", "sceneName": "Fungus3_10" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Secret Mask", "sceneName": "Fungus3_10" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Plant Trap", "sceneName": "Fungus3_48" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Plant Trap (2)", "sceneName": "Fungus3_48" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Remasker", "sceneName": "Fungus3_48" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Shiny Item", "sceneName": "Fungus3_48" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "secret sound", "sceneName": "Fungus3_48" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Grub Bottle", "sceneName": "Fungus3_48" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Plant Trap (8)", "sceneName": "Fungus3_48" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Mantis Heavy Flyer (1)", "sceneName": "Fungus3_48" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Plant Trap (5)", "sceneName": "Fungus3_48" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Plant Trap (7)", "sceneName": "Fungus3_48" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Plant Trap (4)", "sceneName": "Fungus3_48" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Mantis Heavy Flyer", "sceneName": "Fungus3_48" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Plant Trap (10)", "sceneName": "Fungus3_48" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Plant Trap (3)", "sceneName": "Fungus3_48" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Plant Trap (9)", "sceneName": "Fungus3_48" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Plant Trap (11)", "sceneName": "Fungus3_48" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Plant Trap (1)", "sceneName": "Fungus3_48" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Plant Trap (6)", "sceneName": "Fungus3_48" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "secret sound", "sceneName": "Fungus3_40" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Secret Sound Region", "sceneName": "Fungus3_40" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Gate Switch", "sceneName": "Fungus3_40" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Remasker", "sceneName": "Fungus3_40" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Mantis Heavy Flyer", "sceneName": "Fungus3_40" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Mantis Heavy Flyer (1)", "sceneName": "Fungus3_40" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Lesser Mawlek", "sceneName": "Abyss_17" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Lesser Mawlek (1)", "sceneName": "Abyss_17" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Lesser Mawlek (2)", "sceneName": "Abyss_17" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Lesser Mawlek (3)", "sceneName": "Abyss_17" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Lesser Mawlek (4)", "sceneName": "Abyss_17" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Lesser Mawlek 1", "sceneName": "Abyss_17" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Lesser Mawlek 2", "sceneName": "Abyss_17" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Ceiling Dropper (3)", "sceneName": "Abyss_17" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Lesser Mawlek (8)", "sceneName": "Abyss_17" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Ceiling Dropper (2)", "sceneName": "Abyss_17" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Ceiling Dropper (1)", "sceneName": "Abyss_17" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Ceiling Dropper", "sceneName": "Abyss_17" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Secret Mask", "sceneName": "Abyss_17" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Secret Mask (1)", "sceneName": "Abyss_17" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Grub Bottle", "sceneName": "Abyss_17" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Battle Scene Ore", "sceneName": "Abyss_17" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "One Way Wall", "sceneName": "Abyss_17" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Quake Floor", "sceneName": "Abyss_17" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Shiny Item Stand", "sceneName": "Abyss_17" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Mantis Heavy", "sceneName": "Fungus3_10" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Mantis Heavy Spawn", "sceneName": "Fungus3_39" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "ruind_dressing_light_01", "sceneName": "Fungus3_22" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Grub Bottle", "sceneName": "Fungus3_22" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Garden Zombie (2)", "sceneName": "Fungus3_22" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Garden Zombie", "sceneName": "Fungus3_22" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Garden Zombie (1)", "sceneName": "Fungus3_22" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Mantis Heavy Flyer (1)", "sceneName": "Fungus3_22" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Mantis Heavy Flyer", "sceneName": "Fungus3_22" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Battle Scene", "sceneName": "Fungus3_23" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Remasker", "sceneName": "Room_Queen" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Inverse Remasker", "sceneName": "Room_Queen" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Shiny Item RoyalCharm", "sceneName": "Room_Queen" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Cloth Ghost NPC", "sceneName": "Fungus3_23" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Dream Plant Orb (4)", "sceneName": "Fungus1_13" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (15)", "sceneName": "Fungus1_13" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (26)", "sceneName": "Fungus1_13" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (5)", "sceneName": "Fungus1_13" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (25)", "sceneName": "Fungus1_13" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (36)", "sceneName": "Fungus1_13" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (41)", "sceneName": "Fungus1_13" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (16)", "sceneName": "Fungus1_13" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (23)", "sceneName": "Fungus1_13" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (42)", "sceneName": "Fungus1_13" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (3)", "sceneName": "Fungus1_13" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (38)", "sceneName": "Fungus1_13" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Chest", "sceneName": "Fungus1_13" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Dream Plant Orb (20)", "sceneName": "Fungus1_13" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (2)", "sceneName": "Fungus1_13" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (35)", "sceneName": "Fungus1_13" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (27)", "sceneName": "Fungus1_13" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (43)", "sceneName": "Fungus1_13" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (14)", "sceneName": "Fungus1_13" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (22)", "sceneName": "Fungus1_13" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant", "sceneName": "Fungus1_13" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (34)", "sceneName": "Fungus1_13" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (17)", "sceneName": "Fungus1_13" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (8)", "sceneName": "Fungus1_13" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb", "sceneName": "Fungus1_13" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (21)", "sceneName": "Fungus1_13" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (40)", "sceneName": "Fungus1_13" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (6)", "sceneName": "Fungus1_13" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (12)", "sceneName": "Fungus1_13" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (11)", "sceneName": "Fungus1_13" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (29)", "sceneName": "Fungus1_13" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Grub Bottle", "sceneName": "Fungus1_13" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Dream Plant Orb (31)", "sceneName": "Fungus1_13" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (24)", "sceneName": "Fungus1_13" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (10)", "sceneName": "Fungus1_13" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Vessel Fragment", "sceneName": "Fungus1_13" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (9)", "sceneName": "Fungus1_13" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (7)", "sceneName": "Fungus1_13" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (28)", "sceneName": "Fungus1_13" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Vine Platform", "sceneName": "Fungus1_13" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Dream Plant Orb (13)", "sceneName": "Fungus1_13" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (1)", "sceneName": "Fungus1_13" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (18)", "sceneName": "Fungus1_13" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (39)", "sceneName": "Fungus1_13" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Vine Platform (2)", "sceneName": "Fungus1_13" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Dream Plant Orb (32)", "sceneName": "Fungus1_13" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (30)", "sceneName": "Fungus1_13" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (19)", "sceneName": "Fungus1_13" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (37)", "sceneName": "Fungus1_13" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Vine Platform (1)", "sceneName": "Fungus1_13" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Dream Plant Orb (33)", "sceneName": "Fungus1_13" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Acid Walker (4)", "sceneName": "Fungus1_13" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Acid Walker", "sceneName": "Fungus1_13" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Acid Walker (5)", "sceneName": "Fungus1_13" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Acid Walker (2)", "sceneName": "Fungus1_13" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Acid Walker (3)", "sceneName": "Fungus1_13" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Acid Walker (1)", "sceneName": "Fungus1_13" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Dream Plant Orb (34)", "sceneName": "Deepnest_East_07" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Shiny Item (1)", "sceneName": "Deepnest_East_07" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (30)", "sceneName": "Deepnest_East_07" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Shiny Item", "sceneName": "Deepnest_East_07" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (41)", "sceneName": "Deepnest_East_07" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (33)", "sceneName": "Deepnest_East_07" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (17)", "sceneName": "Deepnest_East_07" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (13)", "sceneName": "Deepnest_East_07" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (48)", "sceneName": "Deepnest_East_07" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (42)", "sceneName": "Deepnest_East_07" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb", "sceneName": "Deepnest_East_07" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (40)", "sceneName": "Deepnest_East_07" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (29)", "sceneName": "Deepnest_East_07" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (9)", "sceneName": "Deepnest_East_07" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Secret Mask", "sceneName": "Deepnest_East_07" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Dream Plant Orb (8)", "sceneName": "Deepnest_East_07" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (14)", "sceneName": "Deepnest_East_07" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (44)", "sceneName": "Deepnest_East_07" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (47)", "sceneName": "Deepnest_East_07" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (4)", "sceneName": "Deepnest_East_07" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (18)", "sceneName": "Deepnest_East_07" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (31)", "sceneName": "Deepnest_East_07" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (46)", "sceneName": "Deepnest_East_07" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (1)", "sceneName": "Deepnest_East_07" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (23)", "sceneName": "Deepnest_East_07" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (21)", "sceneName": "Deepnest_East_07" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (6)", "sceneName": "Deepnest_East_07" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (20)", "sceneName": "Deepnest_East_07" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Remasker", "sceneName": "Deepnest_East_07" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Dream Plant Orb (10)", "sceneName": "Deepnest_East_07" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (51)", "sceneName": "Deepnest_East_07" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (19)", "sceneName": "Deepnest_East_07" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (45)", "sceneName": "Deepnest_East_07" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (12)", "sceneName": "Deepnest_East_07" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (15)", "sceneName": "Deepnest_East_07" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (50)", "sceneName": "Deepnest_East_07" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (25)", "sceneName": "Deepnest_East_07" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (5)", "sceneName": "Deepnest_East_07" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (36)", "sceneName": "Deepnest_East_07" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (2)", "sceneName": "Deepnest_East_07" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (24)", "sceneName": "Deepnest_East_07" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (26)", "sceneName": "Deepnest_East_07" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant", "sceneName": "Deepnest_East_07" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (27)", "sceneName": "Deepnest_East_07" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Secret Mask (1)", "sceneName": "Deepnest_East_07" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Dream Plant Orb (49)", "sceneName": "Deepnest_East_07" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (22)", "sceneName": "Deepnest_East_07" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (37)", "sceneName": "Deepnest_East_07" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (3)", "sceneName": "Deepnest_East_07" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (32)", "sceneName": "Deepnest_East_07" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (39)", "sceneName": "Deepnest_East_07" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (43)", "sceneName": "Deepnest_East_07" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (35)", "sceneName": "Deepnest_East_07" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (28)", "sceneName": "Deepnest_East_07" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (7)", "sceneName": "Deepnest_East_07" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (16)", "sceneName": "Deepnest_East_07" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (11)", "sceneName": "Deepnest_East_07" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Super Spitter (1)", "sceneName": "Deepnest_East_07" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Super Spitter (5)", "sceneName": "Deepnest_East_07" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Super Spitter", "sceneName": "Deepnest_East_07" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Super Spitter (2)", "sceneName": "Deepnest_East_07" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Super Spitter (4)", "sceneName": "Deepnest_East_07" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Blow Fly (1)", "sceneName": "Deepnest_East_07" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Ceiling Dropper (5)", "sceneName": "Deepnest_East_07" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Ceiling Dropper (7)", "sceneName": "Deepnest_East_07" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Blow Fly (2)", "sceneName": "Deepnest_East_07" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Ceiling Dropper", "sceneName": "Deepnest_East_07" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Blow Fly", "sceneName": "Deepnest_East_07" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Ceiling Dropper (6)", "sceneName": "Deepnest_East_07" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Ceiling Dropper (4)", "sceneName": "Deepnest_East_07" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Ceiling Dropper (2)", "sceneName": "Deepnest_East_07" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Ceiling Dropper (1)", "sceneName": "Deepnest_East_07" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Remasker (2)", "sceneName": "Ruins2_11_b" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Break Jar", "sceneName": "Ruins2_11_b" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Remasker (1)", "sceneName": "Ruins2_11_b" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Inverse Remasker", "sceneName": "Ruins2_11_b" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Ruins Lever", "sceneName": "Ruins2_11_b" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Break Jar (1)", "sceneName": "Ruins2_11_b" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Break Jar (4)", "sceneName": "Ruins2_11" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "inver remask_below second corridor", "sceneName": "Ruins2_11" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "inver remask_below second corridor (1)", "sceneName": "Ruins2_11" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "secret sound_grub room", "sceneName": "Ruins2_11" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Secret Mask", "sceneName": "Ruins2_11" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Break Jar (3)", "sceneName": "Ruins2_11" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Grub Bottle", "sceneName": "Ruins2_11" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Break Jar (5)", "sceneName": "Ruins2_11" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Break Jar (7)", "sceneName": "Ruins2_11" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "inver remask_above boss encounter", "sceneName": "Ruins2_11" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Break Jar (2)", "sceneName": "Ruins2_11" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Break Jar (8)", "sceneName": "Ruins2_11" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Shiny Item CollectorMap", "sceneName": "Ruins2_11" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Break Jar (6)", "sceneName": "Ruins2_11" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "inver remask_above first corridor", "sceneName": "Ruins2_11" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "inver remask_below second corridor (2)", "sceneName": "Ruins2_11" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Battle Scene", "sceneName": "Ruins2_11" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Grub Bottle", "sceneName": "Ruins_House_01" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Battle Scene", "sceneName": "Ruins_House_01" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Shiny Item", "sceneName": "Waterways_04b" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Flukeman (1)", "sceneName": "Waterways_04b" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Secret Sound Region", "sceneName": "Waterways_04b" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Flukeman", "sceneName": "Waterways_04b" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Secret Mask", "sceneName": "Waterways_04b" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Heart Piece", "sceneName": "Waterways_04b" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Ceiling Dropper", "sceneName": "Waterways_04b" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Ceiling Dropper (1)", "sceneName": "Waterways_04b" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Flukeman", "sceneName": "Waterways_08" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Flukeman (1)", "sceneName": "Waterways_08" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Flukeman (2)", "sceneName": "Waterways_08" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Flukeman (3)", "sceneName": "Waterways_08" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Ceiling Dropper (2)", "sceneName": "Waterways_08" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Flukeman (16)", "sceneName": "Waterways_08" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Flukeman (5)", "sceneName": "Waterways_08" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Flukeman (4)", "sceneName": "Waterways_08" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Flukeman (13)", "sceneName": "Waterways_08" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Flukeman (15)", "sceneName": "Waterways_08" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Inverse Remasker", "sceneName": "Waterways_08" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Breakable Wall Waterways", "sceneName": "Waterways_08" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Break Floor 1", "sceneName": "Waterways_08" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Secret Mask", "sceneName": "Waterways_08" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Ceiling Dropper (3)", "sceneName": "Waterways_08" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Ceiling Dropper", "sceneName": "Waterways_08" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Ceiling Dropper (4)", "sceneName": "Waterways_08" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Ceiling Dropper (1)", "sceneName": "Waterways_08" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Shiny Item", "sceneName": "Waterways_12" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Battle Scene", "sceneName": "Waterways_09" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Ruins Lever", "sceneName": "Waterways_09" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Collapser Small", "sceneName": "Waterways_09" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Shiny", "sceneName": "Waterways_09" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Shiny Item Ore", "sceneName": "Crossroads_38" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Health Cocoon", "sceneName": "Deepnest_East_15" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Super Spitter(Clone)", "sceneName": "Deepnest_East_04" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Giant Hopper", "sceneName": "Deepnest_East_08" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Ceiling Dropper", "sceneName": "Deepnest_East_08" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Ceiling Dropper (3)", "sceneName": "Deepnest_East_08" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Shiny Item", "sceneName": "Deepnest_East_08" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Breakable Wall", "sceneName": "Deepnest_East_08" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Ceiling Dropper (2)", "sceneName": "Deepnest_East_08" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Ceiling Dropper (1)", "sceneName": "Deepnest_East_08" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Ceiling Dropper (4)", "sceneName": "Deepnest_East_09" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Ceiling Dropper (5)", "sceneName": "Deepnest_East_09" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Ceiling Dropper (6)", "sceneName": "Deepnest_East_09" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Secret Mask", "sceneName": "Deepnest_East_09" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "One Way Wall", "sceneName": "Deepnest_East_09" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Ceiling Dropper (3)", "sceneName": "Deepnest_East_09" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Ceiling Dropper", "sceneName": "Deepnest_East_09" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Ceiling Dropper (2)", "sceneName": "Deepnest_East_09" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Ceiling Dropper (1)", "sceneName": "Deepnest_East_09" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Inverse Remasker", "sceneName": "Room_Colosseum_02" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Breakable Wall_Silhouette", "sceneName": "Room_Colosseum_02" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Remasker", "sceneName": "Room_Colosseum_02" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Bursting Bouncer(Clone)", "sceneName": "Room_Colosseum_Bronze" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Col_Glow_Remasker", "sceneName": "Room_Colosseum_Bronze" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Shiny Item", "sceneName": "Room_Colosseum_Bronze" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Royal Zombie 1", "sceneName": "Ruins2_05" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Royal Zombie Fat", "sceneName": "Ruins2_05" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Royal Zombie Coward (1)", "sceneName": "Ruins2_05" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Ruins Sentry 1", "sceneName": "Ruins2_05" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Ruins Sentry 1 (2)", "sceneName": "Ruins2_05" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Ruins Sentry Fat", "sceneName": "Ruins2_05" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Ruins Sentry 1 (1)", "sceneName": "Ruins2_05" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Ruins Flying Sentry", "sceneName": "Ruins2_05" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Royal Zombie Fat (1)", "sceneName": "Ruins2_05" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Royal Zombie Coward (2)", "sceneName": "Ruins2_05" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Ruins Sentry Fat (1)", "sceneName": "Ruins2_05" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Royal Zombie Coward (3)", "sceneName": "Ruins2_05" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Royal Zombie 1 (1)", "sceneName": "Ruins2_05" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Ruins Sentry 1 (3)", "sceneName": "Ruins2_05" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Royal Zombie Fat (2)", "sceneName": "Ruins2_05" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Royal Zombie 1 (2)", "sceneName": "Ruins2_05" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Shiny Item", "sceneName": "Ruins2_05" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Royal Zombie 1", "sceneName": "Ruins2_09" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Royal Zombie Coward", "sceneName": "Ruins2_09" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Royal Zombie Fat", "sceneName": "Ruins2_09" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Battle Scene", "sceneName": "Ruins2_09" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Vessel Fragment", "sceneName": "Ruins2_09" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "secret sound", "sceneName": "Fungus2_34" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Remasker", "sceneName": "Fungus2_34" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Shiny Item", "sceneName": "Fungus2_34" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Remasker (1)", "sceneName": "Fungus2_34" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Jellyfish 1", "sceneName": "Fungus3_01" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Jellyfish 4", "sceneName": "Fungus3_01" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Jellyfish 3", "sceneName": "Fungus3_01" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Jellyfish", "sceneName": "Fungus3_01" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Jellyfish 6", "sceneName": "Fungus3_01" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Jellyfish 2", "sceneName": "Fungus3_01" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Shiny Item", "sceneName": "Fungus3_30" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Health Cocoon", "sceneName": "Fungus3_30" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Acid Walker", "sceneName": "Fungus1_12" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Secret Mask", "sceneName": "Fungus1_12" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Secret Mask sounder", "sceneName": "Fungus1_12" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Breakable Wall", "sceneName": "Abyss_03_c" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Zombie Hive", "sceneName": "Hive_01" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Hive Breakable Pillar", "sceneName": "Hive_01" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Hive Bench", "sceneName": "Hive_01" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Bee Stinger", "sceneName": "Hive_01" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Hive Breakable Pillar (1)", "sceneName": "Hive_01" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Dream Plant Orb (1)", "sceneName": "Hive_02" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (12)", "sceneName": "Hive_02" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (18)", "sceneName": "Hive_02" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (22)", "sceneName": "Hive_02" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (11)", "sceneName": "Hive_02" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Bee Stinger (1)", "sceneName": "Hive_02" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Hive Breakable Pillar (2)", "sceneName": "Hive_02" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Dream Plant Orb (6)", "sceneName": "Hive_02" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (16)", "sceneName": "Hive_02" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (8)", "sceneName": "Hive_02" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (15)", "sceneName": "Hive_02" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (13)", "sceneName": "Hive_02" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (9)", "sceneName": "Hive_02" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (19)", "sceneName": "Hive_02" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (17)", "sceneName": "Hive_02" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (14)", "sceneName": "Hive_02" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (4)", "sceneName": "Hive_02" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Bee Stinger (3)", "sceneName": "Hive_02" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Dream Plant Orb (20)", "sceneName": "Hive_02" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant", "sceneName": "Hive_02" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (23)", "sceneName": "Hive_02" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (21)", "sceneName": "Hive_02" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (2)", "sceneName": "Hive_02" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Bee Stinger (2)", "sceneName": "Hive_02" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Dream Plant Orb (7)", "sceneName": "Hive_02" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Zombie Hive (1)", "sceneName": "Hive_02" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Zombie Hive (2)", "sceneName": "Hive_02" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Zombie Hive (3)", "sceneName": "Hive_02" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Bee Stinger (4)", "sceneName": "Hive_03_c" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Bee Stinger (5)", "sceneName": "Hive_03_c" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Break Floor 1", "sceneName": "Hive_03_c" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Hive Breakable Pillar (5)", "sceneName": "Hive_03_c" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Big Bee (1)", "sceneName": "Hive_03_c" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Bee Stinger (6)", "sceneName": "Hive_03_c" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Big Bee", "sceneName": "Hive_03_c" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Secret Mask", "sceneName": "Hive_03_c" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Secret Mask", "sceneName": "Deepnest_East_01" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "One Way Wall", "sceneName": "Deepnest_East_01" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Blow Fly (1)", "sceneName": "Deepnest_East_01" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Blow Fly (4)", "sceneName": "Deepnest_East_01" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Blow Fly (2)", "sceneName": "Deepnest_East_01" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Blow Fly (5)", "sceneName": "Deepnest_East_01" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Blow Fly (3)", "sceneName": "Deepnest_East_01" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Blow Fly", "sceneName": "Deepnest_East_01" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Bee Stinger (7)", "sceneName": "Hive_03" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Big Bee (2)", "sceneName": "Hive_03" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Bee Stinger (8)", "sceneName": "Hive_03" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Grub Bottle", "sceneName": "Hive_03" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Remasker", "sceneName": "Hive_03" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Bee Stinger (10)", "sceneName": "Hive_04" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Zombie Hive (4)", "sceneName": "Hive_04" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Bee Stinger (9)", "sceneName": "Hive_04" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Bee Stinger (11)", "sceneName": "Hive_04" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Zombie Hive (6)", "sceneName": "Hive_04" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Hive Break Wall", "sceneName": "Hive_04" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Grub Bottle", "sceneName": "Hive_04" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Hive Breakable Pillar (4)", "sceneName": "Hive_04" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Big Bee (3)", "sceneName": "Hive_04" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Hive Breakable Pillar (3)", "sceneName": "Hive_04" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Hive Breakable Pillar (5)", "sceneName": "Hive_04" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Heart Piece", "sceneName": "Hive_04" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Big Bee (4)", "sceneName": "Hive_04" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Bee Stinger (12)", "sceneName": "Hive_04" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Hive Breakable Pillar", "sceneName": "Hive_05" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Hive Breakable Pillar (1)", "sceneName": "Hive_05" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Hive Breakable Pillar (2)", "sceneName": "Hive_05" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Shiny Item Stand", "sceneName": "Hive_05" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Vespa NPC", "sceneName": "Hive_05" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Quake Floor", "sceneName": "Deepnest_East_02" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "One Way Wall", "sceneName": "Deepnest_East_02" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Super Spitter(Clone)", "sceneName": "Room_Colosseum_Silver" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Bursting Bouncer(Clone)", "sceneName": "Room_Colosseum_Silver" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Col_Glow_Remasker", "sceneName": "Room_Colosseum_Silver" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Giant Hopper(Clone)", "sceneName": "Room_Colosseum_Silver" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Shiny Item", "sceneName": "Room_Colosseum_Silver" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Shiny Item Relic3", "sceneName": "Crossroads_38" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Flamebearer Spawn", "sceneName": "Mines_10" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Flamebearer Spawn", "sceneName": "Ruins1_28" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Flamebearer Spawn", "sceneName": "Fungus1_10" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Flamebearer Spawn", "sceneName": "Tutorial_01" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Flamebearer Spawn", "sceneName": "RestingGrounds_06" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Flamebearer Spawn", "sceneName": "Deepnest_East_03" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Grimm Chest", "sceneName": "Grimm_Main_Tent" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Shiny Item", "sceneName": "Grimm_Main_Tent" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Royal Gaurd", "sceneName": "White_Palace_11" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "White Palace Orb Lever", "sceneName": "White_Palace_02" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Collapser Small", "sceneName": "White_Palace_02" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Battle Scene", "sceneName": "White_Palace_02" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "WP Lever", "sceneName": "White_Palace_03_hub" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "WP Lever", "sceneName": "White_Palace_15" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "White Palace Orb Lever", "sceneName": "White_Palace_15" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Quake Floor", "sceneName": "White_Palace_15" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "White Palace Orb Lever", "sceneName": "White_Palace_14" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Breakable Wall Ruin Lift", "sceneName": "White_Palace_06" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Secret Sound Region", "sceneName": "White_Palace_18" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Remasker", "sceneName": "White_Palace_18" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Breakable Wall Waterways", "sceneName": "White_Palace_12" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "WP Lever", "sceneName": "White_Palace_12" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Break Floor 1", "sceneName": "White_Palace_12" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Breakable Wall Waterways", "sceneName": "White_Palace_09" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Quake Floor", "sceneName": "White_Palace_09" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Shiny Item RoyalCharm", "sceneName": "White_Palace_09" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Shiny Item", "sceneName": "Abyss_15" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Secret Mask", "sceneName": "Dream_Final" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Shiny Item(Clone)", "sceneName": "Mines_20" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Zombie Beam Miner (3)", "sceneName": "Mines_23" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Dream Plant Orb (7)", "sceneName": "Mines_23" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (16)", "sceneName": "Mines_23" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb", "sceneName": "Mines_23" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (18)", "sceneName": "Mines_23" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant", "sceneName": "Mines_23" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (8)", "sceneName": "Mines_23" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (11)", "sceneName": "Mines_23" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (10)", "sceneName": "Mines_23" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (1)", "sceneName": "Mines_23" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (3)", "sceneName": "Mines_23" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (13)", "sceneName": "Mines_23" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (20)", "sceneName": "Mines_23" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (4)", "sceneName": "Mines_23" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (6)", "sceneName": "Mines_23" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (2)", "sceneName": "Mines_23" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (17)", "sceneName": "Mines_23" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (12)", "sceneName": "Mines_23" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (14)", "sceneName": "Mines_23" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (5)", "sceneName": "Mines_23" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (9)", "sceneName": "Mines_23" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (15)", "sceneName": "Mines_23" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (19)", "sceneName": "Mines_23" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Crystal Flyer (2)", "sceneName": "Mines_23" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Zombie Beam Miner (1)", "sceneName": "Mines_23" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Zombie Beam Miner (2)", "sceneName": "Mines_23" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Zombie Beam Miner (4)", "sceneName": "Mines_23" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Crystal Flyer", "sceneName": "Mines_23" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Crystal Flyer (1)", "sceneName": "Mines_23" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Zombie Beam Miner", "sceneName": "Mines_23" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Zombie Beam Miner", "sceneName": "Mines_24" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Grub Bottle", "sceneName": "Mines_24" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Zombie Beam Miner", "sceneName": "Mines_25" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Crystal Flyer", "sceneName": "Mines_25" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Zombie Beam Miner (2)", "sceneName": "Mines_25" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Crystal Flyer (2)", "sceneName": "Mines_25" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Zombie Beam Miner (1)", "sceneName": "Mines_25" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Crystal Flyer (1)", "sceneName": "Mines_25" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Secret Mask", "sceneName": "Mines_25" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Breakable Wall", "sceneName": "Mines_25" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "One Way Wall", "sceneName": "Mines_25" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Quake Floor", "sceneName": "Mines_25" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Zombie Beam Miner (3)", "sceneName": "Mines_25" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Crystal Flyer", "sceneName": "Mines_34" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Shiny Item Stand", "sceneName": "Mines_34" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Remasker", "sceneName": "Mines_34" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Dream Plant Orb (7)", "sceneName": "Fungus2_17" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (16)", "sceneName": "Fungus2_17" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (1)", "sceneName": "Fungus2_17" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (13)", "sceneName": "Fungus2_17" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (17)", "sceneName": "Fungus2_17" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (15)", "sceneName": "Fungus2_17" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (6)", "sceneName": "Fungus2_17" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (3)", "sceneName": "Fungus2_17" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (5)", "sceneName": "Fungus2_17" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (14)", "sceneName": "Fungus2_17" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (12)", "sceneName": "Fungus2_17" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Shiny Item", "sceneName": "Fungus2_17" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb", "sceneName": "Fungus2_17" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (8)", "sceneName": "Fungus2_17" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (10)", "sceneName": "Fungus2_17" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (4)", "sceneName": "Fungus2_17" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (9)", "sceneName": "Fungus2_17" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (2)", "sceneName": "Fungus2_17" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant", "sceneName": "Fungus2_17" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Dream Plant Orb (11)", "sceneName": "Fungus2_17" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Mushroom Roller", "sceneName": "Fungus2_29" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Mushroom Roller (1)", "sceneName": "Fungus2_29" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Fungus Flyer (1)", "sceneName": "Fungus2_29" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Fungus Flyer", "sceneName": "Fungus2_29" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Mushroom Roller (2)", "sceneName": "Fungus2_29" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Mushroom Roller (3)", "sceneName": "Fungus2_29" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Mushroom Brawler (1)", "sceneName": "Fungus2_29" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Mushroom Brawler", "sceneName": "Fungus2_29" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Secret Mask (2)", "sceneName": "Fungus2_29" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Secret Mask", "sceneName": "Fungus2_29" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Shiny Item", "sceneName": "Fungus2_29" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Break Floor 1", "sceneName": "Fungus2_29" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Secret Mask (1)", "sceneName": "Fungus2_29" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Fungus Flyer (1)", "sceneName": "Fungus2_30" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Mushroom Roller", "sceneName": "Fungus2_30" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Mushroom Brawler (1)", "sceneName": "Fungus2_30" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Mushroom Brawler", "sceneName": "Fungus2_30" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Secret Mask", "sceneName": "Fungus2_30" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Mushroom Roller (3)", "sceneName": "Fungus2_30" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Flamebearer Spawn", "sceneName": "Fungus2_30" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Mushroom Roller (2)", "sceneName": "Fungus2_30" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Flamebearer Spawn", "sceneName": "Abyss_02" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Angry Buzzer", "sceneName": "Crossroads_12" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Hatcher (1)", "sceneName": "Crossroads_35" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Heart Piece", "sceneName": "Room_Mansion" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Super Spitter(Clone)", "sceneName": "Deepnest_East_11" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Quake Floor (4)", "sceneName": "Deepnest_East_17" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Quake Floor (2)", "sceneName": "Deepnest_East_17" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Quake Floor (5)", "sceneName": "Deepnest_East_17" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Secret Mask", "sceneName": "Deepnest_East_17" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Quake Floor (10)", "sceneName": "Deepnest_East_17" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Quake Floor", "sceneName": "Deepnest_East_17" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Quake Floor (6)", "sceneName": "Deepnest_East_17" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Quake Floor (3)", "sceneName": "Deepnest_East_17" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Quake Floor (8)", "sceneName": "Deepnest_East_17" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Quake Floor (9)", "sceneName": "Deepnest_East_17" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Quake Floor (7)", "sceneName": "Deepnest_East_17" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Quake Floor (1)", "sceneName": "Deepnest_East_17" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Shiny Item(Clone)", "sceneName": "Deepnest_East_14" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Grub Bottle", "sceneName": "Waterways_14" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Collapser Small", "sceneName": "Waterways_14" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Remasker", "sceneName": "Waterways_14" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Remasker (1)", "sceneName": "Waterways_15" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Remasker", "sceneName": "Waterways_15" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Shiny Item Stand", "sceneName": "Waterways_15" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Remasker (2)", "sceneName": "Waterways_15" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Secret Sound Region", "sceneName": "Waterways_15" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Secret Mask", "sceneName": "Deepnest_42" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Plank Solid 1 (1)", "sceneName": "Deepnest_42" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Plank Solid 1 (2)", "sceneName": "Deepnest_42" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Plank Solid 1", "sceneName": "Deepnest_42" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Secret Mask", "sceneName": "Deepnest_02" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Breakable Wall", "sceneName": "Deepnest_02" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Collapser Small", "sceneName": "Deepnest_02" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Grub Mimic 2", "sceneName": "Deepnest_36" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Grub Mimic 1", "sceneName": "Deepnest_36" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Grub Mimic 3", "sceneName": "Deepnest_36" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Grub Mimic Bottle (1)", "sceneName": "Deepnest_36" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Grub Mimic Bottle (2)", "sceneName": "Deepnest_36" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Grub Bottle", "sceneName": "Deepnest_36" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Grub Mimic Bottle", "sceneName": "Deepnest_36" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Secret Sound Region", "sceneName": "Ruins_Elevator" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Ghost NPC", "sceneName": "Ruins_Elevator" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Shiny Item", "sceneName": "Ruins_Elevator" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Remasker (2)", "sceneName": "Ruins_Elevator" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "One Way Wall", "sceneName": "Ruins_Elevator" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Secret Sound Region (1)", "sceneName": "Ruins_Elevator" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Remasker", "sceneName": "Ruins_Elevator" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Remasker (3)", "sceneName": "Ruins_Elevator" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Remasker (1)", "sceneName": "Ruins_Elevator" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Shiny Item (1)", "sceneName": "Ruins_Elevator" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Ghost NPC", "sceneName": "Ruins_Bathhouse" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Remasker", "sceneName": "Ruins_Bathhouse" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Breakable Wall", "sceneName": "Ruins_Bathhouse" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Breakable Wall_Silhouette", "sceneName": "Room_Colosseum_Spectate" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Secret Mask (1)", "sceneName": "GG_Lurker" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Secret Mask", "sceneName": "GG_Lurker" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Shiny Item", "sceneName": "GG_Lurker" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Col_Glow_Remasker", "sceneName": "Room_Colosseum_Gold" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Mage (1)", "sceneName": "Room_Colosseum_Gold" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Angry Buzzer(Clone)", "sceneName": "Room_Colosseum_Gold" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Mantis Heavy(Clone)", "sceneName": "Room_Colosseum_Gold" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Mantis Heavy Flyer(Clone)", "sceneName": "Room_Colosseum_Gold" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Lesser Mawlek(Clone)", "sceneName": "Room_Colosseum_Gold" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Poo Strength", "sceneName": "Grimm_Divine" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Toll Gate Machine (1)", "sceneName": "Mines_33" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "One Way Wall", "sceneName": "Mines_33" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Toll Gate Machine", "sceneName": "Mines_33" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Secret Mask", "sceneName": "Mines_33" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "gramaphone", "sceneName": "Room_Tram_RG" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "gramaphone (1)", "sceneName": "Room_Tram_RG" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Flukeman", "sceneName": "GG_Pipeway" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Fat Fluke", "sceneName": "GG_Pipeway" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Flukeman (1)", "sceneName": "GG_Pipeway" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Fat Fluke (1)", "sceneName": "GG_Pipeway" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Collapser Small", "sceneName": "GG_Pipeway" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Fat Fluke (3)", "sceneName": "GG_Pipeway" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Fat Fluke (2)", "sceneName": "GG_Pipeway" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Quake Floor", "sceneName": "GG_Pipeway" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Remasker (1)", "sceneName": "GG_Waterways" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Chest (1)", "sceneName": "GG_Waterways" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Chest (3)", "sceneName": "GG_Waterways" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Remasker", "sceneName": "GG_Waterways" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Chest", "sceneName": "GG_Waterways" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Chest (4)", "sceneName": "GG_Waterways" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Chest (2)", "sceneName": "GG_Waterways" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Shiny Item Godfinder", "sceneName": "GG_Waterways" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Remasker", "sceneName": "Room_GG_Shortcut" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Secret Sound Region", "sceneName": "Room_GG_Shortcut" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Breakable Grate", "sceneName": "Room_GG_Shortcut" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Col_Glow_Remasker", "sceneName": "GG_Atrium" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Zote_Break_wall", "sceneName": "GG_Workshop" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Breakable Wall_Silhouette", "sceneName": "GG_Workshop" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Col_Glow_Remasker", "sceneName": "GG_Workshop" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Radiance Statue Cage", "sceneName": "GG_Workshop" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Knight Statue Cage", "sceneName": "GG_Workshop" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Collapser Small", "sceneName": "White_Palace_17" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Secret Mask", "sceneName": "White_Palace_17" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "WP Lever", "sceneName": "White_Palace_17" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Secret Sound Region", "sceneName": "White_Palace_19" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Inverse Remasker (1)", "sceneName": "White_Palace_19" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Inverse Remasker", "sceneName": "White_Palace_20" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Secret Mask", "sceneName": "GG_Atrium_Roof" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Remasker", "sceneName": "GG_Atrium_Roof" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Breakable Wall_Silhouette", "sceneName": "GG_Atrium_Roof" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Col_Glow_Remasker", "sceneName": "GG_Atrium_Roof" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "GG Fall Platform", "sceneName": "GG_Atrium_Roof" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "gg_roof_lever", "sceneName": "GG_Atrium_Roof" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Shiny Item", "sceneName": "GG_False_Knight" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Col_Glow_Remasker", "sceneName": "GG_Spa" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Col_Glow_Remasker", "sceneName": "GG_Engine" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Col_Glow_Remasker", "sceneName": "GG_Engine_Prime" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "white_scene_glow", "sceneName": "GG_Hollow_Knight" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "white_scene_glow (1)", "sceneName": "GG_Hollow_Knight" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "gg_roof_lever", "sceneName": "GG_Atrium" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Col_Glow_Remasker", "sceneName": "GG_Unn" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Col_Glow_Remasker", "sceneName": "GG_Engine_Root" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Col_Glow_Remasker", "sceneName": "GG_Wyrm" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Plaque_statue_03 (46)", "sceneName": "Dream_Room_Believer_Shrine" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Plaque_statue_03 (18)", "sceneName": "Dream_Room_Believer_Shrine" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Plaque_statue_03 (7)", "sceneName": "Dream_Room_Believer_Shrine" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Plaque_statue_03 (23)", "sceneName": "Dream_Room_Believer_Shrine" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Plaque_statue_03 (27)", "sceneName": "Dream_Room_Believer_Shrine" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Plaque_statue_03 (36)", "sceneName": "Dream_Room_Believer_Shrine" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Plaque_statue_03 (41)", "sceneName": "Dream_Room_Believer_Shrine" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Plaque_statue_02 (5)", "sceneName": "Dream_Room_Believer_Shrine" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Plaque_statue_03 (1)", "sceneName": "Dream_Room_Believer_Shrine" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Plaque_statue_03 (10)", "sceneName": "Dream_Room_Believer_Shrine" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Plaque_statue_01 (3)", "sceneName": "Dream_Room_Believer_Shrine" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Plaque_statue_03 (25)", "sceneName": "Dream_Room_Believer_Shrine" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Plaque_statue_02 (6)", "sceneName": "Dream_Room_Believer_Shrine" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Plaque_statue_03 (15)", "sceneName": "Dream_Room_Believer_Shrine" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Plaque_statue_03 (5)", "sceneName": "Dream_Room_Believer_Shrine" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Plaque_statue_03 (40)", "sceneName": "Dream_Room_Believer_Shrine" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Plaque_statue_03 (42)", "sceneName": "Dream_Room_Believer_Shrine" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Plaque_statue_03 (17)", "sceneName": "Dream_Room_Believer_Shrine" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Plaque_statue_02 (9)", "sceneName": "Dream_Room_Believer_Shrine" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Plaque_statue_03 (20)", "sceneName": "Dream_Room_Believer_Shrine" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Plaque_statue_01 (1)", "sceneName": "Dream_Room_Believer_Shrine" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Plaque_statue_02", "sceneName": "Dream_Room_Believer_Shrine" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Plaque_statue_03 (6)", "sceneName": "Dream_Room_Believer_Shrine" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Plaque_statue_03 (26)", "sceneName": "Dream_Room_Believer_Shrine" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Plaque_statue_03 (3)", "sceneName": "Dream_Room_Believer_Shrine" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Plaque_statue_03 (16)", "sceneName": "Dream_Room_Believer_Shrine" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Plaque_statue_03 (4)", "sceneName": "Dream_Room_Believer_Shrine" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Plaque_statue_03 (43)", "sceneName": "Dream_Room_Believer_Shrine" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Plaque_statue_03 (21)", "sceneName": "Dream_Room_Believer_Shrine" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Plaque_statue_03 (34)", "sceneName": "Dream_Room_Believer_Shrine" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Plaque_statue_02 (8)", "sceneName": "Dream_Room_Believer_Shrine" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Plaque_statue_02 (10)", "sceneName": "Dream_Room_Believer_Shrine" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Plaque_statue_03 (11)", "sceneName": "Dream_Room_Believer_Shrine" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Plaque_statue_02 (2)", "sceneName": "Dream_Room_Believer_Shrine" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Plaque_statue_02 (3)", "sceneName": "Dream_Room_Believer_Shrine" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Plaque_statue_03 (2)", "sceneName": "Dream_Room_Believer_Shrine" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Plaque_statue_03 (32)", "sceneName": "Dream_Room_Believer_Shrine" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Plaque_statue_03 (28)", "sceneName": "Dream_Room_Believer_Shrine" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Plaque_statue_03", "sceneName": "Dream_Room_Believer_Shrine" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Plaque_statue_03 (9)", "sceneName": "Dream_Room_Believer_Shrine" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Plaque_statue_03 (22)", "sceneName": "Dream_Room_Believer_Shrine" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Plaque_statue_02 (4)", "sceneName": "Dream_Room_Believer_Shrine" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Plaque_statue_03 (38)", "sceneName": "Dream_Room_Believer_Shrine" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Plaque_statue_03 (45)", "sceneName": "Dream_Room_Believer_Shrine" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Plaque_statue_03 (13)", "sceneName": "Dream_Room_Believer_Shrine" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Plaque_statue_01 (2)", "sceneName": "Dream_Room_Believer_Shrine" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Plaque_statue_03 (24)", "sceneName": "Dream_Room_Believer_Shrine" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Plaque_statue_02 (1)", "sceneName": "Dream_Room_Believer_Shrine" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Plaque_statue_03 (30)", "sceneName": "Dream_Room_Believer_Shrine" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Plaque_statue_03 (14)", "sceneName": "Dream_Room_Believer_Shrine" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Plaque_statue_01 (4)", "sceneName": "Dream_Room_Believer_Shrine" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Plaque_statue_03 (29)", "sceneName": "Dream_Room_Believer_Shrine" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Plaque_statue_03 (8)", "sceneName": "Dream_Room_Believer_Shrine" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Plaque_statue_03 (31)", "sceneName": "Dream_Room_Believer_Shrine" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Plaque_statue_02 (7)", "sceneName": "Dream_Room_Believer_Shrine" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Plaque_statue_03 (33)", "sceneName": "Dream_Room_Believer_Shrine" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Plaque_statue_02 (11)", "sceneName": "Dream_Room_Believer_Shrine" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Plaque_statue_03 (37)", "sceneName": "Dream_Room_Believer_Shrine" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Plaque_statue_03 (35)", "sceneName": "Dream_Room_Believer_Shrine" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Plaque_statue_03 (19)", "sceneName": "Dream_Room_Believer_Shrine" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Plaque_statue_03 (12)", "sceneName": "Dream_Room_Believer_Shrine" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Plaque_statue_03 (39)", "sceneName": "Dream_Room_Believer_Shrine" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Plaque_statue_03 (44)", "sceneName": "Dream_Room_Believer_Shrine" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Ruins Lever", "sceneName": "Ruins_House_03" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Inverse Remasker (1)", "sceneName": "Ruins_House_03" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Inverse Remasker", "sceneName": "Ruins_House_03" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Poo Heart", "sceneName": "Grimm_Divine" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Shiny Item HunterMark", "sceneName": "Fungus1_08" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Angry Buzzer (1)", "sceneName": "Crossroads_05" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Angry Buzzer", "sceneName": "Crossroads_05" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Bursting Zombie (1)", "sceneName": "Crossroads_05" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Bursting Zombie", "sceneName": "Crossroads_05" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Poo Greed", "sceneName": "Grimm_Divine" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Health Cocoon (1)", "sceneName": "GG_Spa" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Shiny Item(Clone)", "sceneName": "Waterways_02" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Shiny Item GG Storms", "sceneName": "GG_Land_of_Storms" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Shiny Item(Clone)", "sceneName": "Deepnest_39" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Shiny Item(Clone)", "sceneName": "Waterways_04" }, - "Value": false + "Value": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + } }, { "Key": { "id": "Shiny", "sceneName": "Fungus3_25" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } } ], "persistentIntItems": [ @@ -18693,476 +30420,680 @@ "id": "Soul Totem mini_two_horned", "sceneName": "Crossroads_19" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Soul Totem 2", "sceneName": "Crossroads_ShamanTemple" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Soul Totem mini_horned", "sceneName": "Crossroads_18" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Soul Totem mini_horned", "sceneName": "Fungus2_10" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Soul Totem 5", "sceneName": "Fungus2_21" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Ruins Lift 1", "sceneName": "Ruins1_02" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Ruins Lift 1", "sceneName": "Ruins1_03" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Ruins Lift 3", "sceneName": "Ruins1_05c" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Ruins Lift 2", "sceneName": "Ruins1_05b" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Ruins Lift 1", "sceneName": "Ruins1_05" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Ruins Lift", "sceneName": "Ruins1_31" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Ruins Lift 2", "sceneName": "Ruins1_23" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Ruins Lift 1", "sceneName": "Ruins1_23" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Ruins Lift", "sceneName": "Ruins1_25" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Soul Totem 1", "sceneName": "Ruins1_24" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Soul Totem 3", "sceneName": "Ruins1_32" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Soul Totem 5", "sceneName": "Crossroads_45" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Soul Totem 5", "sceneName": "Mines_20" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Soul Totem mini_horned", "sceneName": "Mines_31" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Soul Totem mini_two_horned", "sceneName": "Mines_31" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Soul Totem 5", "sceneName": "Mines_28" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Soul Totem mini_horned", "sceneName": "Mines_35" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Soul Totem 4", "sceneName": "RestingGrounds_05" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Soul Totem mini_horned", "sceneName": "Crossroads_35" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Soul Totem mini_horned", "sceneName": "Waterways_01" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Soul Totem mini_horned", "sceneName": "Waterways_07" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Ruins Lift (1)", "sceneName": "Ruins2_01_b" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Ruins Lift", "sceneName": "Ruins2_01_b" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Ruins Lift", "sceneName": "Ruins2_03b" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Ruins Lift (1)", "sceneName": "Ruins2_03" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Ruins Lift", "sceneName": "Ruins2_Watcher_Room" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Soul Totem 1", "sceneName": "Deepnest_10" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Soul Totem 5", "sceneName": "Deepnest_Spider_Town" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Soul Totem 5", "sceneName": "Cliffs_02" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Soul Totem mini_two_horned", "sceneName": "Cliffs_01" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Soul Totem mini_horned", "sceneName": "Cliffs_04" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Soul Totem mini_horned", "sceneName": "Fungus1_30" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Soul Totem mini_horned", "sceneName": "Fungus1_07" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Soul Totem mini_horned", "sceneName": "Abyss_04" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Soul Totem mini_two_horned", "sceneName": "RestingGrounds_06" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Soul Totem 5", "sceneName": "Deepnest_East_16" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Soul Totem mini_two_horned", "sceneName": "Deepnest_East_14" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Soul Totem mini_horned", "sceneName": "Deepnest_East_14" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Soul Totem 5", "sceneName": "Deepnest_East_11" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Soul Totem mini_two_horned", "sceneName": "Crossroads_25" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Soul Totem 4", "sceneName": "Crossroads_36" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Soul Totem 4", "sceneName": "Deepnest_38" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Soul Totem mini_two_horned", "sceneName": "Fungus3_40" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Soul Totem mini_horned", "sceneName": "Fungus3_21" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Soul Totem mini_two_horned", "sceneName": "Deepnest_East_07" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Soul Totem mini_two_horned", "sceneName": "Waterways_04b" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Soul Totem mini_two_horned", "sceneName": "Waterways_08" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Ruins Lift", "sceneName": "Ruins2_05" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Ruins Lift (1)", "sceneName": "Ruins2_05" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Soul Totem mini_horned", "sceneName": "Fungus1_29" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Soul Totem mini_two_horned", "sceneName": "Deepnest_East_01" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Soul Totem mini_horned", "sceneName": "Deepnest_East_02" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Soul Totem white", "sceneName": "White_Palace_02" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Soul Totem white", "sceneName": "White_Palace_03_hub" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Soul Totem white", "sceneName": "White_Palace_15" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Soul Totem white", "sceneName": "White_Palace_04" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Soul Totem white", "sceneName": "White_Palace_09" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Soul Totem mini_two_horned", "sceneName": "Mines_25" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Soul Totem mini_horned", "sceneName": "Fungus2_29" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Soul Totem mini_two_horned", "sceneName": "Deepnest_East_17" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Soul Totem mini_two_horned", "sceneName": "Deepnest_42" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Ruins Lift", "sceneName": "Ruins_Elevator" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } }, { "Key": { "id": "Soul Totem 3", "sceneName": "GG_Lurker" }, - "Value": true + "Value": { + "Sync": true, + "SyncType": "Server" + } } ], "stringListVariables": [ diff --git a/HKMPServer/ConsoleServerManager.cs b/HKMPServer/ConsoleServerManager.cs index 9e5aad23..b053a604 100644 --- a/HKMPServer/ConsoleServerManager.cs +++ b/HKMPServer/ConsoleServerManager.cs @@ -20,7 +20,7 @@ internal class ConsoleServerManager : ServerManager { /// Name of the file used to store save data. /// private const string SaveFileName = "save.dat"; - + /// /// The logger class for logging to console. /// @@ -35,7 +35,7 @@ internal class ConsoleServerManager : ServerManager { /// The absolute file path of the save file. /// private string _saveFilePath; - + public ConsoleServerManager( NetServer netServer, ServerSettings serverSettings, @@ -43,7 +43,7 @@ public ConsoleServerManager( ConsoleLogger consoleLogger ) : base(netServer, serverSettings, packetManager) { _consoleLogger = consoleLogger; - + // Start loading addons AddonManager.LoadAddons(); @@ -55,7 +55,7 @@ ConsoleLogger consoleLogger Stop(); }; - + InitializeSaveFile(); } @@ -67,13 +67,13 @@ protected override void RegisterCommands() { CommandManager.RegisterCommand(new ConsoleSettingsCommand(this, InternalServerSettings)); CommandManager.RegisterCommand(new LogCommand(_consoleLogger)); } - + /// protected override void OnSaveUpdate(ushort id, SaveUpdate packet) { base.OnSaveUpdate(id, packet); - + // After the server manager has processed the save update, we write the current save data to file - WriteToSaveFile(CurrentSaveData); + WriteToSaveFile(ServerSaveData); } /// @@ -100,21 +100,21 @@ private void InitializeSaveFile() { // If the file exists, simply read it into the current save data for the server // Otherwise, create an empty dictionary for save data and save it to file if (File.Exists(_saveFilePath) && TryReadSaveFile(out var saveData)) { - CurrentSaveData = saveData; + ServerSaveData = saveData; } else { - CurrentSaveData = new Dictionary(); + ServerSaveData = new ServerSaveData(); - WriteToSaveFile(CurrentSaveData); + WriteToSaveFile(ServerSaveData); } } } /// - /// Try to read the save data in the save file into the given dictionary. + /// Try to read the save data in the save file into a server save data instance. /// - /// The save data in a dictionary if it was read, otherwise null. + /// The server save data instance if it was read, otherwise null. /// true if the save file could be read, false otherwise. - private bool TryReadSaveFile(out Dictionary saveData) { + private bool TryReadSaveFile(out ServerSaveData serverSaveData) { lock (_saveFileLock) { // Read the raw bytes from the file var bytes = File.ReadAllBytes(_saveFilePath); @@ -123,37 +123,70 @@ private bool TryReadSaveFile(out Dictionary saveData) { // We use the Packet class to easily read the raw bytes in the data var packet = new Packet(bytes); - // Then we use the CurrentSave class to parse the packet into the desired format - var currentSave = new CurrentSave(); - currentSave.ReadData(packet); + // First read the global save data from the packet using the method in CurrentSave + var globalSaveData = CurrentSave.ReadSaveDataDict(packet); + + // Then read the number of players from the packet for player specific save data + var numPlayers = packet.ReadUShort(); - saveData = currentSave.SaveData; + var playerSaveData = new Dictionary>(); + + // Next, for each of the players read their save data into the dictionary + for (var i = 0; i < numPlayers; i++) { + // Read the auth key of the player + var authKey = packet.ReadString(); + + // And read the save data + var saveData = CurrentSave.ReadSaveDataDict(packet); + + // Store it in the player save data dictionary + playerSaveData[authKey] = saveData; + } + + serverSaveData = new ServerSaveData { + GlobalSaveData = globalSaveData, + PlayerSaveData = playerSaveData + }; return true; } catch (Exception e) { Logger.Error($"Could not read the save data from file:\n{e}"); } - saveData = null; + serverSaveData = null; return false; } } /// - /// Write the save data in the given dictionary to the save file. + /// Write the save data from the server to the save file. /// - /// The dictionary containing the save data. - private void WriteToSaveFile(Dictionary saveData) { + /// The save data from the server to write to file. + private void WriteToSaveFile(ServerSaveData serverSaveData) { lock (_saveFileLock) { try { // We use the Packet class to easily write the data to raw bytes var packet = new Packet(); - - // Then we use the CurrentSave class to write the data into the packet as bytes - var currentSave = new CurrentSave { - SaveData = saveData - }; - currentSave.WriteData(packet); - + + // First write the global save data to the packet using the method in CurrentSave + CurrentSave.WriteSaveDataDict(serverSaveData.GlobalSaveData, packet); + + // Then write the number of players for which we have player specific save data + var numPlayers = serverSaveData.PlayerSaveData.Keys.Count; + if (numPlayers > ushort.MaxValue) { + throw new Exception( + $"Number of players for player specific save data is too large: {numPlayers}"); + } + + packet.Write((ushort) numPlayers); + + foreach (var playerDataEntry in serverSaveData.PlayerSaveData) { + var authKey = playerDataEntry.Key; + var saveData = playerDataEntry.Value; + + packet.Write(authKey); + CurrentSave.WriteSaveDataDict(saveData, packet); + } + // And finally obtain the byte array to write to file var bytes = packet.ToArray(); From 44799e2cde64bc8f5d5f2b7a7c1b4194014a8c24 Mon Sep 17 00:00:00 2001 From: Extremelyd1 <10898310+Extremelyd1@users.noreply.github.com> Date: Mon, 8 Jul 2024 22:03:44 +0200 Subject: [PATCH 091/216] Improve player specific saves --- HKMP/Game/Client/Save/SaveManager.cs | 99 ------------ HKMP/Resource/save-data.json | 222 +++++++++++++++++++++++---- 2 files changed, 189 insertions(+), 132 deletions(-) diff --git a/HKMP/Game/Client/Save/SaveManager.cs b/HKMP/Game/Client/Save/SaveManager.cs index e694aaaf..3cc03c8d 100644 --- a/HKMP/Game/Client/Save/SaveManager.cs +++ b/HKMP/Game/Client/Save/SaveManager.cs @@ -1,8 +1,6 @@ using System; -using System.Collections; using System.Collections.Generic; using System.Linq; -using GlobalEnums; using Hkmp.Collection; using Hkmp.Game.Client.Entity; using Hkmp.Networking.Client; @@ -21,11 +19,6 @@ namespace Hkmp.Game.Client.Save; /// Class that manages save data synchronisation. /// internal class SaveManager { - /// - /// The index of the save data entry for the warp. - /// - private const ushort SaveWarpIndex = ushort.MaxValue; - /// /// The save data instance that contains mappings for what to sync and their indices. /// @@ -823,20 +816,6 @@ private void UpdateSaveWithData(ushort index, byte[] encodedValue) { }); } - // TODO: refactor this, remove probably - if (index == SaveWarpIndex) { - // Specific handling of warp bench data - var respawnScene = DecodeString(encodedValue, 0); - var respawnMarkerName = DecodeString(encodedValue, 2); - var mapZone = (MapZone) encodedValue[4]; - - pd.respawnScene = respawnScene; - pd.respawnMarkerName = respawnMarkerName; - pd.mapZone = mapZone; - - MonoBehaviourUtil.Instance.StartCoroutine(WarpToBench()); - } - // Decode a string from the given byte array and start index in that array using the EncodeUtil string DecodeString(byte[] encoded, int startIndex) { var sceneIndex = BitConverter.ToUInt16(encoded, startIndex); @@ -937,14 +916,6 @@ Func valueFunc intData => intData.value ); - // Specific handling of last bench data - var encodedBenchData = new List(); - encodedBenchData.AddRange(EncodeValue(pd.respawnScene)); - encodedBenchData.AddRange(EncodeValue(pd.respawnMarkerName)); - encodedBenchData.Add((byte) pd.mapZone); - - saveData.Add(SaveWarpIndex, encodedBenchData.ToArray()); - return saveData; } @@ -963,74 +934,4 @@ private static int GetStringListHashCode(List list) { .Select(item => item.GetHashCode()) .Aggregate((total, nextCode) => total ^ nextCode); } - - private static IEnumerator WarpToBench() { - var gm = global::GameManager.instance; - - UIManager.instance.UIClosePauseMenu(); - - // Collection of various redundant attempts to fix the infamous soul orb bug - HeroController.instance.TakeMPQuick(PlayerData.instance.MPCharge); // actually broadcasts the event - HeroController.instance.SetMPCharge(0); - PlayerData.instance.MPReserve = 0; - HeroController.instance.ClearMP(); // useless - PlayMakerFSM.BroadcastEvent("MP DRAIN"); // This is the main fsm path for removing soul from the orb - PlayMakerFSM.BroadcastEvent("MP LOSE"); // This is an alternate path (used for bindings and other things) that actually plays an animation? - PlayMakerFSM.BroadcastEvent("MP RESERVE DOWN"); - - // Set some stuff which would normally be set by LoadSave - HeroController.instance.AffectedByGravity(false); - HeroController.instance.transitionState = HeroTransitionState.EXITING_SCENE; - if (HeroController.SilentInstance != null) { - if (HeroController.instance.cState.onConveyor || HeroController.instance.cState.onConveyorV || - HeroController.instance.cState.inConveyorZone) { - HeroController.instance.GetComponent()?.StopConveyorMove(); - HeroController.instance.cState.inConveyorZone = false; - HeroController.instance.cState.onConveyor = false; - HeroController.instance.cState.onConveyorV = false; - } - - HeroController.instance.cState.nearBench = false; - } - - gm.cameraCtrl.FadeOut(CameraFadeType.LEVEL_TRANSITION); - - yield return new WaitForSecondsRealtime(0.5f); - - // Actually respawn the character - gm.SetPlayerDataBool(nameof(PlayerData.atBench), false); - // Allow the player to have control if they warp to a non-bench while diving or cdashing - if (HeroController.SilentInstance != null) { - HeroController.instance.cState.superDashing = false; - HeroController.instance.cState.spellQuake = false; - } - - gm.ReadyForRespawn(false); - - yield return new WaitWhile(() => gm.IsInSceneTransition); - - EventRegister.SendEvent("UPDATE BLUE HEALTH"); // checks if hp is adjusted for Joni's blessing - - // Revert pause menu timescale - Time.timeScale = 1f; - gm.FadeSceneIn(); - - // We have to set the game non-paused because TogglePauseMenu sucks and UIClosePauseMenu doesn't do it for us. - gm.isPaused = false; - - // Restore various things normally handled by exiting the pause menu. None of these are necessary afaik - GameCameras.instance.ResumeCameraShake(); - if (HeroController.SilentInstance != null) { - HeroController.instance.UnPause(); - } - - MenuButtonList.ClearAllLastSelected(); - - //This allows the next pause to stop the game correctly - TimeController.GenericTimeScale = 1f; - - // Restores audio to normal levels. Unfortunately, some warps pop atm when music changes over - gm.actorSnapshotUnpaused.TransitionTo(0f); - gm.ui.AudioGoToGameplay(.2f); - } } diff --git a/HKMP/Resource/save-data.json b/HKMP/Resource/save-data.json index 89b162ee..850df204 100644 --- a/HKMP/Resource/save-data.json +++ b/HKMP/Resource/save-data.json @@ -1,5 +1,10 @@ { "playerData": { + "maxHealthBase": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, "heartPieces": { "Sync": true, "SyncType": "Player", @@ -15,6 +20,11 @@ "SyncType": "Player", "IgnoreSceneHost": true }, + "soulLimited": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, "vesselFragments": { "Sync": true, "SyncType": "Player", @@ -25,6 +35,66 @@ "SyncType": "Player", "IgnoreSceneHost": true }, + "respawnScene": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "respawnMarkerName": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "shadeScene": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "shadeMapZone": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "shadePositionX": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "shadePositionY": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "shadeHealth": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "shadeMP": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "shadeFireballLevel": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "shadeQuakeLevel": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "shadeScreamLevel": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "shadeSpecialType": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, "dreamgateMapPos": { "Sync": true, "SyncType": "Player", @@ -219,11 +289,6 @@ "SyncType": "Player", "IgnoreSceneHost": true }, - "ghostCoins": { - "Sync": true, - "SyncType": "Player", - "IgnoreSceneHost": true - }, "ore": { "Sync": true, "SyncType": "Player", @@ -341,8 +406,7 @@ }, "guardiansDefeated": { "Sync": true, - "SyncType": "Player", - "IgnoreSceneHost": true + "SyncType": "Server" }, "lurienDefeated": { "Sync": true, @@ -759,10 +823,6 @@ "Sync": true, "SyncType": "Server" }, - "slyBeta": { - "Sync": true, - "SyncType": "Server" - }, "metSlyShop": { "Sync": true, "SyncType": "Player", @@ -803,26 +863,6 @@ "SyncType": "Player", "IgnoreSceneHost": true }, - "slyVesselFrag3": { - "Sync": true, - "SyncType": "Player", - "IgnoreSceneHost": true - }, - "slyVesselFrag4": { - "Sync": true, - "SyncType": "Player", - "IgnoreSceneHost": true - }, - "slyNotch1": { - "Sync": true, - "SyncType": "Player", - "IgnoreSceneHost": true - }, - "slyNotch2": { - "Sync": true, - "SyncType": "Player", - "IgnoreSceneHost": true - }, "slySimpleKey": { "Sync": true, "SyncType": "Player", @@ -1383,7 +1423,8 @@ }, "legEaterLeft": { "Sync": true, - "SyncType": "Server" + "SyncType": "Player", + "IgnoreSceneHost": true }, "tukMet": { "Sync": true, @@ -1681,7 +1722,7 @@ }, "xunFlowerGiven": { "Sync": true, - "SyncType": "Player", + "SyncType": "Server", "IgnoreSceneHost": true }, "xunRewardGiven": { @@ -4359,6 +4400,101 @@ "Sync": true, "SyncType": "Server" }, + "visitedCrossroads": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "visitedGreenpath": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "visitedFungus": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "visitedHive": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "visitedCrossroadsInfected": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "visitedRuins": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "visitedMines": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "visitedRoyalGardens": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "visitedFogCanyon": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "visitedDeepnest": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "visitedRestingGrounds": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "visitedWaterways": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "visitedAbyss": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "visitedOutskirts": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "visitedWhitePalace": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "visitedCliffs": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "visitedAbyssLower": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "visitedGodhome": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "visitedMines10": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, "scenesVisited": { "Sync": true, "SyncType": "Server" @@ -4552,6 +4688,26 @@ "SyncType": "Player", "IgnoreSceneHost": true }, + "spareMarkers_r": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "spareMarkers_b": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "spareMarkers_y": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "spareMarkers_w": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, "openedTramLower": { "Sync": true, "SyncType": "Player", From 4ec6aafbe5f65e2e58a8d1078500a77bfbb61bf9 Mon Sep 17 00:00:00 2001 From: Extremelyd1 <10898310+Extremelyd1@users.noreply.github.com> Date: Tue, 9 Jul 2024 20:28:12 +0200 Subject: [PATCH 092/216] Improve server save data --- HKMP/Resource/save-data.json | 45 ++++++++++++++++++++++++------------ 1 file changed, 30 insertions(+), 15 deletions(-) diff --git a/HKMP/Resource/save-data.json b/HKMP/Resource/save-data.json index 850df204..1836a48d 100644 --- a/HKMP/Resource/save-data.json +++ b/HKMP/Resource/save-data.json @@ -670,7 +670,8 @@ }, "corn_crossroadsEncountered": { "Sync": true, - "SyncType": "Server" + "SyncType": "Server", + "IgnoreSceneHost": true }, "corn_crossroadsLeft": { "Sync": true, @@ -678,7 +679,8 @@ }, "corn_greenpathEncountered": { "Sync": true, - "SyncType": "Server" + "SyncType": "Server", + "IgnoreSceneHost": true }, "corn_greenpathLeft": { "Sync": true, @@ -686,7 +688,8 @@ }, "corn_fogCanyonEncountered": { "Sync": true, - "SyncType": "Server" + "SyncType": "Server", + "IgnoreSceneHost": true }, "corn_fogCanyonLeft": { "Sync": true, @@ -694,7 +697,8 @@ }, "corn_fungalWastesEncountered": { "Sync": true, - "SyncType": "Server" + "SyncType": "Server", + "IgnoreSceneHost": true }, "corn_fungalWastesLeft": { "Sync": true, @@ -702,7 +706,8 @@ }, "corn_cityEncountered": { "Sync": true, - "SyncType": "Server" + "SyncType": "Server", + "IgnoreSceneHost": true }, "corn_cityLeft": { "Sync": true, @@ -710,7 +715,8 @@ }, "corn_waterwaysEncountered": { "Sync": true, - "SyncType": "Server" + "SyncType": "Server", + "IgnoreSceneHost": true }, "corn_waterwaysLeft": { "Sync": true, @@ -718,7 +724,8 @@ }, "corn_minesEncountered": { "Sync": true, - "SyncType": "Server" + "SyncType": "Server", + "IgnoreSceneHost": true }, "corn_minesLeft": { "Sync": true, @@ -726,7 +733,8 @@ }, "corn_cliffsEncountered": { "Sync": true, - "SyncType": "Server" + "SyncType": "Server", + "IgnoreSceneHost": true }, "corn_cliffsLeft": { "Sync": true, @@ -734,7 +742,8 @@ }, "corn_deepnestEncountered": { "Sync": true, - "SyncType": "Server" + "SyncType": "Server", + "IgnoreSceneHost": true }, "corn_deepnestLeft": { "Sync": true, @@ -750,7 +759,8 @@ }, "corn_outskirtsEncountered": { "Sync": true, - "SyncType": "Server" + "SyncType": "Server", + "IgnoreSceneHost": true }, "corn_outskirtsLeft": { "Sync": true, @@ -758,7 +768,8 @@ }, "corn_royalGardensEncountered": { "Sync": true, - "SyncType": "Server" + "SyncType": "Server", + "IgnoreSceneHost": true }, "corn_royalGardensLeft": { "Sync": true, @@ -766,7 +777,8 @@ }, "corn_abyssEncountered": { "Sync": true, - "SyncType": "Server" + "SyncType": "Server", + "IgnoreSceneHost": true }, "corn_abyssLeft": { "Sync": true, @@ -789,7 +801,8 @@ }, "brettaRescued": { "Sync": true, - "SyncType": "Server" + "SyncType": "Server", + "IgnoreSceneHost": true }, "brettaPosition": { "Sync": true, @@ -809,11 +822,13 @@ }, "brettaSeenBenchDiary": { "Sync": true, - "SyncType": "Server" + "SyncType": "Server", + "IgnoreSceneHost": true }, "brettaSeenBedDiary": { "Sync": true, - "SyncType": "Server" + "SyncType": "Server", + "IgnoreSceneHost": true }, "brettaLeftTown": { "Sync": true, From 152c7d408dc007511f36922d7a4726069798dca4 Mon Sep 17 00:00:00 2001 From: Extremelyd1 <10898310+Extremelyd1@users.noreply.github.com> Date: Tue, 9 Jul 2024 20:29:11 +0200 Subject: [PATCH 093/216] Fix switches not activating for remote players, improvements to battle scenes --- HKMP/Fsm/FsmPatcher.cs | 34 ++++------- HKMP/Game/Client/Entity/Entity.cs | 78 +++++++++++++++++++----- HKMP/Game/Client/Entity/EntityManager.cs | 43 +++++++++++++ HKMP/Util/FsmUtilExt.cs | 20 ++++++ 4 files changed, 138 insertions(+), 37 deletions(-) diff --git a/HKMP/Fsm/FsmPatcher.cs b/HKMP/Fsm/FsmPatcher.cs index 59026d4f..9aaec8dc 100644 --- a/HKMP/Fsm/FsmPatcher.cs +++ b/HKMP/Fsm/FsmPatcher.cs @@ -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("Init", 6); - var findBrawler2 = self.GetAction("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")) { @@ -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("Range"); + } + + if (self.GetState("Check If Nail") != null) { + self.RemoveFirstAction("Check If Nail"); + } + } } } diff --git a/HKMP/Game/Client/Entity/Entity.cs b/HKMP/Game/Client/Entity/Entity.cs index af7ffefd..6faae271 100644 --- a/HKMP/Game/Client/Entity/Entity.cs +++ b/HKMP/Game/Client/Entity/Entity.cs @@ -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; @@ -79,13 +80,18 @@ internal class Entity { /// /// Whether the unity game object for the host entity was originally active. /// - private readonly bool _originalIsActive; + private bool _originalIsActive; /// /// Whether the entity is controlled, i.e. in control by updates from the server. /// private bool _isControlled; + /// + /// Whether the scene host is determined, or alternatively whether the entity has been determined to be a host or client entity. + /// + private bool _isSceneHostDetermined; + /// /// The last position of the entity. /// @@ -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> { Host = Object.Host.GetComponents().ToList(), @@ -654,12 +664,12 @@ private void OnUpdate() { } // Define a method that allows generalization of checking for changes in all FSM variables - void CondAddData( - VarType[] fsmVars, - BaseType[] snapshotArray, - Func fsmVarValue, + void CondAddData( + TVar[] fsmVars, + TBase[] snapshotArray, + Func fsmVarValue, EntityHostFsmData.Type type, - Dictionary dataDict + Dictionary dataDict ) { for (byte i = 0; i < fsmVars.Length; i++) { var fsmVar = fsmVars[i]; @@ -680,11 +690,11 @@ Dictionary 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; } } } @@ -803,6 +813,31 @@ private void ObjectPoolOnRecycleGameObject(On.ObjectPool.orig_Recycle_GameObject orig(obj); } + + /// + /// 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. + /// + 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; + } /// /// Initializes the entity when the client user is the scene host. @@ -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; + } + + /// + /// Initializes the entity when the client user is a scene client. Only sets a variable to indicate the scene + /// host has been determined. + /// + public void InitializeClient() { + _isSceneHostDetermined = true; + + // Deregister the hook for updating the active value of the host object + ActivateGameObject.DoActivateGameObject -= OnDoActivateGameObject; } /// @@ -1232,16 +1282,16 @@ public void UpdateHostFsmData(Dictionary hostFsmData) { var fsms = new[] { hostFsm, _fsms.Client[fsmIndex] }; - void CondUpdateVars( + void CondUpdateVars( EntityHostFsmData.Type type, - Dictionary dataDict, - FsmType[] fsmVarArray, - Action setValueAction + Dictionary dataDict, + TFsm[] fsmVarArray, + Action 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); } diff --git a/HKMP/Game/Client/Entity/EntityManager.cs b/HKMP/Game/Client/Entity/EntityManager.cs index a874b48b..6868b46d 100644 --- a/HKMP/Game/Client/Entity/EntityManager.cs +++ b/HKMP/Game/Client/Entity/EntityManager.cs @@ -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; @@ -56,6 +57,8 @@ public EntityManager(NetClient netClient) { EntityFsmActions.EntitySpawnEvent += OnGameObjectSpawned; UnityEngine.SceneManagement.SceneManager.sceneLoaded += OnSceneLoaded; UnityEngine.SceneManagement.SceneManager.activeSceneChanged += OnSceneChanged; + + FindGameObject.Find += OnFindGameObject; } /// @@ -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; @@ -454,4 +461,40 @@ private void FindEntitiesInScene(Scene scene, bool lateLoad) { }.Process(); } } + + /// + /// 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. + /// + 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"); + } } diff --git a/HKMP/Util/FsmUtilExt.cs b/HKMP/Util/FsmUtilExt.cs index d0ea16b6..110f4864 100644 --- a/HKMP/Util/FsmUtilExt.cs +++ b/HKMP/Util/FsmUtilExt.cs @@ -119,6 +119,26 @@ public static void RemoveAction(this PlayMakerFSM fsm, string stateName, int ind state.Actions = actions; } + + /// + /// Removes the first action in the given state of the given type from the FSM. + /// + /// The FSM. + /// The name of the state with the action to remove. + /// The type of the action to remove. + public static void RemoveFirstAction(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(); + } } /// From 9a0eae98f40fb691d987cc3419b06325b9fd3d4b Mon Sep 17 00:00:00 2001 From: Extremelyd1 <10898310+Extremelyd1@users.noreply.github.com> Date: Wed, 10 Jul 2024 20:21:30 +0200 Subject: [PATCH 094/216] Fix FSM util method --- HKMP/Util/FsmUtilExt.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/HKMP/Util/FsmUtilExt.cs b/HKMP/Util/FsmUtilExt.cs index 110f4864..e97cc985 100644 --- a/HKMP/Util/FsmUtilExt.cs +++ b/HKMP/Util/FsmUtilExt.cs @@ -75,7 +75,7 @@ public static FsmState GetState(this PlayMakerFSM fsm, string stateName) { /// The name of the state. /// The FSM action to insert. /// The index at which to insert the action. - public static void InsertAction(PlayMakerFSM fsm, string stateName, FsmStateAction action, int index) { + public static void InsertAction(this PlayMakerFSM fsm, string stateName, FsmStateAction action, int index) { foreach (FsmState t in fsm.FsmStates) { if (t.Name != stateName) continue; List actions = t.Actions.ToList(); @@ -131,7 +131,11 @@ public static void RemoveFirstAction(this PlayMakerFSM fsm, string stateName) var skipped = false; state.Actions = state.Actions.Where(a => { - if (!skipped && a.GetType() != typeof(T)) { + if (skipped) { + return true; + } + + if (a.GetType() == typeof(T)) { skipped = true; return false; } From 76957366079235d5407f881971baa77a0a51b572 Mon Sep 17 00:00:00 2001 From: Extremelyd1 <10898310+Extremelyd1@users.noreply.github.com> Date: Wed, 10 Jul 2024 20:56:28 +0200 Subject: [PATCH 095/216] Fix Greenpath Vine platforms not syncing --- HKMP/Fsm/FsmPatcher.cs | 18 +++++++++++++- HKMP/Game/Client/Save/PersistentFsmData.cs | 11 ++++---- HKMP/Game/Client/Save/SaveManager.cs | 29 ++++++++++++++-------- 3 files changed, 42 insertions(+), 16 deletions(-) diff --git a/HKMP/Fsm/FsmPatcher.cs b/HKMP/Fsm/FsmPatcher.cs index 9aaec8dc..73c1400b 100644 --- a/HKMP/Fsm/FsmPatcher.cs +++ b/HKMP/Fsm/FsmPatcher.cs @@ -1,6 +1,6 @@ using Hkmp.Util; -using Hkmp.Logging; using HutongGames.PlayMaker.Actions; +using Logger = Hkmp.Logging.Logger; namespace Hkmp.Fsm; @@ -55,5 +55,21 @@ private void OnFsmEnable(On.PlayMakerFSM.orig_OnEnable orig, PlayMakerFSM self) self.RemoveFirstAction("Check If Nail"); } } + + // Code for modifying the collision check on collapsing floors to include remote players (not working) + // if (self.name.Equals("Collapser Small") && self.Fsm.Name.Equals("collapse small")) { + // self.InsertAction("Idle", new Collision2dEventLayer { + // Enabled = true, + // collideLayer = 9, + // collideTag = new FsmString(), + // sendEvent = FsmEvent.GetFsmEvent("BREAK"), + // storeCollider = new FsmGameObject(), + // storeForce = new FsmFloat() + // }, 7); + // self.RemoveFirstAction("Idle"); + // + // var rigidbody = self.gameObject.AddComponent(); + // rigidbody.isKinematic = true; + // } } } diff --git a/HKMP/Game/Client/Save/PersistentFsmData.cs b/HKMP/Game/Client/Save/PersistentFsmData.cs index d862556e..83e27c13 100644 --- a/HKMP/Game/Client/Save/PersistentFsmData.cs +++ b/HKMP/Game/Client/Save/PersistentFsmData.cs @@ -1,3 +1,4 @@ +using System; using HutongGames.PlayMaker; namespace Hkmp.Game.Client.Save; @@ -12,13 +13,13 @@ internal class PersistentFsmData { public PersistentItemData PersistentItemData { get; set; } /// - /// The FSM variable for an integer. Could be null if a boolean is used instead. + /// The function to get the current integer value. Could be null if a boolean is used instead. /// - public FsmInt FsmInt { get; set; } + public Func CurrentInt { get; set; } /// - /// The FSM variable for a boolean. Could be null if an integer is used instead. + /// The function to get the current boolean value. Could be null if an integer is used instead. /// - public FsmBool FsmBool { get; set; } + public Func CurrentBool { get; set; } /// /// The last value for the integer if used. @@ -32,5 +33,5 @@ internal class PersistentFsmData { /// /// Whether an int is stored for this data. /// - public bool IsInt => FsmInt != null; + public bool IsInt => CurrentInt != null; } diff --git a/HKMP/Game/Client/Save/SaveManager.cs b/HKMP/Game/Client/Save/SaveManager.cs index 3cc03c8d..0f28ac4f 100644 --- a/HKMP/Game/Client/Save/SaveManager.cs +++ b/HKMP/Game/Client/Save/SaveManager.cs @@ -285,7 +285,7 @@ private void OnSceneChanged(Scene oldScene, Scene newScene) { var persistentFsmData = new PersistentFsmData { PersistentItemData = persistentItemData, - FsmInt = fsmInt, + CurrentInt = () => fsmInt.Value, LastIntValue = fsmInt.Value }; @@ -306,18 +306,27 @@ private void OnSceneChanged(Scene oldScene, Scene newScene) { Logger.Info($"Found persistent bool in scene: {persistentItemData}"); + Func currentBoolFunc = null; + var fsm = FSMUtility.FindFSMWithPersistentBool(itemObject.GetComponents()); - if (fsm == null) { - Logger.Info(" Could not find FSM belonging to persistent bool object, skipping"); - continue; + if (fsm != null) { + var fsmBool = fsm.FsmVariables.GetFsmBool("Activated"); + currentBoolFunc = () => fsmBool.Value; } - var fsmBool = fsm.FsmVariables.GetFsmBool("Activated"); + var vinePlatform = itemObject.GetComponent(); + if (vinePlatform != null) { + currentBoolFunc = () => ReflectionHelper.GetField(vinePlatform, "activated"); + } + if (currentBoolFunc == null) { + continue; + } + var persistentFsmData = new PersistentFsmData { PersistentItemData = persistentItemData, - FsmBool = fsmBool, - LastBoolValue = fsmBool.Value + CurrentBool = currentBoolFunc, + LastBoolValue = currentBoolFunc.Invoke() }; _persistentFsmData.Add(persistentFsmData); @@ -347,7 +356,7 @@ private void OnSceneChanged(Scene oldScene, Scene newScene) { var persistentFsmData = new PersistentFsmData { PersistentItemData = persistentItemData, - FsmInt = fsmInt, + CurrentInt = () => fsmInt.Value, LastIntValue = fsmInt.Value }; @@ -408,7 +417,7 @@ private void OnUpdatePersistents() { } if (persistentFsmData.IsInt) { - var value = persistentFsmData.FsmInt.Value; + var value = persistentFsmData.CurrentInt.Invoke(); if (value == persistentFsmData.LastIntValue) { continue; } @@ -469,7 +478,7 @@ private void OnUpdatePersistents() { Logger.Info("Cannot find persistent int/geo rock data bool, not sending save update"); } } else { - var value = persistentFsmData.FsmBool.Value; + var value = persistentFsmData.CurrentBool.Invoke(); if (value == persistentFsmData.LastBoolValue) { continue; } From 8e9cc13362c9c4bdecdc18cf8fe7d32764e1b4bc Mon Sep 17 00:00:00 2001 From: Extremelyd1 <10898310+Extremelyd1@users.noreply.github.com> Date: Fri, 12 Jul 2024 16:21:30 +0200 Subject: [PATCH 096/216] Fix bugs with Mantis Lords --- .../Client/Entity/Component/ChildrenActivationComponent.cs | 4 ++-- HKMP/Game/Client/Entity/Entity.cs | 7 ++++--- HKMP/Game/Client/Entity/EntityInitializer.cs | 3 ++- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/HKMP/Game/Client/Entity/Component/ChildrenActivationComponent.cs b/HKMP/Game/Client/Entity/Component/ChildrenActivationComponent.cs index 1020786e..08644aab 100644 --- a/HKMP/Game/Client/Entity/Component/ChildrenActivationComponent.cs +++ b/HKMP/Game/Client/Entity/Component/ChildrenActivationComponent.cs @@ -7,7 +7,7 @@ namespace Hkmp.Game.Client.Entity.Component; /// -/// This component manages the gravity scale of an entity. +/// This component manages the activation of the children of an entity. internal class ChildrenActivationComponent : EntityComponent { private readonly List _hostChildren; private readonly List _clientChildren; @@ -80,4 +80,4 @@ public override void Update(EntityNetworkData data) { public override void Destroy() { MonoBehaviourUtil.Instance.OnUpdateEvent -= OnUpdate; } -} \ No newline at end of file +} diff --git a/HKMP/Game/Client/Entity/Entity.cs b/HKMP/Game/Client/Entity/Entity.cs index 6faae271..cfa920ec 100644 --- a/HKMP/Game/Client/Entity/Entity.cs +++ b/HKMP/Game/Client/Entity/Entity.cs @@ -820,7 +820,7 @@ private void ObjectPoolOnRecycleGameObject(On.ObjectPool.orig_Recycle_GameObject /// 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) { + if (self.Fsm.GetOwnerDefaultTarget(self.gameObject) != Object.Host || Object.Host == null) { orig(self); return; } @@ -951,9 +951,10 @@ public void MakeHost() { // We need to set the isKinematic property of rigid bodies to ensure physics work again after enabling // the host object. In Hornet 1 this is necessary because another state sets this property normally in the - // fight. See the "Wake" or "Refight Ready" state of the "Control" FSM on Hornet 1 + // fight. See the "Wake" or "Refight Ready" state of the "Control" FSM on Hornet 1. + // In the Mantis Lord entity, this should never be disabled, since they are always kinematic. var rigidBody = Object.Host.GetComponent(); - if (rigidBody != null) { + if (rigidBody != null && Type != EntityType.MantisLord) { Logger.Debug(" Resetting isKinematic of Rigidbody to ensure physics work for host object"); rigidBody.isKinematic = false; } diff --git a/HKMP/Game/Client/Entity/EntityInitializer.cs b/HKMP/Game/Client/Entity/EntityInitializer.cs index a730928f..8327f46a 100644 --- a/HKMP/Game/Client/Entity/EntityInitializer.cs +++ b/HKMP/Game/Client/Entity/EntityInitializer.cs @@ -44,7 +44,8 @@ internal static class EntityInitializer { /// Array of types of actions that should be skipped during initialization. /// private static readonly Type[] ToSkipTypes = { - typeof(Tk2dPlayAnimation) + typeof(Tk2dPlayAnimation), + typeof(ActivateAllChildren) }; /// From 56f70cfaa20b2ee7f3aacf5211552ae13e7e2b5b Mon Sep 17 00:00:00 2001 From: Extremelyd1 <10898310+Extremelyd1@users.noreply.github.com> Date: Fri, 12 Jul 2024 18:21:41 +0200 Subject: [PATCH 097/216] Fix challenge prompt for Mantis Lords --- .../Component/ChallengePromptComponent.cs | 55 +++++++++++++++++++ .../Entity/Component/ComponentFactory.cs | 2 + .../Entity/Component/EntityComponent.cs | 5 +- HKMP/Networking/Packet/Data/EntityUpdate.cs | 4 +- HKMP/Resource/entity-registry.json | 3 + 5 files changed, 65 insertions(+), 4 deletions(-) create mode 100644 HKMP/Game/Client/Entity/Component/ChallengePromptComponent.cs diff --git a/HKMP/Game/Client/Entity/Component/ChallengePromptComponent.cs b/HKMP/Game/Client/Entity/Component/ChallengePromptComponent.cs new file mode 100644 index 00000000..ddd7f75b --- /dev/null +++ b/HKMP/Game/Client/Entity/Component/ChallengePromptComponent.cs @@ -0,0 +1,55 @@ +using Hkmp.Networking.Client; +using Hkmp.Networking.Packet.Data; +using Hkmp.Util; +using UnityEngine; + +namespace Hkmp.Game.Client.Entity.Component; + +/// +/// This component manages the challenge prompt that appears for Mantis Lords. +internal class ChallengePromptComponent : EntityComponent { + /// + /// The game object that handles the challenge prompt pop-up. + /// + private readonly GameObject _promptObj; + + public ChallengePromptComponent( + NetClient netClient, + ushort entityId, + HostClientPair gameObject + ) : base(netClient, entityId, gameObject) { + var hostObj = gameObject.Host; + var parent = hostObj.transform.parent; + _promptObj = parent.Find("Challenge Prompt").gameObject; + + var promptFsm = _promptObj.LocateMyFSM("Challenge Start"); + promptFsm.InsertMethod("Take Control", 6, () => { + var data = new EntityNetworkData { + Type = EntityComponentType.ChallengePrompt + }; + data.Packet.Write(true); + + SendData(data); + }); + } + + /// + public override void InitializeHost() { + } + + /// + public override void Update(EntityNetworkData data) { + var destroyPrompt = data.Packet.ReadBool(); + if (!destroyPrompt) { + return; + } + + if (_promptObj != null) { + Object.Destroy(_promptObj); + } + } + + /// + public override void Destroy() { + } +} diff --git a/HKMP/Game/Client/Entity/Component/ComponentFactory.cs b/HKMP/Game/Client/Entity/Component/ComponentFactory.cs index 497a8506..bc9c06e3 100644 --- a/HKMP/Game/Client/Entity/Component/ComponentFactory.cs +++ b/HKMP/Game/Client/Entity/Component/ComponentFactory.cs @@ -63,6 +63,8 @@ HostClientPair objects Client = spriteRendererClient, Host = spriteRendererHost }); + case EntityComponentType.ChallengePrompt: + return new ChallengePromptComponent(netClient, entityId, objects); default: throw new ArgumentOutOfRangeException(nameof(type), type, $"Could not instantiate entity component for type: {type}"); } diff --git a/HKMP/Game/Client/Entity/Component/EntityComponent.cs b/HKMP/Game/Client/Entity/Component/EntityComponent.cs index 8b72e63a..236b00fe 100644 --- a/HKMP/Game/Client/Entity/Component/EntityComponent.cs +++ b/HKMP/Game/Client/Entity/Component/EntityComponent.cs @@ -69,7 +69,7 @@ protected void SendData(EntityNetworkData data) { /// Enum for data types. /// [JsonConverter(typeof(StringEnumConverter))] -internal enum EntityComponentType : byte { +internal enum EntityComponentType : ushort { Fsm = 0, Death, Invincibility, @@ -85,4 +85,5 @@ internal enum EntityComponentType : byte { ChildrenActivation, SpawnJar, SpriteRenderer, -} \ No newline at end of file + ChallengePrompt +} diff --git a/HKMP/Networking/Packet/Data/EntityUpdate.cs b/HKMP/Networking/Packet/Data/EntityUpdate.cs index 10571035..f6b91db4 100644 --- a/HKMP/Networking/Packet/Data/EntityUpdate.cs +++ b/HKMP/Networking/Packet/Data/EntityUpdate.cs @@ -545,7 +545,7 @@ public EntityNetworkData() { /// public void WriteData(IPacket packet) { - packet.Write((byte) Type); + packet.Write((ushort) Type); var data = Packet.ToArray(); @@ -563,7 +563,7 @@ public void WriteData(IPacket packet) { /// public void ReadData(IPacket packet) { - Type = (EntityComponentType) packet.ReadByte(); + Type = (EntityComponentType) packet.ReadUShort(); var length = packet.ReadUShort(); var data = new byte[length]; diff --git a/HKMP/Resource/entity-registry.json b/HKMP/Resource/entity-registry.json index cbc9bef4..5bcf8200 100644 --- a/HKMP/Resource/entity-registry.json +++ b/HKMP/Resource/entity-registry.json @@ -395,6 +395,9 @@ "type": "MantisLord", "fsm_name": "Mantis Lord" } + ], + "components": [ + "ChallengePrompt" ] }, { From 4d3b60daf37dc220ba174e816b4ee4d996c9f231 Mon Sep 17 00:00:00 2001 From: Extremelyd1 <10898310+Extremelyd1@users.noreply.github.com> Date: Fri, 12 Jul 2024 22:40:10 +0200 Subject: [PATCH 098/216] Fix issue with Grub Mimics --- .../Client/Entity/Action/EntityFsmActions.cs | 76 +++++++++++++++++++ 1 file changed, 76 insertions(+) diff --git a/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs b/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs index 5ec9eacd..d77b528e 100644 --- a/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs +++ b/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs @@ -2815,6 +2815,82 @@ IEnumerator Behaviour() { #endregion + #region SendMessage + + private static bool GetNetworkDataFromAction(EntityNetworkData data, SendMessage action) { + return true; + } + + private static void ApplyNetworkDataFromAction(EntityNetworkData data, SendMessage action) { + var gameObject = action.Fsm.GetOwnerDefaultTarget(action.gameObject); + if (gameObject == null) { + return; + } + + object parameter = null; + switch (action.functionCall.ParameterType) { + case "Array": + parameter = action.functionCall.ArrayParameter.Values; + break; + case "Color": + parameter = action.functionCall.ColorParameter.Value; + break; + case "Enum": + parameter = action.functionCall.EnumParameter.Value; + break; + case "GameObject": + parameter = action.functionCall.GameObjectParameter.Value; + break; + case "Material": + parameter = action.functionCall.MaterialParameter.Value; + break; + case "Object": + parameter = action.functionCall.ObjectParameter.Value; + break; + case "Quaternion": + parameter = action.functionCall.QuaternionParameter.Value; + break; + case "Rect": + parameter = action.functionCall.RectParamater.Value; + break; + case "Texture": + parameter = action.functionCall.TextureParameter.Value; + break; + case "Vector2": + parameter = action.functionCall.Vector2Parameter.Value; + break; + case "Vector3": + parameter = action.functionCall.Vector3Parameter.Value; + break; + case "bool": + parameter = action.functionCall.BoolParameter.Value; + break; + case "float": + parameter = action.functionCall.FloatParameter.Value; + break; + case "int": + parameter = action.functionCall.IntParameter.Value; + break; + case "string": + parameter = action.functionCall.StringParameter.Value; + break; + } + + switch (action.delivery) { + case SendMessage.MessageType.SendMessage: + gameObject.SendMessage(action.functionCall.FunctionName, parameter, action.options); + break; + case SendMessage.MessageType.SendMessageUpwards: + gameObject.SendMessageUpwards(action.functionCall.FunctionName, parameter, action.options); + break; + case SendMessage.MessageType.BroadcastMessage: + gameObject.BroadcastMessage(action.functionCall.FunctionName, parameter, action.options); + break; + } + } + + #endregion + /// /// Class that keeps track of an action that executes while in a certain state of the FSM. /// From e5a115d32fa0391d65a966c0b69a438f7a14eaff Mon Sep 17 00:00:00 2001 From: Extremelyd1 <10898310+Extremelyd1@users.noreply.github.com> Date: Sun, 14 Jul 2024 16:41:09 +0200 Subject: [PATCH 099/216] Fix remote attacks having physics --- HKMP/Game/Client/PlayerManager.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/HKMP/Game/Client/PlayerManager.cs b/HKMP/Game/Client/PlayerManager.cs index c34b414a..b387e199 100644 --- a/HKMP/Game/Client/PlayerManager.cs +++ b/HKMP/Game/Client/PlayerManager.cs @@ -165,6 +165,7 @@ private void CreatePlayerPool() { var rigidbody = attacks.AddComponent(); rigidbody.collisionDetectionMode = CollisionDetectionMode2D.Continuous; rigidbody.gravityScale = 0; + rigidbody.isKinematic = true; new GameObject("Effects") { layer = 9 }.transform.SetParent(playerPrefab.transform); new GameObject("Spells") { layer = 9 }.transform.SetParent(playerPrefab.transform); From 7f32dcac25775dfca0a02028a0e94a7fe30ea6b3 Mon Sep 17 00:00:00 2001 From: Extremelyd1 <10898310+Extremelyd1@users.noreply.github.com> Date: Sun, 14 Jul 2024 16:41:37 +0200 Subject: [PATCH 100/216] Add camera lock areas to entity registry --- HKMP/Game/Client/Entity/EntityManager.cs | 21 ++++++++++++--------- HKMP/Game/Client/Entity/EntityType.cs | 1 + HKMP/Resource/entity-registry.json | 4 ++++ 3 files changed, 17 insertions(+), 9 deletions(-) diff --git a/HKMP/Game/Client/Entity/EntityManager.cs b/HKMP/Game/Client/Entity/EntityManager.cs index 6868b46d..acc66786 100644 --- a/HKMP/Game/Client/Entity/EntityManager.cs +++ b/HKMP/Game/Client/Entity/EntityManager.cs @@ -376,17 +376,11 @@ private void OnSceneLoaded(Scene scene, LoadSceneMode mode) { /// Whether this scene was loaded late. private void FindEntitiesInScene(Scene scene, bool lateLoad) { // Find all EnemyDeathEffects components - // Filter out EnemyDeathEffects components not in the current scene - // Project each death effect to their GameObject and the corpse of the pre-instantiated EnemyDeathEffects - // component - // Concatenate all GameObjects for PlayMakerFSM components in the current scene, and check whether it is the - // FSM for a Colosseum Cage, in which case we pre-instantiate the enemy inside and concatenate it as well - // Project each GameObject into its children including itself - // Concatenate all GameObjects for Climber components (Tiktiks) - // Concatenate all GameObjects for Walker components (Amblooms) - // Filter out GameObjects not in the current scene var objectsToCheck = Object.FindObjectsOfType() + // Filter out EnemyDeathEffects components not in the current scene .Where(e => e.gameObject.scene == scene) + // Project each death effect to their GameObject and the corpse of the pre-instantiated EnemyDeathEffects + // component .SelectMany(enemyDeathEffects => { try { enemyDeathEffects.PreInstantiate(); @@ -404,6 +398,8 @@ private void FindEntitiesInScene(Scene scene, bool lateLoad) { return new[] { enemyDeathEffects.gameObject, corpse }; }) + // Concatenate all GameObjects for PlayMakerFSM components in the current scene, and check whether it is the + // FSM for a Colosseum Cage, in which case we pre-instantiate the enemy inside and concatenate it as well .Concat(Object.FindObjectsOfType(true) .Where(fsm => fsm.gameObject.scene == scene) .SelectMany(fsm => { @@ -445,10 +441,17 @@ private void FindEntitiesInScene(Scene scene, bool lateLoad) { return new[] { fsm.gameObject, createdObject }; }) ) + // Project each GameObject into its children including itself .SelectMany(obj => obj == null ? Array.Empty() : obj.GetChildren().Prepend(obj)) + // Concatenate all GameObjects for Climber components (Tiktiks) .Concat(Object.FindObjectsOfType(true).Select(climber => climber.gameObject)) + // Concatenate all GameObjects for Walker components (Amblooms) .Concat(Object.FindObjectsOfType(true).Select(walker => walker.gameObject)) + // Concatenate all GameObjects for BigCentipede components (Garpedes) .Concat(Object.FindObjectsOfType(true).Select(centipede => centipede.gameObject)) + // Concatenate all GameObjects for CameraLockArea components + .Concat(Object.FindObjectsOfType(true).Select(cameraLockArea => cameraLockArea.gameObject)) + // Filter out GameObjects not in the current scene .Where(obj => obj.scene == scene) .Distinct(); diff --git a/HKMP/Game/Client/Entity/EntityType.cs b/HKMP/Game/Client/Entity/EntityType.cs index 5258ab1a..6f4509eb 100644 --- a/HKMP/Game/Client/Entity/EntityType.cs +++ b/HKMP/Game/Client/Entity/EntityType.cs @@ -5,6 +5,7 @@ namespace Hkmp.Game.Client.Entity; /// internal enum EntityType { BattleGate = 0, + CameraLockArea, Crawlid, Tiktik, Vengefly, diff --git a/HKMP/Resource/entity-registry.json b/HKMP/Resource/entity-registry.json index 5bcf8200..41125da3 100644 --- a/HKMP/Resource/entity-registry.json +++ b/HKMP/Resource/entity-registry.json @@ -9,6 +9,10 @@ "type": "BattleGate", "fsm_name": "Control" }, + { + "base_object_name": "CameraLockArea", + "type": "CameraLockArea" + }, { "base_object_name": "Crawler", "type": "Crawlid", From f27d3fa825a57f52422f607360182258c3a35f44 Mon Sep 17 00:00:00 2001 From: Extremelyd1 <10898310+Extremelyd1@users.noreply.github.com> Date: Mon, 15 Jul 2024 19:06:47 +0200 Subject: [PATCH 101/216] Fix Nosk glob projectiles --- .../Client/Entity/Action/EntityFsmActions.cs | 80 ++++++++++++++++++- HKMP/Game/Client/Entity/EntitySpawner.cs | 55 +++++++++++++ HKMP/Game/Client/Entity/EntityType.cs | 1 + HKMP/Resource/entity-registry.json | 5 ++ 4 files changed, 140 insertions(+), 1 deletion(-) diff --git a/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs b/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs index d77b528e..61034870 100644 --- a/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs +++ b/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs @@ -210,8 +210,16 @@ private static void EmitRandomInterceptInstructions(ILCursor c) // Push the current instance of the class onto the stack c.Emit(OpCodes.Ldarg_0); - // Emit a delegate that pops the current int off the stack (our random value) and + // 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 gameObject = ReflectionHelper.GetField(instance, "gameObject"); + if (gameObject != null && IsObjectInRegistry(gameObject)) { + return value; + } + if (!RandomActionValues.TryGetValue(instance, out var queue)) { queue = new Queue(); RandomActionValues[instance] = queue; @@ -238,6 +246,32 @@ private static void FlingObjectsFromGlobalPoolOnEnter(ILContext il) { EmitRandomInterceptInstructions(c); EmitRandomInterceptInstructions(c); EmitRandomInterceptInstructions(c); + + // Reset cursor + c = new ILCursor(il); + + // Goto the next call instruction for ObjectPoolExtensions.Spawn + c.GotoNext(i => i.MatchCall(typeof(ObjectPoolExtensions), "Spawn")); + + // 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 spawned game object off the stack and uses it, then puts it back again + c.EmitDelegate>((go, action) => { + Logger.Debug($"Delegate of FlingObjectsFromGlobalPool: {go.name}"); + if (EntitySpawnEvent != null && EntitySpawnEvent.Invoke(new EntitySpawnDetails { + Type = EntitySpawnType.FsmAction, + Action = action, + GameObject = go + })) { + Logger.Debug("FlingObjectsFromGlobalPool IL spawned object is entity"); + } + + return go; + }); } catch (Exception e) { Logger.Error($"Could not change FlingObjectsFromGlobalPool#OnEnter IL:\n{e}"); } @@ -258,6 +292,32 @@ private static void FlingObjectsFromGlobalPoolVelOnEnter(ILContext il) { EmitRandomInterceptInstructions(c); EmitRandomInterceptInstructions(c); EmitRandomInterceptInstructions(c); + + // Reset cursor + c = new ILCursor(il); + + // Goto the next call instruction for ObjectPoolExtensions.Spawn + c.GotoNext(i => i.MatchCall(typeof(ObjectPoolExtensions), "Spawn")); + + // 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 spawned game object off the stack and uses it, then puts it back again + c.EmitDelegate>((go, action) => { + Logger.Debug($"Delegate of FlingObjectsFromGlobalPoolVel: {go.name}"); + if (EntitySpawnEvent != null && EntitySpawnEvent.Invoke(new EntitySpawnDetails { + Type = EntitySpawnType.FsmAction, + Action = action, + GameObject = go + })) { + Logger.Debug("FlingObjectsFromGlobalPoolVel IL spawned object is entity"); + } + + return go; + }); } catch (Exception e) { Logger.Error($"Could not change FlingObjectsFromGlobalPoolVel#OnEnter IL:\n{e}"); } @@ -408,6 +468,15 @@ private static void ApplyNetworkDataFromAction(EntityNetworkData data, SpawnObje #region FlingObjectsFromGlobalPool private static bool GetNetworkDataFromAction(EntityNetworkData data, FlingObjectsFromGlobalPool action) { + // We first check whether the game object belonging to the Rigidbody2D in the action is an object that is + // managed by the system. Because if so, it means that we have already caught its spawning in the IL hook + // for the action and sent an EntitySpawn packet instead. So we need not also network this action separately. + var rigidbody = ReflectionHelper.GetField(action, "rb2d"); + if (rigidbody != null && rigidbody.gameObject != null && IsObjectInRegistry(rigidbody.gameObject)) { + Logger.Debug("Skipping getting network data for FlingObjectsFromGlobalPool, because spawned objects are managed by system"); + return false; + } + var position = Vector3.zero; var spawnPoint = action.spawnPoint.Value; @@ -521,6 +590,15 @@ private static void ApplyNetworkDataFromAction(EntityNetworkData data, FlingObje #region FlingObjectsFromGlobalPoolVel private static bool GetNetworkDataFromAction(EntityNetworkData data, FlingObjectsFromGlobalPoolVel action) { + // We first check whether the game object belonging to the Rigidbody2D in the action is an object that is + // managed by the system. Because if so, it means that we have already caught its spawning in the IL hook + // for the action and sent an EntitySpawn packet instead. So we need not also network this action separately. + var rigidbody = ReflectionHelper.GetField(action, "rb2d"); + if (rigidbody != null && rigidbody.gameObject != null && IsObjectInRegistry(rigidbody.gameObject)) { + Logger.Debug("Skipping getting network data for FlingObjectsFromGlobalPool, because spawned objects are managed by system"); + return false; + } + var position = Vector3.zero; var spawnPoint = action.spawnPoint.Value; diff --git a/HKMP/Game/Client/Entity/EntitySpawner.cs b/HKMP/Game/Client/Entity/EntitySpawner.cs index 79fbd159..2cbe6007 100644 --- a/HKMP/Game/Client/Entity/EntitySpawner.cs +++ b/HKMP/Game/Client/Entity/EntitySpawner.cs @@ -153,6 +153,22 @@ List clientFsms return SpawnRadianceNailFromComb(clientFsms[0]); } + if (spawningType == EntityType.WingedNosk) { + if (spawnedType == EntityType.InfectedBalloon) { + return SpawnInfectedBalloonWingedNoskObject(clientFsms[0]); + } + + if (spawnedType == EntityType.NoskBlob) { + return SpawnWingedNoskBlobObject(clientFsms[0]); + } + } + + if (spawningType == EntityType.WingedNoskGlobDropper && spawnedType == EntityType.NoskBlob) { + return SpawnWingedNoskGlobDropper(clientFsms[0]); + } + + Logger.Warn($"No implementation for spawning entity game object: {spawningType}, {spawnedType}"); + return null; } @@ -217,6 +233,24 @@ private static GameObject SpawnFromGlobalPool(SpawnObjectFromGlobalPool action, return spawnedObject; } + private static GameObject SpawnFromFlingGlobalPool(FlingObjectsFromGlobalPool action, GameObject gameObject) { + var position = Vector3.zero; + var zero = Vector3.zero; + if (action.spawnPoint.Value != null) { + position = action.spawnPoint.Value.transform.position; + if (!action.position.IsNone) { + position += action.position.Value; + } + } else { + if (!action.position.IsNone) { + position = action.position.Value; + } + } + + var spawnedObject = gameObject.Spawn(position, Quaternion.Euler(zero)); + return spawnedObject; + } + private static GameObject SpawnFromFlingGlobalPoolTime( FlingObjectsFromGlobalPoolTime action, GameObject gameObject @@ -483,4 +517,25 @@ private static GameObject SpawnRadianceNailFromComb(PlayMakerFSM fsm) { return SpawnFromGlobalPool(action, gameObject); } + + private static GameObject SpawnInfectedBalloonWingedNoskObject(PlayMakerFSM fsm) { + var action = fsm.GetFirstAction("Summon"); + var gameObject = action.gameObject.Value; + + return SpawnFromGlobalPool(action, gameObject); + } + + private static GameObject SpawnWingedNoskBlobObject(PlayMakerFSM fsm) { + var action = fsm.GetFirstAction("Spit 1"); + var gameObject = action.gameObject.Value; + + return SpawnFromFlingGlobalPool(action, gameObject); + } + + private static GameObject SpawnWingedNoskGlobDropper(PlayMakerFSM fsm) { + var action = fsm.GetFirstAction("Drop"); + var gameObject = action.gameObject.Value; + + return SpawnFromFlingGlobalPool(action, gameObject); + } } diff --git a/HKMP/Game/Client/Entity/EntityType.cs b/HKMP/Game/Client/Entity/EntityType.cs index 6f4509eb..20bfcb17 100644 --- a/HKMP/Game/Client/Entity/EntityType.cs +++ b/HKMP/Game/Client/Entity/EntityType.cs @@ -222,6 +222,7 @@ internal enum EntityType { PureVessel, PureVesselBlast, WingedNosk, + WingedNoskGlobDropper, TurretZoteling, LankyZoteling, HeadOfZote, diff --git a/HKMP/Resource/entity-registry.json b/HKMP/Resource/entity-registry.json index 41125da3..5c88ee37 100644 --- a/HKMP/Resource/entity-registry.json +++ b/HKMP/Resource/entity-registry.json @@ -1320,6 +1320,11 @@ "type": "WingedNosk", "fsm_name": "Hornet Nosk" }, + { + "base_object_name": "Glob Dropper", + "type": "WingedNoskGlobDropper", + "fsm_name": "Dropper" + }, { "base_object_name": "Zote Turret", "type": "TurretZoteling", From c248c55d45b7ac6cbb6e8d36d0a3f39db8ad05fe Mon Sep 17 00:00:00 2001 From: Extremelyd1 <10898310+Extremelyd1@users.noreply.github.com> Date: Mon, 15 Jul 2024 19:48:43 +0200 Subject: [PATCH 102/216] Fix issues with Sly fight --- .../Client/Entity/Action/EntityFsmActions.cs | 40 +++++++++++++++++++ HKMP/Resource/entity-registry.json | 5 ++- 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs b/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs index 61034870..8b2e032e 100644 --- a/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs +++ b/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs @@ -2967,6 +2967,46 @@ private static void ApplyNetworkDataFromAction(EntityNetworkData data, SendMessa } } + #endregion + + #region SetCircleCollider + + private static bool GetNetworkDataFromAction(EntityNetworkData data, SetCircleCollider action) { + return action.gameObject != null; + } + + private static void ApplyNetworkDataFromAction(EntityNetworkData data, SetCircleCollider action) { + var gameObject = action.Fsm.GetOwnerDefaultTarget(action.gameObject); + if (gameObject == null) { + return; + } + + var collider = gameObject.GetComponent(); + if (collider != null) { + collider.enabled = action.active.Value; + } + } + + #endregion + + #region SetPolygonCollider + + private static bool GetNetworkDataFromAction(EntityNetworkData data, SetPolygonCollider action) { + return action.gameObject != null; + } + + private static void ApplyNetworkDataFromAction(EntityNetworkData data, SetPolygonCollider action) { + var gameObject = action.Fsm.GetOwnerDefaultTarget(action.gameObject); + if (gameObject == null) { + return; + } + + var collider = gameObject.GetComponent(); + if (collider != null) { + collider.enabled = action.active.Value; + } + } + #endregion /// diff --git a/HKMP/Resource/entity-registry.json b/HKMP/Resource/entity-registry.json index 5c88ee37..d971e1e5 100644 --- a/HKMP/Resource/entity-registry.json +++ b/HKMP/Resource/entity-registry.json @@ -1303,7 +1303,10 @@ { "base_object_name": "Sly Boss", "type": "Sly", - "fsm_name": "Control" + "fsm_name": "Control", + "components": [ + "GravityScale" + ] }, { "base_object_name": "HK Prime", From 7cb9fc7908d4f615780fea347fc4f835582f8c30 Mon Sep 17 00:00:00 2001 From: Extremelyd1 <10898310+Extremelyd1@users.noreply.github.com> Date: Mon, 15 Jul 2024 23:11:22 +0200 Subject: [PATCH 103/216] Rename some entities --- HKMP/Game/Client/Entity/EntityType.cs | 4 +++- HKMP/Resource/entity-registry.json | 10 ++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/HKMP/Game/Client/Entity/EntityType.cs b/HKMP/Game/Client/Entity/EntityType.cs index 20bfcb17..ccdf9333 100644 --- a/HKMP/Game/Client/Entity/EntityType.cs +++ b/HKMP/Game/Client/Entity/EntityType.cs @@ -229,5 +229,7 @@ internal enum EntityType { FlukeZoteling, ZoteCurse, HeavyZoteling, - AbsoluteRadiance + AbsoluteRadiance, + RadiancePlatform, + RadianceAbyss } diff --git a/HKMP/Resource/entity-registry.json b/HKMP/Resource/entity-registry.json index d971e1e5..9ded791b 100644 --- a/HKMP/Resource/entity-registry.json +++ b/HKMP/Resource/entity-registry.json @@ -1362,5 +1362,15 @@ "base_object_name": "Absolute Radiance", "type": "AbsoluteRadiance", "fsm_name": "Control" + }, + { + "base_object_name": "Radiant Plat", + "type": "RadiancePlatform", + "fsm_name": "radiant_plat" + }, + { + "base_object_name": "Abyss Pit", + "type": "RadianceAbyss", + "fsm_name": "Ascend" } ] From 787d40ede6663190bbbf84b0a6cf9d617cdcb35f Mon Sep 17 00:00:00 2001 From: Extremelyd1 <10898310+Extremelyd1@users.noreply.github.com> Date: Wed, 17 Jul 2024 18:43:12 +0200 Subject: [PATCH 104/216] Fix various save data issues --- HKMP/Game/Client/Entity/EntityManager.cs | 2 +- HKMP/Resource/scene-data.json | 40 +++++++++++++++++++++++- 2 files changed, 40 insertions(+), 2 deletions(-) diff --git a/HKMP/Game/Client/Entity/EntityManager.cs b/HKMP/Game/Client/Entity/EntityManager.cs index acc66786..446e9c76 100644 --- a/HKMP/Game/Client/Entity/EntityManager.cs +++ b/HKMP/Game/Client/Entity/EntityManager.cs @@ -489,7 +489,7 @@ private void OnFindGameObject(FindGameObject.orig_Find orig, HutongGames.PlayMak // 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) { + if (obj != null && 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; diff --git a/HKMP/Resource/scene-data.json b/HKMP/Resource/scene-data.json index c0a76a5c..9ee703a5 100644 --- a/HKMP/Resource/scene-data.json +++ b/HKMP/Resource/scene-data.json @@ -580,5 +580,43 @@ "WHITE_PALACE", "TRAM_UPPER", "TRAM_LOWER", - "Death Respawn Marker" + "Death Respawn Marker", + "Gruz Boss Scene", + "False Knight Boss Scene", + "Mega Moss Charger Boss Scene", + "Hornet 1 Boss Scene", + "Gorb Boss Scene", + "White Defender Boss Scene", + "Dung Defender Boss Scene", + "Brooding Mawlek Boss Scene", + "Nailmaster Oro Mato Boss Scene", + "Xero Boss Scene", + "Crystal Guardian Boss Scene", + "Soul Master Boss Scene", + "Marmu Boss Scene", + "Nosk Boss Scene", + "Flukemarm Boss Scene", + "Broken Vessel Boss Scene", + "Paintmaster Boss Scene", + "Hive Knight Boss Scene", + "Elder Hu Boss Scene", + "Grimm Boss Scene", + "Galien Boss Scene", + "Hornet 2 Boss Scene", + "Nailsage Sly Boss Scene", + "Crystal Guardian 2 Boss Scene", + "Lost Kin Boss Scene", + "Failed Champion Boss Scene", + "Nosk Boss Scene V", + "Traitor Lord Boss Scene", + "No Eyes Boss Scene", + "Markoth Boss Scene", + "Nightmare Grimm Boss Scene", + "Soul Tyrant Boss Scene", + "Hollow Knight Boss Scene", + "Mantis Lords Boss Scene V", + "Nosk Hornet Boss Scene", + "Radiance Boss Scene", + "Oblobbles Boss Scene", + "God Tamer Boss Scene" ] From e135b155c0201e08aa3afd4518ed7ded4306e271 Mon Sep 17 00:00:00 2001 From: Extremelyd1 <10898310+Extremelyd1@users.noreply.github.com> Date: Wed, 17 Jul 2024 20:47:33 +0200 Subject: [PATCH 105/216] Fix issue with Pantheons --- HKMP/Game/Client/Entity/Entity.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/HKMP/Game/Client/Entity/Entity.cs b/HKMP/Game/Client/Entity/Entity.cs index cfa920ec..ac708d9e 100644 --- a/HKMP/Game/Client/Entity/Entity.cs +++ b/HKMP/Game/Client/Entity/Entity.cs @@ -484,7 +484,10 @@ private void HandleEnemyDeathEffects() { /// fights will end on scene clients if the client objects die. /// private void CheckGodhome() { - var bossSceneController = UnityEngine.Object.FindObjectOfType(); + var bossSceneControllers = UnityEngine.Object.FindObjectsOfType(); + var bossSceneController = bossSceneControllers.FirstOrDefault( + con => con.gameObject.scene.Equals(UnityEngine.SceneManagement.SceneManager.GetActiveScene()) + ); if (bossSceneController == null) { return; } From 213a746691d3266b57acbee660743ba5bd4c1de3 Mon Sep 17 00:00:00 2001 From: Extremelyd1 <10898310+Extremelyd1@users.noreply.github.com> Date: Wed, 17 Jul 2024 20:47:43 +0200 Subject: [PATCH 106/216] Fix Godhome Gorb --- HKMP/Resource/entity-registry.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/HKMP/Resource/entity-registry.json b/HKMP/Resource/entity-registry.json index 9ded791b..6ee9bfc9 100644 --- a/HKMP/Resource/entity-registry.json +++ b/HKMP/Resource/entity-registry.json @@ -1041,7 +1041,7 @@ { "base_object_name": "Ghost Warrior Slug", "type": "Gorb", - "fsm_name": "FSM" + "fsm_name": "Attacking" }, { "base_object_name": "Ghost Warrior Hu", From 64911766693859fdee1b3b96406e12998c5e581a Mon Sep 17 00:00:00 2001 From: Extremelyd1 <10898310+Extremelyd1@users.noreply.github.com> Date: Thu, 18 Jul 2024 19:19:06 +0200 Subject: [PATCH 107/216] Fix Vengefly King in Godhome --- .../Client/Entity/Action/EntityFsmActions.cs | 53 +++++++++++++++++++ HKMP/Game/Client/Entity/EntitySpawner.cs | 47 ++++++++++++++-- 2 files changed, 97 insertions(+), 3 deletions(-) diff --git a/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs b/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs index 8b2e032e..f68493ea 100644 --- a/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs +++ b/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs @@ -3007,6 +3007,59 @@ private static void ApplyNetworkDataFromAction(EntityNetworkData data, SetPolygo } } + #endregion + + #region PreSpawnGameObjects + + private static bool GetNetworkDataFromAction(EntityNetworkData data, PreSpawnGameObjects action) { + if (EntitySpawnEvent != null) { + var spawnedEntity = false; + + var arr = action.storeArray.Values; + for (var i = 0; i < arr.Length; i++) { + var spawnedGo = (GameObject) arr[i]; + + if (EntitySpawnEvent.Invoke(new EntitySpawnDetails { + Type = EntitySpawnType.FsmAction, + Action = action, + GameObject = spawnedGo + })) { + Logger.Debug("Tried getting PreSpawnGameObjects network data, but spawned objects contains entity"); + spawnedEntity = true; + } + } + + if (spawnedEntity) { + return false; + } + } + + return true; + } + + private static void ApplyNetworkDataFromAction(EntityNetworkData data, PreSpawnGameObjects action) { + if (action.prefab.Value == null) { + return; + } + + if (action.storeArray.IsNone) { + return; + } + + if (action.spawnAmount.Value <= 0 || action.spawnAmountMultiplier.Value <= 0) { + return; + } + + var length = action.spawnAmount.Value * action.spawnAmountMultiplier.Value; + action.storeArray.Resize(length); + + for (var i = 0; i < length; i++) { + var go = Object.Instantiate(action.prefab.Value); + go.SetActive(false); + action.storeArray.Values[i] = go; + } + } + #endregion /// diff --git a/HKMP/Game/Client/Entity/EntitySpawner.cs b/HKMP/Game/Client/Entity/EntitySpawner.cs index 2cbe6007..0fe2e9ce 100644 --- a/HKMP/Game/Client/Entity/EntitySpawner.cs +++ b/HKMP/Game/Client/Entity/EntitySpawner.cs @@ -46,8 +46,14 @@ List clientFsms return SpawnBaldurGameObject(clientFsms[0]); } - if (spawningType == EntityType.VengeflyKing && spawnedType == EntityType.VengeflySummon) { - return SpawnVengeflySummonObject(clientFsms[0]); + if (spawningType == EntityType.VengeflyKing) { + if (spawnedType == EntityType.VengeflySummon) { + return SpawnVengeflySummonObject(clientFsms[0]); + } + + if (spawnedType == EntityType.Vengefly) { + return SpawnVengeflyFromKing(clientFsms[0]); + } } if (spawningType == EntityType.VengeflySummon && spawnedType == EntityType.Vengefly) { @@ -305,7 +311,42 @@ private static GameObject SpawnVengeflyObjectFromSummon(GameObject spawningObjec return spawnedEnemy; } - + + private static GameObject SpawnVengeflyFromKing(PlayMakerFSM fsm) { + var action = fsm.GetFirstAction("Set GG"); + if (action == null) { + return null; + } + + if (action.prefab.Value == null) { + return null; + } + + // Check if the store array exists and needs to be resized + if (!action.storeArray.IsNone) { + var length = action.spawnAmount.Value * action.spawnAmountMultiplier.Value; + if (action.storeArray.Values.Length != length) { + action.storeArray.Resize(length); + } + } + + var go = Object.Instantiate(action.prefab.Value); + + // Find a space in the store array to store this object in case we are host transferred + // That way, the pre-spawning of objects is already done + if (!action.storeArray.IsNone) { + var arr = action.storeArray.Values; + for (var i = 0; i < arr.Length; i++) { + if (arr[i] == null) { + arr[i] = go; + break; + } + } + } + + return go; + } + private static GameObject SpawnOomaCoreObject(PlayMakerFSM fsm) { var action = fsm.GetAction("Explode", 3); From 33be489a9bf99bff9121e1b0e6f901fdb84f83ee Mon Sep 17 00:00:00 2001 From: Extremelyd1 <10898310+Extremelyd1@users.noreply.github.com> Date: Thu, 18 Jul 2024 23:07:56 +0200 Subject: [PATCH 108/216] Fix standalone server issues and crashes --- HKMP/Game/Client/Entity/Action/EntityFsmActions.cs | 4 ++-- HKMP/Game/Client/Save/SaveDataMapping.cs | 2 -- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs b/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs index f68493ea..13c23652 100644 --- a/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs +++ b/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs @@ -215,8 +215,8 @@ private static void EmitRandomInterceptInstructions(ILCursor c) // 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 gameObject = ReflectionHelper.GetField(instance, "gameObject"); - if (gameObject != null && IsObjectInRegistry(gameObject)) { + var fsmGameObject = ReflectionHelper.GetField(instance, "gameObject"); + if (fsmGameObject != null && fsmGameObject.Value != null && IsObjectInRegistry(fsmGameObject.Value)) { return value; } diff --git a/HKMP/Game/Client/Save/SaveDataMapping.cs b/HKMP/Game/Client/Save/SaveDataMapping.cs index c3222d64..cefa5fb0 100644 --- a/HKMP/Game/Client/Save/SaveDataMapping.cs +++ b/HKMP/Game/Client/Save/SaveDataMapping.cs @@ -3,7 +3,6 @@ using Hkmp.Collection; using Hkmp.Logging; using Hkmp.Util; -using JetBrains.Annotations; using Newtonsoft.Json; using Newtonsoft.Json.Converters; @@ -179,7 +178,6 @@ public void Initialize() { /// /// Properties that denote when to sync values. /// - [UsedImplicitly] internal class SyncProperties { /// /// Whether to sync this value. If true, the variable indicates where to store From f31617d30ee13ba4dec5db9cc00e90265b2ae01d Mon Sep 17 00:00:00 2001 From: Extremelyd1 <10898310+Extremelyd1@users.noreply.github.com> Date: Fri, 19 Jul 2024 13:57:22 +0200 Subject: [PATCH 109/216] Fix soul being gained from remote nail swings --- HKMP/Game/Client/PlayerManager.cs | 57 +++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/HKMP/Game/Client/PlayerManager.cs b/HKMP/Game/Client/PlayerManager.cs index b387e199..165d334e 100644 --- a/HKMP/Game/Client/PlayerManager.cs +++ b/HKMP/Game/Client/PlayerManager.cs @@ -7,6 +7,8 @@ using Hkmp.Networking.Packet.Data; using Hkmp.Ui.Resources; using Hkmp.Util; +using Mono.Cecil.Cil; +using MonoMod.Cil; using TMPro; using UnityEngine; using Logger = Hkmp.Logging.Logger; @@ -107,6 +109,8 @@ Dictionary playerData OnPlayerTeamUpdate); packetManager.RegisterClientPacketHandler(ClientPacketId.PlayerSkinUpdate, OnPlayerSkinUpdate); + + IL.HealthManager.TakeDamage += HealthManagerOnTakeDamage; } /// @@ -780,4 +784,57 @@ private void ToggleBodyDamage(ClientPlayerData playerData, bool enabled) { playerObject.GetComponent().enabled = false; } } + + /// + /// IL Hook to modify the behaviour of the TakeDamage method in HealthManager. This modification adds a + /// conditional branch in case the nail swing from the HitInstance was from a remote player to ensure that + /// soul is not gained for remote hits. + /// + private void HealthManagerOnTakeDamage(ILContext il) { + try { + // Create a cursor for this context + var c = new ILCursor(il); + + // Goto the next virtual call to HeroController.SoulGain() + c.GotoNext(i => i.MatchCallvirt(typeof(HeroController), "SoulGain")); + + // Move the cursor to before the call and call virtual instructions + c.Index -= 1; + + // Emit the instruction to load the first parameter (hitInstance) onto the stack + c.Emit(OpCodes.Ldarg_1); + + // Emit a delegate that takes the hitInstance parameter from the stack and pushes a boolean on the stack + // that indicates whether the hitInstance was from a remote player's nail swing + c.EmitDelegate>(hitInstance => { + if (hitInstance.Source == null || hitInstance.Source.transform == null) { + return false; + } + + // Find the top-level parent of the hit instance + var transform = hitInstance.Source.transform; + while (transform.parent != null) { + transform = transform.parent; + } + + var go = transform.gameObject; + + return go.tag != "Player"; + }); + + // Define a label for the branch instruction + var afterLabel = c.DefineLabel(); + + // Emit the branch (on true) instruction with the label + c.Emit(OpCodes.Brtrue, afterLabel); + + // Move the cursor after the SoulGain method call + c.Index += 2; + + // Mark the label here, so we branch after the SoulGain method call on true + c.MarkLabel(afterLabel); + } catch (Exception e) { + Logger.Error($"Could not change HealthManager#TakeDamage IL:\n{e}"); + } + } } From e8e5aee97a183bf122192fb74f97974b3a0d150b Mon Sep 17 00:00:00 2001 From: Extremelyd1 <10898310+Extremelyd1@users.noreply.github.com> Date: Fri, 19 Jul 2024 13:57:35 +0200 Subject: [PATCH 110/216] Fix issue with entity objects being null --- HKMP/Game/Client/Entity/Entity.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/HKMP/Game/Client/Entity/Entity.cs b/HKMP/Game/Client/Entity/Entity.cs index ac708d9e..c06f526f 100644 --- a/HKMP/Game/Client/Entity/Entity.cs +++ b/HKMP/Game/Client/Entity/Entity.cs @@ -562,7 +562,11 @@ private void OnUpdate() { if (_lastIsActive) { // If the host object was active, but now it null (or destroyed in Unity), we can send // to the server that the entity can be regarded as inactive - Logger.Info($"Entity '{Object.Client.name}' host object is null (or destroyed) and was active"); + if (Object.Client == null) { + Logger.Info($"Entity ({Id}, {Type}) host and client object is null (or destroyed) and was active"); + } else { + Logger.Info($"Entity '{Object.Client.name}' host object is null (or destroyed) and was active"); + } _lastIsActive = false; From 5f685d0af1038626bc2efff98f0fedfd76c12b66 Mon Sep 17 00:00:00 2001 From: Extremelyd1 <10898310+Extremelyd1@users.noreply.github.com> Date: Sat, 20 Jul 2024 13:51:02 +0200 Subject: [PATCH 111/216] Improve save data handling for in-game hosting --- HKMP/Game/Client/ClientManager.cs | 6 ++ HKMP/Game/Client/Save/SaveManager.cs | 104 +++++++++++++++++++++------ HKMP/Resource/save-data.json | 3 +- 3 files changed, 92 insertions(+), 21 deletions(-) diff --git a/HKMP/Game/Client/ClientManager.cs b/HKMP/Game/Client/ClientManager.cs index 8fbdc338..5fd35e29 100644 --- a/HKMP/Game/Client/ClientManager.cs +++ b/HKMP/Game/Client/ClientManager.cs @@ -247,6 +247,12 @@ ModSettings modSettings Connect(address, port, username); }; uiManager.RequestClientDisconnectEvent += Disconnect; + uiManager.RequestServerStartHostEvent += _ => { + _saveManager.IsHostingServer = true; + }; + uiManager.RequestServerStopHostEvent += () => { + _saveManager.IsHostingServer = false; + }; UiManager.InternalChatBox.ChatInputEvent += OnChatInput; diff --git a/HKMP/Game/Client/Save/SaveManager.cs b/HKMP/Game/Client/Save/SaveManager.cs index 0f28ac4f..4a8bfd29 100644 --- a/HKMP/Game/Client/Save/SaveManager.cs +++ b/HKMP/Game/Client/Save/SaveManager.cs @@ -59,6 +59,12 @@ internal class SaveManager { /// private readonly Dictionary _bsCompHashes; + /// + /// Whether the player is hosting the server, which means that player specific save data is not networked + /// to the server. + /// + public bool IsHostingServer { get; set; } + public SaveManager(NetClient netClient, PacketManager packetManager, EntityManager entityManager) { _netClient = netClient; _packetManager = packetManager; @@ -391,6 +397,11 @@ private void CheckSendSaveUpdate(string name, Func encodeFunc) { return; } + if (syncProps.SyncType == SaveDataMapping.SyncType.Player && IsHostingServer) { + Logger.Debug("Player specific save data, but player is hosting the server, not sending save update"); + return; + } + if (!SaveDataMapping.PlayerDataIndices.TryGetValue(name, out var index)) { Logger.Info($"Cannot find save data index, not sending save update ({name})"); return; @@ -458,10 +469,15 @@ private void OnUpdatePersistents() { // If we should do the scene host check and the player is not scene host, skip sending if (!syncProps.IgnoreSceneHost && !_entityManager.IsSceneHost) { Logger.Info( - $"Not scene host, not sending geo rock save update ({itemData.Id}, {itemData.SceneName})"); + $"Not scene host, not sending persistent int save update ({itemData.Id}, {itemData.SceneName})"); continue; } - + + if (syncProps.SyncType == SaveDataMapping.SyncType.Player && IsHostingServer) { + Logger.Debug("Player specific save data, but player is hosting the server, not sending persistent int save update"); + continue; + } + if (!SaveDataMapping.PersistentIntDataIndices.TryGetValue(itemData, out var index)) { Logger.Info( $"Cannot find persistent int save data index, not sending save update ({itemData.Id}, {itemData.SceneName})"); @@ -507,6 +523,11 @@ private void OnUpdatePersistents() { continue; } + if (syncProps.SyncType == SaveDataMapping.SyncType.Player && IsHostingServer) { + Logger.Debug("Player specific save data, but player is hosting the server, not sending persistent bool save update"); + continue; + } + if (!SaveDataMapping.PersistentBoolDataIndices.TryGetValue(itemData, out var index)) { Logger.Info( $"Cannot find persistent bool save data index, not sending save update ({itemData.Id}, {itemData.SceneName})"); @@ -534,41 +555,48 @@ void CheckUpdates( Func changeFunc ) { foreach (var varName in variableNames) { - if (!SaveDataMapping.PlayerDataBools.TryGetValue(varName, out var syncProps)) { + var variable = (TVar) typeof(PlayerData).GetField(varName).GetValue(PlayerData.instance); + var newCheck = newCheckFunc.Invoke(variable); + + if (!checkDict.TryGetValue(varName, out var check)) { + checkDict[varName] = newCheck; continue; } - if (!syncProps.Sync) { + if (!changeFunc(newCheck, check)) { continue; } - if (!syncProps.IgnoreSceneHost && !_entityManager.IsSceneHost) { + Logger.Info($"Compound variable ({varName}) changed value"); + + checkDict[varName] = newCheck; + + if (!SaveDataMapping.PlayerDataBools.TryGetValue(varName, out var syncProps)) { continue; } - if (!SaveDataMapping.PlayerDataIndices.TryGetValue(varName, out var index)) { + if (!syncProps.Sync) { continue; } - var variable = (TVar) typeof(PlayerData).GetField(varName).GetValue(PlayerData.instance); - var newCheck = newCheckFunc.Invoke(variable); - - if (!checkDict.TryGetValue(varName, out var check)) { - checkDict[varName] = newCheck; + if (!syncProps.IgnoreSceneHost && !_entityManager.IsSceneHost) { continue; } - if (changeFunc(newCheck, check)) { - Logger.Info($"Compound variable ({varName}) changed value"); + if (syncProps.SyncType == SaveDataMapping.SyncType.Player && IsHostingServer) { + Logger.Debug("Player specific save data, but player is hosting the server, not sending compound save update"); + return; + } - checkDict[varName] = newCheck; + if (!SaveDataMapping.PlayerDataIndices.TryGetValue(varName, out var index)) { + continue; + } - if (_netClient.IsConnected) { - _netClient.UpdateManager.SetSaveUpdate( - index, - EncodeValue(variable) - ); - } + if (_netClient.IsConnected) { + _netClient.UpdateManager.SetSaveUpdate( + index, + EncodeValue(variable) + ); } } } @@ -629,6 +657,11 @@ private void UpdateSaveWithData(SaveUpdate saveUpdate) { /// /// The save data to set. public void SetSaveWithData(CurrentSave currentSave) { + if (IsHostingServer) { + Logger.Info("Received current save, but player is hosting, not updating"); + return; + } + Logger.Info("Received current save, updating..."); foreach (var keyValuePair in currentSave.SaveData) { @@ -651,6 +684,10 @@ private void UpdateSaveWithData(ushort index, byte[] encodedValue) { var sceneData = SceneData.instance; if (SaveDataMapping.PlayerDataIndices.TryGetValue(index, out var name)) { + if (CheckPlayerSpecificHosting(SaveDataMapping.PlayerDataBools, name)) { + return; + } + Logger.Info($"Received save update ({index}, {name})"); var fieldInfo = typeof(PlayerData).GetField(name); @@ -781,6 +818,10 @@ private void UpdateSaveWithData(ushort index, byte[] encodedValue) { hitsLeft = value }); } else if (SaveDataMapping.PersistentBoolDataIndices.TryGetValue(index, out itemData)) { + if (CheckPlayerSpecificHosting(SaveDataMapping.PersistentBoolDataBools, itemData)) { + return; + } + var value = encodedValue[0] == 1; Logger.Info($"Received persistent bool save update: {itemData.Id}, {itemData.SceneName}, {value}"); @@ -800,6 +841,10 @@ private void UpdateSaveWithData(ushort index, byte[] encodedValue) { activated = value }); } else if (SaveDataMapping.PersistentIntDataIndices.TryGetValue(index, out itemData)) { + if (CheckPlayerSpecificHosting(SaveDataMapping.PersistentIntDataBools, itemData)) { + return; + } + var value = (int) encodedValue[0]; // Add a special case for the -1 value that some persistent ints might have // 255 is never used in the byte space, so we use it for compact networking @@ -835,6 +880,25 @@ string DecodeString(byte[] encoded, int startIndex) { return value; } + + // Do the checks for whether the player is hosting and the received save data is player specific and should + // thus be ignored. Returns true if the data should be ignored, false otherwise. + bool CheckPlayerSpecificHosting(Dictionary dict, TKey value) { + if (!IsHostingServer) { + return false; + } + + if (!dict.TryGetValue(value, out var syncProps)) { + return true; + } + + if (syncProps.SyncType != SaveDataMapping.SyncType.Player) { + return false; + } + + Logger.Info($"Received player specific save update ({index}, {name}), but player is hosting"); + return true; + } } /// diff --git a/HKMP/Resource/save-data.json b/HKMP/Resource/save-data.json index 1836a48d..f89ecdef 100644 --- a/HKMP/Resource/save-data.json +++ b/HKMP/Resource/save-data.json @@ -1009,7 +1009,8 @@ }, "shaman": { "Sync": true, - "SyncType": "Server" + "SyncType": "Player", + "IgnoreSceneHost": true }, "shamanScreamConvo": { "Sync": true, From 797c2389f1ce017a28ad7c2f1b77f60e064a581b Mon Sep 17 00:00:00 2001 From: Extremelyd1 <10898310+Extremelyd1@users.noreply.github.com> Date: Sat, 20 Jul 2024 20:37:54 +0200 Subject: [PATCH 112/216] Initial work for sound/music networking --- .../Client/Entity/Component/MusicComponent.cs | 148 ++++++++++++++++++ HKMP/HKMP.csproj | 1 + HKMP/Resource/music-data.json | 104 ++++++++++++ 3 files changed, 253 insertions(+) create mode 100644 HKMP/Game/Client/Entity/Component/MusicComponent.cs create mode 100644 HKMP/Resource/music-data.json diff --git a/HKMP/Game/Client/Entity/Component/MusicComponent.cs b/HKMP/Game/Client/Entity/Component/MusicComponent.cs new file mode 100644 index 00000000..dc1cfbc7 --- /dev/null +++ b/HKMP/Game/Client/Entity/Component/MusicComponent.cs @@ -0,0 +1,148 @@ +using System; +using System.Collections.Generic; +using Hkmp.Networking.Client; +using Hkmp.Networking.Packet.Data; +using Hkmp.Util; +using HutongGames.PlayMaker.Actions; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; +using UnityEngine; +using UnityEngine.Audio; +using Logger = Hkmp.Logging.Logger; + +namespace Hkmp.Game.Client.Entity.Component; + +// TODO: document all fields and methods +/// +/// This component manages the music that plays for boss fights. +internal class MusicComponent : EntityComponent { + /// + /// The file path of the embedded resource file for music data. + /// + private const string MusicDataFilePath = "Hkmp.Resource.music-data.json"; + + private static readonly List MusicCueDataList; + private static readonly List SnapshotDataList; + + static MusicComponent() { + var dataPair = FileUtil.LoadObjectFromEmbeddedJson< + (List, List) + >(MusicDataFilePath); + + MusicCueDataList = dataPair.Item1; + SnapshotDataList = dataPair.Item2; + + On.PlayMakerFSM.OnEnable += OnFsmEnable; + } + + public MusicComponent( + NetClient netClient, + ushort entityId, + HostClientPair gameObject + ) : base(netClient, entityId, gameObject) { + // TODO: register hooks for entering ApplyMusicCue and TransitionToAudioSnapshot actions + // TODO: these hooks should network changes in Music and Audio to the server if the player is scene host + } + + /// + public override void InitializeHost() { + } + + /// + public override void Update(EntityNetworkData data) { + // TODO: handle receiving Music and Audio updates from the server and applying them + } + + /// + public override void Destroy() { + } + + private static void OnFsmEnable(On.PlayMakerFSM.orig_OnEnable orig, PlayMakerFSM self) { + orig(self); + + foreach (var state in self.FsmStates) { + foreach (var action in state.Actions) { + if (action is ApplyMusicCue applyMusicCue) { + var musicCue = applyMusicCue.musicCue.Value as MusicCue; + if (musicCue == null) { + continue; + } + + Logger.Debug($"Found music cue '{musicCue.name}' in FSM '{self.Fsm.Name}', '{state.Name}'"); + + if (GetMusicCueData( + data => data.Name.Equals(musicCue.name), + out var musicCueData + )) { + Logger.Debug($" Adding to data with type: {musicCueData.Type}"); + musicCueData.MusicCue = musicCue; + } + } else if (action is TransitionToAudioSnapshot snapshotAction) { + var snapshot = snapshotAction.snapshot.Value as AudioMixerSnapshot; + if (snapshot == null) { + continue; + } + + Logger.Debug($"Found audio mixer snapshot '{snapshot.name}' in FSM '{self.Fsm.Name}', '{state.Name}'"); + } + } + } + } + + private static bool GetMusicCueData(Func predicate, out MusicCueData musicCueData) { + foreach (var data in MusicCueDataList) { + if (predicate.Invoke(data)) { + musicCueData = data; + return true; + } + } + + musicCueData = null; + return false; + } + + private class MusicCueData { + public MusicCueType Type { get; set; } + public string Name { get; set; } + public byte Index { get; set; } + [JsonIgnore] + public MusicCue MusicCue { get; set; } + } + + private class AudioMixerSnapshotData { + public AudioMixerSnapshotType Type { get; set; } + public string Name { get; set; } + public byte Index { get; set; } + [JsonIgnore] + public AudioMixerSnapshot Snapshot { get; set; } + } + + [JsonConverter(typeof(StringEnumConverter))] + private enum MusicCueType { + None, + FalseKnight, + Hornet, + GGHornet, + MantisLords, + SoulMaster, + SoulMaster2, + GGHeavy, + EnemyBattle, + DreamFight, + Hive, + HiveKnight, + DungDefender, + BrokenVessel, + Nosk, + TheHollowKnight, + Greenpath, + Waterways + } + + [JsonConverter(typeof(StringEnumConverter))] + private enum AudioMixerSnapshotType { + Silent, + None, + Off, + } +} diff --git a/HKMP/HKMP.csproj b/HKMP/HKMP.csproj index 76902f0a..dc66cd49 100644 --- a/HKMP/HKMP.csproj +++ b/HKMP/HKMP.csproj @@ -23,6 +23,7 @@ + diff --git a/HKMP/Resource/music-data.json b/HKMP/Resource/music-data.json new file mode 100644 index 00000000..e9669f41 --- /dev/null +++ b/HKMP/Resource/music-data.json @@ -0,0 +1,104 @@ +{ + "Item1": [ + { + "Type": "None", + "Name": "None", + "Index": 0 + }, + { + "Type": "FalseKnight", + "Name": "Boss1", + "Index": 1 + }, + { + "Type": "Hornet", + "Name": "BossHornet", + "Index": 2 + }, + { + "Type": "GGHornet", + "Name": "GGHornet", + "Index": 3 + }, + { + "Type": "MantisLords", + "Name": "BossMantisLords", + "Index": 4 + }, + { + "Type": "SoulMaster", + "Name": "BossMageLord", + "Index": 5 + }, + { + "Type": "SoulMaster2", + "Name": "MageLord2", + "Index": 6 + }, + { + "Type": "GGHeavy", + "Name": "GG Heavy", + "Index": 7 + },{ + "Type": "DreamFight", + "Name": "DreamFight", + "Index": 8 + }, + { + "Type": "Hive", + "Name": "Hive", + "Index": 9 + }, + { + "Type": "HiveKnight", + "Name": "HiveKnight", + "Index": 10 + }, + { + "Type": "DungDefender", + "Name": "DungDefender", + "Index": 11 + }, + { + "Type": "BrokenVessel", + "Name": "BossIK", + "Index": 12 + }, + { + "Type": "Nosk", + "Name": "MimicSpider", + "Index": 13 + },{ + "Type": "TheHollowKnight", + "Name": "HKBattle", + "Index": 14 + }, + { + "Type": "Greenpath", + "Name": "Greenpath", + "Index": 15 + }, + { + "Type": "Waterways", + "Name": "Waterways", + "Index": 16 + } + ], + "Item2": [ + { + "Type": "Silent", + "Name": "Silent", + "Index": 0 + }, + { + "Type": "None", + "Name": "None", + "Index": 1 + }, + { + "Type": "Off", + "Name": "Off", + "Index": 2 + } + ] +} From c0ce06bc52574debdad98a1cba050a4ce1531699 Mon Sep 17 00:00:00 2001 From: Extremelyd1 <10898310+Extremelyd1@users.noreply.github.com> Date: Sun, 21 Jul 2024 18:07:34 +0200 Subject: [PATCH 113/216] Implement music synchronisation --- .../Entity/Component/ComponentFactory.cs | 6 + .../Entity/Component/EntityComponent.cs | 3 +- .../Client/Entity/Component/MusicComponent.cs | 240 ++++++++++++++++-- HKMP/Game/Client/Entity/Entity.cs | 7 +- HKMP/Game/Client/Entity/EntityManager.cs | 3 + HKMP/Resource/entity-registry.json | 135 +++++++--- HKMP/Resource/music-data.json | 64 ++--- 7 files changed, 366 insertions(+), 92 deletions(-) diff --git a/HKMP/Game/Client/Entity/Component/ComponentFactory.cs b/HKMP/Game/Client/Entity/Component/ComponentFactory.cs index bc9c06e3..75e20e1b 100644 --- a/HKMP/Game/Client/Entity/Component/ComponentFactory.cs +++ b/HKMP/Game/Client/Entity/Component/ComponentFactory.cs @@ -65,6 +65,12 @@ HostClientPair objects }); case EntityComponentType.ChallengePrompt: return new ChallengePromptComponent(netClient, entityId, objects); + case EntityComponentType.Music: + if (MusicComponent.CreateInstance(netClient, entityId, objects, out var musicComponent)) { + return musicComponent; + } + + return null; default: throw new ArgumentOutOfRangeException(nameof(type), type, $"Could not instantiate entity component for type: {type}"); } diff --git a/HKMP/Game/Client/Entity/Component/EntityComponent.cs b/HKMP/Game/Client/Entity/Component/EntityComponent.cs index 236b00fe..f20a8c8e 100644 --- a/HKMP/Game/Client/Entity/Component/EntityComponent.cs +++ b/HKMP/Game/Client/Entity/Component/EntityComponent.cs @@ -85,5 +85,6 @@ internal enum EntityComponentType : ushort { ChildrenActivation, SpawnJar, SpriteRenderer, - ChallengePrompt + ChallengePrompt, + Music } diff --git a/HKMP/Game/Client/Entity/Component/MusicComponent.cs b/HKMP/Game/Client/Entity/Component/MusicComponent.cs index dc1cfbc7..6825311f 100644 --- a/HKMP/Game/Client/Entity/Component/MusicComponent.cs +++ b/HKMP/Game/Client/Entity/Component/MusicComponent.cs @@ -24,6 +24,11 @@ internal class MusicComponent : EntityComponent { private static readonly List MusicCueDataList; private static readonly List SnapshotDataList; + private static MusicComponent _instance; + + private byte _lastMusicCueIndex; + private byte _lastSnapshotIndex; + static MusicComponent() { var dataPair = FileUtil.LoadObjectFromEmbeddedJson< (List, List) @@ -32,16 +37,156 @@ static MusicComponent() { MusicCueDataList = dataPair.Item1; SnapshotDataList = dataPair.Item2; + byte index = 1; + foreach (var data in MusicCueDataList) { + data.Index = index++; + } + + foreach (var data in SnapshotDataList) { + data.Index = index++; + } + On.PlayMakerFSM.OnEnable += OnFsmEnable; } - public MusicComponent( + public static bool CreateInstance( + NetClient netClient, + ushort entityId, + HostClientPair gameObject, + out MusicComponent musicComponent + ) { + if (_instance == null) { + _instance = new MusicComponent(netClient, entityId, gameObject); + musicComponent = _instance; + return true; + } + + musicComponent = null; + return false; + } + + public static void ClearInstance() { + _instance = null; + } + + private static bool GetMusicCueData(Func predicate, out MusicCueData musicCueData) { + foreach (var data in MusicCueDataList) { + if (predicate.Invoke(data)) { + musicCueData = data; + return true; + } + } + + musicCueData = null; + return false; + } + + private static bool GetAudioMixerSnapshotData(Func predicate, out AudioMixerSnapshotData snapshotData) { + foreach (var data in SnapshotDataList) { + if (predicate.Invoke(data)) { + snapshotData = data; + return true; + } + } + + snapshotData = null; + return false; + } + + private MusicComponent( NetClient netClient, ushort entityId, HostClientPair gameObject ) : base(netClient, entityId, gameObject) { - // TODO: register hooks for entering ApplyMusicCue and TransitionToAudioSnapshot actions - // TODO: these hooks should network changes in Music and Audio to the server if the player is scene host + On.HutongGames.PlayMaker.Actions.ApplyMusicCue.OnEnter += ApplyMusicCueOnEnter; + On.HutongGames.PlayMaker.Actions.TransitionToAudioSnapshot.OnEnter += TransitionToAudioSnapshotOnEnter; + } + + private void ApplyMusicCueOnEnter( + On.HutongGames.PlayMaker.Actions.ApplyMusicCue.orig_OnEnter orig, + ApplyMusicCue self + ) { + + Logger.Debug($"ApplyMusicCueOnEnter: {self.Fsm.GameObject.gameObject.name}, {self.Fsm.Name}"); + + if (IsControlled) { + return; + } + + orig(self); + + Logger.Debug(" Not controlled"); + + var musicCue = self.musicCue.Value; + if (musicCue == null) { + Logger.Debug(" Music Cue null"); + return; + } + + Logger.Debug($" Music Cue not null, name: {musicCue.name}"); + + foreach (var musicCueData in MusicCueDataList) { + Logger.Debug($" Loop, music cue: {musicCueData.Name}, {musicCueData.Type}"); + + if (musicCueData.MusicCue == musicCue || musicCueData.Name == musicCue.name) { + Logger.Debug($" Sending data, index: {musicCueData.Index}"); + + var networkData = new EntityNetworkData { + Type = EntityComponentType.Music + }; + networkData.Packet.Write(musicCueData.Index); + networkData.Packet.Write(_lastSnapshotIndex); + + SendData(networkData); + + _lastMusicCueIndex = musicCueData.Index; + + return; + } + } + } + + private void TransitionToAudioSnapshotOnEnter( + On.HutongGames.PlayMaker.Actions.TransitionToAudioSnapshot.orig_OnEnter orig, + TransitionToAudioSnapshot self + ) { + Logger.Debug($"TransitionToAudioSnapshotOnEnter: {self.Fsm.GameObject.gameObject.name}, {self.Fsm.Name}"); + + if (IsControlled) { + return; + } + + orig(self); + + Logger.Debug(" Not controlled"); + + var snapshot = self.snapshot.Value; + if (snapshot == null) { + Logger.Debug(" Snapshot null"); + return; + } + + Logger.Debug($" Snapshot not null, name: {snapshot.name}"); + + foreach (var snapshotData in SnapshotDataList) { + Logger.Debug($" Loop, snapshot: {snapshotData.Name}, {snapshotData.Type}"); + + if (snapshotData.Snapshot == snapshot || snapshotData.Name == snapshot.name) { + Logger.Debug($" Sending data, index: {snapshotData.Index}"); + + var networkData = new EntityNetworkData { + Type = EntityComponentType.Music + }; + networkData.Packet.Write(_lastMusicCueIndex); + networkData.Packet.Write(snapshotData.Index); + + SendData(networkData); + + _lastSnapshotIndex = snapshotData.Index; + + return; + } + } } /// @@ -50,11 +195,73 @@ public override void InitializeHost() { /// public override void Update(EntityNetworkData data) { - // TODO: handle receiving Music and Audio updates from the server and applying them + Logger.Debug("Update MusicComponent"); + + if (!IsControlled) { + Logger.Debug(" Not controlled, skipping"); + return; + } + + var musicCueIndex = data.Packet.ReadByte(); + var snapshotIndex = data.Packet.ReadByte(); + + Logger.Debug($"Applying entity network data for music component with indices: {musicCueIndex}, {snapshotIndex}"); + + if (musicCueIndex != _lastMusicCueIndex) { + ApplyIndex(musicCueIndex); + _lastMusicCueIndex = musicCueIndex; + } + + if (snapshotIndex != _lastSnapshotIndex) { + ApplyIndex(snapshotIndex); + _lastSnapshotIndex = snapshotIndex; + } + + void ApplyIndex(byte index) { + foreach (var musicCueData in MusicCueDataList) { + Logger.Debug($" Loop, index: {musicCueData.Index}"); + + if (musicCueData.Index != index) { + continue; + } + + if (musicCueData.MusicCue == null) { + Logger.Debug(" Could not find music cue in data"); + continue; + } + + Logger.Debug($" Found music cue ({musicCueData.Name}, {musicCueData.Type}), applying it"); + + var gm = global::GameManager.instance; + gm.AudioManager.ApplyMusicCue(musicCueData.MusicCue, 0f, 0f, false); + return; + } + + foreach (var snapshotData in SnapshotDataList) { + Logger.Debug($" Loop, index: {snapshotData.Index}"); + + if (snapshotData.Index != index) { + continue; + } + + if (snapshotData.Snapshot == null) { + Logger.Debug(" Could not find snapshot in data"); + continue; + } + + Logger.Debug(" Found audio mixer snapshot, transitioning to it"); + snapshotData.Snapshot.TransitionTo(0f); + return; + } + + Logger.Debug(" Could not find music cue or audio mixer snapshot matching ID"); + } } /// public override void Destroy() { + On.HutongGames.PlayMaker.Actions.ApplyMusicCue.OnEnter -= ApplyMusicCueOnEnter; + On.HutongGames.PlayMaker.Actions.TransitionToAudioSnapshot.OnEnter -= TransitionToAudioSnapshotOnEnter; } private static void OnFsmEnable(On.PlayMakerFSM.orig_OnEnable orig, PlayMakerFSM self) { @@ -82,28 +289,25 @@ out var musicCueData if (snapshot == null) { continue; } - + Logger.Debug($"Found audio mixer snapshot '{snapshot.name}' in FSM '{self.Fsm.Name}', '{state.Name}'"); - } - } - } - } - private static bool GetMusicCueData(Func predicate, out MusicCueData musicCueData) { - foreach (var data in MusicCueDataList) { - if (predicate.Invoke(data)) { - musicCueData = data; - return true; + if (GetAudioMixerSnapshotData( + data => data.Name.Equals(snapshot.name), + out var snapshotData + )) { + Logger.Debug($" Adding to data with type: {snapshotData.Type}"); + snapshotData.Snapshot = snapshot; + } + } } } - - musicCueData = null; - return false; } private class MusicCueData { public MusicCueType Type { get; set; } public string Name { get; set; } + [JsonIgnore] public byte Index { get; set; } [JsonIgnore] public MusicCue MusicCue { get; set; } @@ -112,6 +316,7 @@ private class MusicCueData { private class AudioMixerSnapshotData { public AudioMixerSnapshotType Type { get; set; } public string Name { get; set; } + [JsonIgnore] public byte Index { get; set; } [JsonIgnore] public AudioMixerSnapshot Snapshot { get; set; } @@ -144,5 +349,6 @@ private enum AudioMixerSnapshotType { Silent, None, Off, + Normal } } diff --git a/HKMP/Game/Client/Entity/Entity.cs b/HKMP/Game/Client/Entity/Entity.cs index c06f526f..130a445c 100644 --- a/HKMP/Game/Client/Entity/Entity.cs +++ b/HKMP/Game/Client/Entity/Entity.cs @@ -433,7 +433,12 @@ private void HandleComponents(EntityComponentType[] types) { // Instantiate all types defined in the entity registry, which are passed to the constructor foreach (var type in types) { - _components[type] = ComponentFactory.InstantiateByType(type, _netClient, Id, Object); + var component = ComponentFactory.InstantiateByType(type, _netClient, Id, Object); + if (component == null) { + Logger.Debug($"Could not instantiate component for type: {type}"); + } else { + _components[type] = component; + } addedComponentsString += $" {type}"; } diff --git a/HKMP/Game/Client/Entity/EntityManager.cs b/HKMP/Game/Client/Entity/EntityManager.cs index 446e9c76..1d39714e 100644 --- a/HKMP/Game/Client/Entity/EntityManager.cs +++ b/HKMP/Game/Client/Entity/EntityManager.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using Hkmp.Game.Client.Entity.Action; +using Hkmp.Game.Client.Entity.Component; using Hkmp.Networking.Client; using Hkmp.Networking.Packet.Data; using Hkmp.Util; @@ -333,6 +334,8 @@ private void OnSceneChanged(Scene oldScene, Scene newScene) { } _entities.Clear(); + + MusicComponent.ClearInstance(); if (!_netClient.IsConnected) { return; diff --git a/HKMP/Resource/entity-registry.json b/HKMP/Resource/entity-registry.json index 6ee9bfc9..c836df6c 100644 --- a/HKMP/Resource/entity-registry.json +++ b/HKMP/Resource/entity-registry.json @@ -97,7 +97,7 @@ "type": "FalseKnight", "fsm_name": "FalseyControl", "components": [ - "Velocity" + "Velocity", "Music" ], "children": [ { @@ -145,14 +145,17 @@ { "base_object_name": "Giant Fly", "type": "GruzMother", - "fsm_name": "Big Fly Control" + "fsm_name": "Big Fly Control", + "components": [ + "Music" + ] }, { "base_object_name": "Mawlek Body", "type": "BroodingMawlek", "fsm_name": "Mawlek Control", "components": [ - "ZPosition", "GravityScale" + "ZPosition", "GravityScale", "Music" ], "children": [ { @@ -243,7 +246,7 @@ "type": "MassiveMossCharger", "fsm_name": "Mossy Control", "components": [ - "GravityScale" + "GravityScale", "Music" ] }, { @@ -267,7 +270,10 @@ { "base_object_name": "Giant Buzzer", "type": "VengeflyKing", - "fsm_name": "Big Buzzer" + "fsm_name": "Big Buzzer", + "components": [ + "Music" + ] }, { "base_object_name": "Buzzer Summon", @@ -286,7 +292,7 @@ "type": "Hornet", "fsm_name": "Control", "components": [ - "GravityScale", "Rotation" + "GravityScale", "Rotation", "Music" ] }, { @@ -315,7 +321,10 @@ { "base_object_name": "Mega Jellyfish", "type": "Uumuu", - "fsm_name": "Mega Jellyfish" + "fsm_name": "Mega Jellyfish", + "components": [ + "Music" + ] }, { "base_object_name": "Quirrel Land", @@ -401,7 +410,7 @@ } ], "components": [ - "ChallengePrompt" + "ChallengePrompt", "Music" ] }, { @@ -491,7 +500,10 @@ { "base_object_name": "Mage Lord", "type": "SoulMaster", - "fsm_name": "Mage Lord" + "fsm_name": "Mage Lord", + "components": [ + "Music" + ] }, { "base_object_name": "Orb Spinner", @@ -547,7 +559,10 @@ { "base_object_name": "Black Knight", "type": "WatcherKnight", - "fsm_name": "Black Knight" + "fsm_name": "Black Knight", + "components": [ + "Music" + ] }, { "base_object_name": "Jar Collector", @@ -622,7 +637,7 @@ "type": "DungDefender", "fsm_name": "Dung Defender", "components": [ - "GravityScale" + "GravityScale", "Music" ] }, { @@ -643,7 +658,10 @@ { "base_object_name": "Fluke Mother", "type": "Flukemarm", - "fsm_name": "Fluke Mother" + "fsm_name": "Fluke Mother", + "components": [ + "Music" + ] }, { "base_object_name": "Zombie Miner", @@ -681,7 +699,10 @@ { "base_object_name": "Mega Zombie Beam Miner", "type": "CrystalGuardian", - "fsm_name": "Beam Miner" + "fsm_name": "Beam Miner", + "components": [ + "Music" + ] }, { "base_object_name": "Laser Turret Mega", @@ -770,7 +791,7 @@ "type": "Nosk", "fsm_name": "Mimic Spider", "components": [ - "GravityScale" + "GravityScale", "Music" ] }, { @@ -803,7 +824,10 @@ { "base_object_name": "Infected Knight", "type": "BrokenVessel", - "fsm_name": "IK Control" + "fsm_name": "IK Control", + "components": [ + "Music" + ] }, { "base_object_name": "Blow Fly", @@ -904,7 +928,10 @@ { "base_object_name": "Mantis Traitor Lord", "type": "TraitorLord", - "fsm_name": "Mantis" + "fsm_name": "Mantis", + "components": [ + "Music" + ] }, { "base_object_name": "Colosseum Manager", @@ -998,7 +1025,10 @@ { "base_object_name": "Mega Fat Bee", "type": "Oblobble", - "fsm_name": "Fatty Fly Attack" + "fsm_name": "Fatty Fly Attack", + "components": [ + "Music" + ] }, { "base_object_name": "Electric Mage", @@ -1013,12 +1043,18 @@ { "base_object_name": "Zote Boss", "type": "Zote", - "fsm_name": "Control" + "fsm_name": "Control", + "components": [ + "Music" + ] }, { "base_object_name": "Lancer", "type": "Tamer", - "fsm_name": "Control" + "fsm_name": "Control", + "components": [ + "Music" + ] }, { "base_object_name": "Lobster", @@ -1028,7 +1064,10 @@ { "base_object_name": "Ghost Warrior Xero", "type": "Xero", - "fsm_name": "FSM" + "fsm_name": "FSM", + "components": [ + "Music" + ] }, { "base_object_name": "Sword", @@ -1041,27 +1080,42 @@ { "base_object_name": "Ghost Warrior Slug", "type": "Gorb", - "fsm_name": "Attacking" + "fsm_name": "Attacking", + "components": [ + "Music" + ] }, { "base_object_name": "Ghost Warrior Hu", "type": "ElderHu", - "fsm_name": "FSM" + "fsm_name": "FSM", + "components": [ + "Music" + ] }, { "base_object_name": "Ghost Warrior Marmu", "type": "Marmu", - "fsm_name": "FSM" + "fsm_name": "FSM", + "components": [ + "Music" + ] }, { "base_object_name": "Ghost Warrior No Eyes", "type": "NoEyes", - "fsm_name": "FSM" + "fsm_name": "FSM", + "components": [ + "Music" + ] }, { "base_object_name": "Ghost Warrior Galien", "type": "Galien", - "fsm_name": "FSM" + "fsm_name": "FSM", + "components": [ + "Music" + ] }, { "base_object_name": "Galien Hammer", @@ -1079,7 +1133,10 @@ { "base_object_name": "Ghost Warrior Markoth", "type": "Markoth", - "fsm_name": "FSM" + "fsm_name": "FSM", + "components": [ + "Music" + ] }, { "base_object_name": "Markoth Shield", @@ -1124,13 +1181,16 @@ "type": "HollowKnight", "fsm_name": "Control", "components": [ - "GravityScale" + "GravityScale", "Music" ] }, { "base_object_name": "Radiance", "type": "Radiance", - "fsm_name": "Control" + "fsm_name": "Control", + "components": [ + "Music" + ] }, { "base_object_name": "Radiant Orb", @@ -1156,7 +1216,10 @@ { "base_object_name": "Grey Prince", "type": "GreyPrinceZote", - "fsm_name": "Control" + "fsm_name": "Control", + "components": [ + "Music" + ] }, { "base_object_name": "Zoteling", @@ -1204,7 +1267,7 @@ "type": "Grimm", "fsm_name": "Control", "components": [ - "Rotation" + "Rotation", "Music" ] }, { @@ -1217,7 +1280,7 @@ "type": "NightmareKingGrimm", "fsm_name": "Control", "components": [ - "Rotation" + "Rotation", "Music" ] }, { @@ -1237,7 +1300,10 @@ { "base_object_name": "Hive Knight", "type": "HiveKnight", - "fsm_name": "Control" + "fsm_name": "Control", + "components": [ + "Music" + ] }, { "base_object_name": "Hive Knight Glob", @@ -1273,7 +1339,10 @@ { "base_object_name": "Dream Mage Lord", "type": "SoulTyrant", - "fsm_name": "Mage Lord" + "fsm_name": "Mage Lord", + "components": [ + "Music" + ] }, { "base_object_name": "Dream Mage Lord Phase2", diff --git a/HKMP/Resource/music-data.json b/HKMP/Resource/music-data.json index e9669f41..7418cd48 100644 --- a/HKMP/Resource/music-data.json +++ b/HKMP/Resource/music-data.json @@ -2,103 +2,87 @@ "Item1": [ { "Type": "None", - "Name": "None", - "Index": 0 + "Name": "None" }, { "Type": "FalseKnight", - "Name": "Boss1", - "Index": 1 + "Name": "Boss1" }, { "Type": "Hornet", - "Name": "BossHornet", - "Index": 2 + "Name": "BossHornet" }, { "Type": "GGHornet", - "Name": "GGHornet", - "Index": 3 + "Name": "GGHornet" }, { "Type": "MantisLords", - "Name": "BossMantisLords", - "Index": 4 + "Name": "BossMantisLords" }, { "Type": "SoulMaster", - "Name": "BossMageLord", - "Index": 5 + "Name": "BossMageLord" }, { "Type": "SoulMaster2", - "Name": "MageLord2", - "Index": 6 + "Name": "MageLord2" }, { "Type": "GGHeavy", - "Name": "GG Heavy", - "Index": 7 + "Name": "GG Heavy" },{ "Type": "DreamFight", - "Name": "DreamFight", - "Index": 8 + "Name": "DreamFight" }, { "Type": "Hive", - "Name": "Hive", - "Index": 9 + "Name": "Hive" }, { "Type": "HiveKnight", - "Name": "HiveKnight", - "Index": 10 + "Name": "HiveKnight" }, { "Type": "DungDefender", - "Name": "DungDefender", - "Index": 11 + "Name": "DungDefender" }, { "Type": "BrokenVessel", - "Name": "BossIK", - "Index": 12 + "Name": "BossIK" }, { "Type": "Nosk", - "Name": "MimicSpider", - "Index": 13 + "Name": "MimicSpider" },{ "Type": "TheHollowKnight", - "Name": "HKBattle", - "Index": 14 + "Name": "HKBattle" }, { "Type": "Greenpath", - "Name": "Greenpath", - "Index": 15 + "Name": "Greenpath" }, { "Type": "Waterways", - "Name": "Waterways", - "Index": 16 + "Name": "Waterways" } ], "Item2": [ { "Type": "Silent", - "Name": "Silent", - "Index": 0 + "Name": "Silent" }, { "Type": "None", - "Name": "None", - "Index": 1 + "Name": "None" }, { "Type": "Off", - "Name": "Off", - "Index": 2 + "Name": "Off" + }, + { + "Type": "Normal", + "Name": "Normal" } ] } From e16e43041e49794e8e461c4a08dccc1c67bef439 Mon Sep 17 00:00:00 2001 From: Extremelyd1 <10898310+Extremelyd1@users.noreply.github.com> Date: Sun, 21 Jul 2024 21:20:51 +0200 Subject: [PATCH 114/216] Fix Geo drops for entities --- HKMP/Game/Client/Entity/Entity.cs | 9 +++++++++ HKMP/Game/Client/Entity/EntityManager.cs | 7 ------- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/HKMP/Game/Client/Entity/Entity.cs b/HKMP/Game/Client/Entity/Entity.cs index 130a445c..38c8260f 100644 --- a/HKMP/Game/Client/Entity/Entity.cs +++ b/HKMP/Game/Client/Entity/Entity.cs @@ -349,6 +349,15 @@ private void HandleComponents(EntityComponentType[] types) { _components[EntityComponentType.Death] = hmComponent; _components[EntityComponentType.Invincibility] = hmComponent; + // Check if the object from the health manager is in any of the colosseum trial scenes and remove the + // geo drops from them if so + var goScene = hostHealthManager.gameObject.scene.name; + if (goScene is "Room_Colosseum_Bronze" or "Room_Colosseum_Silver" or "Room_Colosseum_Gold") { + clientHealthManager.SetGeoSmall(0); + clientHealthManager.SetGeoMedium(0); + clientHealthManager.SetGeoLarge(0); + } + addedComponentsString += " Death Invincibility"; } diff --git a/HKMP/Game/Client/Entity/EntityManager.cs b/HKMP/Game/Client/Entity/EntityManager.cs index 1d39714e..3697e1a0 100644 --- a/HKMP/Game/Client/Entity/EntityManager.cs +++ b/HKMP/Game/Client/Entity/EntityManager.cs @@ -434,13 +434,6 @@ private void FindEntitiesInScene(Scene scene, bool lateLoad) { createdObject.transform.rotation = Quaternion.Euler(fsmTransform.eulerAngles); createdObject.SetActive(false); - var healthManager = createdObject.GetComponent(); - if (healthManager != null) { - healthManager.SetGeoSmall(0); - healthManager.SetGeoMedium(0); - healthManager.SetGeoLarge(0); - } - return new[] { fsm.gameObject, createdObject }; }) ) From 4d3909cf4a5c221cec5c4d50b3e245969ff9e89b Mon Sep 17 00:00:00 2001 From: Extremelyd1 <10898310+Extremelyd1@users.noreply.github.com> Date: Sun, 21 Jul 2024 21:21:01 +0200 Subject: [PATCH 115/216] Fix Charm page not showing --- HKMP/Resource/save-data.json | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/HKMP/Resource/save-data.json b/HKMP/Resource/save-data.json index f89ecdef..73d27658 100644 --- a/HKMP/Resource/save-data.json +++ b/HKMP/Resource/save-data.json @@ -1916,6 +1916,11 @@ "SyncType": "Player", "IgnoreSceneHost": true }, + "hasCharm": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, "charmsOwned": { "Sync": true, "SyncType": "Player", From 2c746619a40dce48261137a8d940392b2aaee136 Mon Sep 17 00:00:00 2001 From: Extremelyd1 <10898310+Extremelyd1@users.noreply.github.com> Date: Mon, 22 Jul 2024 08:04:32 +0200 Subject: [PATCH 116/216] Fix various issues with Mantis Lords --- HKMP/Fsm/FsmPatcher.cs | 31 ++++++++++++ .../Client/Entity/Action/EntityFsmActions.cs | 5 +- .../Component/ChallengePromptComponent.cs | 49 +++++++++++++++---- HKMP/Game/Client/Entity/EntityManager.cs | 20 ++++---- 4 files changed, 85 insertions(+), 20 deletions(-) diff --git a/HKMP/Fsm/FsmPatcher.cs b/HKMP/Fsm/FsmPatcher.cs index 73c1400b..b53951bb 100644 --- a/HKMP/Fsm/FsmPatcher.cs +++ b/HKMP/Fsm/FsmPatcher.cs @@ -55,6 +55,37 @@ private void OnFsmEnable(On.PlayMakerFSM.orig_OnEnable orig, PlayMakerFSM self) 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"); + + // 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"); + } // Code for modifying the collision check on collapsing floors to include remote players (not working) // if (self.name.Equals("Collapser Small") && self.Fsm.Name.Equals("collapse small")) { diff --git a/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs b/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs index 13c23652..47ae59ef 100644 --- a/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs +++ b/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs @@ -2402,7 +2402,10 @@ private static void ApplyNetworkDataFromAction(EntityNetworkData data, GetPositi private static bool GetNetworkDataFromAction(EntityNetworkData data, CallMethodProper action) { Logger.Debug($"Getting network data for CallMethodProper: {action.Fsm.GameObject.name}, {action.Fsm.Name}"); - return action.Fsm.GameObject.name.StartsWith("Colosseum Manager") && action.Fsm.Name.Equals("Battle Control"); + return action.Fsm.GameObject.name.StartsWith("Colosseum Manager") && + action.Fsm.Name.Equals("Battle Control") || + action.Fsm.GameObject.name.StartsWith("Mantis Lord Throne") && + action.Fsm.Name.Equals("Mantis Throne Main"); } private static void ApplyNetworkDataFromAction(EntityNetworkData data, CallMethodProper action) { diff --git a/HKMP/Game/Client/Entity/Component/ChallengePromptComponent.cs b/HKMP/Game/Client/Entity/Component/ChallengePromptComponent.cs index ddd7f75b..a33b0ada 100644 --- a/HKMP/Game/Client/Entity/Component/ChallengePromptComponent.cs +++ b/HKMP/Game/Client/Entity/Component/ChallengePromptComponent.cs @@ -1,6 +1,7 @@ using Hkmp.Networking.Client; using Hkmp.Networking.Packet.Data; using Hkmp.Util; +using HutongGames.PlayMaker.Actions; using UnityEngine; namespace Hkmp.Game.Client.Entity.Component; @@ -12,6 +13,11 @@ internal class ChallengePromptComponent : EntityComponent { /// The game object that handles the challenge prompt pop-up. /// private readonly GameObject _promptObj; + + /// + /// The FSM corresponding to the challenge prompt object. + /// + private readonly PlayMakerFSM _promptFsm; public ChallengePromptComponent( NetClient netClient, @@ -21,13 +27,13 @@ HostClientPair gameObject var hostObj = gameObject.Host; var parent = hostObj.transform.parent; _promptObj = parent.Find("Challenge Prompt").gameObject; + _promptFsm = _promptObj.LocateMyFSM("Challenge Start"); - var promptFsm = _promptObj.LocateMyFSM("Challenge Start"); - promptFsm.InsertMethod("Take Control", 6, () => { + _promptFsm.InsertMethod("Take Control", 6, () => { var data = new EntityNetworkData { Type = EntityComponentType.ChallengePrompt }; - data.Packet.Write(true); + data.Packet.Write(0); SendData(data); }); @@ -39,13 +45,38 @@ public override void InitializeHost() { /// public override void Update(EntityNetworkData data) { - var destroyPrompt = data.Packet.ReadBool(); - if (!destroyPrompt) { - return; - } + var type = data.Packet.ReadByte(); + + // If the player is a scene client we destroy the prompt, otherwise we start the fight by progressing the FSM + if (IsControlled) { + if (_promptObj != null) { + Object.Destroy(_promptObj); + } + } else { + // Remove actions that rely on the local player + _promptFsm.RemoveFirstAction("Challenge"); + _promptFsm.RemoveFirstAction("Challenge"); + + // Get some actions that we want to re-use in another state + var activateObjAction = _promptFsm.GetFirstAction("Take Control"); + var sendEventAction = _promptFsm.GetFirstAction("Take Control"); + + // Put these actions in the Challenge state for execution + _promptFsm.InsertAction("Challenge", activateObjAction, 0); + _promptFsm.InsertAction("Challenge", sendEventAction, 1); - if (_promptObj != null) { - Object.Destroy(_promptObj); + // Get the watch animation events action so we can get the FsmEvent is sends + var watchAnimationEvent = _promptFsm.GetFirstAction("Challenge Audio"); + + // Insert a method that sends the event to go to the next stage instead of waiting for the animation to finish + _promptFsm.InsertMethod("Challenge Audio", 1, () => { + _promptFsm.Fsm.Event(watchAnimationEvent.animationCompleteEvent); + }); + // Remove the original action + _promptFsm.RemoveFirstAction("Challenge Audio"); + + // Start the FSM from the state 'Challenge' + _promptFsm.SetState("Challenge"); } } diff --git a/HKMP/Game/Client/Entity/EntityManager.cs b/HKMP/Game/Client/Entity/EntityManager.cs index 3697e1a0..238d4870 100644 --- a/HKMP/Game/Client/Entity/EntityManager.cs +++ b/HKMP/Game/Client/Entity/EntityManager.cs @@ -202,10 +202,6 @@ public bool HandleEntityUpdate(EntityUpdate entityUpdate, bool alreadyInSceneUpd /// The reliable entity update to handle. /// Whether this is the update from the already in scene packet. public bool HandleReliableEntityUpdate(ReliableEntityUpdate entityUpdate, bool alreadyInSceneUpdate = false) { - if (IsSceneHost) { - return true; - } - if (!_entities.TryGetValue(entityUpdate.Id, out var entity) || !IsSceneHostDetermined) { if (IsSceneHostDetermined) { Logger.Debug($"Could not find entity ({entityUpdate.Id}) to apply update for; storing update for now"); @@ -218,18 +214,22 @@ public bool HandleReliableEntityUpdate(ReliableEntityUpdate entityUpdate, bool a return false; } - if (entityUpdate.UpdateTypes.Contains(EntityUpdateType.Active)) { - entity.UpdateIsActive(entityUpdate.IsActive); + // Check if we are not the scene host for processing this data, active state and host FSM data should only + // be applied if we not the scene host, while data type below should always be applied + if (!IsSceneHost) { + if (entityUpdate.UpdateTypes.Contains(EntityUpdateType.Active)) { + entity.UpdateIsActive(entityUpdate.IsActive); + } + + if (entityUpdate.UpdateTypes.Contains(EntityUpdateType.HostFsm)) { + entity.UpdateHostFsmData(entityUpdate.HostFsmData); + } } if (entityUpdate.UpdateTypes.Contains(EntityUpdateType.Data)) { entity.UpdateData(entityUpdate.GenericData); } - if (entityUpdate.UpdateTypes.Contains(EntityUpdateType.HostFsm)) { - entity.UpdateHostFsmData(entityUpdate.HostFsmData); - } - return true; } From f624f3d77fdacdb83ce68e1503bcddbfd00c3f27 Mon Sep 17 00:00:00 2001 From: Extremelyd1 <10898310+Extremelyd1@users.noreply.github.com> Date: Mon, 22 Jul 2024 22:49:53 +0200 Subject: [PATCH 117/216] Improve immediate save synchronisation --- HKMP/Fsm/FsmPatcher.cs | 47 ++-- HKMP/Game/Client/Save/PersistentFsmData.cs | 21 +- HKMP/Game/Client/Save/SaveChanges.cs | 265 +++++++++++++++++++++ HKMP/Game/Client/Save/SaveManager.cs | 44 +++- HKMP/Resource/save-data.json | 219 +++++++++++------ 5 files changed, 489 insertions(+), 107 deletions(-) create mode 100644 HKMP/Game/Client/Save/SaveChanges.cs diff --git a/HKMP/Fsm/FsmPatcher.cs b/HKMP/Fsm/FsmPatcher.cs index b53951bb..c7e1cd41 100644 --- a/HKMP/Fsm/FsmPatcher.cs +++ b/HKMP/Fsm/FsmPatcher.cs @@ -86,21 +86,38 @@ private void OnFsmEnable(On.PlayMakerFSM.orig_OnEnable orig, PlayMakerFSM self) // 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"); + } - // Code for modifying the collision check on collapsing floors to include remote players (not working) - // if (self.name.Equals("Collapser Small") && self.Fsm.Name.Equals("collapse small")) { - // self.InsertAction("Idle", new Collision2dEventLayer { - // Enabled = true, - // collideLayer = 9, - // collideTag = new FsmString(), - // sendEvent = FsmEvent.GetFsmEvent("BREAK"), - // storeCollider = new FsmGameObject(), - // storeForce = new FsmFloat() - // }, 7); - // self.RemoveFirstAction("Idle"); - // - // var rigidbody = self.gameObject.AddComponent(); - // rigidbody.isKinematic = true; - // } + // 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"); + } } } diff --git a/HKMP/Game/Client/Save/PersistentFsmData.cs b/HKMP/Game/Client/Save/PersistentFsmData.cs index 83e27c13..91029a11 100644 --- a/HKMP/Game/Client/Save/PersistentFsmData.cs +++ b/HKMP/Game/Client/Save/PersistentFsmData.cs @@ -1,5 +1,4 @@ using System; -using HutongGames.PlayMaker; namespace Hkmp.Game.Client.Save; @@ -10,16 +9,24 @@ internal class PersistentFsmData { /// /// The persistent item data with the ID and scene name. /// - public PersistentItemData PersistentItemData { get; set; } + public PersistentItemData PersistentItemData { get; init; } /// - /// The function to get the current integer value. Could be null if a boolean is used instead. + /// Function to get the current integer value. Could be null if a boolean is used instead. /// - public Func CurrentInt { get; set; } + public Func GetCurrentInt { get; init; } /// - /// The function to get the current boolean value. Could be null if an integer is used instead. + /// Action to set the current integer value. Could be null if a boolean is used instead. /// - public Func CurrentBool { get; set; } + public Action SetCurrentInt { get; init; } + /// + /// Function to get the current boolean value. Could be null if an integer is used instead. + /// + public Func GetCurrentBool { get; init; } + /// + /// Action to set the current boolean value. Could be null if an integer is used instead. + /// + public Action SetCurrentBool { get; init; } /// /// The last value for the integer if used. @@ -33,5 +40,5 @@ internal class PersistentFsmData { /// /// Whether an int is stored for this data. /// - public bool IsInt => CurrentInt != null; + public bool IsInt => GetCurrentInt != null; } diff --git a/HKMP/Game/Client/Save/SaveChanges.cs b/HKMP/Game/Client/Save/SaveChanges.cs new file mode 100644 index 00000000..4cd988e6 --- /dev/null +++ b/HKMP/Game/Client/Save/SaveChanges.cs @@ -0,0 +1,265 @@ +using Hkmp.Util; +using HutongGames.PlayMaker.Actions; +using UnityEngine; +using Logger = Hkmp.Logging.Logger; + +namespace Hkmp.Game.Client.Save; + +/// +/// Class that handles incoming save changes that have an immediate effect in the current scene for the local player. +/// E.g. breakable walls that also break in another scene, tollgates that are being paid, stag station being bought. +/// +internal class SaveChanges { + /// + /// Apply a change in player data from a save update for the given name immediately. This checks whether + /// the local player is in a scene where the changes in player data have an effect on the environment. + /// For example, a breakable wall that also opens up in another scene or a stag station being bought. + /// + /// The name of the PlayerData entry. + public void ApplyPlayerDataSaveChange(string name) { + Logger.Debug($"ApplyPlayerData for name: {name}"); + + var currentScene = UnityEngine.SceneManagement.SceneManager.GetActiveScene().name; + + if (name == "crossroadsMawlekWall" && currentScene == "Crossroads_33") { + GameObject breakWall = null; + PlayMakerFSM breakWallFsm = null; + PlayMakerFSM fullWallFsm = null; + + var found = 0; + + var fsms = Object.FindObjectsOfType(true); + foreach (var fsm in fsms) { + if (fsm.Fsm.Name.Equals("playerdata_activation")) { + if (fsm.name.Equals("break_wall_left")) { + breakWall = fsm.gameObject; + breakWallFsm = fsm; + + found++; + if (found == 2) { + break; + } + } else if (fsm.name.Equals("full_wall_left")) { + fullWallFsm = fsm; + + found++; + if (found == 2) { + break; + } + } + } + } + + if (found < 2) { + Logger.Error("Could not find breakable wall objects for 'crossroadsMawlekWall' player data change"); + return; + } + + // Activate the breakWall object and set variable and state in the FSM + breakWall!.SetActive(true); + breakWallFsm.FsmVariables.GetFsmBool("Activate").Value = true; + breakWallFsm.SetState("Check Activation"); + + // Disable the full wall + fullWallFsm!.SetState("Check Activation"); + return; + } + + if (name == "dungDefenderWallBroken" && currentScene == "Abyss_01") { + GameObject wall = null; + GameObject wallBroken = null; + + var found = 0; + + var fsms = Object.FindObjectsOfType(true); + foreach (var fsm in fsms) { + if (fsm.name.Equals("dung_defender_wall")) { + wall = fsm.gameObject; + + found++; + if (found == 2) { + break; + } + } else if (fsm.name.Equals("dung_defender_wall_broken")) { + wallBroken = fsm.gameObject; + + found++; + if (found == 2) { + break; + } + } + } + + if (found < 2) { + Logger.Error("Could not find breakable wall objects for 'dungDefenderWallBroken' player data change"); + return; + } + + wall!.SetActive(false); + wallBroken!.SetActive(true); + return; + } + + if ( + name == "openedCrossroads" && currentScene == "Crossroads_47" || + name == "openedGreenpath" && currentScene == "Fungus1_16_alt" || + name == "openedFungalWastes" && currentScene == "Fungus2_02" || + name == "openedRuins1" && currentScene == "Ruins1_29" || + name == "openedRuins2" && currentScene == "Ruins2_08" || + name == "openedDeepnest" && currentScene == "Deepnest_09" || + name == "openedRoyalGardens" && currentScene == "Fungus3_40" || + name == "openedHiddenStation" && currentScene == "Abyss_22" + ) { + var go = GameObject.Find("Station Bell"); + var fsm = go.LocateMyFSM("Stag Bell"); + + fsm.SetState("Box Disappear Anim"); + return; + } + + if ( + name == "tollBenchCity" && currentScene == "Ruins1_31" || + name == "tollBenchAbyss" && currentScene == "Abyss_18" || + name == "tollBenchQueensGardens" && currentScene == "Fungus3_50" + ) { + var go = GameObject.Find("Toll Machine Bench"); + var fsm = go.LocateMyFSM("Toll Machine Bench"); + + fsm.SetState("Box Down"); + return; + } + + if (name == "openedRestingGrounds02" && currentScene == "RestingGrounds_02") { + var go = GameObject.Find("Bottom Gate Collider"); + if (go == null) { + return; + } + + var fsm = go.LocateMyFSM("FSM"); + fsm.SetState("Destroy"); + return; + } + + if (name == "waterwaysGate" && currentScene == "Fungus2_23") { + var go = GameObject.Find("Waterways Gate"); + if (go == null) { + return; + } + + var fsm = go.LocateMyFSM("Gate Control"); + fsm.SetState("Destroy"); + return; + } + + if (name == "deepnestWall") { + GameObject go = null; + if (currentScene == "Deepnest_01") { + go = GameObject.Find("Breakable Wall"); + } else if (currentScene == "Fungus2_20") { + go = GameObject.Find("Breakable Wall Waterways"); + } + + if (go == null) { + return; + } + + var fsm = go.LocateMyFSM("breakable_wall_v2"); + fsm.SetState("Pause Frame"); + return; + } + + if (name == "oneWayArchive" && currentScene == "Fungus3_02") { + var go = GameObject.Find("One Way Wall Exit"); + if (go == null) { + return; + } + + var fsm = go.LocateMyFSM("FSM"); + fsm.SetState("Destroy"); + return; + } + + if (name == "openedGardensStagStation" && currentScene == "Fungus3_13") { + var go = GameObject.Find("royal_garden_slide_door"); + if (go == null) { + return; + } + + var fsm = go.LocateMyFSM("FSM"); + fsm.SetState("Destroy"); + } + } + + /// + /// Apply a change in persistent values from a save update for the given name immediately. This checks whether + /// the local player is in a scene where the changes in player data have an effect on the environment. + /// For example, a breakable wall that also opens up in another scene or a stag station being bought. + /// + /// The persistent item data containing the ID and scene name of the changed object. + public void ApplyPersistentValueSaveChange(PersistentItemData itemData) { + Logger.Debug($"ApplyPersistent for item data: {itemData}"); + + var currentScene = UnityEngine.SceneManagement.SceneManager.GetActiveScene().name; + + if ( + itemData.Id.StartsWith("Toll Gate Machine") && ( + itemData.SceneName == "Mines_33" && currentScene == "Mines_33" || + itemData.SceneName == "Fungus1_31" && currentScene == "Fungus1_31" + )) { + var go = GameObject.Find("Toll Gate Machine"); + var fsm = go.LocateMyFSM("Toll Machine"); + + fsm.RemoveFirstAction("Open Gates"); + + fsm.SetState("Box Disappear Anim"); + return; + } + + if (itemData.Id == "Collapser Tute 01" && itemData.SceneName == "Tutorial_01" && currentScene == "Tutorial_01") { + var go = GameObject.Find("Collapser Tute 01"); + if (go == null) { + return; + } + + var fsm = go.LocateMyFSM("collapse tute"); + fsm.RemoveFirstAction("Force Hard Landing"); + fsm.SetState("Crumble"); + return; + } + + if (itemData.Id.StartsWith("Collapser Small") && ( + itemData.SceneName == "Crossroads_21" && currentScene == "Crossroads_21" || + itemData.SceneName == "Crossroads_36" && currentScene == "Crossroads_36" || + itemData.SceneName == "Fungus1_24" && currentScene == "Fungus1_24" || + itemData.SceneName == "Fungus2_23" && currentScene == "Fungus2_23" || + itemData.SceneName == "Fungus3_28" && currentScene == "Fungus3_28" || + itemData.SceneName == "Fungus2_25" && currentScene == "Fungus2_25" || + itemData.SceneName == "Mines_06" && currentScene == "Mines_06" || + itemData.SceneName == "Deepnest_02" && currentScene == "Deepnest_02" || + itemData.SceneName == "Deepnest_03" && currentScene == "Deepnest_03" || + itemData.SceneName == "Deepnest_14" && currentScene == "Deepnest_14" || + itemData.SceneName == "Deepnest_16" && currentScene == "Deepnest_16" || + itemData.SceneName == "Deepnest_30" && currentScene == "Deepnest_30" || + itemData.SceneName == "Deepnest_33" && currentScene == "Deepnest_33" || + itemData.SceneName == "Deepnest_38" && currentScene == "Deepnest_38" || + itemData.SceneName == "Deepnest_39" && currentScene == "Deepnest_39" || + itemData.SceneName == "Deepnest_41" && currentScene == "Deepnest_41" || + itemData.SceneName == "Deepnest_45_v02" && currentScene == "Deepnest_45_v02" || + itemData.SceneName == "RestingGrounds_10" && currentScene == "RestingGrounds_10" || + itemData.SceneName == "Deepnest_Spider_Town" && currentScene == "Deepnest_Spider_Town" || + itemData.SceneName == "Waterways_09" && currentScene == "Waterways_09" || + itemData.SceneName == "Waterways_14" && currentScene == "Waterways_14" || + itemData.SceneName == "GG_Pipeway" && currentScene == "GG_Pipeway" || + itemData.SceneName == "White_Palace_02" && currentScene == "White_Palace_02" || + itemData.SceneName == "White_Palace_17" && currentScene == "White_Palace_17" + )) { + var go = GameObject.Find(itemData.Id); + if (go == null) { + return; + } + + var fsm = go.LocateMyFSM("collapse small"); + fsm.SetState("Split"); + } + } +} diff --git a/HKMP/Game/Client/Save/SaveManager.cs b/HKMP/Game/Client/Save/SaveManager.cs index 4a8bfd29..4620fcff 100644 --- a/HKMP/Game/Client/Save/SaveManager.cs +++ b/HKMP/Game/Client/Save/SaveManager.cs @@ -39,6 +39,11 @@ internal class SaveManager { /// private readonly EntityManager _entityManager; + /// + /// The save changes instance to apply immediate in-world changes from received save data. + /// + private readonly SaveChanges _saveChanges; + /// /// List of data classes for each FSM that has a persistent int/bool or geo rock attached to it. /// @@ -69,6 +74,7 @@ public SaveManager(NetClient netClient, PacketManager packetManager, EntityManag _netClient = netClient; _packetManager = packetManager; _entityManager = entityManager; + _saveChanges = new SaveChanges(); _persistentFsmData = new List(); _stringListHashes = new Dictionary(); @@ -291,7 +297,8 @@ private void OnSceneChanged(Scene oldScene, Scene newScene) { var persistentFsmData = new PersistentFsmData { PersistentItemData = persistentItemData, - CurrentInt = () => fsmInt.Value, + GetCurrentInt = () => fsmInt.Value, + SetCurrentInt = value => fsmInt.Value = value, LastIntValue = fsmInt.Value }; @@ -312,27 +319,31 @@ private void OnSceneChanged(Scene oldScene, Scene newScene) { Logger.Info($"Found persistent bool in scene: {persistentItemData}"); - Func currentBoolFunc = null; + Func getCurrentBoolFunc = null; + Action setCurrentBoolAction = null; var fsm = FSMUtility.FindFSMWithPersistentBool(itemObject.GetComponents()); if (fsm != null) { var fsmBool = fsm.FsmVariables.GetFsmBool("Activated"); - currentBoolFunc = () => fsmBool.Value; + getCurrentBoolFunc = () => fsmBool.Value; + setCurrentBoolAction = value => fsmBool.Value = value; } var vinePlatform = itemObject.GetComponent(); if (vinePlatform != null) { - currentBoolFunc = () => ReflectionHelper.GetField(vinePlatform, "activated"); + getCurrentBoolFunc = () => ReflectionHelper.GetField(vinePlatform, "activated"); + setCurrentBoolAction = value => ReflectionHelper.SetField(vinePlatform, "activated", value); } - if (currentBoolFunc == null) { + if (getCurrentBoolFunc == null) { continue; } var persistentFsmData = new PersistentFsmData { PersistentItemData = persistentItemData, - CurrentBool = currentBoolFunc, - LastBoolValue = currentBoolFunc.Invoke() + GetCurrentBool = getCurrentBoolFunc, + SetCurrentBool = setCurrentBoolAction, + LastBoolValue = getCurrentBoolFunc.Invoke() }; _persistentFsmData.Add(persistentFsmData); @@ -362,7 +373,8 @@ private void OnSceneChanged(Scene oldScene, Scene newScene) { var persistentFsmData = new PersistentFsmData { PersistentItemData = persistentItemData, - CurrentInt = () => fsmInt.Value, + GetCurrentInt = () => fsmInt.Value, + SetCurrentInt = value => fsmInt.Value = value, LastIntValue = fsmInt.Value }; @@ -428,7 +440,7 @@ private void OnUpdatePersistents() { } if (persistentFsmData.IsInt) { - var value = persistentFsmData.CurrentInt.Invoke(); + var value = persistentFsmData.GetCurrentInt.Invoke(); if (value == persistentFsmData.LastIntValue) { continue; } @@ -494,7 +506,7 @@ private void OnUpdatePersistents() { Logger.Info("Cannot find persistent int/geo rock data bool, not sending save update"); } } else { - var value = persistentFsmData.CurrentBool.Invoke(); + var value = persistentFsmData.GetCurrentBool.Invoke(); if (value == persistentFsmData.LastBoolValue) { continue; } @@ -796,6 +808,8 @@ private void UpdateSaveWithData(ushort index, byte[] encodedValue) { } else { throw new NotImplementedException($"Could not decode type: {type}"); } + + _saveChanges.ApplyPlayerDataSaveChange(name); } if (SaveDataMapping.GeoRockDataIndices.TryGetValue(index, out var itemData)) { @@ -803,11 +817,11 @@ private void UpdateSaveWithData(ushort index, byte[] encodedValue) { Logger.Info($"Received geo rock save update: {itemData.Id}, {itemData.SceneName}, {value}"); - // TODO: make the _persistentFsmData a dictionary for quicker lookups foreach (var persistentFsmData in _persistentFsmData) { var existingItemData = persistentFsmData.PersistentItemData; if (existingItemData.Id == itemData.Id && existingItemData.SceneName == itemData.SceneName) { + persistentFsmData.SetCurrentInt.Invoke(value); persistentFsmData.LastIntValue = value; } } @@ -826,11 +840,12 @@ private void UpdateSaveWithData(ushort index, byte[] encodedValue) { Logger.Info($"Received persistent bool save update: {itemData.Id}, {itemData.SceneName}, {value}"); - // TODO: make the _persistentFsmData a dictionary for quicker lookups foreach (var persistentFsmData in _persistentFsmData) { var existingItemData = persistentFsmData.PersistentItemData; if (existingItemData.Id == itemData.Id && existingItemData.SceneName == itemData.SceneName) { + Logger.Debug($"Setting last bool value for {existingItemData} to {value}"); + persistentFsmData.SetCurrentBool.Invoke(value); persistentFsmData.LastBoolValue = value; } } @@ -840,6 +855,8 @@ private void UpdateSaveWithData(ushort index, byte[] encodedValue) { sceneName = itemData.SceneName, activated = value }); + + _saveChanges.ApplyPersistentValueSaveChange(itemData); } else if (SaveDataMapping.PersistentIntDataIndices.TryGetValue(index, out itemData)) { if (CheckPlayerSpecificHosting(SaveDataMapping.PersistentIntDataBools, itemData)) { return; @@ -859,6 +876,7 @@ private void UpdateSaveWithData(ushort index, byte[] encodedValue) { var existingItemData = persistentFsmData.PersistentItemData; if (existingItemData.Id == itemData.Id && existingItemData.SceneName == itemData.SceneName) { + persistentFsmData.SetCurrentInt.Invoke(value); persistentFsmData.LastIntValue = value; } } @@ -868,6 +886,8 @@ private void UpdateSaveWithData(ushort index, byte[] encodedValue) { sceneName = itemData.SceneName, value = value }); + + _saveChanges.ApplyPersistentValueSaveChange(itemData); } // Decode a string from the given byte array and start index in that array using the EncodeUtil diff --git a/HKMP/Resource/save-data.json b/HKMP/Resource/save-data.json index 73d27658..3f280bc5 100644 --- a/HKMP/Resource/save-data.json +++ b/HKMP/Resource/save-data.json @@ -1873,27 +1873,33 @@ }, "openedCrossroads": { "Sync": true, - "SyncType": "Server" + "SyncType": "Server", + "IgnoreSceneHost": true }, "openedGreenpath": { "Sync": true, - "SyncType": "Server" + "SyncType": "Server", + "IgnoreSceneHost": true }, "openedRuins1": { "Sync": true, - "SyncType": "Server" + "SyncType": "Server", + "IgnoreSceneHost": true }, "openedRuins2": { "Sync": true, - "SyncType": "Server" + "SyncType": "Server", + "IgnoreSceneHost": true }, "openedFungalWastes": { "Sync": true, - "SyncType": "Server" + "SyncType": "Server", + "IgnoreSceneHost": true }, "openedRoyalGardens": { "Sync": true, - "SyncType": "Server" + "SyncType": "Server", + "IgnoreSceneHost": true }, "openedRestingGrounds": { "Sync": true, @@ -1901,7 +1907,8 @@ }, "openedDeepnest": { "Sync": true, - "SyncType": "Server" + "SyncType": "Server", + "IgnoreSceneHost": true }, "openedStagNest": { "Sync": true, @@ -1909,7 +1916,8 @@ }, "openedHiddenStation": { "Sync": true, - "SyncType": "Server" + "SyncType": "Server", + "IgnoreSceneHost": true }, "charmSlots": { "Sync": true, @@ -4959,7 +4967,8 @@ }, "tollBenchCity": { "Sync": true, - "SyncType": "Server" + "SyncType": "Server", + "IgnoreSceneHost": true }, "waterwaysGate": { "Sync": true, @@ -5057,7 +5066,8 @@ }, "tollBenchQueensGardens": { "Sync": true, - "SyncType": "Server" + "SyncType": "Server", + "IgnoreSceneHost": true }, "blizzardEnded": { "Sync": true, @@ -5093,7 +5103,8 @@ }, "tollBenchAbyss": { "Sync": true, - "SyncType": "Server" + "SyncType": "Server", + "IgnoreSceneHost": true }, "fountainGeo": { "Sync": true, @@ -7152,7 +7163,8 @@ }, "Value": { "Sync": true, - "SyncType": "Server" + "SyncType": "Server", + "IgnoreSceneHost": true } }, { @@ -8051,7 +8063,8 @@ }, "Value": { "Sync": true, - "SyncType": "Server" + "SyncType": "Server", + "IgnoreSceneHost": true } }, { @@ -9105,7 +9118,8 @@ }, "Value": { "Sync": true, - "SyncType": "Server" + "SyncType": "Server", + "IgnoreSceneHost": true } }, { @@ -9115,7 +9129,8 @@ }, "Value": { "Sync": true, - "SyncType": "Server" + "SyncType": "Server", + "IgnoreSceneHost": true } }, { @@ -14661,7 +14676,8 @@ }, "Value": { "Sync": true, - "SyncType": "Server" + "SyncType": "Server", + "IgnoreSceneHost": true } }, { @@ -15945,7 +15961,8 @@ }, "Value": { "Sync": true, - "SyncType": "Server" + "SyncType": "Server", + "IgnoreSceneHost": true } }, { @@ -16069,7 +16086,8 @@ }, "Value": { "Sync": true, - "SyncType": "Server" + "SyncType": "Server", + "IgnoreSceneHost": true } }, { @@ -16079,7 +16097,8 @@ }, "Value": { "Sync": true, - "SyncType": "Server" + "SyncType": "Server", + "IgnoreSceneHost": true } }, { @@ -16089,7 +16108,8 @@ }, "Value": { "Sync": true, - "SyncType": "Server" + "SyncType": "Server", + "IgnoreSceneHost": true } }, { @@ -16099,7 +16119,8 @@ }, "Value": { "Sync": true, - "SyncType": "Server" + "SyncType": "Server", + "IgnoreSceneHost": true } }, { @@ -16210,7 +16231,8 @@ }, "Value": { "Sync": true, - "SyncType": "Server" + "SyncType": "Server", + "IgnoreSceneHost": true } }, { @@ -16230,7 +16252,8 @@ }, "Value": { "Sync": true, - "SyncType": "Server" + "SyncType": "Server", + "IgnoreSceneHost": true } }, { @@ -16250,7 +16273,8 @@ }, "Value": { "Sync": true, - "SyncType": "Server" + "SyncType": "Server", + "IgnoreSceneHost": true } }, { @@ -16260,7 +16284,8 @@ }, "Value": { "Sync": true, - "SyncType": "Server" + "SyncType": "Server", + "IgnoreSceneHost": true } }, { @@ -16270,7 +16295,8 @@ }, "Value": { "Sync": true, - "SyncType": "Server" + "SyncType": "Server", + "IgnoreSceneHost": true } }, { @@ -16300,7 +16326,8 @@ }, "Value": { "Sync": true, - "SyncType": "Server" + "SyncType": "Server", + "IgnoreSceneHost": true } }, { @@ -16601,7 +16628,8 @@ }, "Value": { "Sync": true, - "SyncType": "Server" + "SyncType": "Server", + "IgnoreSceneHost": true } }, { @@ -16655,7 +16683,8 @@ }, "Value": { "Sync": true, - "SyncType": "Server" + "SyncType": "Server", + "IgnoreSceneHost": true } }, { @@ -16741,7 +16770,8 @@ }, "Value": { "Sync": true, - "SyncType": "Server" + "SyncType": "Server", + "IgnoreSceneHost": true } }, { @@ -16762,7 +16792,8 @@ }, "Value": { "Sync": true, - "SyncType": "Server" + "SyncType": "Server", + "IgnoreSceneHost": true } }, { @@ -16815,7 +16846,8 @@ }, "Value": { "Sync": true, - "SyncType": "Server" + "SyncType": "Server", + "IgnoreSceneHost": true } }, { @@ -16997,7 +17029,8 @@ }, "Value": { "Sync": true, - "SyncType": "Server" + "SyncType": "Server", + "IgnoreSceneHost": true } }, { @@ -17062,7 +17095,8 @@ }, "Value": { "Sync": true, - "SyncType": "Server" + "SyncType": "Server", + "IgnoreSceneHost": true } }, { @@ -17072,7 +17106,8 @@ }, "Value": { "Sync": true, - "SyncType": "Server" + "SyncType": "Server", + "IgnoreSceneHost": true } }, { @@ -17166,7 +17201,8 @@ }, "Value": { "Sync": true, - "SyncType": "Server" + "SyncType": "Server", + "IgnoreSceneHost": true } }, { @@ -17196,7 +17232,8 @@ }, "Value": { "Sync": true, - "SyncType": "Server" + "SyncType": "Server", + "IgnoreSceneHost": true } }, { @@ -17236,7 +17273,8 @@ }, "Value": { "Sync": true, - "SyncType": "Server" + "SyncType": "Server", + "IgnoreSceneHost": true } }, { @@ -17246,7 +17284,8 @@ }, "Value": { "Sync": true, - "SyncType": "Server" + "SyncType": "Server", + "IgnoreSceneHost": true } }, { @@ -17276,7 +17315,8 @@ }, "Value": { "Sync": true, - "SyncType": "Server" + "SyncType": "Server", + "IgnoreSceneHost": true } }, { @@ -17316,7 +17356,8 @@ }, "Value": { "Sync": true, - "SyncType": "Server" + "SyncType": "Server", + "IgnoreSceneHost": true } }, { @@ -17346,7 +17387,8 @@ }, "Value": { "Sync": true, - "SyncType": "Server" + "SyncType": "Server", + "IgnoreSceneHost": true } }, { @@ -17376,7 +17418,8 @@ }, "Value": { "Sync": true, - "SyncType": "Server" + "SyncType": "Server", + "IgnoreSceneHost": true } }, { @@ -17426,7 +17469,8 @@ }, "Value": { "Sync": true, - "SyncType": "Server" + "SyncType": "Server", + "IgnoreSceneHost": true } }, { @@ -17446,7 +17490,8 @@ }, "Value": { "Sync": true, - "SyncType": "Server" + "SyncType": "Server", + "IgnoreSceneHost": true } }, { @@ -17456,7 +17501,8 @@ }, "Value": { "Sync": true, - "SyncType": "Server" + "SyncType": "Server", + "IgnoreSceneHost": true } }, { @@ -17476,7 +17522,8 @@ }, "Value": { "Sync": true, - "SyncType": "Server" + "SyncType": "Server", + "IgnoreSceneHost": true } }, { @@ -17496,7 +17543,8 @@ }, "Value": { "Sync": true, - "SyncType": "Server" + "SyncType": "Server", + "IgnoreSceneHost": true } }, { @@ -17516,7 +17564,8 @@ }, "Value": { "Sync": true, - "SyncType": "Server" + "SyncType": "Server", + "IgnoreSceneHost": true } }, { @@ -20096,7 +20145,8 @@ }, "Value": { "Sync": true, - "SyncType": "Server" + "SyncType": "Server", + "IgnoreSceneHost": true } }, { @@ -20208,7 +20258,8 @@ }, "Value": { "Sync": true, - "SyncType": "Server" + "SyncType": "Server", + "IgnoreSceneHost": true } }, { @@ -20258,7 +20309,8 @@ }, "Value": { "Sync": true, - "SyncType": "Server" + "SyncType": "Server", + "IgnoreSceneHost": true } }, { @@ -20278,7 +20330,8 @@ }, "Value": { "Sync": true, - "SyncType": "Server" + "SyncType": "Server", + "IgnoreSceneHost": true } }, { @@ -20288,7 +20341,8 @@ }, "Value": { "Sync": true, - "SyncType": "Server" + "SyncType": "Server", + "IgnoreSceneHost": true } }, { @@ -20298,7 +20352,8 @@ }, "Value": { "Sync": true, - "SyncType": "Server" + "SyncType": "Server", + "IgnoreSceneHost": true } }, { @@ -20318,7 +20373,8 @@ }, "Value": { "Sync": true, - "SyncType": "Server" + "SyncType": "Server", + "IgnoreSceneHost": true } }, { @@ -20338,7 +20394,8 @@ }, "Value": { "Sync": true, - "SyncType": "Server" + "SyncType": "Server", + "IgnoreSceneHost": true } }, { @@ -20348,7 +20405,8 @@ }, "Value": { "Sync": true, - "SyncType": "Server" + "SyncType": "Server", + "IgnoreSceneHost": true } }, { @@ -21500,7 +21558,8 @@ }, "Value": { "Sync": true, - "SyncType": "Server" + "SyncType": "Server", + "IgnoreSceneHost": true } }, { @@ -21510,7 +21569,8 @@ }, "Value": { "Sync": true, - "SyncType": "Server" + "SyncType": "Server", + "IgnoreSceneHost": true } }, { @@ -22074,7 +22134,8 @@ }, "Value": { "Sync": true, - "SyncType": "Server" + "SyncType": "Server", + "IgnoreSceneHost": true } }, { @@ -22235,7 +22296,8 @@ }, "Value": { "Sync": true, - "SyncType": "Server" + "SyncType": "Server", + "IgnoreSceneHost": true } }, { @@ -22589,7 +22651,8 @@ }, "Value": { "Sync": true, - "SyncType": "Server" + "SyncType": "Server", + "IgnoreSceneHost": true } }, { @@ -22733,7 +22796,8 @@ }, "Value": { "Sync": true, - "SyncType": "Server" + "SyncType": "Server", + "IgnoreSceneHost": true } }, { @@ -22904,7 +22968,8 @@ }, "Value": { "Sync": true, - "SyncType": "Server" + "SyncType": "Server", + "IgnoreSceneHost": true } }, { @@ -25921,7 +25986,8 @@ }, "Value": { "Sync": true, - "SyncType": "Server" + "SyncType": "Server", + "IgnoreSceneHost": true } }, { @@ -27514,7 +27580,8 @@ }, "Value": { "Sync": true, - "SyncType": "Server" + "SyncType": "Server", + "IgnoreSceneHost": true } }, { @@ -28813,7 +28880,8 @@ }, "Value": { "Sync": true, - "SyncType": "Server" + "SyncType": "Server", + "IgnoreSceneHost": true } }, { @@ -28944,7 +29012,8 @@ }, "Value": { "Sync": true, - "SyncType": "Server" + "SyncType": "Server", + "IgnoreSceneHost": true } }, { @@ -29268,7 +29337,8 @@ }, "Value": { "Sync": true, - "SyncType": "Server" + "SyncType": "Server", + "IgnoreSceneHost": true } }, { @@ -29288,7 +29358,8 @@ }, "Value": { "Sync": true, - "SyncType": "Server" + "SyncType": "Server", + "IgnoreSceneHost": true } }, { @@ -29368,7 +29439,8 @@ }, "Value": { "Sync": true, - "SyncType": "Server" + "SyncType": "Server", + "IgnoreSceneHost": true } }, { @@ -29579,7 +29651,8 @@ }, "Value": { "Sync": true, - "SyncType": "Server" + "SyncType": "Server", + "IgnoreSceneHost": true } }, { From 446640e380aa7b08bdc0e01a76f7b09249b89d2c Mon Sep 17 00:00:00 2001 From: Extremelyd1 <10898310+Extremelyd1@users.noreply.github.com> Date: Wed, 24 Jul 2024 11:33:14 +0200 Subject: [PATCH 118/216] Refactor commands to fit new UI --- HKMP/Game/Client/ClientManager.cs | 2 - HKMP/Game/Command/Client/AddonCommand.cs | 30 ++++++---- HKMP/Game/Command/Client/ConnectCommand.cs | 61 --------------------- HKMP/Game/Command/Client/HostCommand.cs | 64 ---------------------- HKMP/Ui/Chat/ChatBox.cs | 46 ++++++++++++---- 5 files changed, 53 insertions(+), 150 deletions(-) delete mode 100644 HKMP/Game/Command/Client/ConnectCommand.cs delete mode 100644 HKMP/Game/Command/Client/HostCommand.cs diff --git a/HKMP/Game/Client/ClientManager.cs b/HKMP/Game/Client/ClientManager.cs index 5fd35e29..9fbc079e 100644 --- a/HKMP/Game/Client/ClientManager.cs +++ b/HKMP/Game/Client/ClientManager.cs @@ -278,8 +278,6 @@ ModSettings modSettings /// Register the default client commands. /// private void RegisterCommands() { - _commandManager.RegisterCommand(new ConnectCommand(this)); - _commandManager.RegisterCommand(new HostCommand(_serverManager)); _commandManager.RegisterCommand(new AddonCommand(_addonManager, _netClient)); } diff --git a/HKMP/Game/Command/Client/AddonCommand.cs b/HKMP/Game/Command/Client/AddonCommand.cs index 89814b6a..726bae30 100644 --- a/HKMP/Game/Command/Client/AddonCommand.cs +++ b/HKMP/Game/Command/Client/AddonCommand.cs @@ -42,18 +42,24 @@ public void Execute(string[] arguments) { var action = arguments[1]; if (action == "list") { - var message = "Loaded addons: "; - message += string.Join( - ", ", - _addonManager.GetLoadedAddons().Select(addon => { - var msg = $"{addon.GetName()} {addon.GetVersion()}"; - if (addon is TogglableClientAddon {Disabled: true }) { - msg += " (disabled)"; - } - - return msg; - }) - ); + string message; + var addons = _addonManager.GetLoadedAddons(); + if (addons.Count == 0) { + message = "No addons loaded."; + } else { + message = "Loaded addons: "; + message += string.Join( + ", ", + addons.Select(addon => { + var msg = $"{addon.GetName()} {addon.GetVersion()}"; + if (addon is TogglableClientAddon { Disabled: true }) { + msg += " (disabled)"; + } + + return msg; + }) + ); + } UiManager.InternalChatBox.AddMessage(message); return; diff --git a/HKMP/Game/Command/Client/ConnectCommand.cs b/HKMP/Game/Command/Client/ConnectCommand.cs deleted file mode 100644 index 62eb290e..00000000 --- a/HKMP/Game/Command/Client/ConnectCommand.cs +++ /dev/null @@ -1,61 +0,0 @@ -using Hkmp.Api.Command.Client; -using Hkmp.Game.Client; -using Hkmp.Ui; - -namespace Hkmp.Game.Command.Client; - -/// -/// Command for connecting the local user to a server with a given address, port and username. -/// -internal class ConnectCommand : IClientCommand { - /// - public string Trigger => "/connect"; - - /// - public string[] Aliases => new[] { "/disconnect" }; - - /// - /// The client manager instance. - /// - private readonly ClientManager _clientManager; - - public ConnectCommand(ClientManager clientManager) { - _clientManager = clientManager; - } - - /// - public void Execute(string[] arguments) { - var command = arguments[0]; - if (command == Aliases[0]) { - _clientManager.Disconnect(); - UiManager.InternalChatBox.AddMessage("You are disconnected from the server"); - return; - } - - if (arguments.Length != 4) { - SendUsage(); - return; - } - - var address = arguments[1]; - - var portString = arguments[2]; - var parsedPort = int.TryParse(portString, out var port); - if (!parsedPort || port < 1 || port > 99999) { - UiManager.InternalChatBox.AddMessage("Invalid port!"); - return; - } - - var username = arguments[3]; - - _clientManager.Connect(address, port, username); - UiManager.InternalChatBox.AddMessage($"Trying to connect to {address}:{port} as {username}..."); - } - - /// - /// Sends the command usage to the chat box. - /// - private void SendUsage() { - UiManager.InternalChatBox.AddMessage($"Invalid usage: {Trigger}
"); - } -} diff --git a/HKMP/Game/Command/Client/HostCommand.cs b/HKMP/Game/Command/Client/HostCommand.cs deleted file mode 100644 index 2b50d70b..00000000 --- a/HKMP/Game/Command/Client/HostCommand.cs +++ /dev/null @@ -1,64 +0,0 @@ -using System; -using Hkmp.Api.Command.Client; -using Hkmp.Game.Server; -using Hkmp.Ui; - -namespace Hkmp.Game.Command.Client; - -/// -/// Command for controlling local server hosting. -/// -internal class HostCommand : IClientCommand { - /// - public string Trigger => "/host"; - - /// - public string[] Aliases => Array.Empty(); - - /// - /// The server manager instance. - /// - private readonly ServerManager _serverManager; - - public HostCommand(ServerManager serverManager) { - _serverManager = serverManager; - } - - /// - public void Execute(string[] arguments) { - if (arguments.Length < 2) { - SendUsage(); - return; - } - - var action = arguments[1]; - if (action == "start") { - if (arguments.Length != 3) { - SendUsage(); - return; - } - - var portString = arguments[2]; - var parsedPort = int.TryParse(portString, out var port); - if (!parsedPort || port < 1 || port > 99999) { - UiManager.InternalChatBox.AddMessage("Invalid port!"); - return; - } - - _serverManager.Start(port); - UiManager.InternalChatBox.AddMessage($"Started server on port {port}"); - } else if (action == "stop") { - _serverManager.Stop(); - UiManager.InternalChatBox.AddMessage("Stopped server"); - } else { - SendUsage(); - } - } - - /// - /// Sends the command usage to the chat box. - /// - private void SendUsage() { - UiManager.InternalChatBox.AddMessage($"Invalid usage: {Trigger} [port]"); - } -} diff --git a/HKMP/Ui/Chat/ChatBox.cs b/HKMP/Ui/Chat/ChatBox.cs index 90e5d7c5..50a2f850 100644 --- a/HKMP/Ui/Chat/ChatBox.cs +++ b/HKMP/Ui/Chat/ChatBox.cs @@ -7,6 +7,7 @@ using Hkmp.Ui.Resources; using Hkmp.Util; using UnityEngine; +using Logger = Hkmp.Logging.Logger; using Object = UnityEngine.Object; namespace Hkmp.Ui.Chat; @@ -171,19 +172,42 @@ private void CheckKeyBinds(ModSettings modSettings) { } } else if (Input.GetKeyDown(modSettings.OpenChatKey)) { var gameManager = GameManager.instance; + if (gameManager == null) { + Logger.Debug("Could not open chat, GM is null"); + return; + } + + var gameState = gameManager.gameState; + if (gameState != GameState.PLAYING && gameState != GameState.MAIN_MENU) { + Logger.Debug($"Could not open chat, game state is incorrect: {gameState}"); + return; + } + var uiManager = UIManager.instance; + if (uiManager == null) { + Logger.Debug("Could not open chat, UIM is null"); + return; + } + + var uiState = uiManager.uiState; + if (uiState != UIState.PLAYING && uiState != UIState.MAIN_MENU_HOME) { + Logger.Debug($"Could not open chat, UI state is incorrect: {uiState}"); + return; + } + var heroController = HeroController.instance; - if (gameManager == null - || uiManager == null - || gameManager.gameState != GameState.PLAYING - || uiManager.uiState != UIState.PLAYING - // If the hero is charging their nail and chat opens, it will cause a flashing effect - || (heroController != null && heroController.cState.nailCharging) - // If we are in the inventory, opening the chat has side-effects, such as floating - || IsInventoryOpen() - // If we are in a godhome menu, we will soft-lock opening the chat - || IsGodHomeMenuOpen() - ) { + if (heroController != null && heroController.cState.nailCharging) { + Logger.Debug("Could not open chat, player is charging nail"); + return; + } + + if (gameState == GameState.PLAYING && IsInventoryOpen()) { + Logger.Debug("Could not open chat, inventory is open"); + return; + } + + if (IsGodHomeMenuOpen()) { + Logger.Debug("Could not open chat, GodHome menu is open"); return; } From 5c2a857098d170ed77d90a572c7443faa141185d Mon Sep 17 00:00:00 2001 From: Extremelyd1 <10898310+Extremelyd1@users.noreply.github.com> Date: Wed, 24 Jul 2024 11:33:52 +0200 Subject: [PATCH 119/216] Remove unused server manager in client manager --- HKMP/Game/Client/ClientManager.cs | 6 ------ 1 file changed, 6 deletions(-) diff --git a/HKMP/Game/Client/ClientManager.cs b/HKMP/Game/Client/ClientManager.cs index 9fbc079e..b1200907 100644 --- a/HKMP/Game/Client/ClientManager.cs +++ b/HKMP/Game/Client/ClientManager.cs @@ -35,11 +35,6 @@ internal class ClientManager : IClientManager { /// private readonly NetClient _netClient; - /// - /// The server manager instance. - /// - private readonly ServerManager _serverManager; - /// /// The UI manager instance. /// @@ -179,7 +174,6 @@ public ClientManager( ModSettings modSettings ) { _netClient = netClient; - _serverManager = serverManager; _uiManager = uiManager; _serverSettings = serverSettings; _modSettings = modSettings; From d609a952396fa650659485db3b3bc6c18fa0d645 Mon Sep 17 00:00:00 2001 From: Extremelyd1 <10898310+Extremelyd1@users.noreply.github.com> Date: Wed, 24 Jul 2024 21:35:37 +0200 Subject: [PATCH 120/216] Fix scene entry issue with new saves --- HKMP/Game/Server/ServerManager.cs | 2 -- HKMP/Networking/UdpUpdateManager.cs | 7 ++++++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/HKMP/Game/Server/ServerManager.cs b/HKMP/Game/Server/ServerManager.cs index c1269a16..89a7102f 100644 --- a/HKMP/Game/Server/ServerManager.cs +++ b/HKMP/Game/Server/ServerManager.cs @@ -291,8 +291,6 @@ private void OnHelloServer(ushort id, HelloServer helloServer) { } catch (Exception e) { Logger.Error($"Exception thrown while invoking PlayerConnect event:\n{e}"); } - - OnClientEnterScene(playerData); } /// diff --git a/HKMP/Networking/UdpUpdateManager.cs b/HKMP/Networking/UdpUpdateManager.cs index 0531cbe0..835fc9c9 100644 --- a/HKMP/Networking/UdpUpdateManager.cs +++ b/HKMP/Networking/UdpUpdateManager.cs @@ -216,7 +216,12 @@ private void CreateAndSendUpdatePacket() { CurrentUpdatePacket.AckField[i] = receivedQueue.Contains(pastSequence); } - packet = CurrentUpdatePacket.CreatePacket(); + try { + packet = CurrentUpdatePacket.CreatePacket(); + } catch (Exception e) { + Logger.Error($"An error occurred while trying to create packet:\n{e}"); + return; + } // Reset the packet by creating a new instance, // but keep the original instance for reliability data re-sending From 57747a437bdb88aeddddde7121bcf6d3fc0a8ea7 Mon Sep 17 00:00:00 2001 From: Extremelyd1 <10898310+Extremelyd1@users.noreply.github.com> Date: Thu, 25 Jul 2024 11:03:51 +0200 Subject: [PATCH 121/216] Fix player lock if remote player hits City bridge lever --- HKMP/Game/Client/Entity/EntityManager.cs | 135 +++++++++++++++++++++++ 1 file changed, 135 insertions(+) diff --git a/HKMP/Game/Client/Entity/EntityManager.cs b/HKMP/Game/Client/Entity/EntityManager.cs index 238d4870..f16a5c8e 100644 --- a/HKMP/Game/Client/Entity/EntityManager.cs +++ b/HKMP/Game/Client/Entity/EntityManager.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Reflection; using Hkmp.Game.Client.Entity.Action; using Hkmp.Game.Client.Entity.Component; using Hkmp.Networking.Client; @@ -8,11 +9,15 @@ using Hkmp.Util; using HutongGames.PlayMaker.Actions; using Modding; +using Mono.Cecil.Cil; +using MonoMod.Cil; +using MonoMod.RuntimeDetour; using UnityEngine; using UnityEngine.SceneManagement; using FindGameObject = On.HutongGames.PlayMaker.Actions.FindGameObject; using Logger = Hkmp.Logging.Logger; using Object = UnityEngine.Object; +using OpCodes = Mono.Cecil.Cil.OpCodes; namespace Hkmp.Game.Client.Entity; @@ -60,6 +65,17 @@ public EntityManager(NetClient netClient) { UnityEngine.SceneManagement.SceneManager.activeSceneChanged += OnSceneChanged; FindGameObject.Find += OnFindGameObject; + + On.BridgeLever.OnTriggerEnter2D += BridgeLeverOnTriggerEnter2D; + + var type = typeof(BridgeLever).GetNestedType("d__13", + BindingFlags.NonPublic | BindingFlags.Instance); + + // TODO: store this hook and unregister if entity system is not used + new ILHook( + type.GetMethod("MoveNext", BindingFlags.NonPublic | BindingFlags.Instance), + BridgeLeverOnOpenBridge + ); } /// @@ -496,4 +512,123 @@ private void OnFindGameObject(FindGameObject.orig_Find orig, HutongGames.PlayMak Logger.Debug(" Name did not match any entity"); } + + /// + /// Whether the local player hit the bridge lever. + /// + private bool _localPlayerBridgeLever; + + /// + /// On Hook that stores a boolean depending on whether the local player hit the bridge lever or not. Used in the + /// IL Hook below. + /// + private void BridgeLeverOnTriggerEnter2D(On.BridgeLever.orig_OnTriggerEnter2D orig, BridgeLever self, Collider2D collision) { + Logger.Debug("BridgeLeverOnTriggerEnter2D"); + + var activated = ReflectionHelper.GetField(self, "activated"); + + Logger.Debug($" activated: {activated}, collision tag: {collision.tag}"); + if (!activated && collision.tag == "Nail Attack") { + _localPlayerBridgeLever = collision.transform.parent?.parent?.tag == "Player"; + Logger.Debug($" Bridge lever hit: bool: {_localPlayerBridgeLever}"); + } + + orig(self, collision); + } + + /// + /// IL Hook to modify the OpenBridge method of BridgeLever to exclude locking players in place that did not hit + /// the lever. + /// + private void BridgeLeverOnOpenBridge(ILContext il) { + Logger.Debug("BridgeLeverOnOpenBridge IL"); + try { + // Create a cursor for this context + var c = new ILCursor(il); + + // Define the collection of instructions that matches the FreezeMoment call + Func[] freezeMomentInstructions = [ + i => i.MatchCall(typeof(global::GameManager), "get_instance"), + i => i.MatchLdcI4(1), + i => i.MatchCallvirt(typeof(global::GameManager), "FreezeMoment") + ]; + + // Goto after the FreezeMoment call + c.GotoNext(MoveType.Before, freezeMomentInstructions); + + // Emit a delegate that puts the boolean on the stack + c.EmitDelegate(() => _localPlayerBridgeLever); + + // Define the label to branch to + var afterFreezeLabel = c.DefineLabel(); + + // Then emit an instruction that branches to after the freeze if the boolean is false + c.Emit(OpCodes.Brfalse, afterFreezeLabel); + + // Goto after the FreezeMoment call + c.GotoNext(MoveType.After, freezeMomentInstructions); + + // Mark the label after the FreezeMoment call so we branch here + c.MarkLabel(afterFreezeLabel); + + // Goto after the rumble call + c.GotoNext( + MoveType.After, + i => i.MatchCall(typeof(GameCameras), "get_instance"), + i => i.MatchLdfld(typeof(GameCameras), "cameraShakeFSM"), + i => i.MatchLdstr("RumblingMed"), + i => i.MatchLdcI4(1), + i => i.MatchCall(typeof(FSMUtility), "SetBool") + ); + + // Emit a delegate that puts the boolean on the stack + c.EmitDelegate(() => _localPlayerBridgeLever); + + // Define the label to branch to + var afterRoarEnterLabel = c.DefineLabel(); + + // Emit another instruction that branches over the roar enter FSM calls + c.Emit(OpCodes.Brfalse, afterRoarEnterLabel); + + // Goto after the roar enter call + c.GotoNext( + MoveType.After, + i => i.MatchLdstr("ROAR ENTER"), + i => i.MatchLdcI4(0), + i => i.MatchCall(typeof(FSMUtility), "SendEventToGameObject") + ); + + // Mark the label after the Roar Enter call so we branch here + c.MarkLabel(afterRoarEnterLabel); + + // Define the collection of instructions that matches the roar exit FSM call + Func[] roarExitInstructions = [ + i => i.MatchCall(typeof(HeroController), "get_instance"), + i => i.MatchCallvirt(typeof(UnityEngine.Component), "get_gameObject"), + i => i.MatchLdstr("ROAR EXIT"), + i => i.MatchLdcI4(0), + i => i.MatchCall(typeof(FSMUtility), "SendEventToGameObject") + ]; + + // Goto before the roar exit FSM call + c.GotoNext(MoveType.Before, roarExitInstructions); + + // Emit a delegate that puts the boolean on the stack + c.EmitDelegate(() => _localPlayerBridgeLever); + + // Define the label to branch to + var afterRoarExitLabel = c.DefineLabel(); + + // Emit the last instruction to branch over the roar exit call + c.Emit(OpCodes.Brfalse, afterRoarExitLabel); + + // Goto after the roar exit FSM call + c.GotoNext(MoveType.After, roarExitInstructions); + + // Mark the label so we branch here + c.MarkLabel(afterRoarExitLabel); + } catch (Exception e) { + Logger.Error($"Could not change BridgeLever#OnOpenBridge IL: \n{e}"); + } + } } From 0ac08b5a36735075dc8eac00ac8ac96efb8ffe6d Mon Sep 17 00:00:00 2001 From: Extremelyd1 <10898310+Extremelyd1@users.noreply.github.com> Date: Thu, 25 Jul 2024 13:02:37 +0200 Subject: [PATCH 122/216] Refactor game patches to separate class --- HKMP/Animation/AnimationManager.cs | 41 ---- HKMP/Game/Client/ClientManager.cs | 1 + HKMP/Game/Client/Entity/EntityManager.cs | 130 ------------ HKMP/Game/Client/GamePatcher.cs | 258 +++++++++++++++++++++++ HKMP/Game/Client/PlayerManager.cs | 55 ----- 5 files changed, 259 insertions(+), 226 deletions(-) create mode 100644 HKMP/Game/Client/GamePatcher.cs diff --git a/HKMP/Animation/AnimationManager.cs b/HKMP/Animation/AnimationManager.cs index b877f9a0..931b3cd7 100644 --- a/HKMP/Animation/AnimationManager.cs +++ b/HKMP/Animation/AnimationManager.cs @@ -455,9 +455,6 @@ ServerSettings serverSettings // Register when the player dies to send the animation ModHooks.BeforePlayerDeadHook += OnDeath; - - // Register IL hook for changing the behaviour of tink effects - IL.TinkEffect.OnTriggerEnter2D += TinkEffectOnTriggerEnter2D; // Set the server settings for all animation effects foreach (var effect in AnimationEffects.Values) { @@ -1134,42 +1131,4 @@ public static AnimationClip GetCurrentAnimationClip() { return 0; } - - /// - /// IL hook to change the TinkEffect OnTriggerEnter2D to not trigger on remote players. - /// This method will insert IL to check whether the player responsible for the attack is the local player. - /// - private void TinkEffectOnTriggerEnter2D(ILContext il) { - try { - // Create a cursor for this context - var c = new ILCursor(il); - - // Find the first return instruction in the method to branch to later - var retInstr = il.Instrs.First(i => i.MatchRet()); - - // Load the 'collision' argument onto the stack - c.Emit(OpCodes.Ldarg_1); - - // Emit a delegate that pops the TinkEffect from the stack, checks whether the parent - // of the effect is the knight and pushes a bool on the stack based on this - c.EmitDelegate>(collider => { - var parent = collider.transform.parent; - if (parent == null) { - return true; - } - - parent = parent.parent; - if (parent == null) { - return true; - } - - return parent.gameObject.name != "Knight"; - }); - - // Based on the bool we pushed to the stack earlier, we conditionally branch to the return instruction - c.Emit(OpCodes.Brtrue, retInstr); - } catch (Exception e) { - Logger.Error($"Could not change TinkEffect#OnTriggerEnter2D IL:\n{e}"); - } - } } diff --git a/HKMP/Game/Client/ClientManager.cs b/HKMP/Game/Client/ClientManager.cs index b1200907..48dbc109 100644 --- a/HKMP/Game/Client/ClientManager.cs +++ b/HKMP/Game/Client/ClientManager.cs @@ -190,6 +190,7 @@ ModSettings modSettings _saveManager.Initialize(); new PauseManager(netClient).RegisterHooks(); + new GamePatcher().RegisterHooks(); new FsmPatcher().RegisterHooks(); _commandManager = new ClientCommandManager(); diff --git a/HKMP/Game/Client/Entity/EntityManager.cs b/HKMP/Game/Client/Entity/EntityManager.cs index f16a5c8e..b5ab3b7c 100644 --- a/HKMP/Game/Client/Entity/EntityManager.cs +++ b/HKMP/Game/Client/Entity/EntityManager.cs @@ -65,17 +65,6 @@ public EntityManager(NetClient netClient) { UnityEngine.SceneManagement.SceneManager.activeSceneChanged += OnSceneChanged; FindGameObject.Find += OnFindGameObject; - - On.BridgeLever.OnTriggerEnter2D += BridgeLeverOnTriggerEnter2D; - - var type = typeof(BridgeLever).GetNestedType("d__13", - BindingFlags.NonPublic | BindingFlags.Instance); - - // TODO: store this hook and unregister if entity system is not used - new ILHook( - type.GetMethod("MoveNext", BindingFlags.NonPublic | BindingFlags.Instance), - BridgeLeverOnOpenBridge - ); } /// @@ -512,123 +501,4 @@ private void OnFindGameObject(FindGameObject.orig_Find orig, HutongGames.PlayMak Logger.Debug(" Name did not match any entity"); } - - /// - /// Whether the local player hit the bridge lever. - /// - private bool _localPlayerBridgeLever; - - /// - /// On Hook that stores a boolean depending on whether the local player hit the bridge lever or not. Used in the - /// IL Hook below. - /// - private void BridgeLeverOnTriggerEnter2D(On.BridgeLever.orig_OnTriggerEnter2D orig, BridgeLever self, Collider2D collision) { - Logger.Debug("BridgeLeverOnTriggerEnter2D"); - - var activated = ReflectionHelper.GetField(self, "activated"); - - Logger.Debug($" activated: {activated}, collision tag: {collision.tag}"); - if (!activated && collision.tag == "Nail Attack") { - _localPlayerBridgeLever = collision.transform.parent?.parent?.tag == "Player"; - Logger.Debug($" Bridge lever hit: bool: {_localPlayerBridgeLever}"); - } - - orig(self, collision); - } - - /// - /// IL Hook to modify the OpenBridge method of BridgeLever to exclude locking players in place that did not hit - /// the lever. - /// - private void BridgeLeverOnOpenBridge(ILContext il) { - Logger.Debug("BridgeLeverOnOpenBridge IL"); - try { - // Create a cursor for this context - var c = new ILCursor(il); - - // Define the collection of instructions that matches the FreezeMoment call - Func[] freezeMomentInstructions = [ - i => i.MatchCall(typeof(global::GameManager), "get_instance"), - i => i.MatchLdcI4(1), - i => i.MatchCallvirt(typeof(global::GameManager), "FreezeMoment") - ]; - - // Goto after the FreezeMoment call - c.GotoNext(MoveType.Before, freezeMomentInstructions); - - // Emit a delegate that puts the boolean on the stack - c.EmitDelegate(() => _localPlayerBridgeLever); - - // Define the label to branch to - var afterFreezeLabel = c.DefineLabel(); - - // Then emit an instruction that branches to after the freeze if the boolean is false - c.Emit(OpCodes.Brfalse, afterFreezeLabel); - - // Goto after the FreezeMoment call - c.GotoNext(MoveType.After, freezeMomentInstructions); - - // Mark the label after the FreezeMoment call so we branch here - c.MarkLabel(afterFreezeLabel); - - // Goto after the rumble call - c.GotoNext( - MoveType.After, - i => i.MatchCall(typeof(GameCameras), "get_instance"), - i => i.MatchLdfld(typeof(GameCameras), "cameraShakeFSM"), - i => i.MatchLdstr("RumblingMed"), - i => i.MatchLdcI4(1), - i => i.MatchCall(typeof(FSMUtility), "SetBool") - ); - - // Emit a delegate that puts the boolean on the stack - c.EmitDelegate(() => _localPlayerBridgeLever); - - // Define the label to branch to - var afterRoarEnterLabel = c.DefineLabel(); - - // Emit another instruction that branches over the roar enter FSM calls - c.Emit(OpCodes.Brfalse, afterRoarEnterLabel); - - // Goto after the roar enter call - c.GotoNext( - MoveType.After, - i => i.MatchLdstr("ROAR ENTER"), - i => i.MatchLdcI4(0), - i => i.MatchCall(typeof(FSMUtility), "SendEventToGameObject") - ); - - // Mark the label after the Roar Enter call so we branch here - c.MarkLabel(afterRoarEnterLabel); - - // Define the collection of instructions that matches the roar exit FSM call - Func[] roarExitInstructions = [ - i => i.MatchCall(typeof(HeroController), "get_instance"), - i => i.MatchCallvirt(typeof(UnityEngine.Component), "get_gameObject"), - i => i.MatchLdstr("ROAR EXIT"), - i => i.MatchLdcI4(0), - i => i.MatchCall(typeof(FSMUtility), "SendEventToGameObject") - ]; - - // Goto before the roar exit FSM call - c.GotoNext(MoveType.Before, roarExitInstructions); - - // Emit a delegate that puts the boolean on the stack - c.EmitDelegate(() => _localPlayerBridgeLever); - - // Define the label to branch to - var afterRoarExitLabel = c.DefineLabel(); - - // Emit the last instruction to branch over the roar exit call - c.Emit(OpCodes.Brfalse, afterRoarExitLabel); - - // Goto after the roar exit FSM call - c.GotoNext(MoveType.After, roarExitInstructions); - - // Mark the label so we branch here - c.MarkLabel(afterRoarExitLabel); - } catch (Exception e) { - Logger.Error($"Could not change BridgeLever#OnOpenBridge IL: \n{e}"); - } - } } diff --git a/HKMP/Game/Client/GamePatcher.cs b/HKMP/Game/Client/GamePatcher.cs new file mode 100644 index 00000000..d4b4d84f --- /dev/null +++ b/HKMP/Game/Client/GamePatcher.cs @@ -0,0 +1,258 @@ +using System; +using System.Linq; +using System.Reflection; +using Modding; +using Mono.Cecil.Cil; +using MonoMod.Cil; +using MonoMod.RuntimeDetour; +using UnityEngine; +using Logger = Hkmp.Logging.Logger; + +namespace Hkmp.Game.Client; + +/// +/// Class that manager patches such as IL and On hooks that are standalone patches for the multiplayer to function +/// correctly. +/// +internal class GamePatcher { + /// + /// The binding flags for obtaining certain types for hooking. + /// + private const BindingFlags BindingFlags = System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance; + + /// + /// The IL Hook for the bridge lever method. + /// + private ILHook _bridgeLeverIlHook; + + /// + /// Register the hooks. + /// + public void RegisterHooks() { + // Register IL hook for changing the behaviour of tink effects + IL.TinkEffect.OnTriggerEnter2D += TinkEffectOnTriggerEnter2D; + + IL.HealthManager.TakeDamage += HealthManagerOnTakeDamage; + + On.BridgeLever.OnTriggerEnter2D += BridgeLeverOnTriggerEnter2D; + + var type = typeof(BridgeLever).GetNestedType("d__13", BindingFlags); + _bridgeLeverIlHook = new ILHook(type.GetMethod("MoveNext", BindingFlags), BridgeLeverOnOpenBridge); + } + + /// + /// De-register the hooks. + /// + public void DeregisterHooks() { + IL.HealthManager.TakeDamage -= HealthManagerOnTakeDamage; + + On.BridgeLever.OnTriggerEnter2D -= BridgeLeverOnTriggerEnter2D; + + _bridgeLeverIlHook?.Dispose(); + } + + /// + /// IL hook to change the TinkEffect OnTriggerEnter2D to not trigger on remote players. + /// This method will insert IL to check whether the player responsible for the attack is the local player. + /// + private void TinkEffectOnTriggerEnter2D(ILContext il) { + try { + // Create a cursor for this context + var c = new ILCursor(il); + + // Find the first return instruction in the method to branch to later + var retInstr = il.Instrs.First(i => i.MatchRet()); + + // Load the 'collision' argument onto the stack + c.Emit(OpCodes.Ldarg_1); + + // Emit a delegate that pops the TinkEffect from the stack, checks whether the parent + // of the effect is the knight and pushes a bool on the stack based on this + c.EmitDelegate>(collider => { + var parent = collider.transform.parent; + if (parent == null) { + return true; + } + + parent = parent.parent; + if (parent == null) { + return true; + } + + return parent.gameObject.name != "Knight"; + }); + + // Based on the bool we pushed to the stack earlier, we conditionally branch to the return instruction + c.Emit(OpCodes.Brtrue, retInstr); + } catch (Exception e) { + Logger.Error($"Could not change TinkEffect#OnTriggerEnter2D IL:\n{e}"); + } + } + + /// + /// IL Hook to modify the behaviour of the TakeDamage method in HealthManager. This modification adds a + /// conditional branch in case the nail swing from the HitInstance was from a remote player to ensure that + /// soul is not gained for remote hits. + /// + private void HealthManagerOnTakeDamage(ILContext il) { + try { + // Create a cursor for this context + var c = new ILCursor(il); + + // Goto the next virtual call to HeroController.SoulGain() + c.GotoNext(i => i.MatchCallvirt(typeof(HeroController), "SoulGain")); + + // Move the cursor to before the call and call virtual instructions + c.Index -= 1; + + // Emit the instruction to load the first parameter (hitInstance) onto the stack + c.Emit(OpCodes.Ldarg_1); + + // Emit a delegate that takes the hitInstance parameter from the stack and pushes a boolean on the stack + // that indicates whether the hitInstance was from a remote player's nail swing + c.EmitDelegate>(hitInstance => { + if (hitInstance.Source == null || hitInstance.Source.transform == null) { + return false; + } + + // Find the top-level parent of the hit instance + var transform = hitInstance.Source.transform; + while (transform.parent != null) { + transform = transform.parent; + } + + var go = transform.gameObject; + + return go.tag != "Player"; + }); + + // Define a label for the branch instruction + var afterLabel = c.DefineLabel(); + + // Emit the branch (on true) instruction with the label + c.Emit(OpCodes.Brtrue, afterLabel); + + // Move the cursor after the SoulGain method call + c.Index += 2; + + // Mark the label here, so we branch after the SoulGain method call on true + c.MarkLabel(afterLabel); + } catch (Exception e) { + Logger.Error($"Could not change HealthManager#TakeDamage IL:\n{e}"); + } + } + + /// + /// Whether the local player hit the bridge lever. + /// + private bool _localPlayerBridgeLever; + + /// + /// On Hook that stores a boolean depending on whether the local player hit the bridge lever or not. Used in the + /// IL Hook below. + /// + private void BridgeLeverOnTriggerEnter2D(On.BridgeLever.orig_OnTriggerEnter2D orig, BridgeLever self, Collider2D collision) { + var activated = ReflectionHelper.GetField(self, "activated"); + + if (!activated && collision.tag == "Nail Attack") { + _localPlayerBridgeLever = collision.transform.parent?.parent?.tag == "Player"; + } + + orig(self, collision); + } + + /// + /// IL Hook to modify the OpenBridge method of BridgeLever to exclude locking players in place that did not hit + /// the lever. + /// + private void BridgeLeverOnOpenBridge(ILContext il) { + try { + // Create a cursor for this context + var c = new ILCursor(il); + + // Define the collection of instructions that matches the FreezeMoment call + Func[] freezeMomentInstructions = [ + i => i.MatchCall(typeof(global::GameManager), "get_instance"), + i => i.MatchLdcI4(1), + i => i.MatchCallvirt(typeof(global::GameManager), "FreezeMoment") + ]; + + // Goto after the FreezeMoment call + c.GotoNext(MoveType.Before, freezeMomentInstructions); + + // Emit a delegate that puts the boolean on the stack + c.EmitDelegate(() => _localPlayerBridgeLever); + + // Define the label to branch to + var afterFreezeLabel = c.DefineLabel(); + + // Then emit an instruction that branches to after the freeze if the boolean is false + c.Emit(OpCodes.Brfalse, afterFreezeLabel); + + // Goto after the FreezeMoment call + c.GotoNext(MoveType.After, freezeMomentInstructions); + + // Mark the label after the FreezeMoment call so we branch here + c.MarkLabel(afterFreezeLabel); + + // Goto after the rumble call + c.GotoNext( + MoveType.After, + i => i.MatchCall(typeof(GameCameras), "get_instance"), + i => i.MatchLdfld(typeof(GameCameras), "cameraShakeFSM"), + i => i.MatchLdstr("RumblingMed"), + i => i.MatchLdcI4(1), + i => i.MatchCall(typeof(FSMUtility), "SetBool") + ); + + // Emit a delegate that puts the boolean on the stack + c.EmitDelegate(() => _localPlayerBridgeLever); + + // Define the label to branch to + var afterRoarEnterLabel = c.DefineLabel(); + + // Emit another instruction that branches over the roar enter FSM calls + c.Emit(OpCodes.Brfalse, afterRoarEnterLabel); + + // Goto after the roar enter call + c.GotoNext( + MoveType.After, + i => i.MatchLdstr("ROAR ENTER"), + i => i.MatchLdcI4(0), + i => i.MatchCall(typeof(FSMUtility), "SendEventToGameObject") + ); + + // Mark the label after the Roar Enter call so we branch here + c.MarkLabel(afterRoarEnterLabel); + + // Define the collection of instructions that matches the roar exit FSM call + Func[] roarExitInstructions = [ + i => i.MatchCall(typeof(HeroController), "get_instance"), + i => i.MatchCallvirt(typeof(UnityEngine.Component), "get_gameObject"), + i => i.MatchLdstr("ROAR EXIT"), + i => i.MatchLdcI4(0), + i => i.MatchCall(typeof(FSMUtility), "SendEventToGameObject") + ]; + + // Goto before the roar exit FSM call + c.GotoNext(MoveType.Before, roarExitInstructions); + + // Emit a delegate that puts the boolean on the stack + c.EmitDelegate(() => _localPlayerBridgeLever); + + // Define the label to branch to + var afterRoarExitLabel = c.DefineLabel(); + + // Emit the last instruction to branch over the roar exit call + c.Emit(OpCodes.Brfalse, afterRoarExitLabel); + + // Goto after the roar exit FSM call + c.GotoNext(MoveType.After, roarExitInstructions); + + // Mark the label so we branch here + c.MarkLabel(afterRoarExitLabel); + } catch (Exception e) { + Logger.Error($"Could not change BridgeLever#OnOpenBridge IL: \n{e}"); + } + } +} diff --git a/HKMP/Game/Client/PlayerManager.cs b/HKMP/Game/Client/PlayerManager.cs index 165d334e..ff2ec592 100644 --- a/HKMP/Game/Client/PlayerManager.cs +++ b/HKMP/Game/Client/PlayerManager.cs @@ -109,8 +109,6 @@ Dictionary playerData OnPlayerTeamUpdate); packetManager.RegisterClientPacketHandler(ClientPacketId.PlayerSkinUpdate, OnPlayerSkinUpdate); - - IL.HealthManager.TakeDamage += HealthManagerOnTakeDamage; } /// @@ -784,57 +782,4 @@ private void ToggleBodyDamage(ClientPlayerData playerData, bool enabled) { playerObject.GetComponent().enabled = false; } } - - /// - /// IL Hook to modify the behaviour of the TakeDamage method in HealthManager. This modification adds a - /// conditional branch in case the nail swing from the HitInstance was from a remote player to ensure that - /// soul is not gained for remote hits. - /// - private void HealthManagerOnTakeDamage(ILContext il) { - try { - // Create a cursor for this context - var c = new ILCursor(il); - - // Goto the next virtual call to HeroController.SoulGain() - c.GotoNext(i => i.MatchCallvirt(typeof(HeroController), "SoulGain")); - - // Move the cursor to before the call and call virtual instructions - c.Index -= 1; - - // Emit the instruction to load the first parameter (hitInstance) onto the stack - c.Emit(OpCodes.Ldarg_1); - - // Emit a delegate that takes the hitInstance parameter from the stack and pushes a boolean on the stack - // that indicates whether the hitInstance was from a remote player's nail swing - c.EmitDelegate>(hitInstance => { - if (hitInstance.Source == null || hitInstance.Source.transform == null) { - return false; - } - - // Find the top-level parent of the hit instance - var transform = hitInstance.Source.transform; - while (transform.parent != null) { - transform = transform.parent; - } - - var go = transform.gameObject; - - return go.tag != "Player"; - }); - - // Define a label for the branch instruction - var afterLabel = c.DefineLabel(); - - // Emit the branch (on true) instruction with the label - c.Emit(OpCodes.Brtrue, afterLabel); - - // Move the cursor after the SoulGain method call - c.Index += 2; - - // Mark the label here, so we branch after the SoulGain method call on true - c.MarkLabel(afterLabel); - } catch (Exception e) { - Logger.Error($"Could not change HealthManager#TakeDamage IL:\n{e}"); - } - } } From 6ed94bbe5416e91713fe306bd2c880a60801cfd5 Mon Sep 17 00:00:00 2001 From: Extremelyd1 <10898310+Extremelyd1@users.noreply.github.com> Date: Thu, 25 Jul 2024 13:23:02 +0200 Subject: [PATCH 123/216] Fix City entrance door opening not syncing --- HKMP/Game/Client/Save/SaveChanges.cs | 7 +++++++ HKMP/Resource/save-data.json | 3 ++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/HKMP/Game/Client/Save/SaveChanges.cs b/HKMP/Game/Client/Save/SaveChanges.cs index 4cd988e6..1f209639 100644 --- a/HKMP/Game/Client/Save/SaveChanges.cs +++ b/HKMP/Game/Client/Save/SaveChanges.cs @@ -188,6 +188,13 @@ public void ApplyPlayerDataSaveChange(string name) { var fsm = go.LocateMyFSM("FSM"); fsm.SetState("Destroy"); } + + if (name == "openedCityGate" && currentScene == "Fungus2_21") { + var go = GameObject.Find("City Gate Control"); + var fsm = go.LocateMyFSM("Conversation Control"); + + fsm.SetState("Activate"); + } } /// diff --git a/HKMP/Resource/save-data.json b/HKMP/Resource/save-data.json index 3f280bc5..67d16d3a 100644 --- a/HKMP/Resource/save-data.json +++ b/HKMP/Resource/save-data.json @@ -4923,7 +4923,8 @@ }, "openedCityGate": { "Sync": true, - "SyncType": "Server" + "SyncType": "Server", + "IgnoreSceneHost": true }, "cityGateClosed": { "Sync": true, From 1844e06bfde27dc3fdec28908f5da51b64c1eded Mon Sep 17 00:00:00 2001 From: Extremelyd1 <10898310+Extremelyd1@users.noreply.github.com> Date: Fri, 26 Jul 2024 10:15:16 +0200 Subject: [PATCH 124/216] Fix synchronisation of City elevators --- .../Client/Entity/Action/EntityFsmActions.cs | 64 +++++++++++++++++++ HKMP/Game/Client/Entity/EntityManager.cs | 5 -- HKMP/Game/Client/Entity/EntityType.cs | 1 + HKMP/Resource/entity-registry.json | 5 ++ 4 files changed, 70 insertions(+), 5 deletions(-) diff --git a/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs b/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs index 47ae59ef..34404774 100644 --- a/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs +++ b/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs @@ -3063,6 +3063,70 @@ private static void ApplyNetworkDataFromAction(EntityNetworkData data, PreSpawnG } } + #endregion + + #region SetSpriteRenderer + + private static bool GetNetworkDataFromAction(EntityNetworkData data, SetSpriteRenderer action) { + return action.gameObject != null; + } + + private static void ApplyNetworkDataFromAction(EntityNetworkData data, SetSpriteRenderer action) { + var gameObject = action.Fsm.GetOwnerDefaultTarget(action.gameObject); + if (gameObject == null) { + return; + } + + var spriteRenderer = gameObject.GetComponent(); + if (spriteRenderer != null) { + spriteRenderer.enabled = action.active.Value; + } + } + + #endregion + + #region MoveLiftChain + + private static bool GetNetworkDataFromAction(EntityNetworkData data, MoveLiftChain action) { + return true; + } + + private static void ApplyNetworkDataFromAction(EntityNetworkData data, MoveLiftChain action) { + var go = action.target.GetSafe(action); + if (go == null) { + return; + } + + var liftChain = go.GetComponent(); + if (liftChain == null) { + return; + } + + ReflectionHelper.CallMethod(action, "Apply", [liftChain]); + } + + #endregion + + #region StopLiftChain + + private static bool GetNetworkDataFromAction(EntityNetworkData data, StopLiftChain action) { + return true; + } + + private static void ApplyNetworkDataFromAction(EntityNetworkData data, StopLiftChain action) { + var go = action.target.GetSafe(action); + if (go == null) { + return; + } + + var liftChain = go.GetComponent(); + if (liftChain == null) { + return; + } + + ReflectionHelper.CallMethod(action, "Apply", [liftChain]); + } + #endregion /// diff --git a/HKMP/Game/Client/Entity/EntityManager.cs b/HKMP/Game/Client/Entity/EntityManager.cs index b5ab3b7c..238d4870 100644 --- a/HKMP/Game/Client/Entity/EntityManager.cs +++ b/HKMP/Game/Client/Entity/EntityManager.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Reflection; using Hkmp.Game.Client.Entity.Action; using Hkmp.Game.Client.Entity.Component; using Hkmp.Networking.Client; @@ -9,15 +8,11 @@ using Hkmp.Util; using HutongGames.PlayMaker.Actions; using Modding; -using Mono.Cecil.Cil; -using MonoMod.Cil; -using MonoMod.RuntimeDetour; using UnityEngine; using UnityEngine.SceneManagement; using FindGameObject = On.HutongGames.PlayMaker.Actions.FindGameObject; using Logger = Hkmp.Logging.Logger; using Object = UnityEngine.Object; -using OpCodes = Mono.Cecil.Cil.OpCodes; namespace Hkmp.Game.Client.Entity; diff --git a/HKMP/Game/Client/Entity/EntityType.cs b/HKMP/Game/Client/Entity/EntityType.cs index ccdf9333..2c500489 100644 --- a/HKMP/Game/Client/Entity/EntityType.cs +++ b/HKMP/Game/Client/Entity/EntityType.cs @@ -6,6 +6,7 @@ namespace Hkmp.Game.Client.Entity; internal enum EntityType { BattleGate = 0, CameraLockArea, + CityElevator, Crawlid, Tiktik, Vengefly, diff --git a/HKMP/Resource/entity-registry.json b/HKMP/Resource/entity-registry.json index c836df6c..89ad68d1 100644 --- a/HKMP/Resource/entity-registry.json +++ b/HKMP/Resource/entity-registry.json @@ -13,6 +13,11 @@ "base_object_name": "CameraLockArea", "type": "CameraLockArea" }, + { + "base_object_name": "Ruins Lift", + "type": "CityElevator", + "fsm_name": "Lift Control" + }, { "base_object_name": "Crawler", "type": "Crawlid", From 49086ef7946e4259418268172256b5ae0cbbb418 Mon Sep 17 00:00:00 2001 From: Extremelyd1 <10898310+Extremelyd1@users.noreply.github.com> Date: Fri, 26 Jul 2024 12:37:11 +0200 Subject: [PATCH 125/216] Fix Crystal Peak platforms not syncing --- .../Entity/Component/ComponentFactory.cs | 2 + .../Entity/Component/EntityComponent.cs | 3 +- .../Entity/Component/FlipPlatformComponent.cs | 123 ++++++++++++++++++ HKMP/Game/Client/Entity/EntityManager.cs | 2 + HKMP/Game/Client/Entity/EntityType.cs | 1 + HKMP/Resource/entity-registry.json | 7 + 6 files changed, 137 insertions(+), 1 deletion(-) create mode 100644 HKMP/Game/Client/Entity/Component/FlipPlatformComponent.cs diff --git a/HKMP/Game/Client/Entity/Component/ComponentFactory.cs b/HKMP/Game/Client/Entity/Component/ComponentFactory.cs index 75e20e1b..d08d9ae2 100644 --- a/HKMP/Game/Client/Entity/Component/ComponentFactory.cs +++ b/HKMP/Game/Client/Entity/Component/ComponentFactory.cs @@ -71,6 +71,8 @@ HostClientPair objects } return null; + case EntityComponentType.FlipPlatform: + return new FlipPlatformComponent(netClient, entityId, objects); default: throw new ArgumentOutOfRangeException(nameof(type), type, $"Could not instantiate entity component for type: {type}"); } diff --git a/HKMP/Game/Client/Entity/Component/EntityComponent.cs b/HKMP/Game/Client/Entity/Component/EntityComponent.cs index f20a8c8e..c8d69c7b 100644 --- a/HKMP/Game/Client/Entity/Component/EntityComponent.cs +++ b/HKMP/Game/Client/Entity/Component/EntityComponent.cs @@ -86,5 +86,6 @@ internal enum EntityComponentType : ushort { SpawnJar, SpriteRenderer, ChallengePrompt, - Music + Music, + FlipPlatform } diff --git a/HKMP/Game/Client/Entity/Component/FlipPlatformComponent.cs b/HKMP/Game/Client/Entity/Component/FlipPlatformComponent.cs new file mode 100644 index 00000000..6a18c5e3 --- /dev/null +++ b/HKMP/Game/Client/Entity/Component/FlipPlatformComponent.cs @@ -0,0 +1,123 @@ +using System.Collections; +using Hkmp.Networking.Client; +using Hkmp.Networking.Packet.Data; +using Hkmp.Util; +using Modding; +using UnityEngine; +using Logger = Hkmp.Logging.Logger; + +namespace Hkmp.Game.Client.Entity.Component; + +/// +/// This component manages the flipping of platforms in Crystal Peak. +internal class FlipPlatformComponent : EntityComponent { + /// + /// Host-client pair of the FlipPlatform behaviours. + /// + private readonly HostClientPair _platform; + + /// + /// The last boolean value of the 'hitCancel' boolean in the behaviour. + /// + private bool _lastHitCancel; + + public FlipPlatformComponent( + NetClient netClient, + ushort entityId, + HostClientPair gameObject + ) : base(netClient, entityId, gameObject) { + _platform = new HostClientPair { + Client = gameObject.Client.GetComponent(), + Host = gameObject.Host.GetComponent() + }; + + On.FlipPlatform.Flip += FlipPlatformOnFlip; + MonoBehaviourUtil.Instance.OnUpdateEvent += OnUpdate; + } + + /// + /// Hook method that fires when the Flip method is called on the behaviour. Will network that the platform + /// should be flipped regardless of scene host. + /// + private IEnumerator FlipPlatformOnFlip(On.FlipPlatform.orig_Flip orig, FlipPlatform self) { + if (self != _platform.Client && self != _platform.Host) { + yield return orig(self); + yield break; + } + + var data = new EntityNetworkData { + Type = EntityComponentType.FlipPlatform + }; + + data.Packet.Write((byte) 0); + + SendData(data); + + yield return orig(self); + } + + /// + /// Update method that checks the value of the 'hitCancel' boolean and conditionally networks it indicating that + /// the platform should be flipped back. + /// + private void OnUpdate() { + var platform = IsControlled ? _platform.Client : _platform.Host; + + var hitCancel = ReflectionHelper.GetField(platform, "hitCancel"); + if (hitCancel == _lastHitCancel) { + return; + } + + _lastHitCancel = hitCancel; + + if (!hitCancel) { + return; + } + + var data = new EntityNetworkData { + Type = EntityComponentType.FlipPlatform + }; + + data.Packet.Write((byte) 1); + + SendData(data); + } + + /// + public override void InitializeHost() { + } + + /// + public override void Update(EntityNetworkData data) { + var platform = IsControlled ? _platform.Client : _platform.Host; + + var type = data.Packet.ReadByte(); + + if (type == 0) { + var idleRoutine = ReflectionHelper.GetField(platform, "idleRoutine"); + var flipRoutine = ReflectionHelper.GetField(platform, "flipRoutine"); + + if (idleRoutine != null) { + platform.StopCoroutine(idleRoutine); + } + + if (flipRoutine != null) { + return; + } + + var flipCall = ReflectionHelper.CallMethod(platform, "Flip"); + flipRoutine = platform.StartCoroutine(flipCall); + ReflectionHelper.SetField(platform, "flipRoutine", flipRoutine); + } else if (type == 1) { + ReflectionHelper.SetField(platform, "hitCancel", true); + } else { + Logger.Error("Received unknown type of data for FlipPlatform"); + } + } + + /// + public override void Destroy() { + On.FlipPlatform.Flip -= FlipPlatformOnFlip; + MonoBehaviourUtil.Instance.OnUpdateEvent -= OnUpdate; + } +} diff --git a/HKMP/Game/Client/Entity/EntityManager.cs b/HKMP/Game/Client/Entity/EntityManager.cs index 238d4870..e3fa5f03 100644 --- a/HKMP/Game/Client/Entity/EntityManager.cs +++ b/HKMP/Game/Client/Entity/EntityManager.cs @@ -447,6 +447,8 @@ private void FindEntitiesInScene(Scene scene, bool lateLoad) { .Concat(Object.FindObjectsOfType(true).Select(centipede => centipede.gameObject)) // Concatenate all GameObjects for CameraLockArea components .Concat(Object.FindObjectsOfType(true).Select(cameraLockArea => cameraLockArea.gameObject)) + // Concatenate all GameObjects for FlipPlatform components + .Concat(Object.FindObjectsOfType(true).Select(flipPlatform => flipPlatform.gameObject)) // Filter out GameObjects not in the current scene .Where(obj => obj.scene == scene) .Distinct(); diff --git a/HKMP/Game/Client/Entity/EntityType.cs b/HKMP/Game/Client/Entity/EntityType.cs index 2c500489..3904ff66 100644 --- a/HKMP/Game/Client/Entity/EntityType.cs +++ b/HKMP/Game/Client/Entity/EntityType.cs @@ -7,6 +7,7 @@ internal enum EntityType { BattleGate = 0, CameraLockArea, CityElevator, + CrystalPeakPlatform, Crawlid, Tiktik, Vengefly, diff --git a/HKMP/Resource/entity-registry.json b/HKMP/Resource/entity-registry.json index 89ad68d1..b1aa5e12 100644 --- a/HKMP/Resource/entity-registry.json +++ b/HKMP/Resource/entity-registry.json @@ -18,6 +18,13 @@ "type": "CityElevator", "fsm_name": "Lift Control" }, + { + "base_object_name": "Mines Platform", + "type": "CrystalPeakPlatform", + "components": [ + "FlipPlatform" + ] + }, { "base_object_name": "Crawler", "type": "Crawlid", From 256436246297e2adc8feef6d2180e695b4aca5c8 Mon Sep 17 00:00:00 2001 From: Extremelyd1 <10898310+Extremelyd1@users.noreply.github.com> Date: Fri, 26 Jul 2024 17:21:17 +0200 Subject: [PATCH 126/216] Improve nail detection and interference for remote players --- HKMP/Game/Client/GamePatcher.cs | 252 +++++++++++++++++++++++++++++--- 1 file changed, 235 insertions(+), 17 deletions(-) diff --git a/HKMP/Game/Client/GamePatcher.cs b/HKMP/Game/Client/GamePatcher.cs index d4b4d84f..d5928252 100644 --- a/HKMP/Game/Client/GamePatcher.cs +++ b/HKMP/Game/Client/GamePatcher.cs @@ -1,5 +1,4 @@ using System; -using System.Linq; using System.Reflection; using Modding; using Mono.Cecil.Cil; @@ -32,63 +31,243 @@ public void RegisterHooks() { // Register IL hook for changing the behaviour of tink effects IL.TinkEffect.OnTriggerEnter2D += TinkEffectOnTriggerEnter2D; + IL.HealthManager.Invincible += HealthManagerOnInvincible; + IL.HealthManager.TakeDamage += HealthManagerOnTakeDamage; On.BridgeLever.OnTriggerEnter2D += BridgeLeverOnTriggerEnter2D; var type = typeof(BridgeLever).GetNestedType("d__13", BindingFlags); _bridgeLeverIlHook = new ILHook(type.GetMethod("MoveNext", BindingFlags), BridgeLeverOnOpenBridge); + + On.HutongGames.PlayMaker.Actions.CallMethodProper.DoMethodCall += CallMethodProperOnDoMethodCall; } /// /// De-register the hooks. /// public void DeregisterHooks() { + IL.TinkEffect.OnTriggerEnter2D -= TinkEffectOnTriggerEnter2D; + + IL.HealthManager.Invincible -= HealthManagerOnInvincible; + IL.HealthManager.TakeDamage -= HealthManagerOnTakeDamage; On.BridgeLever.OnTriggerEnter2D -= BridgeLeverOnTriggerEnter2D; _bridgeLeverIlHook?.Dispose(); + + On.HutongGames.PlayMaker.Actions.CallMethodProper.DoMethodCall -= CallMethodProperOnDoMethodCall; } /// - /// IL hook to change the TinkEffect OnTriggerEnter2D to not trigger on remote players. - /// This method will insert IL to check whether the player responsible for the attack is the local player. + /// IL hook to change the TinkEffect OnTriggerEnter2D to not trigger certain effects of it on remote players. + /// This method will insert IL to check whether the player responsible for the attack is the local player and + /// based on this, omit certain effects. /// private void TinkEffectOnTriggerEnter2D(ILContext il) { try { // Create a cursor for this context var c = new ILCursor(il); - - // Find the first return instruction in the method to branch to later - var retInstr = il.Instrs.First(i => i.MatchRet()); // Load the 'collision' argument onto the stack c.Emit(OpCodes.Ldarg_1); + + // Keep track of a whether the local player is responsible for the hit + var isLocalPlayer = true; - // Emit a delegate that pops the TinkEffect from the stack, checks whether the parent - // of the effect is the knight and pushes a bool on the stack based on this - c.EmitDelegate>(collider => { + // Emit a delegate that pops the collision argument from the stack, checks whether the parent + // of the collider is the knight and changes the above bool based on this + c.EmitDelegate>(collider => { var parent = collider.transform.parent; if (parent == null) { - return true; + isLocalPlayer = true; + return; } - + parent = parent.parent; if (parent == null) { - return true; + isLocalPlayer = true; + return; } - - return parent.gameObject.name != "Knight"; + + isLocalPlayer = parent.gameObject.name == "Knight"; }); + + // Define a label to branch to after the camera shake call + var afterCameraShakeLabel = c.DefineLabel(); + + // Goto before the camera shake call, which is after the 'if' instructions + c.GotoNext( + MoveType.After, + i => i.MatchLdfld(typeof(TinkEffect), "gameCam"), + i => i.MatchCall(typeof(UnityEngine.Object), "op_Implicit"), + i => i.MatchBrfalse(out _) + ); + + // Emit the 'isLocalPlayer' bool to the stack + c.EmitDelegate(() => isLocalPlayer); + + // Emit an instruction that branches to after the camera shake based on the bool + c.Emit(OpCodes.Brfalse, afterCameraShakeLabel); + + // Goto after the camera shake call + c.GotoNext( + MoveType.After, + i => i.MatchLdfld(typeof(GameCameras), "cameraShakeFSM"), + i => i.MatchLdstr("EnemyKillShake"), + i => i.MatchCallvirt(typeof(PlayMakerFSM), "SendEvent") + ); + + // Mark the label for branching here + c.MarkLabel(afterCameraShakeLabel); - // Based on the bool we pushed to the stack earlier, we conditionally branch to the return instruction - c.Emit(OpCodes.Brtrue, retInstr); + // Goto after setting the 'position2' local variable + c.GotoNext( + MoveType.After, + i => i.MatchCallvirt(typeof(Component), "get_transform"), + i => i.MatchCallvirt(typeof(Transform), "get_position"), + i => i.MatchStloc(3) + ); + + // Define a label for branching + var afterRemotePositionLabel = c.DefineLabel(); + + // Emit the 'isLocalPlayer' bool to the stack + c.EmitDelegate(() => isLocalPlayer); + + // Emit the instruction for branching to behind the setting of the remote player position (below) + c.Emit(OpCodes.Brtrue, afterRemotePositionLabel); + + // Load the 'collision' argument onto the stack + c.Emit(OpCodes.Ldarg_1); + // Emit a delegate that pops the 'collision' argument from the stack and pushes the position of the + // remote player to the stack + c.EmitDelegate>(collider => collider.transform.parent.parent.position); + + // Emit the instruction for pushing the stack value into the 'position2' local variable + c.Emit(OpCodes.Stloc, 3); + + // Mark the label for branching after setting the remote position, that we skip when the local player + // is responsible for the hit + c.MarkLabel(afterRemotePositionLabel); + + // Loop 3 times for the 3 'Recoil' method calls to HeroController + for (var i = 0; i < 3; i++) { + // Goto before the 'Recoil' method call + c.GotoNext( + MoveType.Before, + inst => inst.MatchCall(typeof(HeroController), "get_instance") + ); + + // Define a label for branching + var afterRecoilLabel = c.DefineLabel(); + + // Emit the 'isLocalPlayer' bool to the stack + c.EmitDelegate(() => isLocalPlayer); + // Emit the instruction for branching after the 'Recoil' call + c.Emit(OpCodes.Brfalse, afterRecoilLabel); + + // Goto after the FSM 'SendEvent' call + c.GotoNext( + MoveType.After, + inst => inst.MatchCallvirt(typeof(PlayMakerFSM), "SendEvent") + ); + + // Mark the label for branching here + c.MarkLabel(afterRecoilLabel); + } + + // Goto after the last if statement that checks for the 'sendFSMEvent' variable + c.GotoNext( + MoveType.After, + i => i.MatchLdarg(0), + i => i.MatchLdfld(typeof(TinkEffect), "sendFSMEvent"), + i => i.MatchBrfalse(out _) + ); + + // Define a label to branch to + var afterSendEventLabel = c.DefineLabel(); + + // Emit the 'isLocalPlayer' bool to the stack + c.EmitDelegate(() => isLocalPlayer); + // Emit the instruction for branching after the 'SendEvent' call in case of a remote player hit + c.Emit(OpCodes.Brfalse, afterSendEventLabel); + + // Goto after the 'SendEvent' call to mark the label + c.GotoNext( + MoveType.After, + i => i.MatchLdarg(0), + i => i.MatchLdfld(typeof(TinkEffect), "FSMEvent"), + i => i.MatchCallvirt(typeof(PlayMakerFSM), "SendEvent") + ); + + // Mark the label to branch to here + c.MarkLabel(afterSendEventLabel); } catch (Exception e) { Logger.Error($"Could not change TinkEffect#OnTriggerEnter2D IL:\n{e}"); } } + private void HealthManagerOnInvincible(ILContext il) { + try { + // Create a cursor for this context + var c = new ILCursor(il); + + // Load the 'hitInstance' argument onto the stack + c.Emit(OpCodes.Ldarg_1); + + // Keep track of a whether the local player is responsible for the hit + var isLocalPlayer = true; + + // Emit a delegate that pops the collision argument from the stack, checks whether the parent + // of the collider is the knight and changes the above bool based on this + c.EmitDelegate>(hitInstance => { + if (hitInstance.Source == null) { + isLocalPlayer = true; + return; + } + + var parent = hitInstance.Source.transform.parent; + if (parent == null) { + isLocalPlayer = true; + return; + } + + parent = parent.parent; + if (parent == null) { + isLocalPlayer = true; + return; + } + + isLocalPlayer = parent.gameObject.name == "Knight"; + }); + + c.GotoNext( + MoveType.Before, + i => i.MatchLdarg(1), + i => i.MatchLdfld(typeof(HitInstance), "AttackType"), + i => i.MatchBrtrue(out _) + ); + + var afterRecoilFreezeShakeLabel = c.DefineLabel(); + + c.EmitDelegate(() => isLocalPlayer); + c.Emit(OpCodes.Brfalse, afterRecoilFreezeShakeLabel); + + c.GotoNext( + MoveType.After, + i => i.MatchLdfld(typeof(GameCameras), "cameraShakeFSM"), + i => i.MatchLdstr("EnemyKillShake"), + i => i.MatchCallvirt(typeof(PlayMakerFSM), "SendEvent") + ); + + c.MarkLabel(afterRecoilFreezeShakeLabel); + } catch (Exception e) { + Logger.Error($"Could not change HealthManager#OnInvincible IL:\n{e}"); + } + } + /// /// IL Hook to modify the behaviour of the TakeDamage method in HealthManager. This modification adds a /// conditional branch in case the nail swing from the HitInstance was from a remote player to ensure that @@ -228,7 +407,7 @@ private void BridgeLeverOnOpenBridge(ILContext il) { // Define the collection of instructions that matches the roar exit FSM call Func[] roarExitInstructions = [ i => i.MatchCall(typeof(HeroController), "get_instance"), - i => i.MatchCallvirt(typeof(UnityEngine.Component), "get_gameObject"), + i => i.MatchCallvirt(typeof(Component), "get_gameObject"), i => i.MatchLdstr("ROAR EXIT"), i => i.MatchLdcI4(0), i => i.MatchCall(typeof(FSMUtility), "SendEventToGameObject") @@ -255,4 +434,43 @@ private void BridgeLeverOnOpenBridge(ILContext il) { Logger.Error($"Could not change BridgeLever#OnOpenBridge IL: \n{e}"); } } + + /// + /// Hook for the 'DoMethodCall' method in the 'CallMethodProper' FSM action. This is used for the Crystal Shot + /// game object to ensure that knockback is not applied to the local player if a remote player hits the crystal. + /// + private void CallMethodProperOnDoMethodCall( + On.HutongGames.PlayMaker.Actions.CallMethodProper.orig_DoMethodCall orig, + HutongGames.PlayMaker.Actions.CallMethodProper self + ) { + // If the FSM and game object do not match the Crystal Shot, we execute the original method and return + if (!self.Fsm.Name.Equals("FSM") || !self.Fsm.GameObject.name.Contains("Crystal Shot")) { + orig(self); + return; + } + + // Find the damager game object from the FSM variables, if it, its parent, or their parent is null, we + // execute the original method and return, because we know that it was not a remote player's nail slash + var damager = self.Fsm.Variables.GetFsmGameObject("Damager").Value; + if (damager == null) { + orig(self); + return; + } + + var parent = damager.transform.parent; + if (parent == null) { + orig(self); + return; + } + + parent = parent.parent; + if (parent == null) { + orig(self); + return; + } + + if (parent.name.Equals("Knight")) { + orig(self); + } + } } From 4f68c06a75d9b0392dbc0202a8c928e5e0ce7959 Mon Sep 17 00:00:00 2001 From: Extremelyd1 <10898310+Extremelyd1@users.noreply.github.com> Date: Fri, 26 Jul 2024 17:32:59 +0200 Subject: [PATCH 127/216] Add Crystal Laser turrets to entity system --- HKMP/Game/Client/Entity/EntityType.cs | 1 + HKMP/Resource/entity-registry.json | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/HKMP/Game/Client/Entity/EntityType.cs b/HKMP/Game/Client/Entity/EntityType.cs index 3904ff66..1157a751 100644 --- a/HKMP/Game/Client/Entity/EntityType.cs +++ b/HKMP/Game/Client/Entity/EntityType.cs @@ -113,6 +113,7 @@ internal enum EntityType { Glimback, CrystalHunter, CrystalCrawler, + CrystalTurret, HuskMiner, CrystallisedHusk, CrystalGuardian, diff --git a/HKMP/Resource/entity-registry.json b/HKMP/Resource/entity-registry.json index b1aa5e12..0c52ef7f 100644 --- a/HKMP/Resource/entity-registry.json +++ b/HKMP/Resource/entity-registry.json @@ -708,6 +708,11 @@ "type": "CrystalCrawler", "fsm_name": "Laser Bug" }, + { + "base_object_name": "Laser Turret", + "type": "CrystalTurret", + "fsm_name": "Laser Bug" + }, { "base_object_name": "Mega Zombie Beam Miner", "type": "CrystalGuardian", From 5a62e1ed830707bd3c0d9b746d4b63f5b453967e Mon Sep 17 00:00:00 2001 From: Extremelyd1 <10898310+Extremelyd1@users.noreply.github.com> Date: Fri, 26 Jul 2024 19:22:54 +0200 Subject: [PATCH 128/216] Sync Waterways entrance and quake floors --- HKMP/Game/Client/Save/SaveChanges.cs | 108 ++++++++++++++++------ HKMP/Resource/save-data.json | 132 ++++++++++++++++++--------- 2 files changed, 169 insertions(+), 71 deletions(-) diff --git a/HKMP/Game/Client/Save/SaveChanges.cs b/HKMP/Game/Client/Save/SaveChanges.cs index 1f209639..68324236 100644 --- a/HKMP/Game/Client/Save/SaveChanges.cs +++ b/HKMP/Game/Client/Save/SaveChanges.cs @@ -111,7 +111,14 @@ public void ApplyPlayerDataSaveChange(string name) { name == "openedHiddenStation" && currentScene == "Abyss_22" ) { var go = GameObject.Find("Station Bell"); + if (go == null) { + return; + } + var fsm = go.LocateMyFSM("Stag Bell"); + if (fsm == null) { + return; + } fsm.SetState("Box Disappear Anim"); return; @@ -123,7 +130,14 @@ public void ApplyPlayerDataSaveChange(string name) { name == "tollBenchQueensGardens" && currentScene == "Fungus3_50" ) { var go = GameObject.Find("Toll Machine Bench"); + if (go == null) { + return; + } + var fsm = go.LocateMyFSM("Toll Machine Bench"); + if (fsm == null) { + return; + } fsm.SetState("Box Down"); return; @@ -136,6 +150,10 @@ public void ApplyPlayerDataSaveChange(string name) { } var fsm = go.LocateMyFSM("FSM"); + if (fsm == null) { + return; + } + fsm.SetState("Destroy"); return; } @@ -147,6 +165,10 @@ public void ApplyPlayerDataSaveChange(string name) { } var fsm = go.LocateMyFSM("Gate Control"); + if (fsm == null) { + return; + } + fsm.SetState("Destroy"); return; } @@ -158,12 +180,16 @@ public void ApplyPlayerDataSaveChange(string name) { } else if (currentScene == "Fungus2_20") { go = GameObject.Find("Breakable Wall Waterways"); } - + if (go == null) { return; } var fsm = go.LocateMyFSM("breakable_wall_v2"); + if (fsm == null) { + return; + } + fsm.SetState("Pause Frame"); return; } @@ -175,6 +201,10 @@ public void ApplyPlayerDataSaveChange(string name) { } var fsm = go.LocateMyFSM("FSM"); + if (fsm == null) { + return; + } + fsm.SetState("Destroy"); return; } @@ -186,13 +216,43 @@ public void ApplyPlayerDataSaveChange(string name) { } var fsm = go.LocateMyFSM("FSM"); + if (fsm == null) { + return; + } + fsm.SetState("Destroy"); + return; } if (name == "openedCityGate" && currentScene == "Fungus2_21") { var go = GameObject.Find("City Gate Control"); + if (go == null) { + return; + } + + var fsm = go.LocateMyFSM("Conversation Control"); + if (fsm == null) { + return; + } + + fsm.SetState("Activate"); + return; + } + + if (name == "openedWaterwaysManhole" && currentScene == "Ruins1_05b") { + var go = GameObject.Find("Waterways Machine"); + if (go == null) { + return; + } + var fsm = go.LocateMyFSM("Conversation Control"); + if (fsm == null) { + return; + } + fsm.RemoveFirstAction("Activate"); + fsm.RemoveFirstAction("Activate"); + fsm.SetState("Activate"); } } @@ -234,39 +294,33 @@ public void ApplyPersistentValueSaveChange(PersistentItemData itemData) { return; } - if (itemData.Id.StartsWith("Collapser Small") && ( - itemData.SceneName == "Crossroads_21" && currentScene == "Crossroads_21" || - itemData.SceneName == "Crossroads_36" && currentScene == "Crossroads_36" || - itemData.SceneName == "Fungus1_24" && currentScene == "Fungus1_24" || - itemData.SceneName == "Fungus2_23" && currentScene == "Fungus2_23" || - itemData.SceneName == "Fungus3_28" && currentScene == "Fungus3_28" || - itemData.SceneName == "Fungus2_25" && currentScene == "Fungus2_25" || - itemData.SceneName == "Mines_06" && currentScene == "Mines_06" || - itemData.SceneName == "Deepnest_02" && currentScene == "Deepnest_02" || - itemData.SceneName == "Deepnest_03" && currentScene == "Deepnest_03" || - itemData.SceneName == "Deepnest_14" && currentScene == "Deepnest_14" || - itemData.SceneName == "Deepnest_16" && currentScene == "Deepnest_16" || - itemData.SceneName == "Deepnest_30" && currentScene == "Deepnest_30" || - itemData.SceneName == "Deepnest_33" && currentScene == "Deepnest_33" || - itemData.SceneName == "Deepnest_38" && currentScene == "Deepnest_38" || - itemData.SceneName == "Deepnest_39" && currentScene == "Deepnest_39" || - itemData.SceneName == "Deepnest_41" && currentScene == "Deepnest_41" || - itemData.SceneName == "Deepnest_45_v02" && currentScene == "Deepnest_45_v02" || - itemData.SceneName == "RestingGrounds_10" && currentScene == "RestingGrounds_10" || - itemData.SceneName == "Deepnest_Spider_Town" && currentScene == "Deepnest_Spider_Town" || - itemData.SceneName == "Waterways_09" && currentScene == "Waterways_09" || - itemData.SceneName == "Waterways_14" && currentScene == "Waterways_14" || - itemData.SceneName == "GG_Pipeway" && currentScene == "GG_Pipeway" || - itemData.SceneName == "White_Palace_02" && currentScene == "White_Palace_02" || - itemData.SceneName == "White_Palace_17" && currentScene == "White_Palace_17" - )) { + if (itemData.Id.StartsWith("Collapser Small") && itemData.SceneName == currentScene) { var go = GameObject.Find(itemData.Id); if (go == null) { return; } var fsm = go.LocateMyFSM("collapse small"); + if (fsm == null) { + return; + } + fsm.SetState("Split"); + return; + } + + if (itemData.Id.StartsWith("Quake Floor") && itemData.SceneName == currentScene) { + var go = GameObject.Find(itemData.Id); + if (go == null) { + return; + } + + var fsm = go.LocateMyFSM("quake_floor"); + if (fsm == null) { + return; + } + + fsm.SetState("Audio"); } } } diff --git a/HKMP/Resource/save-data.json b/HKMP/Resource/save-data.json index 67d16d3a..9079fb96 100644 --- a/HKMP/Resource/save-data.json +++ b/HKMP/Resource/save-data.json @@ -4993,7 +4993,8 @@ }, "openedWaterwaysManhole": { "Sync": true, - "SyncType": "Server" + "SyncType": "Server", + "IgnoreSceneHost": true }, "waterwaysAcidDrained": { "Sync": true, @@ -9906,7 +9907,8 @@ }, "Value": { "Sync": true, - "SyncType": "Server" + "SyncType": "Server", + "IgnoreSceneHost": true } }, { @@ -11471,7 +11473,8 @@ }, "Value": { "Sync": true, - "SyncType": "Server" + "SyncType": "Server", + "IgnoreSceneHost": true } }, { @@ -11511,7 +11514,8 @@ }, "Value": { "Sync": true, - "SyncType": "Server" + "SyncType": "Server", + "IgnoreSceneHost": true } }, { @@ -11541,7 +11545,8 @@ }, "Value": { "Sync": true, - "SyncType": "Server" + "SyncType": "Server", + "IgnoreSceneHost": true } }, { @@ -11682,7 +11687,8 @@ }, "Value": { "Sync": true, - "SyncType": "Server" + "SyncType": "Server", + "IgnoreSceneHost": true } }, { @@ -11702,7 +11708,8 @@ }, "Value": { "Sync": true, - "SyncType": "Server" + "SyncType": "Server", + "IgnoreSceneHost": true } }, { @@ -11712,7 +11719,8 @@ }, "Value": { "Sync": true, - "SyncType": "Server" + "SyncType": "Server", + "IgnoreSceneHost": true } }, { @@ -11732,7 +11740,8 @@ }, "Value": { "Sync": true, - "SyncType": "Server" + "SyncType": "Server", + "IgnoreSceneHost": true } }, { @@ -11752,7 +11761,8 @@ }, "Value": { "Sync": true, - "SyncType": "Server" + "SyncType": "Server", + "IgnoreSceneHost": true } }, { @@ -11772,7 +11782,8 @@ }, "Value": { "Sync": true, - "SyncType": "Server" + "SyncType": "Server", + "IgnoreSceneHost": true } }, { @@ -11812,7 +11823,8 @@ }, "Value": { "Sync": true, - "SyncType": "Server" + "SyncType": "Server", + "IgnoreSceneHost": true } }, { @@ -12318,7 +12330,8 @@ }, "Value": { "Sync": true, - "SyncType": "Server" + "SyncType": "Server", + "IgnoreSceneHost": true } }, { @@ -12852,7 +12865,8 @@ }, "Value": { "Sync": true, - "SyncType": "Server" + "SyncType": "Server", + "IgnoreSceneHost": true } }, { @@ -13336,7 +13350,8 @@ }, "Value": { "Sync": true, - "SyncType": "Server" + "SyncType": "Server", + "IgnoreSceneHost": true } }, { @@ -13346,7 +13361,8 @@ }, "Value": { "Sync": true, - "SyncType": "Server" + "SyncType": "Server", + "IgnoreSceneHost": true } }, { @@ -13466,7 +13482,8 @@ }, "Value": { "Sync": true, - "SyncType": "Server" + "SyncType": "Server", + "IgnoreSceneHost": true } }, { @@ -13486,7 +13503,8 @@ }, "Value": { "Sync": true, - "SyncType": "Server" + "SyncType": "Server", + "IgnoreSceneHost": true } }, { @@ -13566,7 +13584,8 @@ }, "Value": { "Sync": true, - "SyncType": "Server" + "SyncType": "Server", + "IgnoreSceneHost": true } }, { @@ -15020,7 +15039,8 @@ }, "Value": { "Sync": true, - "SyncType": "Server" + "SyncType": "Server", + "IgnoreSceneHost": true } }, { @@ -15220,7 +15240,8 @@ }, "Value": { "Sync": true, - "SyncType": "Server" + "SyncType": "Server", + "IgnoreSceneHost": true } }, { @@ -18388,7 +18409,8 @@ }, "Value": { "Sync": true, - "SyncType": "Server" + "SyncType": "Server", + "IgnoreSceneHost": true } }, { @@ -20942,7 +20964,8 @@ }, "Value": { "Sync": true, - "SyncType": "Server" + "SyncType": "Server", + "IgnoreSceneHost": true } }, { @@ -20982,7 +21005,8 @@ }, "Value": { "Sync": true, - "SyncType": "Server" + "SyncType": "Server", + "IgnoreSceneHost": true } }, { @@ -21002,7 +21026,8 @@ }, "Value": { "Sync": true, - "SyncType": "Server" + "SyncType": "Server", + "IgnoreSceneHost": true } }, { @@ -21184,7 +21209,8 @@ }, "Value": { "Sync": true, - "SyncType": "Server" + "SyncType": "Server", + "IgnoreSceneHost": true } }, { @@ -21194,7 +21220,8 @@ }, "Value": { "Sync": true, - "SyncType": "Server" + "SyncType": "Server", + "IgnoreSceneHost": true } }, { @@ -23891,7 +23918,8 @@ }, "Value": { "Sync": true, - "SyncType": "Server" + "SyncType": "Server", + "IgnoreSceneHost": true } }, { @@ -27398,7 +27426,8 @@ }, "Value": { "Sync": true, - "SyncType": "Server" + "SyncType": "Server", + "IgnoreSceneHost": true } }, { @@ -27632,7 +27661,8 @@ }, "Value": { "Sync": true, - "SyncType": "Server" + "SyncType": "Server", + "IgnoreSceneHost": true } }, { @@ -27722,7 +27752,8 @@ }, "Value": { "Sync": true, - "SyncType": "Server" + "SyncType": "Server", + "IgnoreSceneHost": true } }, { @@ -28207,7 +28238,8 @@ }, "Value": { "Sync": true, - "SyncType": "Server" + "SyncType": "Server", + "IgnoreSceneHost": true } }, { @@ -28740,7 +28772,8 @@ }, "Value": { "Sync": true, - "SyncType": "Server" + "SyncType": "Server", + "IgnoreSceneHost": true } }, { @@ -28750,7 +28783,8 @@ }, "Value": { "Sync": true, - "SyncType": "Server" + "SyncType": "Server", + "IgnoreSceneHost": true } }, { @@ -28760,7 +28794,8 @@ }, "Value": { "Sync": true, - "SyncType": "Server" + "SyncType": "Server", + "IgnoreSceneHost": true } }, { @@ -28780,7 +28815,8 @@ }, "Value": { "Sync": true, - "SyncType": "Server" + "SyncType": "Server", + "IgnoreSceneHost": true } }, { @@ -28790,7 +28826,8 @@ }, "Value": { "Sync": true, - "SyncType": "Server" + "SyncType": "Server", + "IgnoreSceneHost": true } }, { @@ -28800,7 +28837,8 @@ }, "Value": { "Sync": true, - "SyncType": "Server" + "SyncType": "Server", + "IgnoreSceneHost": true } }, { @@ -28810,7 +28848,8 @@ }, "Value": { "Sync": true, - "SyncType": "Server" + "SyncType": "Server", + "IgnoreSceneHost": true } }, { @@ -28820,7 +28859,8 @@ }, "Value": { "Sync": true, - "SyncType": "Server" + "SyncType": "Server", + "IgnoreSceneHost": true } }, { @@ -28830,7 +28870,8 @@ }, "Value": { "Sync": true, - "SyncType": "Server" + "SyncType": "Server", + "IgnoreSceneHost": true } }, { @@ -28840,7 +28881,8 @@ }, "Value": { "Sync": true, - "SyncType": "Server" + "SyncType": "Server", + "IgnoreSceneHost": true } }, { @@ -28850,7 +28892,8 @@ }, "Value": { "Sync": true, - "SyncType": "Server" + "SyncType": "Server", + "IgnoreSceneHost": true } }, { @@ -29471,7 +29514,8 @@ }, "Value": { "Sync": true, - "SyncType": "Server" + "SyncType": "Server", + "IgnoreSceneHost": true } }, { From 0e7d956076a136a02cb5b01c0b747c77cf5feafb Mon Sep 17 00:00:00 2001 From: Extremelyd1 <10898310+Extremelyd1@users.noreply.github.com> Date: Sat, 27 Jul 2024 00:11:38 +0200 Subject: [PATCH 129/216] Partial refactor of save synchronisation --- HKMP/Game/Client/Save/SaveDataMapping.cs | 42 ++--- HKMP/Game/Client/Save/SaveManager.cs | 212 ++++++++++++----------- HKMP/Game/Server/ServerManager.cs | 33 +++- HKMP/Resource/save-data.json | 5 + 4 files changed, 160 insertions(+), 132 deletions(-) diff --git a/HKMP/Game/Client/Save/SaveDataMapping.cs b/HKMP/Game/Client/Save/SaveDataMapping.cs index cefa5fb0..96cad608 100644 --- a/HKMP/Game/Client/Save/SaveDataMapping.cs +++ b/HKMP/Game/Client/Save/SaveDataMapping.cs @@ -40,7 +40,7 @@ public static SaveDataMapping Instance { /// Dictionary mapping player data values to booleans indicating whether they should be synchronised. /// [JsonProperty("playerData")] - public Dictionary PlayerDataBools { get; private set; } + public Dictionary PlayerDataSyncProperties { get; private set; } /// /// Bi-directional lookup that maps save data names and their indices. @@ -60,13 +60,13 @@ public static SaveDataMapping Instance { /// Dictionary mapping geo rock data values to booleans indicating whether they should be synchronised. /// [JsonIgnore] - public Dictionary GeoRockDataBools { get; private set; } + public Dictionary GeoRockBools { get; private set; } /// /// Bi-directional lookup that maps geo rock names and their indices. /// [JsonIgnore] - public BiLookup GeoRockDataIndices { get; private set; } + public BiLookup GeoRockIndices { get; private set; } /// /// Deserialized key-value pairs for the persistent bool data in the JSON. @@ -80,13 +80,13 @@ public static SaveDataMapping Instance { /// Dictionary mapping persistent bool data values to booleans indicating whether they should be synchronised. /// [JsonIgnore] - public Dictionary PersistentBoolDataBools { get; private set; } + public Dictionary PersistentBoolSyncProperties { get; private set; } /// /// Bi-directional lookup that maps persistent bool names and their indices. /// [JsonIgnore] - public BiLookup PersistentBoolDataIndices { get; private set; } + public BiLookup PersistentBoolIndices { get; private set; } /// /// Deserialized key-value pairs for the persistent int data in the JSON. @@ -100,13 +100,13 @@ public static SaveDataMapping Instance { /// Dictionary mapping persistent int data values to booleans indicating whether they should be synchronised. /// [JsonIgnore] - public Dictionary PersistentIntDataBools { get; private set; } + public Dictionary PersistentIntSyncProperties { get; private set; } /// /// Bi-directional lookup that maps persistent int names and their indices. /// [JsonIgnore] - public BiLookup PersistentIntDataIndices { get; private set; } + public BiLookup PersistentIntIndices { get; private set; } /// /// Deserialized list of strings that represent variable names with the type of a string list. @@ -130,7 +130,7 @@ public static SaveDataMapping Instance { /// Initializes the class by converting the deserialized data fields into the various dictionaries and lookups. /// public void Initialize() { - if (PlayerDataBools == null) { + if (PlayerDataSyncProperties == null) { Logger.Warn("Player data bools for save data is null"); return; } @@ -152,26 +152,26 @@ public void Initialize() { PlayerDataIndices = new BiLookup(); ushort index = 0; - foreach (var playerDataBool in PlayerDataBools.Keys) { + foreach (var playerDataBool in PlayerDataSyncProperties.Keys) { PlayerDataIndices.Add(playerDataBool, index++); } - GeoRockDataBools = _geoRockDataValues.ToDictionary(kv => kv.Key, kv => kv.Value); - GeoRockDataIndices = new BiLookup(); - foreach (var geoRockData in GeoRockDataBools.Keys) { - GeoRockDataIndices.Add(geoRockData, index++); + GeoRockBools = _geoRockDataValues.ToDictionary(kv => kv.Key, kv => kv.Value); + GeoRockIndices = new BiLookup(); + foreach (var geoRockData in GeoRockBools.Keys) { + GeoRockIndices.Add(geoRockData, index++); } - PersistentBoolDataBools = _persistentBoolsDataValues.ToDictionary(kv => kv.Key, kv => kv.Value); - PersistentBoolDataIndices = new BiLookup(); - foreach (var persistentBoolData in PersistentBoolDataBools.Keys) { - PersistentBoolDataIndices.Add(persistentBoolData, index++); + PersistentBoolSyncProperties = _persistentBoolsDataValues.ToDictionary(kv => kv.Key, kv => kv.Value); + PersistentBoolIndices = new BiLookup(); + foreach (var persistentBoolData in PersistentBoolSyncProperties.Keys) { + PersistentBoolIndices.Add(persistentBoolData, index++); } - PersistentIntDataBools = _persistentIntDataValues.ToDictionary(kv => kv.Key, kv => kv.Value); - PersistentIntDataIndices = new BiLookup(); - foreach (var persistentIntData in PersistentIntDataBools.Keys) { - PersistentIntDataIndices.Add(persistentIntData, index++); + PersistentIntSyncProperties = _persistentIntDataValues.ToDictionary(kv => kv.Key, kv => kv.Value); + PersistentIntIndices = new BiLookup(); + foreach (var persistentIntData in PersistentIntSyncProperties.Keys) { + PersistentIntIndices.Add(persistentIntData, index++); } } diff --git a/HKMP/Game/Client/Save/SaveManager.cs b/HKMP/Game/Client/Save/SaveManager.cs index 4620fcff..4099c11f 100644 --- a/HKMP/Game/Client/Save/SaveManager.cs +++ b/HKMP/Game/Client/Save/SaveManager.cs @@ -1,6 +1,8 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Reflection; +using GlobalEnums; using Hkmp.Collection; using Hkmp.Game.Client.Entity; using Hkmp.Networking.Client; @@ -64,6 +66,13 @@ internal class SaveManager { /// private readonly Dictionary _bsCompHashes; + private readonly List _playerDataSyncFields; + + /// + /// PlayerData instance that contains the last values of the currently used PlayerData for comparison checking. + /// + private PlayerData _lastPlayerData; + /// /// Whether the player is hosting the server, which means that player specific save data is not networked /// to the server. @@ -80,24 +89,89 @@ public SaveManager(NetClient netClient, PacketManager packetManager, EntityManag _stringListHashes = new Dictionary(); _bsdCompHashes = new Dictionary(); _bsCompHashes = new Dictionary(); + _playerDataSyncFields = new List(); } /// /// Initializes the save manager by loading the save data json. /// public void Initialize() { - ModHooks.SetPlayerBoolHook += OnSetPlayerBoolHook; - ModHooks.SetPlayerFloatHook += OnSetPlayerFloatHook; - ModHooks.SetPlayerIntHook += OnSetPlayerIntHook; - ModHooks.SetPlayerStringHook += OnSetPlayerStringHook; - ModHooks.SetPlayerVector3Hook += OnSetPlayerVector3Hook; - + On.GameManager.StartNewGame += (orig, self, mode, rushMode) => { + orig(self, mode, rushMode); + ResetLastPlayerData(); + MonoBehaviourUtil.Instance.OnUpdateEvent += OnUpdatePlayerData; + }; + On.GameManager.ContinueGame += (orig, self) => { + orig(self); + ResetLastPlayerData(); + MonoBehaviourUtil.Instance.OnUpdateEvent += OnUpdatePlayerData; + }; + On.UIManager.GoToMainMenu += (orig, self) => { + MonoBehaviourUtil.Instance.OnUpdateEvent -= OnUpdatePlayerData; + return orig(self); + }; + UnityEngine.SceneManagement.SceneManager.activeSceneChanged += OnSceneChanged; MonoBehaviourUtil.Instance.OnUpdateEvent += OnUpdatePersistents; MonoBehaviourUtil.Instance.OnUpdateEvent += OnUpdateCompounds; _packetManager.RegisterClientPacketHandler(ClientPacketId.SaveUpdate, UpdateSaveWithData); + + foreach (var field in typeof(PlayerData).GetFields()) { + var fieldName = field.Name; + if (SaveDataMapping.PlayerDataSyncProperties.TryGetValue(fieldName, out var syncProps) && syncProps.Sync) { + _playerDataSyncFields.Add(field); + } + } + } + + /// + /// Resets the PlayerData instance that stores the last values of all synchronised fields. + /// + private void ResetLastPlayerData() { + var pd = PlayerData.instance; + + var pdConstructor = typeof(PlayerData).GetConstructor( + BindingFlags.NonPublic | BindingFlags.CreateInstance | BindingFlags.Instance, + null, + [], + null + ); + if (pdConstructor == null) { + Logger.Error("Could not find protected constructor of PlayerData"); + return; + } + + _lastPlayerData = (PlayerData) pdConstructor.Invoke([]); + + foreach (var field in _playerDataSyncFields) { + var value = field.GetValue(pd); + field.SetValue(_lastPlayerData, value); + } + } + + /// + /// Update hook to check for changes in the PlayerData instance. + /// + private void OnUpdatePlayerData() { + var pd = PlayerData.instance; + if (_lastPlayerData == null) { + return; + } + + foreach (var field in _playerDataSyncFields) { + var currentValue = field.GetValue(pd); + var lastValue = field.GetValue(_lastPlayerData); + + if (!currentValue.Equals(lastValue)) { + Logger.Debug($"PlayerData value changed from: {lastValue} to {currentValue}"); + + field.SetValue(_lastPlayerData, currentValue); + + CheckSendSaveUpdate(field.Name, () => EncodeValue(currentValue)); + } + } } /// @@ -185,83 +259,11 @@ byte[] EncodeString(string stringValue) { return [EncodeUtil.GetByte(bools)]; } - throw new NotImplementedException($"No encoding implementation for type: {value.GetType()}"); - } - - /// - /// Callback method for when a boolean is set in the player data. - /// - /// Name of the boolean variable. - /// The original value of the boolean. - private bool OnSetPlayerBoolHook(string name, bool orig) { - if (PlayerData.instance.GetBool(name) == orig) { - return orig; - } - - CheckSendSaveUpdate(name, () => EncodeValue(orig)); - - return orig; - } - - /// - /// Callback method for when a float is set in the player data. - /// - /// Name of the float variable. - /// The original value of the float. - private float OnSetPlayerFloatHook(string name, float orig) { - // ReSharper disable once CompareOfFloatsByEqualityOperator - if (PlayerData.instance.GetFloat(name) == orig) { - return orig; + if (value is MapZone mapZone) { + return [(byte) mapZone]; } - CheckSendSaveUpdate(name, () => EncodeValue(orig)); - - return orig; - } - - /// - /// Callback method for when a int is set in the player data. - /// - /// Name of the int variable. - /// The original value of the int. - private int OnSetPlayerIntHook(string name, int orig) { - if (PlayerData.instance.GetInt(name) == orig) { - return orig; - } - - CheckSendSaveUpdate(name, () => EncodeValue(orig)); - - return orig; - } - - /// - /// Callback method for when a string is set in the player data. - /// - /// Name of the string variable. - /// The original value of the boolean. - private string OnSetPlayerStringHook(string name, string res) { - if (PlayerData.instance.GetString(name) == res) { - return res; - } - - CheckSendSaveUpdate(name, () => EncodeValue(res)); - - return res; - } - - /// - /// Callback method for when a vector3 is set in the player data. - /// - /// Name of the vector3 variable. - /// The original value of the vector3. - private Vector3 OnSetPlayerVector3Hook(string name, Vector3 orig) { - if (PlayerData.instance.GetVector3(name) == orig) { - return orig; - } - - CheckSendSaveUpdate(name, () => EncodeValue(orig)); - - return orig; + throw new NotImplementedException($"No encoding implementation for type: {value.GetType()}"); } /// @@ -393,7 +395,7 @@ private void CheckSendSaveUpdate(string name, Func encodeFunc) { return; } - if (!SaveDataMapping.PlayerDataBools.TryGetValue(name, out var syncProps)) { + if (!SaveDataMapping.PlayerDataSyncProperties.TryGetValue(name, out var syncProps)) { Logger.Info($"Not in save data values, not sending save update ({name})"); return; } @@ -455,14 +457,14 @@ private void OnUpdatePersistents() { continue; } - if (SaveDataMapping.GeoRockDataBools.TryGetValue(itemData, out var shouldSync) && shouldSync) { + if (SaveDataMapping.GeoRockBools.TryGetValue(itemData, out var shouldSync) && shouldSync) { if (!_entityManager.IsSceneHost) { Logger.Info( $"Not scene host, not sending geo rock save update ({itemData.Id}, {itemData.SceneName})"); continue; } - if (!SaveDataMapping.GeoRockDataIndices.TryGetValue(itemData, out var index)) { + if (!SaveDataMapping.GeoRockIndices.TryGetValue(itemData, out var index)) { Logger.Info( $"Cannot find geo rock save data index, not sending save update ({itemData.Id}, {itemData.SceneName})"); continue; @@ -475,7 +477,7 @@ private void OnUpdatePersistents() { new[] { (byte) value } ); } else if ( - SaveDataMapping.PersistentIntDataBools.TryGetValue(itemData, out var syncProps) && + SaveDataMapping.PersistentIntSyncProperties.TryGetValue(itemData, out var syncProps) && syncProps.Sync ) { // If we should do the scene host check and the player is not scene host, skip sending @@ -490,7 +492,7 @@ private void OnUpdatePersistents() { continue; } - if (!SaveDataMapping.PersistentIntDataIndices.TryGetValue(itemData, out var index)) { + if (!SaveDataMapping.PersistentIntIndices.TryGetValue(itemData, out var index)) { Logger.Info( $"Cannot find persistent int save data index, not sending save update ({itemData.Id}, {itemData.SceneName})"); continue; @@ -521,7 +523,7 @@ private void OnUpdatePersistents() { continue; } - if (!SaveDataMapping.PersistentBoolDataBools.TryGetValue(itemData, out var syncProps) || + if (!SaveDataMapping.PersistentBoolSyncProperties.TryGetValue(itemData, out var syncProps) || !syncProps.Sync) { Logger.Info( $"Not in persistent bool save data values or false in sync props, not sending save update ({itemData.Id}, {itemData.SceneName})"); @@ -540,7 +542,7 @@ private void OnUpdatePersistents() { continue; } - if (!SaveDataMapping.PersistentBoolDataIndices.TryGetValue(itemData, out var index)) { + if (!SaveDataMapping.PersistentBoolIndices.TryGetValue(itemData, out var index)) { Logger.Info( $"Cannot find persistent bool save data index, not sending save update ({itemData.Id}, {itemData.SceneName})"); continue; @@ -583,7 +585,7 @@ Func changeFunc checkDict[varName] = newCheck; - if (!SaveDataMapping.PlayerDataBools.TryGetValue(varName, out var syncProps)) { + if (!SaveDataMapping.PlayerDataSyncProperties.TryGetValue(varName, out var syncProps)) { continue; } @@ -696,7 +698,7 @@ private void UpdateSaveWithData(ushort index, byte[] encodedValue) { var sceneData = SceneData.instance; if (SaveDataMapping.PlayerDataIndices.TryGetValue(index, out var name)) { - if (CheckPlayerSpecificHosting(SaveDataMapping.PlayerDataBools, name)) { + if (CheckPlayerSpecificHosting(SaveDataMapping.PlayerDataSyncProperties, name)) { return; } @@ -805,6 +807,12 @@ private void UpdateSaveWithData(ushort index, byte[] encodedValue) { _bsCompHashes[name] = bsComp; pd.SetVariableInternal(name, bsComp); + } else if (type == typeof(MapZone)) { + if (valueLength != 1) { + Logger.Warn($"Received save update with incorrect value length for MapZone: {valueLength}"); + } + + pd.SetVariableInternal(name, (MapZone) encodedValue[0]); } else { throw new NotImplementedException($"Could not decode type: {type}"); } @@ -812,7 +820,7 @@ private void UpdateSaveWithData(ushort index, byte[] encodedValue) { _saveChanges.ApplyPlayerDataSaveChange(name); } - if (SaveDataMapping.GeoRockDataIndices.TryGetValue(index, out var itemData)) { + if (SaveDataMapping.GeoRockIndices.TryGetValue(index, out var itemData)) { var value = encodedValue[0]; Logger.Info($"Received geo rock save update: {itemData.Id}, {itemData.SceneName}, {value}"); @@ -831,8 +839,8 @@ private void UpdateSaveWithData(ushort index, byte[] encodedValue) { sceneName = itemData.SceneName, hitsLeft = value }); - } else if (SaveDataMapping.PersistentBoolDataIndices.TryGetValue(index, out itemData)) { - if (CheckPlayerSpecificHosting(SaveDataMapping.PersistentBoolDataBools, itemData)) { + } else if (SaveDataMapping.PersistentBoolIndices.TryGetValue(index, out itemData)) { + if (CheckPlayerSpecificHosting(SaveDataMapping.PersistentBoolSyncProperties, itemData)) { return; } @@ -857,8 +865,8 @@ private void UpdateSaveWithData(ushort index, byte[] encodedValue) { }); _saveChanges.ApplyPersistentValueSaveChange(itemData); - } else if (SaveDataMapping.PersistentIntDataIndices.TryGetValue(index, out itemData)) { - if (CheckPlayerSpecificHosting(SaveDataMapping.PersistentIntDataBools, itemData)) { + } else if (SaveDataMapping.PersistentIntIndices.TryGetValue(index, out itemData)) { + if (CheckPlayerSpecificHosting(SaveDataMapping.PersistentIntSyncProperties, itemData)) { return; } @@ -971,7 +979,7 @@ Func valueFunc AddToSaveData( typeof(PlayerData).GetFields(), fieldInfo => fieldInfo.Name, - SaveDataMapping.PlayerDataBools, + SaveDataMapping.PlayerDataSyncProperties, SaveDataMapping.PlayerDataIndices, fieldInfo => fieldInfo.GetValue(pd) ); @@ -982,8 +990,8 @@ Func valueFunc Id = geoRock.id, SceneName = geoRock.sceneName }, - SaveDataMapping.GeoRockDataBools, - SaveDataMapping.GeoRockDataIndices, + SaveDataMapping.GeoRockBools, + SaveDataMapping.GeoRockIndices, geoRock => geoRock.hitsLeft ); @@ -993,8 +1001,8 @@ Func valueFunc Id = boolData.id, SceneName = boolData.sceneName }, - SaveDataMapping.PersistentBoolDataBools, - SaveDataMapping.PersistentBoolDataIndices, + SaveDataMapping.PersistentBoolSyncProperties, + SaveDataMapping.PersistentBoolIndices, boolData => boolData.activated ); @@ -1004,8 +1012,8 @@ Func valueFunc Id = intData.id, SceneName = intData.sceneName }, - SaveDataMapping.PersistentIntDataBools, - SaveDataMapping.PersistentIntDataIndices, + SaveDataMapping.PersistentIntSyncProperties, + SaveDataMapping.PersistentIntIndices, intData => intData.value ); diff --git a/HKMP/Game/Server/ServerManager.cs b/HKMP/Game/Server/ServerManager.cs index 89a7102f..67b7b18f 100644 --- a/HKMP/Game/Server/ServerManager.cs +++ b/HKMP/Game/Server/ServerManager.cs @@ -1336,8 +1336,10 @@ protected virtual void OnSaveUpdate(ushort id, SaveUpdate packet) { // Find the properties for syncing this save update, based on whether it is a geo rock, player data or // persistent bool/int item SaveDataMapping.SyncProperties syncProps; - if (SaveDataMapping.Instance.GeoRockDataIndices.TryGetValue(packet.SaveDataIndex, out var persistentItemData)) { - if (!SaveDataMapping.Instance.GeoRockDataBools.TryGetValue(persistentItemData, out _)) { + if (SaveDataMapping.Instance.GeoRockIndices.TryGetValue(packet.SaveDataIndex, out var persistentItemData)) { + Logger.Debug($" Found GeoRockData: {persistentItemData.Id}, {persistentItemData.SceneName}"); + + if (!SaveDataMapping.Instance.GeoRockBools.TryGetValue(persistentItemData, out _)) { return; } @@ -1347,42 +1349,55 @@ protected virtual void OnSaveUpdate(ushort id, SaveUpdate packet) { IgnoreSceneHost = false }; } else if (SaveDataMapping.Instance.PlayerDataIndices.TryGetValue(packet.SaveDataIndex, out var name)) { - if (!SaveDataMapping.Instance.PlayerDataBools.TryGetValue(name, out syncProps)) { + Logger.Debug($" Found PlayerData: {name}"); + + if (!SaveDataMapping.Instance.PlayerDataSyncProperties.TryGetValue(name, out syncProps)) { return; } - } else if (SaveDataMapping.Instance.PersistentBoolDataIndices.TryGetValue( + } else if (SaveDataMapping.Instance.PersistentBoolIndices.TryGetValue( packet.SaveDataIndex, out persistentItemData) ) { - if (!SaveDataMapping.Instance.PersistentBoolDataBools.TryGetValue(persistentItemData, out syncProps)) { + Logger.Debug($" Found PersistentBoolData: {persistentItemData.Id}, {persistentItemData.SceneName}"); + + if (!SaveDataMapping.Instance.PersistentBoolSyncProperties.TryGetValue(persistentItemData, out syncProps)) { return; } - } else if (SaveDataMapping.Instance.PersistentIntDataIndices.TryGetValue( + } else if (SaveDataMapping.Instance.PersistentIntIndices.TryGetValue( packet.SaveDataIndex, out persistentItemData) ) { - if (!SaveDataMapping.Instance.PersistentIntDataBools.TryGetValue(persistentItemData, out syncProps)) { + Logger.Debug($" Found PersistentIntData: {persistentItemData.Id}, {persistentItemData.SceneName}"); + + if (!SaveDataMapping.Instance.PersistentIntSyncProperties.TryGetValue(persistentItemData, out syncProps)) { return; } } else { - Logger.Info(" Could not find sync props for save update"); + Logger.Debug(" Could not find sync props for save update"); return; } // Check whether this save update requires the player to be scene host and do the check for it if (!syncProps.IgnoreSceneHost && !playerData.IsSceneHost) { - Logger.Info(" Player is not scene host, but should be for update, not broadcasting"); + Logger.Debug(" Player is not scene host, but should be for update, not broadcasting"); return; } if (syncProps.SyncType == SaveDataMapping.SyncType.Player) { + Logger.Debug(" SyncType is Player"); + if (!ServerSaveData.PlayerSaveData.TryGetValue(playerData.AuthKey, out var playerSaveData)) { + Logger.Debug(" No PlayerSaveData for player yet, creating one"); playerSaveData = new Dictionary(); ServerSaveData.PlayerSaveData[playerData.AuthKey] = playerSaveData; } + + Logger.Debug(" Storing player data"); playerSaveData[packet.SaveDataIndex] = packet.Value; } else if (syncProps.SyncType == SaveDataMapping.SyncType.Server) { + Logger.Debug(" SyncType is Server, broadcasting save update"); + ServerSaveData.GlobalSaveData[packet.SaveDataIndex] = packet.Value; foreach (var idPlayerDataPair in _playerData) { diff --git a/HKMP/Resource/save-data.json b/HKMP/Resource/save-data.json index 9079fb96..468681cb 100644 --- a/HKMP/Resource/save-data.json +++ b/HKMP/Resource/save-data.json @@ -45,6 +45,11 @@ "SyncType": "Player", "IgnoreSceneHost": true }, + "mapZone": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, "shadeScene": { "Sync": true, "SyncType": "Player", From e6eaa719f8c752aeba7a9c71a281567e0908d3dd Mon Sep 17 00:00:00 2001 From: Extremelyd1 <10898310+Extremelyd1@users.noreply.github.com> Date: Sat, 27 Jul 2024 09:34:35 +0200 Subject: [PATCH 130/216] Improve save synchronisation --- HKMP/Game/Client/Save/SaveChanges.cs | 7 +++++++ HKMP/Game/Client/Save/SaveManager.cs | 14 +++++++------- HKMP/Resource/save-data.json | 7 ++++++- 3 files changed, 20 insertions(+), 8 deletions(-) diff --git a/HKMP/Game/Client/Save/SaveChanges.cs b/HKMP/Game/Client/Save/SaveChanges.cs index 68324236..aa229bee 100644 --- a/HKMP/Game/Client/Save/SaveChanges.cs +++ b/HKMP/Game/Client/Save/SaveChanges.cs @@ -18,6 +18,13 @@ internal class SaveChanges { /// The name of the PlayerData entry. public void ApplyPlayerDataSaveChange(string name) { Logger.Debug($"ApplyPlayerData for name: {name}"); + + // If we receive the dash from a save update, we need to also set the 'canDash' boolean to ensure that + // the input for dashing is accepted + if (name == "hasDash") { + PlayerData.instance.SetBool("canDash", true); + return; + } var currentScene = UnityEngine.SceneManagement.SceneManager.GetActiveScene().name; diff --git a/HKMP/Game/Client/Save/SaveManager.cs b/HKMP/Game/Client/Save/SaveManager.cs index 4099c11f..c21b78ca 100644 --- a/HKMP/Game/Client/Save/SaveManager.cs +++ b/HKMP/Game/Client/Save/SaveManager.cs @@ -99,20 +99,15 @@ public void Initialize() { On.GameManager.StartNewGame += (orig, self, mode, rushMode) => { orig(self, mode, rushMode); ResetLastPlayerData(); - MonoBehaviourUtil.Instance.OnUpdateEvent += OnUpdatePlayerData; }; On.GameManager.ContinueGame += (orig, self) => { orig(self); ResetLastPlayerData(); - MonoBehaviourUtil.Instance.OnUpdateEvent += OnUpdatePlayerData; }; - On.UIManager.GoToMainMenu += (orig, self) => { - MonoBehaviourUtil.Instance.OnUpdateEvent -= OnUpdatePlayerData; - return orig(self); - }; - + UnityEngine.SceneManagement.SceneManager.activeSceneChanged += OnSceneChanged; + MonoBehaviourUtil.Instance.OnUpdateEvent += OnUpdatePlayerData; MonoBehaviourUtil.Instance.OnUpdateEvent += OnUpdatePersistents; MonoBehaviourUtil.Instance.OnUpdateEvent += OnUpdateCompounds; @@ -159,6 +154,11 @@ private void OnUpdatePlayerData() { if (_lastPlayerData == null) { return; } + + var gm = global::GameManager.instance; + if (gm.gameState == GameState.MAIN_MENU) { + return; + } foreach (var field in _playerDataSyncFields) { var currentValue = field.GetValue(pd); diff --git a/HKMP/Resource/save-data.json b/HKMP/Resource/save-data.json index 468681cb..4f3e4d72 100644 --- a/HKMP/Resource/save-data.json +++ b/HKMP/Resource/save-data.json @@ -40,12 +40,17 @@ "SyncType": "Player", "IgnoreSceneHost": true }, + "mapZone": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, "respawnMarkerName": { "Sync": true, "SyncType": "Player", "IgnoreSceneHost": true }, - "mapZone": { + "respawnType": { "Sync": true, "SyncType": "Player", "IgnoreSceneHost": true From 5b0b2fb27bf6d77d73ce2648a77c93324691ed42 Mon Sep 17 00:00:00 2001 From: Extremelyd1 <10898310+Extremelyd1@users.noreply.github.com> Date: Sat, 27 Jul 2024 11:00:37 +0200 Subject: [PATCH 131/216] Fix Dung Defender, Broken Vessel, and Lost Kin --- .../Client/Entity/Action/EntityFsmActions.cs | 56 ++++++++++++++++++- HKMP/Game/Client/Entity/Entity.cs | 19 +++++-- HKMP/Game/Client/Entity/EntitySpawner.cs | 4 ++ HKMP/Game/Client/Entity/EntityType.cs | 4 ++ HKMP/Resource/entity-registry.json | 23 ++++++++ 5 files changed, 99 insertions(+), 7 deletions(-) diff --git a/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs b/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs index 34404774..1ac143e0 100644 --- a/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs +++ b/HKMP/Game/Client/Entity/Action/EntityFsmActions.cs @@ -1130,10 +1130,24 @@ private static void ApplyNetworkDataFromAction(EntityNetworkData data, SetPartic if (particleSystem == null) { return; } - + + Action(); + + if (action.everyFrame) { + MonoBehaviourUtil.Instance.OnUpdateEvent += Action; + + new ActionInState { + Fsm = action.Fsm, + StateName = action.State.Name, + ExitAction = () => MonoBehaviourUtil.Instance.OnUpdateEvent -= Action + }.Register(); + } + + void Action() { #pragma warning disable CS0618 - particleSystem.emissionRate = action.emissionRate.Value; + particleSystem.emissionRate = action.emissionRate.Value; #pragma warning restore CS0618 + } } #endregion @@ -1159,9 +1173,23 @@ private static void ApplyNetworkDataFromAction(EntityNetworkData data, SetPartic return; } + Action(); + + if (action.everyFrame) { + MonoBehaviourUtil.Instance.OnUpdateEvent += Action; + + new ActionInState { + Fsm = action.Fsm, + StateName = action.State.Name, + ExitAction = () => MonoBehaviourUtil.Instance.OnUpdateEvent -= Action + }.Register(); + } + + void Action() { #pragma warning disable CS0618 - particleSystem.startSpeed = action.emissionSpeed.Value; + particleSystem.startSpeed = action.emissionSpeed.Value; #pragma warning restore CS0618 + } } #endregion @@ -3129,6 +3157,28 @@ private static void ApplyNetworkDataFromAction(EntityNetworkData data, StopLiftC #endregion + #region SetIsKinematic2d + + private static bool GetNetworkDataFromAction(EntityNetworkData data, SetIsKinematic2d action) { + return true; + } + + private static void ApplyNetworkDataFromAction(EntityNetworkData data, SetIsKinematic2d action) { + var go = action.Fsm.GetOwnerDefaultTarget(action.gameObject); + if (go == null) { + return; + } + + var rigidbody = go.GetComponent(); + if (rigidbody == null) { + return; + } + + rigidbody.isKinematic = action.isKinematic.Value; + } + + #endregion + /// /// Class that keeps track of an action that executes while in a certain state of the FSM. /// diff --git a/HKMP/Game/Client/Entity/Entity.cs b/HKMP/Game/Client/Entity/Entity.cs index 38c8260f..a5cfffd9 100644 --- a/HKMP/Game/Client/Entity/Entity.cs +++ b/HKMP/Game/Client/Entity/Entity.cs @@ -473,11 +473,20 @@ private void HandleEnemyDeathEffects() { case EntityType.WanderingHusk: corpseName = "Zombie Spider 1(Clone)"; break; + case EntityType.DungDefender: + corpseName = "Corpse Dung Defender(Clone)"; + break; + case EntityType.BrokenVessel: + corpseName = "Corpse Infected Knight(Clone)"; + break; + case EntityType.LostKin: + corpseName = "Corpse Infected Knight Dream(Clone)"; + break; default: return; } - Logger.Debug("Entity has corpse that is also enemy, deleting death effects and corpse from client entity"); + Logger.Debug($"Entity ({Id}, {Type}) has corpse that is also enemy, deleting death effects and corpse from client entity"); var enemyDeathEffects = Object.Client.GetComponent(); if (enemyDeathEffects == null) { @@ -486,10 +495,12 @@ private void HandleEnemyDeathEffects() { UnityEngine.Object.Destroy(enemyDeathEffects); var corpse = Object.Client.FindGameObjectInChildren(corpseName); - if (corpse == null) { - Logger.Debug(" Could not find corpse in children"); + if (corpse != null) { + Logger.Debug($" Destroying corpse of client object: {corpse.name}"); + UnityEngine.Object.Destroy(corpse); + } else { + Logger.Debug(" Could not find corpse of client object"); } - UnityEngine.Object.Destroy(corpse); } /// diff --git a/HKMP/Game/Client/Entity/EntitySpawner.cs b/HKMP/Game/Client/Entity/EntitySpawner.cs index 0fe2e9ce..8f8f1ffe 100644 --- a/HKMP/Game/Client/Entity/EntitySpawner.cs +++ b/HKMP/Game/Client/Entity/EntitySpawner.cs @@ -107,6 +107,10 @@ List clientFsms return SpawnBrokenVesselBalloonObject(clientFsms[7]); } + if (spawningType == EntityType.LostKin && spawnedType == EntityType.InfectedBalloon) { + return SpawnBrokenVesselBalloonObject(clientFsms[2]); + } + if (spawningType == EntityType.MantisPetra && spawnedType == EntityType.MantisPetraScythe) { return SpawnMantisPetraScytheObject(clientFsms[0]); } diff --git a/HKMP/Game/Client/Entity/EntityType.cs b/HKMP/Game/Client/Entity/EntityType.cs index 1157a751..63c3d5fd 100644 --- a/HKMP/Game/Client/Entity/EntityType.cs +++ b/HKMP/Game/Client/Entity/EntityType.cs @@ -108,6 +108,7 @@ internal enum EntityType { LargeDungBall, SmallDungBall, DungDefenderBurrow, + DungDefenderCorpse, Flukemarm, Shardmite, Glimback, @@ -137,6 +138,9 @@ internal enum EntityType { Mawlurk, InfectedBalloon, BrokenVessel, + BrokenVesselCorpse, + LostKin, + LostKinCorpse, Boofly, PrimalAspid, Hopper, diff --git a/HKMP/Resource/entity-registry.json b/HKMP/Resource/entity-registry.json index 0c52ef7f..f66cc96d 100644 --- a/HKMP/Resource/entity-registry.json +++ b/HKMP/Resource/entity-registry.json @@ -667,6 +667,11 @@ "type": "DungDefenderBurrow", "fsm_name": "Burrow Effect" }, + { + "base_object_name": "Corpse Dung Defender", + "type": "DungDefenderCorpse", + "fsm_name": "Corpse" + }, { "base_object_name": "Fluke Mother", "type": "Flukemarm", @@ -838,6 +843,19 @@ "type": "InfectedBalloon", "fsm_name": "Control" }, + { + "base_object_name": "Lost Kin", + "type": "LostKin", + "fsm_name": "IK Control", + "components": [ + "Music" + ] + }, + { + "base_object_name": "Corpse Infected Knight Dream", + "type": "LostKinCorpse", + "fsm_name": "corpse" + }, { "base_object_name": "Infected Knight", "type": "BrokenVessel", @@ -846,6 +864,11 @@ "Music" ] }, + { + "base_object_name": "Corpse Infected Knight", + "type": "BrokenVesselCorpse", + "fsm_name": "corpse" + }, { "base_object_name": "Blow Fly", "type": "Boofly", From 8f47e1e98fe19424630a453f72558e90b7a38891 Mon Sep 17 00:00:00 2001 From: Extremelyd1 <10898310+Extremelyd1@users.noreply.github.com> Date: Sat, 27 Jul 2024 11:02:16 +0200 Subject: [PATCH 132/216] Adjust logging for music component --- .../Client/Entity/Component/MusicComponent.cs | 20 ------------------- 1 file changed, 20 deletions(-) diff --git a/HKMP/Game/Client/Entity/Component/MusicComponent.cs b/HKMP/Game/Client/Entity/Component/MusicComponent.cs index 6825311f..0b5584c3 100644 --- a/HKMP/Game/Client/Entity/Component/MusicComponent.cs +++ b/HKMP/Game/Client/Entity/Component/MusicComponent.cs @@ -115,19 +115,12 @@ ApplyMusicCue self orig(self); - Logger.Debug(" Not controlled"); - var musicCue = self.musicCue.Value; if (musicCue == null) { - Logger.Debug(" Music Cue null"); return; } - Logger.Debug($" Music Cue not null, name: {musicCue.name}"); - foreach (var musicCueData in MusicCueDataList) { - Logger.Debug($" Loop, music cue: {musicCueData.Name}, {musicCueData.Type}"); - if (musicCueData.MusicCue == musicCue || musicCueData.Name == musicCue.name) { Logger.Debug($" Sending data, index: {musicCueData.Index}"); @@ -158,19 +151,12 @@ TransitionToAudioSnapshot self orig(self); - Logger.Debug(" Not controlled"); - var snapshot = self.snapshot.Value; if (snapshot == null) { - Logger.Debug(" Snapshot null"); return; } - Logger.Debug($" Snapshot not null, name: {snapshot.name}"); - foreach (var snapshotData in SnapshotDataList) { - Logger.Debug($" Loop, snapshot: {snapshotData.Name}, {snapshotData.Type}"); - if (snapshotData.Snapshot == snapshot || snapshotData.Name == snapshot.name) { Logger.Debug($" Sending data, index: {snapshotData.Index}"); @@ -219,14 +205,11 @@ public override void Update(EntityNetworkData data) { void ApplyIndex(byte index) { foreach (var musicCueData in MusicCueDataList) { - Logger.Debug($" Loop, index: {musicCueData.Index}"); - if (musicCueData.Index != index) { continue; } if (musicCueData.MusicCue == null) { - Logger.Debug(" Could not find music cue in data"); continue; } @@ -238,14 +221,11 @@ void ApplyIndex(byte index) { } foreach (var snapshotData in SnapshotDataList) { - Logger.Debug($" Loop, index: {snapshotData.Index}"); - if (snapshotData.Index != index) { continue; } if (snapshotData.Snapshot == null) { - Logger.Debug(" Could not find snapshot in data"); continue; } From 0e7de2e3f82e357ce878da812049976e3456bf21 Mon Sep 17 00:00:00 2001 From: Extremelyd1 <10898310+Extremelyd1@users.noreply.github.com> Date: Sun, 28 Jul 2024 15:07:55 +0200 Subject: [PATCH 133/216] Synchronise dream platforms --- .../Component/ChallengePromptComponent.cs | 2 +- .../Component/ChildrenActivationComponent.cs | 2 +- .../Entity/Component/ClimberComponent.cs | 4 +- .../Entity/Component/ColliderComponent.cs | 4 +- .../Entity/Component/ComponentFactory.cs | 2 + .../Entity/Component/DamageHeroComponent.cs | 4 +- .../Component/DreamPlatformComponent.cs | 196 ++++++++++++++++++ .../Entity/Component/EnemySpawnerComponent.cs | 4 +- .../Entity/Component/EntityComponent.cs | 7 +- .../Entity/Component/FlipPlatformComponent.cs | 2 +- .../Entity/Component/GravityScaleComponent.cs | 4 +- .../Component/HealthManagerComponent.cs | 4 +- .../Entity/Component/MeshRendererComponent.cs | 4 +- .../Client/Entity/Component/MusicComponent.cs | 2 +- .../Entity/Component/RotationComponent.cs | 4 +- .../Entity/Component/SpawnJarComponent.cs | 4 +- .../Component/SpriteRendererComponent.cs | 4 +- .../Entity/Component/VelocityComponent.cs | 4 +- .../Entity/Component/ZPositionComponent.cs | 4 +- HKMP/Game/Client/Entity/Entity.cs | 5 +- HKMP/Game/Client/Entity/EntityManager.cs | 4 +- HKMP/Game/Client/Entity/EntityRegistry.cs | 6 +- HKMP/Game/Client/Entity/EntityType.cs | 1 + HKMP/Resource/entity-registry.json | 14 ++ 24 files changed, 257 insertions(+), 34 deletions(-) create mode 100644 HKMP/Game/Client/Entity/Component/DreamPlatformComponent.cs diff --git a/HKMP/Game/Client/Entity/Component/ChallengePromptComponent.cs b/HKMP/Game/Client/Entity/Component/ChallengePromptComponent.cs index a33b0ada..5e808a6d 100644 --- a/HKMP/Game/Client/Entity/Component/ChallengePromptComponent.cs +++ b/HKMP/Game/Client/Entity/Component/ChallengePromptComponent.cs @@ -44,7 +44,7 @@ public override void InitializeHost() { } /// - public override void Update(EntityNetworkData data) { + public override void Update(EntityNetworkData data, bool alreadyInSceneUpdate) { var type = data.Packet.ReadByte(); // If the player is a scene client we destroy the prompt, otherwise we start the fight by progressing the FSM diff --git a/HKMP/Game/Client/Entity/Component/ChildrenActivationComponent.cs b/HKMP/Game/Client/Entity/Component/ChildrenActivationComponent.cs index 08644aab..4ca4b4fc 100644 --- a/HKMP/Game/Client/Entity/Component/ChildrenActivationComponent.cs +++ b/HKMP/Game/Client/Entity/Component/ChildrenActivationComponent.cs @@ -61,7 +61,7 @@ public override void InitializeHost() { } /// - public override void Update(EntityNetworkData data) { + public override void Update(EntityNetworkData data, bool alreadyInSceneUpdate) { if (!IsControlled) { return; } diff --git a/HKMP/Game/Client/Entity/Component/ClimberComponent.cs b/HKMP/Game/Client/Entity/Component/ClimberComponent.cs index 08840c8d..2df0a2c8 100644 --- a/HKMP/Game/Client/Entity/Component/ClimberComponent.cs +++ b/HKMP/Game/Client/Entity/Component/ClimberComponent.cs @@ -30,10 +30,10 @@ public override void InitializeHost() { } /// - public override void Update(EntityNetworkData data) { + public override void Update(EntityNetworkData data, bool alreadyInSceneUpdate) { } /// public override void Destroy() { } -} \ No newline at end of file +} diff --git a/HKMP/Game/Client/Entity/Component/ColliderComponent.cs b/HKMP/Game/Client/Entity/Component/ColliderComponent.cs index 26b85458..ec18f45b 100644 --- a/HKMP/Game/Client/Entity/Component/ColliderComponent.cs +++ b/HKMP/Game/Client/Entity/Component/ColliderComponent.cs @@ -61,7 +61,7 @@ public override void InitializeHost() { } /// - public override void Update(EntityNetworkData data) { + public override void Update(EntityNetworkData data, bool alreadyInSceneUpdate) { Logger.Info($"Received collider update for {GameObject.Client.name}"); if (!IsControlled) { @@ -80,4 +80,4 @@ public override void Update(EntityNetworkData data) { public override void Destroy() { MonoBehaviourUtil.Instance.OnUpdateEvent -= OnUpdateCollider; } -} \ No newline at end of file +} diff --git a/HKMP/Game/Client/Entity/Component/ComponentFactory.cs b/HKMP/Game/Client/Entity/Component/ComponentFactory.cs index d08d9ae2..18b29e5c 100644 --- a/HKMP/Game/Client/Entity/Component/ComponentFactory.cs +++ b/HKMP/Game/Client/Entity/Component/ComponentFactory.cs @@ -73,6 +73,8 @@ HostClientPair objects return null; case EntityComponentType.FlipPlatform: return new FlipPlatformComponent(netClient, entityId, objects); + case EntityComponentType.DreamPlatform: + return new DreamPlatformComponent(netClient, entityId, objects); default: throw new ArgumentOutOfRangeException(nameof(type), type, $"Could not instantiate entity component for type: {type}"); } diff --git a/HKMP/Game/Client/Entity/Component/DamageHeroComponent.cs b/HKMP/Game/Client/Entity/Component/DamageHeroComponent.cs index 2ff7bcf7..9fbe3c8c 100644 --- a/HKMP/Game/Client/Entity/Component/DamageHeroComponent.cs +++ b/HKMP/Game/Client/Entity/Component/DamageHeroComponent.cs @@ -60,7 +60,7 @@ public override void InitializeHost() { } /// - public override void Update(EntityNetworkData data) { + public override void Update(EntityNetworkData data, bool alreadyInSceneUpdate) { var damageDealt = data.Packet.ReadByte(); _damageHero.Host.damageDealt = damageDealt; _damageHero.Client.damageDealt = damageDealt; @@ -70,4 +70,4 @@ public override void Update(EntityNetworkData data) { public override void Destroy() { MonoBehaviourUtil.Instance.OnUpdateEvent -= OnUpdate; } -} \ No newline at end of file +} diff --git a/HKMP/Game/Client/Entity/Component/DreamPlatformComponent.cs b/HKMP/Game/Client/Entity/Component/DreamPlatformComponent.cs new file mode 100644 index 00000000..34ecad07 --- /dev/null +++ b/HKMP/Game/Client/Entity/Component/DreamPlatformComponent.cs @@ -0,0 +1,196 @@ +using Hkmp.Networking.Client; +using Hkmp.Networking.Packet.Data; +using UnityEngine; +using Logger = Hkmp.Logging.Logger; + +namespace Hkmp.Game.Client.Entity.Component; + +/// +/// This component manages the platforms that (dis)appear in dream sequences. +internal class DreamPlatformComponent : EntityComponent { + /// + /// Host-client pair of the DreamPlatform components. + /// + private readonly HostClientPair _platform; + + /// + /// The number of players currently in range of the platform. + /// + private ushort _numInRange; + /// + /// Whether the local player is in range of the platform. + /// + private bool _isInRange; + + public DreamPlatformComponent( + NetClient netClient, + ushort entityId, + HostClientPair gameObject + ) : base(netClient, entityId, gameObject) { + _platform = new HostClientPair { + Client = gameObject.Client.GetComponent(), + Host = gameObject.Host.GetComponent() + }; + + if (!_platform.Client.showOnEnable) { + _platform.Client.outerCollider.OnTriggerExited += OuterColliderOnTriggerExited; + _platform.Host.outerCollider.OnTriggerExited += OuterColliderOnTriggerExited; + + _platform.Client.innerCollider.OnTriggerEntered += InnerColliderOnTriggerEntered; + _platform.Host.innerCollider.OnTriggerEntered += InnerColliderOnTriggerEntered; + + On.DreamPlatform.Start += DreamPlatformOnStart; + } + } + + /// + /// Hook for the Start method of DreamPlatform. Used to prevent the original method from registering event + /// handlers to the trigger enter/exit. + /// + private void DreamPlatformOnStart(On.DreamPlatform.orig_Start orig, DreamPlatform self) { + if (self == _platform.Client || self == _platform.Host) { + return; + } + + orig(self); + } + + /// + /// Show the correct platform based on whether the entity is controlled or not. + /// + private void Show() { + if (IsControlled) { + _platform.Client.Show(); + } else { + _platform.Host.Show(); + } + } + + /// + /// Hide the correct platform based on whether the entity is controlled or not. + /// + private void Hide() { + if (IsControlled) { + _platform.Client.Hide(); + } else { + _platform.Host.Hide(); + } + } + + /// + /// Event handler for when the trigger for the outer collider of the platform is exited. + /// + private void OuterColliderOnTriggerExited(Collider2D collider, GameObject sender) { + // If we haven't been in range of the platform but trigger the exit, we do not want to update anything + if (!_isInRange) { + return; + } + + _isInRange = false; + + _numInRange--; + + // If the number of players in range is now 0 (or lower), we can hide the platform + if (_numInRange == 0) { + Hide(); + } + + var data = new EntityNetworkData { + Type = EntityComponentType.DreamPlatform + }; + + data.Packet.Write(_numInRange); + data.Packet.Write(0); + + SendData(data); + } + + /// + /// Event handler for when the trigger for the inner collider of the platform is entered. + /// + private void InnerColliderOnTriggerEntered(Collider2D collider, GameObject sender) { + _isInRange = true; + + EnterPlatform(); + + var data = new EntityNetworkData { + Type = EntityComponentType.DreamPlatform + }; + + data.Packet.Write(_numInRange); + data.Packet.Write(1); + + SendData(data); + } + + /// + /// Exit the platform and decrease the number of players in range. If the number hits zero, the platform will be + /// hidden. + /// + private void ExitPlatform() { + _numInRange--; + + // If the number of players in range is now 0 (or lower), we can hide the platform + if (_numInRange == 0) { + Hide(); + } + } + + /// + /// Enter the platform and increase the number of players in range. If the number is exactly 1, the platform will + /// be shown. + /// + private void EnterPlatform() { + _numInRange++; + + // If the number of players in range is exactly 1 now, we show the platform + if (_numInRange == 1) { + Show(); + } + } + + /// + public override void InitializeHost() { + } + + /// + public override void Update(EntityNetworkData data, bool alreadyInSceneUpdate) { + var numInRange = data.Packet.ReadUShort(); + var action = data.Packet.ReadByte(); + + if (alreadyInSceneUpdate) { + _numInRange = numInRange; + + if (_numInRange > 0) { + Show(); + } + + return; + } + + if (action == 0) { + // Action 0 is exited collider + ExitPlatform(); + } else if (action == 1) { + // Action 1 is entered collider + EnterPlatform(); + } else { + Logger.Error($"Could not process unknown action for DreamPlatformComponent update: {action}"); + } + } + + /// + public override void Destroy() { + if (_platform.Client != null) { + _platform.Client.outerCollider.OnTriggerExited -= OuterColliderOnTriggerExited; + _platform.Client.innerCollider.OnTriggerEntered -= InnerColliderOnTriggerEntered; + } + + if (_platform.Host != null) { + _platform.Host.outerCollider.OnTriggerExited -= OuterColliderOnTriggerExited; + _platform.Host.innerCollider.OnTriggerEntered -= InnerColliderOnTriggerEntered; + } + + On.DreamPlatform.Start -= DreamPlatformOnStart; + } +} diff --git a/HKMP/Game/Client/Entity/Component/EnemySpawnerComponent.cs b/HKMP/Game/Client/Entity/Component/EnemySpawnerComponent.cs index efe7a690..9b8f8e71 100644 --- a/HKMP/Game/Client/Entity/Component/EnemySpawnerComponent.cs +++ b/HKMP/Game/Client/Entity/Component/EnemySpawnerComponent.cs @@ -63,7 +63,7 @@ public override void InitializeHost() { } /// - public override void Update(EntityNetworkData data) { + public override void Update(EntityNetworkData data, bool alreadyInSceneUpdate) { iTween.MoveBy(_spawner.Client.gameObject, new Hashtable { { "amount", @@ -89,4 +89,4 @@ public override void Destroy() { _spawner.Host.OnEnemySpawned -= OnEnemySpawned; } } -} \ No newline at end of file +} diff --git a/HKMP/Game/Client/Entity/Component/EntityComponent.cs b/HKMP/Game/Client/Entity/Component/EntityComponent.cs index c8d69c7b..e12c9a4e 100644 --- a/HKMP/Game/Client/Entity/Component/EntityComponent.cs +++ b/HKMP/Game/Client/Entity/Component/EntityComponent.cs @@ -54,11 +54,13 @@ protected void SendData(EntityNetworkData data) { /// Initializes the entity component when the client user is the scene host. /// public abstract void InitializeHost(); + /// /// Update the entity component with the given data. /// /// The data to update with. - public abstract void Update(EntityNetworkData data); + /// Whether this data is from an already in scene packet. + public abstract void Update(EntityNetworkData data, bool alreadyInSceneUpdate); /// /// Destroy the entity component. /// @@ -87,5 +89,6 @@ internal enum EntityComponentType : ushort { SpriteRenderer, ChallengePrompt, Music, - FlipPlatform + FlipPlatform, + DreamPlatform } diff --git a/HKMP/Game/Client/Entity/Component/FlipPlatformComponent.cs b/HKMP/Game/Client/Entity/Component/FlipPlatformComponent.cs index 6a18c5e3..a1d88c1d 100644 --- a/HKMP/Game/Client/Entity/Component/FlipPlatformComponent.cs +++ b/HKMP/Game/Client/Entity/Component/FlipPlatformComponent.cs @@ -88,7 +88,7 @@ public override void InitializeHost() { } /// - public override void Update(EntityNetworkData data) { + public override void Update(EntityNetworkData data, bool alreadyInSceneUpdate) { var platform = IsControlled ? _platform.Client : _platform.Host; var type = data.Packet.ReadByte(); diff --git a/HKMP/Game/Client/Entity/Component/GravityScaleComponent.cs b/HKMP/Game/Client/Entity/Component/GravityScaleComponent.cs index 4d0b8627..cb705dc9 100644 --- a/HKMP/Game/Client/Entity/Component/GravityScaleComponent.cs +++ b/HKMP/Game/Client/Entity/Component/GravityScaleComponent.cs @@ -71,7 +71,7 @@ public override void InitializeHost() { } /// - public override void Update(EntityNetworkData data) { + public override void Update(EntityNetworkData data, bool alreadyInSceneUpdate) { if (!IsControlled) { return; } @@ -83,4 +83,4 @@ public override void Update(EntityNetworkData data) { public override void Destroy() { MonoBehaviourUtil.Instance.OnUpdateEvent -= OnUpdate; } -} \ No newline at end of file +} diff --git a/HKMP/Game/Client/Entity/Component/HealthManagerComponent.cs b/HKMP/Game/Client/Entity/Component/HealthManagerComponent.cs index 31eab343..52aa1c38 100644 --- a/HKMP/Game/Client/Entity/Component/HealthManagerComponent.cs +++ b/HKMP/Game/Client/Entity/Component/HealthManagerComponent.cs @@ -137,7 +137,7 @@ public override void InitializeHost() { } /// - public override void Update(EntityNetworkData data) { + public override void Update(EntityNetworkData data, bool alreadyInSceneUpdate) { Logger.Info("Received health manager update"); if (!IsControlled) { @@ -173,4 +173,4 @@ public override void Destroy() { On.HealthManager.Die -= HealthManagerOnDie; MonoBehaviourUtil.Instance.OnUpdateEvent -= OnUpdate; } -} \ No newline at end of file +} diff --git a/HKMP/Game/Client/Entity/Component/MeshRendererComponent.cs b/HKMP/Game/Client/Entity/Component/MeshRendererComponent.cs index db80132f..2043faf2 100644 --- a/HKMP/Game/Client/Entity/Component/MeshRendererComponent.cs +++ b/HKMP/Game/Client/Entity/Component/MeshRendererComponent.cs @@ -63,7 +63,7 @@ public override void InitializeHost() { } /// - public override void Update(EntityNetworkData data) { + public override void Update(EntityNetworkData data, bool alreadyInSceneUpdate) { var enabled = data.Packet.ReadBool(); _meshRenderer.Host.enabled = enabled; _meshRenderer.Client.enabled = enabled; @@ -73,4 +73,4 @@ public override void Update(EntityNetworkData data) { public override void Destroy() { MonoBehaviourUtil.Instance.OnUpdateEvent -= OnUpdate; } -} \ No newline at end of file +} diff --git a/HKMP/Game/Client/Entity/Component/MusicComponent.cs b/HKMP/Game/Client/Entity/Component/MusicComponent.cs index 0b5584c3..536daee7 100644 --- a/HKMP/Game/Client/Entity/Component/MusicComponent.cs +++ b/HKMP/Game/Client/Entity/Component/MusicComponent.cs @@ -180,7 +180,7 @@ public override void InitializeHost() { } /// - public override void Update(EntityNetworkData data) { + public override void Update(EntityNetworkData data, bool alreadyInSceneUpdate) { Logger.Debug("Update MusicComponent"); if (!IsControlled) { diff --git a/HKMP/Game/Client/Entity/Component/RotationComponent.cs b/HKMP/Game/Client/Entity/Component/RotationComponent.cs index b9114809..ca4c973f 100644 --- a/HKMP/Game/Client/Entity/Component/RotationComponent.cs +++ b/HKMP/Game/Client/Entity/Component/RotationComponent.cs @@ -53,7 +53,7 @@ public override void InitializeHost() { } /// - public override void Update(EntityNetworkData data) { + public override void Update(EntityNetworkData data, bool alreadyInSceneUpdate) { var rotation = data.Packet.ReadFloat(); SetRotation(GameObject.Host); @@ -74,4 +74,4 @@ void SetRotation(GameObject obj) { public override void Destroy() { MonoBehaviourUtil.Instance.OnUpdateEvent -= OnUpdateRotation; } -} \ No newline at end of file +} diff --git a/HKMP/Game/Client/Entity/Component/SpawnJarComponent.cs b/HKMP/Game/Client/Entity/Component/SpawnJarComponent.cs index e761f60e..26e25361 100644 --- a/HKMP/Game/Client/Entity/Component/SpawnJarComponent.cs +++ b/HKMP/Game/Client/Entity/Component/SpawnJarComponent.cs @@ -119,7 +119,7 @@ public override void InitializeHost() { } /// - public override void Update(EntityNetworkData data) { + public override void Update(EntityNetworkData data, bool alreadyInSceneUpdate) { Logger.Debug("Received SpawnJarComponent data"); MonoBehaviourUtil.Instance.StartCoroutine(Behaviour()); IEnumerator Behaviour() { @@ -177,4 +177,4 @@ public override void Destroy() { SpawnJarControlOnBehaviour ); } -} \ No newline at end of file +} diff --git a/HKMP/Game/Client/Entity/Component/SpriteRendererComponent.cs b/HKMP/Game/Client/Entity/Component/SpriteRendererComponent.cs index 17d1c21b..e4e94244 100644 --- a/HKMP/Game/Client/Entity/Component/SpriteRendererComponent.cs +++ b/HKMP/Game/Client/Entity/Component/SpriteRendererComponent.cs @@ -60,7 +60,7 @@ public override void InitializeHost() { } /// - public override void Update(EntityNetworkData data) { + public override void Update(EntityNetworkData data, bool alreadyInSceneUpdate) { var enabled = data.Packet.ReadBool(); _spriteRenderer.Host.enabled = enabled; _spriteRenderer.Client.enabled = enabled; @@ -70,4 +70,4 @@ public override void Update(EntityNetworkData data) { public override void Destroy() { MonoBehaviourUtil.Instance.OnUpdateEvent -= OnUpdate; } -} \ No newline at end of file +} diff --git a/HKMP/Game/Client/Entity/Component/VelocityComponent.cs b/HKMP/Game/Client/Entity/Component/VelocityComponent.cs index 00039891..a3a2d76b 100644 --- a/HKMP/Game/Client/Entity/Component/VelocityComponent.cs +++ b/HKMP/Game/Client/Entity/Component/VelocityComponent.cs @@ -72,7 +72,7 @@ public override void InitializeHost() { } /// - public override void Update(EntityNetworkData data) { + public override void Update(EntityNetworkData data, bool alreadyInSceneUpdate) { if (!IsControlled) { return; } @@ -88,4 +88,4 @@ public override void Update(EntityNetworkData data) { public override void Destroy() { MonoBehaviourUtil.Instance.OnUpdateEvent -= OnUpdate; } -} \ No newline at end of file +} diff --git a/HKMP/Game/Client/Entity/Component/ZPositionComponent.cs b/HKMP/Game/Client/Entity/Component/ZPositionComponent.cs index deeff4f5..d7adea53 100644 --- a/HKMP/Game/Client/Entity/Component/ZPositionComponent.cs +++ b/HKMP/Game/Client/Entity/Component/ZPositionComponent.cs @@ -54,7 +54,7 @@ public override void InitializeHost() { } /// - public override void Update(EntityNetworkData data) { + public override void Update(EntityNetworkData data, bool alreadyInSceneUpdate) { if (!IsControlled) { return; } @@ -78,4 +78,4 @@ void SetZ(GameObject gameObject) { public override void Destroy() { MonoBehaviourUtil.Instance.OnUpdateEvent -= OnUpdate; } -} \ No newline at end of file +} diff --git a/HKMP/Game/Client/Entity/Entity.cs b/HKMP/Game/Client/Entity/Entity.cs index a5cfffd9..5b2bebb7 100644 --- a/HKMP/Game/Client/Entity/Entity.cs +++ b/HKMP/Game/Client/Entity/Entity.cs @@ -1235,7 +1235,8 @@ public void UpdateIsActive(bool active) { /// Updates generic data for the client entity. /// /// A list of data to update the client entity with. - public void UpdateData(List entityNetworkData) { + /// Whether this data is from an already in scene update. + public void UpdateData(List entityNetworkData, bool alreadyInSceneUpdate) { foreach (var data in entityNetworkData) { if (data.Type == EntityComponentType.Fsm) { PlayMakerFSM fsm; @@ -1276,7 +1277,7 @@ public void UpdateData(List entityNetworkData) { } if (_components.TryGetValue(data.Type, out var component)) { - component.Update(data); + component.Update(data, alreadyInSceneUpdate); } } } diff --git a/HKMP/Game/Client/Entity/EntityManager.cs b/HKMP/Game/Client/Entity/EntityManager.cs index e3fa5f03..8501c90d 100644 --- a/HKMP/Game/Client/Entity/EntityManager.cs +++ b/HKMP/Game/Client/Entity/EntityManager.cs @@ -227,7 +227,7 @@ public bool HandleReliableEntityUpdate(ReliableEntityUpdate entityUpdate, bool a } if (entityUpdate.UpdateTypes.Contains(EntityUpdateType.Data)) { - entity.UpdateData(entityUpdate.GenericData); + entity.UpdateData(entityUpdate.GenericData, alreadyInSceneUpdate); } return true; @@ -449,6 +449,8 @@ private void FindEntitiesInScene(Scene scene, bool lateLoad) { .Concat(Object.FindObjectsOfType(true).Select(cameraLockArea => cameraLockArea.gameObject)) // Concatenate all GameObjects for FlipPlatform components .Concat(Object.FindObjectsOfType(true).Select(flipPlatform => flipPlatform.gameObject)) + // Concatenate all GameObjects for DreamPlatform components + .Concat(Object.FindObjectsOfType(true).Select(dreamPlatform => dreamPlatform.gameObject)) // Filter out GameObjects not in the current scene .Where(obj => obj.scene == scene) .Distinct(); diff --git a/HKMP/Game/Client/Entity/EntityRegistry.cs b/HKMP/Game/Client/Entity/EntityRegistry.cs index d925904f..2cb1392a 100644 --- a/HKMP/Game/Client/Entity/EntityRegistry.cs +++ b/HKMP/Game/Client/Entity/EntityRegistry.cs @@ -84,7 +84,11 @@ out EntityRegistryEntry foundEntry // Specifically check for entries that don't have a defined FSM whether they contain the // correct component(s) - if (entry.Type == EntityType.Tiktik) { + if (entry.Type == EntityType.DreamPlatform) { + if (gameObject.GetComponent() == null) { + continue; + } + } else if (entry.Type == EntityType.Tiktik) { if (gameObject.GetComponent() == null) { continue; } diff --git a/HKMP/Game/Client/Entity/EntityType.cs b/HKMP/Game/Client/Entity/EntityType.cs index 63c3d5fd..6831d32e 100644 --- a/HKMP/Game/Client/Entity/EntityType.cs +++ b/HKMP/Game/Client/Entity/EntityType.cs @@ -8,6 +8,7 @@ internal enum EntityType { CameraLockArea, CityElevator, CrystalPeakPlatform, + DreamPlatform, Crawlid, Tiktik, Vengefly, diff --git a/HKMP/Resource/entity-registry.json b/HKMP/Resource/entity-registry.json index f66cc96d..f81c45d4 100644 --- a/HKMP/Resource/entity-registry.json +++ b/HKMP/Resource/entity-registry.json @@ -25,6 +25,20 @@ "FlipPlatform" ] }, + { + "base_object_name": "F Plat", + "type": "DreamPlatform", + "components": [ + "DreamPlatform" + ] + }, + { + "base_object_name": "dream_plat_", + "type": "DreamPlatform", + "components": [ + "DreamPlatform" + ] + }, { "base_object_name": "Crawler", "type": "Crawlid", From 561063bddd8e6794f0669fae0cfbf01524090d09 Mon Sep 17 00:00:00 2001 From: Extremelyd1 <10898310+Extremelyd1@users.noreply.github.com> Date: Sun, 28 Jul 2024 16:33:11 +0200 Subject: [PATCH 134/216] Fix camera lock host transfer issue --- HKMP/Game/Client/ClientManager.cs | 2 +- HKMP/Game/Client/GamePatcher.cs | 77 +++++++++++++++++++++++++++++++ 2 files changed, 78 insertions(+), 1 deletion(-) diff --git a/HKMP/Game/Client/ClientManager.cs b/HKMP/Game/Client/ClientManager.cs index 48dbc109..28fe1bec 100644 --- a/HKMP/Game/Client/ClientManager.cs +++ b/HKMP/Game/Client/ClientManager.cs @@ -190,7 +190,7 @@ ModSettings modSettings _saveManager.Initialize(); new PauseManager(netClient).RegisterHooks(); - new GamePatcher().RegisterHooks(); + new GamePatcher(netClient).RegisterHooks(); new FsmPatcher().RegisterHooks(); _commandManager = new ClientCommandManager(); diff --git a/HKMP/Game/Client/GamePatcher.cs b/HKMP/Game/Client/GamePatcher.cs index d5928252..793293cd 100644 --- a/HKMP/Game/Client/GamePatcher.cs +++ b/HKMP/Game/Client/GamePatcher.cs @@ -1,5 +1,7 @@ using System; using System.Reflection; +using GlobalEnums; +using Hkmp.Networking.Client; using Modding; using Mono.Cecil.Cil; using MonoMod.Cil; @@ -19,10 +21,19 @@ internal class GamePatcher { /// private const BindingFlags BindingFlags = System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance; + /// + /// The NetClient instance to check if we are connected to a server. + /// + private readonly NetClient _netClient; + /// /// The IL Hook for the bridge lever method. /// private ILHook _bridgeLeverIlHook; + + public GamePatcher(NetClient netClient) { + _netClient = netClient; + } /// /// Register the hooks. @@ -41,6 +52,8 @@ public void RegisterHooks() { _bridgeLeverIlHook = new ILHook(type.GetMethod("MoveNext", BindingFlags), BridgeLeverOnOpenBridge); On.HutongGames.PlayMaker.Actions.CallMethodProper.DoMethodCall += CallMethodProperOnDoMethodCall; + + IL.CameraLockArea.IsInApplicableGameState += CameraLockAreaOnIsInApplicableGameState; } /// @@ -58,6 +71,8 @@ public void DeregisterHooks() { _bridgeLeverIlHook?.Dispose(); On.HutongGames.PlayMaker.Actions.CallMethodProper.DoMethodCall -= CallMethodProperOnDoMethodCall; + + IL.CameraLockArea.IsInApplicableGameState -= CameraLockAreaOnIsInApplicableGameState; } /// @@ -473,4 +488,66 @@ HutongGames.PlayMaker.Actions.CallMethodProper self orig(self); } } + + /// + /// IL Hook for the 'IsInApplicableGameState' method in 'CameraLockArea'. This is used to add a check + /// for being in the pause menu and connected to a server. Otherwise, the camera will sometimes not lock while + /// in the pause menu during host transfers. + /// + private void CameraLockAreaOnIsInApplicableGameState(ILContext il) { + try { + // Create a cursor for this context + var c = new ILCursor(il); + + // Goto after the first 'gameState' check in the method + // IL_0011: ldloc.0 // unsafeInstance + // IL_0012: ldfld valuetype GlobalEnums.GameState GameManager::gameState + // IL_0017: ldc.i4.4 + // IL_0018: beq.s IL_0024 + c.GotoNext( + MoveType.After, + i => i.MatchLdloc(0), + i => i.MatchLdfld(typeof(global::GameManager), "gameState"), + i => i.MatchLdcI4(4), + i => i.MatchBeq(out _) + ); + + // Define a label for branching to if the conditions fail + var afterChecksLabel = c.DefineLabel(); + + // Load GameManager 'unsafeInstance' onto evaluation stack + c.Emit(OpCodes.Ldloc, 0); + // Check if the game state is paused and push a boolean onto the stack based on the result + c.EmitDelegate>(gm => gm.gameState == GameState.PAUSED); + // If the game state is not paused, we branch to the last check in the method + c.Emit(OpCodes.Brfalse, afterChecksLabel); + + // Check if the NetClient is connected and push a boolean onto the stack based on the result + c.EmitDelegate(() => _netClient.IsConnected); + // If we are not connected, we branch to the last check in the method + c.Emit(OpCodes.Brfalse, afterChecksLabel); + + var returnTrueLabel = c.DefineLabel(); + + // If the previous two checks succeeded, we branch to the return true label + c.Emit(OpCodes.Br, returnTrueLabel); + + // Mark the label after our new checks but before the last check of the method + c.MarkLabel(afterChecksLabel); + + // Goto before the 'return true' IL so we can branch here if our checks succeed + // IL_0024: ldc.i4.1 + // IL_0025: ret + c.GotoNext( + MoveType.Before, + i => i.MatchLdcI4(1), + i => i.MatchRet() + ); + + // Mark the label for return true here + c.MarkLabel(returnTrueLabel); + } catch (Exception e) { + Logger.Error($"Could not change CameraLockArea#IsInApplicableGameState IL: \n{e}"); + } + } } From 61acfee5f5971baf0702c31f318b17816baf1476 Mon Sep 17 00:00:00 2001 From: Extremelyd1 <10898310+Extremelyd1@users.noreply.github.com> Date: Sun, 28 Jul 2024 16:46:42 +0200 Subject: [PATCH 135/216] Fix Flower quest placement synchronisation --- HKMP/Fsm/FsmPatcher.cs | 12 ++++++++++++ HKMP/Game/Client/Save/SaveChanges.cs | 23 +++++++++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/HKMP/Fsm/FsmPatcher.cs b/HKMP/Fsm/FsmPatcher.cs index c7e1cd41..46aad80a 100644 --- a/HKMP/Fsm/FsmPatcher.cs +++ b/HKMP/Fsm/FsmPatcher.cs @@ -119,5 +119,17 @@ private void OnFsmEnable(On.PlayMakerFSM.orig_OnEnable orig, PlayMakerFSM self) 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"); + } } } diff --git a/HKMP/Game/Client/Save/SaveChanges.cs b/HKMP/Game/Client/Save/SaveChanges.cs index aa229bee..c5d493d5 100644 --- a/HKMP/Game/Client/Save/SaveChanges.cs +++ b/HKMP/Game/Client/Save/SaveChanges.cs @@ -261,6 +261,29 @@ public void ApplyPlayerDataSaveChange(string name) { fsm.RemoveFirstAction("Activate"); fsm.SetState("Activate"); + return; + } + + if (name == "xunFlowerGiven" && currentScene == "Fungus3_49") { + var go = GameObject.Find("Inspect Region"); + if (go == null) { + return; + } + + var fsm = go.LocateMyFSM("Conversation Control"); + if (fsm == null) { + return; + } + + // Remove a bunch of actions that only apply to the placing player + fsm.RemoveFirstAction("Flowers"); + fsm.RemoveFirstAction("Ghost Appear"); + fsm.RemoveFirstAction("Ghost Appear"); + fsm.RemoveFirstAction("Look Up"); + fsm.RemoveFirstAction("Get Up"); + + fsm.SetState("Glow"); + return; } } From d1f44416db35f007095869b9e7c38b296933d0d2 Mon Sep 17 00:00:00 2001 From: Extremelyd1 <10898310+Extremelyd1@users.noreply.github.com> Date: Tue, 30 Jul 2024 17:48:52 +0200 Subject: [PATCH 136/216] Fix packet fragmentation --- HKMP/Networking/Client/UdpNetClient.cs | 5 ++-- HKMP/Networking/Packet/PacketManager.cs | 23 +++++++++++------- HKMP/Networking/Server/NetServer.cs | 25 +++++++++++--------- HKMP/Networking/UdpUpdateManager.cs | 31 +++++++++++++++++++++++++ 4 files changed, 63 insertions(+), 21 deletions(-) diff --git a/HKMP/Networking/Client/UdpNetClient.cs b/HKMP/Networking/Client/UdpNetClient.cs index 5f94d60c..397df0c6 100644 --- a/HKMP/Networking/Client/UdpNetClient.cs +++ b/HKMP/Networking/Client/UdpNetClient.cs @@ -76,10 +76,11 @@ private void ReceiveData(CancellationToken token) { EndPoint endPoint = new IPEndPoint(IPAddress.Any, 0); while (!token.IsCancellationRequested) { + var numReceived = 0; var buffer = new byte[MaxUdpPacketSize]; try { - UdpSocket.ReceiveFrom( + numReceived = UdpSocket.ReceiveFrom( buffer, SocketFlags.None, ref endPoint @@ -88,7 +89,7 @@ ref endPoint Logger.Error($"UDP Socket exception:\n{e}"); } - var packets = PacketManager.HandleReceivedData(buffer, ref _leftoverData); + var packets = PacketManager.HandleReceivedData(buffer, numReceived, ref _leftoverData); _onReceive?.Invoke(packets); } diff --git a/HKMP/Networking/Packet/PacketManager.cs b/HKMP/Networking/Packet/PacketManager.cs index d2f42361..2b7b74f3 100644 --- a/HKMP/Networking/Packet/PacketManager.cs +++ b/HKMP/Networking/Packet/PacketManager.cs @@ -479,10 +479,19 @@ Action handler /// /// Handle received data and leftover data and store subsequent leftover data again. /// - /// Byte array of received data. + /// Byte array of the buffer containing received data. The entirety of the array does not + /// necessarily contain received bytes. Rather, the first bytes are received data. + /// + /// Number of received bytes in the buffer. /// Reference byte array that should be filled with leftover data. /// A list of packets that were constructed from the received data. - public static List HandleReceivedData(byte[] receivedData, ref byte[] leftoverData) { + public static List HandleReceivedData(byte[] buffer, int numReceived, ref byte[] leftoverData) { + // Make a new byte array exactly the length of number of received bytes and fill it + var receivedData = new byte[numReceived]; + for (var i = 0; i < numReceived; i++) { + receivedData[i] = buffer[i]; + } + var currentData = receivedData; // Check whether we have leftover data from the previous read, and concatenate the two byte arrays @@ -520,8 +529,7 @@ private static List ByteArrayToPackets(byte[] data, ref byte[] leftover) // The only break from this loop is when there is no new packet to be read do { - // If there is still an int (4 bytes) to read in the data, - // it represents the next packet's length + // If there is still 2 bytes to read in the data, it represents the next packet's length var packetLength = 0; var unreadDataLength = data.Length - readIndex; if (unreadDataLength > 1) { @@ -534,15 +542,14 @@ private static List ByteArrayToPackets(byte[] data, ref byte[] leftover) break; } - // Check whether our given data array actually contains - // the same number of bytes as the packet length + // Check whether our given data array actually contains the same number of bytes as the packet length if (data.Length - readIndex < packetLength) { // There is not enough bytes in the data array to fill the requested packet with // So we put everything, including the packet length ushort (2 bytes) into the leftover byte array leftover = new byte[unreadDataLength]; for (var i = 0; i < unreadDataLength; i++) { - // Make sure to index data 2 bytes earlier, since we incremented - // when we read the packet length ushort + // Make sure to index data 2 bytes earlier, since we incremented when we read the packet + // length ushort leftover[i] = data[readIndex - 2 + i]; } diff --git a/HKMP/Networking/Server/NetServer.cs b/HKMP/Networking/Server/NetServer.cs index 195cfb6e..4d12a987 100644 --- a/HKMP/Networking/Server/NetServer.cs +++ b/HKMP/Networking/Server/NetServer.cs @@ -41,11 +41,6 @@ internal class NetServer : INetServer { /// private readonly PacketManager _packetManager; - /// - /// Object to lock asynchronous access when dealing with clients. - /// - private readonly object _clientLock = new object(); - /// /// Dictionary mapping client IDs to net server clients. /// @@ -155,11 +150,12 @@ private void ReceiveData(CancellationToken token) { EndPoint endPoint = new IPEndPoint(IPAddress.Any, 0); while (!token.IsCancellationRequested) { + var numReceived = 0; var buffer = new byte[MaxUdpPacketSize]; try { // This will block until data is available - _udpSocket.ReceiveFrom( + numReceived = _udpSocket.ReceiveFrom( buffer, SocketFlags.None, ref endPoint @@ -169,7 +165,8 @@ ref endPoint } _receivedQueue.Enqueue(new ReceivedData { - Data = buffer, + Buffer = buffer, + NumReceived = numReceived, EndPoint = endPoint as IPEndPoint }); _processingWaitHandle.Set(); @@ -192,7 +189,8 @@ private void StartProcessing(CancellationToken token) { while (_receivedQueue.TryDequeue(out var receivedData)) { var packets = PacketManager.HandleReceivedData( - receivedData.Data, + receivedData.Buffer, + receivedData.NumReceived, ref _leftoverData ); @@ -529,12 +527,17 @@ Func packetInstantiator /// internal class ReceivedData { /// - /// Byte array of received data. + /// Byte array of the buffer containing received data. + /// + public byte[] Buffer { get; init; } + + /// + /// The number of bytes in the buffer that were received. The rest of the buffer is empty. /// - public byte[] Data { get; set; } + public int NumReceived { get; init; } /// /// The IP end-point of the client from which we received the data. /// - public IPEndPoint EndPoint { get; set; } + public IPEndPoint EndPoint { get; init; } } diff --git a/HKMP/Networking/UdpUpdateManager.cs b/HKMP/Networking/UdpUpdateManager.cs index 835fc9c9..810954c6 100644 --- a/HKMP/Networking/UdpUpdateManager.cs +++ b/HKMP/Networking/UdpUpdateManager.cs @@ -27,6 +27,13 @@ internal abstract class UdpUpdateManager : UdpUpdateManage /// private const int ConnectionTimeout = 5000; + /// + /// The MTU (maximum transfer unit) to use to send packets with. If the length of a packet exceeds this, we break + /// it up into smaller packets before sending. This ensures that we control the breaking of packets in most + /// cases and do not rely on smaller network devices for the breaking up as this could impact performance. + /// + private const int PacketMtu = 1400; + /// /// The number of sequence numbers to store in the received queue to construct ack fields with and /// to check against resent data. @@ -234,6 +241,30 @@ private void CreateAndSendUpdatePacket() { // Increase (and potentially wrap) the current local sequence number _localSequence++; + // Check if the packet exceeds (usual) MTU and break it up if so + if (packet.Length > PacketMtu) { + // Get the original packet's bytes as an array + var byteArray= packet.ToArray(); + + // Keep track of the index in the original array for copying + var index = 0; + // While we have not reached the end of the original array yet with the index + while (index < byteArray.Length) { + // Take the minimum of what's left to copy in the original array and the max MTU + var length = System.Math.Min(byteArray.Length - index, PacketMtu); + // Create a new array that is this calculated length + var newBytes = new byte[length]; + // Copy over the length of bytes starting from index into the new array + Array.Copy(byteArray, index, newBytes, 0, length); + + SendPacket(new Packet.Packet(newBytes)); + + index += length; + } + + return; + } + SendPacket(packet); } From 04217f683a236d8f4d5c79f411fe857c76842f86 Mon Sep 17 00:00:00 2001 From: Extremelyd1 <10898310+Extremelyd1@users.noreply.github.com> Date: Tue, 30 Jul 2024 20:10:57 +0200 Subject: [PATCH 137/216] Fix remote nail swings causing recoil --- HKMP/Game/Client/GamePatcher.cs | 40 ++++++++++++++++++++++++--------- 1 file changed, 29 insertions(+), 11 deletions(-) diff --git a/HKMP/Game/Client/GamePatcher.cs b/HKMP/Game/Client/GamePatcher.cs index 793293cd..238e3fca 100644 --- a/HKMP/Game/Client/GamePatcher.cs +++ b/HKMP/Game/Client/GamePatcher.cs @@ -458,33 +458,51 @@ private void CallMethodProperOnDoMethodCall( On.HutongGames.PlayMaker.Actions.CallMethodProper.orig_DoMethodCall orig, HutongGames.PlayMaker.Actions.CallMethodProper self ) { - // If the FSM and game object do not match the Crystal Shot, we execute the original method and return - if (!self.Fsm.Name.Equals("FSM") || !self.Fsm.GameObject.name.Contains("Crystal Shot")) { + // If the 'behaviour' and 'methodName' strings do not match, we execute the original method and return + if (self.behaviour.Value != "HeroController" || + self.methodName.Value != "RecoilLeft" && + self.methodName.Value != "RecoilRight" && + self.methodName.Value != "RecoilDown" && + self.methodName.Value != "Bounce" + ) { orig(self); return; } - // Find the damager game object from the FSM variables, if it, its parent, or their parent is null, we - // execute the original method and return, because we know that it was not a remote player's nail slash - var damager = self.Fsm.Variables.GetFsmGameObject("Damager").Value; - if (damager == null) { + // If the state does not match, we return as well + if (!self.State.Name.StartsWith("No Box ") && !self.State.Name.StartsWith("Blocked ")) { orig(self); return; } - var parent = damager.transform.parent; - if (parent == null) { + Transform attacks; + + // Find either the 'Damager' or the 'Collider' game object from the FSM variables, and check up the hierarchy. + // If it matches the local player's object, then we know it was the local player's slash and not a remote + // player's slash + var damager = self.Fsm.Variables.FindFsmGameObject("Damager"); + var collider = self.Fsm.Variables.FindFsmGameObject("Collider"); + if (damager != null && damager.Value != null) { + attacks = damager.Value.transform.parent; + } else if (collider != null && collider.Value != null && collider.Value.transform.parent != null) { + attacks = collider.Value.transform.parent.parent; + } else { orig(self); return; } - parent = parent.parent; - if (parent == null) { + if (attacks == null) { orig(self); return; } - if (parent.name.Equals("Knight")) { + var knight = attacks.parent; + if (knight == null) { + orig(self); + return; + } + + if (knight.name.Equals("Knight")) { orig(self); } } From 18c1efda42ef5115d65ee1de77e352baf86bfa11 Mon Sep 17 00:00:00 2001 From: Extremelyd1 <10898310+Extremelyd1@users.noreply.github.com> Date: Tue, 30 Jul 2024 23:13:56 +0200 Subject: [PATCH 138/216] Improve enter scene detection, fix backwards walking players --- HKMP/Game/Client/ClientManager.cs | 75 ++++++------- HKMP/Game/Client/CustomHooks.cs | 160 +++++++++++++++++++++++++++ HKMP/Game/Client/Save/SaveManager.cs | 4 + 3 files changed, 200 insertions(+), 39 deletions(-) create mode 100644 HKMP/Game/Client/CustomHooks.cs diff --git a/HKMP/Game/Client/ClientManager.cs b/HKMP/Game/Client/ClientManager.cs index 28fe1bec..ce5d5f4e 100644 --- a/HKMP/Game/Client/ClientManager.cs +++ b/HKMP/Game/Client/ClientManager.cs @@ -155,11 +155,6 @@ public string Username { /// private Vector3 _lastScale; - /// - /// Whether the scene has just changed and we are in a scene change. - /// - private bool _sceneChanged; - /// /// Whether we have already determined whether we are scene host or not for the entity system. /// @@ -193,6 +188,9 @@ ModSettings modSettings new GamePatcher(netClient).RegisterHooks(); new FsmPatcher().RegisterHooks(); + var customHooks = new CustomHooks(); + customHooks.Initialize(); + _commandManager = new ClientCommandManager(); var eventAggregator = new EventAggregator(); @@ -259,6 +257,8 @@ ModSettings modSettings On.HeroController.Start += OnHeroControllerStart; On.HeroController.Update += OnPlayerUpdate; + customHooks.AfterEnterSceneHeroTransformed += OnEnterScene; + // Register client connect and timeout handler netClient.ConnectEvent += OnClientConnect; netClient.TimeoutEvent += OnTimeout; @@ -871,8 +871,6 @@ private void OnSceneChange(Scene oldScene, Scene newScene) { return; } - _sceneChanged = true; - // Reset the status of whether we determined the scene host or not _sceneHostDetermined = false; @@ -910,38 +908,7 @@ private void OnPlayerUpdate(On.HeroController.orig_Update orig, HeroController s // Update the last position, since it changed _lastPosition = newPosition; - if (_sceneChanged) { - _sceneChanged = false; - - // Set some default values for the packet variables in case we don't have a HeroController instance - // This might happen when we are in a non-gameplay scene without the knight - var position = Vector2.Zero; - var scale = Vector3.zero; - ushort animationClipId = 0; - - // If we do have a HeroController instance, use its values - if (HeroController.instance != null) { - var transform = HeroController.instance.transform; - var transformPos = transform.position; - - position = new Vector2(transformPos.x, transformPos.y); - scale = transform.localScale; - animationClipId = (ushort) AnimationManager.GetCurrentAnimationClip(); - } - - Logger.Debug("Sending EnterScene packet"); - - _netClient.UpdateManager.SetEnterSceneData( - SceneUtil.GetCurrentSceneName(), - position, - scale.x > 0, - animationClipId - ); - } else { - // If this was not the first position update after a scene change, - // we can simply send a position update packet - _netClient.UpdateManager.UpdatePlayerPosition(new Vector2(newPosition.x, newPosition.y)); - } + _netClient.UpdateManager.UpdatePlayerPosition(new Vector2(newPosition.x, newPosition.y)); } var newScale = heroTransform.localScale; @@ -954,6 +921,36 @@ private void OnPlayerUpdate(On.HeroController.orig_Update orig, HeroController s } } + /// + /// Callback method for the local player enters a scene. Used to network to the server that a scene is entered. + /// + private void OnEnterScene() { + // Set some default values for the packet variables in case we don't have a HeroController instance + // This might happen when we are in a non-gameplay scene without the knight + var position = Vector2.Zero; + var scale = Vector3.zero; + ushort animationClipId = 0; + + // If we do have a HeroController instance, use its values + if (HeroController.instance != null) { + var transform = HeroController.instance.transform; + var transformPos = transform.position; + + position = new Vector2(transformPos.x, transformPos.y); + scale = transform.localScale; + animationClipId = (ushort) AnimationManager.GetCurrentAnimationClip(); + } + + Logger.Debug($"Sending EnterScene packet"); + + _netClient.UpdateManager.SetEnterSceneData( + SceneUtil.GetCurrentSceneName(), + position, + scale.x > 0, + animationClipId + ); + } + /// /// Callback method for when a chat message is received. /// diff --git a/HKMP/Game/Client/CustomHooks.cs b/HKMP/Game/Client/CustomHooks.cs new file mode 100644 index 00000000..3bb68555 --- /dev/null +++ b/HKMP/Game/Client/CustomHooks.cs @@ -0,0 +1,160 @@ +using System; +using System.Reflection; +using Hkmp.Logging; +using Mono.Cecil.Cil; +using MonoMod.Cil; +using MonoMod.RuntimeDetour; + +namespace Hkmp.Game.Client; + +// TODO: create method for de-registering the hooks +/// +/// Class that manages and exposes custom hooks that are not possible with On hooks or ModHooks. Uses IL modification +/// to embed event calls in certain methods. +/// +public class CustomHooks { + /// + /// The binding flags for obtaining certain types for hooking. + /// + private const BindingFlags BindingFlags = System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance; + + /// + /// The instruction match set for matching the instructions below. This is the call to HeroInPosition.Invoke. + /// + // IL_01ae: ldloc.1 // V_1 + // IL_01af: ldfld class HeroController/HeroInPosition HeroController::heroInPosition + // IL_01b4: ldc.i4.0 + // IL_01b5: callvirt instance void HeroController/HeroInPosition::Invoke(bool) + private static readonly Func[] HeroInPositionInstructions = [ + i => i.MatchLdfld(typeof(HeroController), "heroInPosition"), + i => i.MatchLdcI4(out _), + i => i.MatchCallvirt(typeof(HeroController.HeroInPosition), "Invoke") + ]; + + /// + /// IL Hook instance for the HeroController EnterScene hook. + /// + private ILHook _heroControllerEnterSceneIlHook; + /// + /// IL Hook instance for the HeroController Respawn hook. + /// + private ILHook _heroControllerRespawnIlHook; + + /// + /// Event for when the player object is done being transformed (changed position, scale) after entering a scene. + /// + public event Action AfterEnterSceneHeroTransformed; + + /// + /// Initialize the class by registering the IL hooks. + /// + public void Initialize() { + IL.HeroController.Start += HeroControllerOnStart; + IL.HeroController.EnterSceneDreamGate += HeroControllerOnEnterSceneDreamGate; + + var type = typeof(HeroController).GetNestedType("d__469", BindingFlags); + _heroControllerEnterSceneIlHook = new ILHook(type.GetMethod("MoveNext", BindingFlags), HeroControllerOnEnterScene); + + type = typeof(HeroController).GetNestedType("d__473", BindingFlags); + _heroControllerRespawnIlHook = new ILHook(type.GetMethod("MoveNext", BindingFlags), HeroControllerOnRespawn); + } + + /// + /// IL Hook for the HeroController Start method. Calls an event within the method. + /// + private void HeroControllerOnStart(ILContext il) { + try { + // Create a cursor for this context + var c = new ILCursor(il); + + EmitAfterEnterSceneEventHeroInPosition(c); + } catch (Exception e) { + Logger.Error($"Could not change HeroControllerOnStart IL: \n{e}"); + } + } + + /// + /// IL Hook for the HeroController EnterSceneDreamGate method. Calls an event within the method. + /// + private void HeroControllerOnEnterSceneDreamGate(ILContext il) { + try { + // Create a cursor for this context + var c = new ILCursor(il); + + EmitAfterEnterSceneEventHeroInPosition(c); + } catch (Exception e) { + Logger.Error($"Could not change HeroControllerOnEnterSceneDreamGate IL: \n{e}"); + } + } + + /// + /// IL Hook for the HeroController EnterScene method. Calls an event multiple times within the method. + /// + private void HeroControllerOnEnterScene(ILContext il) { + try { + // Create a cursor for this context + var c = new ILCursor(il); + + for (var i = 0; i < 2; i++) { + EmitAfterEnterSceneEventHeroInPosition(c); + } + + // IL_0634: ldloc.1 // V_1 + // IL_0635: callvirt instance void HeroController::FaceRight() + Func[] faceDirectionInstructions = [ + i => i.MatchLdloc(1), + i => + i.MatchCallvirt(typeof(HeroController), "FaceRight") || + i.MatchCallvirt(typeof(HeroController), "FaceLeft") + ]; + + for (var i = 0; i < 2; i++) { + c.GotoNext( + MoveType.After, + HeroInPositionInstructions + ); + + c.GotoNext( + MoveType.After, + faceDirectionInstructions + ); + + c.EmitDelegate(() => { AfterEnterSceneHeroTransformed?.Invoke(); }); + } + + EmitAfterEnterSceneEventHeroInPosition(c); + } catch (Exception e) { + Logger.Error($"Could not change HeroController#EnterScene IL: \n{e}"); + } + } + + /// + /// IL Hook for the HeroController Respawn method. Calls an event multiple times within the method. + /// + private void HeroControllerOnRespawn(ILContext il) { + try { + // Create a cursor for this context + var c = new ILCursor(il); + + for (var i = 0; i < 2; i++) { + EmitAfterEnterSceneEventHeroInPosition(c); + } + } catch (Exception e) { + Logger.Error($"Could not change HeroControllerOnRespawn IL: \n{e}"); + } + } + + /// + /// Emit the delegate for calling the event after the + /// 'HeroInPosition' instructions. + /// + /// The IL cursor on which to match the instructions and emit the delegate. + private void EmitAfterEnterSceneEventHeroInPosition(ILCursor c) { + c.GotoNext( + MoveType.After, + HeroInPositionInstructions + ); + + c.EmitDelegate(() => { AfterEnterSceneHeroTransformed?.Invoke(); }); + } +} diff --git a/HKMP/Game/Client/Save/SaveManager.cs b/HKMP/Game/Client/Save/SaveManager.cs index c21b78ca..96e2ceba 100644 --- a/HKMP/Game/Client/Save/SaveManager.cs +++ b/HKMP/Game/Client/Save/SaveManager.cs @@ -156,6 +156,10 @@ private void OnUpdatePlayerData() { } var gm = global::GameManager.instance; + if (gm == null) { + return; + } + if (gm.gameState == GameState.MAIN_MENU) { return; } From 78e0a6e850e31d232a234f5def057f603bee1731 Mon Sep 17 00:00:00 2001 From: Extremelyd1 <10898310+Extremelyd1@users.noreply.github.com> Date: Thu, 1 Aug 2024 21:01:28 +0200 Subject: [PATCH 139/216] Synchronise placed markers --- HKMP/Game/Client/Save/SaveDataMapping.cs | 6 ++ HKMP/Game/Client/Save/SaveManager.cs | 105 ++++++++++++++++++++--- HKMP/Resource/save-data.json | 26 ++++++ 3 files changed, 126 insertions(+), 11 deletions(-) diff --git a/HKMP/Game/Client/Save/SaveDataMapping.cs b/HKMP/Game/Client/Save/SaveDataMapping.cs index 96cad608..3b0ff57a 100644 --- a/HKMP/Game/Client/Save/SaveDataMapping.cs +++ b/HKMP/Game/Client/Save/SaveDataMapping.cs @@ -126,6 +126,12 @@ public static SaveDataMapping Instance { [JsonProperty("bossStatueCompletionVariables")] public readonly List BossStatueCompletionVariables; + /// + /// Deserialized list of strings that represent variable names with the type of a vector3 list. + /// + [JsonProperty("vectorListVariables")] + public readonly List VectorListVariables; + /// /// Initializes the class by converting the deserialized data fields into the various dictionaries and lookups. /// diff --git a/HKMP/Game/Client/Save/SaveManager.cs b/HKMP/Game/Client/Save/SaveManager.cs index 96e2ceba..abb722f5 100644 --- a/HKMP/Game/Client/Save/SaveManager.cs +++ b/HKMP/Game/Client/Save/SaveManager.cs @@ -65,7 +65,16 @@ internal class SaveManager { /// Dictionary of BossStatue.Completion structs in the PlayerData for comparing changes against. /// private readonly Dictionary _bsCompHashes; + + /// + /// Dictionary of hash codes for vector list variables in the PlayerData for comparing changes against. + /// + private readonly Dictionary _vectorListHashes; + /// + /// List of FieldInfo for fields in PlayerData that are simple values that should be synced. Used for looping + /// over to check for changes and network those changes. + /// private readonly List _playerDataSyncFields; /// @@ -89,6 +98,7 @@ public SaveManager(NetClient netClient, PacketManager packetManager, EntityManag _stringListHashes = new Dictionary(); _bsdCompHashes = new Dictionary(); _bsCompHashes = new Dictionary(); + _vectorListHashes = new Dictionary(); _playerDataSyncFields = new List(); } @@ -115,6 +125,15 @@ public void Initialize() { foreach (var field in typeof(PlayerData).GetFields()) { var fieldName = field.Name; + if ( + SaveDataMapping.StringListVariables.Contains(fieldName) || + SaveDataMapping.BossSequenceDoorCompletionVariables.Contains(fieldName) || + SaveDataMapping.BossStatueCompletionVariables.Contains(fieldName) || + SaveDataMapping.VectorListVariables.Contains(fieldName) + ) { + continue; + } + if (SaveDataMapping.PlayerDataSyncProperties.TryGetValue(fieldName, out var syncProps) && syncProps.Sync) { _playerDataSyncFields.Add(field); } @@ -199,6 +218,13 @@ byte[] EncodeString(string stringValue) { return BitConverter.GetBytes(index); } + byte[] EncodeVector3(Vector3 vec3Value) { + return BitConverter.GetBytes(vec3Value.x) + .Concat(BitConverter.GetBytes(vec3Value.y)) + .Concat(BitConverter.GetBytes(vec3Value.z)) + .ToArray(); + } + if (value is bool bValue) { return [(byte) (bValue ? 1 : 0)]; } @@ -216,10 +242,7 @@ byte[] EncodeString(string stringValue) { } if (value is Vector3 vecValue) { - return BitConverter.GetBytes(vecValue.x) - .Concat(BitConverter.GetBytes(vecValue.y)) - .Concat(BitConverter.GetBytes(vecValue.z)) - .ToArray(); + return EncodeVector3(vecValue); } if (value is List listValue) { @@ -263,11 +286,29 @@ byte[] EncodeString(string stringValue) { return [EncodeUtil.GetByte(bools)]; } + if (value is List vecListValue) { + if (vecListValue.Count > ushort.MaxValue) { + throw new ArgumentOutOfRangeException($"Could not encode vector list length: {vecListValue.Count}"); + } + + var length = (ushort) vecListValue.Count; + + IEnumerable byteArray = BitConverter.GetBytes(length); + + for (var i = 0; i < length; i++) { + var encoded = EncodeVector3(vecListValue[i]); + + byteArray = byteArray.Concat(encoded); + } + + return byteArray.ToArray(); + } + if (value is MapZone mapZone) { return [(byte) mapZone]; } - throw new NotImplementedException($"No encoding implementation for type: {value.GetType()}"); + throw new ArgumentException($"No encoding implementation for type: {value.GetType()}"); } /// @@ -588,7 +629,7 @@ Func changeFunc Logger.Info($"Compound variable ({varName}) changed value"); checkDict[varName] = newCheck; - + if (!SaveDataMapping.PlayerDataSyncProperties.TryGetValue(varName, out var syncProps)) { continue; } @@ -655,6 +696,13 @@ Func changeFunc b1.seenTier3Unlock != b2.seenTier3Unlock || b1.usingAltVersion != b2.usingAltVersion ); + + CheckUpdates, int>( + SaveDataMapping.VectorListVariables, + _vectorListHashes, + GetVectorListHashCode, + (hash1, hash2) => hash1 != hash2 + ); } /// @@ -766,7 +814,7 @@ private void UpdateSaveWithData(ushort index, byte[] encodedValue) { list.Add(sceneName); } - // First set the new string list hash so we don't trigger an update and subsequently a feedback loop + // First set the new string list hash, so we don't trigger an update and subsequently a feedback loop _stringListHashes[name] = GetStringListHashCode(list); pd.SetVariableInternal(name, list); @@ -788,7 +836,7 @@ private void UpdateSaveWithData(ushort index, byte[] encodedValue) { boundSoul = byte2 == 1 }; - // First set the new bsdComp obj in the dict so we don't trigger an update and subsequently a + // First set the new bsdComp obj in the dict, so we don't trigger an update and subsequently a // feedback loop _bsdCompHashes[name] = bsdComp; @@ -806,11 +854,31 @@ private void UpdateSaveWithData(ushort index, byte[] encodedValue) { usingAltVersion = bools[6] }; - // First set the new bsComp obj in the dict so we don't trigger an update and subsequently a + // First set the new bsComp obj in the dict, so we don't trigger an update and subsequently a // feedback loop _bsCompHashes[name] = bsComp; pd.SetVariableInternal(name, bsComp); + } else if (type == typeof(List)) { + var length = BitConverter.ToUInt16(encodedValue, 0); + + var list = new List(); + for (var i = 0; i < length; i++) { + // Decode the floats of the vector with offset indices 2, 6, and 10 because we already read 2 + // bytes as the length. The index is multiplied by 12 as this is the length of a single float + var value = new Vector3( + BitConverter.ToSingle(encodedValue, 2 + i * 12), + BitConverter.ToSingle(encodedValue, 6 + i * 12), + BitConverter.ToSingle(encodedValue, 10 + i * 12) + ); + + list.Add(value); + } + + // First set the new string list hash, so we don't trigger an update and subsequently a feedback loop + _vectorListHashes[name] = GetVectorListHashCode(list); + + pd.SetVariableInternal(name, list); } else if (type == typeof(MapZone)) { if (valueLength != 1) { Logger.Warn($"Received save update with incorrect value length for MapZone: {valueLength}"); @@ -818,7 +886,7 @@ private void UpdateSaveWithData(ushort index, byte[] encodedValue) { pd.SetVariableInternal(name, (MapZone) encodedValue[0]); } else { - throw new NotImplementedException($"Could not decode type: {type}"); + throw new ArgumentException($"Could not decode type: {type}"); } _saveChanges.ApplyPlayerDataSaveChange(name); @@ -883,7 +951,6 @@ private void UpdateSaveWithData(ushort index, byte[] encodedValue) { Logger.Info($"Received persistent int save update: {itemData.Id}, {itemData.SceneName}, {value}"); - // TODO: make the _persistentFsmData a dictionary for quicker lookups foreach (var persistentFsmData in _persistentFsmData) { var existingItemData = persistentFsmData.PersistentItemData; @@ -1039,4 +1106,20 @@ private static int GetStringListHashCode(List list) { .Select(item => item.GetHashCode()) .Aggregate((total, nextCode) => total ^ nextCode); } + + /// + /// Get the hash code of the combined values in a Vector3 list. + /// + /// The list of Vector3 to calculate the hash code for. + /// 0 if the list is empty, otherwise a hash code matching the specific order of Vector3 in the list. + /// + private static int GetVectorListHashCode(List list) { + if (list.Count == 0) { + return 0; + } + + return list + .Select(item => item.GetHashCode()) + .Aggregate((total, nextCode) => total ^ nextCode); + } } diff --git a/HKMP/Resource/save-data.json b/HKMP/Resource/save-data.json index 4f3e4d72..3e1fe733 100644 --- a/HKMP/Resource/save-data.json +++ b/HKMP/Resource/save-data.json @@ -4747,6 +4747,26 @@ "SyncType": "Player", "IgnoreSceneHost": true }, + "placedMarkers_r": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "placedMarkers_b": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "placedMarkers_y": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "placedMarkers_w": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, "openedTramLower": { "Sync": true, "SyncType": "Player", @@ -31465,5 +31485,11 @@ "statueStateZote", "statueStateNoskHornet", "statueStateMantisLordsExtra" + ], + "vectorListVariables": [ + "placedMarkers_r", + "placedMarkers_b", + "placedMarkers_y", + "placedMarkers_w" ] } From 20be63825534ff253bb06eb66a66c8f47c4631da Mon Sep 17 00:00:00 2001 From: Extremelyd1 <10898310+Extremelyd1@users.noreply.github.com> Date: Sat, 3 Aug 2024 11:17:06 +0200 Subject: [PATCH 140/216] Fix crashes from boss scene mismatch --- HKMP/Game/Client/Entity/EntityManager.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/HKMP/Game/Client/Entity/EntityManager.cs b/HKMP/Game/Client/Entity/EntityManager.cs index 8501c90d..85de6149 100644 --- a/HKMP/Game/Client/Entity/EntityManager.cs +++ b/HKMP/Game/Client/Entity/EntityManager.cs @@ -333,7 +333,9 @@ private void OnSceneChanged(Scene oldScene, Scene newScene) { entity.Destroy(); } + // Clear the list of entities and the queue of received updates that have not been applied yet _entities.Clear(); + _receivedUpdates.Clear(); MusicComponent.ClearInstance(); From 92cdc2db2a5236fd098850d1cf2e431f9606d2e0 Mon Sep 17 00:00:00 2001 From: Extremelyd1 <10898310+Extremelyd1@users.noreply.github.com> Date: Sat, 3 Aug 2024 15:31:40 +0200 Subject: [PATCH 141/216] Bump version number --- HKMP/Version.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/HKMP/Version.cs b/HKMP/Version.cs index 3a87bc75..072adb27 100644 --- a/HKMP/Version.cs +++ b/HKMP/Version.cs @@ -7,5 +7,5 @@ internal static class Version { /// /// The version as a string. /// - public const string String = "2.4.1-es"; + public const string String = "3.0.0-es"; } From 0de18d68593fe1277f90cd6ec9c28cdebf14a046 Mon Sep 17 00:00:00 2001 From: Extremelyd1 <10898310+Extremelyd1@users.noreply.github.com> Date: Mon, 19 Aug 2024 20:03:03 +0200 Subject: [PATCH 142/216] Fix issue with hosting server on new save --- HKMP/Game/Client/ClientManager.cs | 54 +++++++++++++++++++------------ 1 file changed, 33 insertions(+), 21 deletions(-) diff --git a/HKMP/Game/Client/ClientManager.cs b/HKMP/Game/Client/ClientManager.cs index ce5d5f4e..6704d690 100644 --- a/HKMP/Game/Client/ClientManager.cs +++ b/HKMP/Game/Client/ClientManager.cs @@ -96,6 +96,31 @@ internal class ClientManager : IClientManager { /// 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 we have already determined whether we are scene host or not for the entity system. + /// + private bool _sceneHostDetermined; + #endregion #region IClientManager properties @@ -140,26 +165,6 @@ 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 we have already determined whether we are scene host or not for the entity system. - /// - private bool _sceneHostDetermined; - public ClientManager( NetClient netClient, ServerManager serverManager, @@ -857,6 +862,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(); @@ -875,7 +881,7 @@ private void OnSceneChange(Scene oldScene, Scene newScene) { _sceneHostDetermined = false; // If the old scene is a gameplay scene, we need to notify the server that we left - if (!SceneUtil.IsNonGameplayScene(oldScene.name)) { + if (!SceneUtil.IsNonGameplayScene(oldScene.name) && oldScene.name == _lastScene) { _netClient.UpdateManager.SetLeftScene(); } } @@ -925,6 +931,12 @@ 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; From 2b8eb22be9b20399bc29d20e7c51ce1b8a61666e Mon Sep 17 00:00:00 2001 From: Extremelyd1 <10898310+Extremelyd1@users.noreply.github.com> Date: Sun, 1 Sep 2024 20:58:30 +0200 Subject: [PATCH 143/216] Various fixes for Dream Warriors in Godhome --- HKMP/Game/Client/Entity/EntitySpawner.cs | 8 ++++++-- HKMP/Resource/entity-registry.json | 12 ++++++------ 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/HKMP/Game/Client/Entity/EntitySpawner.cs b/HKMP/Game/Client/Entity/EntitySpawner.cs index 8f8f1ffe..28880c36 100644 --- a/HKMP/Game/Client/Entity/EntitySpawner.cs +++ b/HKMP/Game/Client/Entity/EntitySpawner.cs @@ -116,11 +116,15 @@ List clientFsms } if (spawningType == EntityType.Galien && spawnedType == EntityType.GalienMiniScythe) { - return SpawnGalienMiniScytheObject(clientFsms[2]); + // The reason we do not use a hardcoded index from the FSMs is because the Shield Attack FSM is indexed + // differently depending on whether Markoth is in Godhome or not + return SpawnGalienMiniScytheObject(clientFsms.Find(fsm => fsm.Fsm.Name.Equals("Summon Minis"))); } if (spawningType == EntityType.Markoth && spawnedType == EntityType.MarkothShield) { - return SpawnMarkothShieldObject(clientFsms[3]); + // The reason we do not use a hardcoded index from the FSMs is because the Shield Attack FSM is indexed + // differently depending on whether Markoth is in Godhome or not + return SpawnMarkothShieldObject(clientFsms.Find(fsm => fsm.Fsm.Name.Equals("Shield Attack"))); } if (spawningType == EntityType.Kingsmould && spawnedType == EntityType.KingsmouldBlade) { diff --git a/HKMP/Resource/entity-registry.json b/HKMP/Resource/entity-registry.json index f81c45d4..c1162514 100644 --- a/HKMP/Resource/entity-registry.json +++ b/HKMP/Resource/entity-registry.json @@ -1118,7 +1118,7 @@ { "base_object_name": "Ghost Warrior Xero", "type": "Xero", - "fsm_name": "FSM", + "fsm_name": "Attacking", "components": [ "Music" ] @@ -1142,7 +1142,7 @@ { "base_object_name": "Ghost Warrior Hu", "type": "ElderHu", - "fsm_name": "FSM", + "fsm_name": "Attacking", "components": [ "Music" ] @@ -1150,7 +1150,7 @@ { "base_object_name": "Ghost Warrior Marmu", "type": "Marmu", - "fsm_name": "FSM", + "fsm_name": "Control", "components": [ "Music" ] @@ -1158,7 +1158,7 @@ { "base_object_name": "Ghost Warrior No Eyes", "type": "NoEyes", - "fsm_name": "FSM", + "fsm_name": "Attacking", "components": [ "Music" ] @@ -1166,7 +1166,7 @@ { "base_object_name": "Ghost Warrior Galien", "type": "Galien", - "fsm_name": "FSM", + "fsm_name": "Movement", "components": [ "Music" ] @@ -1187,7 +1187,7 @@ { "base_object_name": "Ghost Warrior Markoth", "type": "Markoth", - "fsm_name": "FSM", + "fsm_name": "Attacking", "components": [ "Music" ] From 33579f19fd7ba2a6757673c3424feddb78c7c4d3 Mon Sep 17 00:00:00 2001 From: Extremelyd1 <10898310+Extremelyd1@users.noreply.github.com> Date: Sat, 7 Sep 2024 10:40:26 +0200 Subject: [PATCH 144/216] Fix Enraged Guardian synchronisation --- HKMP/Game/Client/Entity/EntityType.cs | 1 + HKMP/Resource/entity-registry.json | 8 ++++++++ 2 files changed, 9 insertions(+) diff --git a/HKMP/Game/Client/Entity/EntityType.cs b/HKMP/Game/Client/Entity/EntityType.cs index 6831d32e..afb146b3 100644 --- a/HKMP/Game/Client/Entity/EntityType.cs +++ b/HKMP/Game/Client/Entity/EntityType.cs @@ -120,6 +120,7 @@ internal enum EntityType { CrystallisedHusk, CrystalGuardian, CrystalGuardianLaser, + EnragedGuardian, FuriousVengefly, VolatileGruzzer, ViolentHusk, diff --git a/HKMP/Resource/entity-registry.json b/HKMP/Resource/entity-registry.json index c1162514..ff450c65 100644 --- a/HKMP/Resource/entity-registry.json +++ b/HKMP/Resource/entity-registry.json @@ -699,6 +699,14 @@ "type": "HuskMiner", "fsm_name": "Zombie Miner" }, + { + "base_object_name": "Zombie Beam Miner Rematch", + "type": "EnragedGuardian", + "fsm_name": "Beam Miner", + "components": [ + "Music" + ] + }, { "base_object_name": "Zombie Beam Miner", "type": "CrystallisedHusk", From 31442cf32087eb4016f17dd7338e67aa49284dda Mon Sep 17 00:00:00 2001 From: Extremelyd1 <10898310+Extremelyd1@users.noreply.github.com> Date: Sat, 7 Sep 2024 10:40:48 +0200 Subject: [PATCH 145/216] Fix save data soft-locks --- HKMP/Game/Client/Save/SaveManager.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/HKMP/Game/Client/Save/SaveManager.cs b/HKMP/Game/Client/Save/SaveManager.cs index abb722f5..b2fed242 100644 --- a/HKMP/Game/Client/Save/SaveManager.cs +++ b/HKMP/Game/Client/Save/SaveManager.cs @@ -767,6 +767,7 @@ private void UpdateSaveWithData(ushort index, byte[] encodedValue) { var value = encodedValue[0] == 1; + _lastPlayerData?.SetBoolInternal(name, value); pd.SetBoolInternal(name, value); } else if (type == typeof(float)) { if (valueLength != 4) { @@ -775,6 +776,7 @@ private void UpdateSaveWithData(ushort index, byte[] encodedValue) { var value = BitConverter.ToSingle(encodedValue, 0); + _lastPlayerData?.SetFloatInternal(name, value); pd.SetFloatInternal(name, value); } else if (type == typeof(int)) { if (valueLength != 4) { @@ -783,10 +785,12 @@ private void UpdateSaveWithData(ushort index, byte[] encodedValue) { var value = BitConverter.ToInt32(encodedValue, 0); + _lastPlayerData?.SetIntInternal(name, value); pd.SetIntInternal(name, value); } else if (type == typeof(string)) { var value = DecodeString(encodedValue, 0); + _lastPlayerData?.SetStringInternal(name, value); pd.SetStringInternal(name, value); } else if (type == typeof(Vector3)) { if (valueLength != 12) { @@ -799,6 +803,7 @@ private void UpdateSaveWithData(ushort index, byte[] encodedValue) { BitConverter.ToSingle(encodedValue, 8) ); + _lastPlayerData?.SetVector3Internal(name, value); pd.SetVector3Internal(name, value); } else if (type == typeof(List)) { var length = BitConverter.ToUInt16(encodedValue, 0); @@ -884,6 +889,7 @@ private void UpdateSaveWithData(ushort index, byte[] encodedValue) { Logger.Warn($"Received save update with incorrect value length for MapZone: {valueLength}"); } + _lastPlayerData?.SetVariableInternal(name, (MapZone) encodedValue[0]); pd.SetVariableInternal(name, (MapZone) encodedValue[0]); } else { throw new ArgumentException($"Could not decode type: {type}"); From 1987de2058796b2241daf8615695c2b3dad1c992 Mon Sep 17 00:00:00 2001 From: Extremelyd1 <10898310+Extremelyd1@users.noreply.github.com> Date: Sat, 7 Sep 2024 21:33:38 +0200 Subject: [PATCH 146/216] Fix issue with FSM templates on entities that activate late --- HKMP/Game/Client/Entity/Entity.cs | 62 ++++++++++++++++---- HKMP/Game/Client/Entity/EntityInitializer.cs | 22 +++++-- 2 files changed, 69 insertions(+), 15 deletions(-) diff --git a/HKMP/Game/Client/Entity/Entity.cs b/HKMP/Game/Client/Entity/Entity.cs index 5b2bebb7..9b1bfef7 100644 --- a/HKMP/Game/Client/Entity/Entity.cs +++ b/HKMP/Game/Client/Entity/Entity.cs @@ -1,4 +1,5 @@ using System; +using System.Collections; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; @@ -275,6 +276,7 @@ private void ProcessHostFsm(PlayMakerFSM fsm) { for (var i = 0; i < fsm.FsmStates.Length; i++) { var state = fsm.FsmStates[i]; + var stateName = state.Name; for (var j = 0; j < state.Actions.Length; j++) { var action = state.Actions[j]; @@ -286,18 +288,56 @@ private void ProcessHostFsm(PlayMakerFSM fsm) { continue; } - _hookedActions[action] = new HookedEntityAction { - Action = action, - FsmIndex = _fsms.Host.IndexOf(fsm), - StateIndex = i, - ActionIndex = j - }; - // Logger.Info($"Created hooked action: {action.GetType()}, {_fsms.Host.IndexOf(fsm)}, {state.Name}, {j}"); - - if (!_hookedTypes.Contains(action.GetType())) { - _hookedTypes.Add(action.GetType()); + Func actionFunc; + var stateIndex = i; + var actionIndex = j; + + // Because it can happen that the action from a state is not properly initialized (usually when the + // entity is made active later in the scene and uses an FSM template), we need to check it here + // and create a coroutine that waits for FSM to properly initialize all its states and only then + // continue. When continuing, we no longer use the same instance of the action and state, so we + // use a function that re-obtains the correct action to make a hook from + // This is all complicated logic simply because it happens (empirically) once for the Grimm boss fight + if (action.Fsm == null) { + actionFunc = () => fsm.FsmStates[stateIndex].Actions[actionIndex]; + var checkFunc = () => actionFunc.Invoke().Fsm == null; + var sceneName = fsm.gameObject.scene.name; + + Logger.Debug($"Registering delayed hook for action that is not valid: {action.GetType()}, {_fsms.Host.IndexOf(fsm)}, {stateName}, {actionIndex}"); + + MonoBehaviourUtil.Instance.StartCoroutine(WaitForActionInitialization()); + IEnumerator WaitForActionInitialization() { + while (checkFunc.Invoke()) { + if (UnityEngine.SceneManagement.SceneManager.GetActiveScene().name != sceneName) { + yield break; + } + + yield return new WaitForSeconds(0.1f); + } + + Logger.Debug("Delayed hook action is now valid, continuing..."); + CreateHookedAction(); + } + } else { + actionFunc = () => action; + CreateHookedAction(); + } - FsmActionHooks.RegisterFsmStateActionType(action.GetType(), OnActionEntered); + void CreateHookedAction() { + var hookedAction = actionFunc.Invoke(); + + _hookedActions[hookedAction] = new HookedEntityAction { + Action = hookedAction, + FsmIndex = _fsms.Host.IndexOf(fsm), + StateIndex = stateIndex, + ActionIndex = actionIndex + }; + Logger.Info( + $"Created hooked action: {hookedAction.GetType()}, {_fsms.Host.IndexOf(fsm)}, {stateName}, {actionIndex}"); + + if (_hookedTypes.Add(hookedAction.GetType())) { + FsmActionHooks.RegisterFsmStateActionType(hookedAction.GetType(), OnActionEntered); + } } } } diff --git a/HKMP/Game/Client/Entity/EntityInitializer.cs b/HKMP/Game/Client/Entity/EntityInitializer.cs index 8327f46a..f9e61682 100644 --- a/HKMP/Game/Client/Entity/EntityInitializer.cs +++ b/HKMP/Game/Client/Entity/EntityInitializer.cs @@ -78,9 +78,14 @@ public static void InitializeFsm(PlayMakerFSM fsm) { // Now we can loop over the states in the same order as our "InitStateNames" array foreach (var state in statesToInit) { Logger.Debug($"Found initialization state: {state.Name}, executing actions"); + + // The index of the state in the FSM, NOT in the list of states to initialize + var stateIndex = Array.IndexOf(fsm.FsmStates, state); // Go over each action and try to execute it by applying empty data to it - foreach (var action in state.Actions) { + for (var actionIndex = 0; actionIndex < state.Actions.Length; actionIndex++) { + var action = state.Actions[actionIndex]; + if (!action.Enabled) { continue; } @@ -93,15 +98,24 @@ public static void InitializeFsm(PlayMakerFSM fsm) { if (action.Fsm == null) { Logger.Debug("Initializing FSM and action.Fsm is null, starting coroutine"); + var finalActionIndex = actionIndex; + var sceneName = fsm.gameObject.scene.name; + var actionFunc = () => fsm.FsmStates[stateIndex].Actions[finalActionIndex]; + var checkFunc = () => actionFunc.Invoke().Fsm == null; + MonoBehaviourUtil.Instance.StartCoroutine(WaitForActionInitialization()); IEnumerator WaitForActionInitialization() { - while (action.Fsm == null) { + while (checkFunc.Invoke()) { + if (UnityEngine.SceneManagement.SceneManager.GetActiveScene().name != sceneName) { + yield break; + } + yield return new WaitForSeconds(0.1f); } - Logger.Debug("Initializing FSM action completed"); + Logger.Debug($"Initializing FSM action completed, executing action: {actionFunc.Invoke().GetType()}"); - EntityFsmActions.ApplyNetworkDataFromAction(null, action); + EntityFsmActions.ApplyNetworkDataFromAction(null, actionFunc.Invoke()); } continue; From 84be183674613cfcc84d2af17c5e3fdf8c382cb6 Mon Sep 17 00:00:00 2001 From: Extremelyd1 <10898310+Extremelyd1@users.noreply.github.com> Date: Sat, 7 Sep 2024 23:04:21 +0200 Subject: [PATCH 147/216] Update workflows --- .github/workflows/docs.yml | 6 +++--- .github/workflows/dotnet.yml | 10 +++++----- .github/workflows/label-remover.yml | 2 +- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 7209182e..1578da3c 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -26,10 +26,10 @@ jobs: steps: - name: Checkout commit - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Setup .NET - uses: actions/setup-dotnet@v3 + uses: actions/setup-dotnet@v4 with: dotnet-version: 6.0.x @@ -45,7 +45,7 @@ jobs: - name: Publish to GH Pages if: github.event_name == 'push' - uses: peaceiris/actions-gh-pages@v3 + uses: peaceiris/actions-gh-pages@v4 with: github_token: ${{ secrets.GITHUB_TOKEN }} publish_dir: docs/_site diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index 64581e8c..5a43a849 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/dotnet.yml @@ -16,12 +16,12 @@ jobs: # If it is a push event, we checkout the commit - name: Checkout commit if: github.event_name == 'push' - uses: actions/checkout@v3 + uses: actions/checkout@v4 # If it is a PR, we checkout the head of the PR branch - name: Checkout PR branch if: github.event_name == 'pull_request_target' - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: ref: ${{ github.event.pull_request.head.sha }} @@ -42,7 +42,7 @@ jobs: cp ${{ github.workspace }}/HKMP/lib/Newtonsoft.Json.dll ${{ github.workspace }}/HKMPServer/Lib - name: Setup .NET - uses: actions/setup-dotnet@v3 + uses: actions/setup-dotnet@v4 with: dotnet-version: 6.0.x @@ -53,7 +53,7 @@ jobs: run: dotnet build --no-restore -c release --verbosity n ${{ github.workspace }} - name: Upload HKMP artifact - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: HKMP path: | @@ -62,7 +62,7 @@ jobs: ${{ github.workspace }}/HKMP/bin/Release/net472/HKMP.pdb - name: Upload HKMPServer artifact - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: HKMPServer path: | diff --git a/.github/workflows/label-remover.yml b/.github/workflows/label-remover.yml index 406f59d3..78389831 100644 --- a/.github/workflows/label-remover.yml +++ b/.github/workflows/label-remover.yml @@ -22,7 +22,7 @@ jobs: # Fail workflow to indicate that the PR has not been built because of new commits - name: Fail workflow if: github.event.action == 'synchronize' - uses: actions/github-script@v3 + uses: actions/github-script@v7 with: script: | core.setFailed('PR was not marked with "safe to build" label') From 481a121e1a80f751666a7ddbb0350a07370709f3 Mon Sep 17 00:00:00 2001 From: Extremelyd1 <10898310+Extremelyd1@users.noreply.github.com> Date: Sat, 7 Sep 2024 23:17:12 +0200 Subject: [PATCH 148/216] Bump version number --- HKMP/Version.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/HKMP/Version.cs b/HKMP/Version.cs index 072adb27..28c80d19 100644 --- a/HKMP/Version.cs +++ b/HKMP/Version.cs @@ -7,5 +7,5 @@ internal static class Version { /// /// The version as a string. /// - public const string String = "3.0.0-es"; + public const string String = "3.0.1-es"; } From ed2048b8ec7e9a8a8be106b4ce9b0637ac5c09f6 Mon Sep 17 00:00:00 2001 From: Extremelyd1 <10898310+Extremelyd1@users.noreply.github.com> Date: Thu, 12 Sep 2024 19:49:49 +0200 Subject: [PATCH 149/216] Fix music component causing issues for FSMs --- HKMP/Game/Client/ClientManager.cs | 5 +- HKMP/Game/Client/CustomHooks.cs | 93 ++++++++++++-- .../Client/Entity/Component/MusicComponent.cs | 116 ++++++++++++++---- 3 files changed, 177 insertions(+), 37 deletions(-) diff --git a/HKMP/Game/Client/ClientManager.cs b/HKMP/Game/Client/ClientManager.cs index 6704d690..782a6d22 100644 --- a/HKMP/Game/Client/ClientManager.cs +++ b/HKMP/Game/Client/ClientManager.cs @@ -193,8 +193,7 @@ ModSettings modSettings new GamePatcher(netClient).RegisterHooks(); new FsmPatcher().RegisterHooks(); - var customHooks = new CustomHooks(); - customHooks.Initialize(); + CustomHooks.Initialize(); _commandManager = new ClientCommandManager(); var eventAggregator = new EventAggregator(); @@ -262,7 +261,7 @@ ModSettings modSettings On.HeroController.Start += OnHeroControllerStart; On.HeroController.Update += OnPlayerUpdate; - customHooks.AfterEnterSceneHeroTransformed += OnEnterScene; + CustomHooks.AfterEnterSceneHeroTransformed += OnEnterScene; // Register client connect and timeout handler netClient.ConnectEvent += OnClientConnect; diff --git a/HKMP/Game/Client/CustomHooks.cs b/HKMP/Game/Client/CustomHooks.cs index 3bb68555..cbc34564 100644 --- a/HKMP/Game/Client/CustomHooks.cs +++ b/HKMP/Game/Client/CustomHooks.cs @@ -1,18 +1,21 @@ 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 /// -/// Class that manages and exposes custom hooks that are not possible with On hooks or ModHooks. Uses IL modification +/// 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 class CustomHooks { +public static class CustomHooks { /// /// The binding flags for obtaining certain types for hooking. /// @@ -34,21 +37,32 @@ public class CustomHooks { /// /// IL Hook instance for the HeroController EnterScene hook. /// - private ILHook _heroControllerEnterSceneIlHook; + private static ILHook _heroControllerEnterSceneIlHook; /// /// IL Hook instance for the HeroController Respawn hook. /// - private ILHook _heroControllerRespawnIlHook; + private static ILHook _heroControllerRespawnIlHook; /// /// Event for when the player object is done being transformed (changed position, scale) after entering a scene. /// - public event Action AfterEnterSceneHeroTransformed; + 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; /// /// Initialize the class by registering the IL hooks. /// - public void Initialize() { + public static void Initialize() { IL.HeroController.Start += HeroControllerOnStart; IL.HeroController.EnterSceneDreamGate += HeroControllerOnEnterSceneDreamGate; @@ -57,12 +71,15 @@ public void Initialize() { 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; } /// /// IL Hook for the HeroController Start method. Calls an event within the method. /// - private void HeroControllerOnStart(ILContext il) { + private static void HeroControllerOnStart(ILContext il) { try { // Create a cursor for this context var c = new ILCursor(il); @@ -76,7 +93,7 @@ private void HeroControllerOnStart(ILContext il) { /// /// IL Hook for the HeroController EnterSceneDreamGate method. Calls an event within the method. /// - private void HeroControllerOnEnterSceneDreamGate(ILContext il) { + private static void HeroControllerOnEnterSceneDreamGate(ILContext il) { try { // Create a cursor for this context var c = new ILCursor(il); @@ -90,7 +107,7 @@ private void HeroControllerOnEnterSceneDreamGate(ILContext il) { /// /// IL Hook for the HeroController EnterScene method. Calls an event multiple times within the method. /// - private void HeroControllerOnEnterScene(ILContext il) { + private static void HeroControllerOnEnterScene(ILContext il) { try { // Create a cursor for this context var c = new ILCursor(il); @@ -131,7 +148,7 @@ private void HeroControllerOnEnterScene(ILContext il) { /// /// IL Hook for the HeroController Respawn method. Calls an event multiple times within the method. /// - private void HeroControllerOnRespawn(ILContext il) { + private static void HeroControllerOnRespawn(ILContext il) { try { // Create a cursor for this context var c = new ILCursor(il); @@ -149,7 +166,7 @@ private void HeroControllerOnRespawn(ILContext il) { /// 'HeroInPosition' instructions. /// /// The IL cursor on which to match the instructions and emit the delegate. - private void EmitAfterEnterSceneEventHeroInPosition(ILCursor c) { + private static void EmitAfterEnterSceneEventHeroInPosition(ILCursor c) { c.GotoNext( MoveType.After, HeroInPositionInstructions @@ -157,4 +174,58 @@ private void EmitAfterEnterSceneEventHeroInPosition(ILCursor c) { 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}"); + } + } } diff --git a/HKMP/Game/Client/Entity/Component/MusicComponent.cs b/HKMP/Game/Client/Entity/Component/MusicComponent.cs index 536daee7..ec964ffd 100644 --- a/HKMP/Game/Client/Entity/Component/MusicComponent.cs +++ b/HKMP/Game/Client/Entity/Component/MusicComponent.cs @@ -21,14 +21,35 @@ internal class MusicComponent : EntityComponent { /// private const string MusicDataFilePath = "Hkmp.Resource.music-data.json"; + /// + /// Static list of MusicCueData instances that is loaded from an embedded JSON file. + /// Used for coupling IDs to music cues that can then be used for bidirectional lookups. + /// private static readonly List MusicCueDataList; + /// + /// Static list of AudioMixerSnapshotData instances that is loaded from an embedded JSON file. + /// Used for coupling IDs to audio snapshots that can then be used for bidirectional lookups. + /// private static readonly List SnapshotDataList; + /// + /// The singleton instance of MusicComponent to ensure we only have one MusicComponent responsible for + /// synchronising music in a scene. + /// private static MusicComponent _instance; + /// + /// The index of the last played music cue, so we don't restart them unnecessarily. + /// private byte _lastMusicCueIndex; + /// + /// The index of the last played audio snapshot, so we don't restart them unnecessarily. + /// private byte _lastSnapshotIndex; + /// + /// Static constructor responsible for loading data from the JSON and registering static hooks. + /// static MusicComponent() { var dataPair = FileUtil.LoadObjectFromEmbeddedJson< (List, List) @@ -49,6 +70,15 @@ static MusicComponent() { On.PlayMakerFSM.OnEnable += OnFsmEnable; } + /// + /// Try to create a new instance of MusicComponent if it doesn't exist yet. This will prevent the creation of + /// more instances by keeping track of a singleton instance. + /// + /// The NetClient instance for networking data. + /// The entity ID that this component is attached to. + /// The host-client pair of game objects of the entity. + /// The created instance of MusicComponent if successful, otherwise null. + /// True if a new component could be created, false if a component already existed. public static bool CreateInstance( NetClient netClient, ushort entityId, @@ -65,10 +95,21 @@ out MusicComponent musicComponent return false; } + /// + /// Clear the current singleton instance of the component. + /// public static void ClearInstance() { _instance = null; } + /// + /// Get the MusicCueData instance from the list for which the given predicate holds. + /// + /// The predicate function that should return true for the MusicCueData that is + /// requested. + /// The MusicCueData for which the predicate holds, or null if no such instance could + /// be found. + /// True if the MusicCueData was found, false otherwise. private static bool GetMusicCueData(Func predicate, out MusicCueData musicCueData) { foreach (var data in MusicCueDataList) { if (predicate.Invoke(data)) { @@ -81,6 +122,14 @@ private static bool GetMusicCueData(Func predicate, out Musi return false; } + /// + /// Get the AudioMixerSnapshotData instance from the list for which the given predicate holds. + /// + /// The predicate function that should return true for the AudioMixerSnapshotData that is + /// requested. + /// The AudioMixerSnapshotData for which the predicate holds, or null if no such + /// instance could be found. + /// True if the AudioMixerSnapshotData was found, false otherwise. private static bool GetAudioMixerSnapshotData(Func predicate, out AudioMixerSnapshotData snapshotData) { foreach (var data in SnapshotDataList) { if (predicate.Invoke(data)) { @@ -98,24 +147,23 @@ private MusicComponent( ushort entityId, HostClientPair gameObject ) : base(netClient, entityId, gameObject) { - On.HutongGames.PlayMaker.Actions.ApplyMusicCue.OnEnter += ApplyMusicCueOnEnter; - On.HutongGames.PlayMaker.Actions.TransitionToAudioSnapshot.OnEnter += TransitionToAudioSnapshotOnEnter; + CustomHooks.ApplyMusicCueFromFsmAction += OnApplyMusicCue; + CustomHooks.TransitionToAudioSnapshotFromFsmAction += OnTransitionToAudioSnapshot; } - private void ApplyMusicCueOnEnter( - On.HutongGames.PlayMaker.Actions.ApplyMusicCue.orig_OnEnter orig, - ApplyMusicCue self - ) { - - Logger.Debug($"ApplyMusicCueOnEnter: {self.Fsm.GameObject.gameObject.name}, {self.Fsm.Name}"); + /// + /// Hook that is called when the AudioManager.ApplyMusicCue is called from an ApplyMusicCue FSM action. + /// Used to network the starting of a music cue for the scene host. + /// + /// The ApplyMusicCue FSM action responsible for the call. + private void OnApplyMusicCue(ApplyMusicCue action) { + Logger.Debug($"OnApplyMusicCue: {action.Fsm.GameObject.gameObject.name}, {action.Fsm.Name}"); if (IsControlled) { return; } - orig(self); - - var musicCue = self.musicCue.Value; + var musicCue = action.musicCue.Value; if (musicCue == null) { return; } @@ -139,19 +187,24 @@ ApplyMusicCue self } } - private void TransitionToAudioSnapshotOnEnter( - On.HutongGames.PlayMaker.Actions.TransitionToAudioSnapshot.orig_OnEnter orig, - TransitionToAudioSnapshot self - ) { - Logger.Debug($"TransitionToAudioSnapshotOnEnter: {self.Fsm.GameObject.gameObject.name}, {self.Fsm.Name}"); + /// + /// Hook that is called when the AudioMixerSnapshot.TransitionTo is called from an TransitionToAudioSnapshot FSM + /// action. Used to network the starting of a audio snapshot for the scene host. + /// + /// The TransitionToAudioSnapshot FSM action responsible for the call. + private void OnTransitionToAudioSnapshot(TransitionToAudioSnapshot action) { + Logger.Debug($"OnTransitionToAudioSnapshot: {action.Fsm.GameObject.gameObject.name}, {action.Fsm.Name}"); + + if (action.Fsm.Name.Equals("Door Control")) { + Logger.Debug(" Was door control, allowing"); + return; + } if (IsControlled) { return; } - orig(self); - - var snapshot = self.snapshot.Value; + var snapshot = action.snapshot.Value; if (snapshot == null) { return; } @@ -191,7 +244,7 @@ public override void Update(EntityNetworkData data, bool alreadyInSceneUpdate) { var musicCueIndex = data.Packet.ReadByte(); var snapshotIndex = data.Packet.ReadByte(); - Logger.Debug($"Applying entity network data for music component with indices: {musicCueIndex}, {snapshotIndex}"); + Logger.Debug($"Applying entity network data for music component with indices: {musicCueIndex}, {snapshotIndex}"); if (musicCueIndex != _lastMusicCueIndex) { ApplyIndex(musicCueIndex); @@ -240,10 +293,14 @@ void ApplyIndex(byte index) { /// public override void Destroy() { - On.HutongGames.PlayMaker.Actions.ApplyMusicCue.OnEnter -= ApplyMusicCueOnEnter; - On.HutongGames.PlayMaker.Actions.TransitionToAudioSnapshot.OnEnter -= TransitionToAudioSnapshotOnEnter; + CustomHooks.ApplyMusicCueFromFsmAction -= OnApplyMusicCue; + CustomHooks.TransitionToAudioSnapshotFromFsmAction -= OnTransitionToAudioSnapshot; } + /// + /// Hook for when an FSM becomes enabled. Used to check for ApplyMusicCue or TransitionToAudioSnapshot actions + /// such that their audio data can be added to the lists of data. + /// private static void OnFsmEnable(On.PlayMakerFSM.orig_OnEnable orig, PlayMakerFSM self) { orig(self); @@ -283,7 +340,10 @@ out var snapshotData } } } - + + /// + /// Data for music cues, used for looking up the index or the music cue from index for networking purposes. + /// private class MusicCueData { public MusicCueType Type { get; set; } public string Name { get; set; } @@ -293,6 +353,10 @@ private class MusicCueData { public MusicCue MusicCue { get; set; } } + /// + /// Data for audio snapshots, used for looking up the index or the audio snapshot from index for networking + /// purposes. + /// private class AudioMixerSnapshotData { public AudioMixerSnapshotType Type { get; set; } public string Name { get; set; } @@ -302,6 +366,9 @@ private class AudioMixerSnapshotData { public AudioMixerSnapshot Snapshot { get; set; } } + /// + /// Enum for music cue types. + /// [JsonConverter(typeof(StringEnumConverter))] private enum MusicCueType { None, @@ -324,6 +391,9 @@ private enum MusicCueType { Waterways } + /// + /// Enum for audio snapshot types. + /// [JsonConverter(typeof(StringEnumConverter))] private enum AudioMixerSnapshotType { Silent, From 8efd6946ffc2f11e00640ee7682c4de4a60fc741 Mon Sep 17 00:00:00 2001 From: Extremelyd1 <10898310+Extremelyd1@users.noreply.github.com> Date: Thu, 12 Sep 2024 20:59:17 +0200 Subject: [PATCH 150/216] Fix errors with boss scene entity mismatch --- .../Client/Entity/Component/ColliderComponent.cs | 9 +++++++-- .../Entity/Component/HealthManagerComponent.cs | 13 +++++++++---- .../Entity/Component/MeshRendererComponent.cs | 10 ++++++++-- HKMP/Game/Client/Entity/Entity.cs | 14 ++++++++++---- 4 files changed, 34 insertions(+), 12 deletions(-) diff --git a/HKMP/Game/Client/Entity/Component/ColliderComponent.cs b/HKMP/Game/Client/Entity/Component/ColliderComponent.cs index ec18f45b..8a94b082 100644 --- a/HKMP/Game/Client/Entity/Component/ColliderComponent.cs +++ b/HKMP/Game/Client/Entity/Component/ColliderComponent.cs @@ -70,8 +70,13 @@ public override void Update(EntityNetworkData data, bool alreadyInSceneUpdate) { } var enabled = data.Packet.ReadBool(); - _collider.Host.enabled = enabled; - _collider.Client.enabled = enabled; + if (_collider.Host != null) { + _collider.Host.enabled = enabled; + } + + if (_collider.Client != null) { + _collider.Client.enabled = enabled; + } Logger.Info($" Enabled: {enabled}"); } diff --git a/HKMP/Game/Client/Entity/Component/HealthManagerComponent.cs b/HKMP/Game/Client/Entity/Component/HealthManagerComponent.cs index 52aa1c38..4dea2033 100644 --- a/HKMP/Game/Client/Entity/Component/HealthManagerComponent.cs +++ b/HKMP/Game/Client/Entity/Component/HealthManagerComponent.cs @@ -161,10 +161,15 @@ public override void Update(EntityNetworkData data, bool alreadyInSceneUpdate) { var newInvincible = data.Packet.ReadBool(); var newInvincibleFromDir = data.Packet.ReadByte(); - _healthManager.Host.IsInvincible = newInvincible; - _healthManager.Host.InvincibleFromDirection = newInvincibleFromDir; - _healthManager.Client.IsInvincible = newInvincible; - _healthManager.Client.InvincibleFromDirection = newInvincibleFromDir; + if (_healthManager.Host != null) { + _healthManager.Host.IsInvincible = newInvincible; + _healthManager.Host.InvincibleFromDirection = newInvincibleFromDir; + } + + if (_healthManager.Client != null) { + _healthManager.Client.IsInvincible = newInvincible; + _healthManager.Client.InvincibleFromDirection = newInvincibleFromDir; + } } } diff --git a/HKMP/Game/Client/Entity/Component/MeshRendererComponent.cs b/HKMP/Game/Client/Entity/Component/MeshRendererComponent.cs index 2043faf2..198c856e 100644 --- a/HKMP/Game/Client/Entity/Component/MeshRendererComponent.cs +++ b/HKMP/Game/Client/Entity/Component/MeshRendererComponent.cs @@ -65,8 +65,14 @@ public override void InitializeHost() { /// public override void Update(EntityNetworkData data, bool alreadyInSceneUpdate) { var enabled = data.Packet.ReadBool(); - _meshRenderer.Host.enabled = enabled; - _meshRenderer.Client.enabled = enabled; + + if (_meshRenderer.Host != null) { + _meshRenderer.Host.enabled = enabled; + } + + if (_meshRenderer.Client != null) { + _meshRenderer.Client.enabled = enabled; + } } /// diff --git a/HKMP/Game/Client/Entity/Entity.cs b/HKMP/Game/Client/Entity/Entity.cs index 9b1bfef7..4983f22b 100644 --- a/HKMP/Game/Client/Entity/Entity.cs +++ b/HKMP/Game/Client/Entity/Entity.cs @@ -1110,16 +1110,17 @@ public void MakeHost() { /// /// The new position. public void UpdatePosition(Vector2 position) { + if (Object.Client == null || Object.Host == null) { + Logger.Warn($"Cannot update position for entity ({Id}, {Type}), client or host object is null"); + return; + } + var unityPos = new Vector3( position.X, position.Y, _hasParent ? Object.Host.transform.localPosition.z : Object.Host.transform.position.z ); - if (Object.Client == null) { - return; - } - var positionInterpolation = Object.Client.GetComponent(); if (positionInterpolation == null) { return; @@ -1133,6 +1134,11 @@ public void UpdatePosition(Vector2 position) { /// /// The new scale data. public void UpdateScale(EntityUpdate.ScaleData scale) { + if (Object.Client == null) { + Logger.Warn($"Cannot update scale for entity ({Id}, {Type}), client object is null"); + return; + } + var transform = Object.Client.transform; var localScale = transform.localScale; From 20b55764445605fbeca0de8f86dc774e1d6ad661 Mon Sep 17 00:00:00 2001 From: Extremelyd1 <10898310+Extremelyd1@users.noreply.github.com> Date: Sun, 29 Sep 2024 16:58:03 +0200 Subject: [PATCH 151/216] Add command to copy saves, fix save sync issue --- HKMP/Game/Client/Save/SaveManager.cs | 32 +++------ HKMP/Game/Command/Server/CopySaveCommand.cs | 74 +++++++++++++++++++++ HKMP/Game/GameManager.cs | 3 +- HKMP/Game/Server/ModServerManager.cs | 24 +++++-- HKMP/Game/Server/ServerManager.cs | 4 +- HKMP/Resource/save-data.json | 15 +++++ HKMP/Resource/scene-data.json | 3 +- 7 files changed, 122 insertions(+), 33 deletions(-) create mode 100644 HKMP/Game/Command/Server/CopySaveCommand.cs diff --git a/HKMP/Game/Client/Save/SaveManager.cs b/HKMP/Game/Client/Save/SaveManager.cs index b2fed242..6673eda3 100644 --- a/HKMP/Game/Client/Save/SaveManager.cs +++ b/HKMP/Game/Client/Save/SaveManager.cs @@ -456,11 +456,6 @@ private void CheckSendSaveUpdate(string name, Func encodeFunc) { return; } - if (syncProps.SyncType == SaveDataMapping.SyncType.Player && IsHostingServer) { - Logger.Debug("Player specific save data, but player is hosting the server, not sending save update"); - return; - } - if (!SaveDataMapping.PlayerDataIndices.TryGetValue(name, out var index)) { Logger.Info($"Cannot find save data index, not sending save update ({name})"); return; @@ -532,11 +527,6 @@ private void OnUpdatePersistents() { continue; } - if (syncProps.SyncType == SaveDataMapping.SyncType.Player && IsHostingServer) { - Logger.Debug("Player specific save data, but player is hosting the server, not sending persistent int save update"); - continue; - } - if (!SaveDataMapping.PersistentIntIndices.TryGetValue(itemData, out var index)) { Logger.Info( $"Cannot find persistent int save data index, not sending save update ({itemData.Id}, {itemData.SceneName})"); @@ -582,11 +572,6 @@ private void OnUpdatePersistents() { continue; } - if (syncProps.SyncType == SaveDataMapping.SyncType.Player && IsHostingServer) { - Logger.Debug("Player specific save data, but player is hosting the server, not sending persistent bool save update"); - continue; - } - if (!SaveDataMapping.PersistentBoolIndices.TryGetValue(itemData, out var index)) { Logger.Info( $"Cannot find persistent bool save data index, not sending save update ({itemData.Id}, {itemData.SceneName})"); @@ -642,11 +627,6 @@ Func changeFunc continue; } - if (syncProps.SyncType == SaveDataMapping.SyncType.Player && IsHostingServer) { - Logger.Debug("Player specific save data, but player is hosting the server, not sending compound save update"); - return; - } - if (!SaveDataMapping.PlayerDataIndices.TryGetValue(varName, out var index)) { continue; } @@ -1007,11 +987,11 @@ bool CheckPlayerSpecificHosting(Dictionary - /// Get the current save data as a dictionary with mapped indices and encoded values. This only returns the - /// global save data for a server. E.g. broken walls, open doors, defeated bosses. + /// Get the current save data as a dictionary with mapped indices and encoded values. This returns the global save + /// data if the boolean is set. Otherwise, it returns the player save data. /// /// A dictionary with mapped indices and byte-encoded values. - public static Dictionary GetCurrentGlobalSaveData() { + public static Dictionary GetCurrentSaveData(bool server) { var pd = PlayerData.instance; var sd = SceneData.instance; @@ -1038,7 +1018,11 @@ Func valueFunc // Skip values that are not supposed to be synced, or ones that have the property that it is // server data. Since we will not require the hosting player's save data on the server. - if (!syncProps.Sync || syncProps.SyncType != SaveDataMapping.SyncType.Server) { + if (!syncProps.Sync) { + continue; + } + + if ((syncProps.SyncType == SaveDataMapping.SyncType.Server) != server) { continue; } } diff --git a/HKMP/Game/Command/Server/CopySaveCommand.cs b/HKMP/Game/Command/Server/CopySaveCommand.cs new file mode 100644 index 00000000..d10fbc50 --- /dev/null +++ b/HKMP/Game/Command/Server/CopySaveCommand.cs @@ -0,0 +1,74 @@ +using System; +using System.Collections.Generic; +using Hkmp.Api.Command.Server; +using Hkmp.Game.Server; +using Hkmp.Networking.Server; +using Hkmp.Util; + +namespace Hkmp.Game.Command.Server; + +/// +/// Command for allowing players to copy player-specific save data from another player. This is used to catch up +/// to another player's progression by transferring the save data. +/// +internal class CopySaveCommand : IServerCommand { + /// + public string Trigger => "/copysave"; + + /// + public string[] Aliases => Array.Empty(); + + /// + public bool AuthorizedOnly => true; + + /// + /// The server manager instance to access players. + /// + private readonly ServerManager _serverManager; + + /// + /// The server save data instance for accessing save data to copy. + /// + private readonly ServerSaveData _serverSaveData; + + /// + /// The net server instance for networking save data. + /// + private readonly NetServer _netServer; + + public CopySaveCommand(ServerManager serverManager, ServerSaveData serverSaveData, NetServer netServer) { + _serverManager = serverManager; + _serverSaveData = serverSaveData; + _netServer = netServer; + } + + /// + public void Execute(ICommandSender commandSender, string[] args) { + if (args.Length < 3) { + commandSender.SendMessage($"Invalid usage: {Trigger} "); + return; + } + + var fromUsername = args[1]; + if (!CommandUtil.TryGetPlayerByName(_serverManager.Players, fromUsername, out var fromPlayer)) { + commandSender.SendMessage($"Could not find player with name '{fromUsername}'"); + return; + } + + var toUsername = args[2]; + if (!CommandUtil.TryGetPlayerByName(_serverManager.Players, toUsername, out var toPlayer)) { + commandSender.SendMessage($"Could not find player with name '{toUsername}'"); + return; + } + + var toCopyData = new Dictionary(_serverSaveData.PlayerSaveData[fromPlayer.AuthKey]); + + _serverSaveData.PlayerSaveData[toPlayer.AuthKey] = toCopyData; + + foreach (var saveEntry in toCopyData) { + _netServer.GetUpdateManagerForClient(toPlayer.Id).SetSaveUpdate(saveEntry.Key, saveEntry.Value); + } + + commandSender.SendMessage($"Copied player save file from '{fromUsername}' to '{toUsername}'"); + } +} diff --git a/HKMP/Game/GameManager.cs b/HKMP/Game/GameManager.cs index 13f34cbc..4a3f0c8b 100644 --- a/HKMP/Game/GameManager.cs +++ b/HKMP/Game/GameManager.cs @@ -50,7 +50,8 @@ public GameManager(ModSettings modSettings) { netServer, serverServerSettings, packetManager, - uiManager + uiManager, + modSettings ); ServerManager.Initialize(); diff --git a/HKMP/Game/Server/ModServerManager.cs b/HKMP/Game/Server/ModServerManager.cs index 174dbc10..af8a4166 100644 --- a/HKMP/Game/Server/ModServerManager.cs +++ b/HKMP/Game/Server/ModServerManager.cs @@ -12,6 +12,12 @@ namespace Hkmp.Game.Server; /// Specialization of that adds handlers for the mod specific things. /// internal class ModServerManager : ServerManager { + /// + /// The mod settings instance for retrieving the auth key of the local player to set player save data when + /// hosting a server. + /// + private readonly ModSettings _modSettings; + /// /// Save data that was loaded from selecting a save file. Will be retroactively applied to a server, if one was /// requested to be started after selecting a save file. @@ -22,8 +28,11 @@ public ModServerManager( NetServer netServer, ServerSettings serverSettings, PacketManager packetManager, - UiManager uiManager + UiManager uiManager, + ModSettings modSettings ) : base(netServer, serverSettings, packetManager) { + _modSettings = modSettings; + // Start addon loading once all mods have finished loading ModHooks.FinishedLoadingModsHook += AddonManager.LoadAddons; @@ -37,15 +46,18 @@ UiManager uiManager private void OnRequestServerStartHost(int port) { // Get the global save data from the save manager, which obtains the global save data from the loaded - // save file that the user selected. Then we import the player save data from the (potentially) loaded - // modded save file from the user selected save file. - ServerSaveData = new ServerSaveData { - GlobalSaveData = SaveManager.GetCurrentGlobalSaveData() - }; + // save file that the user selected + ServerSaveData.GlobalSaveData = SaveManager.GetCurrentSaveData(true); + // Then we import the player save data from the (potentially) loaded modded save file from the user selected + // save file if (_loadedLocalSaveData != null) { ServerSaveData.PlayerSaveData = _loadedLocalSaveData.PlayerSaveData; } + + // Lastly, we get the player save data from the save manager, which obtains the player save data from the + // loaded save file that the user selected. We add this data to the server save as the local player + ServerSaveData.PlayerSaveData[_modSettings.AuthKey] = SaveManager.GetCurrentSaveData(false); Start(port); } diff --git a/HKMP/Game/Server/ServerManager.cs b/HKMP/Game/Server/ServerManager.cs index 67b7b18f..f7ebde28 100644 --- a/HKMP/Game/Server/ServerManager.cs +++ b/HKMP/Game/Server/ServerManager.cs @@ -76,7 +76,8 @@ internal abstract class ServerManager : IServerManager { protected readonly ServerAddonManager AddonManager; /// - /// The save data for the server. + /// The save data for the server. The instance will be created in the constructor and is passed around to other + /// objects. Therefore, it should not change instances. /// protected ServerSaveData ServerSaveData; @@ -185,6 +186,7 @@ protected virtual void RegisterCommands() { CommandManager.RegisterCommand(new KickCommand(this)); CommandManager.RegisterCommand(new TeamCommand(this)); CommandManager.RegisterCommand(new SkinCommand(this)); + CommandManager.RegisterCommand(new CopySaveCommand(this, ServerSaveData, _netServer)); } /// diff --git a/HKMP/Resource/save-data.json b/HKMP/Resource/save-data.json index 3e1fe733..eb368917 100644 --- a/HKMP/Resource/save-data.json +++ b/HKMP/Resource/save-data.json @@ -10,6 +10,11 @@ "SyncType": "Player", "IgnoreSceneHost": true }, + "heartPieceCollected": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, "heartPieceMax": { "Sync": true, "SyncType": "Player", @@ -30,6 +35,16 @@ "SyncType": "Player", "IgnoreSceneHost": true }, + "vesselFragmentCollected": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "MPReserveMax": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, "vesselFragmentMax": { "Sync": true, "SyncType": "Player", diff --git a/HKMP/Resource/scene-data.json b/HKMP/Resource/scene-data.json index 9ee703a5..1f26e5fe 100644 --- a/HKMP/Resource/scene-data.json +++ b/HKMP/Resource/scene-data.json @@ -618,5 +618,6 @@ "Nosk Hornet Boss Scene", "Radiance Boss Scene", "Oblobbles Boss Scene", - "God Tamer Boss Scene" + "God Tamer Boss Scene", + "None" ] From 2156130da2f2596a287fad08ecb0cd05edc02ee9 Mon Sep 17 00:00:00 2001 From: Extremelyd1 <10898310+Extremelyd1@users.noreply.github.com> Date: Fri, 4 Oct 2024 20:48:12 +0200 Subject: [PATCH 152/216] Fix issue with battle fight music --- HKMP/Resource/music-data.json | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/HKMP/Resource/music-data.json b/HKMP/Resource/music-data.json index 7418cd48..6521ee3e 100644 --- a/HKMP/Resource/music-data.json +++ b/HKMP/Resource/music-data.json @@ -31,7 +31,12 @@ { "Type": "GGHeavy", "Name": "GG Heavy" - },{ + }, + { + "Type": "EnemyBattle", + "Name": "EnemyBattle" + }, + { "Type": "DreamFight", "Name": "DreamFight" }, From ee149e3a6282d8f2250bac30d773434ab1235feb Mon Sep 17 00:00:00 2001 From: Extremelyd1 <10898310+Extremelyd1@users.noreply.github.com> Date: Fri, 4 Oct 2024 20:48:26 +0200 Subject: [PATCH 153/216] Fix issue with scene encoding for save data --- HKMP/Resource/scene-data.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/HKMP/Resource/scene-data.json b/HKMP/Resource/scene-data.json index 1f26e5fe..28e54ebf 100644 --- a/HKMP/Resource/scene-data.json +++ b/HKMP/Resource/scene-data.json @@ -1,4 +1,6 @@ [ + "", + "None", "Pre_Menu_Intro", "Menu_Title", "Quit_To_Menu", @@ -618,6 +620,5 @@ "Nosk Hornet Boss Scene", "Radiance Boss Scene", "Oblobbles Boss Scene", - "God Tamer Boss Scene", - "None" + "God Tamer Boss Scene" ] From fd1afa796d99ed2f6846cf851fa8715f67c23cb6 Mon Sep 17 00:00:00 2001 From: Extremelyd1 <10898310+Extremelyd1@users.noreply.github.com> Date: Tue, 15 Oct 2024 21:45:36 +0200 Subject: [PATCH 154/216] Fix issue with late action hook registration --- HKMP/Fsm/FsmPatcher.cs | 7 ++++++- HKMP/Game/Client/Entity/Entity.cs | 3 ++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/HKMP/Fsm/FsmPatcher.cs b/HKMP/Fsm/FsmPatcher.cs index 46aad80a..f68b1834 100644 --- a/HKMP/Fsm/FsmPatcher.cs +++ b/HKMP/Fsm/FsmPatcher.cs @@ -75,7 +75,12 @@ private void OnFsmEnable(On.PlayMakerFSM.orig_OnEnable orig, PlayMakerFSM self) // 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 { diff --git a/HKMP/Game/Client/Entity/Entity.cs b/HKMP/Game/Client/Entity/Entity.cs index 4983f22b..551bbbef 100644 --- a/HKMP/Game/Client/Entity/Entity.cs +++ b/HKMP/Game/Client/Entity/Entity.cs @@ -308,7 +308,8 @@ private void ProcessHostFsm(PlayMakerFSM fsm) { MonoBehaviourUtil.Instance.StartCoroutine(WaitForActionInitialization()); IEnumerator WaitForActionInitialization() { while (checkFunc.Invoke()) { - if (UnityEngine.SceneManagement.SceneManager.GetActiveScene().name != sceneName) { + if (UnityEngine.SceneManagement.SceneManager.GetActiveScene().name != + global::GameManager.GetBaseSceneName(sceneName)) { yield break; } From 593cb444a81203775f2875e8dd0ad9ca4658c622 Mon Sep 17 00:00:00 2001 From: Extremelyd1 <10898310+Extremelyd1@users.noreply.github.com> Date: Sun, 20 Oct 2024 12:31:13 +0200 Subject: [PATCH 155/216] Fix issue with Elder Baldur not spawning Baldurs --- HKMP/Game/Client/Entity/EntityManager.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/HKMP/Game/Client/Entity/EntityManager.cs b/HKMP/Game/Client/Entity/EntityManager.cs index 85de6149..1d22b1ff 100644 --- a/HKMP/Game/Client/Entity/EntityManager.cs +++ b/HKMP/Game/Client/Entity/EntityManager.cs @@ -481,6 +481,12 @@ private void OnFindGameObject(FindGameObject.orig_Find orig, HutongGames.PlayMak return; } + // Check for specific instances where we don't want to manually find the entity, because it messes + // with the logic of the FSM + if (self.State.Name.Equals("Can Roller?") && self.Fsm.Name.Equals("Blocker Control")) { + 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 From d37cb1f5cc01bfd8b23ac743975a6bfe33856c5f Mon Sep 17 00:00:00 2001 From: Extremelyd1 <10898310+Extremelyd1@users.noreply.github.com> Date: Sun, 20 Oct 2024 16:12:33 +0200 Subject: [PATCH 156/216] Patch chests to remove range check --- HKMP/Fsm/FsmPatcher.cs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/HKMP/Fsm/FsmPatcher.cs b/HKMP/Fsm/FsmPatcher.cs index f68b1834..1f534689 100644 --- a/HKMP/Fsm/FsmPatcher.cs +++ b/HKMP/Fsm/FsmPatcher.cs @@ -136,5 +136,17 @@ private void OnFsmEnable(On.PlayMakerFSM.orig_OnEnable orig, PlayMakerFSM self) 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; + } } } From 471a8aa35fb567fab9cd492a349add088bb65ff8 Mon Sep 17 00:00:00 2001 From: Extremelyd1 <10898310+Extremelyd1@users.noreply.github.com> Date: Sun, 20 Oct 2024 16:21:44 +0200 Subject: [PATCH 157/216] Sync Ancestral Mound gate --- HKMP/Game/Client/Save/SaveChanges.cs | 14 ++++++++++++++ HKMP/Resource/save-data.json | 3 ++- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/HKMP/Game/Client/Save/SaveChanges.cs b/HKMP/Game/Client/Save/SaveChanges.cs index c5d493d5..85450db8 100644 --- a/HKMP/Game/Client/Save/SaveChanges.cs +++ b/HKMP/Game/Client/Save/SaveChanges.cs @@ -352,5 +352,19 @@ public void ApplyPersistentValueSaveChange(PersistentItemData itemData) { fsm.SetState("Audio"); } + + if (itemData.Id == "Bone Gate" && itemData.SceneName == currentScene) { + var go = GameObject.Find(itemData.Id); + if (go == null) { + return; + } + + var fsm = go.LocateMyFSM("Bone Gate"); + if (fsm == null) { + return; + } + + fsm.SetState("Open Audio"); + } } } diff --git a/HKMP/Resource/save-data.json b/HKMP/Resource/save-data.json index eb368917..115d5bb5 100644 --- a/HKMP/Resource/save-data.json +++ b/HKMP/Resource/save-data.json @@ -8570,7 +8570,8 @@ }, "Value": { "Sync": true, - "SyncType": "Server" + "SyncType": "Server", + "IgnoreSceneHost": true } }, { From 773de9dc7538091b1840585c00f3f3930e24ef61 Mon Sep 17 00:00:00 2001 From: Extremelyd1 <10898310+Extremelyd1@users.noreply.github.com> Date: Mon, 21 Oct 2024 22:50:39 +0200 Subject: [PATCH 158/216] Potential fix for battle gate on host transfer --- HKMP/Game/Client/Entity/EntityInitializer.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/HKMP/Game/Client/Entity/EntityInitializer.cs b/HKMP/Game/Client/Entity/EntityInitializer.cs index f9e61682..f6801653 100644 --- a/HKMP/Game/Client/Entity/EntityInitializer.cs +++ b/HKMP/Game/Client/Entity/EntityInitializer.cs @@ -45,7 +45,8 @@ internal static class EntityInitializer { /// private static readonly Type[] ToSkipTypes = { typeof(Tk2dPlayAnimation), - typeof(ActivateAllChildren) + typeof(ActivateAllChildren), + typeof(SetCollider) // TODO: test whether this has effects on other entities during host transfer (this was added for battle gates) }; /// From a456aebc55949d8b3d5d14591037197c0708d238 Mon Sep 17 00:00:00 2001 From: Extremelyd1 <10898310+Extremelyd1@users.noreply.github.com> Date: Wed, 23 Oct 2024 19:07:09 +0200 Subject: [PATCH 159/216] Fix unusual behaviour of city elevators --- HKMP/Game/Client/Entity/Entity.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/HKMP/Game/Client/Entity/Entity.cs b/HKMP/Game/Client/Entity/Entity.cs index 551bbbef..10a7cc55 100644 --- a/HKMP/Game/Client/Entity/Entity.cs +++ b/HKMP/Game/Client/Entity/Entity.cs @@ -1025,9 +1025,9 @@ public void MakeHost() { // We need to set the isKinematic property of rigid bodies to ensure physics work again after enabling // the host object. In Hornet 1 this is necessary because another state sets this property normally in the // fight. See the "Wake" or "Refight Ready" state of the "Control" FSM on Hornet 1. - // In the Mantis Lord entity, this should never be disabled, since they are always kinematic. + // For the Mantis Lord and City Elevator entity, this should never be disabled, since they are always kinematic. var rigidBody = Object.Host.GetComponent(); - if (rigidBody != null && Type != EntityType.MantisLord) { + if (rigidBody != null && Type != EntityType.MantisLord && Type != EntityType.CityElevator) { Logger.Debug(" Resetting isKinematic of Rigidbody to ensure physics work for host object"); rigidBody.isKinematic = false; } From c9defaf10258b566268789c80ddc680588de1237 Mon Sep 17 00:00:00 2001 From: Extremelyd1 <10898310+Extremelyd1@users.noreply.github.com> Date: Sat, 26 Oct 2024 14:55:29 +0200 Subject: [PATCH 160/216] Fix soft-locks with toll machines, sync grimm lantern --- HKMP/Game/Client/Save/SaveChanges.cs | 200 ++++++++++++++++++++++++++- HKMP/Resource/save-data.json | 9 +- 2 files changed, 204 insertions(+), 5 deletions(-) diff --git a/HKMP/Game/Client/Save/SaveChanges.cs b/HKMP/Game/Client/Save/SaveChanges.cs index 85450db8..90c6bb23 100644 --- a/HKMP/Game/Client/Save/SaveChanges.cs +++ b/HKMP/Game/Client/Save/SaveChanges.cs @@ -1,5 +1,7 @@ +using System.Linq; using Hkmp.Util; using HutongGames.PlayMaker.Actions; +using Modding; using UnityEngine; using Logger = Hkmp.Logging.Logger; @@ -127,6 +129,26 @@ public void ApplyPlayerDataSaveChange(string name) { return; } + // Add additional actions from different states to ensure the player gets control back if they are already + // in the FSM flow + if (IsInTollMachineDialogue(fsm.Fsm.ActiveStateName)) { + var action1 = fsm.GetFirstAction("Yes"); + fsm.InsertAction("Box Disappear Anim", action1, 0); + var action2 = fsm.GetFirstAction("Yes"); + action2.animationCompleteEvent = null; + fsm.InsertAction("Box Disappear Anim", action2, 0); + var action3 = fsm.GetFirstAction("Yes"); + fsm.InsertAction("Box Disappear Anim", action3, 0); + var action4 = fsm.GetFirstAction("Pause Before Box Drop"); + fsm.InsertAction("Box Disappear Anim", action4, 0); + var action5 = fsm.GetFirstAction("Pause Before Box Drop"); + fsm.InsertAction("Box Disappear Anim", action5, 0); + var action6 = fsm.GetAction("Pause Before Box Drop", 2); + fsm.InsertAction("Box Disappear Anim", action6, 0); + + HideDialogueBox(); + } + fsm.SetState("Box Disappear Anim"); return; } @@ -146,6 +168,24 @@ public void ApplyPlayerDataSaveChange(string name) { return; } + // Add additional actions from different states to ensure the player gets control back if they are already + // in the FSM flow + if (IsInTollMachineDialogue(fsm.Fsm.ActiveStateName)) { + var action1 = fsm.GetFirstAction("Yes"); + action1.animationCompleteEvent = null; + fsm.InsertAction("Box Down", action1, 0); + var action2 = fsm.GetFirstAction("Yes"); + fsm.InsertAction("Box Down", action2, 0); + var action3 = fsm.GetFirstAction("Pause Before Box Drop"); + fsm.InsertAction("Box Down", action3, 0); + var action4 = fsm.GetFirstAction("Pause Before Box Drop"); + fsm.InsertAction("Box Down", action4, 0); + var action5 = fsm.GetAction("Pause Before Box Drop", 3); + fsm.InsertAction("Box Down", action5, 0); + + HideDialogueBox(); + } + fsm.SetState("Box Down"); return; } @@ -241,6 +281,19 @@ public void ApplyPlayerDataSaveChange(string name) { if (fsm == null) { return; } + + string[] inDialogueStateNames = [ + "Hero Anim", "Key?", "Box Up YN", "Send Text", "Box Up", "No Key" + ]; + if (inDialogueStateNames.Contains(fsm.Fsm.ActiveStateName)) { + var action1 = fsm.GetFirstAction("Yes"); + fsm.InsertAction("Activate", action1, 0); + + HideDialogueBox(); + } + + fsm.RemoveFirstAction("Activate"); + fsm.RemoveFirstAction("Activate"); fsm.SetState("Activate"); return; @@ -257,8 +310,19 @@ public void ApplyPlayerDataSaveChange(string name) { return; } + string[] inDialogueStateNames = [ + "Hero Anim", "Key?", "Box Up YN", "Send Text", "Box Up", "No Key" + ]; + if (inDialogueStateNames.Contains(fsm.Fsm.ActiveStateName)) { + var action1 = fsm.GetFirstAction("Yes"); + fsm.InsertAction("Activate", action1, 0); + + HideDialogueBox(); + } + fsm.RemoveFirstAction("Activate"); fsm.RemoveFirstAction("Activate"); + fsm.RemoveFirstAction("Activate"); fsm.SetState("Activate"); return; @@ -285,6 +349,55 @@ public void ApplyPlayerDataSaveChange(string name) { fsm.SetState("Glow"); return; } + + if (name == "openedMageDoor_v2" && currentScene == "Ruins1_31") { + var go = GameObject.Find("Mage Door"); + if (go == null) { + return; + } + + var fsm = go.LocateMyFSM("Conversation Control"); + if (fsm == null) { + return; + } + + string[] inDialogueStateNames = [ + "Hero Anim", "Check Key", "Box Up YN", "Send Text", "Box Up", "No Key" + ]; + if (inDialogueStateNames.Contains(fsm.Fsm.ActiveStateName)) { + HideDialogueBox(); + } else { + fsm.RemoveFirstAction("Yes"); + } + + fsm.RemoveFirstAction("Yes"); + + fsm.SetState("Yes"); + return; + } + + if (name == "cityLift1" && currentScene == "Crossroads_49b") { + var go = GameObject.Find("Toll Machine Lift"); + var fsm = go.LocateMyFSM("Toll Machine"); + + // Hide the dialogue box if the local player is in the dialogue flow + if (IsInTollMachineDialogue(fsm.Fsm.ActiveStateName)) { + HideDialogueBox(); + } + + fsm.RemoveFirstAction("Send Message"); + + fsm.SetState("Yes"); + return; + } + + if (name == "nightmareLanternAppeared" && currentScene == "Cliffs_06") { + var go = GameObject.Find("Sycophant Dream"); + var fsm = go.LocateMyFSM("Activate Lantern"); + + fsm.SetState("Impact"); + return; + } } /// @@ -305,9 +418,27 @@ public void ApplyPersistentValueSaveChange(PersistentItemData itemData) { )) { var go = GameObject.Find("Toll Gate Machine"); var fsm = go.LocateMyFSM("Toll Machine"); - + + // Add additional actions from different states to ensure the player gets control back if they are already + // in the FSM flow + if (IsInTollMachineDialogue(fsm.Fsm.ActiveStateName)) { + var action1 = fsm.GetFirstAction("Yes"); + action1.animationCompleteEvent = null; + fsm.InsertAction("Box Disappear Anim", action1, 0); + var action2 = fsm.GetFirstAction("Yes"); + fsm.InsertAction("Box Disappear Anim", action2, 0); + var action3 = fsm.GetFirstAction("Pause Before Box Drop"); + fsm.InsertAction("Box Disappear Anim", action3, 0); + var action4 = fsm.GetFirstAction("Pause Before Box Drop"); + fsm.InsertAction("Box Disappear Anim", action4, 0); + var action5 = fsm.GetAction("Pause Before Box Drop", 2); + fsm.InsertAction("Box Disappear Anim", action5, 0); + + HideDialogueBox(); + } + fsm.RemoveFirstAction("Open Gates"); - + fsm.SetState("Box Disappear Anim"); return; } @@ -367,4 +498,69 @@ public void ApplyPersistentValueSaveChange(PersistentItemData itemData) { fsm.SetState("Open Audio"); } } + + /// + /// Whether the local player is currently in a toll machine dialogue prompt that has claimed control of the + /// character. + /// + /// The name of the current state of the dialogue FSM. + /// true if the player is in dialogue, false otherwise. + private bool IsInTollMachineDialogue(string currentStateName) { + string[] outOfDialogueStateNames = [ + "Out Of Range", "In Range", "Can Inspect?", "Cancel Frame", "Pause", "Activated?", "Paid?", "Get Price", "Init", + "Regain Control" + ]; + + return !outOfDialogueStateNames.Contains(currentStateName); + } + + /// + /// Hide the currently active dialogue box by setting the state of the 'Dialogue Page Control' FSM of the 'Text YN' + /// game object. Needs to be amended if this method should also hide dialogue boxes of other dialogue types. + /// + private void HideDialogueBox() { + var gc = GameCameras.instance; + if (gc == null) { + Logger.Warn("Could not find GameCameras instance"); + return; + } + + var hudCamera = gc.hudCamera; + if (hudCamera == null) { + Logger.Warn("Could not find hudCamera"); + return; + } + + var dialogManager = hudCamera.gameObject.FindGameObjectInChildren("DialogueManager"); + if (dialogManager == null) { + Logger.Warn("Could not find dialogueManager"); + return; + } + + void HideDialogueObject(string objectName, string heroDmgState) { + var obj = dialogManager.FindGameObjectInChildren(objectName); + if (obj != null) { + var dialogueBox = obj.GetComponent(); + if (dialogueBox == null) { + Logger.Warn($"Could not find {objectName} DialogueBox"); + return; + } + + var hidden = ReflectionHelper.GetField(dialogueBox, "hidden"); + if (hidden) { + return; + } + + var pageControlFsm = obj.LocateMyFSM("Dialogue Page Control"); + if (pageControlFsm == null) { + Logger.Warn($"Could not find {objectName} DialoguePageControl FSM"); + return; + } + pageControlFsm.SetState(heroDmgState); + } + } + + HideDialogueObject("Text YN", "Hero Damaged"); + HideDialogueObject("Text", "Pause"); + } } diff --git a/HKMP/Resource/save-data.json b/HKMP/Resource/save-data.json index 115d5bb5..6550d088 100644 --- a/HKMP/Resource/save-data.json +++ b/HKMP/Resource/save-data.json @@ -4924,7 +4924,8 @@ }, "cityLift1": { "Sync": true, - "SyncType": "Server" + "SyncType": "Server", + "IgnoreSceneHost": true }, "cityLift1_isUp": { "Sync": true, @@ -4940,7 +4941,8 @@ }, "openedMageDoor_v2": { "Sync": true, - "SyncType": "Server" + "SyncType": "Server", + "IgnoreSceneHost": true }, "brokenMageWindow": { "Sync": true, @@ -5226,7 +5228,8 @@ }, "nightmareLanternAppeared": { "Sync": true, - "SyncType": "Server" + "SyncType": "Server", + "IgnoreSceneHost": true }, "nightmareLanternLit": { "Sync": true, From ad5d9663fb1b7a519398ec2421b65e8c31e8d2d4 Mon Sep 17 00:00:00 2001 From: Extremelyd1 <10898310+Extremelyd1@users.noreply.github.com> Date: Sun, 27 Oct 2024 12:11:34 +0100 Subject: [PATCH 161/216] Fix equipped charms not syncing --- HKMP/Game/Client/Save/SaveDataMapping.cs | 6 + HKMP/Game/Client/Save/SaveManager.cs | 86 +++-- HKMP/Resource/save-data.json | 428 +++++++++++++++++++++++ 3 files changed, 485 insertions(+), 35 deletions(-) diff --git a/HKMP/Game/Client/Save/SaveDataMapping.cs b/HKMP/Game/Client/Save/SaveDataMapping.cs index 3b0ff57a..a0305a75 100644 --- a/HKMP/Game/Client/Save/SaveDataMapping.cs +++ b/HKMP/Game/Client/Save/SaveDataMapping.cs @@ -131,6 +131,12 @@ public static SaveDataMapping Instance { /// [JsonProperty("vectorListVariables")] public readonly List VectorListVariables; + + /// + /// Deserialized list of strings that represent variable names with the type of integer list. + /// + [JsonProperty("intListVariables")] + public readonly List IntListVariables; /// /// Initializes the class by converting the deserialized data fields into the various dictionaries and lookups. diff --git a/HKMP/Game/Client/Save/SaveManager.cs b/HKMP/Game/Client/Save/SaveManager.cs index 6673eda3..eb1499f7 100644 --- a/HKMP/Game/Client/Save/SaveManager.cs +++ b/HKMP/Game/Client/Save/SaveManager.cs @@ -51,11 +51,6 @@ internal class SaveManager { /// private readonly List _persistentFsmData; - /// - /// Dictionary of hash codes for string list variables in the PlayerData for comparing changes against. - /// - private readonly Dictionary _stringListHashes; - /// /// Dictionary of BossSequenceDoor.Completion structs in the PlayerData for comparing changes against. /// @@ -67,9 +62,9 @@ internal class SaveManager { private readonly Dictionary _bsCompHashes; /// - /// Dictionary of hash codes for vector list variables in the PlayerData for comparing changes against. + /// Dictionary of hash codes for list variables in the PlayerData for comparing changes against. /// - private readonly Dictionary _vectorListHashes; + private readonly Dictionary _listHashes; /// /// List of FieldInfo for fields in PlayerData that are simple values that should be synced. Used for looping @@ -95,10 +90,9 @@ public SaveManager(NetClient netClient, PacketManager packetManager, EntityManag _saveChanges = new SaveChanges(); _persistentFsmData = new List(); - _stringListHashes = new Dictionary(); _bsdCompHashes = new Dictionary(); _bsCompHashes = new Dictionary(); - _vectorListHashes = new Dictionary(); + _listHashes = new Dictionary(); _playerDataSyncFields = new List(); } @@ -308,6 +302,25 @@ byte[] EncodeVector3(Vector3 vec3Value) { return [(byte) mapZone]; } + if (value is List intListValue) { + if (intListValue.Count > byte.MaxValue) { + throw new ArgumentOutOfRangeException($"Could not encode int list length: {intListValue.Count}"); + } + + var length = (byte) intListValue.Count; + + // Create a byte array for the encoded result that has the size of the length of the int list plus one + // for the length itself + var byteArray = new byte[length + 1]; + byteArray[0] = length; + + for (var i = 0; i < length; i++) { + byteArray[i + 1] = (byte) intListValue[i]; + } + + return byteArray; + } + throw new ArgumentException($"No encoding implementation for type: {value.GetType()}"); } @@ -642,8 +655,8 @@ Func changeFunc CheckUpdates, int>( SaveDataMapping.StringListVariables, - _stringListHashes, - GetStringListHashCode, + _listHashes, + GetListHashCode, (hash1, hash2) => hash1 != hash2 ); @@ -679,8 +692,15 @@ Func changeFunc CheckUpdates, int>( SaveDataMapping.VectorListVariables, - _vectorListHashes, - GetVectorListHashCode, + _listHashes, + GetListHashCode, + (hash1, hash2) => hash1 != hash2 + ); + + CheckUpdates, int>( + SaveDataMapping.IntListVariables, + _listHashes, + GetListHashCode, (hash1, hash2) => hash1 != hash2 ); } @@ -800,7 +820,7 @@ private void UpdateSaveWithData(ushort index, byte[] encodedValue) { } // First set the new string list hash, so we don't trigger an update and subsequently a feedback loop - _stringListHashes[name] = GetStringListHashCode(list); + _listHashes[name] = GetListHashCode(list); pd.SetVariableInternal(name, list); } else if (type == typeof(BossSequenceDoor.Completion)) { @@ -861,7 +881,7 @@ private void UpdateSaveWithData(ushort index, byte[] encodedValue) { } // First set the new string list hash, so we don't trigger an update and subsequently a feedback loop - _vectorListHashes[name] = GetVectorListHashCode(list); + _listHashes[name] = GetListHashCode(list); pd.SetVariableInternal(name, list); } else if (type == typeof(MapZone)) { @@ -871,6 +891,18 @@ private void UpdateSaveWithData(ushort index, byte[] encodedValue) { _lastPlayerData?.SetVariableInternal(name, (MapZone) encodedValue[0]); pd.SetVariableInternal(name, (MapZone) encodedValue[0]); + } else if (type == typeof(List)) { + var length = encodedValue[0]; + + var list = new List(); + for (var i = 0; i < length; i++) { + list.Add(encodedValue[i + 1]); + } + + // First set the new string list hash, so we don't trigger an update and subsequently a feedback loop + _listHashes[name] = GetListHashCode(list); + + pd.SetVariableInternal(name, list); } else { throw new ArgumentException($"Could not decode type: {type}"); } @@ -1082,28 +1114,12 @@ Func valueFunc } /// - /// Get the hash code of the combined values in a string list. - /// - /// The list of strings to calculate the hash code for. - /// 0 if the list is empty, otherwise a hash code matching the specific order of strings in the list. - /// - private static int GetStringListHashCode(List list) { - if (list.Count == 0) { - return 0; - } - - return list - .Select(item => item.GetHashCode()) - .Aggregate((total, nextCode) => total ^ nextCode); - } - - /// - /// Get the hash code of the combined values in a Vector3 list. + /// Get the hash code of the combined values in a list. /// - /// The list of Vector3 to calculate the hash code for. - /// 0 if the list is empty, otherwise a hash code matching the specific order of Vector3 in the list. + /// The list to calculate the hash code for. + /// 0 if the list is empty, otherwise a hash code matching the specific order of values in the list. /// - private static int GetVectorListHashCode(List list) { + private static int GetListHashCode(List list) { if (list.Count == 0) { return 0; } diff --git a/HKMP/Resource/save-data.json b/HKMP/Resource/save-data.json index 6550d088..dade44ff 100644 --- a/HKMP/Resource/save-data.json +++ b/HKMP/Resource/save-data.json @@ -1949,216 +1949,641 @@ "SyncType": "Player", "IgnoreSceneHost": true }, + "charmSlotsFilled": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, "hasCharm": { "Sync": true, "SyncType": "Player", "IgnoreSceneHost": true }, + "equippedCharms": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "charmBenchMsg": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, "charmsOwned": { "Sync": true, "SyncType": "Player", "IgnoreSceneHost": true }, + "canOvercharm": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "overcharmed": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, "gotCharm_1": { "Sync": true, "SyncType": "Player", "IgnoreSceneHost": true }, + "equippedCharm_1": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "newCharm_1": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, "gotCharm_2": { "Sync": true, "SyncType": "Player", "IgnoreSceneHost": true }, + "equippedCharm_2": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "newCharm_2": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, "gotCharm_3": { "Sync": true, "SyncType": "Player", "IgnoreSceneHost": true }, + "equippedCharm_3": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "newCharm_3": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, "gotCharm_4": { "Sync": true, "SyncType": "Player", "IgnoreSceneHost": true }, + "equippedCharm_4": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "newCharm_4": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, "gotCharm_5": { "Sync": true, "SyncType": "Player", "IgnoreSceneHost": true }, + "equippedCharm_5": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "newCharm_5": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, "gotCharm_6": { "Sync": true, "SyncType": "Player", "IgnoreSceneHost": true }, + "equippedCharm_6": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "newCharm_6": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, "gotCharm_7": { "Sync": true, "SyncType": "Player", "IgnoreSceneHost": true }, + "equippedCharm_7": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "newCharm_7": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, "gotCharm_8": { "Sync": true, "SyncType": "Player", "IgnoreSceneHost": true }, + "equippedCharm_8": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "newCharm_8": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, "gotCharm_9": { "Sync": true, "SyncType": "Player", "IgnoreSceneHost": true }, + "equippedCharm_9": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "newCharm_9": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, "gotCharm_10": { "Sync": true, "SyncType": "Player", "IgnoreSceneHost": true }, + "equippedCharm_10": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "newCharm_10": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, "gotCharm_11": { "Sync": true, "SyncType": "Player", "IgnoreSceneHost": true }, + "equippedCharm_11": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "newCharm_11": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, "gotCharm_12": { "Sync": true, "SyncType": "Player", "IgnoreSceneHost": true }, + "equippedCharm_12": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "newCharm_12": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, "gotCharm_13": { "Sync": true, "SyncType": "Player", "IgnoreSceneHost": true }, + "equippedCharm_13": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "newCharm_13": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, "gotCharm_14": { "Sync": true, "SyncType": "Player", "IgnoreSceneHost": true }, + "equippedCharm_14": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "newCharm_14": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, "gotCharm_15": { "Sync": true, "SyncType": "Player", "IgnoreSceneHost": true }, + "equippedCharm_15": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "newCharm_15": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, "gotCharm_16": { "Sync": true, "SyncType": "Player", "IgnoreSceneHost": true }, + "equippedCharm_16": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "newCharm_16": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, "gotCharm_17": { "Sync": true, "SyncType": "Player", "IgnoreSceneHost": true }, + "equippedCharm_17": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "newCharm_17": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, "gotCharm_18": { "Sync": true, "SyncType": "Player", "IgnoreSceneHost": true }, + "equippedCharm_18": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "newCharm_18": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, "gotCharm_19": { "Sync": true, "SyncType": "Player", "IgnoreSceneHost": true }, + "equippedCharm_19": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "newCharm_19": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, "gotCharm_20": { "Sync": true, "SyncType": "Player", "IgnoreSceneHost": true }, + "equippedCharm_20": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "newCharm_20": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, "gotCharm_21": { "Sync": true, "SyncType": "Player", "IgnoreSceneHost": true }, + "equippedCharm_21": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "newCharm_21": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, "gotCharm_22": { "Sync": true, "SyncType": "Player", "IgnoreSceneHost": true }, + "equippedCharm_22": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "newCharm_22": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, "gotCharm_23": { "Sync": true, "SyncType": "Player", "IgnoreSceneHost": true }, + "equippedCharm_23": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "newCharm_23": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, "gotCharm_24": { "Sync": true, "SyncType": "Player", "IgnoreSceneHost": true }, + "equippedCharm_24": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "newCharm_24": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, "gotCharm_25": { "Sync": true, "SyncType": "Player", "IgnoreSceneHost": true }, + "equippedCharm_25": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "newCharm_25": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, "gotCharm_26": { "Sync": true, "SyncType": "Player", "IgnoreSceneHost": true }, + "equippedCharm_26": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "newCharm_26": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, "gotCharm_27": { "Sync": true, "SyncType": "Player", "IgnoreSceneHost": true }, + "equippedCharm_27": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "newCharm_27": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, "gotCharm_28": { "Sync": true, "SyncType": "Player", "IgnoreSceneHost": true }, + "equippedCharm_28": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "newCharm_28": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, "gotCharm_29": { "Sync": true, "SyncType": "Player", "IgnoreSceneHost": true }, + "equippedCharm_29": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "newCharm_29": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, "gotCharm_30": { "Sync": true, "SyncType": "Player", "IgnoreSceneHost": true }, + "equippedCharm_30": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "newCharm_30": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, "gotCharm_31": { "Sync": true, "SyncType": "Player", "IgnoreSceneHost": true }, + "equippedCharm_31": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "newCharm_31": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, "gotCharm_32": { "Sync": true, "SyncType": "Player", "IgnoreSceneHost": true }, + "equippedCharm_32": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "newCharm_32": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, "gotCharm_33": { "Sync": true, "SyncType": "Player", "IgnoreSceneHost": true }, + "equippedCharm_33": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "newCharm_33": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, "gotCharm_34": { "Sync": true, "SyncType": "Player", "IgnoreSceneHost": true }, + "equippedCharm_34": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "newCharm_34": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, "gotCharm_35": { "Sync": true, "SyncType": "Player", "IgnoreSceneHost": true }, + "equippedCharm_35": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "newCharm_35": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, "gotCharm_36": { "Sync": true, "SyncType": "Player", "IgnoreSceneHost": true }, + "equippedCharm_36": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "newCharm_36": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, "gotCharm_37": { "Sync": true, "SyncType": "Player", "IgnoreSceneHost": true }, + "equippedCharm_37": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "newCharm_37": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, "gotCharm_38": { "Sync": true, "SyncType": "Player", "IgnoreSceneHost": true }, + "equippedCharm_38": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "newCharm_38": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, "gotCharm_39": { "Sync": true, "SyncType": "Player", "IgnoreSceneHost": true }, + "equippedCharm_39": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "newCharm_39": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, "gotCharm_40": { "Sync": true, "SyncType": "Player", "IgnoreSceneHost": true }, + "equippedCharm_40": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, + "newCharm_40": { + "Sync": true, + "SyncType": "Player", + "IgnoreSceneHost": true + }, "fragileHealth_unbreakable": { "Sync": true, "SyncType": "Player", @@ -31510,5 +31935,8 @@ "placedMarkers_b", "placedMarkers_y", "placedMarkers_w" + ], + "intListVariables": [ + "equippedCharms" ] } From 1cdd467379a67a9810e417ee885355adb0ee4071 Mon Sep 17 00:00:00 2001 From: Extremelyd1 <10898310+Extremelyd1@users.noreply.github.com> Date: Mon, 11 Nov 2024 14:02:57 +0100 Subject: [PATCH 162/216] Properly dispose of enumerator --- HKMP/Networking/Packet/AddonPacketData.cs | 4 ++-- HKMP/Networking/Packet/UpdatePacket.cs | 20 +++++++++++--------- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/HKMP/Networking/Packet/AddonPacketData.cs b/HKMP/Networking/Packet/AddonPacketData.cs index 6d05fbdf..5a97f095 100644 --- a/HKMP/Networking/Packet/AddonPacketData.cs +++ b/HKMP/Networking/Packet/AddonPacketData.cs @@ -20,7 +20,7 @@ internal class AddonPacketData { /// /// Enumerator to go over each packet ID in the packet ID space of this addon. /// - public IEnumerator PacketIdEnumerator { + public IEnumerable PacketIdEnumerable { get { if (_packetIdArray == null) { // Create an array containing all possible IDs for this addon @@ -31,7 +31,7 @@ public IEnumerator PacketIdEnumerator { } // Return a fresh enumerator for the ID space - return ((IEnumerable) _packetIdArray).GetEnumerator(); + return _packetIdArray; } } diff --git a/HKMP/Networking/Packet/UpdatePacket.cs b/HKMP/Networking/Packet/UpdatePacket.cs index 773bc58d..5e12a5c1 100644 --- a/HKMP/Networking/Packet/UpdatePacket.cs +++ b/HKMP/Networking/Packet/UpdatePacket.cs @@ -147,13 +147,12 @@ private bool WritePacketData( Dictionary packetData ) { var enumValues = (T[]) Enum.GetValues(typeof(T)); - var enumerator = ((IEnumerable) enumValues).GetEnumerator(); var packetIdSize = (byte) enumValues.Length; return WritePacketData( packet, packetData, - enumerator, + enumValues, packetIdSize ); } @@ -170,7 +169,7 @@ AddonPacketData addonPacketData ) => WritePacketData( packet, addonPacketData.PacketData, - addonPacketData.PacketIdEnumerator, + addonPacketData.PacketIdEnumerable, addonPacketData.PacketIdSize ); @@ -179,14 +178,14 @@ AddonPacketData addonPacketData /// /// The packet to write into. /// The dictionary containing packet data to write in the packet. - /// An enumerator that enumerates over all possible keys in the dictionary. + /// An enumerator that enumerates over all possible keys in the dictionary. /// The exact size of the key space. /// Dictionary key parameter and enumerator parameter. /// true if any of the data written was reliable; otherwise false. private bool WritePacketData( Packet packet, Dictionary packetData, - IEnumerator keyEnumerator, + IEnumerable keyEnumerable, byte keySpaceSize ) { // Keep track of the bit flag in an unsigned long, which is the largest integer implicit type allowed @@ -194,6 +193,7 @@ byte keySpaceSize // Also keep track of the value of the current bit in an unsigned long ulong currentTypeValue = 1; + var keyEnumerator = keyEnumerable.GetEnumerator(); while (keyEnumerator.MoveNext()) { var key = keyEnumerator.Current; @@ -235,6 +235,8 @@ byte keySpaceSize } } } + + keyEnumerator.Dispose(); return containsReliableData; } @@ -282,7 +284,7 @@ Dictionary addonDataDict // If the addon data writing throws an exception, we skip it entirely and since we // wrote it in a separate packet, it has no impact on the regular packet Logger.Debug($"Addon with ID {addonId} has thrown an exception while writing addon packet data:\n{e}"); - // We decrease the count of addon packet datas we write, so we know how many are actually in + // We decrease the count of addon packet data's we write, so we know how many are actually in // final packet addonPacketDataCount--; continue; @@ -420,7 +422,7 @@ Dictionary packetData /// /// The raw packet instance to read from. /// The dictionary for all addon data to write the read data into. - /// Thrown if the any part of reading the data throws. + /// Thrown if any part of reading the data throws. private void ReadAddonDataDict( Packet packet, Dictionary addonDataDict @@ -480,7 +482,7 @@ public Packet CreatePacket() { // contains reliable data now _containsReliableData = WritePacketData(packet, _normalPacketData); - // Put the length of the resend data as a ushort in the packet + // Put the length of the resend data as an ushort in the packet var resendLength = (ushort) _resendPacketData.Count; if (_resendPacketData.Count > ushort.MaxValue) { resendLength = ushort.MaxValue; @@ -510,7 +512,7 @@ public Packet CreatePacket() { _containsReliableData |= WriteAddonDataDict(packet, _addonPacketData); - // Put the length of the addon resend data as a ushort in the packet + // Put the length of the addon resend data as an ushort in the packet resendLength = (ushort) _resendAddonPacketData.Count; if (_resendAddonPacketData.Count > ushort.MaxValue) { resendLength = ushort.MaxValue; From 941e130edd041b38ce596adb869fc03d771636bb Mon Sep 17 00:00:00 2001 From: Extremelyd1 <10898310+Extremelyd1@users.noreply.github.com> Date: Mon, 25 Nov 2024 21:28:26 +0100 Subject: [PATCH 163/216] Fix rare save issue with shade position --- HKMP/Game/Client/Save/SaveManager.cs | 6 +- HKMP/HKMP.csproj | 2 +- .../{scene-data.json => string-data.json} | 78 +++++++++++++------ HKMP/Util/EncodeUtil.cs | 40 +++++----- 4 files changed, 77 insertions(+), 49 deletions(-) rename HKMP/Resource/{scene-data.json => string-data.json} (95%) diff --git a/HKMP/Game/Client/Save/SaveManager.cs b/HKMP/Game/Client/Save/SaveManager.cs index eb1499f7..d584dadc 100644 --- a/HKMP/Game/Client/Save/SaveManager.cs +++ b/HKMP/Game/Client/Save/SaveManager.cs @@ -203,7 +203,7 @@ private void OnUpdatePlayerData() { private static byte[] EncodeValue(object value) { // Since all strings in the save data are scene names (or map scene names), we can convert them to indices byte[] EncodeString(string stringValue) { - if (!EncodeUtil.GetSceneIndex(stringValue, out var index)) { + if (!EncodeUtil.GetStringIndex(stringValue, out var index)) { // Logger.Info($"Could not encode string value: {stringValue}"); // return Array.Empty(); throw new Exception($"Could not encode string value: {stringValue}"); @@ -812,7 +812,7 @@ private void UpdateSaveWithData(ushort index, byte[] encodedValue) { for (var i = 0; i < length; i++) { var sceneIndex = BitConverter.ToUInt16(encodedValue, 2 + i * 2); - if (!EncodeUtil.GetSceneName(sceneIndex, out var sceneName)) { + if (!EncodeUtil.GetStringName(sceneIndex, out var sceneName)) { throw new Exception($"Could not decode string in list from save update: {sceneIndex}"); } @@ -991,7 +991,7 @@ private void UpdateSaveWithData(ushort index, byte[] encodedValue) { string DecodeString(byte[] encoded, int startIndex) { var sceneIndex = BitConverter.ToUInt16(encoded, startIndex); - if (!EncodeUtil.GetSceneName(sceneIndex, out var value)) { + if (!EncodeUtil.GetStringName(sceneIndex, out var value)) { throw new Exception($"Could not decode string from save update: {encodedValue}"); } diff --git a/HKMP/HKMP.csproj b/HKMP/HKMP.csproj index dc66cd49..fe8c8da2 100644 --- a/HKMP/HKMP.csproj +++ b/HKMP/HKMP.csproj @@ -22,7 +22,7 @@ - + diff --git a/HKMP/Resource/scene-data.json b/HKMP/Resource/string-data.json similarity index 95% rename from HKMP/Resource/scene-data.json rename to HKMP/Resource/string-data.json index 28e54ebf..00633715 100644 --- a/HKMP/Resource/scene-data.json +++ b/HKMP/Resource/string-data.json @@ -554,34 +554,10 @@ "Intro_Cutscene", "Dream_NailCollection", "RestBench", - "TOWN", - "CLIFFS", - "CROSSROADS", "BoneBench", - "SHAMAN_TEMPLE", - "FINAL_BOSS", - "GREEN_PATH", - "FOG_CANYON", - "QUEENS_STATION", - "WASTES", - "CITY", - "WATERWAYS", - "GODS_GLORY", "RestBench (1)", - "DEEPNEST", "RestBench Return", - "BEASTS_DEN", - "ABYSS", - "OUTSKIRTS", - "COLOSSEUM", - "HIVE", - "MINES", - "RESTING_GROUNDS", - "ROYAL_GARDENS", "WhiteBench", - "WHITE_PALACE", - "TRAM_UPPER", - "TRAM_LOWER", "Death Respawn Marker", "Gruz Boss Scene", "False Knight Boss Scene", @@ -620,5 +596,57 @@ "Nosk Hornet Boss Scene", "Radiance Boss Scene", "Oblobbles Boss Scene", - "God Tamer Boss Scene" + "God Tamer Boss Scene", + "NONE", + "TEST_AREA", + "KINGS_PASS", + "CLIFFS", + "TOWN", + "CROSSROADS", + "GREEN_PATH", + "ROYAL_GARDENS", + "FOG_CANYON", + "WASTES", + "DEEPNEST", + "HIVE", + "BONE_FOREST", + "PALACE_GROUNDS", + "MINES", + "RESTING_GROUNDS", + "CITY", + "DREAM_WORLD", + "COLOSSEUM", + "ABYSS", + "ROYAL_QUARTER", + "WHITE_PALACE", + "SHAMAN_TEMPLE", + "WATERWAYS", + "QUEENS_STATION", + "OUTSKIRTS", + "KINGS_STATION", + "MAGE_TOWER", + "TRAM_UPPER", + "TRAM_LOWER", + "FINAL_BOSS", + "SOUL_SOCIETY", + "ACID_LAKE", + "NOEYES_TEMPLE", + "MONOMON_ARCHIVE", + "MANTIS_VILLAGE", + "RUINED_TRAMWAY", + "DISTANT_VILLAGE", + "ABYSS_DEEP", + "ISMAS_GROVE", + "WYRMSKIN", + "LURIENS_TOWER", + "LOVE_TOWER", + "GLADE", + "BLUE_LAKE", + "PEAK", + "JONI_GRAVE", + "OVERGROWN_MOUND", + "CRYSTAL_MOUND", + "BEASTS_DEN", + "GODS_GLORY", + "GODSEEKER_WASTE" ] diff --git a/HKMP/Util/EncodeUtil.cs b/HKMP/Util/EncodeUtil.cs index 58b0479d..b41dbbde 100644 --- a/HKMP/Util/EncodeUtil.cs +++ b/HKMP/Util/EncodeUtil.cs @@ -8,25 +8,25 @@ namespace Hkmp.Util; /// public static class EncodeUtil { /// - /// The file path of the embedded resource file for scene data. + /// The file path of the embedded resource file for string data. /// - private const string SceneDataFilePath = "Hkmp.Resource.scene-data.json"; + private const string StringDataFilePath = "Hkmp.Resource.string-data.json"; /// - /// Bi-directional lookup that maps scene names to their indices. + /// Bi-directional lookup that maps strings (for encoding) to their indices. /// - private static readonly BiLookup SceneIndices; + private static readonly BiLookup StringIndices; /// /// Static construct to load the scene indices. /// static EncodeUtil() { - SceneIndices = new BiLookup(); + StringIndices = new BiLookup(); - var sceneNames = FileUtil.LoadObjectFromEmbeddedJson>(SceneDataFilePath); + var strings = FileUtil.LoadObjectFromEmbeddedJson>(StringDataFilePath); ushort index = 0; - foreach (var sceneName in sceneNames) { - SceneIndices.Add(sceneName, index++); + foreach (var str in strings) { + StringIndices.Add(str, index++); } } @@ -61,22 +61,22 @@ public static bool[] GetBoolsFromByte(byte b) { } /// - /// Try to get the scene index corresponding to the given scene name for encoding/decoding purposes. + /// Try to get the string index corresponding to the given string for encoding/decoding purposes. /// - /// The name of the scene. - /// The index of the scene or default if the scene name could not be found. - /// true if there is a corresponding index for the given scene name, false otherwise. - public static bool GetSceneIndex(string sceneName, out ushort index) { - return SceneIndices.TryGetValue(sceneName, out index); + /// The string. + /// The index of the string or default if the string could not be found. + /// true if there is a corresponding index for the given string, false otherwise. + public static bool GetStringIndex(string sceneName, out ushort index) { + return StringIndices.TryGetValue(sceneName, out index); } /// - /// Try to get the scene name corresponding to the given scene index for encoding/decoding purposes. + /// Try to get the string corresponding to the given string index for encoding/decoding purposes. /// - /// The index of the scene. - /// The name of the scene or default if the scene index could not be found. - /// true if there is a corresponding name for the given scene index, false otherwise. - public static bool GetSceneName(ushort index, out string sceneName) { - return SceneIndices.TryGetValue(index, out sceneName); + /// The string. + /// The string or default if the string index could not be found. + /// true if there is a corresponding string for the given index, false otherwise. + public static bool GetStringName(ushort index, out string sceneName) { + return StringIndices.TryGetValue(index, out sceneName); } } From 3d21be4ef521941b54697c9049f7e5414f0bb1e4 Mon Sep 17 00:00:00 2001 From: Extremelyd1 <10898310+Extremelyd1@users.noreply.github.com> Date: Sat, 30 Nov 2024 21:57:34 +0100 Subject: [PATCH 164/216] Fix more missing string data --- HKMP/Resource/string-data.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/HKMP/Resource/string-data.json b/HKMP/Resource/string-data.json index 00633715..c697567e 100644 --- a/HKMP/Resource/string-data.json +++ b/HKMP/Resource/string-data.json @@ -592,7 +592,10 @@ "Nightmare Grimm Boss Scene", "Soul Tyrant Boss Scene", "Hollow Knight Boss Scene", + "Mantis Lords Boss Scene", "Mantis Lords Boss Scene V", + "Watcher Knights Boss Scene", + "Uumuu Boss Scene", "Nosk Hornet Boss Scene", "Radiance Boss Scene", "Oblobbles Boss Scene", From 6342ca7a9adfac3b243b55ff4208c124def3a6d8 Mon Sep 17 00:00:00 2001 From: Extremelyd1 <10898310+Extremelyd1@users.noreply.github.com> Date: Sun, 19 Jan 2025 16:01:10 +0100 Subject: [PATCH 165/216] DTLS for the entity system (#107) * Initial work for DTLS for connections * Fix server-side to a single socket * Split packet sending for client update manager to different thread * Document the newly added classes, fix various issues * Allow socket receive to time out, improve socket error handling * Update dependencies zip file for dotnet workflow * Update dotnet version of workflow * Add BouncyCastle library to artifact zip in workflow * Fix project file to copy dependencies to output directory --- .github/workflows/dotnet.yml | 6 +- HKMP/HKMP.csproj | 5 +- .../Client/ClientDatagramTransport.cs | 87 ++++++ HKMP/Networking/Client/ClientTlsClient.cs | 110 ++++++++ HKMP/Networking/Client/ClientUpdateManager.cs | 19 +- HKMP/Networking/Client/DtlsClient.cs | 132 +++++++++ HKMP/Networking/Client/NetClient.cs | 74 ++--- HKMP/Networking/Client/UdpNetClient.cs | 108 -------- HKMP/Networking/Server/DtlsServer.cs | 258 ++++++++++++++++++ HKMP/Networking/Server/DtlsServerClient.cs | 28 ++ HKMP/Networking/Server/NetServer.cs | 95 +++---- HKMP/Networking/Server/NetServerClient.cs | 21 +- .../Server/ServerDatagramTransport.cs | 139 ++++++++++ HKMP/Networking/Server/ServerTlsServer.cs | 154 +++++++++++ HKMP/Networking/Server/ServerUpdateManager.cs | 19 +- HKMP/Networking/UdpUpdateManager.cs | 21 +- 16 files changed, 1016 insertions(+), 260 deletions(-) create mode 100644 HKMP/Networking/Client/ClientDatagramTransport.cs create mode 100644 HKMP/Networking/Client/ClientTlsClient.cs create mode 100644 HKMP/Networking/Client/DtlsClient.cs delete mode 100644 HKMP/Networking/Client/UdpNetClient.cs create mode 100644 HKMP/Networking/Server/DtlsServer.cs create mode 100644 HKMP/Networking/Server/DtlsServerClient.cs create mode 100644 HKMP/Networking/Server/ServerDatagramTransport.cs create mode 100644 HKMP/Networking/Server/ServerTlsServer.cs diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index 5a43a849..713acae5 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@v4 with: - dotnet-version: 6.0.x + dotnet-version: 8.0.x - name: Restore dependencies run: dotnet restore ${{ github.workspace }} @@ -60,6 +60,8 @@ 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 + ${{ github.workspace }}/HKMP/bin/Release/net472/BouncyCastle.Cryptography.xml - name: Upload HKMPServer artifact uses: actions/upload-artifact@v4 diff --git a/HKMP/HKMP.csproj b/HKMP/HKMP.csproj index fe8c8da2..5e63a951 100644 --- a/HKMP/HKMP.csproj +++ b/HKMP/HKMP.csproj @@ -99,12 +99,15 @@ $(References)\MonoMod.Utils.dll False + + $(References)\BouncyCastle.Cryptography.dll + - + diff --git a/HKMP/Networking/Client/ClientDatagramTransport.cs b/HKMP/Networking/Client/ClientDatagramTransport.cs new file mode 100644 index 00000000..bc590aef --- /dev/null +++ b/HKMP/Networking/Client/ClientDatagramTransport.cs @@ -0,0 +1,87 @@ +using System.Net.Sockets; +using Hkmp.Logging; +using Org.BouncyCastle.Tls; + +namespace Hkmp.Networking.Client; + +/// +/// Class that implements the DatagramTransport interface from DTLS. This class simply sends and receives data using +/// a UDP socket directly. +/// +internal class ClientDatagramTransport : DatagramTransport { + /// + /// The socket with which to send and over which to receive data. + /// + private readonly Socket _socket; + + public ClientDatagramTransport(Socket socket) { + _socket = socket; + } + + /// + /// The maximum number of bytes to receive in a single call to . + /// + /// The maximum number of bytes that can be received. + public int GetReceiveLimit() { + return DtlsClient.MaxPacketSize; + } + + /// + /// The maximum number of bytes to send in a single call to . + /// + /// The maximum number of bytes that can be sent. + public int GetSendLimit() { + return DtlsClient.MaxPacketSize; + } + + /// + /// This method is called whenever the corresponding DtlsTransport's Receive is called. The implementation + /// receives data from the network and store it in the given buffer. If no data is received within the given + /// , the method returns -1. + /// + /// Byte array to store the received data. + /// The offset at which to begin storing the bytes. + /// The number of bytes that can be stored in the buffer. + /// The number of milliseconds to wait for data to receive. + /// The number of bytes that were received, or -1 if no bytes were received in the given time. + public int Receive(byte[] buf, int off, int len, int waitMillis) { + try { + _socket.ReceiveTimeout = waitMillis; + var numReceived = _socket.Receive( + buf, + off, + len, + SocketFlags.None, + out var socketError + ); + + if (socketError == SocketError.Success) { + return numReceived; + } + + Logger.Error($"UDP Socket Error on receive: {socketError}"); + } catch (SocketException e) { + Logger.Error($"UDP Socket exception, ErrorCode: {e.ErrorCode}, Socket ErrorCode: {e.SocketErrorCode}, Exception:\n{e}"); + } + + return -1; + } + + /// + /// This method is called whenever the corresponding DtlsTransport's Send is called. The implementation simply + /// sends the data in the buffer over the network. + /// + /// Byte array containing the bytes to send. + /// The offset in the buffer at which to start sending bytes. + /// The number of bytes to send. + public void Send(byte[] buf, int off, int len) { + _socket.Send(buf, off, len, SocketFlags.None); + } + + /// + /// Cleanup login for when this transport channel should be closed. + /// Since we handle socket closing in another class (), there is nothing here. + /// + public void Close() { + } +} diff --git a/HKMP/Networking/Client/ClientTlsClient.cs b/HKMP/Networking/Client/ClientTlsClient.cs new file mode 100644 index 00000000..649f52b9 --- /dev/null +++ b/HKMP/Networking/Client/ClientTlsClient.cs @@ -0,0 +1,110 @@ +using System.Text; +using Hkmp.Logging; +using Org.BouncyCastle.Asn1.X509; +using Org.BouncyCastle.Security; +using Org.BouncyCastle.Tls; +using Org.BouncyCastle.Tls.Crypto; +using Org.BouncyCastle.Utilities.Encoders; + +namespace Hkmp.Networking.Client; + +/// +/// Client-side TLS client implementation that handles reporting supported cipher suites, provides client +/// authentication and checks server certificate. +/// +/// TlsCrypto instance for handling low-level cryptography. +internal class ClientTlsClient(TlsCrypto crypto) : AbstractTlsClient(crypto) { + /// + /// List of supported cipher suites on the client-side. + /// + private static readonly int[] SupportedCipherSuites = [ + CipherSuite.TLS_AES_128_GCM_SHA256, + CipherSuite.TLS_AES_256_GCM_SHA384, + CipherSuite.TLS_CHACHA20_POLY1305_SHA256, + CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, + CipherSuite.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256 + ]; + + /// + protected override ProtocolVersion[] GetSupportedVersions() { + return ProtocolVersion.DTLSv12.Only(); + } + + /// + /// Get the supported cipher suites for this TLS client. + /// + /// An int array representing the cipher suites. + protected override int[] GetSupportedCipherSuites() { + return SupportedCipherSuites; + } + + /// + /// + /// Get the authentication implementation for this TLS client that handles providing client credentials and + /// checking server certificates. + /// + /// The TlsAuthentication instance for this TLS client. + public override TlsAuthentication GetAuthentication() { + return new TlsAuthenticationImpl(); + } + + /// + /// Implementation for TLS authentication that handles providing client credentials and checking server + /// certificates. + /// + private class TlsAuthenticationImpl : TlsAuthentication { + /// + /// Notify the TLS client of the server certificate that the server has sent. This method checks whether to + /// trust the server based on this certificate or not. If not, the method will throw an exception which will + /// subsequently abort the connection. + /// In the current implementation, we only log the fingerprints of the certificates in the chain from the + /// server. + /// + /// + /// The server certificate instance. + public void NotifyServerCertificate(TlsServerCertificate serverCertificate) { + if (serverCertificate?.Certificate == null || serverCertificate.Certificate.IsEmpty) { + throw new TlsFatalAlert(AlertDescription.bad_certificate); + } + + var chain = serverCertificate.Certificate.GetCertificateList(); + + Logger.Info("Server certificate fingerprint(s):"); + for (var i = 0; i < chain.Length; i++) { + var entry = X509CertificateStructure.GetInstance(chain[i].GetEncoded()); + Logger.Info($" fingerprint:SHA256 {Fingerprint(entry)} ({entry.Subject})"); + } + } + + /// + /// Get the credentials of the client so the server can verify who we are. Currently, we have no way to + /// provide client-side credentials, so we return null. + /// + /// + public TlsCredentials GetClientCredentials(CertificateRequest certificateRequest) { + // TODO: provide means for a client to have certificate and return it in this method + return null; + } + + /// + /// Return a fingerprint for the given X509 certificate. + /// + /// The 509 certificate to fingerprint. + /// The fingerprint as a string. + private static string Fingerprint(X509CertificateStructure c) { + var der = c.GetEncoded(); + var hash = DigestUtilities.CalculateDigest("SHA256", der); + var hexBytes = Hex.Encode(hash); + var hex = Encoding.ASCII.GetString(hexBytes).ToUpperInvariant(); + + var fp = new StringBuilder(); + var i = 0; + fp.Append(hex.Substring(i, 2)); + while ((i += 2) < hex.Length) { + fp.Append(':'); + fp.Append(hex.Substring(i, 2)); + } + return fp.ToString(); + } + } +} diff --git a/HKMP/Networking/Client/ClientUpdateManager.cs b/HKMP/Networking/Client/ClientUpdateManager.cs index 575e7988..715ba55e 100644 --- a/HKMP/Networking/Client/ClientUpdateManager.cs +++ b/HKMP/Networking/Client/ClientUpdateManager.cs @@ -1,12 +1,10 @@ -using System; using System.Collections.Generic; -using System.Net.Sockets; using Hkmp.Animation; -using Hkmp.Game; using Hkmp.Game.Client.Entity; using Hkmp.Math; using Hkmp.Networking.Packet; using Hkmp.Networking.Packet.Data; +using Org.BouncyCastle.Tls; namespace Hkmp.Networking.Client; @@ -15,19 +13,10 @@ namespace Hkmp.Networking.Client; /// internal class ClientUpdateManager : UdpUpdateManager { /// - /// Construct the update manager with a UDP net client. + /// Construct the update manager a DTLS transport instance. /// - /// The UDP socket for the local client. - public ClientUpdateManager(Socket udpSocket) : base(udpSocket) { - } - - /// - protected override void SendPacket(Packet.Packet packet) { - if (!UdpSocket.Connected) { - return; - } - - UdpSocket?.SendAsync(new ArraySegment(packet.ToArray()), SocketFlags.None); + /// The DTLS transport instance for sending data. + public ClientUpdateManager(DtlsTransport dtlsTransport) : base(dtlsTransport) { } /// diff --git a/HKMP/Networking/Client/DtlsClient.cs b/HKMP/Networking/Client/DtlsClient.cs new file mode 100644 index 00000000..30f256e4 --- /dev/null +++ b/HKMP/Networking/Client/DtlsClient.cs @@ -0,0 +1,132 @@ +using System; +using System.IO; +using System.Net.Sockets; +using System.Threading; +using Hkmp.Logging; +using Org.BouncyCastle.Tls; +using Org.BouncyCastle.Tls.Crypto.Impl.BC; + +namespace Hkmp.Networking.Client; + +/// +/// DTLS implementation for a client-side peer for networking. +/// +internal class DtlsClient { + /// + /// The maximum packet size for sending and receiving DTLS packets. + /// + public const int MaxPacketSize = 1400; + + /// + /// The socket instance for the underlying networking. + /// + private Socket _socket; + /// + /// The TLS client for communicating supported cipher suites and handling certificates. + /// + private ClientTlsClient _tlsClient; + /// + /// The client datagram transport that provides networking to the DTLS client. + /// + private ClientDatagramTransport _clientDatagramTransport; + + /// + /// Token source for cancellation tokens for the update task. + /// + private CancellationTokenSource _updateTaskTokenSource; + + /// + /// DTLS transport instance from establishing a connection to a server. + /// + public DtlsTransport DtlsTransport { get; private set; } + + /// + /// Event that is called when data is received from the server. + /// + public event Action DataReceivedEvent; + + /// + /// Try to establish a connection to a server with the given address and port. + /// + /// The address of the server. + /// The port of the server. + /// Thrown when the underlying socket fails to connect to the server. + /// Thrown when the DTLS protocol fails to connect to the server. + public void Connect(string address, int port) { + if (_socket != null || + _tlsClient != null || + _clientDatagramTransport != null || + DtlsTransport != null || + _updateTaskTokenSource != null + ) { + Disconnect(); + } + + _socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp); + + try { + _socket.Connect(address, port); + } catch (SocketException e) { + Logger.Error($"Socket exception when connecting UDP socket:\n{e}"); + + _socket.Close(); + + throw; + } + + var clientProtocol = new DtlsClientProtocol(); + _tlsClient = new ClientTlsClient(new BcTlsCrypto()); + _clientDatagramTransport = new ClientDatagramTransport(_socket); + + try { + DtlsTransport = clientProtocol.Connect(_tlsClient, _clientDatagramTransport); + } catch (IOException e) { + Logger.Error($"IO exception when connecting DTLS client:\n{e}"); + + _clientDatagramTransport.Close(); + + throw; + } + + Logger.Debug($"Successfully connected DTLS client to endpoint: {address}:{port}"); + + _updateTaskTokenSource = new CancellationTokenSource(); + var cancellationToken = _updateTaskTokenSource.Token; + new Thread(() => ReceiveLoop(cancellationToken)).Start(); + } + + /// + /// Disconnect the DTLS client from the server. + /// + public void Disconnect() { + _updateTaskTokenSource?.Cancel(); + _updateTaskTokenSource?.Dispose(); + _updateTaskTokenSource = null; + + DtlsTransport?.Close(); + DtlsTransport = null; + + _clientDatagramTransport?.Close(); + _clientDatagramTransport = null; + + _tlsClient?.Cancel(); + _tlsClient = null; + + _socket?.Close(); + _socket = null; + } + + /// + /// Continuously tries to receive data from the DTLS transport until cancellation is requested. + /// + /// The cancellation token to cancel the loop. + private void ReceiveLoop(CancellationToken cancellationToken) { + while (!cancellationToken.IsCancellationRequested && DtlsTransport != null) { + var buffer = new byte[MaxPacketSize]; + var length = DtlsTransport.Receive(buffer, 0, buffer.Length, 5); + if (length >= 0) { + DataReceivedEvent?.Invoke(buffer, length); + } + } + } +} diff --git a/HKMP/Networking/Client/NetClient.cs b/HKMP/Networking/Client/NetClient.cs index 88c7d010..8c2d7100 100644 --- a/HKMP/Networking/Client/NetClient.cs +++ b/HKMP/Networking/Client/NetClient.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.IO; using System.Net.Sockets; using System.Threading; using Hkmp.Api.Client; @@ -11,11 +12,6 @@ namespace Hkmp.Networking.Client; -/// -/// Delegate for receiving a list of packets. -/// -internal delegate void OnReceive(List receivedPackets); - /// /// The networking client that manages the UDP client for sending and receiving data. This only /// manages client side networking, e.g. sending to and receiving from the server. @@ -26,11 +22,6 @@ internal class NetClient : INetClient { /// private readonly PacketManager _packetManager; - /// - /// The underlying UDP net client for networking. - /// - private readonly UdpNetClient _udpNetClient; - /// /// The client update manager for this net client. /// @@ -67,9 +58,19 @@ internal class NetClient : INetClient { public bool IsConnecting { get; private set; } /// - /// Cancellation token source for the task for the update manager. + /// Cancellation token source for the update task. + /// + private readonly CancellationTokenSource _updateTaskTokenSource; + + /// + /// The DTLS client instance for handling DTLS connections. + /// + private readonly DtlsClient _dtlsClient; + + /// + /// Byte array containing received data that was not included in a packet object yet. /// - private CancellationTokenSource _updateTaskTokenSource; + private byte[] _leftoverData; /// /// Construct the net client with the given packet manager. @@ -78,10 +79,10 @@ internal class NetClient : INetClient { public NetClient(PacketManager packetManager) { _packetManager = packetManager; - _udpNetClient = new UdpNetClient(); + _updateTaskTokenSource = new CancellationTokenSource(); + _dtlsClient = new DtlsClient(); - // Register the same function for both TCP and UDP receive callbacks - _udpNetClient.RegisterOnReceive(OnReceiveData); + _dtlsClient.DataReceivedEvent += OnReceiveData; } /// @@ -91,7 +92,7 @@ public NetClient(PacketManager packetManager) { private void OnConnect(LoginResponse loginResponse) { Logger.Debug("Connection to server success"); - // De-register the connect failed and register the actual timeout handler if we time out + // De-register the "connect failed" and register the actual timeout handler if we time out UpdateManager.OnTimeout -= OnConnectTimedOut; UpdateManager.OnTimeout += () => { ThreadUtil.RunActionOnMainThread(() => { TimeoutEvent?.Invoke(); }); }; @@ -129,10 +130,15 @@ private void OnConnectFailed(ConnectFailedResult result) { } /// - /// Callback method for when the net client receives data. + /// Callback method for when the DTLS client receives data. This will update the update manager that we have + /// received data, handle packet creation from raw data, handle login responses, and forward received packets to + /// the packet manager. /// - /// A list of raw received packets. - private void OnReceiveData(List packets) { + /// Byte array containing the received bytes. + /// The number of bytes in the . + private void OnReceiveData(byte[] buffer, int length) { + var packets = PacketManager.HandleReceivedData(buffer, length, ref _leftoverData); + foreach (var packet in packets) { // Create a ClientUpdatePacket from the raw packet instance, // and read the values into it @@ -209,28 +215,32 @@ public void Connect( List addonData ) { IsConnecting = true; - + try { - _udpNetClient.Connect(address, port); + _dtlsClient.Connect(address, port); } catch (SocketException e) { Logger.Error($"Failed to connect due to SocketException:\n{e}"); + OnConnectFailed(new ConnectFailedResult { + Type = ConnectFailedResult.FailType.SocketException + }); + return; + } catch (IOException e) { + Logger.Error($"Failed to connect due to IOException:\n{e}"); + OnConnectFailed(new ConnectFailedResult { Type = ConnectFailedResult.FailType.SocketException }); return; } - UpdateManager = new ClientUpdateManager(_udpNetClient.UdpSocket); + UpdateManager = new ClientUpdateManager(_dtlsClient.DtlsTransport); // During the connection process we register the connection failed callback if we time out UpdateManager.OnTimeout += OnConnectTimedOut; - UpdateManager.StartUpdates(); - - // Start a thread that will process the updates for the update manager - // Also make a cancellation token source so we can cancel the thread on demand - _updateTaskTokenSource = new CancellationTokenSource(); - var cancellationToken = _updateTaskTokenSource.Token; + new Thread(() => { + var cancellationToken = _updateTaskTokenSource.Token; + while (!cancellationToken.IsCancellationRequested) { UpdateManager.ProcessUpdate(); @@ -240,6 +250,8 @@ List addonData Thread.Sleep(5); } }).Start(); + + UpdateManager.StartUpdates(); UpdateManager.SetLoginRequestData(username, authKey, addonData); Logger.Debug("Sending login request"); @@ -250,13 +262,13 @@ List addonData /// public void Disconnect() { UpdateManager.StopUpdates(); - - _udpNetClient.Disconnect(); + + _dtlsClient.Disconnect(); IsConnected = false; // Request cancellation for the update task - _updateTaskTokenSource.Cancel(); + _updateTaskTokenSource?.Cancel(); // Clear all client addon packet handlers, because their IDs become invalid _packetManager.ClearClientAddonPacketHandlers(); diff --git a/HKMP/Networking/Client/UdpNetClient.cs b/HKMP/Networking/Client/UdpNetClient.cs deleted file mode 100644 index 397df0c6..00000000 --- a/HKMP/Networking/Client/UdpNetClient.cs +++ /dev/null @@ -1,108 +0,0 @@ -using System.Net; -using System.Net.Sockets; -using System.Threading; -using Hkmp.Logging; -using Hkmp.Networking.Packet; - -namespace Hkmp.Networking.Client; - -/// -/// NetClient that uses the UDP protocol. -/// -internal class UdpNetClient { - /// - /// Maximum size of a UDP packet in bytes. - /// - private const int MaxUdpPacketSize = 65527; - - /// - /// The underlying UDP socket. - /// - public Socket UdpSocket; - - /// - /// Delegate called when packets are received. - /// - private OnReceive _onReceive; - - /// - /// Byte array containing received data that was not included in a packet object yet. - /// - private byte[] _leftoverData; - - /// - /// Cancellation token source for the thread of receiving network data. - /// - private CancellationTokenSource _receiveTokenSource; - - /// - /// Register a callback for when packets are received. - /// - /// The delegate that handles the received packets. - public void RegisterOnReceive(OnReceive onReceive) { - _onReceive = onReceive; - } - - /// - /// Connects the UDP socket to the host at the given address and port. - /// - /// The address of the host. - /// The port of the host. - public void Connect(string address, int port) { - UdpSocket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp); - try { - UdpSocket.Connect(address, port); - } catch (SocketException e) { - Logger.Error($"Socket exception when connecting UDP socket:\n{e}"); - - UdpSocket.Close(); - UdpSocket = null; - - throw; - } - - Logger.Debug($"Starting receiving UDP data on endpoint {UdpSocket.LocalEndPoint}"); - - // Start a thread to receive network data and create a corresponding cancellation token - _receiveTokenSource = new CancellationTokenSource(); - new Thread(() => ReceiveData(_receiveTokenSource.Token)).Start(); - } - - /// - /// Continuously receive network UDP data and queue it for processing. - /// - /// The cancellation token for checking whether this method is requested to cancel. - private void ReceiveData(CancellationToken token) { - EndPoint endPoint = new IPEndPoint(IPAddress.Any, 0); - - while (!token.IsCancellationRequested) { - var numReceived = 0; - var buffer = new byte[MaxUdpPacketSize]; - - try { - numReceived = UdpSocket.ReceiveFrom( - buffer, - SocketFlags.None, - ref endPoint - ); - } catch (SocketException e) { - Logger.Error($"UDP Socket exception:\n{e}"); - } - - var packets = PacketManager.HandleReceivedData(buffer, numReceived, ref _leftoverData); - - _onReceive?.Invoke(packets); - } - } - - /// - /// Disconnect the UDP client and clean it up. - /// - public void Disconnect() { - // Request cancellation of the receive thread - _receiveTokenSource.Cancel(); - - UdpSocket?.Close(); - UdpSocket = null; - } -} diff --git a/HKMP/Networking/Server/DtlsServer.cs b/HKMP/Networking/Server/DtlsServer.cs new file mode 100644 index 00000000..18df25bd --- /dev/null +++ b/HKMP/Networking/Server/DtlsServer.cs @@ -0,0 +1,258 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Net; +using System.Net.Sockets; +using System.Threading; +using Hkmp.Logging; +using Org.BouncyCastle.Tls; +using Org.BouncyCastle.Tls.Crypto.Impl.BC; + +namespace Hkmp.Networking.Server; + +/// +/// DTLS implementation for a server-side peer for networking. +/// +internal class DtlsServer { + /// + /// The maximum packet size for sending and receiving DTLS packets. + /// + public const int MaxPacketSize = 1400; + + /// + /// The DTLS server protocol instance from which to start establishing connections with clients. + /// + private DtlsServerProtocol _serverProtocol; + /// + /// The TLS client for communicating supported cipher suites and handling certificates. + /// + private ServerTlsServer _tlsServer; + + /// + /// The socket instance for the underlying networking. + /// The server only uses a single socket for all connections given that with UDP, we cannot create more than one + /// on the same listening port. + /// + private Socket _socket; + /// + /// The server datagram transport that provides networking to the DTLS server. + /// + private ServerDatagramTransport _currentDatagramTransport; + + /// + /// Token source for cancellation tokens for the accept and receive loop tasks. + /// + private CancellationTokenSource _cancellationTokenSource; + + /// + /// Dictionary mapping IP endpoints to DTLS server client instances. This keeps track of individual clients + /// connected to the server and their respective objects. + /// + private readonly Dictionary _dtlsClients; + + /// + /// Event that is called when data is received from the given DTLS server client. + /// + public event Action DataReceivedEvent; + + /// + /// The port that the server is started on. + /// + private int _port; + + public DtlsServer() { + _dtlsClients = new Dictionary(); + } + + /// + /// Start the DTLS server on the given port. + /// + /// The port to start listening on. + public void Start(int port) { + _port = port; + + _serverProtocol = new DtlsServerProtocol(); + _tlsServer = new ServerTlsServer(new BcTlsCrypto()); + + _cancellationTokenSource = new CancellationTokenSource(); + + _socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp); + _socket.Bind(new IPEndPoint(IPAddress.Any, _port)); + + new Thread(() => AcceptLoop(_cancellationTokenSource.Token)).Start(); + new Thread(() => SocketReceiveLoop(_cancellationTokenSource.Token)).Start(); + } + + /// + /// Stop the DTLS server by disconnecting all clients and cancelling all running threads. + /// + public void Stop() { + _cancellationTokenSource?.Cancel(); + _cancellationTokenSource?.Dispose(); + _cancellationTokenSource = null; + + foreach (var dtlsServerClient in _dtlsClients.Values) { + InternalDisconnectClient(dtlsServerClient); + } + + _dtlsClients.Clear(); + } + + /// + /// Disconnect the client with the given IP endpoint from the server. + /// + /// The IP endpoint of the client. + public void DisconnectClient(IPEndPoint endPoint) { + if (!_dtlsClients.TryGetValue(endPoint, out var dtlsServerClient)) { + Logger.Warn("Could not find DtlsServerClient to disconnect"); + return; + } + + _dtlsClients.Remove(endPoint); + + InternalDisconnectClient(dtlsServerClient); + } + + /// + /// Disconnect the given DTLS server client from the server. This will request cancellation of the "receive loop" + /// for the client and close/cleanup the underlying DTLS objects. + /// + /// + private void InternalDisconnectClient(DtlsServerClient dtlsServerClient) { + dtlsServerClient.ReceiveLoopTokenSource?.Cancel(); + dtlsServerClient.ReceiveLoopTokenSource?.Dispose(); + + dtlsServerClient.DatagramTransport?.Close(); + dtlsServerClient.DtlsTransport?.Close(); + } + + /// + /// Start a loop that will continuously receive data on the socket for existing and new clients. + /// + /// The cancellation token to cancel the loop. + private void SocketReceiveLoop(CancellationToken cancellationToken) { + while (!cancellationToken.IsCancellationRequested) { + EndPoint endPoint = new IPEndPoint(IPAddress.Any, 0); + + var numReceived = 0; + var buffer = new byte[MaxPacketSize]; + + try { + numReceived = _socket.ReceiveFrom( + buffer, + SocketFlags.None, + ref endPoint + ); + } catch (SocketException e) { + Logger.Error($"UDP Socket exception, ErrorCode: {e.ErrorCode}, Socket ErrorCode: {e.SocketErrorCode}, Exception:\n{e}"); + } + + var ipEndPoint = (IPEndPoint) endPoint; + + ServerDatagramTransport serverDatagramTransport; + if (!_dtlsClients.TryGetValue(ipEndPoint, out var dtlsServerClient)) { + Logger.Debug($"Received data on server socket from unknown IP ({ipEndPoint}), length: {numReceived}"); + + serverDatagramTransport = _currentDatagramTransport; + // Set the IP endpoint of the datagram transport instance so it can send data to the correct IP + serverDatagramTransport.IPEndPoint = ipEndPoint; + } else { + serverDatagramTransport = dtlsServerClient.DatagramTransport; + } + + try { + serverDatagramTransport.ReceivedDataCollection.Add(new ServerDatagramTransport.ReceivedData { + Buffer = buffer, + Length = numReceived + }, cancellationToken); + } catch (OperationCanceledException) { + break; + } + } + + _socket?.Close(); + _socket = null; + } + + /// + /// Start a loop that will continuously accept new clients on the DTLS protocol for new incoming connections. + /// + /// The cancellation token to cancel the loop. + private void AcceptLoop(CancellationToken cancellationToken) { + while (!cancellationToken.IsCancellationRequested) { + Logger.Debug("Creating new ServerDatagramTransport for handling new connection"); + + _currentDatagramTransport = new ServerDatagramTransport(_socket); + + DtlsTransport dtlsTransport; + try { + dtlsTransport = _serverProtocol.Accept(_tlsServer, _currentDatagramTransport); + } catch (IOException e) { + Logger.Error($"IOException while accepting DTLS connection:\n{e}"); + break; + } + + var endPoint = _currentDatagramTransport.IPEndPoint; + + Logger.Debug($"Accepted DTLS connection on socket, endpoint: {endPoint}"); + + if (_dtlsClients.ContainsKey(endPoint)) { + Logger.Error($"DtlsClient with endpoint ({endPoint}) already exists, cannot add"); + continue; + } + + var dtlsServerClient = new DtlsServerClient { + DtlsTransport = dtlsTransport, + DatagramTransport = _currentDatagramTransport, + EndPoint = endPoint, + ReceiveLoopTokenSource = new CancellationTokenSource() + }; + + _dtlsClients.Add(endPoint, dtlsServerClient); + + Logger.Debug("Starting receive loop for client"); + new Thread(() => ClientReceiveLoop( + dtlsServerClient, + dtlsServerClient.ReceiveLoopTokenSource.Token + )).Start(); + } + + _currentDatagramTransport?.Close(); + _currentDatagramTransport = null; + + _serverProtocol = null; + + foreach (var dtlsServerClient in _dtlsClients.Values) { + dtlsServerClient.DtlsTransport?.Close(); + dtlsServerClient.DatagramTransport?.Close(); + } + + _dtlsClients.Clear(); + } + + /// + /// Start a loop for the given DTLS server client that will continuously check whether new data is available + /// on the DTLS transport for that client. Will evoke the in case data is + /// received for that client. + /// + /// The DTLS server client to receive data for. + /// The cancellation token to cancel to loop. + private void ClientReceiveLoop(DtlsServerClient dtlsServerClient, CancellationToken cancellationToken) { + var dtlsTransport = dtlsServerClient.DtlsTransport; + + while (!cancellationToken.IsCancellationRequested) { + var buffer = new byte[dtlsTransport.GetReceiveLimit()]; + + var numReceived = dtlsTransport.Receive(buffer, 0, dtlsTransport.GetReceiveLimit(), 5); + if (numReceived <= 0) { + continue; + } + + try { + DataReceivedEvent?.Invoke(dtlsServerClient, buffer, numReceived); + } catch (Exception e) { + Logger.Error($"Error occurred while invoking DataReceivedEvent:\n{e}"); + } + } + } +} diff --git a/HKMP/Networking/Server/DtlsServerClient.cs b/HKMP/Networking/Server/DtlsServerClient.cs new file mode 100644 index 00000000..40456263 --- /dev/null +++ b/HKMP/Networking/Server/DtlsServerClient.cs @@ -0,0 +1,28 @@ +using System.Net; +using System.Threading; +using Org.BouncyCastle.Tls; + +namespace Hkmp.Networking.Server; + +/// +/// Data class containing the related object instances for a DTLS server client. +/// +internal class DtlsServerClient { + /// + /// The DTLS transport instance. + /// + public DtlsTransport DtlsTransport { get; init; } + /// + /// The server datagram transport. + /// + public ServerDatagramTransport DatagramTransport { get; init; } + /// + /// The IP endpoint of the client. + /// + public IPEndPoint EndPoint { get; init; } + + /// + /// The cancellation token source for the "receive loop". + /// + public CancellationTokenSource ReceiveLoopTokenSource { get; init; } +} diff --git a/HKMP/Networking/Server/NetServer.cs b/HKMP/Networking/Server/NetServer.cs index 4d12a987..53c1c123 100644 --- a/HKMP/Networking/Server/NetServer.cs +++ b/HKMP/Networking/Server/NetServer.cs @@ -3,12 +3,10 @@ using System.Collections.Generic; using System.Diagnostics; using System.Net; -using System.Net.Sockets; using System.Threading; using Hkmp.Api.Server; using Hkmp.Api.Server.Networking; using Hkmp.Logging; -using Hkmp.Networking.Client; using Hkmp.Networking.Packet; using Hkmp.Networking.Packet.Data; @@ -28,9 +26,6 @@ ServerUpdateManager updateManager /// Server that manages connection with clients. /// internal class NetServer : INetServer { - /// - private const int MaxUdpPacketSize = 65527; - /// /// The time to throttle a client after they were rejected connection in milliseconds. /// @@ -40,6 +35,11 @@ internal class NetServer : INetServer { /// The packet manager instance. /// private readonly PacketManager _packetManager; + + /// + /// Underlying DTLS server instance. + /// + private readonly DtlsServer _dtlsServer; /// /// Dictionary mapping client IDs to net server clients. @@ -58,11 +58,6 @@ internal class NetServer : INetServer { /// private readonly ConcurrentDictionary _throttledClients; - /// - /// The underlying UDP socket. - /// - private Socket _udpSocket; - private readonly ConcurrentQueue _receivedQueue; /// @@ -102,6 +97,8 @@ internal class NetServer : INetServer { public NetServer(PacketManager packetManager) { _packetManager = packetManager; + _dtlsServer = new DtlsServer(); + _registeredClients = new ConcurrentDictionary(); _clients = new ConcurrentDictionary(); _throttledClients = new ConcurrentDictionary(); @@ -114,14 +111,14 @@ public NetServer(PacketManager packetManager) { /// /// The networking port. public void Start(int port) { + if (IsStarted) { + Stop(); + } + Logger.Info($"Starting NetServer on port {port}"); IsStarted = true; - - // Initialize the UDP socket - _udpSocket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp); - - // Bind the socket to the given port and allow incoming packets on any address - _udpSocket.Bind(new IPEndPoint(IPAddress.Any, port)); + + _dtlsServer.Start(port); _processingWaitHandle = new ManualResetEventSlim(); @@ -134,43 +131,14 @@ public void Start(int port) { // Start a thread for sending updates to clients new Thread(() => StartClientUpdates(_taskTokenSource.Token)).Start(); - // Start a thread to receive network data from the socket - new Thread(() => ReceiveData(_taskTokenSource.Token)).Start(); - } - - /// - /// Continuously receive network UDP data and queue it for processing. - /// - /// The cancellation token for checking whether this method is requested to cancel. - private void ReceiveData(CancellationToken token) { - // Take advantage of pre-pinned memory here using pinned object heap - // var buffer = GC.AllocateArray(65527, true); - // var bufferMem = buffer.AsMemory(); - - EndPoint endPoint = new IPEndPoint(IPAddress.Any, 0); - - while (!token.IsCancellationRequested) { - var numReceived = 0; - var buffer = new byte[MaxUdpPacketSize]; - - try { - // This will block until data is available - numReceived = _udpSocket.ReceiveFrom( - buffer, - SocketFlags.None, - ref endPoint - ); - } catch (SocketException e) { - Logger.Error($"UDP Socket exception:\n{e}"); - } - + _dtlsServer.DataReceivedEvent += (dtlsServerClient, buffer, length) => { _receivedQueue.Enqueue(new ReceivedData { + DtlsServerClient = dtlsServerClient, Buffer = buffer, - NumReceived = numReceived, - EndPoint = endPoint as IPEndPoint + NumReceived = length }); _processingWaitHandle.Set(); - } + }; } /// @@ -194,7 +162,8 @@ private void StartProcessing(CancellationToken token) { ref _leftoverData ); - var endPoint = receivedData.EndPoint; + var dtlsServerClient = receivedData.DtlsServerClient; + var endPoint = dtlsServerClient.EndPoint; if (!_clients.TryGetValue(endPoint, out var client)) { // If the client is throttled, check their stopwatch for how long still @@ -214,7 +183,7 @@ ref _leftoverData // We didn't find a client with the given address, so we assume it is a new client // that wants to connect - client = CreateNewClient(endPoint); + client = CreateNewClient(dtlsServerClient); HandlePacketsUnregisteredClient(client, packets); } else { @@ -227,14 +196,14 @@ ref _leftoverData /// /// Create a new client and start sending UDP updates and registering the timeout event. /// - /// The endpoint of the new client. + /// The DTLS server client to create the client from. /// A new net server client instance. - private NetServerClient CreateNewClient(IPEndPoint endPoint) { - var netServerClient = new NetServerClient(_udpSocket, endPoint); + private NetServerClient CreateNewClient(DtlsServerClient dtlsServerClient) { + var netServerClient = new NetServerClient(dtlsServerClient.DtlsTransport, dtlsServerClient.EndPoint); netServerClient.UpdateManager.OnTimeout += () => HandleClientTimeout(netServerClient); netServerClient.UpdateManager.StartUpdates(); - _clients.TryAdd(endPoint, netServerClient); + _clients.TryAdd(dtlsServerClient.EndPoint, netServerClient); return netServerClient; } @@ -270,6 +239,7 @@ private void HandleClientTimeout(NetServerClient client) { } client.Disconnect(); + _dtlsServer.DisconnectClient(client.EndPoint); _registeredClients.TryRemove(id, out _); _clients.TryRemove(client.EndPoint, out _); @@ -384,13 +354,14 @@ public void Stop() { // Clean up existing clients foreach (var client in _clients.Values) { client.Disconnect(); + _dtlsServer.DisconnectClient(client.EndPoint); } _clients.Clear(); _registeredClients.Clear(); _throttledClients.Clear(); - - _udpSocket.Close(); + + _dtlsServer.Stop(); _leftoverData = null; @@ -399,7 +370,7 @@ public void Stop() { // Request cancellation for the tasks that are still running _taskTokenSource.Cancel(); - _processingWaitHandle.Dispose(); + _processingWaitHandle?.Dispose(); // Invoke the shutdown event to notify all registered parties of the shutdown ShutdownEvent?.Invoke(); @@ -416,6 +387,7 @@ public void OnClientDisconnect(ushort id) { } client.Disconnect(); + _dtlsServer.DisconnectClient(client.EndPoint); _registeredClients.TryRemove(id, out _); _clients.TryRemove(client.EndPoint, out _); @@ -526,6 +498,8 @@ Func packetInstantiator /// Data class for storing received data from a given IP end-point. /// internal class ReceivedData { + public DtlsServerClient DtlsServerClient { get; init; } + /// /// Byte array of the buffer containing received data. /// @@ -535,9 +509,4 @@ internal class ReceivedData { /// The number of bytes in the buffer that were received. The rest of the buffer is empty. /// public int NumReceived { get; init; } - - /// - /// The IP end-point of the client from which we received the data. - /// - public IPEndPoint EndPoint { get; init; } } diff --git a/HKMP/Networking/Server/NetServerClient.cs b/HKMP/Networking/Server/NetServerClient.cs index 3ec9af8b..abfc6bb8 100644 --- a/HKMP/Networking/Server/NetServerClient.cs +++ b/HKMP/Networking/Server/NetServerClient.cs @@ -1,6 +1,6 @@ using System.Collections.Concurrent; using System.Net; -using System.Net.Sockets; +using Org.BouncyCastle.Tls; namespace Hkmp.Networking.Server; @@ -40,26 +40,15 @@ internal class NetServerClient { public readonly IPEndPoint EndPoint; /// - /// Construct the client with the given UDP socket and endpoint. + /// Construct the client with the given DTLS transport and endpoint. /// - /// The underlying UDP socket. + /// The underlying DTLS transport. /// The endpoint. - public NetServerClient(Socket udpSocket, IPEndPoint endPoint) { - // Also store endpoint with TCP address and TCP port + public NetServerClient(DtlsTransport dtlsTransport, IPEndPoint endPoint) { EndPoint = endPoint; Id = GetId(); - UpdateManager = new ServerUpdateManager(udpSocket, EndPoint); - } - - /// - /// Whether this client resides at the given endpoint. - /// - /// The endpoint to test for. - /// true if the address and port of the endpoint match the endpoint of the client; otherwise - /// false. - public bool HasAddress(IPEndPoint endPoint) { - return EndPoint.Address.Equals(endPoint.Address) && EndPoint.Port == endPoint.Port; + UpdateManager = new ServerUpdateManager(dtlsTransport); } /// diff --git a/HKMP/Networking/Server/ServerDatagramTransport.cs b/HKMP/Networking/Server/ServerDatagramTransport.cs new file mode 100644 index 00000000..d53dae63 --- /dev/null +++ b/HKMP/Networking/Server/ServerDatagramTransport.cs @@ -0,0 +1,139 @@ +using System.Collections.Concurrent; +using System.Net; +using System.Net.Sockets; +using Hkmp.Logging; +using Org.BouncyCastle.Tls; + +namespace Hkmp.Networking.Server; + +/// +/// Class that implements the DatagramTransport interface from DTLS. This class simply sends and receives data based +/// on a blocking collection that is filled by data received by the DTLS server. +/// +internal class ServerDatagramTransport : DatagramTransport { + /// + /// The socket instance solely used to send data. + /// + private readonly Socket _socket; + + /// + /// The IP endpoint for the client that this datagram transport belongs to. + /// + public IPEndPoint IPEndPoint { get; set; } + + /// + /// A thread-safe blocking collection storing received data that is used to handle the "Receive" calls from the + /// DTLS transport. + /// + public BlockingCollection ReceivedDataCollection { get; } + + public ServerDatagramTransport(Socket socket) { + _socket = socket; + + ReceivedDataCollection = new BlockingCollection(); + } + + /// + /// The maximum number of bytes to receive in a single call to . + /// + /// The maximum number of bytes that can be received. + public int GetReceiveLimit() { + return DtlsServer.MaxPacketSize; + } + + /// + /// The maximum number of bytes to send in a single call to . + /// + /// The maximum number of bytes that can be sent. + public int GetSendLimit() { + return DtlsServer.MaxPacketSize; + } + + /// + /// This method is called whenever the corresponding DtlsTransport's Receive is called. The implementation + /// obtains data from the blocking collection and store it in the given buffer. If no data is present in the + /// collection within the given , the method returns -1. + /// + /// Byte array to store the received data. + /// The offset at which to begin storing the data. + /// The number of bytes that can be stored in the buffer. + /// The number of milliseconds to wait for data to fill. + /// The number of bytes that were received, or -1 if no bytes were received in the given time. + public int Receive(byte[] buf, int off, int len, int waitMillis) { + if (!ReceivedDataCollection.TryTake(out var data, waitMillis)) { + return -1; + } + + // If there is more data in the entry we received from the blocking collection than space in the buffer + // from the method, we need to add as much data into the buffer and put the rest back in the collection + if (len < data.Length) { + // Fill the buffer from the method with as much data from the entry as possible + for (var i = off; i < off + len; i++) { + buf[i] = data.Buffer[i - off]; + } + + // Calculate the length of the leftover buffer and instantiate it + var leftoverLength = data.Length - len; + var leftoverBuffer = new byte[leftoverLength]; + + // Fill the leftover buffer with the leftover data from the entry + for (var i = 0; i < leftoverLength; i++) { + leftoverBuffer[i] = data.Buffer[len + i]; + } + + // Add the leftover buffer and its length back to the collection + ReceivedDataCollection.Add(new ReceivedData { + Buffer = leftoverBuffer, + Length = leftoverLength + }); + + return len; + } + + // In this case, the space in the buffer from the method is large enough, so we fill it with all the data + // from the collection entry + for (var i = 0; i < data.Length; i++) { + buf[off + i] = data.Buffer[i]; + } + + return data.Length; + } + + /// + /// This method is called whenever the corresponding DtlsTransport's Send is called. The implementation simply + /// sends the data in the buffer over the network. + /// + /// Byte array containing the bytes to send. + /// The offset in the buffer at which to start sending bytes. + /// The number of bytes to send. + public void Send(byte[] buf, int off, int len) { + if (IPEndPoint == null) { + Logger.Error("Cannot send because transport has no endpoint"); + return; + } + + _socket.SendTo(buf, off, len, SocketFlags.None, IPEndPoint); + } + + /// + /// Cleanup login for when this transport channel should be closed. + /// Since we handle socket closing in another class (), there is nothing here. + /// + public void Close() { + } + + /// + /// Data class containing a buffer and the corresponding length of bytes stored in that buffer. Not necessarily + /// the length of the buffer. + /// + public class ReceivedData { + /// + /// Byte array containing the data. + /// + public byte[] Buffer { get; set; } + /// + /// The number of bytes in the buffer. + /// + public int Length { get; set; } + } +} diff --git a/HKMP/Networking/Server/ServerTlsServer.cs b/HKMP/Networking/Server/ServerTlsServer.cs new file mode 100644 index 00000000..b476102e --- /dev/null +++ b/HKMP/Networking/Server/ServerTlsServer.cs @@ -0,0 +1,154 @@ +using System; +using Org.BouncyCastle.Asn1.Pkcs; +using Org.BouncyCastle.Asn1.X509; +using Org.BouncyCastle.Asn1.X9; +using Org.BouncyCastle.Crypto; +using Org.BouncyCastle.Crypto.Generators; +using Org.BouncyCastle.Crypto.Operators; +using Org.BouncyCastle.Crypto.Parameters; +using Org.BouncyCastle.Math; +using Org.BouncyCastle.Security; +using Org.BouncyCastle.Tls; +using Org.BouncyCastle.Tls.Crypto; +using Org.BouncyCastle.Tls.Crypto.Impl.BC; +using Org.BouncyCastle.X509; +// ReSharper disable InconsistentNaming + +namespace Hkmp.Networking.Server; + +/// +/// Server-side TLS client implementation that handles reporting supported cipher suites and provides the server's +/// certificate. +/// +internal class ServerTlsServer : AbstractTlsServer { + /// + /// List of supported cipher suites on the server-side. + /// + private static readonly int[] SupportedCipherSuites = [ + CipherSuite.TLS_AES_128_GCM_SHA256, + CipherSuite.TLS_AES_256_GCM_SHA384, + CipherSuite.TLS_CHACHA20_POLY1305_SHA256, + CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, + CipherSuite.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256 + ]; + + /// + /// Asymmetric key pair for the server. Used to create the server certificate. + /// + private readonly AsymmetricCipherKeyPair _keyPair; + /// + /// X509 certificate for the server. Reported to clients to validate they are connecting to the correct server. + /// + private readonly X509Certificate _certificate; + + // TODO: use existing certificate on disk if available and store generated certificate to disk if a new one was generated + public ServerTlsServer(TlsCrypto crypto) : base(crypto) { + _keyPair = GenerateECDHKeyPair(GetECBuiltInBinaryDomainParameters()); + _certificate = GenerateCertificate( + new X509Name("CN=TestCA"), + new X509Name("CN=TestEE"), + _keyPair.Private, + _keyPair.Public + ); + } + + /// + /// Generate a X509 certificate with the given issuer and subject names, and with the given keys. + /// + /// The issuer name. + /// The subject name. + /// The issuer private key. + /// The subject public key. + /// The generated X509 certificate. + private static X509Certificate GenerateCertificate( + X509Name issuer, X509Name subject, + AsymmetricKeyParameter issuerPrivate, + AsymmetricKeyParameter subjectPublic + ) { + ISignatureFactory signatureFactory; + if (issuerPrivate is ECPrivateKeyParameters) { + signatureFactory = new Asn1SignatureFactory( + X9ObjectIdentifiers.ECDsaWithSha256.ToString(), + issuerPrivate); + } else { + signatureFactory = new Asn1SignatureFactory( + PkcsObjectIdentifiers.Sha256WithRsaEncryption.ToString(), + issuerPrivate); + } + + var certGenerator = new X509V3CertificateGenerator(); + certGenerator.SetIssuerDN(issuer); + certGenerator.SetSubjectDN(subject); + certGenerator.SetSerialNumber(BigInteger.ValueOf(1)); + certGenerator.SetNotAfter(DateTime.UtcNow.AddHours(1)); + certGenerator.SetNotBefore(DateTime.UtcNow); + certGenerator.SetPublicKey(subjectPublic); + return certGenerator.Generate(signatureFactory); + } + + /// + /// Get built-in binary domain parameters for elliptic curve crypto using curve "K-283". + /// + /// The domain parameters corresponding to "K-283". + private static ECDomainParameters GetECBuiltInBinaryDomainParameters() { + var ecParams = ECNamedCurveTable.GetByName("K-283"); + + return new ECDomainParameters(ecParams.Curve, ecParams.G, ecParams.N, ecParams.H, ecParams.GetSeed()); + } + + /// + /// Generate an asymmetric key pair using the given domain parameters. + /// + /// The domain parameters for the generation. + /// The asymmetric key pair. + private static AsymmetricCipherKeyPair GenerateECDHKeyPair(ECDomainParameters ecParams) { + var ecKeyGenParams = new ECKeyGenerationParameters(ecParams, new SecureRandom()); + var ecKeyPairGen = new ECKeyPairGenerator(); + ecKeyPairGen.Init(ecKeyGenParams); + var ecKeyPair = ecKeyPairGen.GenerateKeyPair(); + + return ecKeyPair; + } + + /// + protected override ProtocolVersion[] GetSupportedVersions() { + return ProtocolVersion.DTLSv12.Only(); + } + + /// + /// Get the supported cipher suites for this TLS client. + /// + /// An int array representing the cipher suites. + protected override int[] GetSupportedCipherSuites() { + return SupportedCipherSuites; + } + + /// + /// Get the server credentials for sending to clients to authenticate the server. + /// + /// TlsCredentials instance representing the credentials. + /// Thrown when the key exchange algorithm agreed upon by the server and client + /// does not match the expected, and we can thus not send our credentials. + public override TlsCredentials GetCredentials() { + var keyExchangeAlgorithm = TlsUtilities.GetKeyExchangeAlgorithm(m_selectedCipherSuite); + + if (keyExchangeAlgorithm != KeyExchangeAlgorithm.ECDHE_ECDSA) { + throw new TlsFatalAlert(AlertDescription.internal_error); + } + + var bcTlsCrypto = new BcTlsCrypto(new SecureRandom()); + + return new DefaultTlsCredentialedSigner( + new TlsCryptoParameters(m_context), + new BcTlsECDsaSigner( + bcTlsCrypto, + (ECPrivateKeyParameters) _keyPair.Private + ), + new Certificate([new BcTlsCertificate(bcTlsCrypto, _certificate.CertificateStructure)]), + SignatureAndHashAlgorithm.GetInstance( + HashAlgorithm.sha384, + SignatureAlgorithm.ecdsa + ) + ); + } +} diff --git a/HKMP/Networking/Server/ServerUpdateManager.cs b/HKMP/Networking/Server/ServerUpdateManager.cs index 6e2a6bf0..9718310b 100644 --- a/HKMP/Networking/Server/ServerUpdateManager.cs +++ b/HKMP/Networking/Server/ServerUpdateManager.cs @@ -1,13 +1,12 @@ using System; using System.Collections.Generic; -using System.Net; -using System.Net.Sockets; using Hkmp.Game; using Hkmp.Game.Client.Entity; using Hkmp.Game.Settings; using Hkmp.Math; using Hkmp.Networking.Packet; using Hkmp.Networking.Packet.Data; +using Org.BouncyCastle.Tls; namespace Hkmp.Networking.Server; @@ -15,23 +14,11 @@ namespace Hkmp.Networking.Server; /// Specialization of for server to client packet sending. /// internal class ServerUpdateManager : UdpUpdateManager { - /// - /// The endpoint of the client. - /// - private readonly IPEndPoint _endPoint; - /// /// Construct the update manager with the given details. /// - /// The underlying UDP socket for this client. - /// The endpoint of the client. - public ServerUpdateManager(Socket udpSocket, IPEndPoint endPoint) : base(udpSocket) { - _endPoint = endPoint; - } - - /// - protected override void SendPacket(Packet.Packet packet) { - UdpSocket.SendToAsync(new ArraySegment(packet.ToArray()), SocketFlags.None, _endPoint); + /// The DTLS transport instance for the client used for sending data. + public ServerUpdateManager(DtlsTransport dtlsTransport) : base(dtlsTransport) { } /// diff --git a/HKMP/Networking/UdpUpdateManager.cs b/HKMP/Networking/UdpUpdateManager.cs index 810954c6..6fc6fca6 100644 --- a/HKMP/Networking/UdpUpdateManager.cs +++ b/HKMP/Networking/UdpUpdateManager.cs @@ -1,9 +1,9 @@ using System; -using System.Net.Sockets; using Hkmp.Concurrency; using Hkmp.Logging; using Hkmp.Networking.Packet; using Hkmp.Networking.Packet.Data; +using Org.BouncyCastle.Tls; namespace Hkmp.Networking; @@ -31,8 +31,9 @@ internal abstract class UdpUpdateManager : UdpUpdateManage /// The MTU (maximum transfer unit) to use to send packets with. If the length of a packet exceeds this, we break /// it up into smaller packets before sending. This ensures that we control the breaking of packets in most /// cases and do not rely on smaller network devices for the breaking up as this could impact performance. + /// This size is lower than the limit for DTLS packets, since there is a slight DTLS overhead for packets. /// - private const int PacketMtu = 1400; + private const int PacketMtu = 1200; /// /// The number of sequence numbers to store in the received queue to construct ack fields with and @@ -43,7 +44,7 @@ internal abstract class UdpUpdateManager : UdpUpdateManage /// /// The Socket instance to use to send packets. /// - protected readonly Socket UdpSocket; + private readonly DtlsTransport _dtlsTransport; /// /// The UDP congestion manager instance. @@ -108,9 +109,9 @@ internal abstract class UdpUpdateManager : UdpUpdateManage /// /// Construct the update manager with a UDP socket. /// - /// The UDP socket instance. - protected UdpUpdateManager(Socket udpSocket) { - UdpSocket = udpSocket; + /// The DTLS transport instance used to send data. + protected UdpUpdateManager(DtlsTransport dtlsTransport) { + _dtlsTransport = dtlsTransport; _udpCongestionManager = new UdpCongestionManager(this); @@ -202,7 +203,7 @@ public void OnReceivePacket(TIncoming packet) /// Create and send the current update packet. /// private void CreateAndSendUpdatePacket() { - if (UdpSocket == null) { + if (_dtlsTransport == null) { return; } @@ -292,7 +293,11 @@ private bool IsSequenceGreaterThan(ushort sequence1, ushort sequence2) { /// Send the given packet over the corresponding medium. /// /// The raw packet instance. - protected abstract void SendPacket(Packet.Packet packet); + private void SendPacket(Packet.Packet packet) { + var buffer = packet.ToArray(); + + _dtlsTransport?.Send(buffer, 0, buffer.Length); + } /// /// Either get or create an AddonPacketData instance for the given addon. From 28aeadd4e30791955d53959684337316cdebcc31 Mon Sep 17 00:00:00 2001 From: Extremelyd1 <10898310+Extremelyd1@users.noreply.github.com> Date: Mon, 20 Jan 2025 18:52:44 +0100 Subject: [PATCH 166/216] Fix various logging, connection and addon loading issues Also revert embedding dependencies and add them to dotnet build workflow --- .github/workflows/dotnet.yml | 5 ++++- HKMP/Api/Addon/AddonLoader.cs | 15 +++++++++++++++ HKMP/HKMP.csproj | 16 ++++++++-------- .../Client/ClientDatagramTransport.cs | 4 +++- HKMP/Networking/Client/NetClient.cs | 18 ++++++++++++------ HKMP/Networking/Server/NetServer.cs | 2 ++ HKMPServer/HKMPServer.csproj | 17 ++++------------- HKMPServer/LocalBuildProperties_example.props | 6 ++++++ 8 files changed, 54 insertions(+), 29 deletions(-) create mode 100644 HKMPServer/LocalBuildProperties_example.props diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index 713acae5..9af64763 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/dotnet.yml @@ -61,7 +61,6 @@ jobs: ${{ 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 - ${{ github.workspace }}/HKMP/bin/Release/net472/BouncyCastle.Cryptography.xml - name: Upload HKMPServer artifact uses: actions/upload-artifact@v4 @@ -70,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/Api/Addon/AddonLoader.cs b/HKMP/Api/Addon/AddonLoader.cs index 420983ec..9bb1b877 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/HKMP.csproj b/HKMP/HKMP.csproj index 5e63a951..1c27823f 100644 --- a/HKMP/HKMP.csproj +++ b/HKMP/HKMP.csproj @@ -31,6 +31,9 @@ $(References)\Assembly-CSharp.dll False + + $(References)\BouncyCastle.Cryptography.dll + $(References)\MMHOOK_Assembly-CSharp.dll False @@ -43,12 +46,16 @@ $(References)\MonoMod.RuntimeDetour.dll False + + $(References)\MonoMod.Utils.dll + False + $(References)\Mono.Cecil.dll False - ..\HKMPServer\Lib\Newtonsoft.Json.dll + $(References)\Newtonsoft.Json.dll False @@ -95,13 +102,6 @@ $(References)\UnityEngine.UIModule.dll False - - $(References)\MonoMod.Utils.dll - False - - - $(References)\BouncyCastle.Cryptography.dll - + .\lib + + From 05682e88bdfdb428e131bbe5cecfeddbffa26efe Mon Sep 17 00:00:00 2001 From: Extremelyd1 <10898310+Extremelyd1@users.noreply.github.com> Date: Mon, 20 Jan 2025 18:53:25 +0100 Subject: [PATCH 167/216] Remove obsolete assembly resolution code --- HKMPServer/Launcher.cs | 59 ------------------------------------------ 1 file changed, 59 deletions(-) diff --git a/HKMPServer/Launcher.cs b/HKMPServer/Launcher.cs index a7ccdaf7..6496f819 100644 --- a/HKMPServer/Launcher.cs +++ b/HKMPServer/Launcher.cs @@ -1,7 +1,3 @@ -using System; -using System.IO; -using System.Reflection; - namespace HkmpServer { /// /// Launcher class with the entry point for the program. Primarily here to make sure embedded assemblies @@ -13,62 +9,7 @@ internal static class Launcher { /// /// Command line arguments for the server. public static void Main(string[] args) { - // Register event listeners for when assemblies are trying to get resolved - AppDomain.CurrentDomain.ReflectionOnlyAssemblyResolve += ResolveAssembly; - AppDomain.CurrentDomain.AssemblyResolve += ResolveAssembly; - new HkmpServer().Initialize(args); } - - /// - /// Callback for assembly resolve event. Will try to find and load an embedded assembly for the assembly - /// that is trying to get resolved. - /// - /// The sender of the event. - /// The event arguments. - /// The resolved assembly, or null if no such assembly could be found. - private static Assembly ResolveAssembly(object sender, ResolveEventArgs eventArgs) { - var assemblyName = eventArgs.Name.Split(',')[0]; - var currentAssembly = Assembly.GetExecutingAssembly(); - - // Try to find the assembly as an embedded resource - var assemblyStream = currentAssembly.GetManifestResourceStream($"HkmpServer.Lib.{assemblyName}.dll"); - if (assemblyStream == null) { - return null; - } - - var assemblyMemoryStream = new MemoryStream(); - assemblyStream.CopyTo(assemblyMemoryStream); - - Console.WriteLine($"Found resource for resolving assembly: {assemblyName}"); - - // Exception message for when assembly loading fails - const string assemblyLoadingExceptionMsg = "Exception occurred while loading assembly: "; - - // Try to get the PDB for the assembly if it exists - var symbolStream = currentAssembly.GetManifestResourceStream($"HkmpServer.Lib.{assemblyName}.pdb"); - if (symbolStream != null) { - Console.WriteLine(" Found PDB for assembly"); - - var symbolMemoryStream = new MemoryStream(); - symbolStream.CopyTo(symbolMemoryStream); - - // Load the assembly with the PDB - try { - return Assembly.Load(assemblyMemoryStream.ToArray(), symbolMemoryStream.ToArray()); - } catch (BadImageFormatException ex) { - Console.WriteLine(assemblyLoadingExceptionMsg + ex.Message); - } - } - - // Load the assembly without the PDB - try { - return Assembly.Load(assemblyMemoryStream.ToArray()); - } catch (BadImageFormatException ex) { - Console.WriteLine(assemblyLoadingExceptionMsg + ex.Message); - } - - return null; - } } } From b9b8a4b5080bd8604b449f921e16e6cb014234d8 Mon Sep 17 00:00:00 2001 From: Extremelyd1 <10898310+Extremelyd1@users.noreply.github.com> Date: Mon, 20 Jan 2025 22:21:07 +0100 Subject: [PATCH 168/216] Store server key and certificate and load on existing key/certificate --- HKMP/Networking/Server/ServerTlsServer.cs | 191 +++++++++++++++++++--- 1 file changed, 171 insertions(+), 20 deletions(-) diff --git a/HKMP/Networking/Server/ServerTlsServer.cs b/HKMP/Networking/Server/ServerTlsServer.cs index b476102e..bea35f28 100644 --- a/HKMP/Networking/Server/ServerTlsServer.cs +++ b/HKMP/Networking/Server/ServerTlsServer.cs @@ -1,4 +1,7 @@ using System; +using System.IO; +using Hkmp.Logging; +using Hkmp.Util; using Org.BouncyCastle.Asn1.Pkcs; using Org.BouncyCastle.Asn1.X509; using Org.BouncyCastle.Asn1.X9; @@ -11,7 +14,11 @@ using Org.BouncyCastle.Tls; using Org.BouncyCastle.Tls.Crypto; using Org.BouncyCastle.Tls.Crypto.Impl.BC; +using Org.BouncyCastle.Utilities.IO.Pem; using Org.BouncyCastle.X509; +using PemReader = Org.BouncyCastle.OpenSsl.PemReader; +using PemWriter = Org.BouncyCastle.OpenSsl.PemWriter; + // ReSharper disable InconsistentNaming namespace Hkmp.Networking.Server; @@ -32,6 +39,24 @@ internal class ServerTlsServer : AbstractTlsServer { CipherSuite.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256 ]; + /// + /// File name of the file that stores the private key. + /// + private const string KeyPairFileName = "key.pem"; + /// + /// File name of the file that stores the certificate. + /// + private const string CertificateFileName = "cert.cer"; + + /// + /// Full file path of the file that stores the private key. + /// + private readonly string KeyPairFilePath; + /// + /// Full file path of the file that stores the certificate. + /// + private readonly string CertificateFilePath; + /// /// Asymmetric key pair for the server. Used to create the server certificate. /// @@ -41,15 +66,133 @@ internal class ServerTlsServer : AbstractTlsServer { /// private readonly X509Certificate _certificate; - // TODO: use existing certificate on disk if available and store generated certificate to disk if a new one was generated public ServerTlsServer(TlsCrypto crypto) : base(crypto) { - _keyPair = GenerateECDHKeyPair(GetECBuiltInBinaryDomainParameters()); - _certificate = GenerateCertificate( + KeyPairFilePath = Path.Combine(FileUtil.GetCurrentPath(), KeyPairFileName); + CertificateFilePath = Path.Combine(FileUtil.GetCurrentPath(), CertificateFileName); + + _keyPair = LoadOrGenerateECDHKeyPair(); + _certificate = LoadOrGenerateCertificate(); + } + + /// + /// Loads the ECDH key pair from file if it exists, otherwise generates the key pair and stores it to a file. + /// + /// The loaded or generated asymmetric EC key pair. + private AsymmetricCipherKeyPair LoadOrGenerateECDHKeyPair() { + Logger.Info($"LoadOrGenerateECDHKeyPair: {KeyPairFilePath}"); + + if (File.Exists(KeyPairFilePath)) { + Logger.Info("KeyPair file exists, loading..."); + return LoadECDHKeyPair(); + } + + Logger.Info("KeyPair file does not exist, generating and storing..."); + var generatedKeyPair = GenerateECDHKeyPair(GetECBuiltInBinaryDomainParameters()); + WriteObjectAsPemToFile(KeyPairFilePath, generatedKeyPair); + + return generatedKeyPair; + } + + /// + /// Load the ECDH key pair from file. + /// + /// The loaded asymmetric EC key pair, or null if the file could not be found or read. + private AsymmetricCipherKeyPair LoadECDHKeyPair() { + string fileContents; + try { + fileContents = File.ReadAllText(KeyPairFilePath); + } catch (Exception e) { + Logger.Error($"Could not read PEM key file:\n{e}"); + return null; + } + + var stringReader = new StringReader(fileContents); + var pemReader = new PemReader(stringReader); + + try { + return (AsymmetricCipherKeyPair) pemReader.ReadObject(); + } catch (Exception e) { + Logger.Error($"Could not read PEM key file:\n{e}"); + return null; + } finally { + stringReader.Close(); + pemReader.Dispose(); + } + } + + /// + /// Get built-in binary domain parameters for elliptic curve crypto using curve "K-283". + /// + /// The domain parameters corresponding to "K-283". + private static ECDomainParameters GetECBuiltInBinaryDomainParameters() { + var ecParams = ECNamedCurveTable.GetByName("K-283"); + + return new ECDomainParameters(ecParams.Curve, ecParams.G, ecParams.N, ecParams.H, ecParams.GetSeed()); + } + + /// + /// Generate an asymmetric key pair using the given domain parameters. + /// + /// The domain parameters for the generation. + /// The asymmetric key pair. + private static AsymmetricCipherKeyPair GenerateECDHKeyPair(ECDomainParameters ecParams) { + var ecKeyGenParams = new ECKeyGenerationParameters(ecParams, new SecureRandom()); + var ecKeyPairGen = new ECKeyPairGenerator(); + ecKeyPairGen.Init(ecKeyGenParams); + var ecKeyPair = ecKeyPairGen.GenerateKeyPair(); + + return ecKeyPair; + } + + /// + /// Loads the X509 certificate from file if it exists, otherwise generates the certificate and stores it to a file. + /// + /// The loaded or generated certificate. + private X509Certificate LoadOrGenerateCertificate() { + Logger.Info($"LoadOrGenerateCertificate: {CertificateFilePath}"); + + if (File.Exists(CertificateFilePath)) { + Logger.Info("Certificate file exists, loading..."); + return LoadCertificate(); + } + + Logger.Info("Certificate does not exist, generating and storing..."); + var generatedCertificate = GenerateCertificate( new X509Name("CN=TestCA"), new X509Name("CN=TestEE"), _keyPair.Private, _keyPair.Public ); + WriteObjectAsPemToFile(CertificateFilePath, generatedCertificate); + + return generatedCertificate; + } + + /// + /// Load the X509 certificate from file. + /// + /// The loaded certificate, or null if the file could not be found or read. + private X509Certificate LoadCertificate() { + string fileContents; + try { + fileContents = File.ReadAllText(CertificateFilePath); + } catch (Exception e) { + Logger.Error($"Could not read certificate file:\n{e}"); + return null; + } + + var stringReader = new StringReader(fileContents); + var pemReader = new PemReader(stringReader); + + try { + return (X509Certificate) pemReader.ReadObject(); + } catch (Exception e) { + Logger.Error($"Could not read certificate file:\n{e}"); + return null; + } finally { + stringReader.Close(); + pemReader.Dispose(); + } } /// @@ -87,27 +230,35 @@ AsymmetricKeyParameter subjectPublic } /// - /// Get built-in binary domain parameters for elliptic curve crypto using curve "K-283". + /// Write a given object to a file at the given file path. This uses a and thus can + /// only write certain objects: + /// X509Certificate, X509Crl, AsymmetricCipherKeyPair, AsymmetricKeyParameter, + /// IX509AttributeCertificate, Pkcs10CertificationRequest, Asn1.Cms.ContentInfo /// - /// The domain parameters corresponding to "K-283". - private static ECDomainParameters GetECBuiltInBinaryDomainParameters() { - var ecParams = ECNamedCurveTable.GetByName("K-283"); + /// The full path of the file to write the object to. + /// The object to write to file. + private static void WriteObjectAsPemToFile(string filePath, object obj) { + var stringWriter = new StringWriter(); + var pemWriter = new PemWriter(stringWriter); - return new ECDomainParameters(ecParams.Curve, ecParams.G, ecParams.N, ecParams.H, ecParams.GetSeed()); - } + try { + pemWriter.WriteObject(obj); + } catch (PemGenerationException e) { + Logger.Error($"Could not write object to PEM file:\n{e}"); + return; + } - /// - /// Generate an asymmetric key pair using the given domain parameters. - /// - /// The domain parameters for the generation. - /// The asymmetric key pair. - private static AsymmetricCipherKeyPair GenerateECDHKeyPair(ECDomainParameters ecParams) { - var ecKeyGenParams = new ECKeyGenerationParameters(ecParams, new SecureRandom()); - var ecKeyPairGen = new ECKeyPairGenerator(); - ecKeyPairGen.Init(ecKeyGenParams); - var ecKeyPair = ecKeyPairGen.GenerateKeyPair(); + pemWriter.Writer.Flush(); - return ecKeyPair; + var contents = stringWriter.ToString(); + + stringWriter.Close(); + + try { + File.WriteAllText(filePath, contents); + } catch (Exception e) { + Logger.Error($"Could not write object to PEM file:\n{e}"); + } } /// From e4355c2d9dc643cebcd225b4edf179046c513216 Mon Sep 17 00:00:00 2001 From: Extremelyd1 <10898310+Extremelyd1@users.noreply.github.com> Date: Thu, 23 Jan 2025 21:48:31 +0100 Subject: [PATCH 169/216] Increase expiration date on generated server certificate --- HKMP/Networking/Server/ServerTlsServer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/HKMP/Networking/Server/ServerTlsServer.cs b/HKMP/Networking/Server/ServerTlsServer.cs index bea35f28..b31aaa20 100644 --- a/HKMP/Networking/Server/ServerTlsServer.cs +++ b/HKMP/Networking/Server/ServerTlsServer.cs @@ -223,7 +223,7 @@ AsymmetricKeyParameter subjectPublic certGenerator.SetIssuerDN(issuer); certGenerator.SetSubjectDN(subject); certGenerator.SetSerialNumber(BigInteger.ValueOf(1)); - certGenerator.SetNotAfter(DateTime.UtcNow.AddHours(1)); + certGenerator.SetNotAfter(DateTime.UtcNow.AddYears(1)); certGenerator.SetNotBefore(DateTime.UtcNow); certGenerator.SetPublicKey(subjectPublic); return certGenerator.Generate(signatureFactory); From e550425b886270c8b4218a6f87a6821af99590ae Mon Sep 17 00:00:00 2001 From: Extremelyd1 <10898310+Extremelyd1@users.noreply.github.com> Date: Fri, 14 Feb 2025 20:38:12 +0100 Subject: [PATCH 170/216] Implement chunk system for handling large data transfer (#108) * Initial code for chunk/slice system for connections * Progress on connection refactor * Improve connections and persist chunk system through connection * Fix various issues with chunks and connections * Fix username not appearing on hosting players * Fix error with multiple clients connecting * Replace problematic thread loops with timers * Add missing documentation for chunk system * Add missing documentation to changed files in connection refactor * Fix connection never timing out * Catch TLS exceptions in server clients loop * Fix issue with resetting chunk system * Fix server-side connection timeout handling --- HKMP/Animation/AnimationManager.cs | 6 +- .../Networking/ClientAddonNetworkReceiver.cs | 9 +- .../Networking/ServerAddonNetworkReceiver.cs | 6 +- HKMP/Game/Client/ClientManager.cs | 104 +- HKMP/Game/Client/PlayerManager.cs | 10 +- HKMP/Game/Client/Save/SaveManager.cs | 3 +- HKMP/Game/Command/Client/AddonCommand.cs | 2 +- HKMP/Game/Server/ServerManager.cs | 249 +++-- HKMP/Networking/Chunk/ChunkReceiver.cs | 181 ++++ HKMP/Networking/Chunk/ChunkSender.cs | 352 +++++++ HKMP/Networking/Chunk/ClientChunkReceiver.cs | 22 + HKMP/Networking/Chunk/ClientChunkSender.cs | 22 + HKMP/Networking/Chunk/ServerChunkReceiver.cs | 22 + HKMP/Networking/Chunk/ServerChunkSender.cs | 22 + .../Client/ClientConnectionManager.cs | 101 ++ .../Client/ClientConnectionStatus.cs | 19 + HKMP/Networking/Client/ClientTlsClient.cs | 8 + HKMP/Networking/Client/ClientUpdateManager.cs | 98 +- .../Client/ConnectionFailedResult.cs | 60 ++ HKMP/Networking/Client/DtlsClient.cs | 10 +- HKMP/Networking/Client/NetClient.cs | 354 +++---- HKMP/Networking/ConnectionManager.cs | 48 + HKMP/Networking/Packet/BasePacket.cs | 517 ++++++++++ .../Connection/ClientConnectionPacket.cs | 18 + .../Connection/ClientConnectionPacketId.cs | 11 + .../Connection/ServerConnectionPacket.cs | 18 + .../Connection/ServerConnectionPacketId.cs | 11 + .../Data/{LoginRequest.cs => ClientInfo.cs} | 21 +- HKMP/Networking/Packet/Data/ServerInfo.cs | 137 +++ HKMP/Networking/Packet/Data/SliceAckData.cs | 114 +++ HKMP/Networking/Packet/Data/SliceData.cs | 80 ++ HKMP/Networking/Packet/PacketManager.cs | 603 +++++++++-- .../Packet/Update/ClientUpdatePacket.cs | 56 ++ .../ClientUpdatePacketId.cs} | 84 +- .../Packet/Update/ServerUpdatePacket.cs | 36 + .../Packet/Update/ServerUpdatePacketId.cs | 71 ++ HKMP/Networking/Packet/Update/UpdatePacket.cs | 355 +++++++ HKMP/Networking/Packet/UpdatePacket.cs | 945 ------------------ HKMP/Networking/Server/DtlsServer.cs | 10 +- HKMP/Networking/Server/NetServer.cs | 250 +++-- HKMP/Networking/Server/NetServerClient.cs | 32 +- .../Server/ServerConnectionManager.cs | 128 +++ HKMP/Networking/Server/ServerUpdateManager.cs | 108 +- HKMP/Networking/ServerConnectionResult.cs | 19 + HKMP/Networking/UdpCongestionManager.cs | 3 +- HKMP/Networking/UdpUpdateManager.cs | 129 +-- HKMP/Ui/ConnectInterface.cs | 28 +- HKMP/Ui/UiManager.cs | 2 +- HKMP/Util/ThreadUtil.cs | 11 +- 49 files changed, 3656 insertions(+), 1849 deletions(-) create mode 100644 HKMP/Networking/Chunk/ChunkReceiver.cs create mode 100644 HKMP/Networking/Chunk/ChunkSender.cs create mode 100644 HKMP/Networking/Chunk/ClientChunkReceiver.cs create mode 100644 HKMP/Networking/Chunk/ClientChunkSender.cs create mode 100644 HKMP/Networking/Chunk/ServerChunkReceiver.cs create mode 100644 HKMP/Networking/Chunk/ServerChunkSender.cs create mode 100644 HKMP/Networking/Client/ClientConnectionManager.cs create mode 100644 HKMP/Networking/Client/ClientConnectionStatus.cs create mode 100644 HKMP/Networking/Client/ConnectionFailedResult.cs create mode 100644 HKMP/Networking/ConnectionManager.cs create mode 100644 HKMP/Networking/Packet/BasePacket.cs create mode 100644 HKMP/Networking/Packet/Connection/ClientConnectionPacket.cs create mode 100644 HKMP/Networking/Packet/Connection/ClientConnectionPacketId.cs create mode 100644 HKMP/Networking/Packet/Connection/ServerConnectionPacket.cs create mode 100644 HKMP/Networking/Packet/Connection/ServerConnectionPacketId.cs rename HKMP/Networking/Packet/Data/{LoginRequest.cs => ClientInfo.cs} (90%) create mode 100644 HKMP/Networking/Packet/Data/ServerInfo.cs create mode 100644 HKMP/Networking/Packet/Data/SliceAckData.cs create mode 100644 HKMP/Networking/Packet/Data/SliceData.cs create mode 100644 HKMP/Networking/Packet/Update/ClientUpdatePacket.cs rename HKMP/Networking/Packet/{PacketId.cs => Update/ClientUpdatePacketId.cs} (53%) create mode 100644 HKMP/Networking/Packet/Update/ServerUpdatePacket.cs create mode 100644 HKMP/Networking/Packet/Update/ServerUpdatePacketId.cs create mode 100644 HKMP/Networking/Packet/Update/UpdatePacket.cs delete mode 100644 HKMP/Networking/Packet/UpdatePacket.cs create mode 100644 HKMP/Networking/Server/ServerConnectionManager.cs create mode 100644 HKMP/Networking/ServerConnectionResult.cs diff --git a/HKMP/Animation/AnimationManager.cs b/HKMP/Animation/AnimationManager.cs index 931b3cd7..ef5f97be 100644 --- a/HKMP/Animation/AnimationManager.cs +++ b/HKMP/Animation/AnimationManager.cs @@ -1,4 +1,3 @@ -using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; @@ -13,15 +12,14 @@ using Hkmp.Networking.Client; using Hkmp.Networking.Packet; using Hkmp.Networking.Packet.Data; +using Hkmp.Networking.Packet.Update; using Hkmp.Util; using HutongGames.PlayMaker.Actions; using Modding; -using MonoMod.Cil; using UnityEngine; using UnityEngine.SceneManagement; using Logger = Hkmp.Logging.Logger; using Object = UnityEngine.Object; -using OpCodes = Mono.Cecil.Cil.OpCodes; using Random = UnityEngine.Random; namespace Hkmp.Animation; @@ -422,7 +420,7 @@ ServerSettings serverSettings _chargedEndEffectStopwatch = new Stopwatch(); // Register packet handler - packetManager.RegisterClientPacketHandler(ClientPacketId.PlayerDeath, + packetManager.RegisterClientUpdatePacketHandler(ClientUpdatePacketId.PlayerDeath, OnPlayerDeath); // Register scene change, which is where we update the animation event handler 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/Game/Client/ClientManager.cs b/HKMP/Game/Client/ClientManager.cs index 782a6d22..75167259 100644 --- a/HKMP/Game/Client/ClientManager.cs +++ b/HKMP/Game/Client/ClientManager.cs @@ -13,6 +13,7 @@ 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; @@ -214,29 +215,28 @@ ModSettings modSettings serverManager.AuthorizeKey(modSettings.AuthKey); // Register packet handlers - packetManager.RegisterClientPacketHandler(ClientPacketId.HelloClient, OnHelloClient); - packetManager.RegisterClientPacketHandler(ClientPacketId.ServerClientDisconnect, + packetManager.RegisterClientUpdatePacketHandler(ClientUpdatePacketId.ServerClientDisconnect, OnDisconnect); - packetManager.RegisterClientPacketHandler(ClientPacketId.PlayerConnect, OnPlayerConnect); - packetManager.RegisterClientPacketHandler(ClientPacketId.PlayerDisconnect, + packetManager.RegisterClientUpdatePacketHandler(ClientUpdatePacketId.PlayerConnect, OnPlayerConnect); + packetManager.RegisterClientUpdatePacketHandler(ClientUpdatePacketId.PlayerDisconnect, OnPlayerDisconnect); - packetManager.RegisterClientPacketHandler(ClientPacketId.PlayerEnterScene, + packetManager.RegisterClientUpdatePacketHandler(ClientUpdatePacketId.PlayerEnterScene, OnPlayerEnterScene); - packetManager.RegisterClientPacketHandler(ClientPacketId.PlayerAlreadyInScene, + packetManager.RegisterClientUpdatePacketHandler(ClientUpdatePacketId.PlayerAlreadyInScene, OnPlayerAlreadyInScene); - packetManager.RegisterClientPacketHandler(ClientPacketId.PlayerLeaveScene, + packetManager.RegisterClientUpdatePacketHandler(ClientUpdatePacketId.PlayerLeaveScene, OnPlayerLeaveScene); - packetManager.RegisterClientPacketHandler(ClientPacketId.PlayerUpdate, OnPlayerUpdate); - packetManager.RegisterClientPacketHandler(ClientPacketId.PlayerMapUpdate, + packetManager.RegisterClientUpdatePacketHandler(ClientUpdatePacketId.PlayerUpdate, OnPlayerUpdate); + packetManager.RegisterClientUpdatePacketHandler(ClientUpdatePacketId.PlayerMapUpdate, OnPlayerMapUpdate); - packetManager.RegisterClientPacketHandler(ClientPacketId.EntitySpawn, OnEntitySpawn); - packetManager.RegisterClientPacketHandler(ClientPacketId.EntityUpdate, OnEntityUpdate); - packetManager.RegisterClientPacketHandler(ClientPacketId.ReliableEntityUpdate, + packetManager.RegisterClientUpdatePacketHandler(ClientUpdatePacketId.EntitySpawn, OnEntitySpawn); + packetManager.RegisterClientUpdatePacketHandler(ClientUpdatePacketId.EntityUpdate, OnEntityUpdate); + packetManager.RegisterClientUpdatePacketHandler(ClientUpdatePacketId.ReliableEntityUpdate, OnReliableEntityUpdate); - packetManager.RegisterClientPacketHandler(ClientPacketId.SceneHostTransfer, OnSceneHostTransfer); - packetManager.RegisterClientPacketHandler(ClientPacketId.ServerSettingsUpdated, + packetManager.RegisterClientUpdatePacketHandler(ClientUpdatePacketId.SceneHostTransfer, OnSceneHostTransfer); + packetManager.RegisterClientUpdatePacketHandler(ClientUpdatePacketId.ServerSettingsUpdated, OnServerSettingsUpdated); - packetManager.RegisterClientPacketHandler(ClientPacketId.ChatMessage, OnChatMessage); + packetManager.RegisterClientUpdatePacketHandler(ClientUpdatePacketId.ChatMessage, OnChatMessage); // Register handlers for events from UI uiManager.RequestClientConnectEvent += (address, port, username, autoConnect) => { @@ -323,6 +323,8 @@ public void Disconnect() { /// Internal logic for disconnecting from the server. /// private void InternalDisconnect() { + Logger.Info("Disconnecting from server"); + _autoConnect = false; _netClient.Disconnect(); @@ -354,19 +356,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}"; @@ -417,47 +420,33 @@ private void OnChatInput(string message) { /// /// 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); - - _netClient.UpdateManager.SetHelloServerData(_username); - } - - /// - /// Callback method for when the HeroController is started so we can add the username to the player object. - /// - private void OnHeroControllerStart(On.HeroController.orig_Start orig, HeroController self) { - orig(self); - - if (_netClient.IsConnected) { - _playerManager.AddNameToPlayer( - HeroController.instance.gameObject, - _username, - _playerManager.LocalPlayerTeam - ); - } - } - - /// - /// Callback method for when we receive the HelloClient data. - /// - /// The HelloClient packet data. - private void OnHelloClient(HelloClient helloClient) { - Logger.Info("Received HelloClient from server"); + /// The server info received from the server. + private void OnClientConnect(ServerInfo serverInfo) { + Logger.Info("Received server info from server"); + // Relay the addon order from the server info to the addon manager + _addonManager.UpdateNetworkedAddonOrder(serverInfo.AddonOrder); + // If this was not an auto-connect, we set save data. Otherwise, we know we already have the save data. if (!_autoConnect) { - _saveManager.SetSaveWithData(helloClient.CurrentSave); + _saveManager.SetSaveWithData(serverInfo.CurrentSave); _uiManager.EnterGameFromMultiplayerMenu(); } // 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 != null && HeroController.instance.gameObject != null) { + _playerManager.AddNameToPlayer( + HeroController.instance.gameObject, + _username, + _playerManager.LocalPlayerTeam + ); + } + try { ConnectEvent?.Invoke(); } catch (Exception e) { @@ -465,6 +454,23 @@ 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(On.HeroController.orig_Start orig, HeroController self) { + Logger.Debug($"OnHeroControllerStart called, netclient connected: {_netClient.IsConnected}"); + + orig(self); + + if (_netClient.IsConnected) { + _playerManager.AddNameToPlayer( + HeroController.instance.gameObject, + _username, + _playerManager.LocalPlayerTeam + ); + } + } /// /// Callback method for when we receive a server disconnect. diff --git a/HKMP/Game/Client/PlayerManager.cs b/HKMP/Game/Client/PlayerManager.cs index ff2ec592..7ce1f495 100644 --- a/HKMP/Game/Client/PlayerManager.cs +++ b/HKMP/Game/Client/PlayerManager.cs @@ -5,10 +5,10 @@ using Hkmp.Game.Settings; using Hkmp.Networking.Packet; using Hkmp.Networking.Packet.Data; +using Hkmp.Networking.Packet.Update; using Hkmp.Ui.Resources; using Hkmp.Util; -using Mono.Cecil.Cil; -using MonoMod.Cil; +using Modding.Utils; using TMPro; using UnityEngine; using Logger = Hkmp.Logging.Logger; @@ -105,9 +105,9 @@ Dictionary playerData }; // Register packet handlers - packetManager.RegisterClientPacketHandler(ClientPacketId.PlayerTeamUpdate, + packetManager.RegisterClientUpdatePacketHandler(ClientUpdatePacketId.PlayerTeamUpdate, OnPlayerTeamUpdate); - packetManager.RegisterClientPacketHandler(ClientPacketId.PlayerSkinUpdate, + packetManager.RegisterClientUpdatePacketHandler(ClientUpdatePacketId.PlayerSkinUpdate, OnPlayerSkinUpdate); } @@ -486,7 +486,7 @@ public void AddNameToPlayer(GameObject playerContainer, string name, Team team = nameObject = CreateUsername(playerContainer); } - var textMeshObject = nameObject.GetComponent(); + var textMeshObject = nameObject.GetOrAddComponent(); if (textMeshObject != null) { textMeshObject.text = name.ToUpper(); diff --git a/HKMP/Game/Client/Save/SaveManager.cs b/HKMP/Game/Client/Save/SaveManager.cs index d584dadc..2ca10adc 100644 --- a/HKMP/Game/Client/Save/SaveManager.cs +++ b/HKMP/Game/Client/Save/SaveManager.cs @@ -8,6 +8,7 @@ using Hkmp.Networking.Client; using Hkmp.Networking.Packet; using Hkmp.Networking.Packet.Data; +using Hkmp.Networking.Packet.Update; using Hkmp.Util; using Modding; using UnityEngine; @@ -115,7 +116,7 @@ public void Initialize() { MonoBehaviourUtil.Instance.OnUpdateEvent += OnUpdatePersistents; MonoBehaviourUtil.Instance.OnUpdateEvent += OnUpdateCompounds; - _packetManager.RegisterClientPacketHandler(ClientPacketId.SaveUpdate, UpdateSaveWithData); + _packetManager.RegisterClientUpdatePacketHandler(ClientUpdatePacketId.SaveUpdate, UpdateSaveWithData); foreach (var field in typeof(PlayerData).GetFields()) { var fieldName = field.Name; diff --git a/HKMP/Game/Command/Client/AddonCommand.cs b/HKMP/Game/Command/Client/AddonCommand.cs index 726bae30..b0bc8cc8 100644 --- a/HKMP/Game/Command/Client/AddonCommand.cs +++ b/HKMP/Game/Command/Client/AddonCommand.cs @@ -70,7 +70,7 @@ public void Execute(string[] arguments) { return; } - if (_netClient.IsConnected || _netClient.IsConnecting) { + if (_netClient.ConnectionStatus != ClientConnectionStatus.NotConnected) { UiManager.InternalChatBox.AddMessage("Cannot toggle addons while connecting or connected to a server."); return; } diff --git a/HKMP/Game/Server/ServerManager.cs b/HKMP/Game/Server/ServerManager.cs index f7ebde28..ef4cc335 100644 --- a/HKMP/Game/Server/ServerManager.cs +++ b/HKMP/Game/Server/ServerManager.cs @@ -2,7 +2,6 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; -using System.Net; using Hkmp.Animation; using Hkmp.Api.Command.Server; using Hkmp.Api.Eventing.ServerEvents; @@ -15,8 +14,10 @@ using Hkmp.Game.Server.Auth; using Hkmp.Game.Settings; using Hkmp.Logging; +using Hkmp.Networking; using Hkmp.Networking.Packet; using Hkmp.Networking.Packet.Data; +using Hkmp.Networking.Packet.Update; using Hkmp.Networking.Server; namespace Hkmp.Game.Server; @@ -33,6 +34,11 @@ internal abstract class ServerManager : IServerManager { /// private const string AuthorizedFileName = "authorized.json"; + /// + /// The maximum length of a username, for validation purposes in multiple places. + /// + public const int MaxUsernameLength = 20; + /// /// The net server instance. /// @@ -138,21 +144,20 @@ PacketManager packetManager _banList = BanList.LoadFromFile(); // Register packet handlers - packetManager.RegisterServerPacketHandler(ServerPacketId.HelloServer, OnHelloServer); - packetManager.RegisterServerPacketHandler(ServerPacketId.PlayerEnterScene, + packetManager.RegisterServerUpdatePacketHandler(ServerUpdatePacketId.PlayerEnterScene, OnClientEnterScene); - packetManager.RegisterServerPacketHandler(ServerPacketId.PlayerLeaveScene, OnClientLeaveScene); - packetManager.RegisterServerPacketHandler(ServerPacketId.PlayerUpdate, OnPlayerUpdate); - packetManager.RegisterServerPacketHandler(ServerPacketId.PlayerMapUpdate, + packetManager.RegisterServerUpdatePacketHandler(ServerUpdatePacketId.PlayerLeaveScene, OnClientLeaveScene); + packetManager.RegisterServerUpdatePacketHandler(ServerUpdatePacketId.PlayerUpdate, OnPlayerUpdate); + packetManager.RegisterServerUpdatePacketHandler(ServerUpdatePacketId.PlayerMapUpdate, OnPlayerMapUpdate); - packetManager.RegisterServerPacketHandler(ServerPacketId.EntitySpawn, OnEntitySpawn); - packetManager.RegisterServerPacketHandler(ServerPacketId.EntityUpdate, OnEntityUpdate); - packetManager.RegisterServerPacketHandler(ServerPacketId.ReliableEntityUpdate, + packetManager.RegisterServerUpdatePacketHandler(ServerUpdatePacketId.EntitySpawn, OnEntitySpawn); + packetManager.RegisterServerUpdatePacketHandler(ServerUpdatePacketId.EntityUpdate, OnEntityUpdate); + packetManager.RegisterServerUpdatePacketHandler(ServerUpdatePacketId.ReliableEntityUpdate, OnReliableEntityUpdate); - packetManager.RegisterServerPacketHandler(ServerPacketId.PlayerDisconnect, OnPlayerDisconnect); - packetManager.RegisterServerPacketHandler(ServerPacketId.PlayerDeath, OnPlayerDeath); - packetManager.RegisterServerPacketHandler(ServerPacketId.ChatMessage, OnChatMessage); - packetManager.RegisterServerPacketHandler(ServerPacketId.SaveUpdate, OnSaveUpdate); + packetManager.RegisterServerUpdatePacketHandler(ServerUpdatePacketId.PlayerDisconnect, OnPlayerDisconnect); + packetManager.RegisterServerUpdatePacketHandler(ServerUpdatePacketId.PlayerDeath, OnPlayerDeath); + packetManager.RegisterServerUpdatePacketHandler(ServerUpdatePacketId.ChatMessage, OnChatMessage); + packetManager.RegisterServerUpdatePacketHandler(ServerUpdatePacketId.SaveUpdate, OnSaveUpdate); // Register a timeout handler _netServer.ClientTimeoutEvent += OnClientTimeout; @@ -160,8 +165,8 @@ PacketManager packetManager // Register server shutdown handler _netServer.ShutdownEvent += OnServerShutdown; - // Register a handler for when a client wants to login - _netServer.LoginRequestEvent += OnLoginRequest; + // Register a handler for when a client wants to connect + _netServer.ConnectionRequestEvent += OnConnectionRequest; } #region Internal server manager methods @@ -240,61 +245,6 @@ public void OnUpdateServerSettings() { _netServer.SetDataForAllClients(updateManager => { updateManager.UpdateServerSettings(InternalServerSettings); }); } - /// - /// Callback method for when HelloServer data is received from a client. - /// - /// The ID of the client. - /// The HelloServer packet data. - private void OnHelloServer(ushort id, HelloServer helloServer) { - Logger.Info($"Received HelloServer data from ({id}, {helloServer.Username})"); - - // Start by sending the new client the current Server Settings - _netServer.GetUpdateManagerForClient(id)?.UpdateServerSettings(InternalServerSettings); - - if (!_playerData.TryGetValue(id, out var playerData)) { - Logger.Warn($"Could not find player data for ({id}, {helloServer.Username})"); - return; - } - - var clientInfo = new List<(ushort, string)>(); - - foreach (var idPlayerDataPair in _playerData) { - var otherId = idPlayerDataPair.Key; - if (otherId == id) { - continue; - } - - var otherPd = idPlayerDataPair.Value; - - clientInfo.Add((otherId, otherPd.Username)); - - // If the other player has an active map icon, we also send that to the new player - if (otherPd.HasMapIcon) { - _netServer.GetUpdateManagerForClient(id).UpdatePlayerMapIcon(otherId, true); - if (otherPd.MapPosition != null) { - _netServer.GetUpdateManagerForClient(id).UpdatePlayerMapPosition(otherId, otherPd.MapPosition); - } - } - - // Send to the other players that this client has just connected - _netServer.GetUpdateManagerForClient(otherId)?.AddPlayerConnectData( - id, - helloServer.Username - ); - } - - _netServer.GetUpdateManagerForClient(id).SetHelloClientData( - ServerSaveData.GetMergedSaveData(playerData.AuthKey), - clientInfo - ); - - try { - PlayerConnectEvent?.Invoke(playerData); - } catch (Exception e) { - Logger.Error($"Exception thrown while invoking PlayerConnect event:\n{e}"); - } - } - /// /// Callback method for when a player enters a scene. /// @@ -1099,76 +1049,79 @@ private void OnServerShutdown() { /// /// Handle a login request for a client that has invalid addons. /// - /// The update manager for the client. - private void HandleInvalidLoginAddons(ServerUpdateManager updateManager) { - var loginResponse = new LoginResponse { - LoginResponseStatus = LoginResponseStatus.InvalidAddons - }; - loginResponse.AddonData.AddRange(AddonManager.GetNetworkedAddonData()); - - updateManager.SetLoginResponse(loginResponse); + /// The server info instance to send to the client containing the invalid addons + /// result. + private void HandleInvalidLoginAddons(ServerInfo serverInfo) { + serverInfo.ConnectionResult = ServerConnectionResult.InvalidAddons; + serverInfo.AddonData.AddRange(AddonManager.GetNetworkedAddonData()); } /// /// Method for handling a login request for a new client. /// - /// The ID of the client. - /// The IP endpoint of the client. - /// The LoginRequest packet data. - /// The update manager for the client. - /// true if the login request was approved, false otherwise. - private bool OnLoginRequest( - ushort id, - IPEndPoint endPoint, - LoginRequest loginRequest, - ServerUpdateManager updateManager - ) { - Logger.Info($"Received login request from IP: {endPoint.Address}, username: {loginRequest.Username}"); - - if (_banList.IsIpBanned(endPoint.Address.ToString()) || _banList.Contains(loginRequest.AuthKey)) { - updateManager.SetLoginResponse(new LoginResponse { - LoginResponseStatus = LoginResponseStatus.Banned - }); - return false; + /// The net server client that tries to connect. + /// The information from the client. + /// The server info instance to modify based on whether the client should be accepted + /// or not. + private void OnConnectionRequest(NetServerClient netServerClient, ClientInfo clientInfo, ServerInfo serverInfo) { + Logger.Info($"Received connection request from IP: {netServerClient.EndPoint}, username: {clientInfo.Username}"); + + if (_banList.IsIpBanned(netServerClient.EndPoint.Address.ToString()) || _banList.Contains(clientInfo.AuthKey)) { + Logger.Debug(" Client is banned from the server, rejected connection"); + + serverInfo.ConnectionResult = ServerConnectionResult.RejectedOther; + serverInfo.ConnectionRejectedMessage = "Banned from the server"; + return; } if (_whiteList.IsEnabled) { - if (!_whiteList.Contains(loginRequest.AuthKey)) { - if (!_whiteList.IsPreListed(loginRequest.Username)) { - updateManager.SetLoginResponse(new LoginResponse { - LoginResponseStatus = LoginResponseStatus.NotWhiteListed - }); - return false; + if (!_whiteList.Contains(clientInfo.AuthKey)) { + if (!_whiteList.IsPreListed(clientInfo.Username)) { + Logger.Debug(" Client is not whitelisted on the server, rejected connection"); + + serverInfo.ConnectionResult = ServerConnectionResult.RejectedOther; + serverInfo.ConnectionRejectedMessage = "Not whitelisted on the server"; + return; } Logger.Info(" Username was pre-listed, auth key has been added to whitelist"); - _whiteList.Add(loginRequest.AuthKey); - _whiteList.RemovePreList(loginRequest.Username); + _whiteList.Add(clientInfo.AuthKey); + _whiteList.RemovePreList(clientInfo.Username); } } // Check whether the username is valid - foreach (var character in loginRequest.Username) { + if (clientInfo.Username.Length > MaxUsernameLength) { + Logger.Debug(" Client has username that is too long, rejected connection"); + + serverInfo.ConnectionResult = ServerConnectionResult.RejectedOther; + serverInfo.ConnectionRejectedMessage = "Invalid username"; + return; + } + + foreach (var character in clientInfo.Username) { if (!char.IsLetterOrDigit(character)) { - updateManager.SetLoginResponse(new LoginResponse { - LoginResponseStatus = LoginResponseStatus.InvalidUsername - }); - return false; + Logger.Debug(" Client has invalid characters in username, rejected connection"); + + serverInfo.ConnectionResult = ServerConnectionResult.RejectedOther; + serverInfo.ConnectionRejectedMessage = "Invalid username"; + return; } } // Check whether the username is not already in use foreach (var existingPlayerData in _playerData.Values) { - if (existingPlayerData.Username.ToLower().Equals(loginRequest.Username.ToLower())) { - updateManager.SetLoginResponse(new LoginResponse { - LoginResponseStatus = LoginResponseStatus.InvalidUsername - }); - return false; + if (existingPlayerData.Username.ToLower().Equals(clientInfo.Username.ToLower())) { + Logger.Debug(" Client username is already in use, rejected connection"); + + serverInfo.ConnectionResult = ServerConnectionResult.RejectedOther; + serverInfo.ConnectionRejectedMessage = "Username already in use"; + return; } } - var addonData = loginRequest.AddonData; + var addonData = clientInfo.AddonData; // Construct a string that contains all addons and respective versions by mapping the items in the addon data var addonStringList = string.Join(", ", addonData.Select(addon => $"{addon.Identifier} v{addon.Version}")); @@ -1177,8 +1130,10 @@ ServerUpdateManager updateManager // If there is a mismatch between the number of networked addons of the client and the server, // we can immediately invalidate the request if (addonData.Count != AddonManager.GetNetworkedAddonData().Count) { - HandleInvalidLoginAddons(updateManager); - return false; + Logger.Debug(" Client addons are invalid, rejected connection"); + + HandleInvalidLoginAddons(serverInfo); + return; } // Create a byte list denoting the order of the addons on the server @@ -1191,10 +1146,12 @@ ServerUpdateManager updateManager addon.Version, out var correspondingServerAddon )) { + Logger.Debug(" Client addons are invalid, rejected connection"); + // There was no corresponding server addon, so we send a login response with an invalid status // and the addon data that is present on the server, so the client knows what is invalid - HandleInvalidLoginAddons(updateManager); - return false; + HandleInvalidLoginAddons(serverInfo); + return; } if (!correspondingServerAddon.Id.HasValue) { @@ -1204,25 +1161,55 @@ out var correspondingServerAddon // If the addon is also present on the server, we append the addon order with the correct index addonOrder.Add(correspondingServerAddon.Id.Value); } + + Logger.Debug(" Accepting client connection, preparing server info"); + + // Finally after all the checks, the client is accepted, and we note that in the server info + serverInfo.ConnectionResult = ServerConnectionResult.Accepted; + serverInfo.AddonOrder = addonOrder.ToArray(); + + // Construct the player info to send to the new client in the server info + var playerInfo = new List<(ushort, string)>(); - var loginResponse = new LoginResponse { - LoginResponseStatus = LoginResponseStatus.Success, - AddonOrder = addonOrder.ToArray() - }; + foreach (var idPlayerDataPair in _playerData) { + var otherId = idPlayerDataPair.Key; + if (otherId == netServerClient.Id) { + continue; + } + + var otherPd = idPlayerDataPair.Value; + + playerInfo.Add((otherId, otherPd.Username)); + + // Send to the other players that this client has just connected + _netServer.GetUpdateManagerForClient(otherId)?.AddPlayerConnectData( + netServerClient.Id, + clientInfo.Username + ); + } - updateManager.SetLoginResponse(loginResponse); + serverInfo.PlayerInfo = playerInfo; + + // Obtain the save data for the connecting client and add it to the server info + serverInfo.CurrentSave = new CurrentSave { + SaveData = ServerSaveData.GetMergedSaveData(clientInfo.AuthKey) + }; // Create new player data and store it var playerData = new ServerPlayerData( - id, - endPoint.Address.ToString(), - loginRequest.Username, - loginRequest.AuthKey, + netServerClient.Id, + netServerClient.EndPoint.ToString(), + clientInfo.Username, + clientInfo.AuthKey, _authorizedList ); - _playerData[id] = playerData; - - return true; + _playerData[netServerClient.Id] = playerData; + + try { + PlayerConnectEvent?.Invoke(playerData); + } catch (Exception e) { + Logger.Error($"Exception thrown while invoking PlayerConnect event:\n{e}"); + } } /// diff --git a/HKMP/Networking/Chunk/ChunkReceiver.cs b/HKMP/Networking/Chunk/ChunkReceiver.cs new file mode 100644 index 00000000..945d5ac5 --- /dev/null +++ b/HKMP/Networking/Chunk/ChunkReceiver.cs @@ -0,0 +1,181 @@ +using System; +using Hkmp.Logging; +using Hkmp.Networking.Packet.Data; + +namespace Hkmp.Networking.Chunk; + +/// +/// Class that processes and manages chunks by receiving slices of those chunks and sending acknowledgements for those +/// slices. +/// +internal abstract class ChunkReceiver { + /// + /// Boolean array where each value indicates whether the slice of the same index was received. + /// + private readonly bool[] _received; + /// + /// Byte array that contains (parts of) the chunk data that is received. + /// + private readonly byte[] _chunkData; + + /// + /// Whether we are currently receiving a chunk. If not, receiving a slice containing a chunk ID that is one higher + /// that the last received chunk will start the reception process again. + /// + private bool _isReceiving; + /// + /// The currently (if receiving) or last received (when not receiving) chunk ID. + /// + private byte _chunkId = 255; + /// + /// The size of the chunk that we are currently receiving. Only calculated when the last slice is received, since + /// that is the only slice with a different slice size. + /// + private int _chunkSize; + /// + /// The number of slices that the chunk we are currently receiving contains. Set whenever we receive the first + /// slice in a chunk. + /// + private int _numSlices; + /// + /// The number of slices we have received so far. Used to keep track when all slices are received. + /// + private int _numReceivedSlices; + + /// + /// Event that is called when the entirety of a chunk is received. + /// + public event Action ChunkReceivedEvent; + + /// + /// Construct the chunk receiver by allocating the readonly arrays with their maximally used lengths. + /// + protected ChunkReceiver() { + _received = new bool[ConnectionManager.MaxSlicesPerChunk]; + _chunkData = new byte[ConnectionManager.MaxChunkSize]; + } + + /// + /// Process received slice data by checking whether we have not yet received this slice and adding it to the data + /// array and marking it received. If this is the first slice received in this chunk we note that we are + /// receiving, set the number of slices we expect to receive and increment the currently receiving chunk ID. + /// If this is the last slice in the chunk we invoke the event that an entire chunk is received. + /// + /// The received slice data. + public void ProcessReceivedData(SliceData sliceData) { + Logger.Debug($"Received slice packet: {sliceData.ChunkId}, {sliceData.SliceId}, {sliceData.NumSlices}"); + + // We check if the received chunk ID is smaller than the current chunk ID accounting for wrapping IDs + if (ConnectionManager.IsWrappingIdSmaller(sliceData.ChunkId, _chunkId)) { + Logger.Debug("Chunk ID of received slice packet is smaller than currently receiving chunk"); + return; + } + + if (!_isReceiving) { + if (sliceData.ChunkId == (byte) (_chunkId + 1)) { + Logger.Debug($"Received new chunk with ID: {sliceData.ChunkId}"); + SoftReset(); + + _chunkId += 1; + _isReceiving = true; + _numSlices = sliceData.NumSlices; + } else if (sliceData.ChunkId == _chunkId) { + Logger.Debug("Already received all slices, resending ack packet"); + SendAckData(); + return; + } else { + Logger.Debug($"Received old chunk: {_chunkId}, ignoring"); + return; + } + } else { + // If the received number of slices does not match the number slices we are keeping track of, we discard + // the slice altogether as it is likely not correct + if (_numSlices != sliceData.NumSlices) { + Logger.Debug("Number of slices in slice packet does not correspond with local number of slices"); + return; + } + } + + if (_received[sliceData.SliceId]) { + Logger.Debug($"Received duplicate slice: {sliceData.SliceId}, ignoring"); + return; + } + + _numReceivedSlices += 1; + _received[sliceData.SliceId] = true; + + // Copy over the data from the received slice into the chunk data array at the correct position + Array.Copy( + sliceData.Data, + 0, + _chunkData, + sliceData.SliceId * ConnectionManager.MaxSliceSize, + sliceData.Data.Length + ); + + SendAckData(); + + // If this is the last slice in the chunk, we can calculate the chunk size + if (sliceData.SliceId == _numSlices - 1) { + _chunkSize = (_numSlices - 1) * ConnectionManager.MaxSliceSize + sliceData.Data.Length; + Logger.Debug($"Received last slice in chunk, chunk size: {_chunkSize}"); + } + + if (_numReceivedSlices == _numSlices) { + var byteArray = new byte[_chunkSize]; + Array.Copy( + _chunkData, + 0, + byteArray, + 0, + _chunkSize + ); + var packet = new Packet.Packet(byteArray); + + ChunkReceivedEvent?.Invoke(packet); + + _isReceiving = false; + } + } + + /// + /// Reset the chunk receiver so it can be used for a new connection. This will reset most variables to their + /// default values. + /// + public void Reset() { + SoftReset(); + + _isReceiving = false; + _chunkId = 255; + } + + /// + /// Send acknowledgement data containing the boolean array of all slices that have been acknowledged thus far. + /// + private void SendAckData() { + var acked = new bool[_numSlices]; + Array.Copy(_received, acked, _numSlices); + + SetSliceAckData(_chunkId, (ushort) _numSlices, acked); + } + + /// + /// Soft reset the chunk receiver by clearing the array of received slices and setting chunk size, number of + /// slices, and number of received slices to 0. + /// + private void SoftReset() { + Array.Clear(_received, 0, _received.Length); + + _chunkSize = 0; + _numSlices = 0; + _numReceivedSlices = 0; + } + + /// + /// Set the slice ack data in the corresponding update manager for sending. + /// + /// The ID of the chunk for this acknowledgement. + /// The number of slices in this chunk. + /// The boolean array containing acknowledgements of all slices. + protected abstract void SetSliceAckData(byte chunkId, ushort numSlices, bool[] acked); +} diff --git a/HKMP/Networking/Chunk/ChunkSender.cs b/HKMP/Networking/Chunk/ChunkSender.cs new file mode 100644 index 00000000..f6a2c216 --- /dev/null +++ b/HKMP/Networking/Chunk/ChunkSender.cs @@ -0,0 +1,352 @@ +using System; +using System.Collections.Concurrent; +using System.Diagnostics; +using System.Threading; +using Hkmp.Logging; +using Hkmp.Networking.Packet.Data; + +namespace Hkmp.Networking.Chunk; + +/// +/// Class that processes and manages chunks by sending slices of those chunks and receiving acknowledgements for those +/// slices. +/// +internal abstract class ChunkSender { + /// + /// The number of milliseconds to wait between sending slices. + /// + private const int WaitMillisBetweenSlices = 20; + /// + /// The number of milliseconds to wait before re-sending a slice. + /// + private const int WaitMillisResendSlice = 100; + + /// + /// Blocking collection of packets that need to be sent as chunks. + /// + private readonly BlockingCollection _toSendPackets; + + /// + /// Boolean array where each value indicates whether the slice of the same index was acknowledged. + /// + private readonly bool[] _acked; + /// + /// Byte array that contains the chunk data that needs to be sent. + /// + private readonly byte[] _chunkData; + + /// + /// Manual reset event that is used for its wait handle to time when to send the next slice. + /// + private readonly ManualResetEventSlim _sliceWaitHandle; + + /// + /// Whether we are currently sending a chunk. If we are not sending anything, we ignore incoming chunk + /// acknowledgements. + /// + private bool _isSending; + /// + /// The ID of the chunk we are currently sending. + /// + private byte _chunkId; + /// + /// The size of the chunk we are currently sending. + /// + private int _chunkSize; + /// + /// The number of slices of the chunk we are currently sending. + /// + private int _numSlices; + /// + /// The number of acknowledged slices in the currently sending chunk. + /// + private int _numAckedSlices; + /// + /// The ID of the slice we are currently sending. + /// + private int _currentSliceId; + + /// + /// Array of stopwatches that keep track of the elapsed time since we have last sent the slice with the same ID. + /// If this time is smaller than a certain threshold, we do not send the slice again yet. + /// + private readonly Stopwatch[] _sliceStopwatches; + + /// + /// Cancellation token source for cancelling the send task. + /// + private CancellationTokenSource _sendTaskTokenSource; + + /// + /// Event that is called when we finish sending data. This is registered internally when the + /// method is called and we are waiting for the current chunk to finish sending. + /// + private event Action FinishSendingDataEvent; + + /// + /// Construct the chunk sender by initializing the blocking collection and manual reset event, and allocating the + /// arrays to their maximally used length. + /// + protected ChunkSender() { + _toSendPackets = new BlockingCollection(); + + _acked = new bool[ConnectionManager.MaxSlicesPerChunk]; + _chunkData = new byte[ConnectionManager.MaxChunkSize]; + _sliceStopwatches = new Stopwatch[ConnectionManager.MaxSlicesPerChunk]; + + _sliceWaitHandle = new ManualResetEventSlim(); + } + + /// + /// Start the chunk sender by starting the thread that manages the chunk sending. + /// + public void Start() { + _sendTaskTokenSource?.Cancel(); + _sendTaskTokenSource?.Dispose(); + _sendTaskTokenSource = new CancellationTokenSource(); + + new Thread(() => StartSends(_sendTaskTokenSource.Token)).Start(); + } + + /// + /// Stop the chunk sender by cancelling the send task. + /// + public void Stop() { + _sendTaskTokenSource?.Cancel(); + _sendTaskTokenSource?.Dispose(); + _sendTaskTokenSource = null; + } + + /// + /// Finish sending data and call the given callback whenever the data is finished sending. + /// + /// The callback to invoke. + public void FinishSendingData(Action callback) { + // If we aren't currently sending and the queue does not contain any packets to send, we immediately invoke + // the callback and return + if (!_isSending && _toSendPackets.Count == 0) { + callback?.Invoke(); + return; + } + + // Otherwise, we register the event + // We do it like this so we can deregister the event immediately after it is called, so it doesn't trigger + // more than once + Action lambda = null; + lambda = () => { + callback?.Invoke(); + FinishSendingDataEvent -= lambda; + }; + FinishSendingDataEvent += lambda; + } + + /// + /// Enqueue a packet to be sent as a chunk. + /// + /// The packet to send. + public void EnqueuePacket(Packet.Packet packet) { + _toSendPackets.Add(packet); + } + + /// + /// Process received slice acknowledgement data. First does sanity checks to see if we are actually sending a + /// chunk, whether the received chunk ID matches the currently sending chunk ID, and whether the number of slices + /// matches. Then for each of the slice indices in the acknowledgement array, it checks whether this is a newly + /// acknowledged slice and locally marks it as acknowledged. + /// + /// The received slice acknowledgement data. + public void ProcessReceivedData(SliceAckData sliceAckData) { + Logger.Debug($"Received slice ack packet: {sliceAckData.ChunkId}, {sliceAckData.NumSlices}"); + + if (!_isSending) { + Logger.Debug("Not sending a chunk, ignoring ack packet"); + return; + } + + if (_chunkId != sliceAckData.ChunkId) { + Logger.Debug("Chunk ID of received ack packet does not correspond with currently sending chunk"); + return; + } + + if (_numSlices != sliceAckData.NumSlices) { + Logger.Debug("Number of slices in ack packet does not correspond with local number of slices"); + return; + } + + for (var i = 0; i < _numSlices; i++) { + if (sliceAckData.Acked[i] && !_acked[i]) { + _acked[i] = true; + _numAckedSlices += 1; + + Logger.Debug($"Received acknowledgement for slice {i}, total acked: {_numAckedSlices}"); + } + } + } + + /// + /// Start the sending process with the given cancellation token. + /// We block on the collection to take a new packet to start sending. Once a packet is taken from the collection, + /// we calculate the chunk size and number of slices that we need to send. Then, wee go over the slices in + /// ascending order and send one with a given delay between each slice. Each slice that is acknowledged already + /// is skipped in the sending order. If we have already sent a given slice less than a certain threshold ago, we + /// also skip sending it. Once all slices have been acknowledged, we go back to blocking on the collection to wait + /// for a new chunk to send. + /// + /// The cancellation token for cancelling the sending process. + private void StartSends(CancellationToken cancellationToken) { + while (!cancellationToken.IsCancellationRequested) { + if (_toSendPackets.Count == 0) { + FinishSendingDataEvent?.Invoke(); + } + + Packet.Packet packet; + try { + packet = _toSendPackets.Take(cancellationToken); + } catch (OperationCanceledException) { + continue; + } + + _isSending = true; + + Logger.Debug("Successfully taken new packet from blocking collection, starting networking chunk"); + + Array.Clear(_sliceStopwatches, 0, _sliceStopwatches.Length); + _numAckedSlices = 0; + + var packetBytes = packet.ToArray(); + + _chunkSize = packetBytes.Length; + _numSlices = _chunkSize / ConnectionManager.MaxSliceSize; + if (_chunkSize % ConnectionManager.MaxSliceSize != 0) { + _numSlices += 1; + } + + Logger.Debug($"ChunkSize: {_chunkSize}, NumSlices: {_numSlices}"); + + // Skip over chunks that exceed the maximum size that our system can handle + if (_chunkSize > ConnectionManager.MaxChunkSize) { + Logger.Error($"Could not send packet that exceeds max chunk size: {_chunkSize}"); + continue; + } + + // Copy the raw bytes from the packet into the chunk data array + Array.Copy(packetBytes, _chunkData, _chunkSize); + + do { + Logger.Debug($"Sending next slice: {_currentSliceId}"); + SendNextSlice(); + + // Obtain (or create) the stopwatch for the slice and start it + var sliceStopwatch = _sliceStopwatches[_currentSliceId]; + if (sliceStopwatch == null) { + sliceStopwatch = new Stopwatch(); + _sliceStopwatches[_currentSliceId] = sliceStopwatch; + } + + sliceStopwatch.Restart(); + + if (!TryGetNextSliceToSend()) { + Logger.Debug($"All slices have been acked ({_numAckedSlices}), stopping sending slices"); + break; + } + + long waitMillisNextSlice; + + // Get the stopwatch for this slice, and check whether we have already sent this slice not too long ago + // If so, we wait longer before resending the slice. Otherwise, we default to the normal send rate. + sliceStopwatch = _sliceStopwatches[_currentSliceId]; + if (sliceStopwatch == null) { + waitMillisNextSlice = WaitMillisBetweenSlices; + } else { + waitMillisNextSlice = WaitMillisResendSlice - sliceStopwatch.ElapsedMilliseconds; + if (waitMillisNextSlice < 0) { + waitMillisNextSlice = WaitMillisBetweenSlices; + } + } + + Logger.Debug($"Waiting on handle for next slice: {waitMillisNextSlice}"); + try { + _sliceWaitHandle.Wait((int) waitMillisNextSlice, cancellationToken); + } catch (OperationCanceledException) { + Logger.Debug("Wait operation was cancelled, breaking"); + break; + } + } while (!cancellationToken.IsCancellationRequested); + + Logger.Debug($"Incrementing chunk ID to: {_chunkId + 1}"); + _chunkId += 1; + _isSending = false; + } + + Logger.Debug("Resetting values of chunk sender"); + + // The loop is over when cancellation is requested, so we reset the variables after it + _isSending = false; + _chunkId = 0; + _chunkSize = 0; + _numSlices = 0; + _numAckedSlices = 0; + _currentSliceId = 0; + + for (var i = 0; i < _sliceStopwatches.Length; i++) { + _sliceStopwatches[i]?.Stop(); + _sliceStopwatches[i] = null; + } + } + + /// + /// Send the next slice, whose ID is . This will figure out the start index of the + /// data in the array and copy the data into a new array for adding to the update packet. + /// + private void SendNextSlice() { + var startIndex = _currentSliceId * ConnectionManager.MaxSliceSize; + + byte[] sliceBytes; + // Figure out if the start index for the next slice would exceed the chunk size. If so, the length of the slice + // is less than the maximum slice size, which we need to calculate + if ((_currentSliceId + 1) * ConnectionManager.MaxSliceSize > _chunkSize) { + var length = _chunkSize - startIndex; + sliceBytes = new byte[length]; + + Array.Copy(_chunkData, startIndex, sliceBytes, 0, length); + } else { + sliceBytes = new byte[ConnectionManager.MaxSliceSize]; + + Array.Copy(_chunkData, startIndex, sliceBytes, 0, sliceBytes.Length); + } + + SetSliceData(_chunkId, (byte) _currentSliceId, (byte) _numSlices, sliceBytes); + } + + /// + /// Try to get the next slice ID that we need to send. We simply iterate in ascending order over slice IDs until + /// we find one that is not yet acknowledged. Each iteration we check whether the number of acknowledged slices + /// equals the number of slices in the chunk, so we don't end up in an infinite loop. + /// + /// True if a next slice could be found, false if all slices are acknowledged. + private bool TryGetNextSliceToSend() { + do { + // We do the check inside the loop to prevent multi-thread issues where another ack is received and + // a non-acked slice cannot be found anywhere, resulting in an infinite while loop + if (_numAckedSlices == _numSlices) { + return false; + } + + _currentSliceId += 1; + if (_currentSliceId >= _numSlices) { + _currentSliceId = 0; + } + } while (_acked[_currentSliceId]); + + return true; + } + + /// + /// Set the slice data in the corresponding update manager for sending. + /// + /// The ID of the chunk for this slice. + /// The ID of the slice. + /// The number of slices in this chunk. + /// The byte array containing the data of the slice. + protected abstract void SetSliceData(byte chunkId, byte sliceId, byte numSlices, byte[] data); +} diff --git a/HKMP/Networking/Chunk/ClientChunkReceiver.cs b/HKMP/Networking/Chunk/ClientChunkReceiver.cs new file mode 100644 index 00000000..77c00d2c --- /dev/null +++ b/HKMP/Networking/Chunk/ClientChunkReceiver.cs @@ -0,0 +1,22 @@ +using Hkmp.Networking.Client; + +namespace Hkmp.Networking.Chunk; + +/// +/// Specialization class of for the client-side chunk receiver. +/// +internal class ClientChunkReceiver : ChunkReceiver { + /// + /// The client update manager instance used for adding slice ack data to the update packet. + /// + private readonly ClientUpdateManager _updateManager; + + public ClientChunkReceiver(ClientUpdateManager updateManager) { + _updateManager = updateManager; + } + + /// + protected override void SetSliceAckData(byte chunkId, ushort numSlices, bool[] acked) { + _updateManager.SetSliceAckData(chunkId, numSlices, acked); + } +} diff --git a/HKMP/Networking/Chunk/ClientChunkSender.cs b/HKMP/Networking/Chunk/ClientChunkSender.cs new file mode 100644 index 00000000..2709f7aa --- /dev/null +++ b/HKMP/Networking/Chunk/ClientChunkSender.cs @@ -0,0 +1,22 @@ +using Hkmp.Networking.Client; + +namespace Hkmp.Networking.Chunk; + +/// +/// Specialization class of for the client-side chunk receiver. +/// +internal class ClientChunkSender : ChunkSender { + /// + /// The client update manager instance used for adding slice data to the update packet. + /// + private readonly ClientUpdateManager _updateManager; + + public ClientChunkSender(ClientUpdateManager updateManager) { + _updateManager = updateManager; + } + + /// + protected override void SetSliceData(byte chunkId, byte sliceId, byte numSlices, byte[] data) { + _updateManager.SetSliceData(chunkId, sliceId, numSlices, data); + } +} diff --git a/HKMP/Networking/Chunk/ServerChunkReceiver.cs b/HKMP/Networking/Chunk/ServerChunkReceiver.cs new file mode 100644 index 00000000..51bf5a32 --- /dev/null +++ b/HKMP/Networking/Chunk/ServerChunkReceiver.cs @@ -0,0 +1,22 @@ +using Hkmp.Networking.Server; + +namespace Hkmp.Networking.Chunk; + +/// +/// Specialization class of for the server-side chunk receiver. +/// +internal class ServerChunkReceiver : ChunkReceiver { + /// + /// The server update manager instance used for adding slice ack data to the update packet. + /// + private readonly ServerUpdateManager _updateManager; + + public ServerChunkReceiver(ServerUpdateManager updateManager) { + _updateManager = updateManager; + } + + /// + protected override void SetSliceAckData(byte chunkId, ushort numSlices, bool[] acked) { + _updateManager.SetSliceAckData(chunkId, numSlices, acked); + } +} diff --git a/HKMP/Networking/Chunk/ServerChunkSender.cs b/HKMP/Networking/Chunk/ServerChunkSender.cs new file mode 100644 index 00000000..e54587c2 --- /dev/null +++ b/HKMP/Networking/Chunk/ServerChunkSender.cs @@ -0,0 +1,22 @@ +using Hkmp.Networking.Server; + +namespace Hkmp.Networking.Chunk; + +/// +/// Specialization class of for the server-side chunk receiver. +/// +internal class ServerChunkSender : ChunkSender { + /// + /// The server update manager instance used for adding slice data to the update packet. + /// + private readonly ServerUpdateManager _updateManager; + + public ServerChunkSender(ServerUpdateManager updateManager) { + _updateManager = updateManager; + } + + /// + protected override void SetSliceData(byte chunkId, byte sliceId, byte numSlices, byte[] data) { + _updateManager.SetSliceData(chunkId, sliceId, numSlices, data); + } +} diff --git a/HKMP/Networking/Client/ClientConnectionManager.cs b/HKMP/Networking/Client/ClientConnectionManager.cs new file mode 100644 index 00000000..6af5524e --- /dev/null +++ b/HKMP/Networking/Client/ClientConnectionManager.cs @@ -0,0 +1,101 @@ +using System; +using System.Collections.Generic; +using Hkmp.Logging; +using Hkmp.Networking.Chunk; +using Hkmp.Networking.Packet; +using Hkmp.Networking.Packet.Connection; +using Hkmp.Networking.Packet.Data; + +namespace Hkmp.Networking.Client; + +/// +/// Client-side manager for handling the initial connection to the server. +/// +internal class ClientConnectionManager : ConnectionManager { + /// + /// The client-side chunk sender used to handle sending chunks. + /// + private readonly ClientChunkSender _chunkSender; + /// + /// The client-side chunk received used to receive chunks. + /// + private readonly ClientChunkReceiver _chunkReceiver; + + /// + /// Event that is called when server info is received from the server we are trying to connect to. + /// + public event Action ServerInfoReceivedEvent; + + /// + /// Construct the connection manager with the given packet manager and chunk sender, and receiver instances. + /// Will register handlers in the packet manager that relate to the connection. + /// + public ClientConnectionManager( + PacketManager packetManager, + ClientChunkSender chunkSender, + ClientChunkReceiver chunkReceiver + ) : base(packetManager) { + _chunkSender = chunkSender; + _chunkReceiver = chunkReceiver; + + packetManager.RegisterClientConnectionPacketHandler( + ClientConnectionPacketId.ServerInfo, + OnServerInfoReceived + ); + _chunkReceiver.ChunkReceivedEvent += OnChunkReceived; + } + + /// + /// Start establishing the connection to the server with the given information. + /// + /// The username of the player. + /// The authentication key of the player. + /// List of addon data that represents the enabled networked addons that the client uses. + /// + public void StartConnection(string username, string authKey, List addonData) { + Logger.Debug("StartConnection"); + + // Create a connection packet that will be the entire chunk we will be sending + var connectionPacket = new ServerConnectionPacket(); + + // Set the client info data in the connection packet + connectionPacket.SetSendingPacketData(ServerConnectionPacketId.ClientInfo, new ClientInfo { + Username = username, + AuthKey = authKey, + AddonData = addonData + }); + + // Create the raw packet from the connection packet + var packet = new Packet.Packet(); + connectionPacket.CreatePacket(packet); + + // Enqueue the raw packet to be sent using the chunk sender + _chunkSender.EnqueuePacket(packet); + } + + /// + /// Callback method for when server info is received from the server. + /// + /// The server info instance received from the server. + private void OnServerInfoReceived(ServerInfo serverInfo) { + Logger.Debug($"ServerInfo received, connection accepted: {serverInfo.ConnectionResult}"); + + ServerInfoReceivedEvent?.Invoke(serverInfo); + } + + /// + /// Callback method for when a new chunk is received from the server. + /// + /// The raw packet that contains the data from the chunk. + private void OnChunkReceived(Packet.Packet packet) { + // Create the connection packet instance and try to read it + var connectionPacket = new ClientConnectionPacket(); + if (!connectionPacket.ReadPacket(packet)) { + Logger.Debug("Received malformed connection packet chunk from server"); + return; + } + + // Let the packet manager handle the connection packet, which will invoke the relevant data handlers + PacketManager.HandleClientConnectionPacket(connectionPacket); + } +} diff --git a/HKMP/Networking/Client/ClientConnectionStatus.cs b/HKMP/Networking/Client/ClientConnectionStatus.cs new file mode 100644 index 00000000..aaad49e5 --- /dev/null +++ b/HKMP/Networking/Client/ClientConnectionStatus.cs @@ -0,0 +1,19 @@ +namespace Hkmp.Networking.Client; + +/// +/// Enumeration of connection statuses for the client. +/// +internal enum ClientConnectionStatus { + /// + /// Not connected to any server. + /// + NotConnected, + /// + /// Trying to establish a connection to a server. + /// + Connecting, + /// + /// Connected to a server. + /// + Connected +} diff --git a/HKMP/Networking/Client/ClientTlsClient.cs b/HKMP/Networking/Client/ClientTlsClient.cs index 649f52b9..37e4bd46 100644 --- a/HKMP/Networking/Client/ClientTlsClient.cs +++ b/HKMP/Networking/Client/ClientTlsClient.cs @@ -38,6 +38,14 @@ protected override int[] GetSupportedCipherSuites() { return SupportedCipherSuites; } + /// + /// The maximum time the handshake can take in milliseconds before timing out. + /// + /// The integer value of the timeout in milliseconds. + public override int GetHandshakeTimeoutMillis() { + return DtlsClient.DtlsHandshakeTimeoutMillis; + } + /// /// /// Get the authentication implementation for this TLS client that handles providing client credentials and diff --git a/HKMP/Networking/Client/ClientUpdateManager.cs b/HKMP/Networking/Client/ClientUpdateManager.cs index 715ba55e..bc89f775 100644 --- a/HKMP/Networking/Client/ClientUpdateManager.cs +++ b/HKMP/Networking/Client/ClientUpdateManager.cs @@ -1,24 +1,15 @@ -using System.Collections.Generic; using Hkmp.Animation; using Hkmp.Game.Client.Entity; using Hkmp.Math; -using Hkmp.Networking.Packet; using Hkmp.Networking.Packet.Data; -using Org.BouncyCastle.Tls; +using Hkmp.Networking.Packet.Update; namespace Hkmp.Networking.Client; /// /// Specialization of for client to server packet sending. /// -internal class ClientUpdateManager : UdpUpdateManager { - /// - /// Construct the update manager a DTLS transport instance. - /// - /// The DTLS transport instance for sending data. - public ClientUpdateManager(DtlsTransport dtlsTransport) : base(dtlsTransport) { - } - +internal class ClientUpdateManager : UdpUpdateManager { /// public override void ResendReliableData(ServerUpdatePacket lostPacket) { lock (Lock) { @@ -32,30 +23,50 @@ public override void ResendReliableData(ServerUpdatePacket lostPacket) { /// The existing or new PlayerUpdate instance. private PlayerUpdate FindOrCreatePlayerUpdate() { if (!CurrentUpdatePacket.TryGetSendingPacketData( - ServerPacketId.PlayerUpdate, + ServerUpdatePacketId.PlayerUpdate, out var packetData)) { packetData = new PlayerUpdate(); - CurrentUpdatePacket.SetSendingPacketData(ServerPacketId.PlayerUpdate, packetData); + CurrentUpdatePacket.SetSendingPacketData(ServerUpdatePacketId.PlayerUpdate, packetData); } return (PlayerUpdate) packetData; } /// - /// Set the login request data in the current packet. + /// Set slice data in the current packet. /// - /// The username of the client. - /// The auth key of the client. - /// A list of addon data of the client. - public void SetLoginRequestData(string username, string authKey, List addonData) { + /// The ID of the chunk the slice belongs to. + /// The ID of the slice within the chunk. + /// The number of slices in the chunk. + /// The raw data in the slice as a byte array. + public void SetSliceData(byte chunkId, byte sliceId, byte numSlices, byte[] data) { lock (Lock) { - var loginRequest = new LoginRequest { - Username = username, - AuthKey = authKey + var sliceData = new SliceData { + ChunkId = chunkId, + SliceId = sliceId, + NumSlices = numSlices, + Data = data }; - loginRequest.AddonData.AddRange(addonData); - CurrentUpdatePacket.SetSendingPacketData(ServerPacketId.LoginRequest, loginRequest); + CurrentUpdatePacket.SetSendingPacketData(ServerUpdatePacketId.Slice, sliceData); + } + } + + /// + /// Set slice acknowledgement data in the current packet. + /// + /// The ID of the chunk the slice belongs to. + /// The number of slices in the chunk. + /// A boolean array containing whether a certain slice in the chunk was acknowledged. + public void SetSliceAckData(byte chunkId, ushort numSlices, bool[] acked) { + lock (Lock) { + var sliceAckData = new SliceAckData { + ChunkId = chunkId, + NumSlices = numSlices, + Acked = acked + }; + + CurrentUpdatePacket.SetSendingPacketData(ServerUpdatePacketId.SliceAck, sliceAckData); } } @@ -102,11 +113,11 @@ public void UpdatePlayerMapPosition(Vector2 mapPosition) { public void UpdatePlayerMapIcon(bool hasIcon) { lock (Lock) { if (!CurrentUpdatePacket.TryGetSendingPacketData( - ServerPacketId.PlayerMapUpdate, + ServerUpdatePacketId.PlayerMapUpdate, out var packetData )) { packetData = new PlayerMapUpdate(); - CurrentUpdatePacket.SetSendingPacketData(ServerPacketId.PlayerMapUpdate, packetData); + CurrentUpdatePacket.SetSendingPacketData(ServerUpdatePacketId.PlayerMapUpdate, packetData); } ((PlayerMapUpdate) packetData).HasIcon = hasIcon; @@ -147,11 +158,11 @@ public void SetEntitySpawn(ushort id, EntityType spawningType, EntityType spawne PacketDataCollection entitySpawnCollection; // Check if there is an existing data collection or create one if not - if (CurrentUpdatePacket.TryGetSendingPacketData(ServerPacketId.EntitySpawn, out var packetData)) { + if (CurrentUpdatePacket.TryGetSendingPacketData(ServerUpdatePacketId.EntitySpawn, out var packetData)) { entitySpawnCollection = (PacketDataCollection) packetData; } else { entitySpawnCollection = new PacketDataCollection(); - CurrentUpdatePacket.SetSendingPacketData(ServerPacketId.EntitySpawn, entitySpawnCollection); + CurrentUpdatePacket.SetSendingPacketData(ServerUpdatePacketId.EntitySpawn, entitySpawnCollection); } entitySpawnCollection.DataInstances.Add(new EntitySpawn { @@ -174,8 +185,8 @@ public void SetEntitySpawn(ushort id, EntityType spawningType, EntityType spawne PacketDataCollection entityUpdateCollection; var packetId = typeof(T) == typeof(EntityUpdate) - ? ServerPacketId.EntityUpdate - : ServerPacketId.ReliableEntityUpdate; + ? ServerUpdatePacketId.EntityUpdate + : ServerUpdatePacketId.ReliableEntityUpdate; // First check whether there actually exists entity data at all if (CurrentUpdatePacket.TryGetSendingPacketData( @@ -313,22 +324,7 @@ public void AddEntityHostFsmData(ushort entityId, byte fsmIndex, EntityHostFsmDa /// public void SetPlayerDisconnect() { lock (Lock) { - CurrentUpdatePacket.SetSendingPacketData(ServerPacketId.PlayerDisconnect, new EmptyData()); - } - } - - /// - /// Set hello server data in the current packet. - /// - /// The username of the player. - public void SetHelloServerData(string username) { - lock (Lock) { - CurrentUpdatePacket.SetSendingPacketData( - ServerPacketId.HelloServer, - new HelloServer { - Username = username - } - ); + CurrentUpdatePacket.SetSendingPacketData(ServerUpdatePacketId.PlayerDisconnect, new EmptyData()); } } @@ -347,7 +343,7 @@ ushort animationClipId ) { lock (Lock) { CurrentUpdatePacket.SetSendingPacketData( - ServerPacketId.PlayerEnterScene, + ServerUpdatePacketId.PlayerEnterScene, new ServerPlayerEnterScene { NewSceneName = sceneName, Position = position, @@ -363,7 +359,7 @@ ushort animationClipId /// public void SetLeftScene() { lock (Lock) { - CurrentUpdatePacket.SetSendingPacketData(ServerPacketId.PlayerLeaveScene, new ReliableEmptyData()); + CurrentUpdatePacket.SetSendingPacketData(ServerUpdatePacketId.PlayerLeaveScene, new ReliableEmptyData()); } } @@ -372,7 +368,7 @@ public void SetLeftScene() { /// public void SetDeath() { lock (Lock) { - CurrentUpdatePacket.SetSendingPacketData(ServerPacketId.PlayerDeath, new ReliableEmptyData()); + CurrentUpdatePacket.SetSendingPacketData(ServerUpdatePacketId.PlayerDeath, new ReliableEmptyData()); } } @@ -382,7 +378,7 @@ public void SetDeath() { /// The string message. public void SetChatMessage(string message) { lock (Lock) { - CurrentUpdatePacket.SetSendingPacketData(ServerPacketId.ChatMessage, new ChatMessage { + CurrentUpdatePacket.SetSendingPacketData(ServerUpdatePacketId.ChatMessage, new ChatMessage { Message = message }); } @@ -397,11 +393,11 @@ public void SetSaveUpdate(ushort index, byte[] value) { lock (Lock) { PacketDataCollection saveUpdateCollection; - if (CurrentUpdatePacket.TryGetSendingPacketData(ServerPacketId.SaveUpdate, out var packetData)) { + if (CurrentUpdatePacket.TryGetSendingPacketData(ServerUpdatePacketId.SaveUpdate, out var packetData)) { saveUpdateCollection = (PacketDataCollection) packetData; } else { saveUpdateCollection = new PacketDataCollection(); - CurrentUpdatePacket.SetSendingPacketData(ServerPacketId.SaveUpdate, saveUpdateCollection); + CurrentUpdatePacket.SetSendingPacketData(ServerUpdatePacketId.SaveUpdate, saveUpdateCollection); } saveUpdateCollection.DataInstances.Add(new SaveUpdate { diff --git a/HKMP/Networking/Client/ConnectionFailedResult.cs b/HKMP/Networking/Client/ConnectionFailedResult.cs new file mode 100644 index 00000000..64178a38 --- /dev/null +++ b/HKMP/Networking/Client/ConnectionFailedResult.cs @@ -0,0 +1,60 @@ +using System.Collections.Generic; +using Hkmp.Networking.Packet.Data; + +namespace Hkmp.Networking.Client; + +/// +/// Class that encapsulates the result of a failed connection. +/// +internal class ConnectionFailedResult { + /// + /// The reason that the connection failed. + /// + public ConnectionFailedReason Reason { get; init; } +} + +/// +/// Specialization class of for invalid addons. +/// +internal class ConnectionInvalidAddonsResult : ConnectionFailedResult { + /// + /// The list of addon data that the server uses to compare against for clients. + /// + public List AddonData { get; init; } +} + +/// +/// Specialization class of for a generic failed connection with message. +/// +internal class ConnectionFailedMessageResult : ConnectionFailedResult { + /// + /// The string message that describes the reason the connection failed. + /// + public string Message { get; init; } +} + +/// +/// Enumeration of reasons why the connection failed. +/// +internal enum ConnectionFailedReason { + /// + /// The client and server addon do not match. + /// + InvalidAddons, + /// + /// The connection timed out (took too long to establish). + /// + TimedOut, + /// + /// A socket exception occurred while trying to establish the connection. + /// + SocketException, + /// + /// An IO exception occurred while trying to establish the connection. + /// + IOException, + /// + /// The reason is miscellaneous. + /// + Other, +} diff --git a/HKMP/Networking/Client/DtlsClient.cs b/HKMP/Networking/Client/DtlsClient.cs index 30f256e4..c73d972c 100644 --- a/HKMP/Networking/Client/DtlsClient.cs +++ b/HKMP/Networking/Client/DtlsClient.cs @@ -17,6 +17,11 @@ internal class DtlsClient { /// public const int MaxPacketSize = 1400; + /// + /// The maximum time the DTLS handshake can take in milliseconds before timing out. + /// + public const int DtlsHandshakeTimeoutMillis = 5000; + /// /// The socket instance for the underlying networking. /// @@ -80,11 +85,12 @@ public void Connect(string address, int port) { try { DtlsTransport = clientProtocol.Connect(_tlsClient, _clientDatagramTransport); + } catch (TlsTimeoutException) { + _clientDatagramTransport.Close(); + throw; } catch (IOException e) { Logger.Error($"IO exception when connecting DTLS client:\n{e}"); - _clientDatagramTransport.Close(); - throw; } diff --git a/HKMP/Networking/Client/NetClient.cs b/HKMP/Networking/Client/NetClient.cs index 9fa1d860..f7778f5d 100644 --- a/HKMP/Networking/Client/NetClient.cs +++ b/HKMP/Networking/Client/NetClient.cs @@ -6,9 +6,12 @@ using Hkmp.Api.Client; using Hkmp.Api.Client.Networking; using Hkmp.Logging; +using Hkmp.Networking.Chunk; using Hkmp.Networking.Packet; using Hkmp.Networking.Packet.Data; +using Hkmp.Networking.Packet.Update; using Hkmp.Util; +using Org.BouncyCastle.Tls; namespace Hkmp.Networking.Client; @@ -25,17 +28,17 @@ internal class NetClient : INetClient { /// /// The client update manager for this net client. /// - public ClientUpdateManager UpdateManager { get; private set; } + public ClientUpdateManager UpdateManager { get; } /// /// Event that is called when the client connects to a server. /// - public event Action ConnectEvent; + public event Action ConnectEvent; /// /// Event that is called when the client fails to connect to a server. /// - public event Action ConnectFailedEvent; + public event Action ConnectFailedEvent; /// /// Event that is called when the client disconnects from a server. @@ -48,14 +51,12 @@ internal class NetClient : INetClient { public event Action TimeoutEvent; /// - /// Boolean denoting whether the client is connected to a server. + /// The connection status of the client. /// - public bool IsConnected { get; private set; } - - /// - /// Boolean denoting whether the client is currently attempting connection. - /// - public bool IsConnecting { get; private set; } + public ClientConnectionStatus ConnectionStatus { get; private set; } = ClientConnectionStatus.NotConnected; + + /// + public bool IsConnected => ConnectionStatus == ClientConnectionStatus.Connected; /// /// The DTLS client instance for handling DTLS connections. @@ -63,9 +64,18 @@ internal class NetClient : INetClient { private readonly DtlsClient _dtlsClient; /// - /// Cancellation token source for the update task. + /// Chunk sender instance for sending large amounts of data. + /// + private readonly ClientChunkSender _chunkSender; + /// + /// Chunk receiver instance for receiving large amounts of data. /// - private CancellationTokenSource _updateTaskTokenSource; + private readonly ClientChunkReceiver _chunkReceiver; + + /// + /// The client connection manager responsible for handling sending and receiving connection data. + /// + private readonly ClientConnectionManager _connectionManager; /// /// Byte array containing received data that was not included in a packet object yet. @@ -81,53 +91,89 @@ public NetClient(PacketManager packetManager) { _dtlsClient = new DtlsClient(); + UpdateManager = new ClientUpdateManager(); + + _chunkSender = new ClientChunkSender(UpdateManager); + _chunkReceiver = new ClientChunkReceiver(UpdateManager); + _connectionManager = new ClientConnectionManager(_packetManager, _chunkSender, _chunkReceiver); + _dtlsClient.DataReceivedEvent += OnReceiveData; + _connectionManager.ServerInfoReceivedEvent += OnServerInfoReceived; } - + /// - /// Callback method for when the client receives a login response from a server connection. + /// Starts establishing a connection with the given host on the given port. /// - /// The LoginResponse packet data. - private void OnConnect(LoginResponse loginResponse) { - Logger.Debug("Connection to server success"); + /// The address of the host to connect to. + /// The port of the host to connect to. + /// The username of the client. + /// The auth key of the client. + /// A list of addon data that the client has. + public void Connect( + string address, + int port, + string username, + string authKey, + List addonData + ) { + Logger.Debug($"Trying to connect NetClient to '{address}:{port}'"); + ConnectionStatus = ClientConnectionStatus.Connecting; + + // Start a new thread for establishing the connection, otherwise Unity will hang + new Thread(() => { + try { + _dtlsClient.Connect(address, port); + } catch (TlsTimeoutException) { + Logger.Info("DTLS connection timed out"); + + ConnectFailedEvent?.Invoke(new ConnectionFailedResult { + Reason = ConnectionFailedReason.TimedOut + }); + } catch (SocketException e) { + Logger.Error($"Failed to connect due to SocketException:\n{e}"); + + ConnectFailedEvent?.Invoke(new ConnectionFailedResult { + Reason = ConnectionFailedReason.SocketException + }); + return; + } catch (IOException e) { + Logger.Error($"Failed to connect due to IOException:\n{e}"); + + ConnectFailedEvent?.Invoke(new ConnectionFailedResult { + Reason = ConnectionFailedReason.IOException + }); + return; + } - // De-register the "connect failed" and register the actual timeout handler if we time out - UpdateManager.OnTimeout -= OnConnectTimedOut; - UpdateManager.OnTimeout += () => { ThreadUtil.RunActionOnMainThread(() => { TimeoutEvent?.Invoke(); }); }; + UpdateManager.DtlsTransport = _dtlsClient.DtlsTransport; + // During the connection process we register the connection failed callback if we time out + UpdateManager.TimeoutEvent += OnConnectTimedOut; - // Invoke callback if it exists on the main thread of Unity - ThreadUtil.RunActionOnMainThread(() => { ConnectEvent?.Invoke(loginResponse); }); + UpdateManager.StartUpdates(); - IsConnected = true; - IsConnecting = false; + Logger.Debug("Starting connection with connection manager"); + _chunkSender.Start(); + _connectionManager.StartConnection(username, authKey, addonData); + }).Start(); } /// - /// Callback method for when the client connection times out. - /// - private void OnConnectTimedOut() => OnConnectFailed(new ConnectFailedResult { - Type = ConnectFailedResult.FailType.TimedOut - }); - - /// - /// Callback method for when the client connection fails. + /// Disconnect from the current server. /// - /// The connection failed result. - private void OnConnectFailed(ConnectFailedResult result) { - Logger.Debug($"Connection to server failed, cause: {result.Type}"); - - UpdateManager?.StopUpdates(); + public void Disconnect() { + UpdateManager.StopUpdates(); + _chunkSender.Stop(); + _chunkReceiver.Reset(); + + _dtlsClient.Disconnect(); - IsConnected = false; - IsConnecting = false; + ConnectionStatus = ClientConnectionStatus.NotConnected; - // Request cancellation for the update task - _updateTaskTokenSource?.Cancel(); - _updateTaskTokenSource?.Dispose(); - _updateTaskTokenSource = null; + // Clear all client addon packet handlers, because their IDs become invalid + _packetManager.ClearClientAddonUpdatePacketHandlers(); - // Invoke callback if it exists on the main thread of Unity - ThreadUtil.RunActionOnMainThread(() => { ConnectFailedEvent?.Invoke(result); }); + // Invoke callback if it exists + DisconnectEvent?.Invoke(); } /// @@ -138,150 +184,96 @@ private void OnConnectFailed(ConnectFailedResult result) { /// Byte array containing the received bytes. /// The number of bytes in the . private void OnReceiveData(byte[] buffer, int length) { + if (ConnectionStatus == ClientConnectionStatus.NotConnected) { + Logger.Error("Client is not connected to a server, but received data, ignoring"); + return; + } + var packets = PacketManager.HandleReceivedData(buffer, length, ref _leftoverData); foreach (var packet in packets) { // Create a ClientUpdatePacket from the raw packet instance, // and read the values into it - var clientUpdatePacket = new ClientUpdatePacket(packet); - if (!clientUpdatePacket.ReadPacket()) { + var clientUpdatePacket = new ClientUpdatePacket(); + if (!clientUpdatePacket.ReadPacket(packet)) { // If ReadPacket returns false, we received a malformed packet, which we simply ignore for now continue; } - UpdateManager.OnReceivePacket(clientUpdatePacket); - - // If we are not yet connected we check whether this packet contains a login response, - // so we can finish connecting - if (!IsConnected) { - if (clientUpdatePacket.GetPacketData().TryGetValue( - ClientPacketId.LoginResponse, - out var packetData) - ) { - var loginResponse = (LoginResponse) packetData; - - switch (loginResponse.LoginResponseStatus) { - case LoginResponseStatus.Success: - OnConnect(loginResponse); - break; - case LoginResponseStatus.NotWhiteListed: - OnConnectFailed(new ConnectFailedResult { - Type = ConnectFailedResult.FailType.NotWhiteListed - }); - return; - case LoginResponseStatus.Banned: - OnConnectFailed(new ConnectFailedResult { - Type = ConnectFailedResult.FailType.Banned - }); - return; - case LoginResponseStatus.InvalidAddons: - OnConnectFailed(new ConnectFailedResult { - Type = ConnectFailedResult.FailType.InvalidAddons, - AddonData = loginResponse.AddonData - }); - return; - case LoginResponseStatus.InvalidUsername: - OnConnectFailed(new ConnectFailedResult { - Type = ConnectFailedResult.FailType.InvalidUsername - }); - return; - default: - OnConnectFailed(new ConnectFailedResult { - Type = ConnectFailedResult.FailType.Unknown - }); - return; - } - - break; - } + UpdateManager.OnReceivePacket(clientUpdatePacket); + + // First check for slice or slice ack data and handle it separately by passing it onto either the chunk + // sender or chunk receiver + var packetData = clientUpdatePacket.GetPacketData(); + if (packetData.TryGetValue(ClientUpdatePacketId.Slice, out var sliceData)) { + packetData.Remove(ClientUpdatePacketId.Slice); + _chunkReceiver.ProcessReceivedData((SliceData) sliceData); + } + + if (packetData.TryGetValue(ClientUpdatePacketId.SliceAck, out var sliceAckData)) { + packetData.Remove(ClientUpdatePacketId.SliceAck); + _chunkSender.ProcessReceivedData((SliceAckData) sliceAckData); } - _packetManager.HandleClientPacket(clientUpdatePacket); + // Then, if we are already connected to a server, we let the packet manager handle the rest of the packet + // data + if (ConnectionStatus == ClientConnectionStatus.Connected) { + _packetManager.HandleClientUpdatePacket(clientUpdatePacket); + } } } - - /// - /// Starts establishing a connection with the given host on the given port. - /// - /// The address of the host to connect to. - /// The port of the host to connect to. - /// The username of the client. - /// The auth key of the client. - /// A list of addon data that the client has. - public void Connect( - string address, - int port, - string username, - string authKey, - List addonData - ) { - Logger.Debug($"Trying to connect NetClient to '{address}:{port}'"); - IsConnecting = true; - - try { - _dtlsClient.Connect(address, port); - } catch (SocketException e) { - Logger.Error($"Failed to connect due to SocketException:\n{e}"); - - OnConnectFailed(new ConnectFailedResult { - Type = ConnectFailedResult.FailType.SocketException - }); - return; - } catch (IOException e) { - Logger.Error($"Failed to connect due to IOException:\n{e}"); - - OnConnectFailed(new ConnectFailedResult { - Type = ConnectFailedResult.FailType.SocketException - }); + + private void OnServerInfoReceived(ServerInfo serverInfo) { + if (serverInfo.ConnectionResult == ServerConnectionResult.Accepted) { + Logger.Debug("Connection to server accepted"); + + // De-register the "connect failed" and register the actual timeout handler if we time out + UpdateManager.TimeoutEvent -= OnConnectTimedOut; + UpdateManager.TimeoutEvent += () => { + ThreadUtil.RunActionOnMainThread(() => { TimeoutEvent?.Invoke(); }); + }; + + // Invoke callback if it exists on the main thread of Unity + ThreadUtil.RunActionOnMainThread(() => { ConnectEvent?.Invoke(serverInfo); }); + + ConnectionStatus = ClientConnectionStatus.Connected; return; } - UpdateManager = new ClientUpdateManager(_dtlsClient.DtlsTransport); - // During the connection process we register the connection failed callback if we time out - UpdateManager.OnTimeout += OnConnectTimedOut; - - _updateTaskTokenSource = new CancellationTokenSource(); + ConnectionFailedResult result; + + if (serverInfo.ConnectionResult == ServerConnectionResult.InvalidAddons) { + Logger.Debug("Connection to server failed due to invalid addons"); + + result = new ConnectionInvalidAddonsResult { + Reason = ConnectionFailedReason.InvalidAddons, + AddonData = serverInfo.AddonData + }; + } else if (serverInfo.ConnectionResult == ServerConnectionResult.RejectedOther) { + Logger.Debug($"Connection to server failed, message: {serverInfo.ConnectionRejectedMessage}"); + + result = new ConnectionFailedMessageResult { + Reason = ConnectionFailedReason.Other, + Message = serverInfo.ConnectionRejectedMessage + }; + } else { + throw new NotImplementedException("Unknown connection result in server info"); + } - new Thread(() => { - var cancellationToken = _updateTaskTokenSource.Token; - - while (!cancellationToken.IsCancellationRequested) { - UpdateManager.ProcessUpdate(); + UpdateManager?.StopUpdates(); - // TODO: figure out a good way to get rid of the sleep here - // some way to signal when clients should be updated again would suffice - // also see NetServer#StartClientUpdates - Thread.Sleep(5); - } - }).Start(); + ConnectionStatus = ClientConnectionStatus.NotConnected; - UpdateManager.StartUpdates(); - - UpdateManager.SetLoginRequestData(username, authKey, addonData); - Logger.Debug("Sending login request"); + // Invoke callback if it exists on the main thread of Unity + ThreadUtil.RunActionOnMainThread(() => { ConnectFailedEvent?.Invoke(result); }); } - + /// - /// Disconnect from the current server. + /// Callback method for when the client connection times out. /// - public void Disconnect() { - UpdateManager.StopUpdates(); - - _dtlsClient.Disconnect(); - - IsConnected = false; - - // Request cancellation for the update task - _updateTaskTokenSource?.Cancel(); - _updateTaskTokenSource?.Dispose(); - _updateTaskTokenSource = null; - - // Clear all client addon packet handlers, because their IDs become invalid - _packetManager.ClearClientAddonPacketHandlers(); - - // Invoke callback if it exists - DisconnectEvent?.Invoke(); - } + private void OnConnectTimedOut() => ConnectFailedEvent?.Invoke(new ConnectionFailedResult { + Reason = ConnectionFailedReason.TimedOut + }); /// public IClientAddonNetworkSender GetNetworkSender( @@ -349,31 +341,3 @@ Func packetInstantiator return addon.NetworkReceiver as IClientAddonNetworkReceiver; } } - -/// -/// Class that stores the result of a failed connection. -/// -internal class ConnectFailedResult { - /// - /// The type of the failed connection. - /// - public FailType Type { get; set; } - - /// - /// If the type for failing is having invalid addons, this field contains the addon data that we should have. - /// - public List AddonData { get; set; } - - /// - /// Enumeration of fail types. - /// - public enum FailType { - NotWhiteListed, - Banned, - InvalidAddons, - InvalidUsername, - TimedOut, - SocketException, - Unknown - } -} diff --git a/HKMP/Networking/ConnectionManager.cs b/HKMP/Networking/ConnectionManager.cs new file mode 100644 index 00000000..bf857283 --- /dev/null +++ b/HKMP/Networking/ConnectionManager.cs @@ -0,0 +1,48 @@ +using Hkmp.Networking.Packet; + +namespace Hkmp.Networking; + +/// +/// Abstract base class that manages handling the initial connection to a server. +/// +internal abstract class ConnectionManager { + /// + /// The maximum size that a slice can be in bytes. + /// + public const int MaxSliceSize = 1024; + + /// + /// The maximum number of slices in a chunk. + /// + public const int MaxSlicesPerChunk = 256; + + /// + /// The maximum size of a chunk in bytes. + /// + public const int MaxChunkSize = MaxSliceSize * MaxSlicesPerChunk; + + /// + /// The number of milliseconds a connection attempt can maximally take before being timed out. + /// + public const int TimeoutMillis = 60000; + + /// + /// The packet manager instance to register handlers for slice and slice ack data. + /// + protected readonly PacketManager PacketManager; + + protected ConnectionManager(PacketManager packetManager) { + PacketManager = packetManager; + } + + /// + /// Check whether the first ID is smaller than the second ID. Accounts for ID wrap-around, by inverse comparison + /// if differences are larger than half of the ID number space. + /// + /// The first ID as a byte. + /// The second ID as a byte. + /// True if the first ID is smaller than the second ID, false otherwise. + public static bool IsWrappingIdSmaller(byte id1, byte id2) { + return id1 < id2 && id2 - id1 <= 128 || id1 > id2 && id1 - id2 > 128; + } +} diff --git a/HKMP/Networking/Packet/BasePacket.cs b/HKMP/Networking/Packet/BasePacket.cs new file mode 100644 index 00000000..3212abcb --- /dev/null +++ b/HKMP/Networking/Packet/BasePacket.cs @@ -0,0 +1,517 @@ +using System; +using System.Collections.Generic; +using Hkmp.Logging; + +namespace Hkmp.Networking.Packet; + +/// +/// Abstract base class for the packets containing structured packet data. +/// +/// The enum type for packet IDs in this packet. +internal abstract class BasePacket where TPacketId : Enum { + // ReSharper disable once StaticMemberInGenericType + /// + /// A dictionary containing addon packet info per addon ID in order to read and convert raw addon + /// packet data into IPacketData instances. + /// + public static Dictionary AddonPacketInfoDict { get; } = new(); + + // TODO: refactor these dictionaries into a class that contains them for readability + /// + /// Normal non-resend packet data. + /// + protected readonly Dictionary NormalPacketData; + + /// + /// Packet data from addons indexed by their ID. + /// + protected readonly Dictionary AddonPacketData; + + /// + /// The combination of normal and resent packet data cached in case it needs to be queried multiple times. + /// + protected Dictionary CachedAllPacketData; + + /// + /// The combination of addon and resent addon data cached in case it needs to be queried multiple times. + /// + protected Dictionary CachedAllAddonData; + + /// + /// Whether the dictionary containing all packet data is cached already or needs to be calculated first. + /// + protected bool IsAllPacketDataCached; + + /// + /// Whether this packet contains data that needs to be reliable. + /// + public bool ContainsReliableData { get; protected set; } + + /// + /// Construct the update packet with the given raw packet instance to read from. + /// + protected BasePacket() { + NormalPacketData = new Dictionary(); + AddonPacketData = new Dictionary(); + } + + /// + /// Write the given dictionary of normal or resent packet data into the given raw packet instance. + /// + /// The packet to write into. + /// Dictionary of packet data to write. + /// true if any of the data written was reliable; otherwise false. + protected bool WritePacketData( + Packet packet, + Dictionary packetData + ) { + var enumValues = (TPacketId[]) Enum.GetValues(typeof(TPacketId)); + var packetIdSize = (byte) enumValues.Length; + + return WritePacketData( + packet, + packetData, + enumValues, + packetIdSize + ); + } + + /// + /// Write the data in the given instance of AddonPacketData into the given raw packet instance. + /// + /// The packet to write into. + /// AddonPacketData instance from which to data should be written. + /// true if any of the data written was reliable; otherwise false. + private bool WriteAddonPacketData( + Packet packet, + AddonPacketData addonPacketData + ) => WritePacketData( + packet, + addonPacketData.PacketData, + addonPacketData.PacketIdEnumerable, + addonPacketData.PacketIdSize + ); + + /// + /// Write the given dictionary of packet data into the given raw packet instance. + /// + /// The packet to write into. + /// The dictionary containing packet data to write in the packet. + /// An enumerator that enumerates over all possible keys in the dictionary. + /// The exact size of the key space. + /// Dictionary key parameter and enumerator parameter. + /// true if any of the data written was reliable; otherwise false. + private bool WritePacketData( + Packet packet, + Dictionary packetData, + IEnumerable keyEnumerable, + byte keySpaceSize + ) { + // Keep track of the bit flag in an unsigned long, which is the largest integer implicit type allowed + ulong idFlag = 0; + // Also keep track of the value of the current bit in an unsigned long + ulong currentTypeValue = 1; + + var keyEnumerator = keyEnumerable.GetEnumerator(); + while (keyEnumerator.MoveNext()) { + var key = keyEnumerator.Current; + + // Update the bit in the flag if the current value is included in the dictionary + if (key != null && packetData.ContainsKey(key)) { + idFlag |= currentTypeValue; + } + + // Always increase the current bit + currentTypeValue *= 2; + } + + // Based on the size of the values space, we cast to the smallest primitive that can hold the flag + // and write it to the packet + if (keySpaceSize <= 8) { + packet.Write((byte) idFlag); + } else if (keySpaceSize <= 16) { + packet.Write((ushort) idFlag); + } else if (keySpaceSize <= 32) { + packet.Write((uint) idFlag); + } else if (keySpaceSize <= 64) { + packet.Write(idFlag); + } + + // Let each individual piece of packet data write themselves into the packet + // and keep track of whether any of them need to be reliable + var containsReliableData = false; + // We loop over the possible IDs in the order from the given array to make it + // consistent between server and client + keyEnumerator.Reset(); + while (keyEnumerator.MoveNext()) { + var key = keyEnumerator.Current; + + if (key != null && packetData.TryGetValue(key, out var iPacketData)) { + iPacketData.WriteData(packet); + + if (iPacketData.IsReliable) { + containsReliableData = true; + } + } + } + + keyEnumerator.Dispose(); + + return containsReliableData; + } + + /// + /// Write the given dictionary containing addon data for all addons in the given packet. + /// + /// The raw packet instance to write into. + /// The dictionary containing all addon data to write. + /// true if any of the data written was reliable; otherwise false. + protected bool WriteAddonDataDict( + Packet packet, + Dictionary addonDataDict + ) { + // Normally, we put the length of the addon packet data as a byte in the packet. + // There should only be a maximum of 255 addons, so the length should fit in a byte. + // But we don't know which addon data is going to get written correctly and which throws + // an exception, so for now we hold off on writing anything yet, but keep track of how + // many instances we are writing + var addonPacketDataCount = (byte) addonDataDict.Count; + + // We also construct a temporary packet that we use to write the progress of all + // addon packet data into. This temp packet we can then later write into the original + // packet as soon as we know the number of successful addon packet data instances we + // have written. + var addonPacketDataPacket = new Packet(); + + // Also keep track of whether we have written reliable data + var containsReliable = false; + + // Add the packet data per addon ID + foreach (var addonPacketDataPair in addonDataDict) { + var addonId = addonPacketDataPair.Key; + var addonPacketData = addonPacketDataPair.Value; + + // Create a new packet to try and write addon packet data into + var addonPacket = new Packet(); + bool addonContainsReliable; + try { + addonContainsReliable = WriteAddonPacketData( + addonPacket, + addonPacketData + ); + } catch (Exception e) { + // If the addon data writing throws an exception, we skip it entirely and since we + // wrote it in a separate packet, it has no impact on the regular packet + Logger.Debug($"Addon with ID {addonId} has thrown an exception while writing addon packet data:\n{e}"); + // We decrease the count of addon packet data's we write, so we know how many are actually in + // final packet + addonPacketDataCount--; + continue; + } + + // Prepend the length of the addon packet data to the addon packet + addonPacket.WriteLength(); + + // Now we add the addon ID to the addon packet data packet and then the contents of the addon packet + addonPacketDataPacket.Write(addonId); + addonPacketDataPacket.Write(addonPacket.ToArray()); + + // Potentially update whether this packet contains reliable data now + containsReliable |= addonContainsReliable; + } + + // Finally write the resulting size and the addon packet data itself in the regular packet + packet.Write(addonPacketDataCount); + packet.Write(addonPacketDataPacket.ToArray()); + + return containsReliable; + } + + /// + /// Read raw data from the given packet into the given packet data dictionary. + /// This method is only for normal and resent packet data, not for addon packet data. + /// + /// The raw packet instance to read from. + /// The dictionary of packet data to write the read data into. + protected void ReadPacketData( + Packet packet, + Dictionary packetData + ) { + // Figure out the size of the packet ID enum + var enumValues = (TPacketId[]) Enum.GetValues(typeof(TPacketId)); + var packetIdSize = (byte) enumValues.Length; + + // Read the byte flag representing which packets are included in this update + // The number of bytes we read is dependent on the size of the enum + ulong dataPacketIdFlag = 0; + if (packetIdSize <= 8) { + dataPacketIdFlag = packet.ReadByte(); + } else if (packetIdSize <= 16) { + dataPacketIdFlag = packet.ReadUShort(); + } else if (packetIdSize <= 32) { + dataPacketIdFlag = packet.ReadUInt(); + } else if (packetIdSize <= 64) { + dataPacketIdFlag = packet.ReadULong(); + } + + // Keep track of value of current bit + ulong currentTypeValue = 1; + + var packetIdValues = Enum.GetValues(typeof(TPacketId)); + foreach (TPacketId packetId in packetIdValues) { + // If this bit was set in our flag, we add the type to the list + if ((dataPacketIdFlag & currentTypeValue) != 0) { + var iPacketData = InstantiatePacketDataFromId(packetId); + iPacketData?.ReadData(packet); + + packetData[packetId] = iPacketData; + } + + // Increase the value of current bit + currentTypeValue *= 2; + } + } + + /// + /// Read raw addon data from the given packet into the given addon data dictionary. + /// + /// The raw packet instance to read from. + /// The size of the packet ID space. + /// A function that instantiate IPacketData implementations given a + /// packet ID in byte form. + /// The dictionary of addon data to write the read data into. + /// Thrown if the given instantiation function returns null. + private void ReadAddonPacketData( + Packet packet, + byte packetIdSize, + Func packetDataInstantiator, + Dictionary packetData + ) { + // Read the byte flag representing which packets are included in this update + // This flag may come in different primitives based on the size of the packet + // ID space + ulong dataPacketIdFlag; + + if (packetIdSize <= 8) { + dataPacketIdFlag = packet.ReadByte(); + } else if (packetIdSize <= 16) { + dataPacketIdFlag = packet.ReadUShort(); + } else if (packetIdSize <= 32) { + dataPacketIdFlag = packet.ReadUInt(); + } else if (packetIdSize <= 64) { + dataPacketIdFlag = packet.ReadULong(); + } else { + // This should never happen, but in case it does, we throw an exception + throw new Exception("Addon packet ID space size is larger than expected"); + } + + // Keep track of value of current bit in the largest integer primitive + ulong currentTypeValue = 1; + + for (byte packetId = 0; packetId < packetIdSize; packetId++) { + // If this bit was set in our flag, we add the type to the list + if ((dataPacketIdFlag & currentTypeValue) != 0) { + IPacketData iPacketData; + + // Wrap this in try catch so we can add information on what happened when the addon submitted + // packet data instantiator throws + try { + iPacketData = packetDataInstantiator.Invoke(packetId); + } catch (Exception e) { + throw new Exception( + $"Packet data instantiator for addon data threw an exception:\n{e}"); + } + + if (iPacketData == null) { + throw new Exception("Addon packet data instantiating method returned null"); + } + + iPacketData.ReadData(packet); + + packetData[packetId] = iPacketData; + } + + // Increase the value of current bit + currentTypeValue *= 2; + } + } + + /// + /// Read all raw addon data from the given packet into the given dictionary containing entries for all addons. + /// + /// The raw packet instance to read from. + /// The dictionary for all addon data to write the read data into. + /// Thrown if any part of reading the data throws. + protected void ReadAddonDataDict( + Packet packet, + Dictionary addonDataDict + ) { + // Read the number of the addon packet data instances from the packet + var numAddonData = packet.ReadByte(); + + while (numAddonData-- > 0) { + var addonId = packet.ReadByte(); + + if (!AddonPacketInfoDict.TryGetValue(addonId, out var addonPacketInfo)) { + // If the addon packet info for this addon could not be found, we need to throw an exception + throw new Exception($"Addon with ID {addonId} has no defined addon packet info"); + } + + // Read the length of the addon packet data for this addon + var addonDataLength = packet.ReadUShort(); + + // Read exactly as many bytes as was indicated by the previously read value + var addonDataBytes = packet.ReadBytes(addonDataLength); + + // Create a new packet object with the given bytes so we can sandbox the reading + var addonPacket = new Packet(addonDataBytes); + + // Create a new instance of AddonPacketData to read packet data into and eventually + // add to this packet instance's dictionary + var addonPacketData = new AddonPacketData(addonPacketInfo.PacketIdSize); + + try { + ReadAddonPacketData( + addonPacket, + addonPacketInfo.PacketIdSize, + addonPacketInfo.PacketDataInstantiator, + addonPacketData.PacketData + ); + } catch (Exception e) { + // If the addon data reading throws an exception, we skip it entirely and since + // we read it into a separate packet, it has no impact on the regular packet + Logger.Debug($"Addon with ID {addonId} has thrown an exception while reading addon packet data:\n{e}"); + continue; + } + + addonDataDict[addonId] = addonPacketData; + } + } + + /// + /// Create a raw packet out of the data contained in this class by writing to the given packet. + /// + /// The packet instance to write the data to. + public virtual void CreatePacket(Packet packet) { + // Write the normal packet data into the packet and keep track of whether this packet contains reliable data + // now + ContainsReliableData = WritePacketData(packet, NormalPacketData); + + // Write the addon packet data into the packet and add to the fact that this packet contains reliable data now + ContainsReliableData |= WriteAddonDataDict(packet, AddonPacketData); + } + + /// + /// Read the raw packet contents into easy to access dictionaries. + /// + /// The packet instance to read the data from. + /// False if the packet cannot be successfully read due to malformed data; otherwise true. + public virtual bool ReadPacket(Packet packet) { + try { + // Read the normal packet data from the packet + ReadPacketData(packet, NormalPacketData); + + // Read the addon packet data + ReadAddonDataDict(packet, AddonPacketData); + } catch (Exception e) { + Logger.Debug($"Exception while reading base packet:\n{e}"); + return false; + } + + return true; + } + + /// + /// Tries to get packet data that is going to be sent with the given packet ID. + /// + /// The packet ID to try and get. + /// Variable to store the retrieved data in. Null if this method returns false. + /// true if the packet data exists and will be stored in the packetData variable; otherwise + /// false. + public bool TryGetSendingPacketData(TPacketId packetId, out IPacketData packetData) { + return NormalPacketData.TryGetValue(packetId, out packetData); + } + + /// + /// Tries to get addon packet data for the addon with the given ID. + /// + /// The ID of the addon to get the data for. + /// An instance of AddonPacketData corresponding to the given ID. + /// Null if this method returns false. + /// true if the addon packet data exists and will be stored in the addonPacketData variable; + /// otherwise false. + public bool TryGetSendingAddonPacketData(byte addonId, out AddonPacketData addonPacketData) { + return AddonPacketData.TryGetValue(addonId, out addonPacketData); + } + + /// + /// Sets the given packetData with the given packet ID for sending. + /// + /// The packet ID to set data for. + /// The packet data to set. + public void SetSendingPacketData(TPacketId packetId, IPacketData packetData) { + NormalPacketData[packetId] = packetData; + } + + /// + /// Sets the given addonPacketData with the given addon ID for sending. + /// + /// The addon ID to set data for. + /// Instance of AddonPacketData to set. + public void SetSendingAddonPacketData(byte addonId, AddonPacketData packetData) { + AddonPacketData[addonId] = packetData; + } + + /// + /// Get all the packet data contained in this packet, normal and resent data (but not addon data). + /// + /// A dictionary containing packet IDs mapped to packet data. + public Dictionary GetPacketData() { + if (!IsAllPacketDataCached) { + CacheAllPacketData(); + } + + return CachedAllPacketData; + } + + /// + /// Get the addon packet data in this packet, normal addon and resent data. + /// + /// A dictionary containing addon IDs mapped to addon packet data. + public Dictionary GetAddonPacketData() { + if (!IsAllPacketDataCached) { + CacheAllPacketData(); + } + + return CachedAllAddonData; + } + + /// + /// Computes all packet data (normal, resent, addon and addon resent data), caches it and sets a boolean + /// indicating that this cache is now available. + /// + protected virtual void CacheAllPacketData() { + // Construct a new dictionary for all the data + CachedAllPacketData = new Dictionary(); + + // Iteratively add the normal packet data + foreach (var packetIdDataPair in NormalPacketData) { + CachedAllPacketData.Add(packetIdDataPair.Key, packetIdDataPair.Value); + } + + // Do the same as above but for addon data + CachedAllAddonData = new Dictionary(); + + // Iteratively add the addon data + foreach (var addonIdDataPair in AddonPacketData) { + CachedAllAddonData.Add(addonIdDataPair.Key, addonIdDataPair.Value); + } + } + + /// + /// Get an instantiation of IPacketData for the given packet ID. + /// + /// The packet ID to get an instance for. + /// A new instance of IPacketData. + protected abstract IPacketData InstantiatePacketDataFromId(TPacketId packetId); +} diff --git a/HKMP/Networking/Packet/Connection/ClientConnectionPacket.cs b/HKMP/Networking/Packet/Connection/ClientConnectionPacket.cs new file mode 100644 index 00000000..c3ed9b90 --- /dev/null +++ b/HKMP/Networking/Packet/Connection/ClientConnectionPacket.cs @@ -0,0 +1,18 @@ +using Hkmp.Networking.Packet.Data; + +namespace Hkmp.Networking.Packet.Connection; + +/// +/// Packet that contains connection information for server to client communication. +/// +internal class ClientConnectionPacket : BasePacket { + /// + protected override IPacketData InstantiatePacketDataFromId(ClientConnectionPacketId packetId) { + switch (packetId) { + case ClientConnectionPacketId.ServerInfo: + return new ServerInfo(); + default: + return new EmptyData(); + } + } +} diff --git a/HKMP/Networking/Packet/Connection/ClientConnectionPacketId.cs b/HKMP/Networking/Packet/Connection/ClientConnectionPacketId.cs new file mode 100644 index 00000000..f23a4fa0 --- /dev/null +++ b/HKMP/Networking/Packet/Connection/ClientConnectionPacketId.cs @@ -0,0 +1,11 @@ +namespace Hkmp.Networking.Packet.Connection; + +/// +/// Enumeration of packet IDs for connection packet for server to client communication. +/// +internal enum ClientConnectionPacketId { + /// + /// Information about the server meant for the client detailing whether the connection was accepted. + /// + ServerInfo = 0, +} diff --git a/HKMP/Networking/Packet/Connection/ServerConnectionPacket.cs b/HKMP/Networking/Packet/Connection/ServerConnectionPacket.cs new file mode 100644 index 00000000..152c05c2 --- /dev/null +++ b/HKMP/Networking/Packet/Connection/ServerConnectionPacket.cs @@ -0,0 +1,18 @@ +using Hkmp.Networking.Packet.Data; + +namespace Hkmp.Networking.Packet.Connection; + +/// +/// Packet that contains connection information for client to server communication. +/// +internal class ServerConnectionPacket : BasePacket { + /// + protected override IPacketData InstantiatePacketDataFromId(ServerConnectionPacketId packetId) { + switch (packetId) { + case ServerConnectionPacketId.ClientInfo: + return new ClientInfo(); + default: + return new EmptyData(); + } + } +} diff --git a/HKMP/Networking/Packet/Connection/ServerConnectionPacketId.cs b/HKMP/Networking/Packet/Connection/ServerConnectionPacketId.cs new file mode 100644 index 00000000..f9b8145d --- /dev/null +++ b/HKMP/Networking/Packet/Connection/ServerConnectionPacketId.cs @@ -0,0 +1,11 @@ +namespace Hkmp.Networking.Packet.Connection; + +/// +/// Enumeration of packet IDs for the connection packet for client to server communication. +/// +internal enum ServerConnectionPacketId { + /// + /// Information about the client that the server can use to determine whether to accept the connection. + /// + ClientInfo = 0, +} diff --git a/HKMP/Networking/Packet/Data/LoginRequest.cs b/HKMP/Networking/Packet/Data/ClientInfo.cs similarity index 90% rename from HKMP/Networking/Packet/Data/LoginRequest.cs rename to HKMP/Networking/Packet/Data/ClientInfo.cs index 7830add1..9356a27f 100644 --- a/HKMP/Networking/Packet/Data/LoginRequest.cs +++ b/HKMP/Networking/Packet/Data/ClientInfo.cs @@ -5,14 +5,14 @@ namespace Hkmp.Networking.Packet.Data; /// -/// Packet data for the login request data. +/// Packet data for the client info data. /// -internal class LoginRequest : IPacketData { +internal class ClientInfo : IPacketData { /// - public bool IsReliable => true; + public bool IsReliable => false; /// - public bool DropReliableDataIfNewerExists => true; + public bool DropReliableDataIfNewerExists => false; /// /// The username of the client. @@ -27,20 +27,15 @@ internal class LoginRequest : IPacketData { /// /// A list of addon data of the client. /// - public List AddonData { get; } - - /// - /// Construct the login request. - /// - public LoginRequest() { - AddonData = new List(); - } + public List AddonData { get; set; } /// public void WriteData(IPacket packet) { packet.Write(Username); packet.Write(AuthKey); + AddonData ??= new List(); + var addonDataLength = (byte) System.Math.Min(byte.MaxValue, AddonData.Count); packet.Write(addonDataLength); @@ -58,6 +53,8 @@ public void ReadData(IPacket packet) { var addonDataLength = packet.ReadByte(); + AddonData = new List(); + for (var i = 0; i < addonDataLength; i++) { var id = packet.ReadString(); var version = packet.ReadString(); diff --git a/HKMP/Networking/Packet/Data/ServerInfo.cs b/HKMP/Networking/Packet/Data/ServerInfo.cs new file mode 100644 index 00000000..52dd3b33 --- /dev/null +++ b/HKMP/Networking/Packet/Data/ServerInfo.cs @@ -0,0 +1,137 @@ +using System; +using System.Collections.Generic; +using Hkmp.Api.Addon; + +namespace Hkmp.Networking.Packet.Data; + +/// +/// Packet data for the server info data. +/// +internal class ServerInfo : IPacketData { + /// + public bool IsReliable => false; + + /// + public bool DropReliableDataIfNewerExists => false; + + /// + /// The result of the connection, whether it was accepted. + /// + public ServerConnectionResult ConnectionResult { get; set; } + + /// + /// The message detailing why the connection was rejected if it was. + /// + public string ConnectionRejectedMessage { get; set; } + + /// + /// List of addon data that the server uses. + /// + public List AddonData { get; set; } + + /// + /// The order in which the addons have been assigned IDs. + /// + public byte[] AddonOrder { get; set; } + + /// + /// The save data currently used on the server. + /// + public CurrentSave CurrentSave { get; set; } + + /// + /// List of ID, username pairs for each connected client. + /// + public List<(ushort, string)> PlayerInfo { get; set; } + + /// + public void WriteData(IPacket packet) { + packet.Write((byte) ConnectionResult); + + if (ConnectionResult == ServerConnectionResult.Accepted) { + packet.Write((byte) AddonOrder.Length); + + foreach (var addonOrderByte in AddonOrder) { + packet.Write(addonOrderByte); + } + + CurrentSave.WriteData(packet); + + packet.Write((ushort) PlayerInfo.Count); + + foreach (var (id, username) in PlayerInfo) { + packet.Write(id); + packet.Write(username); + } + + return; + } + + if (ConnectionResult == ServerConnectionResult.InvalidAddons) { + AddonData ??= new List(); + + var addonDataLength = (byte) System.Math.Min(byte.MaxValue, AddonData.Count); + + packet.Write(addonDataLength); + + for (var i = 0; i < addonDataLength; i++) { + packet.Write(AddonData[i].Identifier); + packet.Write(AddonData[i].Version); + } + + return; + } + + packet.Write(ConnectionRejectedMessage); + } + + /// + public void ReadData(IPacket packet) { + ConnectionResult = (ServerConnectionResult) packet.ReadByte(); + + if (ConnectionResult == ServerConnectionResult.Accepted) { + var addonOrderLength = packet.ReadByte(); + AddonOrder = new byte[addonOrderLength]; + + for (var i = 0; i < addonOrderLength; i++) { + AddonOrder[i] = packet.ReadByte(); + } + + CurrentSave = new CurrentSave(); + CurrentSave.ReadData(packet); + + var length = packet.ReadUShort(); + + PlayerInfo = new List<(ushort, string)>(); + for (var i = 0; i < length; i++) { + PlayerInfo.Add(( + packet.ReadUShort(), + packet.ReadString() + )); + } + + return; + } + + if (ConnectionResult == ServerConnectionResult.InvalidAddons) { + var addonDataLength = packet.ReadByte(); + + AddonData = new List(); + + for (var i = 0; i < addonDataLength; i++) { + var id = packet.ReadString(); + var version = packet.ReadString(); + + if (id.Length > Addon.MaxNameLength || version.Length > Addon.MaxVersionLength) { + throw new ArgumentException("Identifier or version of addon exceeds max length"); + } + + AddonData.Add(new AddonData(id, version)); + } + + return; + } + + ConnectionRejectedMessage = packet.ReadString(); + } +} diff --git a/HKMP/Networking/Packet/Data/SliceAckData.cs b/HKMP/Networking/Packet/Data/SliceAckData.cs new file mode 100644 index 00000000..e8644c3f --- /dev/null +++ b/HKMP/Networking/Packet/Data/SliceAckData.cs @@ -0,0 +1,114 @@ +namespace Hkmp.Networking.Packet.Data; + +/// +/// Packet for acknowledging a received slice packet for large reliable data transfer during connection. +/// +internal class SliceAckData : IPacketData { + /// + public bool IsReliable => false; + + /// + public bool DropReliableDataIfNewerExists => false; + + /// + /// The ID of the chunk that is being networked. + /// + public byte ChunkId { get; set; } + + /// + /// The total number of slices in this chunk. Encoded as a byte where all values are shifted by -1 to ensure we + /// can encode 256 as a value, since we don't use 0. + /// + public ushort NumSlices { get; set; } + + /// + /// Boolean array containing whether a slice was acked. For writing packets, the length of the array can equal + /// the number of slices. For reading packets, the length of the array will equal the maximum possible number + /// of slices per chunk. + /// + public bool[] Acked { get; set; } + + /// + public void WriteData(IPacket packet) { + packet.Write(ChunkId); + + var encodedNumSlices = (byte) (NumSlices - 1); + packet.Write(encodedNumSlices); + + // Keep track of current index for writing ack array + var currentIndex = 0; + // Do while loop, since we will always be writing at least a single byte bit flag + do { + packet.Write(CreateAckFlag(currentIndex, currentIndex + 8, Acked)); + // Continue while loop if we need to write another flag, namely when the new starting index is smaller + // than the number of slices + } while ((currentIndex += 8) <= NumSlices); + } + + /// + public void ReadData(IPacket packet) { + ChunkId = packet.ReadByte(); + + var encodedNumSlices = packet.ReadByte(); + NumSlices = (ushort) (encodedNumSlices + 1); + + var acked = new bool[ConnectionManager.MaxSlicesPerChunk]; + + // Keep track of current index for writing to ack array + var currentIndex = 0; + // Do while loop, since we will always be reading at least one byte for the bit flag + do { + var flag = packet.ReadByte(); + ReadAckFlag(flag, currentIndex, currentIndex + 8, ref acked); + // Continue while loop if we need to read another flag, namely when the new starting index is smaller + // than the number of slices + } while ((currentIndex += 8) <= NumSlices); + + Acked = acked; + } + + /// + /// Create a bit flag as a byte from the given boolean array with start and end indices. + /// + /// The (inclusive) start index to start reading from the boolean array. + /// The (exclusive) end index to stop reading from the boolean array. + /// The boolean array to read values from for the flag. + /// The bit flag as a byte. + private static byte CreateAckFlag(int startIndex, int endIndex, bool[] acked) { + byte flag = 0; + byte currentValue = 1; + + for (var i = startIndex; i < endIndex; i++) { + if (acked.Length <= i) { + break; + } + + if (acked[i]) { + flag |= currentValue; + } + + currentValue *= 2; + } + + return flag; + } + + /// + /// Read a bit flag in byte form and put the bits into the given reference boolean array. + /// + /// The bit flag as a byte. + /// The (inclusive) start index to start reading from the boolean array. + /// The (exclusive) end index to stop reading from the boolean array. + /// The boolean array as a reference to write values to from the flag. + private static void ReadAckFlag(byte flag, int startIndex, int endIndex, ref bool[] acked) { + byte currentValue = 1; + + for (var i = startIndex; i < endIndex; i++) { + if ((flag & currentValue) != 0) { + acked[i] = true; + } + + currentValue *= 2; + } + } +} diff --git a/HKMP/Networking/Packet/Data/SliceData.cs b/HKMP/Networking/Packet/Data/SliceData.cs new file mode 100644 index 00000000..216e0780 --- /dev/null +++ b/HKMP/Networking/Packet/Data/SliceData.cs @@ -0,0 +1,80 @@ +using System; + +namespace Hkmp.Networking.Packet.Data; + +/// +/// Packet with raw byte data as a slice of a bigger chunk meant for large reliable data transfer during connection. +/// +internal class SliceData : IPacketData { + /// + public bool IsReliable => false; + + /// + public bool DropReliableDataIfNewerExists => false; + + /// + /// The ID of the chunk that is being networked. + /// + public byte ChunkId { get; set; } + + /// + /// The ID of this slice. + /// + public byte SliceId { get; set; } + + /// + /// The total number of slices in this chunk. It is an unsigned short because we can have 256 slices in a chunk. + /// It is encoded as a byte, where all values are shifted by one since 0 is not used. + /// + public ushort NumSlices { get; set; } + + /// + /// Byte array containing the data of this slice. + /// + public byte[] Data { get; set; } + + /// + public void WriteData(IPacket packet) { + packet.Write(ChunkId); + packet.Write(SliceId); + + // Shift all values by -1 so that we can encode 256 as a number of slices + var encodedNumSlices = (byte) (NumSlices - 1); + packet.Write(encodedNumSlices); + + var length = Data.Length; + if (length > ConnectionManager.MaxSliceSize) { + throw new ArgumentOutOfRangeException(nameof(Data), "Length of data for slice cannot exceed 1024"); + } + + if (SliceId == NumSlices - 1) { + packet.Write((ushort) length); + } + + for (var i = 0; i < length; i++) { + packet.Write(Data[i]); + } + } + + /// + public void ReadData(IPacket packet) { + ChunkId = packet.ReadByte(); + SliceId = packet.ReadByte(); + + // Read the encoded byte and shift it by 1 again + var encodedNumSlices = packet.ReadByte(); + NumSlices = (ushort) (encodedNumSlices + 1); + + ushort length; + if (SliceId == NumSlices - 1) { + length = packet.ReadUShort(); + } else { + length = ConnectionManager.MaxSliceSize; + } + + Data = new byte[length]; + for (var i = 0; i < length; i++) { + Data[i] = packet.ReadByte(); + } + } +} diff --git a/HKMP/Networking/Packet/PacketManager.cs b/HKMP/Networking/Packet/PacketManager.cs index 2b7b74f3..483e29b2 100644 --- a/HKMP/Networking/Packet/PacketManager.cs +++ b/HKMP/Networking/Packet/PacketManager.cs @@ -1,7 +1,9 @@ using System; using System.Collections.Generic; using Hkmp.Logging; +using Hkmp.Networking.Packet.Connection; using Hkmp.Networking.Packet.Data; +using Hkmp.Networking.Packet.Update; using Hkmp.Util; namespace Hkmp.Networking.Packet; @@ -39,42 +41,68 @@ public delegate void GenericServerPacketHandler(ushort id, TPack /// internal class PacketManager { /// - /// Handlers that deal with data from the server intended for the client. + /// Handlers that deal with update packet data from the server intended for the client. /// - private readonly Dictionary _clientPacketHandlers; + private readonly Dictionary _clientUpdatePacketHandlers; /// - /// Handlers that deal with data from the client intended for the server. + /// Handlers that deal with connection packet data from the server intended for the client. /// - private readonly Dictionary _serverPacketHandlers; + private readonly Dictionary _clientConnectionPacketHandlers; /// - /// Handlers that deal with client addon data from the server intended for the client. + /// Handlers that deal with update packet data from the client intended for the server. /// - private readonly Dictionary> _clientAddonPacketHandlers; + private readonly Dictionary _serverUpdatePacketHandlers; + + /// + /// Handlers that deal with connection packet data from the client intended for the server. + /// + private readonly Dictionary _serverConnectionPacketHandlers; + + /// + /// Handlers that deal with client addon update packet data from the server intended for the client. + /// + private readonly Dictionary> _clientAddonUpdatePacketHandlers; + + /// + /// Handlers that deal with client addon connection packet data from the server intended for the client. + /// + private readonly Dictionary> _clientAddonConnectionPacketHandlers; /// - /// Handlers that deal with server addon data from a client intended for the server. + /// Handlers that deal with server addon update packet data from a client intended for the server. /// - private readonly Dictionary> _serverAddonPacketHandlers; + private readonly Dictionary> _serverAddonUpdatePacketHandlers; + + /// + /// Handlers that deal with server addon connection packet data from a client intended for the server. + /// + private readonly Dictionary> _serverAddonConnectionPacketHandlers; public PacketManager() { - _clientPacketHandlers = new Dictionary(); - _serverPacketHandlers = new Dictionary(); + _clientUpdatePacketHandlers = new Dictionary(); + _clientConnectionPacketHandlers = new Dictionary(); + + _serverUpdatePacketHandlers = new Dictionary(); + _serverConnectionPacketHandlers = new Dictionary(); - _clientAddonPacketHandlers = new Dictionary>(); - _serverAddonPacketHandlers = new Dictionary>(); + _clientAddonUpdatePacketHandlers = new Dictionary>(); + _clientAddonConnectionPacketHandlers = new Dictionary>(); + + _serverAddonUpdatePacketHandlers = new Dictionary>(); + _serverAddonConnectionPacketHandlers = new Dictionary>(); } - #region Client-related packet handling + #region Client-related update packet handling /// /// Handle data received by a client. /// /// The client update packet to handle. - public void HandleClientPacket(ClientUpdatePacket packet) { + public void HandleClientUpdatePacket(ClientUpdatePacket packet) { // Execute corresponding packet handlers for normal packet data - UnpackPacketDataDict(packet.GetPacketData(), ExecuteClientPacketHandler); + UnpackPacketDataDict(packet.GetPacketData(), ExecuteClientUpdatePacketHandler); // Execute corresponding packet handlers for addon packet data of each addon in the packet foreach (var idPacketDataPair in packet.GetAddonPacketData()) { @@ -83,26 +111,26 @@ public void HandleClientPacket(ClientUpdatePacket packet) { UnpackPacketDataDict( packetDataDict, - (packetId, packetData) => ExecuteClientAddonPacketHandler(addonId, packetId, packetData) + (packetId, packetData) => ExecuteClientAddonUpdatePacketHandler(addonId, packetId, packetData) ); } } /// - /// Executes the correct packet handler corresponding to this packet data. + /// Executes the correct packet handler corresponding to this update packet data. /// /// The client packet ID for this data. /// The packet data instance. - private void ExecuteClientPacketHandler(ClientPacketId packetId, IPacketData packetData) { - if (!_clientPacketHandlers.ContainsKey(packetId)) { - Logger.Error($"There is no client packet handler registered for ID: {packetId}"); + private void ExecuteClientUpdatePacketHandler(ClientUpdatePacketId packetId, IPacketData packetData) { + if (!_clientUpdatePacketHandlers.TryGetValue(packetId, out var handler)) { + Logger.Error($"There is no client update packet handler registered for ID: {packetId}"); return; } // Invoke the packet handler for this ID on the Unity main thread ThreadUtil.RunActionOnMainThread(() => { try { - _clientPacketHandlers[packetId].Invoke(packetData); + handler.Invoke(packetData); } catch (Exception e) { Logger.Error($"Exception occured while executing client packet handler for packet ID {packetId}:\n{e}"); } @@ -110,70 +138,166 @@ private void ExecuteClientPacketHandler(ClientPacketId packetId, IPacketData pac } /// - /// Register a packet handler for the given ID. + /// Register an update packet handler for the given ID. /// /// The client packet ID. /// The handler for the data. - private void RegisterClientPacketHandler( - ClientPacketId packetId, + private void RegisterClientUpdatePacketHandler( + ClientUpdatePacketId packetId, ClientPacketHandler handler ) { - if (_clientPacketHandlers.ContainsKey(packetId)) { + if (_clientUpdatePacketHandlers.ContainsKey(packetId)) { Logger.Warn($"Tried to register already existing client packet handler: {packetId}"); return; } - _clientPacketHandlers[packetId] = handler; + _clientUpdatePacketHandlers[packetId] = handler; } /// - /// Register a data-independent packet handler for the given ID. + /// Register a data-independent update packet handler for the given ID. /// /// The client packet ID. /// The handler for the data. - public void RegisterClientPacketHandler( - ClientPacketId packetId, + public void RegisterClientUpdatePacketHandler( + ClientUpdatePacketId packetId, Action handler - ) => RegisterClientPacketHandler(packetId, _ => handler()); + ) => RegisterClientUpdatePacketHandler(packetId, _ => handler()); /// - /// Register a packet handler for the given ID. + /// Register an update packet handler for the given ID. /// /// The client packet ID. /// The handler for the data. /// The type of the packet data passed as parameter to the handler. - public void RegisterClientPacketHandler( - ClientPacketId packetId, + public void RegisterClientUpdatePacketHandler( + ClientUpdatePacketId packetId, GenericClientPacketHandler handler - ) where T : IPacketData => RegisterClientPacketHandler(packetId, iPacket => handler((T) iPacket)); + ) where T : IPacketData => RegisterClientUpdatePacketHandler(packetId, iPacket => handler((T) iPacket)); /// - /// De-register a packet handler for the given ID. + /// De-register an update packet handler for the given ID. /// /// The client packet ID. - public void DeregisterClientPacketHandler(ClientPacketId packetId) { - if (!_clientPacketHandlers.ContainsKey(packetId)) { + public void DeregisterClientPacketHandler(ClientUpdatePacketId packetId) { + if (!_clientUpdatePacketHandlers.ContainsKey(packetId)) { Logger.Warn($"Tried to remove nonexistent client packet handler: {packetId}"); return; } - _clientPacketHandlers.Remove(packetId); + _clientUpdatePacketHandlers.Remove(packetId); } #endregion - #region Server-related packet handling + #region Client-related connection packet handling + + /// + /// Handle connection packet data received by a client. + /// + /// The client connection packet to handle. + public void HandleClientConnectionPacket(ClientConnectionPacket packet) { + // Execute corresponding packet handlers for normal packet data + UnpackPacketDataDict(packet.GetPacketData(), ExecuteClientConnectionPacketHandler); + + // Execute corresponding packet handlers for addon packet data of each addon in the packet + foreach (var idPacketDataPair in packet.GetAddonPacketData()) { + var addonId = idPacketDataPair.Key; + var packetDataDict = idPacketDataPair.Value.PacketData; + + UnpackPacketDataDict( + packetDataDict, + (packetId, packetData) => ExecuteClientAddonConnectionPacketHandler(addonId, packetId, packetData) + ); + } + } + + /// + /// Executes the correct packet handler corresponding to this connection packet data. + /// + /// The client packet ID for this data. + /// The packet data instance. + private void ExecuteClientConnectionPacketHandler(ClientConnectionPacketId packetId, IPacketData packetData) { + if (!_clientConnectionPacketHandlers.TryGetValue(packetId, out var handler)) { + Logger.Error($"There is no client connection packet handler registered for ID: {packetId}"); + return; + } + + // Invoke the packet handler for this ID on the Unity main thread + ThreadUtil.RunActionOnMainThread(() => { + try { + handler.Invoke(packetData); + } catch (Exception e) { + Logger.Error($"Exception occured while executing client packet handler for packet ID {packetId}:\n{e}"); + } + }); + } + + /// + /// Register a connection packet handler for the given ID. + /// + /// The client packet ID. + /// The handler for the data. + private void RegisterClientConnectionPacketHandler( + ClientConnectionPacketId packetId, + ClientPacketHandler handler + ) { + if (_clientConnectionPacketHandlers.ContainsKey(packetId)) { + Logger.Warn($"Tried to register already existing client connection packet handler: {packetId}"); + return; + } + + _clientConnectionPacketHandlers[packetId] = handler; + } + + /// + /// Register a data-independent connection packet handler for the given ID. + /// + /// The client packet ID. + /// The handler for the data. + public void RegisterClientConnectionPacketHandler( + ClientConnectionPacketId packetId, + Action handler + ) => RegisterClientConnectionPacketHandler(packetId, _ => handler()); + + /// + /// Register a connection packet handler for the given ID. + /// + /// The client packet ID. + /// The handler for the data. + /// The type of the packet data passed as parameter to the handler. + public void RegisterClientConnectionPacketHandler( + ClientConnectionPacketId packetId, + GenericClientPacketHandler handler + ) where T : IPacketData => RegisterClientConnectionPacketHandler(packetId, iPacket => handler((T) iPacket)); + + /// + /// De-register a connection packet handler for the given ID. + /// + /// The client packet ID. + public void DeregisterClientConnectionPacketHandler(ClientConnectionPacketId packetId) { + if (!_clientConnectionPacketHandlers.ContainsKey(packetId)) { + Logger.Warn($"Tried to remove nonexistent client connection packet handler: {packetId}"); + return; + } + + _clientConnectionPacketHandlers.Remove(packetId); + } + + #endregion + + #region Server-related update packet handling /// - /// Handle data received by the server. + /// Handle update data received by the server. /// /// The ID of the client that sent the packet. /// The server update packet. - public void HandleServerPacket(ushort id, ServerUpdatePacket packet) { + public void HandleServerUpdatePacket(ushort id, ServerUpdatePacket packet) { // Execute corresponding packet handlers UnpackPacketDataDict( packet.GetPacketData(), - (packetId, packetData) => ExecuteServerPacketHandler(id, packetId, packetData) + (packetId, packetData) => ExecuteServerUpdatePacketHandler(id, packetId, packetData) ); // Execute corresponding packet handler for addon packet data of each addon in the packet @@ -183,7 +307,7 @@ public void HandleServerPacket(ushort id, ServerUpdatePacket packet) { UnpackPacketDataDict( packetDataDict, - (packetId, packetData) => ExecuteServerAddonPacketHandler( + (packetId, packetData) => ExecuteServerAddonUpdatePacketHandler( id, addonId, packetId, @@ -194,14 +318,14 @@ public void HandleServerPacket(ushort id, ServerUpdatePacket packet) { } /// - /// Executes the correct packet handler corresponding to this packet data. + /// Executes the correct update packet handler corresponding to this packet data. /// /// The ID of the client that sent the data. /// The server packet ID. /// The packet data instance. - private void ExecuteServerPacketHandler(ushort id, ServerPacketId packetId, IPacketData packetData) { - if (!_serverPacketHandlers.ContainsKey(packetId)) { - Logger.Warn($"There is no server packet handler registered for ID: {packetId}"); + private void ExecuteServerUpdatePacketHandler(ushort id, ServerUpdatePacketId packetId, IPacketData packetData) { + if (!_serverUpdatePacketHandlers.TryGetValue(packetId, out var handler)) { + Logger.Warn($"There is no server update packet handler registered for ID: {packetId}"); return; } @@ -209,82 +333,188 @@ private void ExecuteServerPacketHandler(ushort id, ServerPacketId packetId, IPac // We don't do anything game specific with server packet handler, so there's no need to do it // on the Unity main thread try { - _serverPacketHandlers[packetId].Invoke(id, packetData); + handler.Invoke(id, packetData); } catch (Exception e) { - Logger.Error($"Exception occured while executing server packet handler for packet ID {packetId}:\n{e}"); + Logger.Error($"Exception occured while executing server update packet handler for packet ID {packetId}:\n{e}"); } } /// - /// Register a packet handler for the given ID. + /// Register an update packet handler for the given ID. /// /// The server packet ID. /// The handler for the data. - private void RegisterServerPacketHandler(ServerPacketId packetId, ServerPacketHandler handler) { - if (_serverPacketHandlers.ContainsKey(packetId)) { - Logger.Warn($"Tried to register already existing client packet handler: {packetId}"); + private void RegisterServerUpdatePacketHandler(ServerUpdatePacketId packetId, ServerPacketHandler handler) { + if (_serverUpdatePacketHandlers.ContainsKey(packetId)) { + Logger.Warn($"Tried to register already existing server update packet handler: {packetId}"); return; } - _serverPacketHandlers[packetId] = handler; + _serverUpdatePacketHandlers[packetId] = handler; } /// - /// Register a data-independent packet handler for the given ID. + /// Register a data-independent update packet handler for the given ID. /// /// The server packet ID. /// The handler for the data. - public void RegisterServerPacketHandler( - ServerPacketId packetId, + public void RegisterServerUpdatePacketHandler( + ServerUpdatePacketId packetId, EmptyServerPacketHandler handler - ) => RegisterServerPacketHandler(packetId, (id, _) => handler(id)); + ) => RegisterServerUpdatePacketHandler(packetId, (id, _) => handler(id)); /// - /// Register a packet for the given ID. + /// Register an update packet handler for the given ID. /// /// The server packet ID. /// The handler for the data. /// The type of the packet data passed as parameter to the handler. - public void RegisterServerPacketHandler( - ServerPacketId packetId, + public void RegisterServerUpdatePacketHandler( + ServerUpdatePacketId packetId, GenericServerPacketHandler handler - ) where T : IPacketData => RegisterServerPacketHandler( + ) where T : IPacketData => RegisterServerUpdatePacketHandler( packetId, (id, iPacket) => handler(id, (T) iPacket) ); /// - /// De-register a packet handler for the given ID. + /// De-register an update packet handler for the given ID. /// /// The server packet ID. - public void DeregisterServerPacketHandler(ServerPacketId packetId) { - if (!_serverPacketHandlers.ContainsKey(packetId)) { - Logger.Warn($"Tried to remove nonexistent server packet handler: {packetId}"); + public void DeregisterServerUpdatePacketHandler(ServerUpdatePacketId packetId) { + if (!_serverUpdatePacketHandlers.ContainsKey(packetId)) { + Logger.Warn($"Tried to remove nonexistent server update packet handler: {packetId}"); return; } - _serverPacketHandlers.Remove(packetId); + _serverUpdatePacketHandlers.Remove(packetId); } #endregion - #region Client-addon-related packet handling + #region Server-related connection packet handling + + /// + /// Handle connection data received by the server. + /// + /// The ID of the client that sent the packet. + /// The server connection packet. + public void HandleServerConnectionPacket(ushort id, ServerConnectionPacket packet) { + // Execute corresponding packet handlers + UnpackPacketDataDict( + packet.GetPacketData(), + (packetId, packetData) => ExecuteServerConnectionPacketHandler(id, packetId, packetData) + ); + + // Execute corresponding packet handler for addon packet data of each addon in the packet + foreach (var idPacketDataPair in packet.GetAddonPacketData()) { + var addonId = idPacketDataPair.Key; + var packetDataDict = idPacketDataPair.Value.PacketData; + + UnpackPacketDataDict( + packetDataDict, + (packetId, packetData) => ExecuteServerAddonConnectionPacketHandler( + id, + addonId, + packetId, + packetData + ) + ); + } + } + + /// + /// Executes the correct connection packet handler corresponding to this packet data. + /// + /// The ID of the client that sent the data. + /// The server packet ID. + /// The packet data instance. + private void ExecuteServerConnectionPacketHandler(ushort id, ServerConnectionPacketId packetId, IPacketData packetData) { + if (!_serverConnectionPacketHandlers.TryGetValue(packetId, out var handler)) { + Logger.Warn($"There is no server connection packet handler registered for ID: {packetId}"); + return; + } + + // Invoke the packet handler for this ID directly, in contrast to the client packet handling. + // We don't do anything game specific with server packet handler, so there's no need to do it + // on the Unity main thread + try { + handler.Invoke(id, packetData); + } catch (Exception e) { + Logger.Error($"Exception occured while executing server connection packet handler for packet ID {packetId}:\n{e}"); + } + } + + /// + /// Register a connection packet handler for the given ID. + /// + /// The server packet ID. + /// The handler for the data. + private void RegisterServerConnectionPacketHandler(ServerConnectionPacketId packetId, ServerPacketHandler handler) { + if (_serverConnectionPacketHandlers.ContainsKey(packetId)) { + Logger.Warn($"Tried to register already existing client packet handler: {packetId}"); + return; + } + + _serverConnectionPacketHandlers[packetId] = handler; + } + + /// + /// Register a data-independent connection packet handler for the given ID. + /// + /// The server packet ID. + /// The handler for the data. + public void RegisterServerConnectionPacketHandler( + ServerConnectionPacketId packetId, + EmptyServerPacketHandler handler + ) => RegisterServerConnectionPacketHandler(packetId, (id, _) => handler(id)); + + /// + /// Register a connection packet handler for the given ID. + /// + /// The server packet ID. + /// The handler for the data. + /// The type of the packet data passed as parameter to the handler. + public void RegisterServerConnectionPacketHandler( + ServerConnectionPacketId packetId, + GenericServerPacketHandler handler + ) where T : IPacketData => RegisterServerConnectionPacketHandler( + packetId, + (id, iPacket) => handler(id, (T) iPacket) + ); /// - /// Execute the packet handler for the client addon data. + /// De-register a connection packet handler for the given ID. + /// + /// The server packet ID. + public void DeregisterServerConnectionPacketHandler(ServerConnectionPacketId packetId) { + if (!_serverConnectionPacketHandlers.ContainsKey(packetId)) { + Logger.Warn($"Tried to remove nonexistent server connection packet handler: {packetId}"); + return; + } + + _serverConnectionPacketHandlers.Remove(packetId); + } + + #endregion + + #region Client-addon-related update packet handling + + /// + /// Execute the packet handler for the client addon data from the update packet. /// /// The ID of the addon. /// The ID of the packet data for the addon. /// The packet data instance. - private void ExecuteClientAddonPacketHandler( + private void ExecuteClientAddonUpdatePacketHandler( byte addonId, byte packetId, IPacketData packetData ) { var addonPacketIdMessage = $"for addon ID {addonId} and packet ID {packetId}"; var noHandlerWarningMessage = - $"There is no client addon packet handler registered {addonPacketIdMessage}"; - if (!_clientAddonPacketHandlers.TryGetValue(addonId, out var addonPacketHandlers)) { + $"There is no client addon update packet handler registered {addonPacketIdMessage}"; + if (!_clientAddonUpdatePacketHandlers.TryGetValue(addonId, out var addonPacketHandlers)) { Logger.Warn(noHandlerWarningMessage); return; } @@ -305,22 +535,114 @@ IPacketData packetData } /// - /// Register a packet handler for client addon data. + /// Register an update packet handler for client addon data. /// /// The ID of the addon. /// The ID of the packet data for the addon. /// The handler for the data. /// Thrown if there is already a handler registered for the /// given ID. - public void RegisterClientAddonPacketHandler( + public void RegisterClientAddonUpdatePacketHandler( byte addonId, byte packetId, ClientPacketHandler handler ) { - if (!_clientAddonPacketHandlers.TryGetValue(addonId, out var addonPacketHandlers)) { + if (!_clientAddonUpdatePacketHandlers.TryGetValue(addonId, out var addonPacketHandlers)) { addonPacketHandlers = new Dictionary(); - _clientAddonPacketHandlers[addonId] = addonPacketHandlers; + _clientAddonUpdatePacketHandlers[addonId] = addonPacketHandlers; + } + + if (addonPacketHandlers.ContainsKey(packetId)) { + throw new InvalidOperationException("There is already an update packet handler for the given ID"); + } + + addonPacketHandlers[packetId] = handler; + } + + /// + /// De-register an update packet handler for client addon data. + /// + /// The ID of the addon. + /// The ID of the packet data for the addon. + /// Thrown if there is no handler registered for the + /// given ID. + public void DeregisterClientAddonUpdatePacketHandler(byte addonId, byte packetId) { + const string invalidOperationExceptionMessage = "Could not remove nonexistent addon update packet handler"; + + if (!_clientAddonUpdatePacketHandlers.TryGetValue(addonId, out var addonPacketHandlers)) { + throw new InvalidOperationException(invalidOperationExceptionMessage); + } + + if (!addonPacketHandlers.ContainsKey(packetId)) { + throw new InvalidOperationException(invalidOperationExceptionMessage); + } + + addonPacketHandlers.Remove(packetId); + } + + /// + /// Clear all registered client addon update packet handlers. + /// + public void ClearClientAddonUpdatePacketHandlers() { + _clientAddonUpdatePacketHandlers.Clear(); + } + + #endregion + + #region Client-addon-related connection packet handling + + /// + /// Execute the packet handler for the client addon data from the connection packet. + /// + /// The ID of the addon. + /// The ID of the packet data for the addon. + /// The packet data instance. + private void ExecuteClientAddonConnectionPacketHandler( + byte addonId, + byte packetId, + IPacketData packetData + ) { + var addonPacketIdMessage = $"for addon ID {addonId} and packet ID {packetId}"; + var noHandlerWarningMessage = + $"There is no client addon connection packet handler registered {addonPacketIdMessage}"; + if (!_clientAddonConnectionPacketHandlers.TryGetValue(addonId, out var addonPacketHandlers)) { + Logger.Warn(noHandlerWarningMessage); + return; + } + + if (!addonPacketHandlers.TryGetValue(packetId, out var handler)) { + Logger.Warn(noHandlerWarningMessage); + return; + } + + // Invoke the packet handler on the Unity main thread + ThreadUtil.RunActionOnMainThread(() => { + try { + handler.Invoke(packetData); + } catch (Exception e) { + Logger.Error($"Exception occurred while executing client addon connection packet handler {addonPacketIdMessage}:\n{e}"); + } + }); + } + + /// + /// Register a connection packet handler for client addon data. + /// + /// The ID of the addon. + /// The ID of the packet data for the addon. + /// The handler for the data. + /// Thrown if there is already a handler registered for the + /// given ID. + public void RegisterClientAddonConnectionPacketHandler( + byte addonId, + byte packetId, + ClientPacketHandler handler + ) { + if (!_clientAddonConnectionPacketHandlers.TryGetValue(addonId, out var addonPacketHandlers)) { + addonPacketHandlers = new Dictionary(); + + _clientAddonConnectionPacketHandlers[addonId] = addonPacketHandlers; } if (addonPacketHandlers.ContainsKey(packetId)) { @@ -331,16 +653,16 @@ ClientPacketHandler handler } /// - /// De-register a packet handler for client addon data. + /// De-register a connection packet handler for client addon data. /// /// The ID of the addon. /// The ID of the packet data for the addon. /// Thrown if there is no handler registered for the /// given ID. - public void DeregisterClientAddonPacketHandler(byte addonId, byte packetId) { + public void DeregisterClientAddonConnectionPacketHandler(byte addonId, byte packetId) { const string invalidOperationExceptionMessage = "Could not remove nonexistent addon packet handler"; - if (!_clientAddonPacketHandlers.TryGetValue(addonId, out var addonPacketHandlers)) { + if (!_clientAddonConnectionPacketHandlers.TryGetValue(addonId, out var addonPacketHandlers)) { throw new InvalidOperationException(invalidOperationExceptionMessage); } @@ -352,24 +674,24 @@ public void DeregisterClientAddonPacketHandler(byte addonId, byte packetId) { } /// - /// Clear all registered client addon packet handlers. + /// Clear all registered client addon connection packet handlers. /// - public void ClearClientAddonPacketHandlers() { - _clientAddonPacketHandlers.Clear(); + public void ClearClientAddonConnectionPacketHandlers() { + _clientAddonConnectionPacketHandlers.Clear(); } #endregion - #region Server-addon-related packet handling + #region Server-addon-related update packet handling /// - /// Execute the packet handler for the server addon data from a client. + /// Execute the packet handler for the server addon data from a client from an update packet. /// /// The ID of the client. /// The ID of the addon. /// The ID of the packet data for the addon. /// The packet data instance. - private void ExecuteServerAddonPacketHandler( + private void ExecuteServerAddonUpdatePacketHandler( ushort id, byte addonId, byte packetId, @@ -377,8 +699,8 @@ IPacketData packetData ) { var addonPacketIdMessage = $"for addon ID {addonId} and packet ID {packetId}"; var noHandlerWarningMessage = - $"There is no server addon packet handler registered {addonPacketIdMessage}"; - if (!_serverAddonPacketHandlers.TryGetValue(addonId, out var addonPacketHandlers)) { + $"There is no server addon update packet handler registered {addonPacketIdMessage}"; + if (!_serverAddonUpdatePacketHandlers.TryGetValue(addonId, out var addonPacketHandlers)) { Logger.Warn(noHandlerWarningMessage); return; } @@ -394,47 +716,47 @@ IPacketData packetData try { handler.Invoke(id, packetData); } catch (Exception e) { - Logger.Error($"Exception occurred while executing server addon packet handler {addonPacketIdMessage}:\n{e}"); + Logger.Error($"Exception occurred while executing server addon update packet handler {addonPacketIdMessage}:\n{e}"); } } /// - /// Register a packet handler for server addon data. + /// Register an update packet handler for server addon data. /// /// The ID of the addon. /// The ID of the packet data for the addon. /// The handler for the data. /// Thrown if there is already a handler registered for the /// given ID. - public void RegisterServerAddonPacketHandler( + public void RegisterServerAddonUpdatePacketHandler( byte addonId, byte packetId, ServerPacketHandler handler ) { - if (!_serverAddonPacketHandlers.TryGetValue(addonId, out var addonPacketHandlers)) { + if (!_serverAddonUpdatePacketHandlers.TryGetValue(addonId, out var addonPacketHandlers)) { addonPacketHandlers = new Dictionary(); - _serverAddonPacketHandlers[addonId] = addonPacketHandlers; + _serverAddonUpdatePacketHandlers[addonId] = addonPacketHandlers; } if (addonPacketHandlers.ContainsKey(packetId)) { - throw new InvalidOperationException("There is already a packet handler for the given ID"); + throw new InvalidOperationException("There is already an update packet handler for the given ID"); } addonPacketHandlers[packetId] = handler; } /// - /// De-register a packet handler for server addon data. + /// De-register an update packet handler for server addon data. /// /// The ID of the addon. /// The ID of the packet data for the addon. /// Thrown if there is no handler register for the /// given ID. - public void DeregisterServerAddonPacketHandler(byte addonId, byte packetId) { - const string invalidOperationExceptionMessage = "Could not remove nonexistent addon packet handler"; + public void DeregisterServerAddonUpdatePacketHandler(byte addonId, byte packetId) { + const string invalidOperationExceptionMessage = "Could not remove nonexistent addon update packet handler"; - if (!_serverAddonPacketHandlers.TryGetValue(addonId, out var addonPacketHandlers)) { + if (!_serverAddonUpdatePacketHandlers.TryGetValue(addonId, out var addonPacketHandlers)) { throw new InvalidOperationException(invalidOperationExceptionMessage); } @@ -447,6 +769,93 @@ public void DeregisterServerAddonPacketHandler(byte addonId, byte packetId) { #endregion + #region Server-addon-related connection packet handling + + /// + /// Execute the packet handler for the server addon data from a client from an connection packet. + /// + /// The ID of the client. + /// The ID of the addon. + /// The ID of the packet data for the addon. + /// The packet data instance. + private void ExecuteServerAddonConnectionPacketHandler( + ushort id, + byte addonId, + byte packetId, + IPacketData packetData + ) { + var addonPacketIdMessage = $"for addon ID {addonId} and packet ID {packetId}"; + var noHandlerWarningMessage = + $"There is no server addon connection packet handler registered {addonPacketIdMessage}"; + if (!_serverAddonUpdatePacketHandlers.TryGetValue(addonId, out var addonPacketHandlers)) { + Logger.Warn(noHandlerWarningMessage); + return; + } + + if (!addonPacketHandlers.TryGetValue(packetId, out var handler)) { + Logger.Warn(noHandlerWarningMessage); + return; + } + + // Invoke the packet handler for this ID directly, in contrast to the client packet handling. + // We don't do anything game specific with server packet handler, so there's no need to do it + // on the Unity main thread + try { + handler.Invoke(id, packetData); + } catch (Exception e) { + Logger.Error($"Exception occurred while executing server addon connection packet handler {addonPacketIdMessage}:\n{e}"); + } + } + + /// + /// Register a connection packet handler for server addon data. + /// + /// The ID of the addon. + /// The ID of the packet data for the addon. + /// The handler for the data. + /// Thrown if there is already a handler registered for the + /// given ID. + public void RegisterServerAddonConnectionPacketHandler( + byte addonId, + byte packetId, + ServerPacketHandler handler + ) { + if (!_serverAddonConnectionPacketHandlers.TryGetValue(addonId, out var addonPacketHandlers)) { + addonPacketHandlers = new Dictionary(); + + _serverAddonConnectionPacketHandlers[addonId] = addonPacketHandlers; + } + + if (addonPacketHandlers.ContainsKey(packetId)) { + throw new InvalidOperationException("There is already a connection packet handler for the given ID"); + } + + addonPacketHandlers[packetId] = handler; + } + + /// + /// De-register a connection packet handler for server addon data. + /// + /// The ID of the addon. + /// The ID of the packet data for the addon. + /// Thrown if there is no handler register for the + /// given ID. + public void DeregisterServerAddonConnectionPacketHandler(byte addonId, byte packetId) { + const string invalidOperationExceptionMessage = "Could not remove nonexistent addon connection packet handler"; + + if (!_serverAddonConnectionPacketHandlers.TryGetValue(addonId, out var addonPacketHandlers)) { + throw new InvalidOperationException(invalidOperationExceptionMessage); + } + + if (!addonPacketHandlers.ContainsKey(packetId)) { + throw new InvalidOperationException(invalidOperationExceptionMessage); + } + + addonPacketHandlers.Remove(packetId); + } + + #endregion + #region Packet handling utilities /// diff --git a/HKMP/Networking/Packet/Update/ClientUpdatePacket.cs b/HKMP/Networking/Packet/Update/ClientUpdatePacket.cs new file mode 100644 index 00000000..b2086e2a --- /dev/null +++ b/HKMP/Networking/Packet/Update/ClientUpdatePacket.cs @@ -0,0 +1,56 @@ +using Hkmp.Networking.Packet.Data; + +namespace Hkmp.Networking.Packet.Update; + +/// +/// Specialization of the update packet for server to client communication. +/// +internal class ClientUpdatePacket : UpdatePacket { + /// + protected override IPacketData InstantiatePacketDataFromId(ClientUpdatePacketId packetId) { + switch (packetId) { + case ClientUpdatePacketId.Slice: + return new SliceData(); + case ClientUpdatePacketId.SliceAck: + return new SliceAckData(); + case ClientUpdatePacketId.ServerClientDisconnect: + return new ServerClientDisconnect(); + case ClientUpdatePacketId.PlayerConnect: + return new PacketDataCollection(); + case ClientUpdatePacketId.PlayerDisconnect: + return new PacketDataCollection(); + case ClientUpdatePacketId.PlayerEnterScene: + return new PacketDataCollection(); + case ClientUpdatePacketId.PlayerAlreadyInScene: + return new ClientPlayerAlreadyInScene(); + case ClientUpdatePacketId.PlayerLeaveScene: + return new PacketDataCollection(); + case ClientUpdatePacketId.PlayerUpdate: + return new PacketDataCollection(); + case ClientUpdatePacketId.PlayerMapUpdate: + return new PacketDataCollection(); + case ClientUpdatePacketId.EntitySpawn: + return new PacketDataCollection(); + case ClientUpdatePacketId.EntityUpdate: + return new PacketDataCollection(); + case ClientUpdatePacketId.ReliableEntityUpdate: + return new PacketDataCollection(); + case ClientUpdatePacketId.SceneHostTransfer: + return new HostTransfer(); + case ClientUpdatePacketId.PlayerDeath: + return new PacketDataCollection(); + case ClientUpdatePacketId.PlayerTeamUpdate: + return new PacketDataCollection(); + case ClientUpdatePacketId.PlayerSkinUpdate: + return new PacketDataCollection(); + case ClientUpdatePacketId.ServerSettingsUpdated: + return new ServerSettingsUpdate(); + case ClientUpdatePacketId.ChatMessage: + return new PacketDataCollection(); + case ClientUpdatePacketId.SaveUpdate: + return new PacketDataCollection(); + default: + return new EmptyData(); + } + } +} diff --git a/HKMP/Networking/Packet/PacketId.cs b/HKMP/Networking/Packet/Update/ClientUpdatePacketId.cs similarity index 53% rename from HKMP/Networking/Packet/PacketId.cs rename to HKMP/Networking/Packet/Update/ClientUpdatePacketId.cs index 9328e02f..3d284bfc 100644 --- a/HKMP/Networking/Packet/PacketId.cs +++ b/HKMP/Networking/Packet/Update/ClientUpdatePacketId.cs @@ -1,18 +1,18 @@ -namespace Hkmp.Networking.Packet; +namespace Hkmp.Networking.Packet.Update; /// -/// Enumeration of packet IDs for server to client communication. +/// Enumeration of packet IDs for the update packet for server to client communication. /// -internal enum ClientPacketId { +internal enum ClientUpdatePacketId { /// - /// A response to the login request to indicate whether the client is allowed to connect. + /// Indicates slice data from a chunk for large data transfer. /// - LoginResponse = 0, + Slice = 0, /// - /// A response to the HelloServer after a succeeding login. + /// Indicates the acknowledgement for a slice from a chunk for large data transfer. /// - HelloClient = 1, + SliceAck = 1, /// /// Indicating that a client has connected. @@ -104,73 +104,3 @@ internal enum ClientPacketId { /// SaveUpdate = 19, } - -/// -/// Enumeration of packet IDs for client to server communication. -/// -public enum ServerPacketId { - /// - /// Login packet that indicates that a new client wants to connect. - /// - LoginRequest = 0, - - /// - /// Initial hello, sent when login succeeds. - /// - HelloServer = 1, - - /// - /// Indicating that a client is disconnecting. - /// - PlayerDisconnect = 2, - - /// - /// Update of realtime player values. - /// - PlayerUpdate = 3, - - /// - /// Update of player map position. - /// - PlayerMapUpdate = 4, - - /// - /// Notify that an entity has spawned. - /// - EntitySpawn = 5, - - /// - /// Update of realtime entity values. - /// - EntityUpdate = 6, - - /// - /// Update of realtime reliable entity values. - /// - ReliableEntityUpdate = 7, - - /// - /// Notify that the player has entered a new scene. - /// - PlayerEnterScene = 8, - - /// - /// Notify that the player has left their current scene. - /// - PlayerLeaveScene = 9, - - /// - /// Notify that a player has died. - /// - PlayerDeath = 10, - - /// - /// Player sent chat message. - /// - ChatMessage = 11, - - /// - /// Value in the save file has updated. - /// - SaveUpdate = 12, -} diff --git a/HKMP/Networking/Packet/Update/ServerUpdatePacket.cs b/HKMP/Networking/Packet/Update/ServerUpdatePacket.cs new file mode 100644 index 00000000..0747f708 --- /dev/null +++ b/HKMP/Networking/Packet/Update/ServerUpdatePacket.cs @@ -0,0 +1,36 @@ +using Hkmp.Networking.Packet.Data; + +namespace Hkmp.Networking.Packet.Update; + +/// +/// Specialization of the update packet for client to server communication. +/// +internal class ServerUpdatePacket : UpdatePacket { + /// + protected override IPacketData InstantiatePacketDataFromId(ServerUpdatePacketId packetId) { + switch (packetId) { + case ServerUpdatePacketId.Slice: + return new SliceData(); + case ServerUpdatePacketId.SliceAck: + return new SliceAckData(); + case ServerUpdatePacketId.PlayerUpdate: + return new PlayerUpdate(); + case ServerUpdatePacketId.PlayerMapUpdate: + return new PlayerMapUpdate(); + case ServerUpdatePacketId.EntitySpawn: + return new PacketDataCollection(); + case ServerUpdatePacketId.EntityUpdate: + return new PacketDataCollection(); + case ServerUpdatePacketId.ReliableEntityUpdate: + return new PacketDataCollection(); + case ServerUpdatePacketId.PlayerEnterScene: + return new ServerPlayerEnterScene(); + case ServerUpdatePacketId.ChatMessage: + return new ChatMessage(); + case ServerUpdatePacketId.SaveUpdate: + return new PacketDataCollection(); + default: + return new EmptyData(); + } + } +} diff --git a/HKMP/Networking/Packet/Update/ServerUpdatePacketId.cs b/HKMP/Networking/Packet/Update/ServerUpdatePacketId.cs new file mode 100644 index 00000000..06a44bfe --- /dev/null +++ b/HKMP/Networking/Packet/Update/ServerUpdatePacketId.cs @@ -0,0 +1,71 @@ +namespace Hkmp.Networking.Packet.Update; + +/// +/// Enumeration of packet IDs for the update packet for client to server communication. +/// +public enum ServerUpdatePacketId { + /// + /// Indicates slice data from a chunk for large data transfer. + /// + Slice = 0, + + /// + /// Indicates the acknowledgement for a slice from a chunk for large data transfer. + /// + SliceAck = 1, + + /// + /// Indicating that a client is disconnecting. + /// + PlayerDisconnect = 2, + + /// + /// Update of realtime player values. + /// + PlayerUpdate = 3, + + /// + /// Update of player map position. + /// + PlayerMapUpdate = 4, + + /// + /// Notify that an entity has spawned. + /// + EntitySpawn = 5, + + /// + /// Update of realtime entity values. + /// + EntityUpdate = 6, + + /// + /// Update of realtime reliable entity values. + /// + ReliableEntityUpdate = 7, + + /// + /// Notify that the player has entered a new scene. + /// + PlayerEnterScene = 8, + + /// + /// Notify that the player has left their current scene. + /// + PlayerLeaveScene = 9, + + /// + /// Notify that a player has died. + /// + PlayerDeath = 10, + + /// + /// Player sent chat message. + /// + ChatMessage = 11, + + /// + /// Value in the save file has updated. + /// + SaveUpdate = 12, +} diff --git a/HKMP/Networking/Packet/Update/UpdatePacket.cs b/HKMP/Networking/Packet/Update/UpdatePacket.cs new file mode 100644 index 00000000..b95b5488 --- /dev/null +++ b/HKMP/Networking/Packet/Update/UpdatePacket.cs @@ -0,0 +1,355 @@ +using System; +using System.Collections.Generic; +using Hkmp.Logging; +using Hkmp.Networking.Packet.Data; + +namespace Hkmp.Networking.Packet.Update; + +/// +/// Abstract base class for the update packet. +/// +/// +internal abstract class UpdatePacket : BasePacket where TPacketId : Enum { + /// + /// The sequence number of this packet. + /// + public ushort Sequence { get; set; } + + /// + /// The acknowledgement number of this packet. + /// + public ushort Ack { get; set; } + + /// + /// An array containing booleans that indicate whether sequence number (Ack - x) is also acknowledged + /// for the x-th value in the array. + /// + public bool[] AckField { get; private set; } + + /// + /// Resend packet data indexed by sequence number it originates from. + /// + protected readonly Dictionary> ResendPacketData; + + /// + /// Resend addon packet data indexed by sequence number it originates from. + /// + protected readonly Dictionary> ResendAddonPacketData; + + protected UpdatePacket() { + AckField = new bool[UdpUpdateManager.AckSize]; + + ResendPacketData = new Dictionary>(); + ResendAddonPacketData = new Dictionary>(); + } + + /// + /// Write header info into the given packet (sequence number, acknowledgement number and ack field). + /// + /// The packet to write the header info into. + private void WriteHeaders(Packet packet) { + packet.Write(Sequence); + packet.Write(Ack); + + ulong ackFieldInt = 0; + ulong currentFieldValue = 1; + for (var i = 0; i < UdpUpdateManager.AckSize; i++) { + if (AckField[i]) { + ackFieldInt |= currentFieldValue; + } + + currentFieldValue *= 2; + } + + packet.Write(ackFieldInt); + } + + /// + /// Read header info from the given packet (sequence number, acknowledgement number and ack field). + /// + /// The packet to read header info from. + private void ReadHeaders(Packet packet) { + Sequence = packet.ReadUShort(); + Ack = packet.ReadUShort(); + + // Initialize the AckField array + AckField = new bool[UdpUpdateManager.AckSize]; + + var ackFieldInt = packet.ReadULong(); + ulong currentFieldValue = 1; + for (var i = 0; i < UdpUpdateManager.AckSize; i++) { + AckField[i] = (ackFieldInt & currentFieldValue) != 0; + + currentFieldValue *= 2; + } + } + + /// + public override void CreatePacket(Packet packet) { + WriteHeaders(packet); + + base.CreatePacket(packet); + + // Put the length of the resend data as an ushort in the packet + var resendLength = (ushort) ResendPacketData.Count; + if (ResendPacketData.Count > ushort.MaxValue) { + resendLength = ushort.MaxValue; + + Logger.Error("Length of resend packet data dictionary does not fit in ushort"); + } + + packet.Write(resendLength); + + // Add each entry of lost data to resend to the packet + foreach (var seqPacketDataPair in ResendPacketData) { + var seq = seqPacketDataPair.Key; + var packetData = seqPacketDataPair.Value; + + // Make sure to not put more resend data in the packet than we specified + if (resendLength-- == 0) { + break; + } + + // First write the sequence number it belongs to + packet.Write(seq); + + // Then write the reliable packet data and note that this packet now contains reliable data + WritePacketData(packet, packetData); + ContainsReliableData = true; + } + + // Put the length of the addon resend data as an ushort in the packet + resendLength = (ushort) ResendAddonPacketData.Count; + if (ResendAddonPacketData.Count > ushort.MaxValue) { + resendLength = ushort.MaxValue; + + Logger.Error("Length of addon resend packet data dictionary does not fit in ushort"); + } + + packet.Write(resendLength); + + // Add each entry of lost addon data to resend to the packet + foreach (var seqAddonDictPair in ResendAddonPacketData) { + var seq = seqAddonDictPair.Key; + var addonDataDict = seqAddonDictPair.Value; + + // Make sure to not put more resend data in the packet than we specified + if (resendLength-- == 0) { + break; + } + + // First write the sequence number it belongs to + packet.Write(seq); + + // Then write the reliable addon data for all addons and note that this packet + // now contains reliable data + WriteAddonDataDict(packet, addonDataDict); + ContainsReliableData = true; + } + + packet.WriteLength(); + } + + /// + public override bool ReadPacket(Packet packet) { + // TODO: maybe get rid of exception catching in packet reading and rely on bool return values for all + // packet reading methods (including Packet class) + try { + ReadHeaders(packet); + } catch (Exception e) { + Logger.Debug($"Exception while reading headers of packet:\n{e}"); + return false; + } + + if (!base.ReadPacket(packet)) { + return false; + } + + try { + // Read the length of the resend data + var resendLength = packet.ReadUShort(); + + while (resendLength-- > 0) { + // Read the sequence number of the packet it was lost from + var seq = packet.ReadUShort(); + + // Create a new dictionary for the packet data and read the data from the packet into it + var packetData = new Dictionary(); + ReadPacketData(packet, packetData); + + // Input the data into the resend dictionary keyed by its sequence number + ResendPacketData[seq] = packetData; + } + + // Read the length of the addon resend data + resendLength = packet.ReadUShort(); + + while (resendLength-- > 0) { + // Read the sequence number of the packet it was lost from + var seq = packet.ReadUShort(); + + // Create a new dictionary for the addon data and read the data from the packet into it + var addonDataDict = new Dictionary(); + ReadAddonDataDict(packet, addonDataDict); + + // Input the dictionary into the resend dictionary keyed by its sequence number + ResendAddonPacketData[seq] = addonDataDict; + } + } catch (Exception e) { + Logger.Debug($"Exception while reading update packet resend data:\n{e}"); + return false; + } + + return true; + } + + /// + /// Set the reliable packet data contained in the lost packet as resend data in this one. + /// + /// The update packet instance that was lost. + public void SetLostReliableData(UpdatePacket lostPacket) { + // Retrieve the lost packet data + var lostPacketData = lostPacket.GetPacketData(); + + // Finally, put the packet data dictionary in the resend dictionary keyed by its sequence number + ResendPacketData[lostPacket.Sequence] = CopyReliableDataDict( + lostPacketData, + t => NormalPacketData.ContainsKey(t) + ); + + // Retrieve the lost addon data + var lostAddonData = lostPacket.GetAddonPacketData(); + // Create a new dictionary of addon data in which we store all reliable data from the lost packet + // for all addons in the dictionary + var toResendAddonData = new Dictionary(); + + foreach (var idLostDataPair in lostAddonData) { + var addonId = idLostDataPair.Key; + var addonPacketData = idLostDataPair.Value; + + // Construct a new AddonPacketData instance that holds the reliable data only + var newAddonPacketData = addonPacketData.GetEmptyCopy(); + newAddonPacketData.PacketData = CopyReliableDataDict( + addonPacketData.PacketData, + rawPacketId => + AddonPacketData.TryGetValue(addonId, out var existingAddonData) + && existingAddonData.PacketData.ContainsKey(rawPacketId)); + + toResendAddonData[addonId] = newAddonPacketData; + } + + // Put the addon data dictionary in the resend dictionary keyed by its sequence number + ResendAddonPacketData[lostPacket.Sequence] = toResendAddonData; + } + + /// + /// Copy all reliable data in the given dictionary of lost packet data into a new dictionary. + /// + /// The dictionary containing all packet data from a lost packet. + /// Function that checks whether for a given key there is newer data + /// available. If it returns true, lost data will be dropped. + /// The key parameter of the dictionaries to copy. + /// A new dictionary containing only the reliable data. + private Dictionary CopyReliableDataDict( + Dictionary lostPacketData, + Func reliabilityCheck + ) { + // Create a new dictionary of packet data in which we store all reliable data + var reliablePacketData = new Dictionary(); + + foreach (var keyDataPair in lostPacketData) { + var key = keyDataPair.Key; + var data = keyDataPair.Value; + + // Check if the packet data is supposed to be reliable + if (!data.IsReliable) { + continue; + } + + // Check whether we can drop it since a newer version of that data already exists + if (data.DropReliableDataIfNewerExists && reliabilityCheck(key)) { + continue; + } + + // Logger.Info($" Resending {data.GetType()} data"); + reliablePacketData[key] = data; + } + + return reliablePacketData; + } + + /// + protected override void CacheAllPacketData() { + base.CacheAllPacketData(); + + void AddResendData( + Dictionary dataDict, + Dictionary cachedData + ) { + foreach (var packetIdDataPair in dataDict) { + // Get the ID and the data itself + var packetId = packetIdDataPair.Key; + var packetData = packetIdDataPair.Value; + + // Check whether for this ID there already exists data + if (cachedData.TryGetValue(packetId, out var existingPacketData)) { + // If the existing data is a PacketDataCollection, we can simply add all the data instance to it + // If not, we simply discard the resent data, since it is older + if (existingPacketData is RawPacketDataCollection existingPacketDataCollection + && packetData is RawPacketDataCollection packetDataCollection) { + existingPacketDataCollection.DataInstances.AddRange(packetDataCollection.DataInstances); + } + } else { + // If no data exists for this ID, we can simply set the resent data for that key + cachedData[packetId] = packetData; + } + } + } + + // Iteratively add the resent packet data, but make sure to merge it with existing data + foreach (var resentPacketData in ResendPacketData.Values) { + AddResendData(resentPacketData, CachedAllPacketData); + } + + // Iteratively add the resent addon data, but make sure to merge it with existing data + foreach (var resentAddonData in ResendAddonPacketData.Values) { + foreach (var addonIdDataPair in resentAddonData) { + var addonId = addonIdDataPair.Key; + var addonPacketData = addonIdDataPair.Value; + + if (CachedAllAddonData.TryGetValue(addonId, out var existingAddonPacketData)) { + AddResendData(addonPacketData.PacketData, existingAddonPacketData.PacketData); + } else { + CachedAllAddonData[addonId] = addonPacketData; + } + } + } + + IsAllPacketDataCached = true; + } + + /// + /// Drops resend data that is duplicate, i.e. that we already received in an earlier packet. + /// + /// A queue containing sequence numbers that were already + /// received. + public void DropDuplicateResendData(Queue receivedSequenceNumbers) { + // For each key in the resend dictionary, we check whether it is contained in the + // queue of sequence numbers that we already received. If so, we remove it from the dictionary + // because it is duplicate data that we already handled + foreach (var resendSequence in new List(ResendPacketData.Keys)) { + if (receivedSequenceNumbers.Contains(resendSequence)) { + // Logger.Info("Dropping resent data due to duplication"); + ResendPacketData.Remove(resendSequence); + } + } + + // Do the same for addon data + foreach (var resendSequence in new List(ResendAddonPacketData.Keys)) { + if (receivedSequenceNumbers.Contains(resendSequence)) { + // Logger.Info("Dropping resent data due to duplication"); + ResendAddonPacketData.Remove(resendSequence); + } + } + } +} diff --git a/HKMP/Networking/Packet/UpdatePacket.cs b/HKMP/Networking/Packet/UpdatePacket.cs deleted file mode 100644 index 5e12a5c1..00000000 --- a/HKMP/Networking/Packet/UpdatePacket.cs +++ /dev/null @@ -1,945 +0,0 @@ -using System; -using System.Collections.Generic; -using Hkmp.Logging; -using Hkmp.Networking.Packet.Data; - -namespace Hkmp.Networking.Packet; - -/// -/// Abstract base class for the update packet. -/// -/// -internal abstract class UpdatePacket where T : Enum { - // ReSharper disable once StaticMemberInGenericType - /// - /// A dictionary containing addon packet info per addon ID in order to read and convert raw addon - /// packet data into IPacketData instances. - /// - public static Dictionary AddonPacketInfoDict { get; } = - new Dictionary(); - - /// - /// The underlying raw packet instance, only used for reading data out of. - /// - private readonly Packet _packet; - - /// - /// The sequence number of this packet. - /// - public ushort Sequence { get; set; } - - /// - /// The acknowledgement number of this packet. - /// - public ushort Ack { get; set; } - - /// - /// An array containing booleans that indicate whether sequence number (Ack - x) is also acknowledged - /// for the x-th value in the array. - /// - public bool[] AckField { get; private set; } - - // TODO: refactor these dictionaries into a class that contains them for readability - /// - /// Normal non-resend packet data. - /// - private readonly Dictionary _normalPacketData; - - /// - /// Resend packet data indexed by sequence number it originates from. - /// - private readonly Dictionary> _resendPacketData; - - /// - /// Packet data from addons indexed by their ID. - /// - private readonly Dictionary _addonPacketData; - - /// - /// Resend addon packet data indexed by sequence number it originates from. - /// - private readonly Dictionary> _resendAddonPacketData; - - /// - /// The combination of normal and resent packet data cached in case it needs to be queried multiple times. - /// - private Dictionary _cachedAllPacketData; - - /// - /// The combination of addon and resent addon data cached in case it needs to be queried multiple times. - /// - private Dictionary _cachedAllAddonData; - - /// - /// Whether the dictionary containing all packet data is cached already or needs to be calculated first. - /// - private bool _isAllPacketDataCached; - - /// - /// Whether this packet contains data that needs to be reliable. - /// - private bool _containsReliableData; - - /// - /// Construct the update packet with the given raw packet instance to read from. - /// - /// The raw packet instance. - protected UpdatePacket(Packet packet) { - _packet = packet; - - AckField = new bool[UdpUpdateManager.AckSize]; - - _normalPacketData = new Dictionary(); - _resendPacketData = new Dictionary>(); - _addonPacketData = new Dictionary(); - _resendAddonPacketData = new Dictionary>(); - } - - /// - /// Write header info into the given packet (sequence number, acknowledgement number and ack field). - /// - /// The packet to write the header info into. - private void WriteHeaders(Packet packet) { - packet.Write(Sequence); - packet.Write(Ack); - - ulong ackFieldInt = 0; - ulong currentFieldValue = 1; - for (var i = 0; i < UdpUpdateManager.AckSize; i++) { - if (AckField[i]) { - ackFieldInt |= currentFieldValue; - } - - currentFieldValue *= 2; - } - - packet.Write(ackFieldInt); - } - - /// - /// Read header info from the given packet (sequence number, acknowledgement number and ack field). - /// - /// The packet to read header info from. - private void ReadHeaders(Packet packet) { - Sequence = packet.ReadUShort(); - Ack = packet.ReadUShort(); - - // Initialize the AckField array - AckField = new bool[UdpUpdateManager.AckSize]; - - var ackFieldInt = packet.ReadULong(); - ulong currentFieldValue = 1; - for (var i = 0; i < UdpUpdateManager.AckSize; i++) { - AckField[i] = (ackFieldInt & currentFieldValue) != 0; - - currentFieldValue *= 2; - } - } - - /// - /// Write the given dictionary of normal or resent packet data into the given raw packet instance. - /// - /// The packet to write into. - /// Dictionary of packet data to write. - /// true if any of the data written was reliable; otherwise false. - private bool WritePacketData( - Packet packet, - Dictionary packetData - ) { - var enumValues = (T[]) Enum.GetValues(typeof(T)); - var packetIdSize = (byte) enumValues.Length; - - return WritePacketData( - packet, - packetData, - enumValues, - packetIdSize - ); - } - - /// - /// Write the data in the given instance of AddonPacketData into the given raw packet instance. - /// - /// The packet to write into. - /// AddonPacketData instance from which to data should be written. - /// true if any of the data written was reliable; otherwise false. - private bool WriteAddonPacketData( - Packet packet, - AddonPacketData addonPacketData - ) => WritePacketData( - packet, - addonPacketData.PacketData, - addonPacketData.PacketIdEnumerable, - addonPacketData.PacketIdSize - ); - - /// - /// Write the given dictionary of packet data into the given raw packet instance. - /// - /// The packet to write into. - /// The dictionary containing packet data to write in the packet. - /// An enumerator that enumerates over all possible keys in the dictionary. - /// The exact size of the key space. - /// Dictionary key parameter and enumerator parameter. - /// true if any of the data written was reliable; otherwise false. - private bool WritePacketData( - Packet packet, - Dictionary packetData, - IEnumerable keyEnumerable, - byte keySpaceSize - ) { - // Keep track of the bit flag in an unsigned long, which is the largest integer implicit type allowed - ulong idFlag = 0; - // Also keep track of the value of the current bit in an unsigned long - ulong currentTypeValue = 1; - - var keyEnumerator = keyEnumerable.GetEnumerator(); - while (keyEnumerator.MoveNext()) { - var key = keyEnumerator.Current; - - // Update the bit in the flag if the current value is included in the dictionary - if (key != null && packetData.ContainsKey(key)) { - idFlag |= currentTypeValue; - } - - // Always increase the current bit - currentTypeValue *= 2; - } - - // Based on the size of the values space, we cast to the smallest primitive that can hold the flag - // and write it to the packet - if (keySpaceSize <= 8) { - packet.Write((byte) idFlag); - } else if (keySpaceSize <= 16) { - packet.Write((ushort) idFlag); - } else if (keySpaceSize <= 32) { - packet.Write((uint) idFlag); - } else if (keySpaceSize <= 64) { - packet.Write(idFlag); - } - - // Let each individual piece of packet data write themselves into the packet - // and keep track of whether any of them need to be reliable - var containsReliableData = false; - // We loop over the possible IDs in the order from the given array to make it - // consistent between server and client - keyEnumerator.Reset(); - while (keyEnumerator.MoveNext()) { - var key = keyEnumerator.Current; - - if (key != null && packetData.TryGetValue(key, out var iPacketData)) { - iPacketData.WriteData(packet); - - if (iPacketData.IsReliable) { - containsReliableData = true; - } - } - } - - keyEnumerator.Dispose(); - - return containsReliableData; - } - - /// - /// Write the given dictionary containing addon data for all addons in the given packet. - /// - /// The raw packet instance to write into. - /// The dictionary containing all addon data to write. - /// true if any of the data written was reliable; otherwise false. - private bool WriteAddonDataDict( - Packet packet, - Dictionary addonDataDict - ) { - // Normally, we put the length of the addon packet data as a byte in the packet. - // There should only be a maximum of 255 addons, so the length should fit in a byte. - // But we don't know which addon data is going to get written correctly and which throws - // an exception, so for now we hold off on writing anything yet, but keep track of how - // many instances we are writing - var addonPacketDataCount = (byte) addonDataDict.Count; - - // We also construct a temporary packet that we use to write the progress of all - // addon packet data into. This temp packet we can then later write into the original - // packet as soon as we know the number of successful addon packet data instances we - // have written. - var addonPacketDataPacket = new Packet(); - - // Also keep track of whether we have written reliable data - var containsReliable = false; - - // Add the packet data per addon ID - foreach (var addonPacketDataPair in addonDataDict) { - var addonId = addonPacketDataPair.Key; - var addonPacketData = addonPacketDataPair.Value; - - // Create a new packet to try and write addon packet data into - var addonPacket = new Packet(); - bool addonContainsReliable; - try { - addonContainsReliable = WriteAddonPacketData( - addonPacket, - addonPacketData - ); - } catch (Exception e) { - // If the addon data writing throws an exception, we skip it entirely and since we - // wrote it in a separate packet, it has no impact on the regular packet - Logger.Debug($"Addon with ID {addonId} has thrown an exception while writing addon packet data:\n{e}"); - // We decrease the count of addon packet data's we write, so we know how many are actually in - // final packet - addonPacketDataCount--; - continue; - } - - // Prepend the length of the addon packet data to the addon packet - addonPacket.WriteLength(); - - // Now we add the addon ID to the addon packet data packet and then the contents of the addon packet - addonPacketDataPacket.Write(addonId); - addonPacketDataPacket.Write(addonPacket.ToArray()); - - // Potentially update whether this packet contains reliable data now - containsReliable |= addonContainsReliable; - } - - // Finally write the resulting size and the addon packet data itself in the regular packet - packet.Write(addonPacketDataCount); - packet.Write(addonPacketDataPacket.ToArray()); - - return containsReliable; - } - - /// - /// Read raw data from the given packet into the given packet data dictionary. - /// This method is only for normal and resent packet data, not for addon packet data. - /// - /// The raw packet instance to read from. - /// The dictionary of packet data to write the read data into. - private void ReadPacketData( - Packet packet, - Dictionary packetData - ) { - // Figure out the size of the packet ID enum - var enumValues = (T[]) Enum.GetValues(typeof(T)); - var packetIdSize = (byte) enumValues.Length; - - // Read the byte flag representing which packets are included in this update - // The number of bytes we read is dependent on the size of the enum - ulong dataPacketIdFlag = 0; - if (packetIdSize <= 8) { - dataPacketIdFlag = packet.ReadByte(); - } else if (packetIdSize <= 16) { - dataPacketIdFlag = packet.ReadUShort(); - } else if (packetIdSize <= 32) { - dataPacketIdFlag = packet.ReadUInt(); - } else if (packetIdSize <= 64) { - dataPacketIdFlag = packet.ReadULong(); - } - - // Keep track of value of current bit - ulong currentTypeValue = 1; - - var packetIdValues = Enum.GetValues(typeof(T)); - foreach (T packetId in packetIdValues) { - // If this bit was set in our flag, we add the type to the list - if ((dataPacketIdFlag & currentTypeValue) != 0) { - var iPacketData = InstantiatePacketDataFromId(packetId); - iPacketData?.ReadData(_packet); - - packetData[packetId] = iPacketData; - } - - // Increase the value of current bit - currentTypeValue *= 2; - } - } - - /// - /// Read raw addon data from the given packet into the given addon data dictionary. - /// - /// The raw packet instance to read from. - /// The size of the packet ID space. - /// A function that instantiate IPacketData implementations given a - /// packet ID in byte form. - /// The dictionary of addon data to write the read data into. - /// Thrown if the given instantiation function returns null. - private void ReadAddonPacketData( - Packet packet, - byte packetIdSize, - Func packetDataInstantiator, - Dictionary packetData - ) { - // Read the byte flag representing which packets are included in this update - // This flag may come in different primitives based on the size of the packet - // ID space - ulong dataPacketIdFlag; - - if (packetIdSize <= 8) { - dataPacketIdFlag = packet.ReadByte(); - } else if (packetIdSize <= 16) { - dataPacketIdFlag = packet.ReadUShort(); - } else if (packetIdSize <= 32) { - dataPacketIdFlag = packet.ReadUInt(); - } else if (packetIdSize <= 64) { - dataPacketIdFlag = packet.ReadULong(); - } else { - // This should never happen, but in case it does, we throw an exception - throw new Exception("Addon packet ID space size is larger than expected"); - } - - // Keep track of value of current bit in the largest integer primitive - ulong currentTypeValue = 1; - - for (byte packetId = 0; packetId < packetIdSize; packetId++) { - // If this bit was set in our flag, we add the type to the list - if ((dataPacketIdFlag & currentTypeValue) != 0) { - IPacketData iPacketData; - - // Wrap this in try catch so we can add information on what happened when the addon submitted - // packet data instantiator throws - try { - iPacketData = packetDataInstantiator.Invoke(packetId); - } catch (Exception e) { - throw new Exception( - $"Packet data instantiator for addon data threw an exception:\n{e}"); - } - - if (iPacketData == null) { - throw new Exception("Addon packet data instantiating method returned null"); - } - - iPacketData.ReadData(packet); - - packetData[packetId] = iPacketData; - } - - // Increase the value of current bit - currentTypeValue *= 2; - } - } - - /// - /// Read all raw addon data from the given packet into the given dictionary containing entries for all addons. - /// - /// The raw packet instance to read from. - /// The dictionary for all addon data to write the read data into. - /// Thrown if any part of reading the data throws. - private void ReadAddonDataDict( - Packet packet, - Dictionary addonDataDict - ) { - // Read the number of the addon packet data instances from the packet - var numAddonData = packet.ReadByte(); - - while (numAddonData-- > 0) { - var addonId = packet.ReadByte(); - - if (!AddonPacketInfoDict.TryGetValue(addonId, out var addonPacketInfo)) { - // If the addon packet info for this addon could not be found, we need to throw an exception - throw new Exception($"Addon with ID {addonId} has no defined addon packet info"); - } - - // Read the length of the addon packet data for this addon - var addonDataLength = packet.ReadUShort(); - - // Read exactly as many bytes as was indicated by the previously read value - var addonDataBytes = packet.ReadBytes(addonDataLength); - - // Create a new packet object with the given bytes so we can sandbox the reading - var addonPacket = new Packet(addonDataBytes); - - // Create a new instance of AddonPacketData to read packet data into and eventually - // add to this packet instance's dictionary - var addonPacketData = new AddonPacketData(addonPacketInfo.PacketIdSize); - - try { - ReadAddonPacketData( - addonPacket, - addonPacketInfo.PacketIdSize, - addonPacketInfo.PacketDataInstantiator, - addonPacketData.PacketData - ); - } catch (Exception e) { - // If the addon data reading throws an exception, we skip it entirely and since - // we read it into a separate packet, it has no impact on the regular packet - Logger.Debug($"Addon with ID {addonId} has thrown an exception while reading addon packet data:\n{e}"); - continue; - } - - addonDataDict[addonId] = addonPacketData; - } - } - - /// - /// Create a raw packet out of the data contained in this class. - /// - /// A new packet instance containing all data. - public Packet CreatePacket() { - var packet = new Packet(); - - WriteHeaders(packet); - - // Write the normal packet data into the packet and keep track of whether this packet - // contains reliable data now - _containsReliableData = WritePacketData(packet, _normalPacketData); - - // Put the length of the resend data as an ushort in the packet - var resendLength = (ushort) _resendPacketData.Count; - if (_resendPacketData.Count > ushort.MaxValue) { - resendLength = ushort.MaxValue; - - Logger.Error("Length of resend packet data dictionary does not fit in ushort"); - } - - packet.Write(resendLength); - - // Add each entry of lost data to resend to the packet - foreach (var seqPacketDataPair in _resendPacketData) { - var seq = seqPacketDataPair.Key; - var packetData = seqPacketDataPair.Value; - - // Make sure to not put more resend data in the packet than we specified - if (resendLength-- == 0) { - break; - } - - // First write the sequence number it belongs to - packet.Write(seq); - - // Then write the reliable packet data and note that this packet now contains reliable data - WritePacketData(packet, packetData); - _containsReliableData = true; - } - - _containsReliableData |= WriteAddonDataDict(packet, _addonPacketData); - - // Put the length of the addon resend data as an ushort in the packet - resendLength = (ushort) _resendAddonPacketData.Count; - if (_resendAddonPacketData.Count > ushort.MaxValue) { - resendLength = ushort.MaxValue; - - Logger.Error("Length of addon resend packet data dictionary does not fit in ushort"); - } - - packet.Write(resendLength); - - // Add each entry of lost addon data to resend to the packet - foreach (var seqAddonDictPair in _resendAddonPacketData) { - var seq = seqAddonDictPair.Key; - var addonDataDict = seqAddonDictPair.Value; - - // Make sure to not put more resend data in the packet than we specified - if (resendLength-- == 0) { - break; - } - - // First write the sequence number it belongs to - packet.Write(seq); - - // Then write the reliable addon data for all addons and note that this packet - // now contains reliable data - WriteAddonDataDict(packet, addonDataDict); - _containsReliableData = true; - } - - packet.WriteLength(); - - return packet; - } - - /// - /// Read the raw packet contents into easy to access dictionaries. - /// - /// false if the packet cannot be successfully read due to malformed data; otherwise true. - public bool ReadPacket() { - try { - ReadHeaders(_packet); - - // Read the normal packet data from the packet - ReadPacketData(_packet, _normalPacketData); - - // Read the length of the resend data - var resendLength = _packet.ReadUShort(); - - while (resendLength-- > 0) { - // Read the sequence number of the packet it was lost from - var seq = _packet.ReadUShort(); - - // Create a new dictionary for the packet data and read the data from the packet into it - var packetData = new Dictionary(); - ReadPacketData(_packet, packetData); - - // Input the data into the resend dictionary keyed by its sequence number - _resendPacketData[seq] = packetData; - } - - // Read the addon packet data (non-resend) - ReadAddonDataDict(_packet, _addonPacketData); - - // Read the length of the addon resend data - resendLength = _packet.ReadUShort(); - - while (resendLength-- > 0) { - // Read the sequence number of the packet it was lost from - var seq = _packet.ReadUShort(); - - // Create a new dictionary for the addon data and read the data from the packet into it - var addonDataDict = new Dictionary(); - ReadAddonDataDict(_packet, addonDataDict); - - // Input the dictionary into the resend dictionary keyed by its sequence number - _resendAddonPacketData[seq] = addonDataDict; - } - } catch { - return false; - } - - return true; - } - - /// - /// Whether this packet contains data that needs to be reliable. - /// - /// true if the packet contains reliable data; otherwise false. - public bool ContainsReliableData() { - return _containsReliableData; - } - - /// - /// Set the reliable packet data contained in the lost packet as resend data in this one. - /// - /// The update packet instance that was lost. - public void SetLostReliableData(UpdatePacket lostPacket) { - // Retrieve the lost packet data - var lostPacketData = lostPacket.GetPacketData(); - - // Finally, put the packet data dictionary in the resend dictionary keyed by its sequence number - _resendPacketData[lostPacket.Sequence] = CopyReliableDataDict( - lostPacketData, - t => _normalPacketData.ContainsKey(t) - ); - - // Retrieve the lost addon data - var lostAddonData = lostPacket.GetAddonPacketData(); - // Create a new dictionary of addon data in which we store all reliable data from the lost packet - // for all addons in the dictionary - var toResendAddonData = new Dictionary(); - - foreach (var idLostDataPair in lostAddonData) { - var addonId = idLostDataPair.Key; - var addonPacketData = idLostDataPair.Value; - - // Construct a new AddonPacketData instance that holds the reliable data only - var newAddonPacketData = addonPacketData.GetEmptyCopy(); - newAddonPacketData.PacketData = CopyReliableDataDict( - addonPacketData.PacketData, - rawPacketId => - _addonPacketData.TryGetValue(addonId, out var existingAddonData) - && existingAddonData.PacketData.ContainsKey(rawPacketId)); - - toResendAddonData[addonId] = newAddonPacketData; - } - - // Put the addon data dictionary in the resend dictionary keyed by its sequence number - _resendAddonPacketData[lostPacket.Sequence] = toResendAddonData; - } - - /// - /// Copy all reliable data in the given dictionary of lost packet data into a new dictionary. - /// - /// The dictionary containing all packet data from a lost packet. - /// Function that checks whether for a given key there is newer data - /// available. If it returns true, lost data will be dropped. - /// The key parameter of the dictionaries to copy. - /// A new dictionary containing only the reliable data. - private Dictionary CopyReliableDataDict( - Dictionary lostPacketData, - Func reliabilityCheck - ) { - // Create a new dictionary of packet data in which we store all reliable data - var reliablePacketData = new Dictionary(); - - foreach (var keyDataPair in lostPacketData) { - var key = keyDataPair.Key; - var data = keyDataPair.Value; - - // Check if the packet data is supposed to be reliable - if (!data.IsReliable) { - continue; - } - - // Check whether we can drop it since a newer version of that data already exists - if (data.DropReliableDataIfNewerExists && reliabilityCheck(key)) { - continue; - } - - // Logger.Info($" Resending {data.GetType()} data"); - reliablePacketData[key] = data; - } - - return reliablePacketData; - } - - /// - /// Tries to get packet data that is going to be sent with the given packet ID. - /// - /// The packet ID to try and get. - /// Variable to store the retrieved data in. Null if this method returns false. - /// true if the packet data exists and will be stored in the packetData variable; otherwise - /// false. - public bool TryGetSendingPacketData(T packetId, out IPacketData packetData) { - return _normalPacketData.TryGetValue(packetId, out packetData); - } - - /// - /// Tries to get addon packet data for the addon with the given ID. - /// - /// The ID of the addon to get the data for. - /// An instance of AddonPacketData corresponding to the given ID. - /// Null if this method returns false. - /// true if the addon packet data exists and will be stored in the addonPacketData variable; - /// otherwise false. - public bool TryGetSendingAddonPacketData(byte addonId, out AddonPacketData addonPacketData) { - return _addonPacketData.TryGetValue(addonId, out addonPacketData); - } - - /// - /// Sets the given packetData with the given packet ID for sending. - /// - /// The packet ID to set data for. - /// The packet data to set. - public void SetSendingPacketData(T packetId, IPacketData packetData) { - _normalPacketData[packetId] = packetData; - } - - /// - /// Sets the given addonPacketData with the given addon ID for sending. - /// - /// The addon ID to set data for. - /// Instance of AddonPacketData to set. - public void SetSendingAddonPacketData(byte addonId, AddonPacketData packetData) { - _addonPacketData[addonId] = packetData; - } - - /// - /// Get all the packet data contained in this packet, normal and resent data (but not addon data). - /// - /// A dictionary containing packet IDs mapped to packet data. - public Dictionary GetPacketData() { - if (!_isAllPacketDataCached) { - CacheAllPacketData(); - } - - return _cachedAllPacketData; - } - - /// - /// Get the addon packet data in this packet, normal addon and resent data. - /// - /// A dictionary containing addon IDs mapped to addon packet data. - public Dictionary GetAddonPacketData() { - if (!_isAllPacketDataCached) { - CacheAllPacketData(); - } - - return _cachedAllAddonData; - } - - /// - /// Computes all packet data (normal, resent, addon and addon resent data), caches it and sets a boolean - /// indicating that this cache is now available. - /// - private void CacheAllPacketData() { - // Construct a new dictionary for all the data - _cachedAllPacketData = new Dictionary(); - - // Iteratively add the normal packet data - foreach (var packetIdDataPair in _normalPacketData) { - _cachedAllPacketData.Add(packetIdDataPair.Key, packetIdDataPair.Value); - } - - void AddResendData( - Dictionary dataDict, - Dictionary cachedData - ) { - foreach (var packetIdDataPair in dataDict) { - // Get the ID and the data itself - var packetId = packetIdDataPair.Key; - var packetData = packetIdDataPair.Value; - - // Check whether for this ID there already exists data - if (cachedData.TryGetValue(packetId, out var existingPacketData)) { - // If the existing data is a PacketDataCollection, we can simply add all the data instance to it - // If not, we simply discard the resent data, since it is older - if (existingPacketData is RawPacketDataCollection existingPacketDataCollection - && packetData is RawPacketDataCollection packetDataCollection) { - existingPacketDataCollection.DataInstances.AddRange(packetDataCollection.DataInstances); - } - } else { - // If no data exists for this ID, we can simply set the resent data for that key - cachedData[packetId] = packetData; - } - } - } - - // Iteratively add the resent packet data, but make sure to merge it with existing data - foreach (var resentPacketData in _resendPacketData.Values) { - AddResendData(resentPacketData, _cachedAllPacketData); - } - - // Do the same as above but for addon data - _cachedAllAddonData = new Dictionary(); - - // Iteratively add the addon data - foreach (var addonIdDataPair in _addonPacketData) { - _cachedAllAddonData.Add(addonIdDataPair.Key, addonIdDataPair.Value); - } - - // Iteratively add the resent addon data, but make sure to merge it with existing data - foreach (var resentAddonData in _resendAddonPacketData.Values) { - foreach (var addonIdDataPair in resentAddonData) { - var addonId = addonIdDataPair.Key; - var addonPacketData = addonIdDataPair.Value; - - if (_cachedAllAddonData.TryGetValue(addonId, out var existingAddonPacketData)) { - AddResendData(addonPacketData.PacketData, existingAddonPacketData.PacketData); - } else { - _cachedAllAddonData[addonId] = addonPacketData; - } - } - } - - _isAllPacketDataCached = true; - } - - /// - /// Drops resend data that is duplicate, i.e. that we already received in an earlier packet. - /// - /// A queue containing sequence numbers that were already - /// received. - public void DropDuplicateResendData(Queue receivedSequenceNumbers) { - // For each key in the resend dictionary, we check whether it is contained in the - // queue of sequence numbers that we already received. If so, we remove it from the dictionary - // because it is duplicate data that we already handled - foreach (var resendSequence in new List(_resendPacketData.Keys)) { - if (receivedSequenceNumbers.Contains(resendSequence)) { - // Logger.Info("Dropping resent data due to duplication"); - _resendPacketData.Remove(resendSequence); - } - } - - // Do the same for addon data - foreach (var resendSequence in new List(_resendAddonPacketData.Keys)) { - if (receivedSequenceNumbers.Contains(resendSequence)) { - // Logger.Info("Dropping resent data due to duplication"); - _resendAddonPacketData.Remove(resendSequence); - } - } - } - - /// - /// Get an instantiation of IPacketData for the given packet ID. - /// - /// The packet ID to get an instance for. - /// A new instance of IPacketData. - protected abstract IPacketData InstantiatePacketDataFromId(T packetId); -} - -/// -/// Specialization of the update packet for client to server communication. -/// -internal class ServerUpdatePacket : UpdatePacket { - // This constructor is not unused, as it is a constraint for a generic parameter in the UdpUpdateManager. - // ReSharper disable once UnusedMember.Global - public ServerUpdatePacket() : this(null) { - } - - public ServerUpdatePacket(Packet packet) : base(packet) { - } - - /// - protected override IPacketData InstantiatePacketDataFromId(ServerPacketId packetId) { - switch (packetId) { - case ServerPacketId.LoginRequest: - return new LoginRequest(); - case ServerPacketId.HelloServer: - return new HelloServer(); - case ServerPacketId.PlayerUpdate: - return new PlayerUpdate(); - case ServerPacketId.PlayerMapUpdate: - return new PlayerMapUpdate(); - case ServerPacketId.EntitySpawn: - return new PacketDataCollection(); - case ServerPacketId.EntityUpdate: - return new PacketDataCollection(); - case ServerPacketId.ReliableEntityUpdate: - return new PacketDataCollection(); - case ServerPacketId.PlayerEnterScene: - return new ServerPlayerEnterScene(); - case ServerPacketId.ChatMessage: - return new ChatMessage(); - case ServerPacketId.SaveUpdate: - return new PacketDataCollection(); - default: - return new EmptyData(); - } - } -} - -/// -/// Specialization of the update packet for server to client communication. -/// -internal class ClientUpdatePacket : UpdatePacket { - public ClientUpdatePacket() : this(null) { - } - - public ClientUpdatePacket(Packet packet) : base(packet) { - } - - /// - protected override IPacketData InstantiatePacketDataFromId(ClientPacketId packetId) { - switch (packetId) { - case ClientPacketId.LoginResponse: - return new LoginResponse(); - case ClientPacketId.HelloClient: - return new HelloClient(); - case ClientPacketId.ServerClientDisconnect: - return new ServerClientDisconnect(); - case ClientPacketId.PlayerConnect: - return new PacketDataCollection(); - case ClientPacketId.PlayerDisconnect: - return new PacketDataCollection(); - case ClientPacketId.PlayerEnterScene: - return new PacketDataCollection(); - case ClientPacketId.PlayerAlreadyInScene: - return new ClientPlayerAlreadyInScene(); - case ClientPacketId.PlayerLeaveScene: - return new PacketDataCollection(); - case ClientPacketId.PlayerUpdate: - return new PacketDataCollection(); - case ClientPacketId.PlayerMapUpdate: - return new PacketDataCollection(); - case ClientPacketId.EntitySpawn: - return new PacketDataCollection(); - case ClientPacketId.EntityUpdate: - return new PacketDataCollection(); - case ClientPacketId.ReliableEntityUpdate: - return new PacketDataCollection(); - case ClientPacketId.SceneHostTransfer: - return new HostTransfer(); - case ClientPacketId.PlayerDeath: - return new PacketDataCollection(); - case ClientPacketId.PlayerTeamUpdate: - return new PacketDataCollection(); - case ClientPacketId.PlayerSkinUpdate: - return new PacketDataCollection(); - case ClientPacketId.ServerSettingsUpdated: - return new ServerSettingsUpdate(); - case ClientPacketId.ChatMessage: - return new PacketDataCollection(); - case ClientPacketId.SaveUpdate: - return new PacketDataCollection(); - default: - return new EmptyData(); - } - } -} diff --git a/HKMP/Networking/Server/DtlsServer.cs b/HKMP/Networking/Server/DtlsServer.cs index 18df25bd..01505390 100644 --- a/HKMP/Networking/Server/DtlsServer.cs +++ b/HKMP/Networking/Server/DtlsServer.cs @@ -243,7 +243,15 @@ private void ClientReceiveLoop(DtlsServerClient dtlsServerClient, CancellationTo while (!cancellationToken.IsCancellationRequested) { var buffer = new byte[dtlsTransport.GetReceiveLimit()]; - var numReceived = dtlsTransport.Receive(buffer, 0, dtlsTransport.GetReceiveLimit(), 5); + int numReceived; + + try { + numReceived = dtlsTransport.Receive(buffer, 0, dtlsTransport.GetReceiveLimit(), 5); + } catch (TlsFatalAlert alert) { + Logger.Debug($"DtlsServerClient receive call TLS fatal alert: {alert.Message}"); + continue; + } + if (numReceived <= 0) { continue; } diff --git a/HKMP/Networking/Server/NetServer.cs b/HKMP/Networking/Server/NetServer.cs index e7f649d0..167e92e5 100644 --- a/HKMP/Networking/Server/NetServer.cs +++ b/HKMP/Networking/Server/NetServer.cs @@ -8,20 +8,12 @@ using Hkmp.Api.Server.Networking; using Hkmp.Logging; using Hkmp.Networking.Packet; +using Hkmp.Networking.Packet.Connection; using Hkmp.Networking.Packet.Data; +using Hkmp.Networking.Packet.Update; namespace Hkmp.Networking.Server; -/// -/// Delegate for handling login requests. -/// -internal delegate bool LoginRequestHandler( - ushort id, - IPEndPoint ip, - LoginRequest loginRequest, - ServerUpdateManager updateManager -); - /// /// Server that manages connection with clients. /// @@ -42,14 +34,14 @@ internal class NetServer : INetServer { private readonly DtlsServer _dtlsServer; /// - /// Dictionary mapping client IDs to net server clients. + /// Dictionary mapping IP end-points to net server clients. /// - private readonly ConcurrentDictionary _registeredClients; + private readonly ConcurrentDictionary _clientsByEndPoint; /// - /// Dictionary mapping IP end-points to net server clients for all clients. + /// Dictionary mapping client IDs to net server clients. /// - private readonly ConcurrentDictionary _clients; + private readonly ConcurrentDictionary _clientsById; /// /// Dictionary for the IP addresses of clients that have their connection throttled mapped to a stopwatch @@ -58,6 +50,9 @@ internal class NetServer : INetServer { /// private readonly ConcurrentDictionary _throttledClients; + /// + /// Concurrent queue that contains received data from a client ready for processing. + /// private readonly ConcurrentQueue _receivedQueue; /// @@ -73,7 +68,7 @@ internal class NetServer : INetServer { /// /// Wait handle for inter-thread signalling when new data is ready to be processed. /// - private ManualResetEventSlim _processingWaitHandle; + private AutoResetEvent _processingWaitHandle; /// /// Event that is called when a client times out. @@ -85,11 +80,10 @@ internal class NetServer : INetServer { /// public event Action ShutdownEvent; - // TODO: expose to API to allow addons to reject connections /// - /// Event that is called when a new client wants to login. + /// Event that is called when a new client wants to connect. /// - public event LoginRequestHandler LoginRequestEvent; + public event Action ConnectionRequestEvent; /// public bool IsStarted { get; private set; } @@ -99,11 +93,16 @@ public NetServer(PacketManager packetManager) { _dtlsServer = new DtlsServer(); - _registeredClients = new ConcurrentDictionary(); - _clients = new ConcurrentDictionary(); + _clientsByEndPoint = new ConcurrentDictionary(); + _clientsById = new ConcurrentDictionary(); _throttledClients = new ConcurrentDictionary(); _receivedQueue = new ConcurrentQueue(); + + _packetManager.RegisterServerConnectionPacketHandler( + ServerConnectionPacketId.ClientInfo, + OnClientInfoReceived + ); } /// @@ -120,7 +119,7 @@ public void Start(int port) { _dtlsServer.Start(port); - _processingWaitHandle = new ManualResetEventSlim(); + _processingWaitHandle = new AutoResetEvent(false); // Create a cancellation token source for the tasks that we are creating _taskTokenSource = new CancellationTokenSource(); @@ -128,9 +127,6 @@ public void Start(int port) { // Start a thread for handling the processing of received data new Thread(() => StartProcessing(_taskTokenSource.Token)).Start(); - // Start a thread for sending updates to clients - new Thread(() => StartClientUpdates(_taskTokenSource.Token)).Start(); - _dtlsServer.DataReceivedEvent += (dtlsServerClient, buffer, length) => { _receivedQueue.Enqueue(new ReceivedData { DtlsServerClient = dtlsServerClient, @@ -147,13 +143,7 @@ public void Start(int port) { /// The cancellation token for checking whether this task is requested to cancel. private void StartProcessing(CancellationToken token) { while (!token.IsCancellationRequested) { - try { - _processingWaitHandle.Wait(token); - } catch (OperationCanceledException) { - return; - } - - _processingWaitHandle.Reset(); + _processingWaitHandle.WaitOne(); while (_receivedQueue.TryDequeue(out var receivedData)) { var packets = PacketManager.HandleReceivedData( @@ -165,7 +155,7 @@ ref _leftoverData var dtlsServerClient = receivedData.DtlsServerClient; var endPoint = dtlsServerClient.EndPoint; - if (!_clients.TryGetValue(endPoint, out var client)) { + if (!_clientsByEndPoint.TryGetValue(endPoint, out var client)) { // If the client is throttled, check their stopwatch for how long still if (_throttledClients.TryGetValue(endPoint.Address, out var clientStopwatch)) { if (clientStopwatch.ElapsedMilliseconds < ThrottleTime) { @@ -184,11 +174,9 @@ ref _leftoverData // We didn't find a client with the given address, so we assume it is a new client // that wants to connect client = CreateNewClient(dtlsServerClient); - - HandlePacketsUnregisteredClient(client, packets); - } else { - HandlePacketsRegisteredClient(client, packets); } + + HandleClientPackets(client, packets); } } } @@ -199,30 +187,21 @@ ref _leftoverData /// The DTLS server client to create the client from. /// A new net server client instance. private NetServerClient CreateNewClient(DtlsServerClient dtlsServerClient) { - var netServerClient = new NetServerClient(dtlsServerClient.DtlsTransport, dtlsServerClient.EndPoint); - netServerClient.UpdateManager.OnTimeout += () => HandleClientTimeout(netServerClient); - netServerClient.UpdateManager.StartUpdates(); + var netServerClient = new NetServerClient(dtlsServerClient.DtlsTransport, _packetManager, dtlsServerClient.EndPoint); + + netServerClient.ChunkSender.Start(); - _clients.TryAdd(dtlsServerClient.EndPoint, netServerClient); + netServerClient.ConnectionManager.ConnectionRequestEvent += OnConnectionRequest; + netServerClient.ConnectionManager.ConnectionTimeoutEvent += () => HandleClientTimeout(netServerClient); + netServerClient.ConnectionManager.StartAcceptingConnection(); - return netServerClient; - } + netServerClient.UpdateManager.TimeoutEvent += () => HandleClientTimeout(netServerClient); + netServerClient.UpdateManager.StartUpdates(); - /// - /// Start updating clients with packets. - /// - /// The cancellation token for checking whether this task is requested to cancel. - private void StartClientUpdates(CancellationToken token) { - while (!token.IsCancellationRequested) { - foreach (var client in _clients.Values) { - client.UpdateManager.ProcessUpdate(); - } + _clientsByEndPoint.TryAdd(dtlsServerClient.EndPoint, netServerClient); + _clientsById.TryAdd(netServerClient.Id, netServerClient); - // TODO: figure out a good way to get rid of the sleep here - // some way to signal when clients should be updated again would suffice - // also see NetClient#Connect - Thread.Sleep(5); - } + return netServerClient; } /// @@ -240,8 +219,8 @@ private void HandleClientTimeout(NetServerClient client) { client.Disconnect(); _dtlsServer.DisconnectClient(client.EndPoint); - _registeredClients.TryRemove(id, out _); - _clients.TryRemove(client.EndPoint, out _); + _clientsByEndPoint.TryRemove(client.EndPoint, out _); + _clientsById.TryRemove(id, out _); Logger.Info($"Client {id} timed out"); } @@ -251,37 +230,20 @@ private void HandleClientTimeout(NetServerClient client) { /// /// The registered client. /// The list of packets to handle. - private void HandlePacketsRegisteredClient(NetServerClient client, List packets) { + private void HandleClientPackets(NetServerClient client, List packets) { var id = client.Id; foreach (var packet in packets) { // Create a server update packet from the raw packet instance - var serverUpdatePacket = new ServerUpdatePacket(packet); - if (!serverUpdatePacket.ReadPacket()) { - // If ReadPacket returns false, we received a malformed packet, which we simply ignore for now - continue; - } - - client.UpdateManager.OnReceivePacket(serverUpdatePacket); - - // Let the packet manager handle the received data - _packetManager.HandleServerPacket(id, serverUpdatePacket); - } - } - - /// - /// Handle a list of packets from an unregistered client. - /// - /// The unregistered client. - /// The list of packets to handle. - private void HandlePacketsUnregisteredClient(NetServerClient client, List packets) { - for (var i = 0; i < packets.Count; i++) { - var packet = packets[i]; - - // Create a server update packet from the raw packet instance - var serverUpdatePacket = new ServerUpdatePacket(packet); - if (!serverUpdatePacket.ReadPacket()) { + var serverUpdatePacket = new ServerUpdatePacket(); + if (!serverUpdatePacket.ReadPacket(packet)) { // If ReadPacket returns false, we received a malformed packet + if (client.IsRegistered) { + // Since the client is registered already, we simply ignore the packet + continue; + } + + // If the client is not yet registered, we log the malformed packet, and throttle the client Logger.Debug($"Received malformed packet from client with IP: {client.EndPoint}"); // We throttle the client, because chances are that they are using an outdated version of the @@ -291,60 +253,81 @@ private void HandlePacketsUnregisteredClient(NetServerClient client, List(serverUpdatePacket); + client.UpdateManager.OnReceivePacket(serverUpdatePacket); - if (!serverUpdatePacket.GetPacketData().TryGetValue( - ServerPacketId.LoginRequest, - out var packetData - )) { - continue; + // First process slice or slice ack data if it exists and pass it onto the chunk sender or chunk receiver + var packetData = serverUpdatePacket.GetPacketData(); + if (packetData.TryGetValue(ServerUpdatePacketId.Slice, out var sliceData)) { + packetData.Remove(ServerUpdatePacketId.Slice); + client.ChunkReceiver.ProcessReceivedData((SliceData) sliceData); } - var loginRequest = (LoginRequest) packetData; - - Logger.Info($"Received login request from '{loginRequest.Username}'"); - - // Check if we actually have a login request handler - if (LoginRequestEvent == null) { - Logger.Error("Login request has no handler"); - return; + if (packetData.TryGetValue(ServerUpdatePacketId.SliceAck, out var sliceAckData)) { + packetData.Remove(ServerUpdatePacketId.SliceAck); + client.ChunkSender.ProcessReceivedData((SliceAckData) sliceAckData); } + + // Then, if the client is registered, we let the packet manager handle the rest of the data + if (client.IsRegistered) { + // Let the packet manager handle the received data + _packetManager.HandleServerUpdatePacket(id, serverUpdatePacket); + } + } + } + + /// + /// Callback method for when a connection request is received. + /// + /// The ID of the client. + /// The client info instance containing details about the client. + /// The server info instance that should be modified to reflect whether the client's + /// connection is accepted or not. + private void OnConnectionRequest(ushort clientId, ClientInfo clientInfo, ServerInfo serverInfo) { + if (!_clientsById.TryGetValue(clientId, out var client)) { + Logger.Error($"Connection request for client without known ID: {clientId}"); + serverInfo.ConnectionResult = ServerConnectionResult.RejectedOther; + serverInfo.ConnectionRejectedMessage = "Unknown client"; - // Invoke the handler of the login request and decide what to do with the client based on the result - var allowClient = LoginRequestEvent.Invoke( - client.Id, - client.EndPoint, - loginRequest, - client.UpdateManager - ); + return; + } + + // Invoke the connection request event ourselves first, then check the result + ConnectionRequestEvent?.Invoke(client, clientInfo, serverInfo); - if (allowClient) { - // Logger.Info($"Login request from '{loginRequest.Username}' approved"); - // client.UpdateManager.SetLoginResponseData(LoginResponseStatus.Success); + if (serverInfo.ConnectionResult == ServerConnectionResult.Accepted) { + Logger.Debug($"Connection request for client ID {clientId} was accepted, finishing connection sends, then registering client"); - // Register the client and add them to the dictionary + client.ConnectionManager.FinishConnection(() => { + Logger.Debug("Connection has finished sending data, registering client"); + client.IsRegistered = true; - _registeredClients[client.Id] = client; + client.ConnectionManager.StopAcceptingConnection(); + }); + } else { + Logger.Debug($"Connection request for client ID {clientId} was rejected, finishing connections sends, then throttling connection"); - // Now that the client is registered, we forward the rest of the packets to the other handler - var leftoverPackets = packets.GetRange( - i + 1, - packets.Count - i - 1 - ); + client.ConnectionManager.FinishConnection(() => { + Logger.Debug("Connection has finished sending data, disconnecting client and throttling"); - HandlePacketsRegisteredClient(client, leftoverPackets); - } else { - client.Disconnect(); - _clients.TryRemove(client.EndPoint, out _); + OnClientDisconnect(clientId); - // Throttle the client by adding their IP address without port to the dict _throttledClients[client.EndPoint.Address] = Stopwatch.StartNew(); + }); + } + } - Logger.Debug($"Throttling connection for client with IP: {client.EndPoint.Address}"); - } - - break; + /// + /// Callback method for when client info is received in a connection packet. + /// + /// The ID of the client that sent the client info. + /// The client info instance. + private void OnClientInfoReceived(ushort clientId, ClientInfo clientInfo) { + if (!_clientsById.TryGetValue(clientId, out var client)) { + Logger.Error($"ClientInfo received from client without known ID: {clientId}"); + return; } + + client.ConnectionManager.ProcessClientInfo(clientInfo); } /// @@ -354,13 +337,13 @@ public void Stop() { Logger.Info("Stopping NetServer"); // Clean up existing clients - foreach (var client in _clients.Values) { + foreach (var client in _clientsByEndPoint.Values) { client.Disconnect(); _dtlsServer.DisconnectClient(client.EndPoint); } - _clients.Clear(); - _registeredClients.Clear(); + _clientsByEndPoint.Clear(); + _clientsById.Clear(); _throttledClients.Clear(); _dtlsServer.Stop(); @@ -383,15 +366,15 @@ public void Stop() { /// /// The ID of the client. public void OnClientDisconnect(ushort id) { - if (!_registeredClients.TryGetValue(id, out var client)) { + if (!_clientsById.TryGetValue(id, out var client)) { Logger.Warn($"Handling disconnect from ID {id}, but there's no matching client"); return; } client.Disconnect(); _dtlsServer.DisconnectClient(client.EndPoint); - _registeredClients.TryRemove(id, out _); - _clients.TryRemove(client.EndPoint, out _); + _clientsByEndPoint.TryRemove(client.EndPoint, out _); + _clientsById.TryRemove(id, out _); Logger.Info($"Client {id} disconnected"); } @@ -403,7 +386,7 @@ public void OnClientDisconnect(ushort id) { /// The update manager for the client, or null if there does not exist a client with the /// given ID. public ServerUpdateManager GetUpdateManagerForClient(ushort id) { - if (!_registeredClients.TryGetValue(id, out var netServerClient)) { + if (!_clientsById.TryGetValue(id, out var netServerClient)) { return null; } @@ -415,7 +398,7 @@ public ServerUpdateManager GetUpdateManagerForClient(ushort id) { /// /// The action to execute with each update manager. public void SetDataForAllClients(Action dataAction) { - foreach (var netServerClient in _registeredClients.Values) { + foreach (var netServerClient in _clientsById.Values) { dataAction(netServerClient.UpdateManager); } } @@ -500,6 +483,9 @@ Func packetInstantiator /// Data class for storing received data from a given IP end-point. /// internal class ReceivedData { + /// + /// The DTLS server client that sent this data. + /// public DtlsServerClient DtlsServerClient { get; init; } /// diff --git a/HKMP/Networking/Server/NetServerClient.cs b/HKMP/Networking/Server/NetServerClient.cs index abfc6bb8..1b256cbf 100644 --- a/HKMP/Networking/Server/NetServerClient.cs +++ b/HKMP/Networking/Server/NetServerClient.cs @@ -1,5 +1,7 @@ using System.Collections.Concurrent; using System.Net; +using Hkmp.Networking.Chunk; +using Hkmp.Networking.Packet; using Org.BouncyCastle.Tls; namespace Hkmp.Networking.Server; @@ -28,11 +30,25 @@ internal class NetServerClient { /// Whether the client is registered. /// public bool IsRegistered { get; set; } - + /// /// The update manager for the client. /// public ServerUpdateManager UpdateManager { get; } + + /// + /// The chunk sender instance for sending large amounts of data. + /// + public ServerChunkSender ChunkSender { get; } + /// + /// The chunk receiver instance for receiving large amounts of data. + /// + public ServerChunkReceiver ChunkReceiver { get; } + + /// + /// The connection manager for the client. + /// + public ServerConnectionManager ConnectionManager { get; } /// /// The endpoint of the client. @@ -43,12 +59,18 @@ internal class NetServerClient { /// Construct the client with the given DTLS transport and endpoint. /// /// The underlying DTLS transport. + /// The packet manager used on the server. /// The endpoint. - public NetServerClient(DtlsTransport dtlsTransport, IPEndPoint endPoint) { + public NetServerClient(DtlsTransport dtlsTransport, PacketManager packetManager, IPEndPoint endPoint) { EndPoint = endPoint; Id = GetId(); - UpdateManager = new ServerUpdateManager(dtlsTransport); + UpdateManager = new ServerUpdateManager { + DtlsTransport = dtlsTransport + }; + ChunkSender = new ServerChunkSender(UpdateManager); + ChunkReceiver = new ServerChunkReceiver(UpdateManager); + ConnectionManager = new ServerConnectionManager(packetManager, ChunkSender, ChunkReceiver, Id); } /// @@ -58,6 +80,8 @@ public void Disconnect() { UsedIds.TryRemove(Id, out _); UpdateManager.StopUpdates(); + ChunkSender.Stop(); + ConnectionManager.StopAcceptingConnection(); } /// @@ -70,7 +94,7 @@ private static ushort GetId() { newId = _lastId++; } while (UsedIds.ContainsKey(newId)); - UsedIds[newId] = default; + UsedIds[newId] = 0; return newId; } } diff --git a/HKMP/Networking/Server/ServerConnectionManager.cs b/HKMP/Networking/Server/ServerConnectionManager.cs new file mode 100644 index 00000000..8103d100 --- /dev/null +++ b/HKMP/Networking/Server/ServerConnectionManager.cs @@ -0,0 +1,128 @@ +using System; +using System.Timers; +using Hkmp.Logging; +using Hkmp.Networking.Chunk; +using Hkmp.Networking.Packet; +using Hkmp.Networking.Packet.Connection; +using Hkmp.Networking.Packet.Data; + +namespace Hkmp.Networking.Server; + +/// +/// Server-side manager for handling the initial connection to a new client. +/// +internal class ServerConnectionManager : ConnectionManager { + /// + /// Server-side chunk sender used to handle sending chunks. + /// + private readonly ServerChunkSender _chunkSender; + /// + /// Server-side chunk received used to receive chunks. + /// + private readonly ServerChunkReceiver _chunkReceiver; + + /// + /// The ID of the client that this class manages. + /// + private readonly ushort _clientId; + + /// + /// Timer that triggers when the connection takes too long and thus times out. + /// + private readonly Timer _timeoutTimer; + + /// + /// Event that is called when the client has sent the client info, and thus we can check the connection request. + /// + public event Action ConnectionRequestEvent; + /// + /// Event that is called when the connection times out. + /// + public event Action ConnectionTimeoutEvent; + + public ServerConnectionManager( + PacketManager packetManager, + ServerChunkSender chunkSender, + ServerChunkReceiver chunkReceiver, + ushort clientId + ) : base(packetManager) { + _chunkSender = chunkSender; + _chunkReceiver = chunkReceiver; + + _clientId = clientId; + + _timeoutTimer = new Timer { + Interval = TimeoutMillis, + AutoReset = false + }; + _timeoutTimer.Elapsed += (_, _) => ConnectionTimeoutEvent?.Invoke(); + + _chunkReceiver.ChunkReceivedEvent += OnChunkReceived; + } + + /// + /// Start accepting connections, which will start the timeout timer. + /// + public void StartAcceptingConnection() { + Logger.Debug("StartAcceptingConnection"); + + _timeoutTimer.Start(); + } + + /// + /// Stop accepting connections, which will stop the timeout timer. + /// + public void StopAcceptingConnection() { + Logger.Debug("StopAcceptingConnection"); + + _timeoutTimer.Stop(); + } + + /// + /// Finish up the connection and execute the given callback when it is finished. + /// + /// The action to execute when the connection is finished. + public void FinishConnection(Action callback) { + _chunkSender.FinishSendingData(callback); + } + + /// + /// Process the given (received) client info. This will invoke the and + /// communicate the resulting server info back to the client. + /// + /// + public void ProcessClientInfo(ClientInfo clientInfo) { + Logger.Debug($"Received client info from client with ID: {_clientId}"); + + var serverInfo = new ServerInfo(); + + try { + ConnectionRequestEvent?.Invoke(_clientId, clientInfo, serverInfo); + } catch (Exception e) { + Logger.Error($"Exception occurred while executing the connection request event:\n{e}"); + } + + var connectionPacket = new ClientConnectionPacket(); + connectionPacket.SetSendingPacketData(ClientConnectionPacketId.ServerInfo, serverInfo); + + var packet = new Packet.Packet(); + connectionPacket.CreatePacket(packet); + + _chunkSender.EnqueuePacket(packet); + } + + /// + /// Callback method for when a chunk is received. Will construct a connection packet and try to read the chunk + /// into it. If successful, will let the packet manager handle the data in it. + /// + /// The raw packet that contains the data from the chunk. + private void OnChunkReceived(Packet.Packet packet) { + var connectionPacket = new ServerConnectionPacket(); + if (!connectionPacket.ReadPacket(packet)) { + Logger.Debug($"Received malformed connection packet chunk from client: {_clientId}"); + return; + } + + PacketManager.HandleServerConnectionPacket(_clientId, connectionPacket); + } +} diff --git a/HKMP/Networking/Server/ServerUpdateManager.cs b/HKMP/Networking/Server/ServerUpdateManager.cs index 9718310b..5fd5ffc3 100644 --- a/HKMP/Networking/Server/ServerUpdateManager.cs +++ b/HKMP/Networking/Server/ServerUpdateManager.cs @@ -6,21 +6,14 @@ using Hkmp.Math; using Hkmp.Networking.Packet; using Hkmp.Networking.Packet.Data; -using Org.BouncyCastle.Tls; +using Hkmp.Networking.Packet.Update; namespace Hkmp.Networking.Server; /// /// Specialization of for server to client packet sending. /// -internal class ServerUpdateManager : UdpUpdateManager { - /// - /// Construct the update manager with the given details. - /// - /// The DTLS transport instance for the client used for sending data. - public ServerUpdateManager(DtlsTransport dtlsTransport) : base(dtlsTransport) { - } - +internal class ServerUpdateManager : UdpUpdateManager { /// public override void ResendReliableData(ClientUpdatePacket lostPacket) { lock (Lock) { @@ -35,7 +28,7 @@ public override void ResendReliableData(ClientUpdatePacket lostPacket) { /// The ID of the packet data. /// The type of the generic client packet data. /// An instance of the packet data in the packet. - private T FindOrCreatePacketData(ushort id, ClientPacketId packetId) where T : GenericClientData, new() { + private T FindOrCreatePacketData(ushort id, ClientUpdatePacketId packetId) where T : GenericClientData, new() { return FindOrCreatePacketData( packetId, packetData => packetData.Id == id, @@ -54,7 +47,7 @@ public override void ResendReliableData(ClientUpdatePacket lostPacket) { /// The type of the generic client packet data. /// An instance of the packet data in the packet. private T FindOrCreatePacketData( - ClientPacketId packetId, + ClientUpdatePacketId packetId, Func findFunc, Func constructFunc ) where T : IPacketData, new() { @@ -87,31 +80,42 @@ Func constructFunc return (T) packetData; } - + /// - /// Set login response data in the current packet. + /// Set slice data in the current packet. /// - /// The login response data. - public void SetLoginResponse(LoginResponse loginResponse) { + /// The ID of the chunk the slice belongs to. + /// The ID of the slice within the chunk. + /// The number of slices in the chunk. + /// The raw data in the slice as a byte array. + public void SetSliceData(byte chunkId, byte sliceId, byte numSlices, byte[] data) { lock (Lock) { - CurrentUpdatePacket.SetSendingPacketData(ClientPacketId.LoginResponse, loginResponse); + var sliceData = new SliceData { + ChunkId = chunkId, + SliceId = sliceId, + NumSlices = numSlices, + Data = data + }; + + CurrentUpdatePacket.SetSendingPacketData(ClientUpdatePacketId.Slice, sliceData); } } /// - /// Set hello client data in the current packet. + /// Set slice acknowledgement data in the current packet. /// - /// Dictionary containing current save data of the server. - /// The list of pairs of client IDs and usernames. - public void SetHelloClientData(Dictionary currentSave, List<(ushort, string)> clientInfo) { + /// The ID of the chunk the slice belongs to. + /// The number of slices in the chunk. + /// A boolean array containing whether a certain slice in the chunk was acknowledged. + public void SetSliceAckData(byte chunkId, ushort numSlices, bool[] acked) { lock (Lock) { - var helloClient = new HelloClient { - ClientInfo = clientInfo, - CurrentSave = new CurrentSave { - SaveData = currentSave - } + var sliceAckData = new SliceAckData { + ChunkId = chunkId, + NumSlices = numSlices, + Acked = acked }; - CurrentUpdatePacket.SetSendingPacketData(ClientPacketId.HelloClient, helloClient); + + CurrentUpdatePacket.SetSendingPacketData(ClientUpdatePacketId.SliceAck, sliceAckData); } } @@ -122,7 +126,7 @@ public void SetHelloClientData(Dictionary currentSave, List<(ush /// The username of the player connecting. public void AddPlayerConnectData(ushort id, string username) { lock (Lock) { - var playerConnect = FindOrCreatePacketData(id, ClientPacketId.PlayerConnect); + var playerConnect = FindOrCreatePacketData(id, ClientUpdatePacketId.PlayerConnect); playerConnect.Id = id; playerConnect.Username = username; } @@ -137,7 +141,7 @@ public void AddPlayerConnectData(ushort id, string username) { public void AddPlayerDisconnectData(ushort id, string username, bool timedOut = false) { lock (Lock) { var playerDisconnect = - FindOrCreatePacketData(id, ClientPacketId.PlayerDisconnect); + FindOrCreatePacketData(id, ClientUpdatePacketId.PlayerDisconnect); playerDisconnect.Id = id; playerDisconnect.Username = username; playerDisconnect.TimedOut = timedOut; @@ -165,7 +169,7 @@ ushort animationClipId ) { lock (Lock) { var playerEnterScene = - FindOrCreatePacketData(id, ClientPacketId.PlayerEnterScene); + FindOrCreatePacketData(id, ClientUpdatePacketId.PlayerEnterScene); playerEnterScene.Id = id; playerEnterScene.Username = username; playerEnterScene.Position = position; @@ -200,7 +204,7 @@ bool sceneHost alreadyInScene.EntityUpdateList.AddRange(entityUpdateList); alreadyInScene.ReliableEntityUpdateList.AddRange(reliableEntityUpdateList); - CurrentUpdatePacket.SetSendingPacketData(ClientPacketId.PlayerAlreadyInScene, alreadyInScene); + CurrentUpdatePacket.SetSendingPacketData(ClientUpdatePacketId.PlayerAlreadyInScene, alreadyInScene); } } @@ -210,7 +214,7 @@ bool sceneHost /// The ID of the leaving player. public void AddPlayerLeaveSceneData(ushort id) { lock (Lock) { - var playerLeaveScene = FindOrCreatePacketData(id, ClientPacketId.PlayerLeaveScene); + var playerLeaveScene = FindOrCreatePacketData(id, ClientUpdatePacketId.PlayerLeaveScene); playerLeaveScene.Id = id; } } @@ -222,7 +226,7 @@ public void AddPlayerLeaveSceneData(ushort id) { /// The position of the player. public void UpdatePlayerPosition(ushort id, Vector2 position) { lock (Lock) { - var playerUpdate = FindOrCreatePacketData(id, ClientPacketId.PlayerUpdate); + var playerUpdate = FindOrCreatePacketData(id, ClientUpdatePacketId.PlayerUpdate); playerUpdate.UpdateTypes.Add(PlayerUpdateType.Position); playerUpdate.Position = position; } @@ -235,7 +239,7 @@ public void UpdatePlayerPosition(ushort id, Vector2 position) { /// The scale of the player. public void UpdatePlayerScale(ushort id, bool scale) { lock (Lock) { - var playerUpdate = FindOrCreatePacketData(id, ClientPacketId.PlayerUpdate); + var playerUpdate = FindOrCreatePacketData(id, ClientUpdatePacketId.PlayerUpdate); playerUpdate.UpdateTypes.Add(PlayerUpdateType.Scale); playerUpdate.Scale = scale; } @@ -248,7 +252,7 @@ public void UpdatePlayerScale(ushort id, bool scale) { /// The map position of the player. public void UpdatePlayerMapPosition(ushort id, Vector2 mapPosition) { lock (Lock) { - var playerUpdate = FindOrCreatePacketData(id, ClientPacketId.PlayerUpdate); + var playerUpdate = FindOrCreatePacketData(id, ClientUpdatePacketId.PlayerUpdate); playerUpdate.UpdateTypes.Add(PlayerUpdateType.MapPosition); playerUpdate.MapPosition = mapPosition; } @@ -261,7 +265,7 @@ public void UpdatePlayerMapPosition(ushort id, Vector2 mapPosition) { /// Whether the player has a map icon. public void UpdatePlayerMapIcon(ushort id, bool hasIcon) { lock (Lock) { - var playerMapUpdate = FindOrCreatePacketData(id, ClientPacketId.PlayerMapUpdate); + var playerMapUpdate = FindOrCreatePacketData(id, ClientUpdatePacketId.PlayerMapUpdate); playerMapUpdate.HasIcon = hasIcon; } } @@ -275,7 +279,7 @@ public void UpdatePlayerMapIcon(ushort id, bool hasIcon) { /// Boolean array containing effect info. public void UpdatePlayerAnimation(ushort id, ushort clipId, byte frame, bool[] effectInfo) { lock (Lock) { - var playerUpdate = FindOrCreatePacketData(id, ClientPacketId.PlayerUpdate); + var playerUpdate = FindOrCreatePacketData(id, ClientUpdatePacketId.PlayerUpdate); playerUpdate.UpdateTypes.Add(PlayerUpdateType.Animation); var animationInfo = new AnimationInfo { @@ -298,11 +302,11 @@ public void SetEntitySpawn(ushort id, EntityType spawningType, EntityType spawne lock (Lock) { PacketDataCollection entitySpawnCollection; - if (CurrentUpdatePacket.TryGetSendingPacketData(ClientPacketId.EntitySpawn, out var packetData)) { + if (CurrentUpdatePacket.TryGetSendingPacketData(ClientUpdatePacketId.EntitySpawn, out var packetData)) { entitySpawnCollection = (PacketDataCollection) packetData; } else { entitySpawnCollection = new PacketDataCollection(); - CurrentUpdatePacket.SetSendingPacketData(ClientPacketId.EntitySpawn, entitySpawnCollection); + CurrentUpdatePacket.SetSendingPacketData(ClientUpdatePacketId.EntitySpawn, entitySpawnCollection); } entitySpawnCollection.DataInstances.Add(new EntitySpawn { @@ -325,8 +329,8 @@ public void SetEntitySpawn(ushort id, EntityType spawningType, EntityType spawne PacketDataCollection entityUpdateCollection; var packetId = typeof(T) == typeof(EntityUpdate) - ? ClientPacketId.EntityUpdate - : ClientPacketId.ReliableEntityUpdate; + ? ClientUpdatePacketId.EntityUpdate + : ClientUpdatePacketId.ReliableEntityUpdate; // First check whether there actually exists entity data at all if (CurrentUpdatePacket.TryGetSendingPacketData( @@ -465,7 +469,7 @@ public void AddEntityHostFsmData(ushort entityId, byte fsmIndex, EntityHostFsmDa /// The name of the scene in which the player becomes scene host. public void SetSceneHostTransfer(string sceneName) { lock (Lock) { - CurrentUpdatePacket.SetSendingPacketData(ClientPacketId.SceneHostTransfer, new HostTransfer { + CurrentUpdatePacket.SetSendingPacketData(ClientUpdatePacketId.SceneHostTransfer, new HostTransfer { SceneName = sceneName }); } @@ -477,7 +481,7 @@ public void SetSceneHostTransfer(string sceneName) { /// The ID of the player. public void AddPlayerDeathData(ushort id) { lock (Lock) { - var playerDeath = FindOrCreatePacketData(id, ClientPacketId.PlayerDeath); + var playerDeath = FindOrCreatePacketData(id, ClientUpdatePacketId.PlayerDeath); playerDeath.Id = id; } } @@ -489,7 +493,7 @@ public void AddPlayerDeathData(ushort id) { public void AddPlayerTeamUpdateData(Team team) { lock (Lock) { var playerTeamUpdate = FindOrCreatePacketData( - ClientPacketId.PlayerTeamUpdate, + ClientUpdatePacketId.PlayerTeamUpdate, packetData => packetData.Self, () => new ClientPlayerTeamUpdate { Self = true @@ -507,7 +511,7 @@ public void AddPlayerTeamUpdateData(Team team) { public void AddOtherPlayerTeamUpdateData(ushort id, Team team) { lock (Lock) { var playerTeamUpdate = FindOrCreatePacketData( - ClientPacketId.PlayerTeamUpdate, + ClientUpdatePacketId.PlayerTeamUpdate, packetData => packetData.Id == id && !packetData.Self, () => new ClientPlayerTeamUpdate { Id = id @@ -524,7 +528,7 @@ public void AddOtherPlayerTeamUpdateData(ushort id, Team team) { public void AddPlayerSkinUpdateData(byte skinId) { lock (Lock) { var playerSkinUpdate = FindOrCreatePacketData( - ClientPacketId.PlayerSkinUpdate, + ClientUpdatePacketId.PlayerSkinUpdate, packetData => packetData.Self, () => new ClientPlayerSkinUpdate { Self = true @@ -542,7 +546,7 @@ public void AddPlayerSkinUpdateData(byte skinId) { public void AddOtherPlayerSkinUpdateData(ushort id, byte skinId) { lock (Lock) { var playerSkinUpdate = FindOrCreatePacketData( - ClientPacketId.PlayerSkinUpdate, + ClientUpdatePacketId.PlayerSkinUpdate, packetData => packetData.Id == id && !packetData.Self, () => new ClientPlayerSkinUpdate { Id = id @@ -559,7 +563,7 @@ public void AddOtherPlayerSkinUpdateData(ushort id, byte skinId) { public void UpdateServerSettings(ServerSettings serverSettings) { lock (Lock) { CurrentUpdatePacket.SetSendingPacketData( - ClientPacketId.ServerSettingsUpdated, + ClientUpdatePacketId.ServerSettingsUpdated, new ServerSettingsUpdate { ServerSettings = serverSettings } @@ -574,7 +578,7 @@ public void UpdateServerSettings(ServerSettings serverSettings) { public void SetDisconnect(DisconnectReason reason) { lock (Lock) { CurrentUpdatePacket.SetSendingPacketData( - ClientPacketId.ServerClientDisconnect, + ClientUpdatePacketId.ServerClientDisconnect, new ServerClientDisconnect { Reason = reason } @@ -590,12 +594,12 @@ public void AddChatMessage(string message) { lock (Lock) { PacketDataCollection packetDataCollection; - if (CurrentUpdatePacket.TryGetSendingPacketData(ClientPacketId.ChatMessage, out var packetData)) { + if (CurrentUpdatePacket.TryGetSendingPacketData(ClientUpdatePacketId.ChatMessage, out var packetData)) { packetDataCollection = (PacketDataCollection) packetData; } else { packetDataCollection = new PacketDataCollection(); - CurrentUpdatePacket.SetSendingPacketData(ClientPacketId.ChatMessage, packetDataCollection); + CurrentUpdatePacket.SetSendingPacketData(ClientUpdatePacketId.ChatMessage, packetDataCollection); } packetDataCollection.DataInstances.Add(new ChatMessage { @@ -613,11 +617,11 @@ public void SetSaveUpdate(ushort index, byte[] value) { lock (Lock) { PacketDataCollection saveUpdateCollection; - if (CurrentUpdatePacket.TryGetSendingPacketData(ClientPacketId.SaveUpdate, out var packetData)) { + if (CurrentUpdatePacket.TryGetSendingPacketData(ClientUpdatePacketId.SaveUpdate, out var packetData)) { saveUpdateCollection = (PacketDataCollection) packetData; } else { saveUpdateCollection = new PacketDataCollection(); - CurrentUpdatePacket.SetSendingPacketData(ClientPacketId.SaveUpdate, saveUpdateCollection); + CurrentUpdatePacket.SetSendingPacketData(ClientUpdatePacketId.SaveUpdate, saveUpdateCollection); } saveUpdateCollection.DataInstances.Add(new SaveUpdate { diff --git a/HKMP/Networking/ServerConnectionResult.cs b/HKMP/Networking/ServerConnectionResult.cs new file mode 100644 index 00000000..f20a167f --- /dev/null +++ b/HKMP/Networking/ServerConnectionResult.cs @@ -0,0 +1,19 @@ +namespace Hkmp.Networking; + +/// +/// Enumeration of connection result values. +/// +internal enum ServerConnectionResult { + /// + /// The client was accepted. + /// + Accepted = 0, + /// + /// The client is using different addons to the server. + /// + InvalidAddons, + /// + /// The client was rejected for other reasons. + /// + RejectedOther +} diff --git a/HKMP/Networking/UdpCongestionManager.cs b/HKMP/Networking/UdpCongestionManager.cs index a9f5ff24..6368b36b 100644 --- a/HKMP/Networking/UdpCongestionManager.cs +++ b/HKMP/Networking/UdpCongestionManager.cs @@ -3,6 +3,7 @@ using System.Diagnostics; using Hkmp.Logging; using Hkmp.Networking.Packet; +using Hkmp.Networking.Packet.Update; namespace Hkmp.Networking; @@ -292,7 +293,7 @@ public void OnSendPacket(ushort sequence, TOutgoing updatePacket) { // Check if this packet contained information that needed to be reliable // and if so, resend the data by adding it to the current packet - if (sentPacket.Packet.ContainsReliableData()) { + if (sentPacket.Packet.ContainsReliableData) { // Logger.Debug( // $"Packet ack of seq: {seqSentPacketPair.Key} with reliable data exceeded maximum RTT, assuming lost, resending data"); diff --git a/HKMP/Networking/UdpUpdateManager.cs b/HKMP/Networking/UdpUpdateManager.cs index 6fc6fca6..85d7c1fa 100644 --- a/HKMP/Networking/UdpUpdateManager.cs +++ b/HKMP/Networking/UdpUpdateManager.cs @@ -1,9 +1,12 @@ using System; +using System.Timers; using Hkmp.Concurrency; using Hkmp.Logging; using Hkmp.Networking.Packet; using Hkmp.Networking.Packet.Data; +using Hkmp.Networking.Packet.Update; using Org.BouncyCastle.Tls; +using Timer = System.Timers.Timer; namespace Hkmp.Networking; @@ -41,21 +44,11 @@ internal abstract class UdpUpdateManager : UdpUpdateManage /// private const int ReceiveQueueSize = AckSize; - /// - /// The Socket instance to use to send packets. - /// - private readonly DtlsTransport _dtlsTransport; - /// /// The UDP congestion manager instance. /// private readonly UdpCongestionManager _udpCongestionManager; - /// - /// Boolean indicating whether we are allowed to send packets. - /// - private bool _canSendPackets; - /// /// The last sent sequence number. /// @@ -82,14 +75,25 @@ internal abstract class UdpUpdateManager : UdpUpdateManage protected TOutgoing CurrentUpdatePacket; /// - /// Stopwatch to keep track of when to send a new update. + /// Timer for keeping track of when to send an update packet. /// - private readonly ConcurrentStopwatch _sendStopwatch; + private readonly Timer _sendTimer; /// - /// Stopwatch to keep track of the heart beat to know when the client times out. + /// Timer for keeping track of the connection timing out. /// - private readonly ConcurrentStopwatch _heartBeatStopwatch; + private readonly Timer _heartBeatTimer; + + /// + /// The last used send rate for the send timer. Used to check whether the interval of the timers needs to be + /// updated. + /// + private int _lastSendRate; + + /// + /// The Socket instance to use to send packets. + /// + public DtlsTransport DtlsTransport { get; set; } /// /// The current send rate in milliseconds between sending packets. @@ -104,58 +108,40 @@ internal abstract class UdpUpdateManager : UdpUpdateManage /// /// Event that is called when the client times out. /// - public event Action OnTimeout; + public event Action TimeoutEvent; /// /// Construct the update manager with a UDP socket. /// - /// The DTLS transport instance used to send data. - protected UdpUpdateManager(DtlsTransport dtlsTransport) { - _dtlsTransport = dtlsTransport; - + protected UdpUpdateManager() { _udpCongestionManager = new UdpCongestionManager(this); _receivedQueue = new ConcurrentFixedSizeQueue(ReceiveQueueSize); CurrentUpdatePacket = new TOutgoing(); - _sendStopwatch = new ConcurrentStopwatch(); - _heartBeatStopwatch = new ConcurrentStopwatch(); + // Construct the timers with correct intervals and register the Elapsed events + _sendTimer = new Timer { + AutoReset = true, + Interval = CurrentSendRate + }; + _sendTimer.Elapsed += OnSendTimerElapsed; + + _heartBeatTimer = new Timer { + AutoReset = false, + Interval = ConnectionTimeout + }; + _heartBeatTimer.Elapsed += OnHeartBeatTimerElapsed; } /// - /// Start the update manager and allow sending updates. + /// Start the update manager. This will start the send and heartbeat timers, which will respectively trigger + /// sending update packets and trigger on connection timing out. /// public void StartUpdates() { - _canSendPackets = true; - - _sendStopwatch.Restart(); - _heartBeatStopwatch.Restart(); - } - - /// - /// Process an update for this update manager. - /// - public void ProcessUpdate() { - if (!_canSendPackets) { - return; - } - - // Check if we can send another update - if (_sendStopwatch.ElapsedMilliseconds > CurrentSendRate) { - CreateAndSendUpdatePacket(); - - _sendStopwatch.Restart(); - } - - // Check heartbeat to make sure the connection is still alive - if (_heartBeatStopwatch.ElapsedMilliseconds > ConnectionTimeout) { - // The stopwatch has surpassed the connection timeout value, so we call the timeout event - OnTimeout?.Invoke(); - - // Stop the stopwatch for now to prevent the callback being executed multiple times - _heartBeatStopwatch.Reset(); - } + _lastSendRate = CurrentSendRate; + _sendTimer.Start(); + _heartBeatTimer.Start(); } /// @@ -163,14 +149,12 @@ public void ProcessUpdate() { /// public void StopUpdates() { Logger.Debug("Stopping UDP updates, sending last packet"); - + // Send the last packet CreateAndSendUpdatePacket(); - _sendStopwatch.Reset(); - _heartBeatStopwatch.Reset(); - - _canSendPackets = false; + _sendTimer.Stop(); + _heartBeatTimer.Stop(); } /// @@ -196,18 +180,20 @@ public void OnReceivePacket(TIncoming packet) _remoteSequence = sequence; } - _heartBeatStopwatch.Restart(); + // Reset the heart beat timer, as we have received a packet and the connection is alive + _heartBeatTimer.Stop(); + _heartBeatTimer.Start(); } /// /// Create and send the current update packet. /// private void CreateAndSendUpdatePacket() { - if (_dtlsTransport == null) { + if (DtlsTransport == null) { return; } - Packet.Packet packet; + var packet = new Packet.Packet(); TOutgoing updatePacket; lock (Lock) { @@ -225,7 +211,7 @@ private void CreateAndSendUpdatePacket() { } try { - packet = CurrentUpdatePacket.CreatePacket(); + CurrentUpdatePacket.CreatePacket(packet); } catch (Exception e) { Logger.Error($"An error occurred while trying to create packet:\n{e}"); return; @@ -269,6 +255,27 @@ private void CreateAndSendUpdatePacket() { SendPacket(packet); } + /// + /// Callback method for when the send timer elapses. Will create and send a new update packet and update the + /// timer interval in case the send rate changes. + /// + private void OnSendTimerElapsed(object sender, ElapsedEventArgs elapsedEventArgs) { + CreateAndSendUpdatePacket(); + + if (_lastSendRate != CurrentSendRate) { + _sendTimer.Interval = CurrentSendRate; + _lastSendRate = CurrentSendRate; + } + } + + /// + /// Callback method for when the heart beat timer elapses. Will invoke the timeout event. + /// + private void OnHeartBeatTimerElapsed(object sender, ElapsedEventArgs elapsedEventArgs) { + // The timer has surpassed the connection timeout value, so we call the timeout event + TimeoutEvent?.Invoke(); + } + /// /// Check whether the first given sequence number is greater than the second given sequence number. /// Accounts for sequence number wrap-around, by inverse comparison if differences are larger than half @@ -296,7 +303,7 @@ private bool IsSequenceGreaterThan(ushort sequence1, ushort sequence2) { private void SendPacket(Packet.Packet packet) { var buffer = packet.ToArray(); - _dtlsTransport?.Send(buffer, 0, buffer.Length); + DtlsTransport?.Send(buffer, 0, buffer.Length); } /// diff --git a/HKMP/Ui/ConnectInterface.cs b/HKMP/Ui/ConnectInterface.cs index 77dfec39..6428e527 100644 --- a/HKMP/Ui/ConnectInterface.cs +++ b/HKMP/Ui/ConnectInterface.cs @@ -137,29 +137,25 @@ public void OnSuccessfulConnect() { /// Callback method for when the client fails to connect. /// /// The result of the failed connection. - public void OnFailedConnect(ConnectFailedResult result) { + public void OnFailedConnect(ConnectionFailedResult result) { // Let the user know that the connection failed based on the result - switch (result.Type) { - case ConnectFailedResult.FailType.NotWhiteListed: - SetFeedbackText(Color.red, "Failed to connect:\nNot whitelisted"); - break; - case ConnectFailedResult.FailType.Banned: - SetFeedbackText(Color.red, "Failed to connect:\nBanned from server"); - break; - case ConnectFailedResult.FailType.InvalidAddons: + switch (result.Reason) { + case ConnectionFailedReason.InvalidAddons: SetFeedbackText(Color.red, "Failed to connect:\nInvalid addons"); break; - case ConnectFailedResult.FailType.InvalidUsername: - SetFeedbackText(Color.red, "Failed to connect:\nInvalid username"); - break; - case ConnectFailedResult.FailType.SocketException: + case ConnectionFailedReason.SocketException: + case ConnectionFailedReason.IOException: SetFeedbackText(Color.red, "Failed to connect:\nInternal error"); break; - case ConnectFailedResult.FailType.TimedOut: + case ConnectionFailedReason.TimedOut: SetFeedbackText(Color.red, "Failed to connect:\nConnection timed out"); break; - case ConnectFailedResult.FailType.Unknown: - SetFeedbackText(Color.red, "Failed to connect:\nUnknown reason"); + case ConnectionFailedReason.Other: + var message = ((ConnectionFailedMessageResult) result).Message; + SetFeedbackText(Color.red, $"Failed to connect:\n{message}"); + break; + default: + SetFeedbackText(Color.red, $"Failed to connect:\nUnknown reason"); break; } diff --git a/HKMP/Ui/UiManager.cs b/HKMP/Ui/UiManager.cs index 012cb8fb..a3007206 100644 --- a/HKMP/Ui/UiManager.cs +++ b/HKMP/Ui/UiManager.cs @@ -513,7 +513,7 @@ public void OnSuccessfulConnect() { /// Callback method for when the client fails to connect. /// /// The result of the failed connection. - public void OnFailedConnect(ConnectFailedResult result) { + public void OnFailedConnect(ConnectionFailedResult result) { _connectInterface.OnFailedConnect(result); } diff --git a/HKMP/Util/ThreadUtil.cs b/HKMP/Util/ThreadUtil.cs index 98d74b95..2e6edae8 100644 --- a/HKMP/Util/ThreadUtil.cs +++ b/HKMP/Util/ThreadUtil.cs @@ -38,12 +38,15 @@ public static void RunActionOnMainThread(Action action) { } public void Update() { - lock (Lock) { - foreach (var action in ActionsToRun) { - action.Invoke(); - } + List actions; + lock (Lock) { + actions = new List(ActionsToRun); ActionsToRun.Clear(); } + + foreach (var action in actions) { + action.Invoke(); + } } } From c291484233c58b356d40a5690927e34d58bb5a80 Mon Sep 17 00:00:00 2001 From: Extremelyd1 <10898310+Extremelyd1@users.noreply.github.com> Date: Thu, 13 Mar 2025 19:59:25 +0100 Subject: [PATCH 171/216] Change BouncyCastle ref to reference package --- HKMP/HKMP.csproj | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/HKMP/HKMP.csproj b/HKMP/HKMP.csproj index 1c27823f..c8eea141 100644 --- a/HKMP/HKMP.csproj +++ b/HKMP/HKMP.csproj @@ -31,9 +31,6 @@ $(References)\Assembly-CSharp.dll False - - $(References)\BouncyCastle.Cryptography.dll - $(References)\MMHOOK_Assembly-CSharp.dll False @@ -102,6 +99,7 @@ $(References)\UnityEngine.UIModule.dll False +