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.4.0", "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("0.4.0.0")] [assembly: TargetFramework(".NETFramework,Version=v4.7.2", FrameworkDisplayName = ".NET Framework 4.7.2")] [assembly: AssemblyVersion("0.4.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.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 CollectAllSpeechEvents() { List list = new List(); try { HashSet hashSet = new HashSet(); SpeechEventArchive[] array = Object.FindObjectsOfType(); 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 disconnectedEvents = SpeechEventPoolManager.GetDisconnectedEvents(); int num2 = 0; foreach (SpeechEvent item in disconnectedEvents) { if (item != null && hashSet.Add(item.Id)) { list.Add(item); num2++; } } List 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 GetPlayDataFromReplayData(object replayData) { if (replayData == null) { return null; } try { return replayData.GetType().GetField("_playData", BindingFlags.Instance | BindingFlags.NonPublic)?.GetValue(replayData) as List; } catch { return null; } } private static List GetVoiceDataFromReplayData(object replayData) { if (replayData == null) { return null; } try { return replayData.GetType().GetField("_voiceData", BindingFlags.Instance | BindingFlags.NonPublic)?.GetValue(replayData) as List; } 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 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(); 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 playDataFromReplayData = GetPlayDataFromReplayData(replayData); if (playDataFromReplayData != null && playDataFromReplayData.Count > 0) { string text = Path.Combine(mimesisSlotPath, "replay_play.bin"); ReplaySerializer.SerializeMsgToFile(ref playDataFromReplayData, ref text); } List 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 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 list = new List(); 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 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 list = (List)methodInfo.Invoke(null, new object[3] { typeof(List), 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((REPLAY_HEADER_VERSION)3); 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 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> 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 LoadReplayVoiceData(int slotId) { //IL_002a: Unknown result type (might be due to invalid IL or missing references) //IL_002f: Unknown result type (might be due to invalid IL or missing references) //IL_0032: Unknown result type (might be due to invalid IL or missing references) //IL_0037: Unknown result type (might be due to invalid IL or missing references) //IL_0042: Unknown result type (might be due to invalid IL or missing references) //IL_0047: 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> val = ReplaySerializer.DeserializeSndFromFileAsync(text, (REPLAY_HEADER_VERSION)3); 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 _pool = new Dictionary(); private static readonly Dictionary> _byPlayerName = new Dictionary>(); private static readonly Dictionary _steamToDissonance = new Dictionary(); private static int _loadedSlotId = -1; private static SpeechEventArchive _localArchive; private static readonly List<(SpeechEventArchive archive, List events)> _deferredNameUpdates = new List<(SpeechEventArchive, List)>(); private static readonly List _deferredInjectionArchives = new List(); private static readonly Dictionary _disconnectedCache = new Dictionary(); private static readonly Dictionary _disconnectedPlayerMappings = new Dictionary(); 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 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(); _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 ClaimEventsForArchive(string playerId, long playerUID, bool isLocal = false, SpeechEventArchive archive = null) { List list = new List(); 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 hashSet = new HashSet(); 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(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 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.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 ForceFallbackToLocal() { List list = new List(); 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 events = val.events; if (events == null) { continue; } float currentSessionTime = GetCurrentSessionTime(); HashSet hashSet = new HashSet(); for (int i = 0; i < events.Count; i++) { hashSet.Add(events[i].Id); } int num2 = 0; if (HasPending()) { List 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 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.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 (pending: num, injected: num2, fallback: num3); } public static List GetPendingEvents() { List list = new List(); 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 ClaimDisconnectedEventsForArchive(string playerId, long playerUID, bool isLocal) { List list = new List(); if (_disconnectedCache.Count == 0) { return list; } HashSet hashSet = new HashSet(); 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 list2 = new List(); foreach (KeyValuePair 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 GetDisconnectedEvents() { return new List(_disconnectedCache.Values); } public static Dictionary GetDisconnectedPlayerMappings() { return new Dictionary(_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(); if (array == null || array.Length == 0) { MelonLogger.Warning("[SpeechEventPoolManager] SavePlayerMapping: no SpeechEventArchive found!"); return; } MelonLogger.Msg($"[SpeechEventPoolManager] SavePlayerMapping: found {array.Length} archives"); Dictionary dictionary = new Dictionary(); 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 disconnectedPlayerMappings = GetDisconnectedPlayerMappings(); int num = 0; foreach (KeyValuePair 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 list = new List(); foreach (KeyValuePair 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 item in ParseSteamToDissonanceJson(text2)) { _steamToDissonance[item.Key] = item.Value; } MelonLogger.Msg($"[SpeechEventPoolManager] Loaded player mapping: {_steamToDissonance.Count} entries"); foreach (KeyValuePair item2 in _steamToDissonance) { MelonLogger.Msg($"[SpeechEventPoolManager] SteamID {item2.Key} -> DissonanceID '{item2.Value}'"); } } catch (Exception ex) { MelonLogger.Warning("[SpeechEventPoolManager] LoadPlayerMapping: " + ex.Message); } } private static Dictionary ParseSteamToDissonanceJson(string json) { Dictionary dictionary = new Dictionary(); 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 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 events = __instance.events; if (events == null) { return; } float currentSessionTime = SpeechEventPoolManager.GetCurrentSessionTime(); HashSet hashSet = new HashSet(); for (int i = 0; i < events.Count; i++) { hashSet.Add(events[i].Id); } int num2 = 0; if (SpeechEventPoolManager.HasPending()) { List 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 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 events) { if ((Object)(object)archive == (Object)null || events == null || events.Count == 0) { return 0; } SyncList events2 = archive.events; if (events2 == null) { return 0; } float currentSessionTime = SpeechEventPoolManager.GetCurrentSessionTime(); HashSet hashSet = new HashSet(); 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.WarmedUpCount > 0) { __result = localArchive; } } } catch (Exception ex) { MelonLogger.Warning("[MimesisPersistence] Hallucination fallback: " + ex.Message); } } } }