Decompiled source of CaveCrawlersLeveling v2.0.0
BepInEx/plugins/CaveCrawlersTweaks/CaveCrawlersTweaks.dll
Decompiled 11 hours ago
The result has been truncated due to the large size, download it to view full contents!
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