using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.Versioning; using BepInEx; using BepInEx.Configuration; using BepInEx.Logging; using HarmonyLib; using UnityEngine; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.Default | DebuggableAttribute.DebuggingModes.DisableOptimizations | DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints | DebuggableAttribute.DebuggingModes.EnableEditAndContinue)] [assembly: AssemblyTitle("Luck On Kill")] [assembly: AssemblyDescription("")] [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("")] [assembly: AssemblyProduct("Luck On Kill")] [assembly: AssemblyCopyright("Copyright © 2026")] [assembly: AssemblyTrademark("")] [assembly: ComVisible(false)] [assembly: Guid("2edd8a2b-56c3-4ee0-871e-fa11d76998ec")] [assembly: AssemblyFileVersion("1.0.0.0")] [assembly: TargetFramework(".NETFramework,Version=v4.7.2", FrameworkDisplayName = ".NET Framework 4.7.2")] [assembly: AssemblyVersion("1.0.0.0")] namespace LuckOnKill; [BepInPlugin("kumo.sulfur.luck_on_kill", "Luck On Kill", "1.0.2")] public sealed class Plugin : BaseUnityPlugin { internal static ManualLogSource Log; internal static ConfigEntry EnableMod; internal static ConfigEntry RewardMultiplier; internal static ConfigEntry RewardFlatBonus; internal static ConfigEntry RewardOverride; internal static ConfigEntry ApplyThroughModifyStatus; internal static ConfigEntry RequireHostileToPlayer; internal static ConfigEntry RewardOnlyExperienceUnits; internal static ConfigEntry RewardCivilians; internal static ConfigEntry RewardBreakables; internal static ConfigEntry PreventDuplicateRewards; internal static ConfigEntry LogRewards; private Harmony harmony; private const string UnitTypeName = "PerfectRandom.Sulfur.Core.Units.Unit"; private const string GameManagerTypeName = "PerfectRandom.Sulfur.Core.World.GameManager"; private const string EntityAttributesTypeName = "PerfectRandom.Sulfur.Core.Stats.EntityAttributes"; private const int StatLuckGain = 58; private const int StatusLuck = 94; private static readonly HashSet rewardedUnitIds = new HashSet(); private void Awake() { //IL_017e: Unknown result type (might be due to invalid IL or missing references) //IL_0188: Expected O, but got Unknown Log = ((BaseUnityPlugin)this).Logger; EnableMod = ((BaseUnityPlugin)this).Config.Bind("General", "EnableMod", true, "Enable Luck On Kill."); RewardMultiplier = ((BaseUnityPlugin)this).Config.Bind("Reward", "RewardMultiplier", 1f, "Multiplier for the Luck gained on enemy death. Base reward is current Stat_LuckGain, which equals one minute of vanilla Luck recovery."); RewardFlatBonus = ((BaseUnityPlugin)this).Config.Bind("Reward", "RewardFlatBonus", 0f, "Flat bonus added to the kill Luck reward after multiplier."); RewardOverride = ((BaseUnityPlugin)this).Config.Bind("Reward", "RewardOverride", -1f, "If >= 0, replaces the kill Luck reward with this fixed value. -1 = disabled."); ApplyThroughModifyStatus = ((BaseUnityPlugin)this).Config.Bind("Compatibility", "ApplyThroughModifyStatus", true, "If true, applies Luck through ModifyStatus. This allows Better Luck Control to affect kill rewards too."); RequireHostileToPlayer = ((BaseUnityPlugin)this).Config.Bind("Filter", "RequireHostileToPlayer", true, "If true, only units hostile to the player grant Luck."); RewardOnlyExperienceUnits = ((BaseUnityPlugin)this).Config.Bind("Filter", "RewardOnlyExperienceUnits", false, "If true, only units with ExperienceOnKill > 0 grant Luck. Default false allows all hostile enemies, including special 0 XP enemies."); RewardCivilians = ((BaseUnityPlugin)this).Config.Bind("Filter", "RewardCivilians", false, "If true, civilian units can grant Luck."); RewardBreakables = ((BaseUnityPlugin)this).Config.Bind("Filter", "RewardBreakables", false, "If true, Breakable units can grant Luck. Recommended false."); PreventDuplicateRewards = ((BaseUnityPlugin)this).Config.Bind("Safety", "PreventDuplicateRewards", true, "Prevents the same unit death from granting Luck more than once. Object-pool reuse is handled by clearing this state on Unit.Spawn()."); LogRewards = ((BaseUnityPlugin)this).Config.Bind("Debug", "LogRewards", true, "Log Luck rewards from enemy deaths."); harmony = new Harmony("kumo.sulfur.luck_on_kill"); harmony.PatchAll(); ((BaseUnityPlugin)this).Logger.LogInfo((object)"Luck On Kill loaded. Patched Unit.Die()."); } private void OnDestroy() { Harmony obj = harmony; if (obj != null) { obj.UnpatchSelf(); } } internal static void ResetRewardState(object unit) { if (unit != null) { int objectId = GetObjectId(unit); rewardedUnitIds.Remove(objectId); } } internal static bool ShouldRewardBeforeDeath(object unit) { if (!EnableMod.Value) { return false; } if (unit == null) { return false; } if (IsDead(unit)) { return false; } if (IsPlayerUnit(unit)) { return false; } if (!RewardBreakables.Value && IsBreakableUnit(unit)) { return false; } if (!RewardCivilians.Value && IsCivilianUnit(unit)) { return false; } if (RequireHostileToPlayer.Value) { bool? flag = IsHostileToPlayer(unit); if (flag.HasValue) { if (!flag.Value) { return false; } } else { int intMember = GetIntMember(unit, "ExperienceOnKill", 0); if (intMember <= 0) { return false; } } } if (RewardOnlyExperienceUnits.Value) { int intMember2 = GetIntMember(unit, "ExperienceOnKill", 0); if (intMember2 <= 0) { return false; } } return true; } internal static void GrantLuckForDeath(object unit) { if (unit == null) { return; } int objectId = GetObjectId(unit); if (PreventDuplicateRewards.Value) { if (rewardedUnitIds.Contains(objectId)) { return; } rewardedUnitIds.Add(objectId); if (rewardedUnitIds.Count > 10000) { rewardedUnitIds.Clear(); } } object playerStats = GetPlayerStats(unit); if (playerStats == null) { if (LogRewards.Value) { ManualLogSource log = Log; if (log != null) { log.LogWarning((object)("[LuckOnKill] Could not find current player stats. Unit=" + GetUnitName(unit))); } } return; } float attribute = GetAttribute(playerStats, 58); if (float.IsNaN(attribute)) { if (LogRewards.Value) { ManualLogSource log2 = Log; if (log2 != null) { log2.LogWarning((object)"[LuckOnKill] Could not read Stat_LuckGain."); } } return; } float num = CalculateReward(attribute); if (num <= 0f) { return; } float status = GetStatus(playerStats, 94); if (ApplyThroughModifyStatus.Value) { ModifyStatus(playerStats, 94, num); } else { SetStatus(playerStats, 94, status + num); } float status2 = GetStatus(playerStats, 94); if (LogRewards.Value) { ManualLogSource log3 = Log; if (log3 != null) { log3.LogInfo((object)("[LuckOnKill] Unit died: " + GetUnitName(unit) + " | LuckGain=" + attribute.ToString("0.###") + " | Reward=" + num.ToString("0.###") + " | Status_Luck " + status.ToString("0.###") + " -> " + status2.ToString("0.###"))); } } } private static object GetPlayerStats(object contextUnit) { object obj = TryGetPlayerStatsFromContextUnit(contextUnit); if (obj != null) { return obj; } object obj2 = TryGetPlayerStatsFromGameManager(); if (obj2 != null) { return obj2; } object obj3 = TryFindPlayerStatsByScanningUnits(); if (obj3 != null) { return obj3; } return null; } private static object TryGetPlayerStatsFromContextUnit(object contextUnit) { if (contextUnit == null) { return null; } object member = GetMember(contextUnit, "PlayerUnit"); if (member == null) { return null; } return GetMember(member, "Stats"); } private static object TryGetPlayerStatsFromGameManager() { object obj = TryGetPlayerUnitFromGameManager(); if (obj == null) { return null; } return GetMember(obj, "Stats"); } private static object TryGetPlayerUnitFromGameManager() { Type type = FindTypeByFullName("PerfectRandom.Sulfur.Core.World.GameManager"); if (type == null) { return null; } object staticMember = GetStaticMember(type, "Instance"); if (staticMember == null) { return null; } return GetMember(staticMember, "PlayerUnit"); } private static object TryFindPlayerStatsByScanningUnits() { Type type = FindTypeByFullName("PerfectRandom.Sulfur.Core.Units.Unit"); if (type == null) { return null; } Object[] array; try { array = Object.FindObjectsOfType(type); } catch { return null; } if (array == null) { return null; } Object[] array2 = array; foreach (Object val in array2) { if (!(val == (Object)null) && IsPlayerUnit(val)) { object member = GetMember(val, "Stats"); if (member != null) { return member; } } } return null; } private static float CalculateReward(float luckGain) { float value = RewardOverride.Value; if (value >= 0f) { return value; } float num = luckGain * RewardMultiplier.Value + RewardFlatBonus.Value; if (float.IsNaN(num) || float.IsInfinity(num)) { num = 0f; } return Math.Max(0f, num); } private static bool? IsHostileToPlayer(object unit) { if (unit == null) { return null; } object playerUnit = TryGetPlayerUnitFromGameManager(); if (playerUnit == null) { playerUnit = TryFindPlayerUnitByScanningUnits(); } if (playerUnit == null) { return null; } MethodInfo methodInfo = FindInstanceMethod(unit.GetType(), "IsHostileTo", (ParameterInfo[] parameters) => parameters.Length == 1 && parameters[0].ParameterType.IsAssignableFrom(playerUnit.GetType())); if (methodInfo == null) { return null; } try { object obj = methodInfo.Invoke(unit, new object[1] { playerUnit }); if (obj is bool) { bool value = (bool)obj; if (true) { return value; } } } catch { return null; } return null; } private static object TryFindPlayerUnitByScanningUnits() { Type type = FindTypeByFullName("PerfectRandom.Sulfur.Core.Units.Unit"); if (type == null) { return null; } Object[] array; try { array = Object.FindObjectsOfType(type); } catch { return null; } if (array == null) { return null; } Object[] array2 = array; foreach (Object val in array2) { if (!(val == (Object)null) && IsPlayerUnit(val)) { return val; } } return null; } private static bool IsDead(object unit) { object member = GetMember(unit, "UnitState"); if (member == null) { member = GetMember(unit, "unitState"); } if (member == null) { return false; } string text = member.ToString(); if (text == "Dead") { return true; } try { return Convert.ToInt32(member) == 0; } catch { return false; } } private static bool IsPlayerUnit(object unit) { if (GetBoolMember(unit, "isPlayer", fallback: false)) { return true; } object member = GetMember(unit, "IsPlayer"); bool flag = default(bool); int num; if (member is bool) { flag = (bool)member; num = 1; } else { num = 0; } if (((uint)num & (flag ? 1u : 0u)) != 0) { return true; } string unitName = GetUnitName(unit); return unitName.Contains("Unit_Player"); } private static bool IsCivilianUnit(object unit) { if (GetMember(unit, "IsCivilian") is bool result) { return result; } return false; } private static bool IsBreakableUnit(object unit) { if (unit == null) { return false; } Type type = unit.GetType(); while (type != null) { if (type.Name == "Breakable" || (type.FullName != null && type.FullName.Contains(".Breakable"))) { return true; } type = type.BaseType; } string unitName = GetUnitName(unit); return unitName.Contains("Breakable"); } private static string GetUnitName(object unit) { if (unit == null) { return "Unknown"; } Object val = (Object)((unit is Object) ? unit : null); if (val != (Object)null) { return val.name; } return unit.ToString(); } private static int GetObjectId(object obj) { Object val = (Object)((obj is Object) ? obj : null); if (val != (Object)null) { return val.GetInstanceID(); } return RuntimeHelpers.GetHashCode(obj); } private static float GetStatus(object stats, int attributeId) { return InvokeStatsGetter(stats, "GetStatus", attributeId); } private static float GetAttribute(object stats, int attributeId) { return InvokeStatsGetter(stats, "GetAttribute", attributeId); } private static float InvokeStatsGetter(object stats, string methodName, int attributeId) { if (stats == null) { return float.NaN; } Type enumType = FindTypeByFullName("PerfectRandom.Sulfur.Core.Stats.EntityAttributes"); if (enumType == null) { return float.NaN; } object obj = Enum.ToObject(enumType, attributeId); MethodInfo methodInfo = FindInstanceMethod(stats.GetType(), methodName, (ParameterInfo[] parameters) => parameters.Length == 1 && parameters[0].ParameterType == enumType); if (methodInfo == null) { return float.NaN; } try { object value = methodInfo.Invoke(stats, new object[1] { obj }); return Convert.ToSingle(value); } catch { return float.NaN; } } private static void ModifyStatus(object stats, int statusId, float amount) { InvokeStatsSetter(stats, "ModifyStatus", statusId, amount); } private static void SetStatus(object stats, int statusId, float value) { InvokeStatsSetter(stats, "SetStatus", statusId, value); } private static void InvokeStatsSetter(object stats, string methodName, int statusId, float value) { if (stats == null) { return; } Type enumType = FindTypeByFullName("PerfectRandom.Sulfur.Core.Stats.EntityAttributes"); if (enumType == null) { return; } object obj = Enum.ToObject(enumType, statusId); MethodInfo methodInfo = FindInstanceMethod(stats.GetType(), methodName, (ParameterInfo[] parameters) => parameters.Length >= 2 && parameters[0].ParameterType == enumType && parameters[1].ParameterType == typeof(float)); if (methodInfo == null) { if (LogRewards.Value) { ManualLogSource log = Log; if (log != null) { log.LogWarning((object)("[LuckOnKill] Could not find " + methodName + ".")); } } return; } ParameterInfo[] parameters2 = methodInfo.GetParameters(); object[] array = new object[parameters2.Length]; array[0] = obj; array[1] = value; for (int i = 2; i < array.Length; i++) { Type parameterType = parameters2[i].ParameterType; if (parameterType == typeof(bool)) { array[i] = false; } else if (parameters2[i].HasDefaultValue) { array[i] = parameters2[i].DefaultValue; } else { array[i] = GetDefaultValue(parameterType); } } try { methodInfo.Invoke(stats, array); } catch (Exception ex) { if (LogRewards.Value) { ManualLogSource log2 = Log; if (log2 != null) { log2.LogWarning((object)("[LuckOnKill] Failed to invoke " + methodName + ": " + ex.GetType().Name + ": " + ex.Message)); } } } } private static object GetMember(object obj, string name) { if (obj == null) { return null; } Type type = obj.GetType(); while (type != null) { PropertyInfo property = type.GetProperty(name, BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic); if (property != null) { try { return property.GetValue(obj, null); } catch { return null; } } FieldInfo field = type.GetField(name, BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic); if (field != null) { try { return field.GetValue(obj); } catch { return null; } } type = type.BaseType; } return null; } private static object GetStaticMember(Type type, string name) { if (type == null) { return null; } Type type2 = type; while (type2 != null) { PropertyInfo property = type2.GetProperty(name, BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.FlattenHierarchy); if (property != null) { try { return property.GetValue(null, null); } catch { return null; } } FieldInfo field = type2.GetField(name, BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.FlattenHierarchy); if (field != null) { try { return field.GetValue(null); } catch { return null; } } type2 = type2.BaseType; } return null; } private static bool GetBoolMember(object obj, string name, bool fallback) { if (GetMember(obj, name) is bool result) { return result; } return fallback; } private static int GetIntMember(object obj, string name, int fallback) { object member = GetMember(obj, name); if (member == null) { return fallback; } try { return Convert.ToInt32(member); } catch { return fallback; } } private static Type FindTypeByFullName(string fullName) { Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies(); foreach (Assembly assembly in assemblies) { Type type = assembly.GetType(fullName, throwOnError: false); if (type != null) { return type; } } return null; } private static MethodInfo FindInstanceMethod(Type type, string methodName, Func parameterPredicate) { if (type == null) { return null; } Type type2 = type; while (type2 != null) { MethodInfo[] methods = type2.GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); MethodInfo[] array = methods; foreach (MethodInfo methodInfo in array) { if (!(methodInfo.Name != methodName)) { ParameterInfo[] parameters = methodInfo.GetParameters(); if (parameterPredicate(parameters)) { return methodInfo; } } } type2 = type2.BaseType; } return null; } private static object GetDefaultValue(Type type) { if (type == null) { return null; } if (type.IsValueType) { return Activator.CreateInstance(type); } return null; } } [HarmonyPatch] internal static class Unit_Spawn_ResetRewardState_Patch { [CompilerGenerated] private sealed class d__0 : IEnumerable, IEnumerable, IEnumerator, IDisposable, IEnumerator { private int <>1__state; private MethodBase <>2__current; private int <>l__initialThreadId; private Type 5__1; private Assembly[] <>s__2; private int <>s__3; private Assembly 5__4; private Type[] 5__5; private Type[] <>s__6; private int <>s__7; private Type 5__8; private MethodInfo 5__9; MethodBase IEnumerator.Current { [DebuggerHidden] get { return <>2__current; } } object IEnumerator.Current { [DebuggerHidden] get { return <>2__current; } } [DebuggerHidden] public d__0(int <>1__state) { this.<>1__state = <>1__state; <>l__initialThreadId = Environment.CurrentManagedThreadId; } [DebuggerHidden] void IDisposable.Dispose() { 5__1 = null; <>s__2 = null; 5__4 = null; 5__5 = null; <>s__6 = null; 5__8 = null; 5__9 = null; <>1__state = -2; } private bool MoveNext() { int num = <>1__state; if (num != 0) { if (num != 1) { return false; } <>1__state = -1; goto IL_015e; } <>1__state = -1; 5__1 = AccessTools.TypeByName("PerfectRandom.Sulfur.Core.Units.Unit"); if (5__1 == null) { return false; } <>s__2 = AppDomain.CurrentDomain.GetAssemblies(); <>s__3 = 0; goto IL_01b2; IL_017b: if (<>s__7 < <>s__6.Length) { 5__8 = <>s__6[<>s__7]; if (!(5__8 == null) && 5__1.IsAssignableFrom(5__8)) { 5__9 = 5__8.GetMethod("Spawn", BindingFlags.DeclaredOnly | BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); if (!(5__9 == null) && !5__9.IsAbstract) { if (5__9.GetParameters().Length == 0) { <>2__current = 5__9; <>1__state = 1; return true; } goto IL_015e; } } goto IL_016d; } <>s__6 = null; 5__5 = null; 5__4 = null; goto IL_01a4; IL_016d: <>s__7++; goto IL_017b; IL_01a4: <>s__3++; goto IL_01b2; IL_01b2: if (<>s__3 < <>s__2.Length) { 5__4 = <>s__2[<>s__3]; try { 5__5 = 5__4.GetTypes(); } catch { goto IL_01a4; } <>s__6 = 5__5; <>s__7 = 0; goto IL_017b; } <>s__2 = null; return false; IL_015e: 5__9 = null; 5__8 = null; goto IL_016d; } bool IEnumerator.MoveNext() { //ILSpy generated this explicit interface implementation from .override directive in MoveNext return this.MoveNext(); } [DebuggerHidden] void IEnumerator.Reset() { throw new NotSupportedException(); } [DebuggerHidden] IEnumerator IEnumerable.GetEnumerator() { if (<>1__state == -2 && <>l__initialThreadId == Environment.CurrentManagedThreadId) { <>1__state = 0; return this; } return new d__0(0); } [DebuggerHidden] IEnumerator IEnumerable.GetEnumerator() { return ((IEnumerable)this).GetEnumerator(); } } [IteratorStateMachine(typeof(d__0))] private static IEnumerable TargetMethods() { //yield-return decompiler failed: Unexpected instruction in Iterator.Dispose() return new d__0(-2); } private static void Postfix(object __instance) { Plugin.ResetRewardState(__instance); } } [HarmonyPatch] internal static class Unit_Die_Patch { [CompilerGenerated] private sealed class d__0 : IEnumerable, IEnumerable, IEnumerator, IDisposable, IEnumerator { private int <>1__state; private MethodBase <>2__current; private int <>l__initialThreadId; private Type 5__1; private Assembly[] <>s__2; private int <>s__3; private Assembly 5__4; private Type[] 5__5; private Type[] <>s__6; private int <>s__7; private Type 5__8; private MethodInfo 5__9; MethodBase IEnumerator.Current { [DebuggerHidden] get { return <>2__current; } } object IEnumerator.Current { [DebuggerHidden] get { return <>2__current; } } [DebuggerHidden] public d__0(int <>1__state) { this.<>1__state = <>1__state; <>l__initialThreadId = Environment.CurrentManagedThreadId; } [DebuggerHidden] void IDisposable.Dispose() { 5__1 = null; <>s__2 = null; 5__4 = null; 5__5 = null; <>s__6 = null; 5__8 = null; 5__9 = null; <>1__state = -2; } private bool MoveNext() { int num = <>1__state; if (num != 0) { if (num != 1) { return false; } <>1__state = -1; goto IL_015e; } <>1__state = -1; 5__1 = AccessTools.TypeByName("PerfectRandom.Sulfur.Core.Units.Unit"); if (5__1 == null) { return false; } <>s__2 = AppDomain.CurrentDomain.GetAssemblies(); <>s__3 = 0; goto IL_01b2; IL_017b: if (<>s__7 < <>s__6.Length) { 5__8 = <>s__6[<>s__7]; if (!(5__8 == null) && 5__1.IsAssignableFrom(5__8)) { 5__9 = 5__8.GetMethod("Die", BindingFlags.DeclaredOnly | BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); if (!(5__9 == null) && !5__9.IsAbstract) { if (5__9.GetParameters().Length == 0) { <>2__current = 5__9; <>1__state = 1; return true; } goto IL_015e; } } goto IL_016d; } <>s__6 = null; 5__5 = null; 5__4 = null; goto IL_01a4; IL_016d: <>s__7++; goto IL_017b; IL_01a4: <>s__3++; goto IL_01b2; IL_01b2: if (<>s__3 < <>s__2.Length) { 5__4 = <>s__2[<>s__3]; try { 5__5 = 5__4.GetTypes(); } catch { goto IL_01a4; } <>s__6 = 5__5; <>s__7 = 0; goto IL_017b; } <>s__2 = null; return false; IL_015e: 5__9 = null; 5__8 = null; goto IL_016d; } bool IEnumerator.MoveNext() { //ILSpy generated this explicit interface implementation from .override directive in MoveNext return this.MoveNext(); } [DebuggerHidden] void IEnumerator.Reset() { throw new NotSupportedException(); } [DebuggerHidden] IEnumerator IEnumerable.GetEnumerator() { if (<>1__state == -2 && <>l__initialThreadId == Environment.CurrentManagedThreadId) { <>1__state = 0; return this; } return new d__0(0); } [DebuggerHidden] IEnumerator IEnumerable.GetEnumerator() { return ((IEnumerable)this).GetEnumerator(); } } [IteratorStateMachine(typeof(d__0))] private static IEnumerable TargetMethods() { //yield-return decompiler failed: Unexpected instruction in Iterator.Dispose() return new d__0(-2); } private static void Prefix(object __instance, out bool __state) { __state = Plugin.ShouldRewardBeforeDeath(__instance); } private static void Postfix(object __instance, bool __state) { if (__state) { Plugin.GrantLuckForDeath(__instance); } } }