Decompiled source of MimesisPersistence v0.3.1

MimesisPersistence.dll

Decompiled 5 days ago
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using System.Text;
using Cysharp.Threading.Tasks;
using FishNet.Object.Synchronizing;
using HarmonyLib;
using MelonLoader;
using MimesisPersistence;
using Mimic.Voice.SpeechSystem;
using ReluProtocol;
using ReluProtocol.Enum;
using ReluReplay;
using ReluReplay.Data;
using ReluReplay.Serializer;
using UnityEngine;

[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)]
[assembly: MelonInfo(typeof(MimesisPersistenceMod), "MimesisPersistence", "0.3.1", "JoanR", null)]
[assembly: MelonGame(null, null)]
[assembly: AssemblyTitle("MimesisPersistence")]
[assembly: AssemblyDescription("Persistence mod for Mimesis - saves SpeechEvents and Replay data")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("MimesisPersistence")]
[assembly: AssemblyCopyright("")]
[assembly: AssemblyTrademark("")]
[assembly: ComVisible(false)]
[assembly: Guid("b6cf6602-9625-476b-a64e-bbd27bbbda35")]
[assembly: AssemblyFileVersion("1.0.0.0")]
[assembly: TargetFramework(".NETFramework,Version=v4.7.2", FrameworkDisplayName = ".NET Framework 4.7.2")]
[assembly: AssemblyVersion("1.0.0.0")]
namespace MimesisPersistence
{
	public class MimesisPersistenceMod : MelonMod
	{
		private static Harmony _harmony;

		private const string HarmonyId = "MimesisPersistence";

		public override void OnInitializeMelon()
		{
			//IL_0005: Unknown result type (might be due to invalid IL or missing references)
			//IL_000f: Expected O, but got Unknown
			try
			{
				_harmony = new Harmony("MimesisPersistence");
				_harmony.PatchAll(typeof(MimesisPersistenceMod).Assembly);
				((MelonBase)this).LoggerInstance.Msg("Persistence enabled (host only). Patches applied.");
			}
			catch (Exception arg)
			{
				((MelonBase)this).LoggerInstance.Error($"Failed to apply patches: {arg}");
			}
		}

		public override void OnUpdate()
		{
			SpeechEventPoolManager.ProcessDeferredUpdates();
		}

		public override void OnDeinitializeMelon()
		{
			Harmony harmony = _harmony;
			if (harmony != null)
			{
				harmony.UnpatchSelf();
			}
		}
	}
	public static class MimesisSaveManager
	{
		private const string MimesisDataFolder = "MimesisData";

		private const string SlotPrefix = "Slot";

		private const string SpeechEventsFile = "speech_events.bin";

		private const string ReplayPlayFile = "replay_play.bin";

		private const string ReplayVoiceFile = "replay_voice.bin";

		private const string MetadataFile = "metadata.json";

		private const int MetadataVersion = 2;

		private const string BackupSuffix = ".bak";

		private const string TempSuffix = ".tmp";

		private static readonly FieldInfo CompressedAudioDataField = typeof(SpeechEvent).GetField("CompressedAudioData", BindingFlags.Instance | BindingFlags.Public);

		private static void SafeWriteAllBytes(string filePath, byte[] data)
		{
			string text = filePath + ".tmp";
			string destFileName = filePath + ".bak";
			File.WriteAllBytes(text, data);
			if (File.Exists(filePath))
			{
				try
				{
					File.Copy(filePath, destFileName, overwrite: true);
				}
				catch (Exception ex)
				{
					MelonLogger.Warning("[MimesisPersistence] Backup failed for " + Path.GetFileName(filePath) + ": " + ex.Message);
				}
			}
			if (File.Exists(filePath))
			{
				File.Delete(filePath);
			}
			File.Move(text, filePath);
		}

		private static void SafeWriteAllText(string filePath, string text)
		{
			SafeWriteAllBytes(filePath, Encoding.UTF8.GetBytes(text));
		}

		private static byte[] SafeReadAllBytes(string filePath)
		{
			if (File.Exists(filePath))
			{
				try
				{
					byte[] array = File.ReadAllBytes(filePath);
					if (array != null && array.Length != 0)
					{
						return array;
					}
				}
				catch (Exception ex)
				{
					MelonLogger.Warning("[MimesisPersistence] Main file read failed (" + Path.GetFileName(filePath) + "): " + ex.Message);
				}
			}
			string path = filePath + ".bak";
			if (File.Exists(path))
			{
				try
				{
					byte[] array2 = File.ReadAllBytes(path);
					if (array2 != null && array2.Length != 0)
					{
						MelonLogger.Warning("[MimesisPersistence] Recovered from backup: " + Path.GetFileName(path));
						return array2;
					}
				}
				catch (Exception ex2)
				{
					MelonLogger.Error("[MimesisPersistence] Backup also failed (" + Path.GetFileName(path) + "): " + ex2.Message);
				}
			}
			return null;
		}

		public static void SafeWritePlayerMapping(string filePath, string json)
		{
			SafeWriteAllText(filePath, json);
		}

		public static string SafeReadPlayerMapping(string filePath)
		{
			if (File.Exists(filePath))
			{
				try
				{
					string text = File.ReadAllText(filePath);
					if (!string.IsNullOrEmpty(text))
					{
						return text;
					}
				}
				catch (Exception ex)
				{
					MelonLogger.Warning("[MimesisPersistence] Player mapping read failed: " + ex.Message);
				}
			}
			string path = filePath + ".bak";
			if (File.Exists(path))
			{
				try
				{
					string text2 = File.ReadAllText(path);
					if (!string.IsNullOrEmpty(text2))
					{
						MelonLogger.Warning("[MimesisPersistence] Recovered player mapping from backup");
						return text2;
					}
				}
				catch (Exception ex2)
				{
					MelonLogger.Error("[MimesisPersistence] Player mapping backup also failed: " + ex2.Message);
				}
			}
			return null;
		}

		public static string GetMimesisSlotPath(int slotId)
		{
			PlatformMgr instance = MonoSingleton<PlatformMgr>.Instance;
			if ((Object)(object)instance == (Object)null)
			{
				return null;
			}
			string saveFileFolderPath = instance.GetSaveFileFolderPath();
			if (string.IsNullOrEmpty(saveFileFolderPath))
			{
				return null;
			}
			return Path.Combine(saveFileFolderPath, "MimesisData", "Slot" + slotId);
		}

		public static bool IsHost()
		{
			if (!IsHostViaNetwork())
			{
				return IsHostViaVWorld();
			}
			return true;
		}

		private static bool IsHostViaNetwork()
		{
			try
			{
				Type type = Type.GetType("FishNet.Managing.NetworkManager, FishNet.Runtime");
				if (type == null)
				{
					return false;
				}
				PropertyInfo property = type.GetProperty("Instance", BindingFlags.Static | BindingFlags.Public);
				if (property == null)
				{
					return false;
				}
				object value = property.GetValue(null);
				if (value == null)
				{
					return false;
				}
				PropertyInfo property2 = type.GetProperty("IsServer", BindingFlags.Instance | BindingFlags.Public);
				return property2 != null && (bool)property2.GetValue(value);
			}
			catch
			{
				return false;
			}
		}

