Decompiled source of CaveCrawlersLeveling v2.0.0

BepInEx/plugins/CaveCrawlersTweaks/CaveCrawlersTweaks.dll

Decompiled 11 hours ago
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.Versioning;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using BepInEx;
using BepInEx.Core.Logging.Interpolation;
using BepInEx.Logging;
using BepInEx.Unity.IL2CPP;
using HarmonyLib;
using Il2CppInterop.Runtime;
using Il2CppInterop.Runtime.InteropTypes;
using Il2CppInterop.Runtime.InteropTypes.Arrays;
using Il2CppSystem;
using Il2CppSystem.Collections.Generic;
using Microsoft.CodeAnalysis;
using Photon.Pun;
using UnityEngine;

[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)]
[assembly: TargetFramework(".NETCoreApp,Version=v6.0", FrameworkDisplayName = ".NET 6.0")]
[assembly: AssemblyCompany("CaveCrawlersTweaks")]
[assembly: AssemblyConfiguration("Release")]
[assembly: AssemblyFileVersion("2.0.0.0")]
[assembly: AssemblyInformationalVersion("2.0.0")]
[assembly: AssemblyProduct("Cave Crawlers Leveling")]
[assembly: AssemblyTitle("CaveCrawlersTweaks")]
[assembly: AssemblyVersion("2.0.0.0")]
namespace Microsoft.CodeAnalysis
{
	[CompilerGenerated]
	[Embedded]
	internal sealed class EmbeddedAttribute : Attribute
	{
	}
}
namespace System.Runtime.CompilerServices
{
	[CompilerGenerated]
	[Embedded]
	[AttributeUsage(AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Event | AttributeTargets.Parameter | AttributeTargets.ReturnValue | AttributeTargets.GenericParameter, AllowMultiple = false, Inherited = false)]
	internal sealed class NullableAttribute : Attribute
	{
		public readonly byte[] NullableFlags;

		public NullableAttribute(byte P_0)
		{
			NullableFlags = new byte[1] { P_0 };
		}

		public NullableAttribute(byte[] P_0)
		{
			NullableFlags = P_0;
		}
	}
	[CompilerGenerated]
	[Embedded]
	[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Method | AttributeTargets.Interface | AttributeTargets.Delegate, AllowMultiple = false, Inherited = false)]
	internal sealed class NullableContextAttribute : Attribute
	{
		public readonly byte Flag;

		public NullableContextAttribute(byte P_0)
		{
			Flag = P_0;
		}
	}
}
namespace CaveCrawlersTweaks
{
	[BepInPlugin("pedro.cavecrawlers.tweaks", "Cave Crawlers Leveling", "2.0.0")]
	public class Plugin : BasePlugin
	{
		public override void Load()
		{
			((BasePlugin)this).Log.LogInfo((object)"Cave Crawlers Leveling loaded.");
			SoloLevelingPersistence.Initialize(((BasePlugin)this).Log);
			AttributeBonusSession.Initialize(((BasePlugin)this).Log);
			((BasePlugin)this).AddComponent<StatsController>();
			SoloLevelingXpObserver.Install(((BasePlugin)this).Log);
		}

		public override bool Unload()
		{
			SoloLevelingPersistence.Flush();
			return false;
		}
	}
	public class StatsController : MonoBehaviour
	{
		private int controllerId;

		public StatsController(IntPtr pointer)
			: base(pointer)
		{
		}

		public void Start()
		{
			controllerId = ((Object)this).GetInstanceID();
			AttributeBonusSession.Attach(controllerId);
		}

		public void Update()
		{
			SoloLevelingPersistence.Tick();
			AttributeBonusSession.Tick(Time.frameCount);
			SoloLevelingStatusMenu.Tick(controllerId);
		}

		public void OnGUI()
		{
			SoloLevelingProfileMenu.Draw(controllerId);
			SoloLevelingXpHud.Draw(controllerId);
			SoloLevelingStatusMenu.Draw(controllerId);
		}

		public void OnApplicationQuit()
		{
			SoloLevelingPersistence.Shutdown();
		}

		public void OnApplicationPause(bool paused)
		{
			if (paused)
			{
				SoloLevelingPersistence.Flush();
			}
		}

		public void OnDestroy()
		{
			if (controllerId != 0)
			{
				SoloLevelingPersistence.Flush();
				SoloLevelingStatusMenu.Detach(controllerId);
				AttributeBonusSession.Detach(controllerId);
			}
		}
	}
	internal static class SoloLevelingUiText
	{
		internal static readonly IReadOnlyDictionary<string, string> StatNames = new Dictionary<string, string>(StringComparer.Ordinal)
		{
			["ExtraDamage"] = "Extra Damage",
			["DamageMultiplier"] = "Damage Multiplier",
			["CriticalChance"] = "Critical Chance",
			["CriticalDamage"] = "Critical Damage",
			["AttackSpeed"] = "Attack Speed",
			["WeaponRange"] = "Weapon Range",
			["Knockback"] = "Knockback",
			["DodgeChance"] = "Dodge Chance",
			["BlockChance"] = "Block Chance",
			["MoveSpeed"] = "Move Speed",
			["SpellHaste"] = "Spell Haste",
			["MaxTorchDuration"] = "Max Torch Duration"
		};

		internal static string Stat(string id)
		{
			if (!StatNames.TryGetValue(id, out string value))
			{
				return id;
			}
			return value;
		}

		internal static string Number(double value)
		{
			return value.ToString("0.##", CultureInfo.InvariantCulture);
		}
	}
	internal static class SoloLevelingHudObstacles
	{
		private static Il2CppStructArray<Vector3>? corners;

		private static bool warned;

		internal static SoloLevelingHudObstacle? HealthBar(GUIManager? gui, int screenHeight)
		{
			//IL_0053: Unknown result type (might be due to invalid IL or missing references)
			//IL_0127: Unknown result type (might be due to invalid IL or missing references)
			//IL_012c: Unknown result type (might be due to invalid IL or missing references)
			//IL_012e: Unknown result type (might be due to invalid IL or missing references)
			//IL_013c: Unknown result type (might be due to invalid IL or missing references)
			//IL_015c: Unknown result type (might be due to invalid IL or missing references)
			//IL_016b: Unknown result type (might be due to invalid IL or missing references)
			//IL_017b: Unknown result type (might be due to invalid IL or missing references)
			//IL_018b: Unknown result type (might be due to invalid IL or missing references)
			try
			{
				if (gui == null || ((Il2CppObjectBase)gui).Pointer == IntPtr.Zero || !Object.op_Implicit((Object)(object)gui))
				{
					return null;
				}
				Canvas myCanvas = gui.myCanvas;
				if (myCanvas == null || ((Il2CppObjectBase)myCanvas).Pointer == IntPtr.Zero || !Object.op_Implicit((Object)(object)myCanvas) || (int)myCanvas.renderMode != 0)
				{
					return null;
				}
				PlayerHealthBar healthBar = gui.HealthBar;
				if (healthBar == null || ((Il2CppObjectBase)healthBar).Pointer == IntPtr.Zero || !Object.op_Implicit((Object)(object)healthBar) || !((Behaviour)healthBar).isActiveAndEnabled)
				{
					return null;
				}
				RectTransform val = ((Il2CppObjectBase)((Component)healthBar).transform).TryCast<RectTransform>();
				if (val == null || ((Il2CppObjectBase)val).Pointer == IntPtr.Zero || !Object.op_Implicit((Object)(object)val))
				{
					return null;
				}
				if (corners == null)
				{
					corners = new Il2CppStructArray<Vector3>(4L);
				}
				val.GetWorldCorners(corners);
				float num = float.PositiveInfinity;
				float num2 = float.NegativeInfinity;
				float num3 = float.PositiveInfinity;
				float num4 = float.NegativeInfinity;
				for (int i = 0; i < 4; i++)
				{
					Vector3 val2 = ((Il2CppArrayBase<Vector3>)(object)corners)[i];
					if (!float.IsFinite(val2.x) || !float.IsFinite(val2.y))
					{
						return null;
					}
					num = Math.Min(num, val2.x);
					num2 = Math.Max(num2, val2.x);
					num3 = Math.Min(num3, val2.y);
					num4 = Math.Max(num4, val2.y);
				}
				return new SoloLevelingHudObstacle(num, (float)screenHeight - num4, num2 - num, num4 - num3);
			}
			catch (Exception ex)
			{
				if (!warned)
				{
					warned = true;
					AttributeBonusSession.LogHudException("Optional health-bar bounds; using corner layout", ex.ToString());
				}
				return null;
			}
		}
	}
	internal static class SoloLevelingStatRules
	{
		internal static readonly IReadOnlyDictionary<string, float> Steps = new Dictionary<string, float>(StringComparer.Ordinal)
		{
			["ExtraDamage"] = 0.2f,
			["DamageMultiplier"] = 5f,
			["CriticalChance"] = 1f,
			["CriticalDamage"] = 5f,
			["AttackSpeed"] = 1f,
			["WeaponRange"] = 0.1f,
			["Knockback"] = 1f,
			["DodgeChance"] = 1f,
			["BlockChance"] = 1f,
			["MoveSpeed"] = 5f,
			["SpellHaste"] = 1f,
			["MaxTorchDuration"] = 5f
		};
	}
	internal sealed class SoloLevelingSaveDocument
	{
		public int SchemaVersion { get; set; } = 3;

		public string ProfileScope { get; set; } = "ExplicitAccountProfiles";

		internal int SourceSchemaVersion { get; set; } = 3;

		public Dictionary<string, SoloLevelingSaveProfile> Profiles { get; set; } = new Dictionary<string, SoloLevelingSaveProfile>(StringComparer.Ordinal);
	}
	internal sealed class SoloLevelingSaveProfile
	{
		public string AccountKey { get; set; } = "";

		public string ProfileId { get; set; } = Guid.NewGuid().ToString("D");

		public string Name { get; set; } = "Hunter Profile";

		public bool IsLegacy { get; set; }

		public int Level { get; set; } = 1;

		public double Xp { get; set; }

		public long FreePoints { get; set; }

		public Dictionary<string, long> Investments { get; set; } = new Dictionary<string, long>(StringComparer.Ordinal);

		public Dictionary<string, long> RetiredInvestments { get; set; } = new Dictionary<string, long>(StringComparer.Ordinal);

		internal ProgressionSnapshot Snapshot => new ProgressionSnapshot(Level, Xp, SoloLevelingSaveCodec.RequiredXp(Level), FreePoints);

		internal SoloLevelingSaveProfile Copy()
		{
			return new SoloLevelingSaveProfile
			{
				AccountKey = AccountKey,
				ProfileId = ProfileId,
				Name = Name,
				IsLegacy = IsLegacy,
				Level = Level,
				Xp = Xp,
				FreePoints = FreePoints,
				Investments = new Dictionary<string, long>(Investments, StringComparer.Ordinal),
				RetiredInvestments = new Dictionary<string, long>(RetiredInvestments, StringComparer.Ordinal)
			};
		}
	}
	internal static class SoloLevelingSaveCodec
	{
		private sealed class LegacyDocument
		{
			public int SchemaVersion { get; set; }

			public string ProfileScope { get; set; } = "";

			public Dictionary<string, SoloLevelingSaveProfile> Profiles { get; set; } = new Dictionary<string, SoloLevelingSaveProfile>(StringComparer.Ordinal);
		}

		internal const int MaxBytes = 1048576;

		internal const int MaxLevel = 3000;

		private static readonly JsonSerializerOptions Options = new JsonSerializerOptions
		{
			WriteIndented = true,
			MaxDepth = 12
		};

