using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using BepInEx; using BepInEx.Configuration; using BepInEx.Logging; using ExtraLives.Patches; using HarmonyLib; using Microsoft.CodeAnalysis; using UnityEngine; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.Default | DebuggableAttribute.DebuggingModes.DisableOptimizations | DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints | DebuggableAttribute.DebuggingModes.EnableEditAndContinue)] [assembly: TargetFramework(".NETStandard,Version=v2.1", FrameworkDisplayName = ".NET Standard 2.1")] [assembly: AssemblyCompany("MiMoCode")] [assembly: AssemblyConfiguration("Debug")] [assembly: AssemblyFileVersion("1.0.0.0")] [assembly: AssemblyInformationalVersion("1.0.0")] [assembly: AssemblyProduct("ExtraLives")] [assembly: AssemblyTitle("ExtraLives")] [assembly: AssemblyVersion("1.0.0.0")] [module: RefSafetyRules(11)] 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; } } [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)] internal sealed class RefSafetyRulesAttribute : Attribute { public readonly int Version; public RefSafetyRulesAttribute(int P_0) { Version = P_0; } } } namespace ExtraLives { internal static class Configuration { internal static ConfigEntry extraLives; internal static ConfigEntry showUI; internal static ConfigEntry resetOnNewRun; internal static ConfigEntry debugMode; internal static void Init(ConfigFile config) { extraLives = config.Bind("General", "ExtraLives", 3, "Number of retry attempts before normal progression resumes. Set to 0 to disable."); showUI = config.Bind("General", "ShowUI", true, "Show remaining retries counter on screen."); resetOnNewRun = config.Bind("General", "ResetOnNewRun", true, "Reset retries at the start of each new run."); debugMode = config.Bind("General", "DebugMode", false, "Enable debug logging for troubleshooting."); } } [BepInPlugin("com.mimocode.extralives", "Extra Lives", "3.0.0")] public class Plugin : BaseUnityPlugin { private static FieldInfo _healthField; private static FieldInfo _maxHealthField; private static FieldInfo _spectatingField; internal static Plugin Instance { get; private set; } = null; internal static ManualLogSource Logger => Instance._logger; internal Harmony? Harmony { get; set; } private ManualLogSource _logger => ((BaseUnityPlugin)this).Logger; internal static int MaxRetries { get; set; } = 3; internal static int RetriesRemaining { get; set; } = 0; internal static int SavedLevelIndex { get; set; } = -1; internal static bool HasStoredLevel { get; set; } = false; internal static bool RedirectActive { get; set; } = false; internal static bool IsRetrying { get; set; } = false; internal static string CurrentSaveName { get; set; } = ""; internal static bool ShowEpicScreen { get; set; } = false; internal static float EpicScreenTimer { get; set; } = 0f; private void Awake() { //IL_0038: Unknown result type (might be due to invalid IL or missing references) //IL_003e: Expected O, but got Unknown //IL_006e: Unknown result type (might be due to invalid IL or missing references) //IL_0073: Unknown result type (might be due to invalid IL or missing references) //IL_0075: Expected O, but got Unknown //IL_007a: Expected O, but got Unknown Instance = this; ((Component)this).gameObject.transform.parent = null; ((Object)((Component)this).gameObject).hideFlags = (HideFlags)61; ConfigFile config = new ConfigFile(Path.Combine(Paths.ConfigPath, "ExtraLives.cfg"), true); Configuration.Init(config); MaxRetries = Configuration.extraLives.Value; if (Harmony == null) { Harmony val = new Harmony(((BaseUnityPlugin)this).Info.Metadata.GUID); Harmony val2 = val; Harmony = val; } GameDirectorPatch.Patch(Harmony); RunManagerPatch.Patch(Harmony); ChatPatch.Patch(Harmony); Logger.LogInfo((object)$"{((BaseUnityPlugin)this).Info.Metadata.GUID} v{((BaseUnityPlugin)this).Info.Metadata.Version} loaded!"); } private void Update() { if (ShowEpicScreen) { EpicScreenTimer += Time.deltaTime; if (EpicScreenTimer > 4f) { ShowEpicScreen = false; EpicScreenTimer = 0f; } } ChatPatch.TryLateRegister(); } internal static int GetCurrentLevel() { try { if ((Object)(object)StatsManager.instance != (Object)null) { return StatsManager.instance.GetRunStatLevel(); } } catch { } return 0; } internal static bool IsModActive() { return true; } internal static void UpdateSaveName() { try { if ((Object)(object)StatsManager.instance == (Object)null) { return; } FieldInfo field = typeof(StatsManager).GetField("saveFileCurrent", BindingFlags.Instance | BindingFlags.NonPublic); if (field != null) { string text = (field.GetValue(StatsManager.instance) as string) ?? ""; if (!string.IsNullOrEmpty(text) && text != CurrentSaveName) { CurrentSaveName = text; LoadRetriesFromSave(); } } } catch { } } internal static int GetPlayerHealth() { try { PlayerAvatar val = SemiFunc.PlayerAvatarLocal(); if ((Object)(object)val?.playerHealth == (Object)null) { return -1; } if (_healthField == null) { _healthField = typeof(PlayerHealth).GetField("health", BindingFlags.Instance | BindingFlags.NonPublic); } if (_healthField != null) { return (int)_healthField.GetValue(val.playerHealth); } } catch { } return -1; } internal static int GetMaxHealth() { try { PlayerAvatar val = SemiFunc.PlayerAvatarLocal(); if ((Object)(object)val?.playerHealth == (Object)null) { return 100; } if (_maxHealthField == null) { _maxHealthField = typeof(PlayerHealth).GetField("maxHealth", BindingFlags.Instance | BindingFlags.NonPublic); } if (_maxHealthField != null) { return (int)_maxHealthField.GetValue(val.playerHealth); } } catch { } return 100; } internal static void RestorePlayerHealth() { try { PlayerAvatar val = SemiFunc.PlayerAvatarLocal(); if ((Object)(object)val?.playerHealth == (Object)null) { return; } int maxHealth = GetMaxHealth(); int playerHealth = GetPlayerHealth(); if (playerHealth >= 0 && maxHealth > 0) { int num = maxHealth - playerHealth; if (num > 0) { val.playerHealth.Heal(num, false); Logger.LogInfo((object)$"Health restored: {playerHealth} → {maxHealth}"); } } } catch { } } internal static bool IsPlayerSpectating() { try { PlayerAvatar val = SemiFunc.PlayerAvatarLocal(); if ((Object)(object)val == (Object)null) { return false; } if (_spectatingField == null) { _spectatingField = typeof(PlayerAvatar).GetField("spectating", BindingFlags.Instance | BindingFlags.NonPublic); } if (_spectatingField != null) { return (bool)_spectatingField.GetValue(val); } } catch { } return false; } internal static void OnDeathSave() { try { int currentLevel = GetCurrentLevel(); Logger.LogInfo((object)$"OnDeathSave: level={currentLevel}"); if (currentLevel < 0) { Logger.LogInfo((object)"Level < 0 — normal death."); return; } if (!HasStoredLevel) { RetriesRemaining = MaxRetries; Logger.LogInfo((object)$"First death. Retries: {RetriesRemaining}/{MaxRetries}."); } else { Logger.LogInfo((object)$"Retry death. Retries: {RetriesRemaining}/{MaxRetries}."); if (RetriesRemaining <= 0) { Logger.LogInfo((object)"No retries left."); return; } } RetriesRemaining--; Logger.LogInfo((object)$"Life consumed on death. Retries: {RetriesRemaining}/{MaxRetries}"); SavedLevelIndex = currentLevel; HasStoredLevel = true; RedirectActive = RetriesRemaining > 0; SaveRetriesToSave(); Logger.LogInfo((object)$"Saved level {SavedLevelIndex}. Saving..."); SemiFunc.SaveFileSave(); } catch (Exception ex) { Logger.LogWarning((object)("OnDeathSave: " + ex.Message)); } } internal static bool ConsumeRetry() { if (!HasStoredLevel || !RedirectActive || RetriesRemaining <= 0) { return false; } RetriesRemaining--; IsRetrying = true; Logger.LogInfo((object)$"Retry consumed: {SavedLevelIndex}, left={RetriesRemaining}"); if (RetriesRemaining <= 0) { RedirectActive = false; } SaveRetriesToSave(); return true; } internal static void AddLife() { if (RetriesRemaining >= MaxRetries) { Logger.LogInfo((object)$"Max lives ({MaxRetries})."); return; } RetriesRemaining++; SaveRetriesToSave(); Logger.LogInfo((object)$"+1 life! {RetriesRemaining}/{MaxRetries}"); } internal static void ResetRetries() { RetriesRemaining = MaxRetries; HasStoredLevel = false; RedirectActive = false; SavedLevelIndex = -1; IsRetrying = false; ShowEpicScreen = false; EpicScreenTimer = 0f; } internal static void SaveRetriesToSave() { try { PlayerPrefs.SetInt("EL_" + CurrentSaveName + "_R", RetriesRemaining); PlayerPrefs.SetInt("EL_" + CurrentSaveName + "_L", SavedLevelIndex); PlayerPrefs.SetInt("EL_" + CurrentSaveName + "_H", HasStoredLevel ? 1 : 0); PlayerPrefs.SetInt("EL_" + CurrentSaveName + "_A", RedirectActive ? 1 : 0); PlayerPrefs.Save(); } catch { } } private static void LoadRetriesFromSave() { try { if (!string.IsNullOrEmpty(CurrentSaveName)) { RetriesRemaining = PlayerPrefs.GetInt("EL_" + CurrentSaveName + "_R", MaxRetries); SavedLevelIndex = PlayerPrefs.GetInt("EL_" + CurrentSaveName + "_L", -1); HasStoredLevel = PlayerPrefs.GetInt("EL_" + CurrentSaveName + "_H", 0) == 1; RedirectActive = PlayerPrefs.GetInt("EL_" + CurrentSaveName + "_A", 0) == 1; Logger.LogInfo((object)$"Loaded: save={CurrentSaveName}, retries={RetriesRemaining}, level={SavedLevelIndex}, stored={HasStoredLevel}"); } } catch { } } private void OnGUI() { //IL_00af: Unknown result type (might be due to invalid IL or missing references) //IL_00b4: Unknown result type (might be due to invalid IL or missing references) //IL_00c0: Unknown result type (might be due to invalid IL or missing references) //IL_00c5: Unknown result type (might be due to invalid IL or missing references) //IL_00fb: Unknown result type (might be due to invalid IL or missing references) //IL_0115: 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_0123: Unknown result type (might be due to invalid IL or missing references) //IL_012b: Unknown result type (might be due to invalid IL or missing references) //IL_0133: Unknown result type (might be due to invalid IL or missing references) //IL_0139: Unknown result type (might be due to invalid IL or missing references) //IL_0146: Expected O, but got Unknown //IL_0150: Unknown result type (might be due to invalid IL or missing references) //IL_0155: Unknown result type (might be due to invalid IL or missing references) //IL_015e: Unknown result type (might be due to invalid IL or missing references) //IL_0166: Unknown result type (might be due to invalid IL or missing references) //IL_016e: Unknown result type (might be due to invalid IL or missing references) //IL_0174: Unknown result type (might be due to invalid IL or missing references) //IL_0181: Expected O, but got Unknown //IL_0191: Unknown result type (might be due to invalid IL or missing references) //IL_01fa: Unknown result type (might be due to invalid IL or missing references) if (ShowEpicScreen) { DrawEpicScreen(); } else if (Configuration.showUI.Value && (SemiFunc.RunIsLevel() || SemiFunc.RunIsShop()) && IsModActive() && (RetriesRemaining < MaxRetries || HasStoredLevel) && (IsPlayerSpectating() || SemiFunc.RunIsShop())) { float num = (float)Screen.height / 1080f; GUI.matrix = Matrix4x4.TRS(Vector3.zero, Quaternion.identity, new Vector3(num, num, 1f)); float num2 = 350f; float num3 = 70f; float num4 = ((float)Screen.width / num - num2) / 2f; float num5 = 20f; GUI.Box(new Rect(num4, num5, num2, num3), ""); GUIStyle val = new GUIStyle(GUI.skin.label) { fontSize = 14, fontStyle = (FontStyle)1, alignment = (TextAnchor)1 }; val.normal.textColor = Color.cyan; GUIStyle val2 = val; GUIStyle val3 = new GUIStyle(GUI.skin.label) { fontSize = 20, fontStyle = (FontStyle)1, alignment = (TextAnchor)4 }; val3.normal.textColor = Color.yellow; GUIStyle val4 = val3; GUI.Label(new Rect(num4, num5 + 5f, num2, 20f), "EXTRA LIVES", val2); string text = (HasStoredLevel ? $"Retries Left: {RetriesRemaining}/{MaxRetries}" : $"Retries: {RetriesRemaining}/{MaxRetries}"); GUI.Label(new Rect(num4, num5 + 28f, num2, 35f), text, val4); } } private void DrawEpicScreen() { //IL_000e: Unknown result type (might be due to invalid IL or missing references) //IL_0013: Unknown result type (might be due to invalid IL or missing references) //IL_001f: Unknown result type (might be due to invalid IL or missing references) //IL_0024: Unknown result type (might be due to invalid IL or missing references) //IL_0055: Unknown result type (might be due to invalid IL or missing references) //IL_006c: Unknown result type (might be due to invalid IL or missing references) //IL_007c: Unknown result type (might be due to invalid IL or missing references) //IL_00cd: Unknown result type (might be due to invalid IL or missing references) //IL_00d2: Unknown result type (might be due to invalid IL or missing references) //IL_00db: Unknown result type (might be due to invalid IL or missing references) //IL_00e3: Unknown result type (might be due to invalid IL or missing references) //IL_00eb: Unknown result type (might be due to invalid IL or missing references) //IL_0102: Unknown result type (might be due to invalid IL or missing references) //IL_010f: Expected O, but got Unknown //IL_0119: Unknown result type (might be due to invalid IL or missing references) //IL_011e: 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_012f: Unknown result type (might be due to invalid IL or missing references) //IL_0137: Unknown result type (might be due to invalid IL or missing references) //IL_014e: Unknown result type (might be due to invalid IL or missing references) //IL_015b: Expected O, but got Unknown //IL_01e1: Unknown result type (might be due to invalid IL or missing references) //IL_0205: Unknown result type (might be due to invalid IL or missing references) //IL_017c: Unknown result type (might be due to invalid IL or missing references) //IL_01a0: Unknown result type (might be due to invalid IL or missing references) float num = (float)Screen.height / 1080f; GUI.matrix = Matrix4x4.TRS(Vector3.zero, Quaternion.identity, new Vector3(num, num, 1f)); float num2 = (float)Screen.width / num; float num3 = (float)Screen.height / num; GUI.color = new Color(0f, 0f, 0f, 0.85f); GUI.DrawTexture(new Rect(0f, 0f, num2, num3), (Texture)(object)Texture2D.whiteTexture); GUI.color = Color.white; float num4 = Mathf.Clamp01(EpicScreenTimer / 0.5f); float num5 = ((EpicScreenTimer > 3f) ? Mathf.Clamp01(4f - EpicScreenTimer) : 1f); float num6 = num4 * num5; GUIStyle val = new GUIStyle(GUI.skin.label) { fontSize = 60, fontStyle = (FontStyle)1, alignment = (TextAnchor)4 }; val.normal.textColor = new Color(1f, 0.2f, 0.2f, num6); GUIStyle val2 = val; GUIStyle val3 = new GUIStyle(GUI.skin.label) { fontSize = 30, fontStyle = (FontStyle)1, alignment = (TextAnchor)4 }; val3.normal.textColor = new Color(1f, 1f, 0.3f, num6); GUIStyle val4 = val3; if (RetriesRemaining > 0) { GUI.Label(new Rect(0f, num3 * 0.35f, num2, 80f), "LIFE LOST!", val2); GUI.Label(new Rect(0f, num3 * 0.5f, num2, 50f), $"Remaining: {RetriesRemaining} / {MaxRetries}", val4); } else { GUI.Label(new Rect(0f, num3 * 0.35f, num2, 80f), "GAME OVER", val2); GUI.Label(new Rect(0f, num3 * 0.5f, num2, 50f), "No retries left", val4); } } } } namespace ExtraLives.Patches { internal static class ChatPatch { internal static bool _commandRegistered; private static float _retryTimer; internal static void Patch(Harmony harmony) { } internal static void TryLateRegister() { if (_commandRegistered) { return; } _retryTimer += Time.deltaTime; if (_retryTimer < 2f) { return; } _retryTimer = 0f; try { Type type = null; Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies(); foreach (Assembly assembly in assemblies) { type = assembly.GetType("DebugCommandHandler"); if (type != null) { break; } } if (!(type == null)) { Object val = Object.FindObjectOfType(type); if (!(val == (Object)null)) { RegisterCommand(val, type); } } } catch (Exception ex) { Plugin.Logger.LogWarning((object)("Command register attempt: " + ex.Message)); } } private static void RegisterCommand(object handler, Type handlerType) { if (_commandRegistered) { return; } try { Type type = null; Type[] nestedTypes = handlerType.GetNestedTypes(BindingFlags.Public | BindingFlags.NonPublic); foreach (Type type2 in nestedTypes) { if (type2.Name == "ChatCommand") { type = type2; break; } } if (type == null) { return; } ConstructorInfo constructorInfo = type.GetConstructors(BindingFlags.Instance | BindingFlags.Public)[0]; Action action = delegate { Plugin.Logger.LogInfo((object)"Command: getlivegameofrepo"); if (SemiFunc.IsMasterClient() || !GameManager.Multiplayer()) { Plugin.AddLife(); ShowNotification(); } }; Func> func = (bool a, string b, string[] c) => new List(); Func func2 = () => true; object obj = constructorInfo.Invoke(new object[6] { "getlivegameofrepo", "Add +1 extra life (host only, max 3)", action, func, func2, false }); MethodInfo method = handlerType.GetMethod("Register", BindingFlags.Instance | BindingFlags.Public); if (method != null) { method.Invoke(handler, new object[1] { obj }); _commandRegistered = true; Plugin.Logger.LogInfo((object)"Chat command registered: getlivegameofrepo"); } } catch (Exception ex) { Plugin.Logger.LogWarning((object)("Register command failed: " + ex.Message)); } } private static void ShowNotification() { //IL_002a: Unknown result type (might be due to invalid IL or missing references) //IL_002f: Unknown result type (might be due to invalid IL or missing references) try { SemiFunc.UIBigMessage($"+1 LIFE! ({Plugin.RetriesRemaining}/{Plugin.MaxRetries})", "", 50f, Color.green, Color.white); } catch { } } } internal static class GameDirectorPatch { internal static void Patch(Harmony harmony) { //IL_004b: Unknown result type (might be due to invalid IL or missing references) //IL_0059: Expected O, but got Unknown Type typeFromHandle = typeof(GameDirector); if (!(typeFromHandle == null)) { MethodInfo method = typeFromHandle.GetMethod("gameStateStart", BindingFlags.Instance | BindingFlags.NonPublic); if (method != null) { harmony.Patch((MethodBase)method, new HarmonyMethod(typeof(GameDirectorPatch).GetMethod("StartPrefix", BindingFlags.Static | BindingFlags.NonPublic)), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); Plugin.Logger.LogInfo((object)"Patched: GameDirector.gameStateStart"); } } } private static void StartPrefix() { Plugin.UpdateSaveName(); if (Plugin.IsRetrying) { Plugin.Logger.LogInfo((object)"gameStateStart: retrying → restore health + epic screen"); Plugin.RestorePlayerHealth(); Plugin.IsRetrying = false; Plugin.ShowEpicScreen = true; Plugin.EpicScreenTimer = 0f; } } } internal static class RunManagerPatch { internal static void Patch(Harmony harmony) { //IL_004b: Unknown result type (might be due to invalid IL or missing references) //IL_0059: Expected O, but got Unknown Type typeFromHandle = typeof(RunManager); if (!(typeFromHandle == null)) { MethodInfo method = typeFromHandle.GetMethod("ChangeLevel", BindingFlags.Instance | BindingFlags.Public); if (method != null) { harmony.Patch((MethodBase)method, new HarmonyMethod(typeof(RunManagerPatch).GetMethod("ChangeLevelPrefix", BindingFlags.Static | BindingFlags.NonPublic)), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); Plugin.Logger.LogInfo((object)"Patched: RunManager.ChangeLevel"); } } } private static bool ChangeLevelPrefix(ref bool _completedLevel, ref bool _levelFailed, ref ChangeLevelType _changeLevelType) { Plugin.UpdateSaveName(); if (_levelFailed && !Plugin.IsRetrying) { int currentLevel = Plugin.GetCurrentLevel(); bool flag = Plugin.IsModActive(); Plugin.Logger.LogInfo((object)$"ChangeLevel: levelFailed, level={currentLevel}, active={flag}, retries={Plugin.RetriesRemaining}"); if (flag && (!Plugin.HasStoredLevel || Plugin.RetriesRemaining > 0) && (SemiFunc.IsMasterClient() || !GameManager.Multiplayer())) { Plugin.Logger.LogInfo((object)"→ Save & redirect to Shop"); Plugin.OnDeathSave(); _changeLevelType = (ChangeLevelType)5; _levelFailed = false; _completedLevel = false; return true; } if (flag) { Plugin.Logger.LogInfo((object)"→ No retries. Game over."); } return true; } if (!_levelFailed && Plugin.RedirectActive && Plugin.HasStoredLevel && Plugin.RetriesRemaining > 0) { Plugin.Logger.LogInfo((object)"ChangeLevel: shop departure with retries → retry same level"); Plugin.IsRetrying = true; Plugin.RedirectActive = false; Plugin.SaveRetriesToSave(); _completedLevel = false; _levelFailed = false; return true; } if (Plugin.IsRetrying) { Plugin.Logger.LogInfo((object)$"ChangeLevel: retrying. completed={_completedLevel}"); _completedLevel = false; _levelFailed = false; } return true; } } }