		private static bool IsHostViaVWorld()
		{
			try
			{
				object hubMember = GetHubMember("vworld");
				if (hubMember == null)
				{
					return false;
				}
				object obj = hubMember.GetType().GetField("_sessionManager", BindingFlags.Instance | BindingFlags.NonPublic)?.GetValue(hubMember);
				if (obj == null)
				{
					return false;
				}
				FieldInfo field = obj.GetType().GetField("_hostSessionContext", BindingFlags.Instance | BindingFlags.NonPublic);
				if (field == null)
				{
					return false;
				}
				object value = field.GetValue(obj);
				if (value == null)
				{
					return false;
				}
				object obj2 = value.GetType().GetField("_vPlayer", BindingFlags.Instance | BindingFlags.NonPublic)?.GetValue(value);
				if (obj2 == null)
				{
					return false;
				}
				PropertyInfo property = obj2.GetType().GetProperty("IsHost", BindingFlags.Instance | BindingFlags.Public);
				return property != null && (bool)property.GetValue(obj2);
			}
			catch
			{
				return false;
			}
		}

		public static List<SpeechEvent> CollectAllSpeechEvents()
		{
			List<SpeechEvent> list = new List<SpeechEvent>();
			try
			{
				HashSet<long> hashSet = new HashSet<long>();
				SpeechEventArchive[] array = Object.FindObjectsOfType<SpeechEventArchive>();
				if (array != null)
				{
					FieldInfo field = typeof(SpeechEventArchive).GetField("events", BindingFlags.Instance | BindingFlags.Public);
					if (field != null)
					{
						SpeechEventArchive[] array2 = array;
						foreach (SpeechEventArchive obj in array2)
						{
							object value = field.GetValue(obj);
							if (value == null)
							{
								continue;
							}
							PropertyInfo property = value.GetType().GetProperty("Count", BindingFlags.Instance | BindingFlags.Public);
							PropertyInfo property2 = value.GetType().GetProperty("Item", new Type[1] { typeof(int) });
							if (property == null || property2 == null)
							{
								continue;
							}
							int num = (int)property.GetValue(value);
							for (int j = 0; j < num; j++)
							{
								object? value2 = property2.GetValue(value, new object[1] { j });
								SpeechEvent val = (SpeechEvent)((value2 is SpeechEvent) ? value2 : null);
								if (val != null && hashSet.Add(val.Id))
								{
									list.Add(val);
								}
							}
						}
					}
				}
				int count = list.Count;
				List<SpeechEvent> disconnectedEvents = SpeechEventPoolManager.GetDisconnectedEvents();
				int num2 = 0;
				foreach (SpeechEvent item in disconnectedEvents)
				{
					if (item != null && hashSet.Add(item.Id))
					{
						list.Add(item);
						num2++;
					}
				}
				List<SpeechEvent> pendingEvents = SpeechEventPoolManager.GetPendingEvents();
				int num3 = 0;
				foreach (SpeechEvent item2 in pendingEvents)
				{
					if (item2 != null && hashSet.Add(item2.Id))
					{
						list.Add(item2);
						num3++;
					}
				}
				if (num2 > 0 || num3 > 0)
				{
					MelonLogger.Msg("[MimesisPersistence] CollectAllSpeechEvents: " + $"{count} live + {num2} disconnected + {num3} pending (absent players) = {list.Count} total");
				}
			}
			catch (Exception ex)
			{
				MelonLogger.Warning("[MimesisPersistence] CollectAllSpeechEvents: " + ex.Message);
			}
			return list;
		}

		private static object GetReplayData()
		{
			try
			{
				object hubMember = GetHubMember("replayManager");
				if (hubMember == null)
				{
					return null;
				}
				FieldInfo field = typeof(ReplayManager).GetField("_recorder", BindingFlags.Instance | BindingFlags.NonPublic);
				if (field == null)
				{
					return null;
				}
				object value = field.GetValue(hubMember);
				return value?.GetType().GetField("_replayData", BindingFlags.Instance | BindingFlags.NonPublic)?.GetValue(value);
			}
			catch
			{
				return null;
			}
		}

		public static int GetCurrentSaveSlotId()
		{
			try
			{
				object hubMember = GetHubMember("vworld");
				if (hubMember == null)
				{
					return -1;
				}
				PropertyInfo property = hubMember.GetType().GetProperty("SaveSlotID", BindingFlags.Instance | BindingFlags.Public);
				if (property == null)
				{
					return -1;
				}
				return (int)property.GetValue(hubMember);
			}
			catch
			{
				return -1;
			}
		}