		internal static string ProfileKey(ulong account)
		{
			return Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes("CaveCrawlersTweaks/SoloLeveling/account/v1:" + account.ToString(CultureInfo.InvariantCulture))));
		}

		internal static string ProfileKey(string accountKey, string profileId)
		{
			return accountKey + ":" + profileId.ToLowerInvariant();
		}

		internal static double RequiredXp(int level)
		{
			if (level < 1 || level > 3000)
			{
				throw new InvalidDataException("Level outside supported save range.");
			}
			double num = 100.0;
			for (int i = 1; i < level; i++)
			{
				num *= 1.2;
			}
			return num;
		}

		internal static void Validate(SoloLevelingSaveProfile profile)
		{
			ValidateProfile(profile, legacyDexterity: false);
		}

		private static void ValidateProfile(SoloLevelingSaveProfile profile, bool legacyDexterity)
		{
			if (profile == null || !IsHash(profile.AccountKey) || !Guid.TryParseExact(profile.ProfileId, "D", out var _) || string.IsNullOrWhiteSpace(profile.Name) || profile.Name.Length > 32 || profile.Name.Any(char.IsControl) || !double.IsFinite(profile.Xp) || profile.Xp < 0.0 || profile.Xp >= RequiredXp(profile.Level) || profile.FreePoints < 0 || profile.Investments == null || profile.RetiredInvestments == null)
			{
				throw new InvalidDataException("Invalid profile metadata or progression values.");
			}
			checked
			{
				long earned = (unchecked((long)profile.Level) - 1L) * 5;
				long num = 0L;
				foreach (KeyValuePair<string, long> investment in profile.Investments)
				{
					if ((!SoloLevelingStatRules.Steps.ContainsKey(investment.Key) && (!legacyDexterity || !(investment.Key == "Dexterity"))) || investment.Value < 0 || investment.Value > earned)
					{
						throw new InvalidDataException("Invalid or unsupported investment.");
					}
					num += investment.Value;
				}
				if (profile.RetiredInvestments.Any<KeyValuePair<string, long>>((KeyValuePair<string, long> p) => p.Key != "Dexterity" || p.Value < 0 || p.Value > earned) || profile.FreePoints + num != earned)
				{
					throw new InvalidDataException("Point budget or retired-investment audit is invalid.");
				}
			}
		}

		internal static void Validate(SoloLevelingSaveDocument document)
		{
			if (document == null || document.SchemaVersion != 3 || document.ProfileScope != "ExplicitAccountProfiles" || document.Profiles == null || document.Profiles.Count > 512)
			{
				throw new InvalidDataException("Unsupported schema or profile count.");
			}
			HashSet<string> hashSet = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
			foreach (KeyValuePair<string, SoloLevelingSaveProfile> profile in document.Profiles)
			{
				Validate(profile.Value);
				if (profile.Key != ProfileKey(profile.Value.AccountKey, profile.Value.ProfileId) || !hashSet.Add(profile.Key))
				{
					throw new InvalidDataException("Profile key does not match account and UUID.");
				}
			}
		}

		internal static byte[] Encode(SoloLevelingSaveDocument document)
		{
			Validate(document);
			byte[] array = JsonSerializer.SerializeToUtf8Bytes(document, Options);
			if (array.Length > 1048576)
			{
				throw new InvalidDataException("Save too large.");
			}
			return array;
		}

		internal static SoloLevelingSaveDocument Decode(byte[] bytes)
		{
			if (bytes.Length > 1048576)
			{
				throw new InvalidDataException("Save too large.");
			}
			checked
			{
				using JsonDocument jsonDocument = JsonDocument.Parse((ReadOnlyMemory<byte>)bytes, new JsonDocumentOptions
				{
					MaxDepth = 12
				});
				int @int = jsonDocument.RootElement.GetProperty("SchemaVersion").GetInt32();
				if ((@int < 1 || @int > 3) ? true : false)
				{
					throw new InvalidDataException("Unsupported schema.");
				}
				switch (@int)
				{
				case 1:
					ExactProperties(jsonDocument.RootElement, "SchemaVersion", "Profiles");
					break;
				case 2:
					ExactProperties(jsonDocument.RootElement, "SchemaVersion", "ProfileScope", "Profiles");
					break;
				default:
					ExactProperties(jsonDocument.RootElement, "SchemaVersion", "ProfileScope", "Profiles");
					break;
				}
				JsonElement property = jsonDocument.RootElement.GetProperty("Profiles");
				ExactProperties(property, (from p in property.EnumerateObject()
					select p.Name).ToArray());
				foreach (JsonProperty item in property.EnumerateObject())
				{
					switch (@int)
					{
					case 1:
						ExactProperties(item.Value, "Level", "Xp", "FreePoints", "Investments");
						break;
					case 2:
						ExactProperties(item.Value, "Level", "Xp", "FreePoints", "Investments", "RetiredInvestments");
						ExactProperties(item.Value.GetProperty("RetiredInvestments"), (from p in item.Value.GetProperty("RetiredInvestments").EnumerateObject()
							select p.Name).ToArray());
						break;
					default:
						ExactProperties(item.Value, "AccountKey", "ProfileId", "Name", "IsLegacy", "Level", "Xp", "FreePoints", "Investments", "RetiredInvestments");
						ExactProperties(item.Value.GetProperty("Investments"), (from p in item.Value.GetProperty("Investments").EnumerateObject()
							select p.Name).ToArray());
						ExactProperties(item.Value.GetProperty("RetiredInvestments"), (from p in item.Value.GetProperty("RetiredInvestments").EnumerateObject()
							select p.Name).ToArray());
						break;
					}
					ExactProperties(item.Value.GetProperty("Investments"), (from p in item.Value.GetProperty("Investments").EnumerateObject()
						select p.Name).ToArray());
				}
				if (@int == 3)
				{
					SoloLevelingSaveDocument? obj = JsonSerializer.Deserialize<SoloLevelingSaveDocument>(bytes, Options) ?? throw new InvalidDataException("Empty save.");
					obj.SourceSchemaVersion = 3;
					Validate(obj);
					return obj;
				}
				LegacyDocument legacyDocument = JsonSerializer.Deserialize<LegacyDocument>(bytes, Options) ?? throw new InvalidDataException("Empty legacy save.");
				if (legacyDocument.Profiles == null || legacyDocument.Profiles.Count > 64 || (@int == 2 && legacyDocument.ProfileScope != "Account"))
				{
					throw new InvalidDataException("Invalid legacy account save.");
				}
				SoloLevelingSaveDocument soloLevelingSaveDocument = new SoloLevelingSaveDocument
				{
					SourceSchemaVersion = @int
				};
				foreach (KeyValuePair<string, SoloLevelingSaveProfile> profile in legacyDocument.Profiles)
				{
					if (!IsHash(profile.Key))
					{
						throw new InvalidDataException("Legacy account key is invalid.");
					}
					SoloLevelingSaveProfile value = profile.Value;
					if (value == null)
					{
						throw new InvalidDataException("Legacy profile is null.");
					}
					if (@int == 1)
					{
						value.AccountKey = profile.Key;
						value.ProfileId = Guid.NewGuid().ToString("D");
						value.Name = "Legacy Profile";
						value.IsLegacy = true;
						value.RetiredInvestments = new Dictionary<string, long>(StringComparer.Ordinal);
						ValidateProfile(value, legacyDexterity: true);
						if (value.Investments.Remove("Dexterity", out var value2) && value2 > 0)
						{
							value.FreePoints += value2;
							value.RetiredInvestments["Dexterity"] = value2;
						}
					}
					else
					{
						value.AccountKey = profile.Key;
						value.ProfileId = Guid.NewGuid().ToString("D");
						value.Name = "Legacy Profile";
						value.IsLegacy = true;
						ValidateProfile(value, legacyDexterity: false);
					}
					soloLevelingSaveDocument.Profiles.Add(ProfileKey(profile.Key, value.ProfileId), value);
				}
				Validate(soloLevelingSaveDocument);
				return soloLevelingSaveDocument;
			}
		}

		private static bool IsHash(string value)
		{
			if (value.Length == 64)
			{
				return value.All(Uri.IsHexDigit);
			}
			return false;
		}

		private static void ExactProperties(JsonElement element, params string[] expected)
		{
			if (element.ValueKind != JsonValueKind.Object)
			{
				throw new InvalidDataException("Expected object.");
			}
			HashSet<string> hashSet = new HashSet<string>(StringComparer.Ordinal);
			foreach (JsonProperty item in element.EnumerateObject())
			{
				if (!hashSet.Add(item.Name) || !expected.Contains<string>(item.Name, StringComparer.Ordinal))
				{
					throw new InvalidDataException("Duplicate or unknown field.");
				}
			}
			if (hashSet.Count != expected.Length)
			{
				throw new InvalidDataException("Missing field.");
			}
		}
	}
	internal sealed class SoloLevelingSaveRepository : IDisposable
	{
		private readonly string primary;

		private string target;

		private byte[]? expectedDigest;

		private byte[]? legacyBytes;

		private readonly FileStream lease;

		private readonly Action<string> warn;

		internal SoloLevelingSaveDocument Document { get; private set; } = new SoloLevelingSaveDocument();

		internal bool RecoveryMode { get; private set; }

		internal SoloLevelingSaveRepository(string path, Action<string> warn)
		{
			primary = Path.GetFullPath(path);
			target = primary;
			this.warn = warn;
			Directory.CreateDirectory(Path.GetDirectoryName(primary));
			lease = new FileStream(primary + ".lock", FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.None);
			try
			{
				Load();
			}
			catch
			{
				lease.Dispose();
				throw;
			}
		}

		private static byte[] ReadBounded(string path)
		{
			using FileStream fileStream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read);
			if (fileStream.Length > 1048576)
			{
				throw new InvalidDataException("Save too large.");
			}
			using MemoryStream memoryStream = new MemoryStream();
			byte[] array = new byte[4096];
			int num;
			while ((num = fileStream.Read(array, 0, array.Length)) > 0)
			{
				if (memoryStream.Length + num > 1048576)
				{
					throw new InvalidDataException("Save too large.");
				}
				memoryStream.Write(array, 0, num);
			}
			return memoryStream.ToArray();
		}

		internal static SoloLevelingSaveDocument ReadOnlySnapshot(string path)
		{
			string[] array = new string[3]
			{
				path,
				path + ".recovered.json",
				path + ".bak"
			};
			foreach (string path2 in array)
			{
				try
				{
					return SoloLevelingSaveCodec.Decode(ReadBounded(path2));
				}
				catch
				{
				}
			}
			return new SoloLevelingSaveDocument();
		}

		private bool TryRead(string path, out SoloLevelingSaveDocument document, out byte[]? digest)
		{
			document = new SoloLevelingSaveDocument();
			digest = null;
			try
			{
				byte[] array = ReadBounded(path);
				document = SoloLevelingSaveCodec.Decode(array);
				if (document.SourceSchemaVersion < 3)
				{
					legacyBytes = array;
				}
				digest = SHA256.HashData(array);
				return true;
			}
			catch (FileNotFoundException)
			{
				return false;
			}
			catch (Exception ex2)
			{
				warn("Save indisponível/inválido; conteúdo preservado. Tipo=" + ex2.GetType().Name);
				return false;
			}
		}

		private void Load()
		{
			bool flag;
			try
			{
				File.GetAttributes(primary);
				flag = true;
			}
			catch (FileNotFoundException)
			{
				flag = false;
			}
			if (flag && TryRead(primary, out SoloLevelingSaveDocument document, out byte[] digest))
			{
				Document = document;
				expectedDigest = digest;
				return;
			}
			byte[] digest2;
			if (!flag)
			{
				if (TryRead(primary + ".bak", out SoloLevelingSaveDocument document2, out digest2))
				{
					Document = document2;
				}
				return;
			}
			RecoveryMode = true;
			target = primary + ".recovered.json";
			if (TryRead(target, out SoloLevelingSaveDocument document3, out byte[] digest3))
			{
				Document = document3;
				expectedDigest = digest3;
			}
			else
			{
				if (File.Exists(target))
				{
					throw new InvalidDataException("Recovery target invalid; persistence disabled.");
				}
				if (TryRead(primary + ".bak", out SoloLevelingSaveDocument document4, out digest2))
				{
					Document = document4;
				}
			}
			warn("Modo de recuperação: original preservado; novas gravações usam SoloLeveling.json.recovered.json.");
		}

		internal void Save()
		{
			byte[] array = SoloLevelingSaveCodec.Encode(Document);
			string text = target + ".tmp-" + Guid.NewGuid().ToString("N");
			try
			{
				using (FileStream fileStream = new FileStream(text, FileMode.CreateNew, FileAccess.Write, FileShare.None, 4096, FileOptions.WriteThrough))
				{
					fileStream.Write(array, 0, array.Length);
					fileStream.Flush(flushToDisk: true);
				}
				if (File.Exists(target))
				{
					byte[] array2 = ReadBounded(target);
					if (expectedDigest == null || !SHA256.HashData(array2).SequenceEqual(expectedDigest))
					{
						throw new IOException("Save changed externally; refusing overwrite.");
					}
					SoloLevelingSaveCodec.Decode(array2);
					PreserveLegacyBackup();
					File.Replace(text, target, target + ".bak");
				}
				else
				{
					if (expectedDigest != null)
					{
						throw new IOException("Save removed externally; refusing recreation.");
					}
					PreserveLegacyBackup();
					File.Move(text, target);
				}
				expectedDigest = SHA256.HashData(array);
				Document.SourceSchemaVersion = 3;
				legacyBytes = null;
			}
			finally
			{
				try
				{
					if (File.Exists(text))
					{
						File.Delete(text);
					}
				}
				catch
				{
				}
			}
		}

		internal string CreateVerifiedProfileDeletionBackup()
		{
			byte[] array = SoloLevelingSaveCodec.Encode(Document);
			string text = Convert.ToHexString(SHA256.HashData(array)).Substring(0, 16);
			string text2 = target + ".pre-profile-delete-" + text + ".bak";
			if (File.Exists(text2))
			{
				if (!ReadBounded(text2).SequenceEqual(array))
				{
					throw new IOException("Existing profile-deletion backup does not match the current save.");
				}
			}
			else
			{
				string text3 = text2 + ".tmp-" + Guid.NewGuid().ToString("N");
				try
				{
					using (FileStream fileStream = new FileStream(text3, FileMode.CreateNew, FileAccess.Write, FileShare.None, 4096, FileOptions.WriteThrough))
					{
						fileStream.Write(array, 0, array.Length);
						fileStream.Flush(flushToDisk: true);
					}
					File.Move(text3, text2);
				}
				finally
				{
					try
					{
						if (File.Exists(text3))
						{
							File.Delete(text3);
						}
					}
					catch
					{
					}
				}
			}
			byte[] array2 = ReadBounded(text2);
			if (!array2.SequenceEqual(array))
			{
				throw new IOException("Profile-deletion backup verification failed.");
			}
			SoloLevelingSaveCodec.Decode(array2);
			return text2;
		}

		internal void RefreshRecoveryBackupFromCommittedState()
		{
			byte[] array = ReadBounded(target);
			SoloLevelingSaveCodec.Decode(array);
			string text = target + ".bak";
			string text2 = text + ".tmp-" + Guid.NewGuid().ToString("N");
			try
			{
				using (FileStream fileStream = new FileStream(text2, FileMode.CreateNew, FileAccess.Write, FileShare.None, 4096, FileOptions.WriteThrough))
				{
					fileStream.Write(array, 0, array.Length);
					fileStream.Flush(flushToDisk: true);
				}
				if (File.Exists(text))
				{
					File.Replace(text2, text, null);
				}
				else
				{
					File.Move(text2, text);
				}
				byte[] array2 = ReadBounded(text);
				if (!array2.SequenceEqual(array))
				{
					throw new IOException("Committed recovery backup verification failed.");
				}
				SoloLevelingSaveCodec.Decode(array2);
			}
			finally
			{
				try
				{
					if (File.Exists(text2))
					{
						File.Delete(text2);
					}
				}
				catch
				{
				}
			}
		}

		private void PreserveLegacyBackup()
		{
			if (legacyBytes == null)
			{
				return;
			}
			string path = primary + ".schema-legacy-" + Convert.ToHexString(SHA256.HashData(legacyBytes)).Substring(0, 16) + ".bak";
			if (File.Exists(path))
			{
				if (!ReadBounded(path).SequenceEqual(legacyBytes))
				{
					throw new IOException("Legacy backup mismatch; migration stopped.");
				}
				return;
			}
			using FileStream fileStream = new FileStream(path, FileMode.CreateNew, FileAccess.Write, FileShare.None, 4096, FileOptions.WriteThrough);
			fileStream.Write(legacyBytes, 0, legacyBytes.Length);
			fileStream.Flush(flushToDisk: true);
		}

		public void Dispose()
		{
			lease.Dispose();
		}
	}
	internal sealed class SoloLevelingSaveSchedule
	{
		private double firstDirty;

		private double lastChange;

		private double retryAt;

		internal bool Dirty { get; private set; }

		internal void Reset()
		{
			Dirty = false;
			firstDirty = (lastChange = (retryAt = 0.0));
		}

		internal void Mark(double now)
		{
			if (!Dirty)
			{
				firstDirty = now;
			}
			Dirty = true;
			lastChange = now;
		}

		internal bool Flush(double now, Action save, bool immediate = false, bool shutdown = false)
		{
			if (!Dirty || (!shutdown && now < retryAt) || (!immediate && now - lastChange < 1.0 && now - firstDirty < 3.0))
			{
				return false;
			}
			try
			{
				save();
				Dirty = false;
				retryAt = 0.0;
				return true;
			}
			catch
			{
				retryAt = now + 15.0;
				throw;
			}
		}
	}
	internal sealed record SoloLevelingProfileInfo(string Id, string Name, bool IsLegacy, int Level);
	internal static class SoloLevelingPersistence
	{
		private static readonly object Gate = new object();

		private static readonly SoloLevelingSaveSchedule Schedule = new SoloLevelingSaveSchedule();

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

		private static SoloLevelingSaveRepository? repository;

		private static ManualLogSource? log;

		private static SoloLevelingSaveDocument readOnlyDocument = new SoloLevelingSaveDocument();

		private static string? accountKey;

		private static string? activeProfileKey;

		private static ProgressionSnapshot lastObserved;

		private static IntPtr savedRecordPointer;

		private static bool initialized;

		private static bool stopped;

		private static bool localPlayerReady;

		private static bool profileSafetyBlocked;

		private static bool lifecycleHooksReady;

		internal static bool StorageReady { get; private set; }

		internal static bool ProfileSelected => activeProfileKey != null;

		internal static bool ProgressionReady
		{
			get
			{
				if (StorageReady && lifecycleHooksReady && ProfileSelected && localPlayerReady && !stopped)
				{
					return !profileSafetyBlocked;
				}
				return false;
			}
		}

		internal static bool Ready => ProgressionReady;

		internal static bool NeedsProfileSelection
		{
			get
			{
				if ((localPlayerReady && !ProgressionReady) || profileSafetyBlocked)
				{
					return !stopped;
				}
				return false;
			}
		}

		internal static bool SafetyBlocked => profileSafetyBlocked;

		internal static bool CanSelectProfile
		{
			get
			{
				if (StorageReady && lifecycleHooksReady)
				{
					return !profileSafetyBlocked;
				}
				return false;
			}
		}

		private static double Now => (double)Stopwatch.GetTimestamp() / (double)Stopwatch.Frequency;

		private static SoloLevelingSaveDocument Document => repository?.Document ?? readOnlyDocument;

		internal static string? ActiveProfileName
		{
			get
			{
				lock (Gate)
				{
					SoloLevelingSaveProfile value;
					return (activeProfileKey != null && Document.Profiles.TryGetValue(activeProfileKey, out value)) ? value.Name : null;
				}
			}
		}

		internal static string? ActiveProfileId
		{
			get
			{
				lock (Gate)
				{
					SoloLevelingSaveProfile value;
					return (activeProfileKey != null && Document.Profiles.TryGetValue(activeProfileKey, out value)) ? value.ProfileId : null;
				}
			}
		}

		internal static void Initialize(ManualLogSource logger)
		{
			lock (Gate)
			{
				if (initialized)
				{
					return;
				}
				initialized = true;
				log = logger;
				string path = Path.Combine(Paths.ConfigPath, "CaveCrawlersTweaks", "SoloLeveling.json");
				try
				{
					repository = new SoloLevelingSaveRepository(path, Warn);
					if (repository.Document.SourceSchemaVersion < 3)
					{
						log.LogInfo((object)"[Cave Crawlers Leveling][Profiles] Legacy account progress is preserved as an unselected Legacy Profile. Migration commits only after an explicit profile action; no character ownership is inferred.");
					}
					log.LogInfo((object)"[Cave Crawlers Leveling][Profiles] Storage initialized; an explicit profile choice is required for each character session.");
				}
				catch (Exception ex)
				{
					readOnlyDocument = SoloLevelingSaveRepository.ReadOnlySnapshot(path);
					Warn("Storage unavailable; a validated copy may be used in memory, but changes cannot be guaranteed on disk. Type=" + ex.GetType().Name);
				}
			}
		}

		internal static void SetLifecycleHooksReady(bool ready)
		{
			lifecycleHooksReady = ready;
			if (!ready)
			{
				Warn("Automatic character-session hooks are incomplete; profile selection and progression remain locked to prevent profile reuse.");
				return;
			}
			ManualLogSource? obj = log;
			if (obj != null)
			{
				obj.LogInfo((object)"[Cave Crawlers Leveling][Profiles] Character selection and removal hooks are active; profiles will not be reused across observed character entries.");
			}
		}

		internal static bool EnsureForData(MainPlayerData? data)
		{
			return SoloLevelingXpObserver.Progression.Read(delegate
			{
				EnsureStorageForData(data);
				return ProgressionReady && AttributeBonusSession.HasReadyLocalPlayer();
			});
		}

		private static bool EnsureStorageForData(MainPlayerData? data)
		{
			lock (Gate)
			{
				if (!initialized || stopped)
				{
					return false;
				}
				try
				{
					MainPlayerData instance = MainPlayerData.Instance;
					if (data == null || ((Il2CppObjectBase)data).Pointer == IntPtr.Zero || instance == null || ((Il2CppObjectBase)instance).Pointer == IntPtr.Zero || ((Il2CppObjectBase)data).Pointer != ((Il2CppObjectBase)instance).Pointer || instance.SteamId == 0L)
					{
						return false;
					}
					string text = SoloLevelingSaveCodec.ProfileKey(instance.SteamId);
					if (accountKey != null && accountKey != text)
					{
						EndProfileLocked("local account changed", removeBonuses: false);
						accountKey = null;
						StorageReady = false;
						localPlayerReady = false;
						savedRecordPointer = IntPtr.Zero;
						Warn("Local account changed; explicit profile selection will be required again.");
					}
					if (accountKey == null)
					{
						accountKey = text;
						StorageReady = repository != null;
						SoloLevelingProfileMenu.ResetForNewCharacter();
					}
					return ProgressionReady;
				}
				catch (Exception ex)
				{
					Warn("Storage/account validation failed; progression remains gated. Type=" + ex.GetType().Name);
					return false;
				}
			}
		}

		internal static void ObserveLocalPlayer(PlayerIdentity identity, IntPtr currentSavedRecord)
		{
			WithStateLock(delegate
			{
				lock (Gate)
				{
					if (!(currentSavedRecord == IntPtr.Zero))
					{
						savedRecordPointer = currentSavedRecord;
						localPlayerReady = true;
					}
				}
			});
		}

		internal static void MarkLocalPlayerPending()
		{
			lock (Gate)
			{
				localPlayerReady = false;
			}
		}

		internal static void EndCharacterSession(string reason)
		{
			WithStateLock(delegate
			{
				lock (Gate)
				{
					if (StorageReady && (activeProfileKey != null || localPlayerReady || !(savedRecordPointer == IntPtr.Zero)))
					{
						EndProfileLocked(reason, removeBonuses: true);
						savedRecordPointer = IntPtr.Zero;
						localPlayerReady = false;
						SoloLevelingProfileMenu.ResetForNewCharacter();
					}
				}
			});
		}

		internal static void BeforeLocalCharacterSelected(Player? player)
		{
			try
			{
				if (!((Object)(object)player == (Object)null) && !(((Il2CppObjectBase)player).Pointer == IntPtr.Zero) && !((Object)(object)((Entity)player).view == (Object)null) && ((Entity)player).view.IsMine)
				{
					EndCharacterSession("local character selection");
				}
			}
			catch (Exception ex)
			{
				Warn("Character-selection lifecycle could not be inspected; profile remains gated on the next local record. Type=" + ex.GetType().Name);
			}
		}

		internal static void BeforeCharacterRecordDeleted(PlayerSaved? record)
		{
			try
			{
				if (record != null && !(((Il2CppObjectBase)record).Pointer == IntPtr.Zero))
				{
					bool flag;
					lock (Gate)
					{
						flag = savedRecordPointer == ((Il2CppObjectBase)record).Pointer;
					}
					if (flag)
					{
						EndCharacterSession("active character death/deletion");
					}
				}
			}
			catch (Exception ex)
			{
				Warn("Character-removal lifecycle could not be inspected. Type=" + ex.GetType().Name);
			}
		}

		private static void EndProfileLocked(string reason, bool removeBonuses)
		{
			if (activeProfileKey == null)
			{
				localPlayerReady = false;
				return;
			}
			Observe();
			FlushLocked(immediate: true, shutdown: true);
			string reason2 = "No removal requested.";
			if (removeBonuses && !AttributeBonusSession.RemoveActivePersistentBonuses(out reason2))
			{
				Warn("Could not remove the outgoing profile's persistent bonuses before " + reason + "; its state is gated and the outgoing player is no longer eligible. " + reason2);
			}
			else if (removeBonuses)
			{
				ManualLogSource? obj = log;
				if (obj != null)
				{
					obj.LogInfo((object)("[Cave Crawlers Leveling][Profiles] Outgoing profile detached; " + reason2));
				}
			}
			activeProfileKey = null;
			localPlayerReady = false;
			SoloLevelingXpObserver.Progression.Restore(new ProgressionSnapshot(1, 0.0, 100.0, 0L));
			SoloLevelingStatusMenu.RestoreInvestments(new Dictionary<StatType, long>());
			AttributeBonusSession.ResetPersistentLedger();
			Schedule.Reset();
			lastObserved = SoloLevelingXpObserver.Progression.Snapshot;
			ManualLogSource? obj2 = log;
			if (obj2 != null)
			{
				obj2.LogInfo((object)("[Cave Crawlers Leveling][Profiles] Profile association ended: " + reason + ". No profile was selected automatically."));
			}
		}

		internal static IReadOnlyList<SoloLevelingProfileInfo> GetProfiles()
		{
			lock (Gate)
			{
				if (!StorageReady || accountKey == null)
				{
					return Array.Empty<SoloLevelingProfileInfo>();
				}
				return (from p in (from p in Document.Profiles.Values
						where p.AccountKey == accountKey
						orderby (!p.IsLegacy) ? 1 : 0
						select p).ThenBy<SoloLevelingSaveProfile, string>((SoloLevelingSaveProfile p) => p.Name, StringComparer.OrdinalIgnoreCase)
					select new SoloLevelingProfileInfo(p.ProfileId, p.Name, p.IsLegacy, p.Level)).ToArray();
			}
		}

		internal static bool SelectProfile(string id, out string reason)
		{
			string result = "";
			bool result2 = WithStateLock(delegate
			{
				lock (Gate)
				{
					if (!CanSelectProfile || !localPlayerReady || accountKey == null || activeProfileKey != null)
					{
						result = "A valid local character session is required, and no profile may already be active.";
						return false;
					}
					if (!TryGetProfile(id, out SoloLevelingSaveProfile profile, out string key))
					{
						result = "Profile was not found for this account.";
						return false;
					}
					if (!LoadProfile(profile, key, out result))
					{
						return false;
					}
					if (CommitMutation(out result))
					{
						return true;
					}
					activeProfileKey = null;
					ResetProgressionState();
					return false;
				}
			});
			reason = result;
			return result2;
		}

		internal static bool CreateAndSelectProfile(string name, out string reason)
		{
			string result = "";
			bool result2 = WithStateLock(delegate
			{
				lock (Gate)
				{
					if (!CanSelectProfile || !localPlayerReady || accountKey == null || activeProfileKey != null)
					{
						result = "A valid local character session is required before creating a profile.";
						return false;
					}
					if (!ValidName(name))
					{
						result = "Profile name must be 1–32 printable characters.";
						return false;
					}
					SoloLevelingSaveProfile soloLevelingSaveProfile = new SoloLevelingSaveProfile
					{
						AccountKey = accountKey,
						ProfileId = Guid.NewGuid().ToString("D"),
						Name = name.Trim()
					};
					string key = SoloLevelingSaveCodec.ProfileKey(accountKey, soloLevelingSaveProfile.ProfileId);
					Document.Profiles.Add(key, soloLevelingSaveProfile);
					if (!LoadProfile(soloLevelingSaveProfile, key, out result))
					{
						Document.Profiles.Remove(key);
						return false;
					}
					if (!CommitMutation(out result))
					{
						Document.Profiles.Remove(key);
						activeProfileKey = null;
						ResetProgressionState();
						return false;
					}
					return true;
				}
			});
			reason = result;
			return result2;
		}

		internal static bool CreateProfileForSwitch(string name, out string id, out string reason)
		{
			string result = "";
			string createdId = "";
			bool result2 = WithStateLock(delegate
			{
				lock (Gate)
				{
					if (profileSafetyBlocked || !ProgressionReady || accountKey == null || !ValidName(name))
					{
						result = "A valid active session and profile name are required.";
						return false;
					}
					Observe();
					FlushLocked(immediate: true, shutdown: true);
					if (Schedule.Dirty)
					{
						result = "Current profile could not be saved; profile creation cancelled.";
						return false;
					}
					SoloLevelingSaveProfile soloLevelingSaveProfile = new SoloLevelingSaveProfile
					{
						AccountKey = accountKey,
						ProfileId = Guid.NewGuid().ToString("D"),
						Name = name.Trim()
					};
					string key = SoloLevelingSaveCodec.ProfileKey(accountKey, soloLevelingSaveProfile.ProfileId);
					Document.Profiles.Add(key, soloLevelingSaveProfile);
					if (!CommitMutation(out result))
					{
						Document.Profiles.Remove(key);
						return false;
					}
					createdId = soloLevelingSaveProfile.ProfileId;
					return true;
				}
			});
			id = createdId;
			reason = result;
			return result2;
		}

		internal static bool PrepareProfileSwitch(string id, out string profileName, out string reason)
		{
			string result = "";
			string selectedName = "";
			bool result2 = WithStateLock(delegate
			{
				lock (Gate)
				{
					if (!ProgressionReady || profileSafetyBlocked || activeProfileKey == null || !TryGetProfile(id, out SoloLevelingSaveProfile profile, out string key))
					{
						result = "Profile switch is not available for the current session.";
						return false;
					}
					if (key == activeProfileKey)
					{
						result = "That profile is already active.";
						return false;
					}
					Observe();
					FlushLocked(immediate: true, shutdown: true);
					if (Schedule.Dirty)
					{
						result = "Current profile could not be saved; switch confirmation was not opened.";
						return false;
					}
					selectedName = profile.Name;
					result = "Current profile saved. Confirm or cancel the switch.";
					return true;
				}
			});
			profileName = selectedName;
			reason = result;
			return result2;
		}

		internal static bool RenameProfile(string id, string name, out string reason)
		{
			string result = "";
			bool result2 = WithStateLock(delegate
			{
				lock (Gate)
				{
					if (!StorageReady || !ValidName(name) || !TryGetProfile(id, out SoloLevelingSaveProfile profile, out string _))
					{
						result = "Profile could not be renamed; check selection and name.";
						return false;
					}
					if (ProgressionReady)
					{
						Observe();
						FlushLocked(immediate: true, shutdown: true);
						if (Schedule.Dirty)
						{
							result = "Current profile could not be saved; rename cancelled.";
							return false;
						}
					}
					string name2 = profile.Name;
					profile.Name = name.Trim();
					if (!CommitMutation(out result))
					{
						profile.Name = name2;
						return false;
					}
					return true;
				}
			});
			reason = result;
			return result2;
		}

		internal static bool DeleteProfile(string id, out string reason)
		{
			string result = "";
			bool result2 = WithStateLock(delegate
			{
				lock (Gate)
				{
					if (!StorageReady || repository == null || !TryGetProfile(id, out SoloLevelingSaveProfile profile, out string key))
					{
						result = "Profile could not be deleted; storage or profile is unavailable.";
						return false;
					}
					bool flag = key == activeProfileKey;
					if (flag && (!ProgressionReady || !AttributeBonusSession.TryReadyPlayer(out Player _, out PlayerIdentity _, out result)))
					{
						result = "Active profile deletion cancelled because the local player is not in a safe ready state. " + result;
						return false;
					}
					if (ProfileSelected)
					{
						Observe();
						FlushLocked(immediate: true, shutdown: true);
						if (Schedule.Dirty)
						{
							result = "Profile deletion cancelled because the current progression could not be saved.";
							return false;
						}
					}
					if (!Document.Profiles.TryGetValue(key, out SoloLevelingSaveProfile value))
					{
						result = "Profile disappeared before deletion could be committed.";
						return false;
					}
					profile = value;
					try
					{
						repository.CreateVerifiedProfileDeletionBackup();
					}
					catch (Exception ex)
					{
						result = "Profile deletion cancelled because a verified recovery backup could not be created. Type=" + ex.GetType().Name;
						Warn(result);
						return false;
					}
					if (flag && !AttributeBonusSession.RemoveActivePersistentBonuses(out result))
					{
						if (result.Contains("could not be fully confirmed", StringComparison.OrdinalIgnoreCase))
						{
							profileSafetyBlocked = true;
							localPlayerReady = false;
							Warn("Profile deletion safety-locked after an unconfirmed persistent-bonus rollback; restart required.");
						}
						result = "Profile deletion cancelled: " + result;
						return false;
					}
					Document.Profiles.Remove(key);
					try
					{
						repository.Save();
					}
					catch (Exception ex2)
					{
						Document.Profiles[key] = profile;
						result = "Profile deletion was not committed; the profile was retained. Type=" + ex2.GetType().Name;
						if (flag && !AttributeBonusSession.RestoreActivePersistentBonuses(out string reason2))
						{
							profileSafetyBlocked = true;
							localPlayerReady = false;
							result = result + "; persistent bonus recovery could not be confirmed, progression locked. " + reason2;
						}
						Warn(result);
						return false;
					}
					try
					{
						repository.RefreshRecoveryBackupFromCommittedState();
					}
					catch (Exception ex3)
					{
						Document.Profiles[key] = profile;
						try
						{
							repository.Save();
							repository.RefreshRecoveryBackupFromCommittedState();
							result = "Profile deletion cancelled; the profile and verified recovery state were restored. Type=" + ex3.GetType().Name;
							if (flag && !AttributeBonusSession.RestoreActivePersistentBonuses(out string reason3))
							{
								profileSafetyBlocked = true;
								localPlayerReady = false;
								result = result + "; persistent bonus recovery could not be confirmed, progression locked. " + reason3;
							}
							Warn(result);
						}
						catch (Exception ex4)
						{
							profileSafetyBlocked = true;
							localPlayerReady = false;
							result = "Profile deletion recovery could not be fully committed. Progression is locked; the verified pre-deletion archive was retained. SaveError=" + ex3.GetType().Name + "; RecoveryError=" + ex4.GetType().Name;
							Warn(result);
						}
						return false;
					}
					if (flag)
					{
						activeProfileKey = null;
						ResetProgressionState();
						SoloLevelingProfileMenu.ResetForNewCharacter();
					}
					result = (flag ? "Profile permanently deleted from the Cave Crawlers Leveling profile list. Persistent profile bonuses were removed; choose a profile to continue." : "Profile permanently deleted from the Cave Crawlers Leveling profile list.");
					ManualLogSource? obj = log;
					if (obj != null)
					{
						obj.LogInfo((object)"[Cave Crawlers Leveling][Profiles] Explicit profile deletion committed after a verified recovery backup.");
					}
					return true;
				}
			});
			reason = result;
			return result2;
		}

		internal static bool ConfirmSwitchProfile(string id, out string reason)
		{
			string result = "";
			bool result2 = WithStateLock(delegate
			{
				lock (Gate)
				{
					if (profileSafetyBlocked || !ProgressionReady || activeProfileKey == null || !TryGetProfile(id, out SoloLevelingSaveProfile profile, out string key))
					{
						result = "Profile switch is not available for the current session.";
						return false;
					}
					if (key == activeProfileKey)
					{
						result = "That profile is already active.";
						return true;
					}
					Observe();
					FlushLocked(immediate: true, shutdown: true);
					if (Schedule.Dirty)
					{
						result = "Current profile could not be saved; switch cancelled.";
						return false;
					}
					if (!AttributeBonusSession.RemoveActivePersistentBonuses(out result))
					{
						if (result.Contains("could not be fully confirmed", StringComparison.OrdinalIgnoreCase))
						{
							profileSafetyBlocked = true;
							localPlayerReady = false;
							Warn("Profile progression locked for safety after an unconfirmed bonus rollback; restart required.");
						}
						result = "Profile switch blocked: " + result;
						return false;
					}
					activeProfileKey = null;
					ResetProgressionState();
					if (!LoadProfile(profile, key, out result))
					{
						return false;
					}
					ManualLogSource? obj = log;
					if (obj != null)
					{
						obj.LogInfo((object)"[Cave Crawlers Leveling][Profiles] Explicit profile switch confirmed; prior profile saved and its persistent bonuses removed.");
					}
					return true;
				}
			});
			reason = result;
			return result2;
		}

		private static bool LoadProfile(SoloLevelingSaveProfile profile, string key, out string reason)
		{
			//IL_00c1: Unknown result type (might be due to invalid IL or missing references)
			//IL_00c7: Expected O, but got Unknown
			try
			{
				SoloLevelingSaveCodec.Validate(profile);
				Dictionary<StatType, long> dictionary = profile.Investments.Where<KeyValuePair<string, long>>((KeyValuePair<string, long> p) => p.Value > 0).ToDictionary((KeyValuePair<string, long> p) => Enum.Parse<StatType>(p.Key), (KeyValuePair<string, long> p) => p.Value);
				SoloLevelingStatusMenu.RestoreInvestments(dictionary);
				AttributeBonusSession.RestoreInvestments(dictionary);
				SoloLevelingXpObserver.Progression.Restore(profile.Snapshot);
				activeProfileKey = key;
				lastObserved = profile.Snapshot;
				Schedule.Reset();
				reason = "Profile selected; XP, level, points and investments restored.";
				ManualLogSource val = log;
				if (val != null)
				{
					bool flag = default(bool);
					BepInExInfoLogInterpolatedStringHandler val2 = new BepInExInfoLogInterpolatedStringHandler(88, 4, ref flag);
					if (flag)
					{
						((BepInExLogInterpolatedStringHandler)val2).AppendLiteral("[Cave Crawlers Leveling][Profiles] Explicitly selected profile '");
						((BepInExLogInterpolatedStringHandler)val2).AppendFormatted<string>(profile.Name);
						((BepInExLogInterpolatedStringHandler)val2).AppendLiteral("'; level ");
						((BepInExLogInterpolatedStringHandler)val2).AppendFormatted<int>(profile.Level);
						((BepInExLogInterpolatedStringHandler)val2).AppendLiteral("; XP ");
						((BepInExLogInterpolatedStringHandler)val2).AppendFormatted<double>(profile.Xp, "0.##");
						((BepInExLogInterpolatedStringHandler)val2).AppendLiteral("; points ");
						((BepInExLogInterpolatedStringHandler)val2).AppendFormatted<long>(profile.FreePoints);
						((BepInExLogInterpolatedStringHandler)val2).AppendLiteral(".");
					}
					val.LogInfo(val2);
				}
				return true;
			}
			catch (Exception ex)
			{
				reason = "Profile could not be loaded safely: " + ex.GetType().Name;
				return false;
			}
		}

		private static void ResetProgressionState()
		{
			SoloLevelingXpObserver.Progression.Restore(new ProgressionSnapshot(1, 0.0, 100.0, 0L));
			SoloLevelingStatusMenu.RestoreInvestments(new Dictionary<StatType, long>());
			AttributeBonusSession.ResetPersistentLedger();
			Schedule.Reset();
			lastObserved = SoloLevelingXpObserver.Progression.Snapshot;
		}

		private static bool TryGetProfile(string id, out SoloLevelingSaveProfile profile, out string key)
		{
			key = ((accountKey == null) ? "" : SoloLevelingSaveCodec.ProfileKey(accountKey, id));
			if (accountKey != null && Guid.TryParseExact(id, "D", out var _) && Document.Profiles.TryGetValue(key, out SoloLevelingSaveProfile value))
			{
				profile = value;
				return true;
			}
			profile = null;
			return false;
		}

		internal static void Tick()
		{
			WithStateLock(TickCore);
		}

		private static void TickCore()
		{
			if (!initialized || stopped)
			{
				return;
			}
			try
			{
				EnsureStorageForData(MainPlayerData.Instance);
			}
			catch (Exception ex)
			{
				Warn("Local account unavailable. Type=" + ex.GetType().Name);
			}
			lock (Gate)
			{
				if (ProgressionReady)
				{
					Observe();
					FlushLocked();
				}
			}
		}

		private static void Observe()
		{
			ProgressionSnapshot snapshot = SoloLevelingXpObserver.Progression.Snapshot;
			if (snapshot != lastObserved)
			{
				lastObserved = snapshot;
				Schedule.Mark(Now);
			}
		}

		internal static void InvestmentConfirmed()
		{
			WithStateLock(delegate
			{
				lock (Gate)
				{
					if (ProgressionReady && !stopped)
					{
						Observe();
						FlushLocked(immediate: true);
					}
				}
			});
		}

		internal static void Flush()
		{
			WithStateLock(delegate
			{
				lock (Gate)
				{
					if (ProfileSelected)
					{
						Observe();
						FlushLocked(immediate: true, shutdown: true);
					}
				}
			});
		}

		internal static void Shutdown()
		{
			WithStateLock(delegate
			{
				lock (Gate)
				{
					if (!stopped)
					{
						if (ProfileSelected)
						{
							Observe();
							FlushLocked(immediate: true, shutdown: true);
						}
						stopped = true;
						repository?.Dispose();
					}
				}
			});
		}

		private static bool CommitMutation(out string reason)
		{
			if (repository == null)
			{
				reason = "Profile exists in memory only because storage is unavailable.";
				Warn(reason);
				return false;
			}
			try
			{
				repository.Save();
				reason = "Saved.";
				return true;
			}
			catch (Exception ex)
			{
				reason = "Save failed; original/backup retained: " + ex.GetType().Name;
				Warn(reason);
				return false;
			}
		}

		private static void FlushLocked(bool immediate = false, bool shutdown = false)
		{
			if (repository == null || activeProfileKey == null || !Document.Profiles.TryGetValue(activeProfileKey, out SoloLevelingSaveProfile target))
			{
				return;
			}
			try
			{
				Schedule.Flush(Now, delegate
				{
					SoloLevelingSaveProfile soloLevelingSaveProfile = SoloLevelingXpObserver.Progression.Read(delegate(ProgressionSnapshot snapshot)
					{
						SoloLevelingSaveProfile soloLevelingSaveProfile2 = target.Copy();
						soloLevelingSaveProfile2.Level = snapshot.Level;
						soloLevelingSaveProfile2.Xp = snapshot.Xp;
						soloLevelingSaveProfile2.FreePoints = snapshot.FreePoints;
						soloLevelingSaveProfile2.Investments = SoloLevelingStatusMenu.ExportInvestments();
						return soloLevelingSaveProfile2;
					});
					SoloLevelingSaveCodec.Validate(soloLevelingSaveProfile);
					Document.Profiles[activeProfileKey] = soloLevelingSaveProfile;
					repository.Save();
				}, immediate, shutdown);
			}
			catch (Exception ex)
			{
				Warn("Save not completed; progress remains in memory for retry. Type=" + ex.GetType().Name);
			}
		}

		private static bool ValidName(string value)
		{
			if (!string.IsNullOrWhiteSpace(value) && value.Trim().Length <= 32)
			{
				return !value.Any(char.IsControl);
			}
			return false;
		}

		private static T WithStateLock<T>(Func<T> action)
		{
			return SoloLevelingXpObserver.Progression.Read((ProgressionSnapshot _) => action());
		}

		private static void WithStateLock(Action action)
		{
			SoloLevelingXpObserver.Progression.Read(delegate
			{
				action();
				return 0;
			});
		}

		private static void Warn(string text)
		{
			lock (Warnings)
			{
				if (!Warnings.Add(text))
				{
					return;
				}
				try
				{
					ManualLogSource? obj = log;
					if (obj != null)
					{
						obj.LogWarning((object)("[Cave Crawlers Leveling][Profiles] " + text));
					}
				}
				catch
				{
				}
			}
		}
	}
	internal static class SoloLevelingXpHud
	{
		private static readonly HashSet<string> loggedFailures = new HashSet<string>();

		private static PlayerIdentity? suspendedPlayer;

		private static int suspendedWidth;

		private static int suspendedHeight;

		internal static void Draw(int controllerId)
		{
			//IL_03ca: Unknown result type (might be due to invalid IL or missing references)
			//IL_0019: Unknown result type (might be due to invalid IL or missing references)
			//IL_003b: Unknown result type (might be due to invalid IL or missing references)
			//IL_0041: Invalid comparison between Unknown and I4
			//IL_01d1: Unknown result type (might be due to invalid IL or missing references)
			//IL_01d6: Unknown result type (might be due to invalid IL or missing references)
			//IL_01fb: Unknown result type (might be due to invalid IL or missing references)
			//IL_023c: Unknown result type (might be due to invalid IL or missing references)
			//IL_02bd: Unknown result type (might be due to invalid IL or missing references)
			//IL_0320: Unknown result type (might be due to invalid IL or missing references)
			//IL_0357: Unknown result type (might be due to invalid IL or missing references)
			//IL_038d: Unknown result type (might be due to invalid IL or missing references)
			if (!AttributeBonusSession.IsPrimaryController(controllerId) || SoloLevelingProfileMenu.IsBlocking)
			{
				return;
			}
			string stage = "Event.current / Event.type";
			Color color = default(Color);
			bool flag = false;
			PlayerIdentity? playerIdentity = null;
			int num = 0;
			int num2 = 0;
			try
			{
				if (Event.current == null || (int)Event.current.type != 7)
				{
					return;
				}
				stage = "GameManagerCC.Instance / validação de partida";
				GameManagerCC instance = Singleton<GameManagerCC>.Instance;
				if (instance == null || ((Il2CppObjectBase)instance).Pointer == IntPtr.Zero || !Object.op_Implicit((Object)(object)instance) || instance.GameOver || instance.LeavingGame || !GameManagerCC.IsGenerationComplete)
				{
					return;
				}
				stage = "GUIManager.expertiseTree.Panel.IsOpen";
				GUIManager instance2 = Singleton<GUIManager>.Instance;
				if (instance2 != null && ((Il2CppObjectBase)instance2).Pointer != IntPtr.Zero && Object.op_Implicit((Object)(object)instance2))
				{
					TalentTree expertiseTree = instance2.expertiseTree;
					if (expertiseTree != null && ((Il2CppObjectBase)expertiseTree).Pointer != IntPtr.Zero && Object.op_Implicit((Object)(object)expertiseTree))
					{
						Panel panel = expertiseTree.Panel;
						if (panel != null && ((Il2CppObjectBase)panel).Pointer != IntPtr.Zero && Object.op_Implicit((Object)(object)panel) && panel.IsOpen)
						{
							return;
						}
					}
				}
				stage = "TryReadyPlayer: identidade local viva e pronta";
				if (!AttributeBonusSession.TryReadyPlayer(out Player _, out PlayerIdentity identity, out string _))
				{
					return;
				}
				playerIdentity = identity;
				stage = "Screen.width / Screen.height / Progression.Snapshot";
				num = Screen.width;
				num2 = Screen.height;
				PlayerIdentity? playerIdentity2 = suspendedPlayer;
				PlayerIdentity playerIdentity3 = identity;
				if (playerIdentity2.HasValue && (!playerIdentity2.HasValue || playerIdentity2.GetValueOrDefault() == playerIdentity3) && suspendedWidth == num && suspendedHeight == num2)
				{
					return;
				}
				ProgressionSnapshot snapshot = SoloLevelingXpObserver.Progression.Snapshot;
				SoloLevelingHudObstacle? obstacle = SoloLevelingHudObstacles.HealthBar(instance2, num2);
				if (SoloLevelingHudLayout.TryCreate(num, num2, snapshot, out var layout, obstacle))
				{
					stage = "GUI.color.get";
					color = GUI.color;
					flag = true;
					stage = "GUI.Box(Rect,string): fundo HUD";
					GUI.Box(new Rect(layout.X, layout.Y, layout.Width, layout.Height), "");
					stage = "GUI.Label(Rect,string): nível/pontos";
					GUI.Label(new Rect(layout.X + 8f, layout.Y + 3f, layout.Width - 16f, 22f), $"Cave Crawlers Leveling   Lv. {snapshot.Level}   |   Points: {snapshot.FreePoints}");
					stage = "GUI.Label(Rect,string): XP independente";
					GUI.Label(new Rect(layout.X + 8f, layout.Y + 23f, layout.Width - 16f, 20f), "XP " + SoloLevelingUiText.Number(snapshot.Xp) + " / " + SoloLevelingUiText.Number(snapshot.RequiredXp));
					stage = "GUI.Box(Rect,string): trilho XP";
					GUI.Box(new Rect(layout.X + 8f, layout.Y + 46f, layout.Width - 16f, 8f), "");
					if (layout.FillWidth > 0f)
					{
						stage = "GUI.color.set: preenchimento XP";
						GUI.color = new Color(0.25f, 0.85f, 1f, 1f);
						stage = "GUI.Box(Rect,string): preenchimento proporcional XP";
						GUI.Box(new Rect(layout.X + 8f, layout.Y + 46f, layout.FillWidth, 8f), "");
					}
				}
			}
			catch (Exception error)
			{
				LogFailure(stage, error);
				if (playerIdentity.HasValue)
				{
					suspendedPlayer = playerIdentity;
					suspendedWidth = num;
					suspendedHeight = num2;
				}
			}
			finally
			{
				if (flag)
				{
					try
					{
						GUI.color = color;
					}
					catch (Exception error2)
					{
						LogFailure("finally: GUI.color.set", error2);
					}
				}
			}
		}

		private static void LogFailure(string stage, Exception error)
		{
			string text = error.ToString();
			if (loggedFailures.Count < 16 && loggedFailures.Add(stage + "\n" + text))
			{
				AttributeBonusSession.LogHudException(stage, text);
			}
		}
	}
	internal readonly record struct SoloLevelingHudObstacle(float X, float Y, float Width, float Height);
	internal readonly record struct SoloLevelingHudLayout(float X, float Y, float Width, float Height, float FillWidth)
	{
		internal static bool TryCreate(int screenWidth, int screenHeight, ProgressionSnapshot xp, out SoloLevelingHudLayout layout, SoloLevelingHudObstacle? obstacle = null)
		{
			layout = default(SoloLevelingHudLayout);
			if (screenWidth < 120 || screenHeight < 120)
			{
				return false;
			}
			float num = Math.Min(420, screenWidth - 24);
			double num2 = ((double.IsFinite(xp.Xp) && double.IsFinite(xp.RequiredXp) && xp.RequiredXp > 0.0) ? Math.Clamp(xp.Xp / xp.RequiredXp, 0.0, 1.0) : 0.0);
			float num3 = screenHeight - 80;
			if (obstacle.HasValue)
			{
				SoloLevelingHudObstacle valueOrDefault = obstacle.GetValueOrDefault();
				if (float.IsFinite(valueOrDefault.X) && float.IsFinite(valueOrDefault.Y) && float.IsFinite(valueOrDefault.Width) && float.IsFinite(valueOrDefault.Height) && valueOrDefault.Width > 0f && valueOrDefault.Height > 0f && valueOrDefault.X < 12f + num && valueOrDefault.X + valueOrDefault.Width > 12f && valueOrDefault.Y < num3 + 60f && valueOrDefault.Y + valueOrDefault.Height > num3)
				{
					num3 = valueOrDefault.Y - 68f;
					if (num3 < 12f)
					{
						return false;
					}
				}
			}
			layout = new SoloLevelingHudLayout(12f, num3, num, 60f, (num - 16f) * (float)num2);
			return true;
		}
	}
	internal static class SoloLevelingStatusMenu
	{
		private readonly record struct StatOption(StatType Stat, float Step, string Unit)
		{
			[CompilerGenerated]
			public void Deconstruct(out StatType Stat, out float Step, out string Unit)
			{
				//IL_0002: Unknown result type (might be due to invalid IL or missing references)
				//IL_0008: Expected I4, but got Unknown
				Stat = (StatType)(int)this.Stat;
				Step = this.Step;
				Unit = this.Unit;
			}
		}

		private static readonly StatOption[] Options = new StatOption[12]
		{
			new StatOption((StatType)3, SoloLevelingStatRules.Steps["ExtraDamage"], "flat damage (fractional storage)"),
			new StatOption((StatType)14, SoloLevelingStatRules.Steps["DamageMultiplier"], "pp; value/100"),
			new StatOption((StatType)12, SoloLevelingStatRules.Steps["CriticalChance"], "chance percentage points"),
			new StatOption((StatType)13, SoloLevelingStatRules.Steps["CriticalDamage"], "pp; value/100"),
			new StatOption((StatType)6, SoloLevelingStatRules.Steps["AttackSpeed"], "rating; +1.5% to speed factor"),
			new StatOption((StatType)9, SoloLevelingStatRules.Steps["WeaponRange"], "absolute distance"),
			new StatOption((StatType)7, SoloLevelingStatRules.Steps["Knockback"], "knockback magnitude; consumer x0.65"),
			new StatOption((StatType)26, SoloLevelingStatRules.Steps["DodgeChance"], "dodge percentage points"),
			new StatOption((StatType)4, SoloLevelingStatRules.Steps["BlockChance"], "block percentage points"),
			new StatOption((StatType)5, SoloLevelingStatRules.Steps["MoveSpeed"], "pp; 1+value/100"),
			new StatOption((StatType)19, SoloLevelingStatRules.Steps["SpellHaste"], "cooldown reduction percentage points"),
			new StatOption((StatType)10, SoloLevelingStatRules.Steps["MaxTorchDuration"], "absolute duration units")
		};

		private static AttributeInvestment<StatType> Investments = new AttributeInvestment<StatType>();

		private static bool open;

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

		private static int lastFrame = -1;

		private static int lastPurchaseFrame = -1;

		private static int page;

		private const int PageSize = 6;

		private static CursorLockMode previousLock;

		private static bool previousVisible;

		private static string message = "Account-wide progression. All XP sources; investments autosave.";

		private static int PageCount => (Options.Length + 6 - 1) / 6;

		internal static void NotifyLegacyRefund(long points)
		{
			message = $"Legacy Dexterity removed. Refunded points: {points}. Other investment counts are unchanged.";
		}

		internal static Dictionary<string, long> ExportInvestments()
		{
			return Investments.Export().ToDictionary<KeyValuePair<StatType, long>, string, long>((KeyValuePair<StatType, long> p) => ((object)p.Key/*cast due to .constrained prefix*/).ToString(), (KeyValuePair<StatType, long> p) => p.Value, StringComparer.Ordinal);
		}

		internal static void RestoreInvestments(IReadOnlyDictionary<StatType, long> saved)
		{
			Investments = new AttributeInvestment<StatType>();
			Investments.Restore(saved);
		}

		internal static void Tick(int id)
		{
			if (AttributeBonusSession.IsPrimaryController(id) && lastFrame != Time.frameCount)
			{
				lastFrame = Time.frameCount;
				if (Input.GetKeyDown((KeyCode)290) && SoloLevelingPersistence.ProgressionReady && !SoloLevelingProfileMenu.IsBlocking)
				{
					SetOpen(!open);
				}
			}
		}

		private static void SetOpen(bool value)
		{
			//IL_0041: Unknown result type (might be due to invalid IL or missing references)
			//IL_0020: Unknown result type (might be due to invalid IL or missing references)
			//IL_0025: Unknown result type (might be due to invalid IL or missing references)
			//IL_004f: Unknown result type (might be due to invalid IL or missing references)
			if (open != value)
			{
				open = value;
				if (open)
				{
					loggedFailures.Clear();
					previousLock = Cursor.lockState;
					previousVisible = Cursor.visible;
					Cursor.lockState = (CursorLockMode)0;
					Cursor.visible = true;
				}
				else if ((int)Cursor.lockState == 0 && Cursor.visible)
				{
					Cursor.lockState = previousLock;
					Cursor.visible = previousVisible;
				}
			}
		}

		internal static void Detach(int id)
		{
			if (AttributeBonusSession.IsPrimaryController(id))
			{
				SetOpen(value: false);
			}
		}

		private static bool CanInvest(StatOption option, out float current, out string detail, out string reason)
		{
			//IL_0029: Unknown result type (might be due to invalid IL or missing references)
			//IL_0037: Unknown result type (might be due to invalid IL or missing references)
			//IL_00ef: Unknown result type (might be due to invalid IL or missing references)
			//IL_00f6: Invalid comparison between Unknown and I4
			//IL_010c: Unknown result type (might be due to invalid IL or missing references)
			//IL_011a: Unknown result type (might be due to invalid IL or missing references)
			//IL_0120: Invalid comparison between Unknown and I4
			//IL_012f: Unknown result type (might be due to invalid IL or missing references)
			//IL_023b: Unknown result type (might be due to invalid IL or missing references)
			//IL_0242: Invalid comparison between Unknown and I4
			//IL_025f: Unknown result type (might be due to invalid IL or missing references)
			//IL_0266: Invalid comparison between Unknown and I4
			//IL_026a: Unknown result type (might be due to invalid IL or missing references)
			//IL_0270: Invalid comparison between Unknown and I4
			//IL_0274: Unknown result type (might be due to invalid IL or missing references)
			//IL_027b: Invalid comparison between Unknown and I4
			current = float.NaN;
			detail = option.Unit;
			if (!AttributeBonusSession.TryReadyPlayer(out Player player, out PlayerIdentity _, out reason))
			{
				detail = "waiting for a ready local player";
				return false;
			}
			current = ((Entity)player).GetStat(option.Stat, true);
			StatTypeInfo val = default(StatTypeInfo);
			if (!StatInfo.TryGetStatInfo(option.Stat, ref val) || val == null)
			{
				reason = (detail = "Stat configuration is unavailable.");
				return false;
			}
			detail = option.Unit + "; base " + SoloLevelingUiText.Number(val.BaseAmount) + (val.UseMax ? ("; max. " + SoloLevelingUiText.Number(val.maxValue)) : "") + (val.UseMin ? ("; min. " + SoloLevelingUiText.Number(val.minValue)) : "");
			if (option.Step <= 0f)
			{
				reason = "Investment disabled: scale not confirmed.";
				return false;
			}
			if ((int)option.Stat == 9 && val.isInteger)
			{
				reason = "The 0.1 step is incompatible with integer rounding; purchase blocked.";
				return false;
			}
			float num = ((Entity)player).GetStat(option.Stat, false);
			if ((int)option.Stat == 3)
			{
				float num2 = default(float);
				if (!((Entity)player).entityStats.stats.TryGetValue(option.Stat, ref num2))
				{
					reason = "Extra Damage is unavailable in raw storage.";
					return false;
				}
				num = num2 + val.BaseAmount;
			}
			if (!AttributeInvestment<StatType>.WithinLimits(num, option.Step, val.UseMin, val.minValue, val.UseMax, val.maxValue))
			{
				reason = $"Stat limit or invalid value: unclamped={num}; step={option.Step}; UseMin={val.UseMin}; min={val.minValue}; UseMax={val.UseMax}; max={val.maxValue}.";
				return false;
			}
			if ((int)option.Stat == 12 && num + option.Step > 100f)
			{
				reason = "Critical Chance would exceed 100%.";
				return false;
			}
			if (((int)option.Stat == 26 || (int)option.Stat == 4 || (int)option.Stat == 19) && num + option.Step > 100f)
			{
				reason = "The next step would exceed 100 percentage points; purchase blocked.";
				return false;
			}
			if (SoloLevelingXpObserver.Progression.Snapshot.FreePoints <= 0)
			{
				reason = "No attribute points available (requires 1).";
				return false;
			}
			reason = $"Available; {reason}; unclamped={num}; step={option.Step}.";
			return true;
		}

		internal static void Draw(int id)
		{
			//IL_0034: Unknown result type (might be due to invalid IL or missing references)
			//IL_0039: Unknown result type (might be due to invalid IL or missing references)
			//IL_00bd: Unknown result type (might be due to invalid IL or missing references)
			//IL_00f8: Unknown result type (might be due to invalid IL or missing references)
			//IL_01a2: Unknown result type (might be due to invalid IL or missing references)
			//IL_01db: Unknown result type (might be due to invalid IL or missing references)
			//IL_0261: Unknown result type (might be due to invalid IL or missing references)
			//IL_02d7: Unknown result type (might be due to invalid IL or missing references)
			//IL_031a: Unknown result type (might be due to invalid IL or missing references)
			//IL_0349: Unknown result type (might be due to invalid IL or missing references)
			//IL_0361: Unknown result type (might be due to invalid IL or missing references)
			//IL_0366: Unknown result type (might be due to invalid IL or missing references)
			//IL_03c4: Unknown result type (might be due to invalid IL or missing references)
			//IL_03fe: Unknown result type (might be due to invalid IL or missing references)
			//IL_0433: Unknown result type (might be due to invalid IL or missing references)
			//IL_0757: Unknown result type (might be due to invalid IL or missing references)
			//IL_0472: Unknown result type (might be due to invalid IL or missing references)
			//IL_07b9: Unknown result type (might be due to invalid IL or missing references)
			//IL_04cc: Unknown result type (might be due to invalid IL or missing references)
			//IL_04f9: Unknown result type (might be due to invalid IL or missing references)
			//IL_080e: Unknown result type (might be due to invalid IL or missing references)
			//IL_086f: Unknown result type (might be due to invalid IL or missing references)
			//IL_087d: Unknown result type (might be due to invalid IL or missing references)
			//IL_0567: Unknown result type (might be due to invalid IL or missing references)
			//IL_0886: Unknown result type (might be due to invalid IL or missing references)
			//IL_088c: Invalid comparison between Unknown and I4
			//IL_0890: Unknown result type (might be due to invalid IL or missing references)
			//IL_0896: Invalid comparison between Unknown and I4
			//IL_05d2: Unknown result type (might be due to invalid IL or missing references)
			//IL_089a: Unknown result type (might be due to invalid IL or missing references)
			//IL_08a0: Invalid comparison between Unknown and I4
			//IL_0623: Unknown result type (might be due to invalid IL or missing references)
			//IL_0634: Unknown result type (might be due to invalid IL or missing references)
			//IL_06a2: Unknown result type (might be due to invalid IL or missing references)
			//IL_06a7: Unknown result type (might be due to invalid IL or missing references)
			//IL_0663: Unknown result type (might be due to invalid IL or missing references)
			//IL_0668: Unknown result type (might be due to invalid IL or missing references)
			if (!open || !SoloLevelingPersistence.ProgressionReady || !AttributeBonusSession.IsPrimaryController(id))
			{
				return;
			}
			bool flag = true;
			bool flag2 = false;
			string stage = "Event.current / Event.type";
			string eventType = "not obtained";
			try
			{
				Event current = Event.current;
				eventType = ((current == null) ? "null" : ((object)current.type/*cast due to .constrained prefix*/).ToString());
				stage = "GUI.enabled.get";
				flag = GUI.enabled;
				flag2 = true;
				stage = "Screen.width / Screen.height";
				float num = Math.Min(760, Math.Max(280, Screen.width - 24));
				float num2 = Math.Min(650, Math.Max(220, Screen.height - 24));
				Rect val = default(Rect);
				((Rect)(ref val))..ctor(12f, 12f, num, num2);
				stage = "GUI.Box(Rect,string): painel";
				GUI.Box(val, "Cave Crawlers Leveling — Status (F9 to close)");
				stage = "Progression.Snapshot";
				ProgressionSnapshot snapshot = SoloLevelingXpObserver.Progression.Snapshot;
				stage = "GUI.Label(Rect,string): nível/XP/pontos";
				GUI.Label(new Rect(26f, 42f, num - 150f, 28f), $"Hunter Lv. {snapshot.Level}     XP {SoloLevelingUiText.Number(snapshot.Xp)} / {SoloLevelingUiText.Number(snapshot.RequiredXp)}     Points: {snapshot.FreePoints}");
				stage = "GUI.Button(Rect,string): Change Profile";
				if (GUI.Button(new Rect(num - 124f, 42f, 96f, 26f), "Change Profile"))
				{
					SetOpen(value: false);
					SoloLevelingProfileMenu.OpenManualSwitch();
				}
				stage = "GUI.Label(Rect,string): mensagem";
				GUI.Label(new Rect(26f, 70f, num - 28f, 34f), message);
				int num3 = page * 6;
				int num4 = Math.Min(num3 + 6, Options.Length);
				float num5 = Math.Min(62f, (num2 - 166f) / 6f);
				for (int i = num3; i < num4; i++)
				{
					StatOption option = Options[i];
					stage = $"CanInvest: {option.Stat} (leitura de jogador/atributo/configuração)";
					float current2;
					string detail;
					string reason;
					bool flag3 = CanInvest(option, out current2, out detail, out reason);
					float num6 = 108f + (float)(i - num3) * num5;
					float num7 = Math.Min(26f, num5 / 2f);
					stage = $"GUI.enabled.set: {option.Stat}, rótulos";
					GUI.enabled = flag;
					stage = $"GUI.Label(Rect,string): {option.Stat}, valor/investidos";
					GUI.Label(new Rect(26f, num6, num - 140f, num7), $"{SoloLevelingUiText.Stat(((object)option.Stat/*cast due to .constrained prefix*/).ToString())}: {(float.IsFinite(current2) ? SoloLevelingUiText.Number(current2) : "—")}    Invested: {Investments.Points(option.Stat)}");
					stage = $"GUI.Label(Rect,string): {option.Stat}, unidade/limites";
					GUI.Label(new Rect(26f, num6 + num7, num - 42f, num5 - num7), flag3 ? detail : (detail + " | " + reason));
					stage = $"GUI.enabled.set: {option.Stat}, botão";
					GUI.enabled = flag && flag3 && lastPurchaseFrame != Time.frameCount;
					stage = $"GUI.Button(Rect,string): {option.Stat}";
					if (GUI.Button(new Rect(num - 100f, num6, 70f, Math.Min(26f, num5)), (option.Step > 0f) ? ("+" + SoloLevelingUiText.Number(option.Step)) : "+"))
					{
						lastPurchaseFrame = Time.frameCount;
						stage = $"Investimento: {option.Stat}, revalidação/aplicação/registro";
						float current3;
						string detail2;
						string attemptReason;
						bool num8 = CanInvest(option, out current3, out detail2, out attemptReason);
						Player player;
						PlayerIdentity purchaseIdentity;
						string reason2;
						bool flag4 = AttributeBonusSession.TryReadyPlayer(out player, out purchaseIdentity, out reason2);
						if (!flag4)
						{
							attemptReason = reason2;
						}
						bool flag5 = num8 && flag4 && Investments.TryInvest(SoloLevelingXpObserver.Progression, option.Stat, () => AttributeBonusSession.Invest(option.Stat, option.Step, purchaseIdentity, out attemptReason));
						if (num8 && !flag5 && attemptReason.StartsWith("Available;"))
						{
							attemptReason = "Point reservation refused: no points or reentrant purchase.";
						}
						if (flag5)
						{
							SoloLevelingPersistence.InvestmentConfirmed();
							AttributeBonusSession.LogInvestment(option.Stat, Investments.Points(option.Stat));
						}
						long freePoints = SoloLevelingXpObserver.Progression.Snapshot.FreePoints;
						message = (flag5 ? $"{SoloLevelingUiText.Stat(((object)option.Stat/*cast due to .constrained prefix*/).ToString())} +{SoloLevelingUiText.Number(option.Step)}: 1 point invested; available {freePoints}." : (SoloLevelingUiText.Stat(((object)option.Stat/*cast due to .constrained prefix*/).ToString()) + ": refused; " + attemptReason));
					}
				}
				stage = "GUI.enabled.set: Anterior";
				GUI.enabled = flag && page > 0;
				stage = "GUI.Button(Rect,string): Anterior";
				if (GUI.Button(new Rect(26f, num2 - 24f, 100f, 26f), "Previous"))
				{
					page = Math.Max(0, page - 1);
				}
				stage = "GUI.enabled.set: Próxima";
				GUI.enabled = flag && page < PageCount - 1;
				stage = "GUI.Button(Rect,string): Próxima";
				if (GUI.Button(new Rect(num - 112f, num2 - 24f, 100f, 26f), "Next"))
				{
					page = Math.Min(PageCount - 1, page + 1);
				}
				stage = "GUI.enabled.set: página";
				GUI.enabled = flag;
				stage = "GUI.Label(Rect,string): página";
				GUI.Label(new Rect(136f, num2 - 24f, num - 260f, 26f), $"Page {page + 1} / {PageCount}");
				stage = "Event.current / Rect.Contains / Event.mousePosition / Event.type";
				Event current4 = Event.current;
				if (current4 != null && ((Rect)(ref val)).Contains(current4.mousePosition) && ((int)current4.type == 0 || (int)current4.type == 1 || (int)current4.type == 6 || (int)current4.type == 3))
				{
					stage = "Event.Use()";
					current4.Use();
				}
			}
			catch (Exception ex)
			{
				message = "Menu unavailable: " + ex.Message;
				LogFailure(stage, eventType, id, ex);
			}
			finally
			{
				if (flag2)
				{
					try
					{
						GUI.enabled = flag;
					}
					catch (Exception error)
					{
						LogFailure("finally: GUI.enabled.set", eventType, id, error);
					}
				}
			}
		}

		private static void LogFailure(string stage, string eventType, int controllerId, Exception error)
		{
			string text = error.ToString();
			string item = stage + "\n" + eventType + "\n" + text;
			if (!loggedFailures.Contains(item) && loggedFailures.Count < 32)
			{
				loggedFailures.Add(item);
				AttributeBonusSession.LogMenuException($"Chamada={stage}; evento={eventType}; controlador={controllerId}. " + "Exceção completa (repetições iguais omitidas até reabrir F9):", text);
			}
		}
	}
	internal static class SoloLevelingProfileMenu
	{
		private static bool manualOpen;

		private static bool confirming;

		private static bool confirmingDelete;

		private static bool pendingLegacy;

		private static string pendingId = "";

		private static string pendingName = "";

		private static string createName = "Hunter Profile";

		private static string renameName = "";

		private static string message = "Select a profile to continue. Create New Profile is the default choice.";

		private static int profilePage;

		private const int ProfilesPerPage = 4;

		private static bool textEntryUnavailable;

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

		internal static bool IsBlocking
		{
			get
			{
				if (!SoloLevelingPersistence.NeedsProfileSelection)
				{
					return manualOpen;
				}
				return true;
			}
		}

		internal static void ResetForNewCharacter()
		{
			manualOpen = false;
			confirming = false;
			confirmingDelete = false;
			pendingLegacy = false;
			pendingId = (pendingName = "");
			message = "Select a profile to continue. Create New Profile is the default choice.";
		}

		internal static void OpenManualSwitch()
		{
			if (SoloLevelingPersistence.ProgressionReady)
			{
				manualOpen = true;
				confirming = false;
				confirmingDelete = false;
				pendingLegacy = false;
				pendingId = (pendingName = "");
				renameName = SoloLevelingPersistence.ActiveProfileName ?? "";
				message = "Choose a profile. The current profile stays active until you confirm.";
			}
		}

		internal static void Draw(int controllerId)
		{
			//IL_0093: Unknown result type (might be due to invalid IL or missing references)
			//IL_00c1: Unknown result type (might be due to invalid IL or missing references)
			//IL_0100: Unknown result type (might be due to invalid IL or missing references)
			//IL_0157: Unknown result type (might be due to invalid IL or missing references)
			//IL_04b3: Unknown result type (might be due to invalid IL or missing references)
			//IL_04e4: Unknown result type (might be due to invalid IL or missing references)
			//IL_02a8: Unknown result type (might be due to invalid IL or missing references)
			//IL_052a: Unknown result type (might be due to invalid IL or missing references)
			//IL_0334: Unknown result type (might be due to invalid IL or missing references)
			//IL_041a: Unknown result type (might be due to invalid IL or missing references)
			//IL_01c4: Unknown result type (might be due to invalid IL or missing references)
			//IL_0451: Unknown result type (might be due to invalid IL or missing references)
			//IL_0239: Unknown result type (might be due to invalid IL or missing references)
			//IL_0270: Unknown result type (might be due to invalid IL or missing references)
			//IL_0829: Unknown result type (might be due to invalid IL or missing references)
			//IL_0646: Unknown result type (might be due to invalid IL or missing references)
			//IL_087f: Unknown result type (might be due to invalid IL or missing references)
			//IL_08b8: Unknown result type (might be due to invalid IL or missing references)
			//IL_0aa6: Unknown result type (might be due to invalid IL or missing references)
			//IL_0937: Unknown result type (might be due to invalid IL or missing references)
			//IL_0968: Unknown result type (might be due to invalid IL or missing references)
			//IL_0ab5: Unknown result type (might be due to invalid IL or missing references)
			//IL_09d0: Unknown result type (might be due to invalid IL or missing references)
			//IL_099f: Unknown result type (might be due to invalid IL or missing references)
			//IL_0a69: Unknown result type (might be due to invalid IL or missing references)
			//IL_06e1: Unknown result type (might be due to invalid IL or missing references)
			//IL_077f: Unknown result type (might be due to invalid IL or missing references)
			if (!AttributeBonusSession.IsPrimaryController(controllerId) || !IsBlocking)
			{
				return;
			}
			string text = "Profile menu layout";
			bool flag = true;
			try
			{
				text = "GUI.enabled.get";
				flag = GUI.enabled;
				float num = Mathf.Min(660, Mathf.Max(300, Screen.width - 24));
				float num2 = Mathf.Min(640, Mathf.Max(500, Screen.height - 24));
				float num3 = ((float)Screen.width - num) / 2f;
				float num4 = ((float)Screen.height - num2) / 2f;
				Rect val = default(Rect);
				((Rect)(ref val))..ctor(num3, num4, num, num2);
				text = "GUI.Box profile picker";
				GUI.Box(val, "Cave Crawlers Leveling Profiles");
				text = "GUI.Label title";
				GUI.Label(new Rect(num3 + 16f, num4 + 34f, num - 32f, 44f), manualOpen ? "Change Profile — confirm before switching" : "Choose a profile for this character session");
				text = "GUI.Label gated status";
				GUI.Label(new Rect(num3 + 16f, num4 + 72f, num - 32f, 34f), SoloLevelingPersistence.SafetyBlocked ? "Profile switch is safety-locked after an unconfirmed stat rollback. Review logs and restart before selecting profiles." : (SoloLevelingPersistence.CanSelectProfile ? message : "Storage or character lifecycle hooks are not ready. Profile selection, XP, investments and persistent bonuses are locked."));
				if (confirmingDelete)
				{
					text = "GUI.Label delete confirmation";
					GUI.Label(new Rect(num3 + 20f, num4 + 145f, num - 40f, 76f), pendingLegacy ? ("Permanently remove '" + pendingName + "'? Its old progress will be removed from the profile list. A verified recovery backup will be created before saving. The backup is not loaded automatically; the game character is not affected.") : ("Permanently remove '" + pendingName + "' from the profile list? A verified recovery backup will be created before saving. The game character is not affected."));
					text = "GUI.Button confirm delete";
					GUI.enabled = flag && SoloLevelingPersistence.CanSelectProfile;
					if (GUI.Button(new Rect(num3 + 36f, num4 + num2 - 64f, 180f, 36f), "Confirm Delete"))
					{
						if (SoloLevelingPersistence.DeleteProfile(pendingId, out string reason))
						{
							confirmingDelete = false;
							pendingLegacy = false;
							pendingId = (pendingName = "");
							message = reason;
						}
						else
						{
							message = reason;
						}
					}
					text = "GUI.Button cancel delete";
					GUI.enabled = flag;
					if (GUI.Button(new Rect(num3 + num - 216f, num4 + num2 - 64f, 180f, 36f), "Cancel"))
					{
						confirmingDelete = false;
						pendingLegacy = false;
						pendingId = (pendingName = "");
						message = "Deletion cancelled; the profile was kept.";
					}
					ConsumePanelMouse(val);
					return;
				}
				if (confirming)
				{
					text = "GUI.Label switch confirmation";
					GUI.Label(new Rect(num3 + 20f, num4 + 145f, num - 40f, 55f), $"Switch from '{SoloLevelingPersistence.ActiveProfileName}' to '{pendingName}'? The current profile will be saved and only its recorded persistent bonuses will be removed.");
					text = "GUI.Button confirm switch";
					GUI.enabled = flag && !SoloLevelingPersistence.SafetyBlocked;
					if (GUI.Button(new Rect(num3 + 36f, num4 + num2 - 64f, 180f, 36f), "Confirm Switch"))
					{
						string reason2 = "";
						if (pendingId == "__CREATE__")
						{
							if (SoloLevelingPersistence.CreateProfileForSwitch(pendingName, out string id, out string reason3))
							{
								pendingId = id;
								if (!SoloLevelingPersistence.PrepareProfileSwitch(id, out string _, out reason2))
								{
									message = reason2;
									confirming = false;
								}
							}
							else
							{
								message = reason3;
								confirming = false;
							}
						}
						if (confirming && pendingId != "__CREATE__" && SoloLevelingPersistence.ConfirmSwitchProfile(pendingId, out reason2))
						{
							manualOpen = false;
							confirming = false;
							message = reason2;
						}
						else if (manualOpen && confirming)
						{
							message = reason2;
						}
					}
					text = "GUI.Button cancel switch";
					GUI.enabled = flag;
					if (GUI.Button(new Rect(num3 + num - 216f, num4 + num2 - 64f, 180f, 36f), "Cancel"))
					{
						confirming = false;
						manualOpen = false;
						pendingId = (pendingName = "");
						message = "Switch cancelled; current profile remains active.";
					}
					ConsumePanelMouse(val);
					return;
				}
				IReadOnlyList<SoloLevelingProfileInfo> profiles = SoloLevelingPersistence.GetProfiles();
				int num5 = Math.Max(1, (profiles.Count + 4 - 1) / 4);
				profilePage = Math.Clamp(profilePage, 0, num5 - 1);
				float num6 = num4 + 112f;
				text = "GUI.Label create profile";
				GUI.Label(new Rect(num3 + 18f, num6, num - 36f, 26f), "Create New Profile");
				text = "GUI.TextField create profile name";
				createName = SafeTextField(new Rect(num3 + 18f, num6 + 28f, num - 212f, 30f), createName);
				text = "GUI.Button create profile";
				GUI.enabled = flag && SoloLevelingPersistence.CanSelectProfile;
				if (GUI.Button(new Rect(num3 + num - 172f, num6 + 28f, 142f, 30f), "Create New Profile"))
				{
					string reason4;
					if (manualOpen)
					{
						pendingId = "__CREATE__";
						pendingName = createName.Trim();
						confirming = true;
					}
					else if (SoloLevelingPersistence.CreateAndSelectProfile(createName, out reason4))
					{
						message = reason4;
						manualOpen = false;
					}
					else
					{
						message = reason4;
					}
				}
				GUI.enabled = flag;
				float num7 = num4 + 184f;
				float num8 = 36f;
				int num9 = profilePage * 4;
				int num10 = Math.Min(4, Math.Max(0, profiles.Count - num9));
				for (int i = 0; i < num10; i++)
				{
					SoloLevelingProfileInfo soloLevelingProfileInfo = profiles[num9 + i];
					bool flag2 = num < 460f;
					float num11 = (flag2 ? 66 : 112);
					float num12 = (flag2 ? 84 : 142);
					float num13 = num3 + num - 18f - num12;
					float num14 = num13 - 8f - num11;
					text = "GUI.Label profile row";
					GUI.Label(new Rect(num3 + 18f, num7 + (float)i * num8, Mathf.Max(30f, num14 - num3 - 26f), 30f), $"{soloLevelingProfileInfo.Name}{(soloLevelingProfileInfo.IsLegacy ? " (Legacy)" : "")} — Lv. {soloLevelingProfileInfo.Level}");
					text = "GUI.Button profile selection";
					string text2 = (manualOpen ? "Switch" : "Select");
					GUI.enabled = flag && SoloLevelingPersistence.CanSelectProfile;
					if (GUI.Button(new Rect(num14, num7 + (float)i * num8, num11, 30f), text2))
					{
						string reason6;
						if (manualOpen)
						{
							if (SoloLevelingPersistence.PrepareProfileSwitch(soloLevelingProfileInfo.Id, out string profileName2, out string reason5))
							{
								pendingId = soloLevelingProfileInfo.Id;
								pendingName = profileName2;
								confirming = true;
								message = reason5;
							}
							else
							{
								message = reason5;
							}
						}
						else if (SoloLevelingPersistence.SelectProfile(soloLevelingProfileInfo.Id, out reason6))
						{
							message = reason6;
							manualOpen = false;
						}
						else
						{
							message = reason6;
						}
					}
					text = "GUI.Button delete profile";
					GUI.enabled = flag && SoloLevelingPersistence.CanSelectProfile;
					if (GUI.Button(new Rect(num13, num7 + (float)i * num8, num12, 30f), flag2 ? "Delete" : "Delete Profile"))
					{
						pendingId = soloLevelingProfileInfo.Id;
						pendingName = soloLevelingProfileInfo.Name;
						pendingLegacy = soloLevelingProfileInfo.IsLegacy;
						confirmingDelete = true;
						message = "Review the permanent deletion confirmation.";
					}
					GUI.enabled = flag;
				}
				float num15 = num7 + (float)num10 * num8 + 4f;
				GUI.enabled = flag && SoloLevelingPersistence.CanSelectProfile && profilePage > 0;
				text = "GUI.Button previous profiles";
				if (GUI.Button(new Rect(num3 + 18f, num15, 110f, 28f), "Previous"))
				{
					profilePage--;
				}
				GUI.enabled = flag && SoloLevelingPersistence.CanSelectProfile && profilePage < num5 - 1;
				text = "GUI.Button next profiles";
				if (GUI.Button(new Rect(num3 + num - 128f, num15, 110f, 28f), "Next"))
				{
					profilePage++;
				}
				GUI.enabled = flag;
				GUI.Label(new Rect(num3 + 150f, num15, num - 300f, 28f), $"Page {profilePage + 1} / {num5}");
				if (manualOpen)
				{
					num6 = num15 + 28f;
					text = "GUI.Label rename current profile";
					GUI.Label(new Rect(num3 + 18f, num6 + 68f, num - 36f, 24f), "Rename Current Profile");
					text = "GUI.TextField rename profile";
					renameName = SafeTextField(new Rect(num3 + 18f, num6 + 94f, num - 212f, 30f), renameName);
					if (textEntryUnavailable)
					{
						GUI.Label(new Rect(num3 + 18f, num6 + 126f, num - 36f, 28f), "Name editing is unavailable; the current/default name remains usable.");
					}
					text = "GUI.Button rename profile";
					if (GUI.Button(new Rect(num3 + num - 172f, num6 + 94f, 142f, 30f), "Rename"))
					{
						SoloLevelingProfileInfo soloLevelingProfileInfo2 = SoloLevelingPersistence.GetProfiles().FirstOrDefault((SoloLevelingProfileInfo p) => p.Id == SoloLevelingPersistence.ActiveProfileId);
						string reason7 = "Active profile could not be found.";
						if (soloLevelingProfileInfo2 != null && SoloLevelingPersistence.RenameProfile(soloLevelingProfileInfo2.Id, renameName, out reason7))
						{
							message = "Profile renamed.";
						}
						else
						{
							message = reason7;
						}
					}
					text = "GUI.Button close profile switch menu";
					if (GUI.Button(new Rect(num3 + num - 142f, num4 + num2 - 64f, 112f, 34f), "Close"))
					{
						manualOpen = false;
					}
				}
				else
				{
					text = "GUI.Label legacy warning";
					GUI.Label(new Rect(num3 + 18f, num4 + num2 - 68f, num - 36f, 50f), "Legacy progress is available only after you select it. Profiles are not linked to character names or slots.");
				}
				ConsumePanelMouse(val);
			}
			catch (Exception ex)
			{
				string item = text + "\n" + ex;
				if (logged.Count < 16 && logged.Add(item))
				{
					AttributeBonusSession.LogMenuException("Profile menu call=" + text, ex.ToString());
				}
				message = "Profile menu unavailable: " + ex.Message;
			}
			finally
			{
				try
				{
					GUI.enabled = flag;
				}
				catch
				{
				}
			}
		}

		private static void ConsumePanelMouse(Rect panel)
		{
			//IL_000c: Unknown result type (might be due to invalid IL or missing references)
			//IL_0019: Unknown result type (might be due to invalid IL or missing references)
			//IL_0021: Unknown result type (might be due to invalid IL or missing references)
			//IL_0027: Invalid comparison between Unknown and I4
			//IL_002a: Unknown result type (might be due to invalid IL or missing references)
			//IL_0030: Invalid comparison between Unknown and I4
			Event current = Event.current;
			if (current != null && ((Rect)(ref panel)).Contains(current.mousePosition) && ((int)current.type == 0 || (int)current.type == 1 || (int)current.type == 3))
			{
				current.Use();
			}
		}

		private static string SafeTextField(Rect rect, string value)
		{
			//IL_000a: Unknown result type (might be due to invalid IL or missing references)
			if (textEntryUnavailable)
			{
				return value;
			}
			try
			{
				return GUI.TextField(rect, value, 32);
			}
			catch (Exception ex)
			{
				textEntryUnavailable = true;
				message = "Text entry is unavailable; profile names use the current/default value.";
				if (logged.Add("GUI.TextField\n" + ex))
				{
					AttributeBonusSession.LogMenuException("Profile menu call=GUI.TextField(Rect,string,int)", ex.ToString());
				}
				return value;
			}
		}
	}
	internal sealed class AttributeInvestment<TStat> where TStat : notnull
	{
		private readonly Dictionary<TStat, long> points = new Dictionary<TStat, long>();

		internal long Points(TStat stat)
		{
			return points.GetValueOrDefault(stat);
		}

		internal bool TryInvest(SoloLevelingProgression progression, TStat stat, Func<bool> apply)
		{
			return progression.TrySpendPoint(delegate
			{
				long value = checked(Points(stat) + 1);
				bool num = apply();
				if (num)
				{
					points[stat] = value;
				}
				return num;
			});
		}

		internal Dictionary<TStat, long> Export()
		{
			return new Dictionary<TStat, long>(points);
		}

		internal void Restore(IReadOnlyDictionary<TStat, long> saved)
		{
			if (points.Count != 0 || saved.Any<KeyValuePair<TStat, long>>((KeyValuePair<TStat, long> p) => p.Value < 0))
			{
				throw new InvalidOperationException("Investment restore must occur once before purchases.");
			}
			foreach (KeyValuePair<TStat, long> item in saved)
			{
				if (item.Value > 0)
				{
					points.Add(item.Key, item.Value);
				}
			}
		}

		internal static bool WithinLimits(float value, float step, bool useMin, float min, bool useMax, float max)
		{
			float num = value + step;
			if (float.IsFinite(value) && float.IsFinite(step) && step > 0f && float.IsFinite(num) && num > value && (!useMin || (float.IsFinite(min) && value >= min)))
			{
				if (useMax)
				{
					if (float.IsFinite(max))
					{
						return num <= max;
					}
					return false;
				}
				return true;
			}
			return false;
		}
	}
	internal sealed class SoloLevelingProgression
	{
		private readonly object gate = new object();

		private int level = 1;

		private double xp;

		private double requiredXp = 100.0;

		private long freePoints;

		private bool spending;

		internal ProgressionSnapshot Snapshot
		{
			get
			{
				lock (gate)
				{
					return new ProgressionSnapshot(level, xp, requiredXp, freePoints);
				}
			}
		}

		internal bool TrySpendPoint(Func<bool> apply)
		{
			lock (gate)
			{
				if (spending || freePoints <= 0)
				{
					return false;
				}
				spending = true;
				freePoints--;
				bool flag = false;
				try
				{
					flag = apply();
					return flag;
				}
				finally
				{
					if (!flag)
					{
						freePoints++;
					}
					spending = false;
				}
			}
		}

		internal T Read<T>(Func<ProgressionSnapshot, T> capture)
		{
			lock (gate)
			{
				return capture(new ProgressionSnapshot(level, xp, requiredXp, freePoints));
			}
		}

		internal void Restore(ProgressionSnapshot state)
		{
			if (state.Level < 1 || !double.IsFinite(state.Xp) || !double.IsFinite(state.RequiredXp) || state.RequiredXp <= 0.0 || state.Xp < 0.0 || state.Xp >= state.RequiredXp || state.FreePoints < 0)
			{
				throw new InvalidOperationException("Invalid restored progression.");
			}
			lock (gate)
			{
				if (spending)
				{
					throw new InvalidOperationException("Cannot restore during a purchase.");
				}
				level = state.Level;
				xp = state.Xp;
				requiredXp = state.RequiredXp;
				freePoints = state.FreePoints;
			}
		}

		internal ProgressionSnapshot AddXp(double grantedXp)
		{
			if (!double.IsFinite(grantedXp) || grantedXp <= 0.0)
			{
				throw new ArgumentOutOfRangeException("grantedXp");
			}
			checked
			{
				lock (gate)
				{
					int num = level;
					double num2 = xp + grantedXp;
					double num3 = requiredXp;
					long num4 = freePoints;
					if (!double.IsFinite(num2))
					{
						throw new OverflowException("Independent XP exceeded the numeric range.");
					}
					while (num2 >= num3)
					{
						num2 -= num3;
						num++;
						num4 += 5;
						num3 *= 1.2;
						if (!double.IsFinite(num3))
						{
							throw new OverflowException("Independent XP requirement exceeded the numeric range.");
						}
					}
					level = num;
					xp = num2;
					requiredXp = num3;
					freePoints = num4;
					return new ProgressionSnapshot(level, xp, requiredXp, freePoints);
				}
			}
		}
	}
	internal readonly record struct ProgressionSnapshot(int Level, double Xp, double RequiredXp, long FreePoints);
	internal static class OriginalXpCredit
	{
		internal static double Measure(int levelBefore, float xpBefore, int levelAfter, float xpAfter, Func<int, float> requiredXp)
		{
			if (levelAfter < levelBefore || (long)levelAfter - (long)levelBefore > 4096 || !float.IsFinite(xpBefore) || !float.IsFinite(xpAfter))
			{
				throw new InvalidOperationException("Original progression reset or invalid XP state; credit is uncertain.");
			}
			double num = (double)xpAfter - (double)xpBefore;
			for (int i = levelBefore; i < levelAfter; i++)
			{
				float num2 = requiredXp(i);
				if (!float.IsFinite(num2) || num2 <= 0f)
				{
					throw new InvalidOperationException("Invalid original XP requirement; credit is uncertain.");
				}
				num += (double)num2;
			}
			if (!double.IsFinite(num) || num < 0.0)
			{
				throw new InvalidOperationException("Original XP credit could not be established.");
			}
			return num;
		}
	}
	internal static class SoloLevelingXpObserver
	{
		private sealed class Observation
		{
			internal readonly IntPtr DataPointer;

			internal readonly int LevelBefore;

			internal readonly float XpBefore;

			internal readonly Observation? Parent;

			internal double NestedCredit;

			internal bool Processed;

			internal Observation(IntPtr pointer, int level, float xp, Observation? parent)
			{
				DataPointer = pointer;
				LevelBefore = level;
				XpBefore = xp;
				Parent = parent;
			}
		}

		private const string HarmonyId = "pedro.cavecrawlers.tweaks.sololeveling.stage1";

		private static readonly object InstallGate = new object();

		internal static readonly SoloLevelingProgression Progression = new SoloLevelingProgression();

		private static ManualLogSource? log;

		private static Harmony? harmony;

		[ThreadStatic]
		private static Observation? active;

		internal static void Install(ManualLogSource logger)
		{
			//IL_008c: Unknown result type (might be due to invalid IL or missing references)
			//IL_0092: Expected O, but got Unknown
			//IL_009e: Unknown result type (might be due to invalid IL or missing references)
			//IL_00ad: Unknown result type (might be due to invalid IL or missing references)
			//IL_00bd: Unknown result type (might be due to invalid IL or missing references)
			//IL_00c8: Expected O, but got Unknown
			//IL_00c8: Expected O, but got Unknown
			//IL_00c8: Expected O, but got Unknown
			lock (InstallGate)
			{
				log = logger;
				if (harmony != null)
				{
					return;
				}
				try
				{
					MethodInfo method = typeof(ExperienceManager).GetMethod("IncreaseXP", BindingFlags.Static | BindingFlags.Public, null, new Type[2]
					{
						typeof(float),
						typeof(MainPlayerData)
					}, null);
					if (method == null || method.ReturnType != typeof(void))
					{
						throw new MissingMethodException("Expected static void ExperienceManager.IncreaseXP(float, MainPlayerData).");
					}
					Harmony val = new Harmony("pedro.cavecrawlers.tweaks.sololeveling.stage1");
					try
					{
						val.Patch((MethodBase)method, new HarmonyMethod(Hook("Prefix")), new HarmonyMethod(Hook("Postfix")), (HarmonyMethod)null, new HarmonyMethod(Hook("Finalizer")), (HarmonyMethod)null);
						bool flag = TryInstallLifecyclePatch(val, typeof(Characters), "OnPlayerSelected", new Type[2]
						{
							typeof(Player),
							typeof(PlayerSaved)
						}, "OnPlayerSelectedPrefix");
						bool flag2 = TryInstallLifecyclePatch(val, typeof(DontDestroy), "DeletePlayer", new Type[2]
						{
							typeof(PlayerSaved),
							typeof(bool)
						}, "DeletePlayerPrefix");
						SoloLevelingPersistence.SetLifecycleHooksReady(flag && flag2);
						harmony = val;
					}
					catch
					{
						val.UnpatchSelf();
						throw;
					}
					logger.LogInfo((object)"[Cave Crawlers Leveling] XP observer active; original game XP is not modified.");
					l