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));
}
}
}
}