		public static object GetHubMember(string name)
		{
			if ((Object)(object)Hub.s == (Object)null)
			{
				return null;
			}
			Type typeFromHandle = typeof(Hub);
			FieldInfo field = typeFromHandle.GetField(name, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
			if (field != null)
			{
				return field.GetValue(Hub.s);
			}
			PropertyInfo property = typeFromHandle.GetProperty(name, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
			if (property != null && property.CanRead)
			{
				return property.GetValue(Hub.s);
			}
			return null;
		}

		private static List<MsgWithTime> GetPlayDataFromReplayData(object replayData)
		{
			if (replayData == null)
			{
				return null;
			}
			try
			{
				return replayData.GetType().GetField("_playData", BindingFlags.Instance | BindingFlags.NonPublic)?.GetValue(replayData) as List<MsgWithTime>;
			}
			catch
			{
				return null;
			}
		}

		private static List<SndWithTime> GetVoiceDataFromReplayData(object replayData)
		{
			if (replayData == null)
			{
				return null;
			}
			try
			{
				return replayData.GetType().GetField("_voiceData", BindingFlags.Instance | BindingFlags.NonPublic)?.GetValue(replayData) as List<SndWithTime>;
			}
			catch
			{
				return null;
			}
		}

		public static void SaveMimesisData(int slotId)
		{
			if (!IsHost())
			{
				MelonLogger.Msg("[MimesisPersistence] Skip save: not host.");
				return;
			}
			string mimesisSlotPath = GetMimesisSlotPath(slotId);
			if (string.IsNullOrEmpty(mimesisSlotPath))
			{
				MelonLogger.Warning("[MimesisPersistence] Skip save: slot path unavailable.");
				return;
			}
			try
			{
				Directory.CreateDirectory(mimesisSlotPath);
				List<SpeechEvent> list = CollectAllSpeechEvents();
				if (list != null && list.Count > 0)
				{
					using MemoryStream memoryStream = new MemoryStream();
					using BinaryWriter binaryWriter = new BinaryWriter(memoryStream);
					List<(byte[], byte[])> list2 = new List<(byte[], byte[])>();
					foreach (SpeechEvent item2 in list)
					{
						byte[] dataFromSndEvent = ReplayableSndEvent.GetDataFromSndEvent(item2);
						if (dataFromSndEvent != null && dataFromSndEvent.Length != 0)
						{
							byte[] item = item2.CompressedAudioData ?? Array.Empty<byte>();
							list2.Add((dataFromSndEvent, item));
						}
					}
					binaryWriter.Write(list2.Count);
					long num = 0L;
					foreach (var (array, array2) in list2)
					{
						binaryWriter.Write(array.Length);
						binaryWriter.Write(array);
						binaryWriter.Write(array2.Length);
						binaryWriter.Write(array2);
						num += array2.Length;
					}
					SafeWriteAllBytes(Path.Combine(mimesisSlotPath, "speech_events.bin"), memoryStream.ToArray());
					MelonLogger.Msg($"[MimesisPersistence] Serialized {list2.Count} SpeechEvents, audio={num / 1024}KB");
				}
				object replayData = GetReplayData();
				List<MsgWithTime> playDataFromReplayData = GetPlayDataFromReplayData(replayData);
				if (playDataFromReplayData != null && playDataFromReplayData.Count > 0)
				{
					string text = Path.Combine(mimesisSlotPath, "replay_play.bin");
					ReplaySerializer.SerializeMsgToFile(ref playDataFromReplayData, ref text);
				}
				List<SndWithTime> voiceDataFromReplayData = GetVoiceDataFromReplayData(replayData);
				if (voiceDataFromReplayData != null && voiceDataFromReplayData.Count > 0)
				{
					string text2 = Path.Combine(mimesisSlotPath, "replay_voice.bin");
					ReplaySerializer.SerializeSndToFile(ref voiceDataFromReplayData, ref text2);
				}
				SpeechEventPoolManager.SavePlayerMapping(slotId);
				int num2 = list?.Count ?? 0;
				int num3 = playDataFromReplayData?.Count ?? 0;
				int num4 = voiceDataFromReplayData?.Count ?? 0;
				SafeWriteAllText(Path.Combine(mimesisSlotPath, "metadata.json"), $"{{\"version\":{2},\"timestamp\":\"{DateTime.UtcNow:O}\",\"speechCount\":{num2},\"playCount\":{num3},\"voiceCount\":{num4}}}");
				MelonLogger.Msg($"[MimesisPersistence] Saved slot {slotId}. Speech={num2}, Play={num3}, Voice={num4}");
			}
			catch (Exception arg)
			{
				MelonLogger.Error($"[MimesisPersistence] SaveMimesisData: {arg}");
			}
		}

		public static List<SpeechEvent> LoadSpeechEvents(int slotId)
		{
			string mimesisSlotPath = GetMimesisSlotPath(slotId);
			if (string.IsNullOrEmpty(mimesisSlotPath))
			{
				return null;
			}
			string text = Path.Combine(mimesisSlotPath, "speech_events.bin");
			if (!File.Exists(text) && !File.Exists(text + ".bak"))
			{
				return null;
			}
			try
			{
				byte[] array = SafeReadAllBytes(text);
				if (array == null || array.Length < 4)
				{
					return null;
				}
				List<SpeechEvent> list = new List<SpeechEvent>();
				int num = 0;
				using (MemoryStream memoryStream = new MemoryStream(array))
				{
					using BinaryReader binaryReader = new BinaryReader(memoryStream);
					num = binaryReader.ReadInt32();
					if (num <= 0 || num > 100000)
					{
						return LoadSpeechEventsOldFormat(array);
					}
					long num2 = 0L;
					for (int i = 0; i < num; i++)
					{
						if (memoryStream.Position >= array.Length)
						{
							break;
						}
						int num3 = binaryReader.ReadInt32();
						if (num3 <= 0 || memoryStream.Position + num3 > array.Length)
						{
							continue;
						}
						byte[] metaData = binaryReader.ReadBytes(num3);
						byte[] audioData = null;
						if (memoryStream.Position + 4 <= array.Length)
						{
							int num4 = binaryReader.ReadInt32();
							if (num4 >= 0 && memoryStream.Position + num4 <= array.Length)
							{
								audioData = ((num4 > 0) ? binaryReader.ReadBytes(num4) : null);
								num2 += num4;
							}
							else
							{
								memoryStream.Position -= 4L;
							}
						}
						SpeechEvent val = DeserializeSingleSpeechEvent(metaData, audioData);
						if (val != null)
						{
							list.Add(val);
						}
					}
					MelonLogger.Msg($"[MimesisPersistence] Loaded {list.Count}/{num} SpeechEvents from slot {slotId}, audio={num2 / 1024}KB");
				}
				return (list.Count > 0) ? list : null;
			}
			catch (Exception ex)
			{
				MelonLogger.Warning("[MimesisPersistence] LoadSpeechEvents: " + ex.Message);
				return null;
			}
		}

		private static List<SpeechEvent> LoadSpeechEventsOldFormat(byte[] data)
		{
			try
			{
				Type type = FindMemoryPackSerializerType();
				if (type == null)
				{
					return null;
				}
				MethodInfo methodInfo = type.GetMethods(BindingFlags.Static | BindingFlags.Public).FirstOrDefault(delegate(MethodInfo m)
				{
					if (m.Name != "Deserialize")
					{
						return false;
					}
					ParameterInfo[] parameters = m.GetParameters();
					return parameters.Length >= 2 && parameters[0].ParameterType == typeof(Type) && parameters[1].ParameterType == typeof(byte[]);
				});
				if (methodInfo == null)
				{
					return null;
				}
				List<SpeechEvent> list = (List<SpeechEvent>)methodInfo.Invoke(null, new object[3]
				{
					typeof(List<SpeechEvent>),
					data,
					null
				});
				if (list != null && list.Count > 0)
				{
					MelonLogger.Msg($"[MimesisPersistence] Loaded {list.Count} SpeechEvents (legacy format)");
					return list;
				}
			}
			catch
			{
			}
			return null;
		}

		private static SpeechEvent DeserializeSingleSpeechEvent(byte[] metaData, byte[] audioData = null)
		{
			//IL_0012: Unknown result type (might be due to invalid IL or missing references)
			if (metaData == null || metaData.Length == 0)
			{
				return null;
			}
			try
			{
				SpeechEvent sndEvent = new ReplayableSndEvent((SndEventType)1, 0, 0L, 0L, metaData, (byte[])null).GetSndEvent();
				if (sndEvent != null && audioData != null && audioData.Length != 0 && CompressedAudioDataField != null)
				{
					CompressedAudioDataField.SetValue(sndEvent, audioData);
				}
				return sndEvent;
			}
			catch
			{
				return null;
			}
		}

		private static Type FindMemoryPackSerializerType()
		{
			Type type = Type.GetType("MemoryPack.MemoryPackSerializer, MemoryPack");
			if (type != null)
			{
				return type;
			}
			type = Type.GetType("MemoryPack.MemoryPackSerializer, MemoryPack.Core");
			if (type != null)
			{
				return type;
			}
			type = Type.GetType("MemoryPack.MemoryPackSerializer, MemoryPack.Runtime");
			if (type != null)
			{
				return type;
			}
			Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies();
			foreach (Assembly assembly in assemblies)
			{
				try
				{
					type = assembly.GetType("MemoryPack.MemoryPackSerializer");
					if (type != null)
					{
						return type;
					}
				}
				catch
				{
				}
			}
			return null;
		}

		public static List<MsgWithTime> LoadReplayPlayData(int slotId)
		{
			//IL_0033: Unknown result type (might be due to invalid IL or missing references)
			//IL_0038: Unknown result type (might be due to invalid IL or missing references)
			//IL_003b: Unknown result type (might be due to invalid IL or missing references)
			//IL_0040: Unknown result type (might be due to invalid IL or missing references)
			//IL_004c: Unknown result type (might be due to invalid IL or missing references)
			//IL_0051: Unknown result type (might be due to invalid IL or missing references)
			string mimesisSlotPath = GetMimesisSlotPath(slotId);
			if (string.IsNullOrEmpty(mimesisSlotPath))
			{
				return null;
			}
			string path = Path.Combine(mimesisSlotPath, "replay_play.bin");
			if (!File.Exists(path))
			{
				return null;
			}
			try
			{
				using FileStream fileStream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read);
				UniTask<List<MsgWithTime>> val = ReplaySerializer.DeserializeMsgFromFileAsync(fileStream);
				val.GetAwaiter().GetResult();
				return val.GetAwaiter().GetResult();
			}
			catch (Exception ex)
			{
				MelonLogger.Warning("[MimesisPersistence] LoadReplayPlayData: " + ex.Message);
				return null;
			}
		}

