using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using System.Threading; using BepInEx; using BepInEx.Configuration; using HarmonyLib; using UnityEngine; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: AssemblyTitle("SimPassiveLevelingOverhaul")] [assembly: AssemblyDescription("Passive XP over time for Erenshor SimPlayers")] [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("")] [assembly: AssemblyProduct("SimPassiveLevelingOverhaul")] [assembly: AssemblyCopyright("")] [assembly: AssemblyTrademark("")] [assembly: AssemblyFileVersion("1.4.2.0")] [assembly: TargetFramework(".NETFramework,Version=v4.8", FrameworkDisplayName = ".NET Framework 4.8")] [assembly: AssemblyVersion("1.4.2.0")] namespace Erenshor.SimXPOverTime; [BepInPlugin("erenshor.simxpovertime", "Sim XP Over Time", "1.4.2")] public class SimXPOverTimePlugin : BaseUnityPlugin { public static bool _cacheBuilt; public static readonly Dictionary _preLoadSnapshot = new Dictionary(); public static readonly Dictionary _saveDataCache = new Dictionary(); private static readonly HashSet _dirtyXp = new HashSet(); private static readonly Queue _dirtyXpOrder = new Queue(); private static readonly HashSet _dirtyLevel = new HashSet(); private static readonly Queue _dirtyLevelOrder = new Queue(); public static readonly HashSet _loadFailedSims = new HashSet(); private const float WriteIntervalSeconds = 0.25f; private ConfigEntry _cfgTickInterval; private ConfigEntry _cfgVarianceMin; private ConfigEntry _cfgVarianceMax; private ConfigEntry _cfgUseFixedSeed; private ConfigEntry _cfgFixedSeedValue; private ConfigEntry _cfgSeedIncrementOnZone; private ConfigEntry _cfgRivalSpeedMult; private ConfigEntry _cfgSlotCatchupPerLevel; private ConfigEntry _cfgSlotCatchupMax; private ConfigEntry _cfgSlotLevelCapOffset; private ConfigEntry _cfgMaxLevel; private ConfigEntry _cfgDebugLogging; public static float _tickIntervalSecs; public static float _varianceMin; public static float _varianceMax; public static bool _useFixedSeed; public static int _fixedSeedValue; public static bool _seedIncrementOnZone; public static float _rivalSpeedMultiplier; public static float _slotCatchupPerLevel; public static float _slotCatchupMax; public static int _slotLevelCapOffset; public static int _maxLevel; public static bool _debugLog; public static int _zoneSeed; public const int FNV_BASIS = -2128831035; public const int FNV_PRIME = 16777619; public static readonly float[] _hoursForLevel = new float[36]; private static readonly float[] DefaultHoursForLevel = new float[36] { 0f, 0f, 0.2f, 0.3f, 0.3f, 0.3f, 0.3f, 0.5f, 0.7f, 0.9f, 1f, 1.1f, 1.2f, 1.3f, 1.4f, 1.6f, 1.8f, 2f, 2.2f, 2.5f, 3f, 3.5f, 4f, 4.4f, 4.8f, 5.1f, 5.4f, 5.7f, 6f, 6.3f, 6.5f, 6.7f, 6.9f, 7.1f, 7.3f, 7.5f }; private static SimPlayerMngr _coroutineHost; public static void MarkDirty(string simName, bool levelChanged) { if (string.IsNullOrEmpty(simName) || (_loadFailedSims.Count > 0 && _loadFailedSims.Contains(simName))) { return; } if (levelChanged) { if (_dirtyLevel.Add(simName)) { _dirtyLevelOrder.Enqueue(simName); } } else if (_dirtyXp.Add(simName)) { _dirtyXpOrder.Enqueue(simName); } } public static void ClearDirty(string simName) { if (!string.IsNullOrEmpty(simName)) { _dirtyXp.Remove(simName); _dirtyLevel.Remove(simName); } } private static string DequeueDirty() { while (_dirtyLevelOrder.Count > 0) { string text = _dirtyLevelOrder.Dequeue(); if (_dirtyLevel.Remove(text)) { _dirtyXp.Remove(text); return text; } } while (_dirtyXpOrder.Count > 0) { string text2 = _dirtyXpOrder.Dequeue(); if (_dirtyXp.Remove(text2)) { return text2; } } return null; } public static void ResetSessionState() { _cacheBuilt = false; _preLoadSnapshot.Clear(); _saveDataCache.Clear(); _dirtyXp.Clear(); _dirtyXpOrder.Clear(); _dirtyLevel.Clear(); _dirtyLevelOrder.Clear(); _loadFailedSims.Clear(); } public static bool WriteSimToDisk(string simName) { if (_loadFailedSims.Count > 0 && _loadFailedSims.Contains(simName)) { return false; } SimPlayerMngr simMngr = GameData.SimMngr; if ((Object)(object)simMngr == (Object)null) { return false; } if (!simMngr.SimDict.TryGetValue(simName, out var value) || value == null) { return false; } if ((Object)(object)value.MyAvatar != (Object)null && (Object)(object)value.MyStats != (Object)null && simMngr.ActiveSimInstances.Contains(value.MyAvatar)) { return false; } if (!_saveDataCache.TryGetValue(simName, out var value2) || value2 == null) { string path = Path.Combine(Application.persistentDataPath, "ESSaveData", "Sims" + simName); if (!File.Exists(path)) { return false; } try { value2 = JsonUtility.FromJson(File.ReadAllText(path)); } catch (Exception ex) { LogWarning("[SimXP] Re-read failed for " + simName + ": " + ex.Message); return false; } if (value2 == null) { return false; } _saveDataCache[simName] = value2; } value2.MyLevel = value.Level; value2.XpForLevelUp = value.CurXp; return SaveSimDataBuffered(value2); } public static bool SaveSimDataBuffered(SimPlayerSaveData data) { if (data == null || string.IsNullOrEmpty(data.NPCName)) { return false; } string text = "Sims" + data.NPCName; string text2 = Path.Combine(Application.persistentDataPath, "ESSaveData"); string text3 = Path.Combine(text2, text); string text4 = text3 + ".tmp"; string text5 = Path.Combine(Application.persistentDataPath, "backups"); string destinationBackupFileName = Path.Combine(text5, text + ".bak"); string text6 = JsonUtility.ToJson((object)data, true); if (string.IsNullOrEmpty(text6)) { return false; } try { if (!Directory.Exists(text2)) { Directory.CreateDirectory(text2); } if (!Directory.Exists(text5)) { Directory.CreateDirectory(text5); } File.WriteAllText(text4, text6); if (File.Exists(text3)) { File.Replace(text4, text3, destinationBackupFileName); } else { File.Move(text4, text3); } return true; } catch (Exception ex) { try { if (File.Exists(text4)) { File.Delete(text4); } } catch { } LogWarning("[SimXP] Buffered save failed for " + data.NPCName + ": " + ex.Message); return false; } } private static IEnumerator WriterCoroutine() { WaitForSeconds wait = new WaitForSeconds(0.25f); while (true) { yield return wait; if (_cacheBuilt) { string text = DequeueDirty(); if (text != null && WriteSimToDisk(text)) { LogDebug("[SimXP] Writer: persisted " + text + "."); } } } } public static void FlushAllDirty(string reason) { int num = 0; int num2 = 0; string simName; while ((simName = DequeueDirty()) != null) { if (WriteSimToDisk(simName)) { num++; } else { num2++; } } if (num > 0 || num2 > 0) { LogDebug("[SimXP] Flush (" + reason + "): wrote " + num + ", skipped " + num2 + "."); } } private static IEnumerator RebuildCacheCoroutine(SimPlayerMngr mgr) { while (!mgr.LoadedSimplayers) { yield return null; } float startTime = Time.realtimeSinceStartup; int restored = 0; int reverted = 0; int processed = 0; List rivalRoster = new List(); for (int i = 0; i < mgr.Sims.Count; i++) { SimPlayerTracking val = mgr.Sims[i]; processed++; if (val == null || string.IsNullOrEmpty(val.SimName)) { continue; } if (_debugLog && (val.Rival || val.TiedToSlot == 99)) { rivalRoster.Add(val.SimName + "(flag=" + (val.Rival ? "Y" : "N") + ", slot=" + val.TiedToSlot + ")"); } if (!_preLoadSnapshot.TryGetValue(val.SimName, out (int, int) value)) { continue; } if (val.Level > value.Item1) { LogDebug("[SimXP] Reverting login bump: " + val.SimName + " L" + val.Level + " -> L" + value.Item1); (val.Level, val.CurXp) = value; if ((Object)(object)val.MyAvatar != (Object)null && (Object)(object)val.MyStats != (Object)null) { val.MyStats.Level = value.Item1; val.MyStats.CurrentExperience = value.Item2; val.MyStats.SetXpForLevelUp(); val.MyStats.CalcStats(); } MarkDirty(val.SimName, levelChanged: true); reverted++; } else if (value.Item2 > 0 && val.Level < _maxLevel) { val.CurXp = value.Item2; restored++; } if (processed % 50 == 0) { yield return null; } } _cacheBuilt = true; if (reverted > 0) { FlushAllDirty("login restore"); } float num = (Time.realtimeSinceStartup - startTime) * 1000f; LogDebug("[SimXP] Login restore: " + processed + " sims (" + _preLoadSnapshot.Count + " snapshot entries) in " + num.ToString("F1") + "ms. " + restored + " XP restored, " + reverted + " login bumps reverted."); if (_debugLog && rivalRoster.Count > 0) { Debug.Log((object)("[SimXP] Rival roster (" + rivalRoster.Count + "): " + string.Join(", ", rivalRoster.ToArray()))); } } public static void LogDebug(string msg) { if (_debugLog) { Debug.Log((object)msg); } } public static void LogWarning(string msg) { Debug.LogWarning((object)msg); } private void Awake() { //IL_002f: Unknown result type (might be due to invalid IL or missing references) //IL_0039: Expected O, but got Unknown //IL_0063: Unknown result type (might be due to invalid IL or missing references) //IL_006d: Expected O, but got Unknown _cfgTickInterval = ((BaseUnityPlugin)this).Config.Bind("General", "TickIntervalSeconds", 120f, new ConfigDescription("How often (in seconds) passive XP is granted to sims. Lower = more frequent but smaller XP chunks. Does NOT change overall leveling speed (each level takes at least one tick, so very large intervals slow the fastest levels). Minimum 90: this keeps the background writer ahead of the tick cycle and avoids per-tick rounding inflating early-level speed. Values outside the range are clamped.", (AcceptableValueBase)(object)new AcceptableValueRange(90f, 3600f), Array.Empty())); _cfgMaxLevel = ((BaseUnityPlugin)this).Config.Bind("General", "MaxLevel", 35, new ConfigDescription("Level cap. Sims at this level stop receiving passive XP. 35 is the game's own maximum; past it the base game's ascension system takes over, which this mod leaves alone. Values outside 1-35 are clamped.", (AcceptableValueBase)(object)new AcceptableValueRange(1, 35), Array.Empty())); _cfgDebugLogging = ((BaseUnityPlugin)this).Config.Bind("General", "DebugLogging", false, "If true, logs every XP tick for every sim (spammy!). Use only for troubleshooting."); _cfgVarianceMin = ((BaseUnityPlugin)this).Config.Bind("Variance", "VarianceMin", 0.5f, "Slowest multiplier for per-sim XP variance. 0.5 = half speed."); _cfgVarianceMax = ((BaseUnityPlugin)this).Config.Bind("Variance", "VarianceMax", 1.5f, "Fastest multiplier for per-sim XP variance. 1.5 = 50% faster."); _cfgUseFixedSeed = ((BaseUnityPlugin)this).Config.Bind("Variance", "UseFixedSeed", false, "If true, uses a fixed seed for per-sim variance. If enabling this, set SeedIncrementOnZoneChange to false as well so sims level at the same pace every session."); _cfgFixedSeedValue = ((BaseUnityPlugin)this).Config.Bind("Variance", "FixedSeedValue", 12345, "The seed value used when UseFixedSeed is true."); _cfgSeedIncrementOnZone = ((BaseUnityPlugin)this).Config.Bind("Variance", "SeedIncrementOnZoneChange", true, "If true, re-rolls per-sim variance at every save point (zone changes, teleports, respawns, altar saves). Set to false for stable variance throughout a session."); _cfgRivalSpeedMult = ((BaseUnityPlugin)this).Config.Bind("Rivals", "RivalSpeedMultiplier", 1.25f, "How much faster rivals level compared to generic sims. 1.0 = same speed, 1.5 = 50% faster."); _cfgSlotCatchupPerLevel = ((BaseUnityPlugin)this).Config.Bind("SlotTied", "SlotCatchupPerLevel", 0.1f, "Catchup boost per level behind the player (for current-slot sims). 0.10 = 10% boost per level. Set to 0 to disable."); _cfgSlotCatchupMax = ((BaseUnityPlugin)this).Config.Bind("SlotTied", "SlotCatchupMax", 3f, "Maximum total catchup multiplier for slot-tied sims."); _cfgSlotLevelCapOffset = ((BaseUnityPlugin)this).Config.Bind("SlotTied", "SlotLevelCapOffset", 2, "How many levels above the player a slot-tied sim can passively reach. Set to 35 to effectively disable this cap."); bool flag = false; for (int i = 2; i <= 35; i++) { string text = ((i == 2) ? "Hours of passive XP (at 1.0x variance) it takes to reach\neach level: HoursForLevelN covers the climb from level N-1\nto N. Applies to every HoursForLevel entry below. Values\noutside 0.01-1000 are clamped on load. A tiny value\ncompletes that climb in a single tick." : ""); ConfigEntry val = ((BaseUnityPlugin)this).Config.Bind("XPCurve", "HoursForLevel" + i, DefaultHoursForLevel[i], text); _hoursForLevel[i] = Mathf.Clamp(val.Value, 0.01f, 1000f); if (_hoursForLevel[i] != DefaultHoursForLevel[i]) { flag = true; } } if (flag) { ((BaseUnityPlugin)this).Logger.LogInfo((object)"Custom leveling curve detected in [XPCurve]."); } _tickIntervalSecs = _cfgTickInterval.Value; _varianceMin = _cfgVarianceMin.Value; _varianceMax = _cfgVarianceMax.Value; _useFixedSeed = _cfgUseFixedSeed.Value; _fixedSeedValue = _cfgFixedSeedValue.Value; _seedIncrementOnZone = _cfgSeedIncrementOnZone.Value; _rivalSpeedMultiplier = _cfgRivalSpeedMult.Value; _slotCatchupPerLevel = _cfgSlotCatchupPerLevel.Value; _slotCatchupMax = _cfgSlotCatchupMax.Value; _slotLevelCapOffset = _cfgSlotLevelCapOffset.Value; _maxLevel = _cfgMaxLevel.Value; _debugLog = _cfgDebugLogging.Value; _zoneSeed = (_useFixedSeed ? _fixedSeedValue : Random.Range(1, int.MaxValue)); Harmony.CreateAndPatchAll(typeof(SimXPOverTimePlugin), (string)null); ((BaseUnityPlugin)this).Logger.LogInfo((object)("SimXPOverTime v1.4.2 initialized. Tick: " + _tickIntervalSecs + "s | Variance: " + _varianceMin + "x–" + _varianceMax + "x | Seed: " + (_useFixedSeed ? ("fixed=" + _fixedSeedValue) : "random") + " | Zone re-roll: " + (_seedIncrementOnZone ? "ON" : "OFF") + " | Rivals: " + _rivalSpeedMultiplier + "x | Slot catchup: " + _slotCatchupPerLevel * 100f + "%/lvl, max " + _slotCatchupMax + "x | Slot cap: player+" + _slotLevelCapOffset + " | Debug: " + (_debugLog ? "ON" : "OFF"))); } [HarmonyPatch(typeof(SimPlayerMngr), "LoadSimPlayersIntoGame")] [HarmonyPrefix] public static void LoadSimPlayersIntoGame_Prefix() { ResetSessionState(); LogDebug("[SimXP] Session state reset for new login."); } [HarmonyPatch(typeof(SimPlayerDataManager), "CheckLoadData")] [HarmonyPrefix] public static bool CheckLoadData_Prefix(string _name, List _inv, int _level, float _skill, ref SimPlayerSaveData __result) { //IL_00ec: Unknown result type (might be due to invalid IL or missing references) //IL_00f1: Unknown result type (might be due to invalid IL or missing references) //IL_0107: Unknown result type (might be due to invalid IL or missing references) //IL_011b: Unknown result type (might be due to invalid IL or missing references) //IL_0122: Unknown result type (might be due to invalid IL or missing references) //IL_0129: Unknown result type (might be due to invalid IL or missing references) //IL_0132: Expected O, but got Unknown try { string text = Path.Combine(Application.persistentDataPath, "ESSaveData", "Sims" + _name); if (!File.Exists(text)) { return true; } bool ioError; string json = TryReadFile(text, out ioError); if (!ioError && TryParseRecord(json) != null) { return true; } string path = Path.Combine(Application.persistentDataPath, "backups", "Sims" + _name + ".bak"); bool ioError2; string text2 = (File.Exists(path) ? TryReadFile(path, out ioError2) : null); SimPlayerSaveData val = TryParseRecord(text2); if (!ioError) { if (val != null && TryWriteMainAtomic(text, text2)) { LogWarning("[SimXP] Repaired damaged save for " + _name + " from backup."); return true; } if (val == null) { Debug.LogError((object)("[SimXP] Save for " + _name + " is damaged and no usable backup exists; the game will reset this sim.")); return true; } } _loadFailedSims.Add(_name); SimPlayerSaveData val2 = val; if (val2 == null) { val2 = new SimPlayerSaveData(_name, _level, _inv, _skill) { Year = DateTime.Now.Year - 1, Day = DateTime.Now.Day, Hour = 1, Min = 1, FriendedBy = -1 }; Debug.LogError((object)("[SimXP] Save and backup for " + _name + " are both unreadable; serving defaults for this session only.")); } else { LogWarning("[SimXP] Save for " + _name + " is unreadable (locked?); serving its backup in memory for this session."); } SimPlayerDataManager.SimPlayerData.Add(val2); __result = val2; return false; } catch (Exception ex) { LogWarning("[SimXP] Save validation failed for " + _name + ": " + ex.Message); return true; } } private static string TryReadFile(string path, out bool ioError) { ioError = false; try { return File.ReadAllText(path); } catch { Thread.Sleep(15); try { return File.ReadAllText(path); } catch { ioError = true; return null; } } } private static SimPlayerSaveData TryParseRecord(string json) { if (string.IsNullOrWhiteSpace(json)) { return null; } try { SimPlayerSaveData val = JsonUtility.FromJson(json); if (val == null || string.IsNullOrEmpty(val.NPCName)) { return null; } return val; } catch { return null; } } private static bool TryWriteMainAtomic(string mainPath, string content) { string text = mainPath + ".fix.tmp"; try { using (FileStream fileStream = new FileStream(text, FileMode.Create, FileAccess.Write, FileShare.None)) { using StreamWriter streamWriter = new StreamWriter(fileStream); streamWriter.Write(content); streamWriter.Flush(); fileStream.Flush(flushToDisk: true); } if (File.Exists(mainPath)) { File.Replace(text, mainPath, null); } else { File.Move(text, mainPath); } return true; } catch (Exception ex) { try { if (File.Exists(text)) { File.Delete(text); } } catch { } LogWarning("[SimXP] Repair write failed for " + mainPath + ": " + ex.Message); return false; } } [HarmonyPatch(typeof(SimPlayerDataManager), "CheckLoadData")] [HarmonyPostfix] public static void CheckLoadData_Postfix(SimPlayerSaveData __result) { CaptureLoadedData(__result); } [HarmonyPatch(typeof(SimPlayerDataManager), "LoadFromFile")] [HarmonyPostfix] public static void LoadFromFile_Postfix(string fullPath, ref SimPlayerSaveData __result) { if (__result == null) { __result = TryRecoverGenericFile(fullPath); } CaptureLoadedData(__result); } private static SimPlayerSaveData TryRecoverGenericFile(string fullPath) { //IL_0174: Unknown result type (might be due to invalid IL or missing references) //IL_0179: Unknown result type (might be due to invalid IL or missing references) //IL_018f: Unknown result type (might be due to invalid IL or missing references) //IL_01a3: Unknown result type (might be due to invalid IL or missing references) //IL_01aa: Unknown result type (might be due to invalid IL or missing references) //IL_01b1: Unknown result type (might be due to invalid IL or missing references) //IL_01ba: Expected O, but got Unknown //IL_00c8: Unknown result type (might be due to invalid IL or missing references) //IL_00cd: Unknown result type (might be due to invalid IL or missing references) //IL_00e3: Unknown result type (might be due to invalid IL or missing references) //IL_00f7: Unknown result type (might be due to invalid IL or missing references) //IL_00fe: Unknown result type (might be due to invalid IL or missing references) //IL_0105: Unknown result type (might be due to invalid IL or missing references) //IL_010e: Expected O, but got Unknown try { string fileName = Path.GetFileName(fullPath); if (string.IsNullOrEmpty(fileName) || !fileName.StartsWith("Sims") || fileName.Length <= 4) { return null; } string text = fileName.Substring(4); TryReadFile(fullPath, out var ioError); string path = Path.Combine(Application.persistentDataPath, "backups", fileName + ".bak"); bool ioError2; string text2 = (File.Exists(path) ? TryReadFile(path, out ioError2) : null); SimPlayerSaveData val = TryParseRecord(text2); if (!ioError) { if (val != null && TryWriteMainAtomic(fullPath, text2)) { LogWarning("[SimXP] Repaired damaged save '" + fileName + "' from backup."); SimPlayerDataManager.SimPlayerData.Add(val); return val; } if (val == null) { SimPlayerSaveData val2 = new SimPlayerSaveData(text, 1, new List(), 15f) { Year = DateTime.Now.Year - 1, Day = DateTime.Now.Day, Hour = 1, Min = 1, FriendedBy = -1 }; if (!TryWriteMainAtomic(fullPath, JsonUtility.ToJson((object)val2, true))) { _loadFailedSims.Add(text); } Debug.LogError((object)("[SimXP] Save '" + fileName + "' is damaged with no usable backup; the sim has been reset.")); SimPlayerDataManager.SimPlayerData.Add(val2); return val2; } } _loadFailedSims.Add(text); SimPlayerSaveData val3 = val; if (val3 == null) { val3 = new SimPlayerSaveData(text, 1, new List(), 15f) { Year = DateTime.Now.Year - 1, Day = DateTime.Now.Day, Hour = 1, Min = 1, FriendedBy = -1 }; Debug.LogError((object)("[SimXP] Save and backup '" + fileName + "' are both unreadable; serving defaults for this session only.")); } else { LogWarning("[SimXP] Save '" + fileName + "' is unreadable (locked?); serving its backup in memory for this session."); } SimPlayerDataManager.SimPlayerData.Add(val3); return val3; } catch (Exception ex) { LogWarning("[SimXP] Recovery failed for '" + fullPath + "': " + ex.Message); return null; } } private static void CaptureLoadedData(SimPlayerSaveData sd) { if (sd != null && !string.IsNullOrEmpty(sd.NPCName) && (_loadFailedSims.Count <= 0 || !_loadFailedSims.Contains(sd.NPCName))) { if (!_cacheBuilt && !_preLoadSnapshot.ContainsKey(sd.NPCName)) { _preLoadSnapshot[sd.NPCName] = (sd.MyLevel, sd.XpForLevelUp); } _saveDataCache[sd.NPCName] = sd; } } [HarmonyPatch(typeof(SimPlayerMngr), "Start")] [HarmonyPostfix] public static void SimPlayerMngr_Start_Postfix(SimPlayerMngr __instance) { if (!((Object)(object)_coroutineHost == (Object)(object)__instance)) { _coroutineHost = __instance; ((MonoBehaviour)__instance).StartCoroutine(WriterCoroutine()); ((MonoBehaviour)__instance).StartCoroutine(XpTickCoroutine()); ((MonoBehaviour)__instance).StartCoroutine(RebuildCacheCoroutine(__instance)); LogDebug("[SimXP] Coroutines launched on SimPlayerMngr."); } } [HarmonyPatch(typeof(SimPlayer), "Start")] [HarmonyPostfix] public static void SimPlayer_Start_Postfix(SimPlayer __instance) { try { SimPlayerTracking mySimTracking = __instance.MySimTracking; if (mySimTracking == null || (Object)(object)__instance.MyStats == (Object)null) { return; } if (_loadFailedSims.Count > 0 && _loadFailedSims.Contains(mySimTracking.SimName)) { bool ioError; SimPlayerSaveData val = TryParseRecord(TryReadFile(Path.Combine(Application.persistentDataPath, "ESSaveData", "Sims" + mySimTracking.SimName), out ioError)); if (val != null && val.NPCName != mySimTracking.SimName) { Debug.LogError((object)("[SimXP] Save file for " + mySimTracking.SimName + " contains a record named '" + val.NPCName + "'; quarantine held.")); val = null; } if (val != null) { mySimTracking.Level = val.MyLevel; mySimTracking.CurXp = val.XpForLevelUp; __instance.MyStats.Level = val.MyLevel; __instance.MyStats.CurrentExperience = val.XpForLevelUp; __instance.MyStats.SetXpForLevelUp(); __instance.MyStats.CalcStats(); _saveDataCache[mySimTracking.SimName] = val; _loadFailedSims.Remove(mySimTracking.SimName); LogWarning("[SimXP] Quarantine lifted for " + mySimTracking.SimName + " (real file read at spawn)."); } } else if (_cacheBuilt) { if (__instance.MyStats.Level != mySimTracking.Level) { __instance.MyStats.Level = mySimTracking.Level; __instance.MyStats.CurrentExperience = mySimTracking.CurXp; __instance.MyStats.SetXpForLevelUp(); __instance.MyStats.CalcStats(); } else if (__instance.MyStats.CurrentExperience != mySimTracking.CurXp) { __instance.MyStats.CurrentExperience = mySimTracking.CurXp; } } else if (mySimTracking.Level > __instance.MyStats.Level || (mySimTracking.Level == __instance.MyStats.Level && mySimTracking.CurXp > __instance.MyStats.CurrentExperience)) { __instance.MyStats.Level = mySimTracking.Level; __instance.MyStats.CurrentExperience = mySimTracking.CurXp; __instance.MyStats.SetXpForLevelUp(); __instance.MyStats.CalcStats(); } } catch (Exception ex) { LogWarning("[SimXP] SimPlayer_Start_Postfix failed: " + ex.Message); } } [HarmonyPatch(typeof(SimPlayer), "SaveSim")] [HarmonyPrefix] public static void SaveSim_Prefix(SimPlayer __instance, out (int level, bool valid) __state) { __state = (level: 0, valid: false); try { SimPlayerMngr simMngr = GameData.SimMngr; if (!((Object)(object)simMngr == (Object)null) && !((Object)(object)__instance == (Object)null) && __instance.myIndex >= 0 && __instance.myIndex < simMngr.Sims.Count) { SimPlayerTracking val = simMngr.Sims[__instance.myIndex]; if (val != null) { __state = (level: val.Level, valid: true); } } } catch { } } [HarmonyPatch(typeof(SimPlayer), "SaveSim")] [HarmonyPostfix] public static void SaveSim_Postfix(SimPlayer __instance, (int level, bool valid) __state) { try { if ((Object)(object)__instance == (Object)null || (Object)(object)__instance.MyStats == (Object)null || !__instance.VerifyLoadComplete) { return; } SimPlayerMngr simMngr = GameData.SimMngr; if ((Object)(object)simMngr == (Object)null || __instance.myIndex < 0 || __instance.myIndex >= simMngr.Sims.Count) { return; } SimPlayerTracking val = simMngr.Sims[__instance.myIndex]; if (val == null) { return; } if (__instance.MySimTracking == null) { if (_loadFailedSims.Count > 0 && _loadFailedSims.Contains(val.SimName)) { val.CurXp = __instance.MyStats.CurrentExperience; _loadFailedSims.Remove(val.SimName); LogWarning("[SimXP] Quarantine lifted for " + val.SimName + " after a clean spawn read."); } else if (__state.valid) { (val.Level, _) = __state; } } else { val.CurXp = __instance.MyStats.CurrentExperience; _saveDataCache.Remove(val.SimName); ClearDirty(val.SimName); } } catch (Exception ex) { LogWarning("[SimXP] SaveSim_Postfix failed: " + ex.Message); } } [HarmonyPatch(typeof(SimPlayerDataManager), "SaveSimData")] [HarmonyPrefix] public static bool SaveSimData_Prefix(SimPlayerSaveData _data) { if (_cacheBuilt) { return true; } if (_data == null || string.IsNullOrEmpty(_data.NPCName)) { return true; } if (_loadFailedSims.Count > 0 && _loadFailedSims.Contains(_data.NPCName)) { LogWarning("[SimXP] Blocked a login-time save for " + _data.NPCName + " (quarantined; substitute data must not reach the real file)."); return false; } return true; } [HarmonyPatch(typeof(SimPlayerDataManager), "SaveSimData")] [HarmonyPostfix] public static void SaveSimData_Postfix(SimPlayerSaveData _data) { if (_cacheBuilt && _data != null && !string.IsNullOrEmpty(_data.NPCName)) { _saveDataCache.Remove(_data.NPCName); } } [HarmonyPatch(typeof(SimPlayerDataManager), "DeleteSimSaveData")] [HarmonyPostfix] public static void DeleteSimSaveData_Postfix(string npcName) { if (!string.IsNullOrEmpty(npcName)) { _saveDataCache.Remove(npcName); ClearDirty(npcName); } } [HarmonyPatch(typeof(SimPlayerMngr), "CollectActiveSimData")] [HarmonyPrefix] public static void CollectActiveSimData_Prefix() { if (_cacheBuilt) { if (_seedIncrementOnZone) { _zoneSeed++; } LogDebug("[SimXP] Zone change: seed=" + _zoneSeed); } } [HarmonyPatch(typeof(SimPlayerDataManager), "SaveAllSimData")] [HarmonyPrefix] public static void SaveAllSimData_Prefix() { if (_cacheBuilt) { FlushAllDirty("save point"); } } [HarmonyPatch(typeof(AuctionHouse), "LoadAllSPData")] [HarmonyFinalizer] public static Exception LoadAllSPData_Finalizer(Exception __exception) { if (__exception != null) { LogWarning("[SimXP] Auction house load failed mid-pass (" + __exception.Message + "); continuing with partial auction data for this session."); } return null; } [HarmonyPatch(typeof(SimPlayerMngr), "SimPlayerCatchupCode")] [HarmonyPrefix] public static bool CatchupCode_Prefix() { return false; } public static int XpForLevel(int level) { int num = level * 10 * (level + level); if (level > 9 && level < 20) { num = Mathf.RoundToInt((float)num * 1.3f); } if (level >= 20 && level < 30) { num = Mathf.RoundToInt((float)num * 1.6f); } if (level >= 30) { num *= 2; } return num; } private static float GetHoursForLevel(int level) { if (level >= 1 && level < _hoursForLevel.Length) { return _hoursForLevel[level]; } return 7.5f; } private static float GetSimVariance(string simName) { int num = -2128831035; foreach (char c in simName) { num = (num ^ c) * 16777619; } num ^= _zoneSeed; float num2 = (float)(num & 0x7FFFFFFF) / 2.1474836E+09f; return _varianceMin + num2 * (_varianceMax - _varianceMin); } private static IEnumerator XpTickCoroutine() { while (true) { yield return (object)new WaitForSeconds(_tickIntervalSecs); SimPlayerMngr mgr = GameData.SimMngr; if ((Object)(object)mgr == (Object)null || !mgr.LoadedSimplayers || !_cacheBuilt || GameData.InCharSelect) { continue; } if ((Object)(object)GameData.PlayerStats == (Object)null || GameData.CurrentCharacterSlot == null) { LogWarning("[SimXP] XP Tick blocked: PlayerStats or CurrentCharacterSlot is null (zone change?)"); continue; } float startTime = Time.realtimeSinceStartup; int processed = 0; int num = 0; for (int i = 0; i < mgr.Sims.Count; i++) { SimPlayerTracking val = mgr.Sims[i]; if (val != null && val.Level < _maxLevel) { try { ProcessXpForSim(val); } catch (Exception ex) { LogWarning("[SimXP] Tick failed for " + (val.SimName ?? "") + ": " + ex.Message); } processed++; if (++num >= 25) { yield return null; num = 0; } } } float num2 = (Time.realtimeSinceStartup - startTime) * 1000f; LogDebug("[SimXP] XP Tick: processed " + processed + " sims in " + num2.ToString("F2") + "ms"); } } public static void ProcessXpForSim(SimPlayerTracking sim) { if (_loadFailedSims.Count > 0 && _loadFailedSims.Contains(sim.SimName)) { return; } for (int i = 0; i < GameData.GroupMembers.Length; i++) { if (GameData.GroupMembers[i] != null && GameData.GroupMembers[i].SimName == sim.SimName) { return; } } bool flag = (Object)(object)sim.MyAvatar != (Object)null && (Object)(object)sim.MyStats != (Object)null && sim.MyAvatar.MySimTracking != null; if (flag) { sim.Level = sim.MyStats.Level; sim.CurXp = sim.MyStats.CurrentExperience; if (sim.Level >= _maxLevel) { return; } } int num; int num2; if (sim.TiedToSlot >= 0) { num = ((sim.TiedToSlot <= 10) ? 1 : 0); if (num != 0 && GameData.CurrentCharacterSlot != null) { num2 = ((sim.TiedToSlot == GameData.CurrentCharacterSlot.index) ? 1 : 0); goto IL_00f4; } } else { num = 0; } num2 = 0; goto IL_00f4; IL_00f4: bool flag2 = (byte)num2 != 0; bool flag3 = sim.TiedToSlot == 99; if ((num != 0 && !flag2) || (flag2 && (Object)(object)GameData.PlayerStats != (Object)null && sim.Level >= GameData.PlayerStats.Level + _slotLevelCapOffset)) { return; } int num3 = XpForLevel(sim.Level); float hoursForLevel = GetHoursForLevel(sim.Level + 1); float num4 = 3600f / _tickIntervalSecs; int num5 = Mathf.RoundToInt(hoursForLevel * num4); if (num5 < 1) { num5 = 1; } int num6 = num3 / num5; if (num6 < 1) { num6 = 1; } float simVariance = GetSimVariance(sim.SimName); float num7 = 1f; float num8 = 1f; if (flag3) { num7 = _rivalSpeedMultiplier; } if (flag2 && (Object)(object)GameData.PlayerStats != (Object)null && sim.Level < GameData.PlayerStats.Level) { int num9 = GameData.PlayerStats.Level - sim.Level; num8 = 1f + Mathf.Min((float)num9 * _slotCatchupPerLevel, _slotCatchupMax - 1f); } float num10 = simVariance * num7 * num8; int num11 = Mathf.RoundToInt((float)num6 * num10); if (num11 < 1) { num11 = 1; } int level = sim.Level; sim.CurXp += num11; while (sim.CurXp >= num3 && sim.Level < _maxLevel) { sim.CurXp -= num3; sim.Level++; num3 = XpForLevel(sim.Level); } if (flag) { sim.MyStats.CurrentExperience = sim.CurXp; if (sim.Level != level) { sim.MyStats.Level = sim.Level; sim.MyStats.SetXpForLevelUp(); sim.MyStats.CalcStats(); } } if (!flag || !GameData.SimMngr.ActiveSimInstances.Contains(sim.MyAvatar)) { MarkDirty(sim.SimName, sim.Level != level); } if (_debugLog) { string text = (flag3 ? "rival" : (flag2 ? "slot" : "generic")); string text2 = ""; if (num7 != 1f) { text2 = text2 + " rival=" + num7.ToString("F2"); } if (num8 != 1f) { text2 = text2 + " catchup=" + num8.ToString("F2"); } Debug.Log((object)("[SimXP] TICK | " + sim.SimName + " L" + sim.Level + " | +" + num11 + " XP | " + sim.CurXp + "/" + num3 + " (" + sim.CurXp * 100 / num3 + "%) | var=" + simVariance.ToString("F2") + " | " + text + text2)); if (sim.Level != level) { Debug.Log((object)("[SimXP] LEVELUP | " + sim.SimName + " L" + level + "→" + sim.Level + " | " + text + text2)); } } } }