diff --git a/HKMP/Game/Client/ClientManager.cs b/HKMP/Game/Client/ClientManager.cs
index a03ea54..5f2089a 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 e4e7db3..63a29ad 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 0000000..f3c476e
--- /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 6b5b9e4..9dd73f0 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 b30b825..700d882 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 6299bfc..ccc8d64 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 0000000..b0b8054
--- /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 a6568ac..d89e21d 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 ce4776b..7263775 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 dece373..f3cddd3 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 0000000..93269f9
--- /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