		public static List<SndWithTime> LoadReplayVoiceData(int slotId)
		{
			//IL_0029: Unknown result type (might be due to invalid IL or missing references)
			//IL_002e: Unknown result type (might be due to invalid IL or missing references)
			//IL_0031: Unknown result type (might be due to invalid IL or missing references)
			//IL_0036: Unknown result type (might be due to invalid IL or missing references)
			//IL_0041: Unknown result type (might be due to invalid IL or missing references)
			//IL_0046: Unknown result type (might be due to invalid IL or missing references)
			string mimesisSlotPath = GetMimesisSlotPath(slotId);
			if (string.IsNullOrEmpty(mimesisSlotPath))
			{
				return null;
			}
			string text = Path.Combine(mimesisSlotPath, "replay_voice.bin");
			if (!File.Exists(text))
			{
				return null;
			}
			try
			{
				UniTask<List<SndWithTime>> val = ReplaySerializer.DeserializeSndFromFileAsync(text);
				val.GetAwaiter().GetResult();
				return val.GetAwaiter().GetResult();
			}
			catch (Exception ex)
			{
				MelonLogger.Warning("[MimesisPersistence] LoadReplayVoiceData: " + ex.Message);
				return null;
			}
		}

		public static bool HasMimesisData(int slotId)
		{
			string mimesisSlotPath = GetMimesisSlotPath(slotId);
			if (string.IsNullOrEmpty(mimesisSlotPath))
			{
				return false;
			}
			if (!File.Exists(Path.Combine(mimesisSlotPath, "speech_events.bin")) && !File.Exists(Path.Combine(mimesisSlotPath, "speech_events.bin.bak")) && !File.Exists(Path.Combine(mimesisSlotPath, "replay_play.bin")))
			{
				return File.Exists(Path.Combine(mimesisSlotPath, "replay_voice.bin"));
			}
			return true;
		}

		public static void DeleteMimesisData(int slotId)
		{
			string mimesisSlotPath = GetMimesisSlotPath(slotId);
			if (string.IsNullOrEmpty(mimesisSlotPath) || !Directory.Exists(mimesisSlotPath))
			{
				return;
			}
			try
			{
				Directory.Delete(mimesisSlotPath, recursive: true);
			}
			catch (Exception ex)
			{
				MelonLogger.Warning("[MimesisPersistence] DeleteMimesisData: " + ex.Message);
			}
		}
	}
	public static class SpeechEventPoolManager
	{
		public enum EventState
		{
			Pending,
			Injected,
			Fallback
		}

		private static readonly Dictionary<long, (SpeechEvent ev, EventState state, string originalPlayerName)> _pool = new Dictionary<long, (SpeechEvent, EventState, string)>();

		private static readonly Dictionary<string, List<long>> _byPlayerName = new Dictionary<string, List<long>>();

		private static readonly Dictionary<ulong, string> _steamToDissonance = new Dictionary<ulong, string>();

		private static int _loadedSlotId = -1;

		private static SpeechEventArchive _localArchive;

		private static readonly List<(SpeechEventArchive archive, List<SpeechEvent> events)> _deferredNameUpdates = new List<(SpeechEventArchive, List<SpeechEvent>)>();

		private static readonly List<SpeechEventArchive> _deferredInjectionArchives = new List<SpeechEventArchive>();

		private static readonly Dictionary<long, SpeechEvent> _disconnectedCache = new Dictionary<long, SpeechEvent>();

		private static readonly Dictionary<ulong, string> _disconnectedPlayerMappings = new Dictionary<ulong, string>();

		private static readonly FieldInfo RecordedTimeField = typeof(SpeechEvent).GetField("RecordedTime", BindingFlags.Instance | BindingFlags.Public);

		private static readonly FieldInfo LastPlayedTimeField = typeof(SpeechEvent).GetField("LastPlayedTime", BindingFlags.Instance | BindingFlags.Public);

		private const string PlayerMappingFile = "player_mapping.json";

		public static int TotalCount => _pool.Count;

		public static bool IsLoaded
		{
			get
			{
				if (_loadedSlotId >= 0)
				{
					return _pool.Count > 0;
				}
				return false;
			}
		}

		public static int DisconnectedCacheCount => _disconnectedCache.Count;

		public static void LoadForSlot(int slotId)
		{
			if (slotId == _loadedSlotId && _pool.Count > 0)
			{
				return;
			}
			Reset();
			_loadedSlotId = slotId;
			List<SpeechEvent> list = MimesisSaveManager.LoadSpeechEvents(slotId);
			if (list == null || list.Count == 0)
			{
				MelonLogger.Msg("[SpeechEventPoolManager] No events to load for slot " + slotId);
				return;
			}
			foreach (SpeechEvent item in list)
			{
				if (item != null && !_pool.ContainsKey(item.Id))
				{
					string text = item.PlayerName ?? "";
					_pool[item.Id] = (item, EventState.Pending, text);
					if (!_byPlayerName.TryGetValue(text, out var value))
					{
						value = new List<long>();
						_byPlayerName[text] = value;
					}
					value.Add(item.Id);
				}
			}
			LoadPlayerMapping(slotId);
			MelonLogger.Msg($"[SpeechEventPoolManager] Loaded {_pool.Count} events for slot {slotId} " + $"({_steamToDissonance.Count} SteamID mappings)");
		}

