Decompiled source of SimPassiveLevelingOverhaul v1.4.2

SimPassiveLevelingOverhaul/SimPassiveLevelingOverhaul.dll

Decompiled 2 months ago
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<string, (int level, int curXp)> _preLoadSnapshot = new Dictionary<string, (int, int)>();

	public static readonly Dictionary<string, SimPlayerSaveData> _saveDataCache = new Dictionary<string, SimPlayerSaveData>();

	private static readonly HashSet<string> _dirtyXp = new HashSet<string>();

	private static readonly Queue<string> _dirtyXpOrder = new Queue<string>();

	private static readonly HashSet<string> _dirtyLevel = new HashSet<string>();

	private static readonly Queue<string> _dirtyLevelOrder = new Queue<string>();

	public static readonly HashSet<string> _loadFailedSims = new HashSet<string>();

	private const float WriteIntervalSeconds = 0.25f;

	private ConfigEntry<float> _cfgTickInterval;

	private ConfigEntry<float> _cfgVarianceMin;

	private ConfigEntry<float> _cfgVarianceMax;

	private ConfigEntry<bool> _cfgUseFixedSeed;

	private ConfigEntry<int> _cfgFixedSeedValue;

	private ConfigEntry<bool> _cfgSeedIncrementOnZone;

	private ConfigEntry<float> _cfgRivalSpeedMult;

	private ConfigEntry<float> _cfgSlotCatchupPerLevel;

	private ConfigEntry<float> _cfgSlotCatchupMax;

	private ConfigEntry<int> _cfgSlotLevelCapOffset;

	private ConfigEntry<int> _cfgMaxLevel;

	private ConfigEntry<bool> _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<SimPlayerSaveData>(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<string> rivalRoster = new List<string>();
		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<float>("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<float>(90f, 3600f), Array.Empty<object>()));
		_cfgMaxLevel = ((BaseUnityPlugin)this).Config.Bind<int>("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<int>(1, 35), Array.Empty<object>()));
		_cfgDebugLogging = ((BaseUnityPlugin)this).Config.Bind<bool>("General", "DebugLogging", false, "If true, logs every XP tick for every sim (spammy!). Use only for troubleshooting.");
		_cfgVarianceMin = ((BaseUnityPlugin)this).Config.Bind<float>("Variance", "VarianceMin", 0.5f, "Slowest multiplier for per-sim XP variance. 0.5 = half speed.");
		_cfgVarianceMax = ((BaseUnityPlugin)this).Config.Bind<float>("Variance", "VarianceMax", 1.5f, "Fastest multiplier for per-sim XP variance. 1.5 = 50% faster.");
		_cfgUseFixedSeed = ((BaseUnityPlugin)this).Config.Bind<bool>("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<int>("Variance", "FixedSeedValue", 12345, "The seed value used when UseFixedSeed is true.");
		_cfgSeedIncrementOnZone = ((BaseUnityPlugin)this).Config.Bind<bool>("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<float>("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<float>("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<float>("SlotTied", "SlotCatchupMax", 3f, "Maximum total catchup multiplier for slot-tied sims.");
		_cfgSlotLevelCapOffset = ((BaseUnityPlugin)this).Config.Bind<int>("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<float> val = ((BaseUnityPlugin)this).Config.Bind<float>("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<Item> _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<SimPlayerSaveData>(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<Item>(), 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<Item>(), 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 ?? "<unnamed>") + ": " + 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));
			}
		}
	}
}