		public static List<SpeechEvent> ClaimEventsForArchive(string playerId, long playerUID, bool isLocal = false, SpeechEventArchive archive = null)
		{
			List<SpeechEvent> list = new List<SpeechEvent>();
			if (_pool.Count == 0)
			{
				return list;
			}
			MelonLogger.Msg("[SpeechEventPoolManager] ClaimEventsForArchive: PlayerId='" + playerId + "', " + $"PlayerUID={playerUID}, isLocal={isLocal}, " + string.Format("pool has {0} events, playerNames in pool: [{1}]", _pool.Count, string.Join(", ", _byPlayerName.Keys)));
			HashSet<string> hashSet = new HashSet<string>();
			if (!string.IsNullOrEmpty(playerId) && _byPlayerName.ContainsKey(playerId))
			{
				hashSet.Add(playerId);
				MelonLogger.Msg("[SpeechEventPoolManager] Level 1 match: DissonanceID '" + playerId + "' found directly");
			}
			ulong steamIdForPlayerUID = GetSteamIdForPlayerUID(playerUID, isLocal);
			if (steamIdForPlayerUID != 0L)
			{
				if (_steamToDissonance.TryGetValue(steamIdForPlayerUID, out var value))
				{
					if (_byPlayerName.ContainsKey(value))
					{
						hashSet.Add(value);
						MelonLogger.Msg($"[SpeechEventPoolManager] Level 2 match: SteamID {steamIdForPlayerUID} " + "-> old DissonanceID '" + value + "' (new: '" + playerId + "')");
					}
				}
				else
				{
					MelonLogger.Msg($"[SpeechEventPoolManager] Level 2: SteamID {steamIdForPlayerUID} not in mapping " + $"({_steamToDissonance.Count} entries)");
				}
			}
			else
			{
				MelonLogger.Msg("[SpeechEventPoolManager] Level 2: Could not resolve SteamID for " + $"PlayerUID={playerUID}");
			}
			foreach (string item in hashSet)
			{
				if (!_byPlayerName.TryGetValue(item, out var value2))
				{
					continue;
				}
				foreach (long item2 in value2)
				{
					if (_pool.TryGetValue(item2, out (SpeechEvent, EventState, string) value3) && value3.Item2 == EventState.Pending)
					{
						_pool[item2] = (value3.Item1, EventState.Injected, value3.Item3);
						list.Add(value3.Item1);
					}
				}
			}
			if (list.Count > 0)
			{
				if (!string.IsNullOrEmpty(playerId))
				{
					foreach (SpeechEvent item3 in list)
					{
						item3.PlayerName = playerId;
					}
					MelonLogger.Msg($"[SpeechEventPoolManager] Claimed {list.Count} events, " + "PlayerName updated to '" + playerId + "'");
				}
				else if ((Object)(object)archive != (Object)null)
				{
					_deferredNameUpdates.Add((archive, new List<SpeechEvent>(list)));
					MelonLogger.Msg($"[SpeechEventPoolManager] Claimed {list.Count} events, " + "PlayerId empty -> deferred PlayerName update registered");
				}
			}
			else
			{
				var (num, num2, num3) = GetCounts();
				MelonLogger.Msg("[SpeechEventPoolManager] No events claimed for PlayerId='" + playerId + "' (matched names: [" + string.Join(", ", hashSet) + "], " + $"pool: {num}P/{num2}I/{num3}F)");
			}
			return list;
		}

		private static ulong GetSteamIdForPlayerUID(long playerUID, bool isLocal = false)
		{
			if (playerUID != 0L)
			{
				try
				{
					object hubMember = MimesisSaveManager.GetHubMember("pdata");
					if (hubMember != null)
					{
						FieldInfo field = hubMember.GetType().GetField("actorUIDToSteamID", BindingFlags.Instance | BindingFlags.Public);
						if (field != null && field.GetValue(hubMember) is Dictionary<long, ulong> dictionary && dictionary.TryGetValue(playerUID, out var value))
						{
							MelonLogger.Msg($"[SpeechEventPoolManager] Resolved PlayerUID {playerUID} -> SteamID {value} (from actorUIDToSteamID)");
							return value;
						}
					}
				}
				catch (Exception ex)
				{
					MelonLogger.Warning("[SpeechEventPoolManager] actorUIDToSteamID lookup error: " + ex.Message);
				}
			}
			if (isLocal)
			{
				ulong localSteamId = GetLocalSteamId();
				if (localSteamId != 0L)
				{
					MelonLogger.Msg($"[SpeechEventPoolManager] Resolved host SteamID={localSteamId} (from PlatformMgr, PlayerUID was {playerUID})");
					return localSteamId;
				}
			}
			MelonLogger.Warning($"[SpeechEventPoolManager] Could not resolve SteamID for PlayerUID={playerUID} (isLocal={isLocal})");
			return 0uL;
		}

		private static ulong GetLocalSteamId()
		{
			try
			{
				PlatformMgr instance = MonoSingleton<PlatformMgr>.Instance;
				if ((Object)(object)instance == (Object)null)
				{
					return 0uL;
				}
				FieldInfo field = typeof(PlatformMgr).GetField("_uniqueUserPath", BindingFlags.Instance | BindingFlags.NonPublic);
				if (field == null)
				{
					return 0uL;
				}
				string text = field.GetValue(instance) as string;
				if (!string.IsNullOrEmpty(text) && ulong.TryParse(text, out var result))
				{
					return result;
				}
			}
			catch (Exception ex)
			{
				MelonLogger.Warning("[SpeechEventPoolManager] GetLocalSteamId error: " + ex.Message);
			}
			return 0uL;
		}

		public static List<SpeechEvent> ForceFallbackToLocal()
		{
			List<SpeechEvent> list = new List<SpeechEvent>();
			foreach (long item in _pool.Keys.ToList())
			{
				(SpeechEvent, EventState, string) tuple = _pool[item];
				if (tuple.Item2 == EventState.Pending)
				{
					_pool[item] = (tuple.Item1, EventState.Fallback, tuple.Item3);
					list.Add(tuple.Item1);
				}
			}
			if (list.Count > 0)
			{
				MelonLogger.Msg($"[SpeechEventPoolManager] Forced {list.Count} events to FALLBACK");
			}
			return list;
		}

		public static void RegisterDeferredInjection(SpeechEventArchive archive)
		{
			if ((Object)(object)archive == (Object)null)
			{
				return;
			}
			foreach (SpeechEventArchive deferredInjectionArchive in _deferredInjectionArchives)
			{
				if ((Object)(object)deferredInjectionArchive == (Object)(object)archive)
				{
					return;
				}
			}
			_deferredInjectionArchives.Add(archive);
			MelonLogger.Msg("[SpeechEventPoolManager] Registered archive for deferred injection " + $"(waiting for SyncVars, {_deferredInjectionArchives.Count} pending)");
		}

		public static void ProcessDeferredUpdates()
		{
			if (_deferredInjectionArchives.Count > 0)
			{
				for (int num = _deferredInjectionArchives.Count - 1; num >= 0; num--)
				{
					SpeechEventArchive val = _deferredInjectionArchives[num];
					if ((Object)(object)val == (Object)null)
					{
						_deferredInjectionArchives.RemoveAt(num);
						continue;
					}
					string playerId;
					long playerUID;
					bool isLocal;
					try
					{
						playerId = val.PlayerId;
						playerUID = val.PlayerUID;
						isLocal = val.IsLocal;
					}
					catch
					{
						continue;
					}
					if (string.IsNullOrEmpty(playerId) && playerUID == 0L)
					{
						continue;
					}
					_deferredInjectionArchives.RemoveAt(num);
					MelonLogger.Msg("[SpeechEventPoolManager] Deferred injection: SyncVars ready for " + $"PlayerId='{playerId}', PlayerUID={playerUID}");
					SyncList<SpeechEvent> events = val.events;
					if (events == null)
					{
						continue;
					}
					float currentSessionTime = GetCurrentSessionTime();
					HashSet<long> hashSet = new HashSet<long>();
					for (int i = 0; i < events.Count; i++)
					{
						hashSet.Add(events[i].Id);
					}
					int num2 = 0;
					if (HasPending())
					{
						List<SpeechEvent> list = ClaimEventsForArchive(playerId, playerUID, isLocal, val);
						if (list != null)
						{
							foreach (SpeechEvent item in list)
							{
								if (item != null && !hashSet.Contains(item.Id))
								{
									FixEventTiming(item, currentSessionTime);
									events.Add(item);
									hashSet.Add(item.Id);
									num2++;
								}
							}
						}
					}
					if (_disconnectedCache.Count > 0)
					{
						List<SpeechEvent> list2 = ClaimDisconnectedEventsForArchive(playerId, playerUID, isLocal);
						if (list2 != null)
						{
							foreach (SpeechEvent item2 in list2)
							{
								if (item2 != null && !hashSet.Contains(item2.Id))
								{
									FixEventTiming(item2, currentSessionTime);
									events.Add(item2);
									hashSet.Add(item2.Id);
									num2++;
								}
							}
						}
					}
					if (num2 > 0)
					{
						(int, int, int) counts = GetCounts();
						MelonLogger.Msg($"[SpeechEventPoolManager] Deferred injection: {num2} events into " + $"PlayerId='{playerId}' [pool: {counts.Item1}P/{counts.Item2}I/{counts.Item3}F, " + $"disconnected cache: {_disconnectedCache.Count}]");
					}
				}
			}
			if (_deferredNameUpdates.Count == 0)
			{
				return;
			}
			for (int num3 = _deferredNameUpdates.Count - 1; num3 >= 0; num3--)
			{
				var (val2, list3) = _deferredNameUpdates[num3];
				if ((Object)(object)val2 == (Object)null)
				{
					_deferredNameUpdates.RemoveAt(num3);
					continue;
				}
				string playerId2;
				try
				{
					playerId2 = val2.PlayerId;
				}
				catch
				{
					continue;
				}
				if (string.IsNullOrEmpty(playerId2))
				{
					continue;
				}
				foreach (SpeechEvent item3 in list3)
				{
					if (item3 != null)
					{
						item3.PlayerName = playerId2;
					}
				}
				MelonLogger.Msg($"[SpeechEventPoolManager] Deferred update: {list3.Count} events " + "PlayerName updated to '" + playerId2 + "'");
				_deferredNameUpdates.RemoveAt(num3);
			}
		}

		public static SpeechEventArchive GetLocalArchive()
		{
			return _localArchive;
		}

		public static void SetLocalArchive(SpeechEventArchive archive)
		{
			_localArchive = archive;
		}

		public static (int pending, int injected, int fallback) GetCounts()
		{
			int num = 0;
			int num2 = 0;
			int num3 = 0;
			using (Dictionary<long, (SpeechEvent, EventState, string)>.ValueCollection.Enumerator enumerator = _pool.Values.GetEnumerator())
			{
				while (enumerator.MoveNext())
				{
					switch (enumerator.Current.Item2)
					{
					case EventState.Pending:
						num++;
						break;
					case EventState.Injected:
						num2++;
						break;
					case EventState.Fallback:
						num3++;
						break;
					}
				}
			}
			return (num, num2, num3);
		}

		public static List<SpeechEvent> GetPendingEvents()
		{
			List<SpeechEvent> list = new List<SpeechEvent>();
			foreach (var value in _pool.Values)
			{
				if (value.state == EventState.Pending)
				{
					list.Add(value.ev);
				}
			}
			return list;
		}

		public static bool HasPending()
		{
			foreach (var value in _pool.Values)
			{
				if (value.state == EventState.Pending)
				{
					return true;
				}
			}
			return false;
		}

		public static void FixEventTiming(SpeechEvent ev, float currentTime)
		{
			if (RecordedTimeField != null)
			{
				RecordedTimeField.SetValue(ev, currentTime);
			}
			if (LastPlayedTimeField != null)
			{
				LastPlayedTimeField.SetValue(ev, currentTime);
			}
		}

		public static float GetCurrentSessionTime()
		{
			try
			{
				object hubMember = MimesisSaveManager.GetHubMember("timeutil");
				if (hubMember != null)
				{
					MethodInfo method = hubMember.GetType().GetMethod("GetCurrentTickSec", BindingFlags.Instance | BindingFlags.Public);
					if (method != null)
					{
						return (int)method.Invoke(hubMember, null);
					}
				}
			}
			catch
			{
			}
			return 0f;
		}

		public static void CacheEventsFromArchive(SpeechEventArchive archive)
		{
			if ((Object)(object)archive == (Object)null)
			{
				return;
			}
			try
			{
				string text = null;
				long num = 0L;
				bool flag = false;
				try
				{
					text = archive.PlayerId;
					num = archive.PlayerUID;
					flag = archive.IsLocal;
				}
				catch
				{
				}
				if (flag)
				{
					return;
				}
				FieldInfo field = typeof(SpeechEventArchive).GetField("events", BindingFlags.Instance | BindingFlags.Public);
				if (field == null)
				{
					return;
				}
				object value = field.GetValue(archive);
				if (value == null)
				{
					return;
				}
				PropertyInfo property = value.GetType().GetProperty("Count", BindingFlags.Instance | BindingFlags.Public);
				PropertyInfo property2 = value.GetType().GetProperty("Item", new Type[1] { typeof(int) });
				if (property == null || property2 == null)
				{
					return;
				}
				int num2 = (int)property.GetValue(value);
				int num3 = 0;
				for (int i = 0; i < num2; i++)
				{
					object? value2 = property2.GetValue(value, new object[1] { i });
					SpeechEvent val = (SpeechEvent)((value2 is SpeechEvent) ? value2 : null);
					if (val != null && !_disconnectedCache.ContainsKey(val.Id))
					{
						_disconnectedCache[val.Id] = val;
						num3++;
					}
				}
				ulong steamIdForPlayerUID = GetSteamIdForPlayerUID(num);
				if (steamIdForPlayerUID != 0L && !string.IsNullOrEmpty(text))
				{
					_disconnectedPlayerMappings[steamIdForPlayerUID] = text;
					MelonLogger.Msg($"[SpeechEventPoolManager] Cached player mapping: SteamID {steamIdForPlayerUID} -> '{text}'");
				}
				MelonLogger.Msg($"[SpeechEventPoolManager] Cached {num3} events from disconnected player " + $"'{text}' (UID={num}). Total cached: {_disconnectedCache.Count}");
			}
			catch (Exception ex)
			{
				MelonLogger.Warning("[SpeechEventPoolManager] CacheEventsFromArchive error: " + ex.Message);
			}
		}

		public static List<SpeechEvent> ClaimDisconnectedEventsForArchive(string playerId, long playerUID, bool isLocal)
		{
			List<SpeechEvent> list = new List<SpeechEvent>();
			if (_disconnectedCache.Count == 0)
			{
				return list;
			}
			HashSet<string> hashSet = new HashSet<string>();
			if (!string.IsNullOrEmpty(playerId))
			{
				hashSet.Add(playerId);
			}
			ulong steamIdForPlayerUID = GetSteamIdForPlayerUID(playerUID, isLocal);
			if (steamIdForPlayerUID != 0L && _disconnectedPlayerMappings.TryGetValue(steamIdForPlayerUID, out var value))
			{
				hashSet.Add(value);
				MelonLogger.Msg($"[SpeechEventPoolManager] Disconnected cache match: SteamID {steamIdForPlayerUID} -> old DissonanceID '{value}'");
			}
			if (hashSet.Count == 0)
			{
				return list;
			}
			List<long> list2 = new List<long>();
			foreach (KeyValuePair<long, SpeechEvent> item2 in _disconnectedCache)
			{
				string item = item2.Value.PlayerName ?? "";
				if (hashSet.Contains(item))
				{
					if (!string.IsNullOrEmpty(playerId))
					{
						item2.Value.PlayerName = playerId;
					}
					list.Add(item2.Value);
					list2.Add(item2.Key);
				}
			}
			foreach (long item3 in list2)
			{
				_disconnectedCache.Remove(item3);
			}
			if (steamIdForPlayerUID != 0L && list.Count > 0)
			{
				_disconnectedPlayerMappings.Remove(steamIdForPlayerUID);
			}
			if (list.Count > 0)
			{
				MelonLogger.Msg($"[SpeechEventPoolManager] Reclaimed {list.Count} events from disconnected cache " + $"for PlayerId='{playerId}' (remaining cache: {_disconnectedCache.Count})");
			}
			return list;
		}

		public static List<SpeechEvent> GetDisconnectedEvents()
		{
			return new List<SpeechEvent>(_disconnectedCache.Values);
		}

		public static Dictionary<ulong, string> GetDisconnectedPlayerMappings()
		{
			return new Dictionary<ulong, string>(_disconnectedPlayerMappings);
		}

		public static void Reset()
		{
			_pool.Clear();
			_byPlayerName.Clear();
			_steamToDissonance.Clear();
			_deferredNameUpdates.Clear();
			_deferredInjectionArchives.Clear();
			_disconnectedCache.Clear();
			_disconnectedPlayerMappings.Clear();
			_loadedSlotId = -1;
			_localArchive = null;
		}

		public static void SavePlayerMapping(int slotId)
		{
			string mimesisSlotPath = MimesisSaveManager.GetMimesisSlotPath(slotId);
			if (string.IsNullOrEmpty(mimesisSlotPath))
			{
				MelonLogger.Warning("[SpeechEventPoolManager] SavePlayerMapping: slot path is null/empty!");
				return;
			}
			try
			{
				SpeechEventArchive[] array = Object.FindObjectsOfType<SpeechEventArchive>();
				if (array == null || array.Length == 0)
				{
					MelonLogger.Warning("[SpeechEventPoolManager] SavePlayerMapping: no SpeechEventArchive found!");
					return;
				}
				MelonLogger.Msg($"[SpeechEventPoolManager] SavePlayerMapping: found {array.Length} archives");
				Dictionary<ulong, string> dictionary = new Dictionary<ulong, string>();
				SpeechEventArchive[] array2 = array;
				foreach (SpeechEventArchive val in array2)
				{
					try
					{
						string playerId = val.PlayerId;
						long playerUID = val.PlayerUID;
						bool isLocal = val.IsLocal;
						if (string.IsNullOrEmpty(playerId))
						{
							MelonLogger.Msg($"[SpeechEventPoolManager]   Archive skipped: empty PlayerId (UID={playerUID})");
							continue;
						}
						ulong steamIdForPlayerUID = GetSteamIdForPlayerUID(playerUID, isLocal);
						MelonLogger.Msg($"[SpeechEventPoolManager]   Archive: PlayerId='{playerId}', PlayerUID={playerUID}, IsLocal={isLocal}, SteamID={steamIdForPlayerUID}");
						if (steamIdForPlayerUID != 0L)
						{
							dictionary[steamIdForPlayerUID] = playerId;
						}
						else
						{
							MelonLogger.Warning($"[SpeechEventPoolManager]   Could not resolve SteamID for PlayerUID={playerUID}");
						}
					}
					catch (Exception ex)
					{
						MelonLogger.Warning("[SpeechEventPoolManager]   Archive error: " + ex.Message);
					}
				}
				Dictionary<ulong, string> disconnectedPlayerMappings = GetDisconnectedPlayerMappings();
				int num = 0;
				foreach (KeyValuePair<ulong, string> item in disconnectedPlayerMappings)
				{
					if (!dictionary.ContainsKey(item.Key))
					{
						dictionary[item.Key] = item.Value;
						num++;
					}
				}
				if (num > 0)
				{
					MelonLogger.Msg($"[SpeechEventPoolManager] Added {num} disconnected player mappings");
				}
				List<string> list = new List<string>();
				foreach (KeyValuePair<ulong, string> item2 in dictionary)
				{
					string arg = EscapeJsonString(item2.Value);
					list.Add($"\"{item2.Key}\":\"{arg}\"");
				}
				string json = "{" + string.Join(",", list) + "}";
				Directory.CreateDirectory(mimesisSlotPath);
				string text = Path.Combine(mimesisSlotPath, "player_mapping.json");
				MimesisSaveManager.SafeWritePlayerMapping(text, json);
				MelonLogger.Msg($"[SpeechEventPoolManager] Saved player mapping: {dictionary.Count} entries ({num} from cache) -> {text}");
			}
			catch (Exception arg2)
			{
				MelonLogger.Error($"[SpeechEventPoolManager] SavePlayerMapping FAILED: {arg2}");
			}
		}

		private static void LoadPlayerMapping(int slotId)
		{
			_steamToDissonance.Clear();
			string mimesisSlotPath = MimesisSaveManager.GetMimesisSlotPath(slotId);
			if (string.IsNullOrEmpty(mimesisSlotPath))
			{
				return;
			}
			string text = Path.Combine(mimesisSlotPath, "player_mapping.json");
			string text2 = MimesisSaveManager.SafeReadPlayerMapping(text);
			if (string.IsNullOrEmpty(text2))
			{
				MelonLogger.Msg("[SpeechEventPoolManager] No player_mapping.json at " + text);
				return;
			}
			try
			{
				foreach (KeyValuePair<ulong, string> item in ParseSteamToDissonanceJson(text2))
				{
					_steamToDissonance[item.Key] = item.Value;
				}
				MelonLogger.Msg($"[SpeechEventPoolManager] Loaded player mapping: {_steamToDissonance.Count} entries");
				foreach (KeyValuePair<ulong, string> item2 in _steamToDissonance)
				{
					MelonLogger.Msg($"[SpeechEventPoolManager]   SteamID {item2.Key} -> DissonanceID '{item2.Value}'");
				}
			}
			catch (Exception ex)
			{
				MelonLogger.Warning("[SpeechEventPoolManager] LoadPlayerMapping: " + ex.Message);
			}
		}

		private static Dictionary<ulong, string> ParseSteamToDissonanceJson(string json)
		{
			Dictionary<ulong, string> dictionary = new Dictionary<ulong, string>();
			if (string.IsNullOrEmpty(json))
			{
				return dictionary;
			}
			json = json.Trim();
			if (json.StartsWith("{"))
			{
				json = json.Substring(1);
			}
			if (json.EndsWith("}"))
			{
				json = json.Substring(0, json.Length - 1);
			}
			json = json.Trim();
			if (string.IsNullOrEmpty(json))
			{
				return dictionary;
			}
			int num = 0;
			while (num < json.Length)
			{
				int num2 = json.IndexOf('"', num);
				if (num2 < 0)
				{
					break;
				}
				int num3 = json.IndexOf('"', num2 + 1);
				if (num3 < 0)
				{
					break;
				}
				string s = json.Substring(num2 + 1, num3 - num2 - 1);
				int num4 = json.IndexOf(':', num3 + 1);
				if (num4 < 0)
				{
					break;
				}
				int num5 = json.IndexOf('"', num4 + 1);
				if (num5 < 0)
				{
					break;
				}
				int num6 = json.IndexOf('"', num5 + 1);
				if (num6 < 0)
				{
					break;
				}
				string value = json.Substring(num5 + 1, num6 - num5 - 1);
				if (ulong.TryParse(s, out var result))
				{
					dictionary[result] = value;
				}
				num = num6 + 1;
				int num7 = json.IndexOf(',', num);
				num = ((num7 >= 0) ? (num7 + 1) : json.Length);
			}
			return dictionary;
		}

		private static string EscapeJsonString(string s)
		{
			if (s == null)
			{
				return "";
			}
			return s.Replace("\\", "\\\\").Replace("\"", "\\\"");
		}
	}
}
namespace MimesisPersistence.Patches
{
	[HarmonyPatch(typeof(MaintenanceRoom), "SaveGameData")]
	public static class MaintenanceRoomPatches
	{
		[HarmonyPostfix]
		public static void Postfix(int saveSlotID, List<string> playerNames, bool isAutoSave, MsgErrorCode __result)
		{
			//IL_0000: Unknown result type (might be due to invalid IL or missing references)
			if ((int)__result == 0 && MimesisSaveManager.IsHost())
			{
				MimesisSaveManager.SaveMimesisData((!isAutoSave) ? saveSlotID : 0);
			}
		}
	}
	[HarmonyPatch(typeof(PlatformMgr), "Delete")]
	public static class PlatformMgrPatches
	{
		[HarmonyPostfix]
		public static void Postfix(string fileName)
		{
			try
			{
				if (!string.IsNullOrEmpty(fileName) && fileName.StartsWith("MMGameData", StringComparison.OrdinalIgnoreCase) && int.TryParse(Path.GetFileNameWithoutExtension(fileName).Replace("MMGameData", ""), out var result) && MMSaveGameData.CheckSaveSlotID(result, true))
				{
					MimesisSaveManager.DeleteMimesisData(result);
				}
			}
			catch (Exception ex)
			{
				MelonLogger.Warning("[MimesisPersistence] PlatformMgr.Delete: " + ex.Message);
			}
		}
	}
	[HarmonyPatch(typeof(SpeechEventArchive), "OnStartClient")]
	public static class SpeechEventArchivePatches
	{
		private static int _poolLoadedForSlot = -999;

		[HarmonyPostfix]
		public static void Postfix(SpeechEventArchive __instance)
		{
			try
			{
				if (!MimesisSaveManager.IsHost())
				{
					return;
				}
				int currentSaveSlotId = MimesisSaveManager.GetCurrentSaveSlotId();
				if (!MMSaveGameData.CheckSaveSlotID(currentSaveSlotId, true))
				{
					return;
				}
				if (currentSaveSlotId != _poolLoadedForSlot)
				{
					_poolLoadedForSlot = currentSaveSlotId;
					SpeechEventPoolManager.Reset();
					if (MimesisSaveManager.HasMimesisData(currentSaveSlotId))
					{
						SpeechEventPoolManager.LoadForSlot(currentSaveSlotId);
					}
				}
				if (__instance.IsLocal)
				{
					SpeechEventPoolManager.SetLocalArchive(__instance);
				}
				string text = null;
				long num = 0L;
				bool flag = false;
				try
				{
					text = __instance.PlayerId;
					num = __instance.PlayerUID;
					flag = __instance.IsLocal;
				}
				catch
				{
				}
				if (!flag && string.IsNullOrEmpty(text) && num == 0L)
				{
					if (SpeechEventPoolManager.HasPending() || SpeechEventPoolManager.DisconnectedCacheCount > 0)
					{
						SpeechEventPoolManager.RegisterDeferredInjection(__instance);
					}
					return;
				}
				SyncList<SpeechEvent> events = __instance.events;
				if (events == null)
				{
					return;
				}
				float currentSessionTime = SpeechEventPoolManager.GetCurrentSessionTime();
				HashSet<long> hashSet = new HashSet<long>();
				for (int i = 0; i < events.Count; i++)
				{
					hashSet.Add(events[i].Id);
				}
				int num2 = 0;
				if (SpeechEventPoolManager.HasPending())
				{
					List<SpeechEvent> list = SpeechEventPoolManager.ClaimEventsForArchive(text, num, flag, __instance);
					if (list != null && list.Count > 0)
					{
						foreach (SpeechEvent item in list)
						{
							if (item != null && !hashSet.Contains(item.Id))
							{
								SpeechEventPoolManager.FixEventTiming(item, currentSessionTime);
								events.Add(item);
								hashSet.Add(item.Id);
								num2++;
							}
						}
					}
				}
				if (SpeechEventPoolManager.DisconnectedCacheCount > 0)
				{
					List<SpeechEvent> list2 = SpeechEventPoolManager.ClaimDisconnectedEventsForArchive(text, num, flag);
					if (list2 != null && list2.Count > 0)
					{
						foreach (SpeechEvent item2 in list2)
						{
							if (item2 != null && !hashSet.Contains(item2.Id))
							{
								SpeechEventPoolManager.FixEventTiming(item2, currentSessionTime);
								events.Add(item2);
								hashSet.Add(item2.Id);
								num2++;
							}
						}
						MelonLogger.Msg($"[MimesisPersistence] Restored {list2.Count} events from disconnected cache " + "into archive PlayerId=" + text);
					}
				}
				if (num2 > 0)
				{
					(int, int, int) counts = SpeechEventPoolManager.GetCounts();
					MelonLogger.Msg($"[MimesisPersistence] Injected {num2} total events into archive " + $"PlayerId={text} (time={currentSessionTime:F1}) " + $"[pool: {counts.Item1}P/{counts.Item2}I/{counts.Item3}F, " + $"disconnected cache: {SpeechEventPoolManager.DisconnectedCacheCount}]");
				}
			}
			catch (Exception ex)
			{
				MelonLogger.Warning("[MimesisPersistence] SpeechEventArchive inject: " + ex.Message);
			}
		}

		public static int InjectEventsIntoArchive(SpeechEventArchive archive, List<SpeechEvent> events)
		{
			if ((Object)(object)archive == (Object)null || events == null || events.Count == 0)
			{
				return 0;
			}
			SyncList<SpeechEvent> events2 = archive.events;
			if (events2 == null)
			{
				return 0;
			}
			float currentSessionTime = SpeechEventPoolManager.GetCurrentSessionTime();
			HashSet<long> hashSet = new HashSet<long>();
			for (int i = 0; i < events2.Count; i++)
			{
				hashSet.Add(events2[i].Id);
			}
			int num = 0;
			foreach (SpeechEvent @event in events)
			{
				if (@event != null && !hashSet.Contains(@event.Id))
				{
					SpeechEventPoolManager.FixEventTiming(@event, currentSessionTime);
					events2.Add(@event);
					hashSet.Add(@event.Id);
					num++;
				}
			}
			return num;
		}

		public static void ResetInjectedSlot()
		{
			_poolLoadedForSlot = -999;
			SpeechEventPoolManager.Reset();
		}
	}
	[HarmonyPatch(typeof(VoiceManager), "GetRandomOtherSpeechEventArchive")]
	public static class VoiceManagerHallucinationPatch
	{
		[HarmonyPostfix]
		public static void Postfix(ref SpeechEventArchive __result)
		{
			try
			{
				if (!((Object)(object)__result != (Object)null))
				{
					SpeechEventArchive localArchive = SpeechEventPoolManager.GetLocalArchive();
					if (!((Object)(object)localArchive == (Object)null) && localArchive.RandomPoolSize > 0)
					{
						__result = localArchive;
					}
				}
			}
			catch (Exception ex)
			{
				MelonLogger.Warning("[MimesisPersistence] Hallucination fallback: " + ex.Message);
			}
		}
	}
}