Please disclose if any significant portion of your mod was created using AI tools by adding the 'AI Generated' category. Failing to do so may result in the mod being removed from Thunderstore.
Decompiled source of AcolyteSacrificeHeal v0.2.1
plugins/AcolyteSacrificeHeal.dll
Decompiled 2 days agousing System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.IO; using System.Reflection; using System.Runtime.CompilerServices; using BepInEx; using BepInEx.Configuration; using BepInEx.Logging; using EntityStates.Acolyte; using EntityStates.Acolyte.Backpack; using EntityStates.Acolyte.Weapon; using EntityStates.LilReaper; using HarmonyLib; using Microsoft.CodeAnalysis; using RoR2; using RoR2.Orbs; using RoR2.UI; using UnityEngine; using UnityEngine.Networking; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: AssemblyVersion("0.0.0.0")] [module: RefSafetyRules(11)] namespace Microsoft.CodeAnalysis { [CompilerGenerated] [Embedded] internal sealed class EmbeddedAttribute : Attribute { } } namespace System.Runtime.CompilerServices { [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 AcolyteSacrificeHeal { internal sealed class AnimaBudget { private readonly HashSet<object> creditedVictims = new HashSet<object>(); internal double GrantedSeconds { get; private set; } internal float Credit(object victim, float secondsPerKill, float maxSeconds) { if (victim == null || !Finite(secondsPerKill) || !Finite(maxSeconds) || secondsPerKill <= 0f || maxSeconds <= 0f) { return 0f; } double num = Math.Max(0.0, (double)maxSeconds - GrantedSeconds); if (num <= 0.0 || !creditedVictims.Add(victim)) { return 0f; } double num2 = Math.Min(secondsPerKill, num); GrantedSeconds += num2; return (float)num2; } private static bool Finite(float value) { if (!float.IsNaN(value)) { return !float.IsInfinity(value); } return false; } } internal sealed class AnimaExtension : IDisposable { private readonly ConfigEntry<bool> enabled; private readonly ConfigEntry<float> secondsPerKill; private readonly ConfigEntry<float> maxSeconds; private readonly ConfigEntry<bool> logExtensions; private readonly ManualLogSource logger; private readonly FieldInfo stopwatch; private readonly ConditionalWeakTable<ReaperActivate, AnimaBudget> budgets = new ConditionalWeakTable<ReaperActivate, AnimaBudget>(); internal bool IsEnabled => enabled.Value; internal float SecondsPerKill => secondsPerKill.Value; internal float MaxBonusSeconds => maxSeconds.Value; internal AnimaExtension(ConfigFile config, ManualLogSource logger) { //IL_005e: Unknown result type (might be due to invalid IL or missing references) //IL_0068: Expected O, but got Unknown //IL_0097: Unknown result type (might be due to invalid IL or missing references) //IL_00a1: Expected O, but got Unknown this.logger = logger; enabled = config.Bind<bool>("Anima", "Enabled", true, "Enemy kills credited to the player in Anima Harvester form extend that transformation. Does not affect Anima Echo."); secondsPerKill = config.Bind<float>("Anima", "SecondsPerKill", 0.2f, new ConfigDescription("Seconds added per eligible enemy kill. Extracted souls, allied kills, and minion kills do not count.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0f, 3f), Array.Empty<object>())); maxSeconds = config.Bind<float>("Anima", "MaxBonusSeconds", 3f, new ConfigDescription("Total bonus seconds available per transformation. Spending bonus time does not refill this allowance. Set to zero to disable.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0f, 3f), Array.Empty<object>())); logExtensions = config.Bind<bool>("Debug", "LogAnimaExtensions", false, "Log bonus time per eligible kill and the total earned this transformation."); stopwatch = typeof(BaseBackpackState).GetField("stopwatch", BindingFlags.Instance | BindingFlags.NonPublic); if (stopwatch == null || stopwatch.FieldType != typeof(float)) { throw new MissingFieldException("Anima Conviction drain timer changed; extension cannot initialize."); } GlobalEventManager.onCharacterDeathServer += OnDeath; logger.LogInfo((object)("Anima kill extension loaded: " + secondsPerKill.Value + "s per kill, maximum " + maxSeconds.Value + "s per transformation; enabled=" + enabled.Value)); } private void OnDeath(DamageReport report) { //IL_007b: Unknown result type (might be due to invalid IL or missing references) //IL_0080: Unknown result type (might be due to invalid IL or missing references) //IL_0085: Unknown result type (might be due to invalid IL or missing references) //IL_008a: Unknown result type (might be due to invalid IL or missing references) if (!NetworkServer.active || !enabled.Value || report == null) { return; } CharacterBody attackerBody = report.attackerBody; HealthComponent victim = report.victim; if (!Object.op_Implicit((Object)(object)attackerBody) || !attackerBody.isPlayerControlled || !Object.op_Implicit((Object)(object)victim) || !Object.op_Implicit((Object)(object)report.victimBody) || !Object.op_Implicit((Object)(object)attackerBody.healthComponent) || !attackerBody.healthComponent.alive || !Object.op_Implicit((Object)(object)attackerBody.teamComponent)) { return; } TeamMask enemyTeams = TeamMask.GetEnemyTeams(attackerBody.teamComponent.teamIndex); if (!((TeamMask)(ref enemyTeams)).HasTeam(report.victimTeamIndex) || Object.op_Implicit((Object)(object)((Component)victim).GetComponent<ExtractedSoulController>()) || Object.op_Implicit((Object)(object)((Component)report.victimBody).GetComponent<ExtractedSoulController>())) { return; } AcolyteFormController component = ((Component)attackerBody).GetComponent<AcolyteFormController>(); if (!Object.op_Implicit((Object)(object)component) || !component.isReaperForm || component.souls <= 0) { return; } EntityStateMachine val = EntityStateMachine.FindByCustomName(((Component)attackerBody).gameObject, "Backpack"); ReaperActivate val2 = (ReaperActivate)(Object.op_Implicit((Object)(object)val) ? /*isinst with value type is only supported in some contexts*/: null); if (val2 == null) { return; } EntityStateMachine[] components = ((Component)attackerBody).GetComponents<EntityStateMachine>(); foreach (EntityStateMachine val3 in components) { if (val3.state is EnterBanish || val3.state is Return) { return; } } float num = (float)stopwatch.GetValue(val2); if (float.IsNaN(num) || float.IsInfinity(num)) { return; } AnimaBudget value = budgets.GetValue(val2, (ReaperActivate _) => new AnimaBudget()); float num2 = value.Credit(victim, secondsPerKill.Value, maxSeconds.Value); if (!(num2 <= 0f)) { stopwatch.SetValue(val2, num - num2); if (logExtensions.Value) { logger.LogInfo((object)("Anima kill: +" + num2.ToString("0.###") + "s; total " + value.GrantedSeconds.ToString("0.###") + "/" + maxSeconds.Value + "s this transformation.")); } } } public void Dispose() { GlobalEventManager.onCharacterDeathServer -= OnDeath; } } [BepInPlugin("local.acolyte.sacrificeheal", "Acolyte Sacrifice Heal", "0.2.1")] public sealed class Plugin : BaseUnityPlugin { private sealed class HitContext { internal GenericDamageOrb Orb; internal HealthComponent Victim; internal HitContext Previous; internal bool Confirmed; } public const string PluginGuid = "local.acolyte.sacrificeheal"; internal static Plugin Instance; private ConfigEntry<float> recoveryPercent; private ConfigEntry<bool> diagnosticLogging; private Harmony harmony; private AnimaExtension animaExtension; private TooltipPresentation presentation; [ThreadStatic] private static HitContext currentHit; private void Awake() { //IL_0035: Unknown result type (might be due to invalid IL or missing references) //IL_003f: Expected O, but got Unknown //IL_00a9: Unknown result type (might be due to invalid IL or missing references) //IL_00b3: Expected O, but got Unknown //IL_00ca: Unknown result type (might be due to invalid IL or missing references) //IL_00df: Unknown result type (might be due to invalid IL or missing references) //IL_00f5: Unknown result type (might be due to invalid IL or missing references) //IL_0100: Expected O, but got Unknown //IL_0100: Expected O, but got Unknown //IL_0100: Expected O, but got Unknown Instance = this; recoveryPercent = ((BaseUnityPlugin)this).Config.Bind<float>("Healing", "MaxHealthPercent", 5f, new ConfigDescription("Maximum percent of max HP transferred from existing recoverable health per successful normal-form Sacrifice hit. No reserve means no bonus heal.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0f, 100f), Array.Empty<object>())); diagnosticLogging = ((BaseUnityPlugin)this).Config.Bind<bool>("Debug", "LogHealing", false, "Log each successful reserve-to-HP transfer for testing."); try { MethodInfo methodInfo = typeof(Sacrifice).GetNestedType("SacrificeOrb", BindingFlags.Public | BindingFlags.NonPublic)?.GetMethod("OnArrival", BindingFlags.DeclaredOnly | BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); if (methodInfo == null) { throw new MissingMethodException("SacrificeOrb.OnArrival: game version unsupported."); } harmony = new Harmony("local.acolyte.sacrificeheal"); harmony.Patch((MethodBase)methodInfo, new HarmonyMethod(typeof(Plugin), "BeginHit", (Type[])null), new HarmonyMethod(typeof(Plugin), "CompleteHit", (Type[])null), (HarmonyMethod)null, new HarmonyMethod(typeof(Plugin), "EndHit", (Type[])null), (HarmonyMethod)null); GlobalEventManager.onServerDamageDealt += OnDamageDealt; try { animaExtension = new AnimaExtension(((BaseUnityPlugin)this).Config, ((BaseUnityPlugin)this).Logger); } catch (Exception ex) { ((BaseUnityPlugin)this).Logger.LogError((object)("Anima extension disabled; R healing remains active: " + ex)); } try { presentation = new TooltipPresentation(recoveryPercent, animaExtension, ((BaseUnityPlugin)this).Logger); } catch (Exception ex2) { ((BaseUnityPlugin)this).Logger.LogError((object)("Ability descriptions unavailable; gameplay remains active: " + ex2)); } ((BaseUnityPlugin)this).Logger.LogInfo((object)("Acolyte Sacrifice Heal 0.2.1 loaded. Normal-form R recovers up to " + recoveryPercent.Value + "% max HP from reserve.")); } catch (Exception ex3) { Harmony obj = harmony; if (obj != null) { obj.UnpatchSelf(); } ((BaseUnityPlugin)this).Logger.LogError((object)("Acolyte Sacrifice Heal could not initialize: " + ex3)); ((Behaviour)this).enabled = false; } } private void OnDestroy() { presentation?.Dispose(); animaExtension?.Dispose(); GlobalEventManager.onServerDamageDealt -= OnDamageDealt; Harmony obj = harmony; if (obj != null) { obj.UnpatchSelf(); } if ((Object)(object)Instance == (Object)(object)this) { Instance = null; } } private static void BeginHit(GenericDamageOrb __instance, out HitContext __state) { __state = new HitContext { Orb = __instance, Previous = currentHit, Victim = (Object.op_Implicit((Object)(object)((Orb)__instance).target) ? ((Orb)__instance).target.healthComponent : null) }; currentHit = __state; } private static void OnDamageDealt(DamageReport report) { //IL_0087: Unknown result type (might be due to invalid IL or missing references) //IL_0099: Unknown result type (might be due to invalid IL or missing references) //IL_00a9: Unknown result type (might be due to invalid IL or missing references) HitContext hitContext = currentHit; if (NetworkServer.active && hitContext != null && !hitContext.Confirmed && report?.damageInfo != null) { DamageInfo damageInfo = report.damageInfo; if (!((Object)(object)report.victim != (Object)(object)hitContext.Victim) && !((Object)(object)report.attacker != (Object)(object)hitContext.Orb.attacker) && !damageInfo.rejected && !(report.damageDealt <= 0f) && !Object.op_Implicit((Object)(object)damageInfo.inflictor) && ((ProcChainMask)(ref damageInfo.procChainMask)).Equals(hitContext.Orb.procChainMask) && damageInfo.damageType.damageSource == hitContext.Orb.damageType.damageSource) { hitContext.Confirmed = true; } } } private static void CompleteHit(HitContext __state) { //IL_009d: Unknown result type (might be due to invalid IL or missing references) //IL_00a3: Invalid comparison between Unknown and I4 //IL_00b6: Unknown result type (might be due to invalid IL or missing references) //IL_00bc: Invalid comparison between Unknown and I4 if (!NetworkServer.active || !__state.Confirmed || !Object.op_Implicit((Object)(object)Instance) || !Object.op_Implicit((Object)(object)__state.Orb.attacker)) { return; } CharacterBody component = __state.Orb.attacker.GetComponent<CharacterBody>(); AcolyteFormController component2 = __state.Orb.attacker.GetComponent<AcolyteFormController>(); if (!Object.op_Implicit((Object)(object)component) || !Object.op_Implicit((Object)(object)component2) || component2.inTransformation || !Object.op_Implicit((Object)(object)component.healthComponent) || !component.healthComponent.alive) { return; } HealthComponent healthComponent = component.healthComponent; bool eclipsePenalty = Object.op_Implicit((Object)(object)component.teamComponent) && (int)component.teamComponent.teamIndex == 1 && Object.op_Implicit((Object)(object)Run.instance) && (int)Run.instance.selectedDifficulty >= 7; float num = RecoveryRules.Amount(healthComponent.fullHealth, healthComponent.health, healthComponent.regainableHealth, Instance.recoveryPercent.Value, component.HasBuff(Buffs.HealingDisabled), eclipsePenalty); if (!(num <= 0f)) { float num2 = healthComponent.SpendRegainableHealthServer(num); healthComponent.Networkhealth = Math.Min(healthComponent.fullHealth, healthComponent.health + num2); if (Instance.diagnosticLogging.Value) { ((BaseUnityPlugin)Instance).Logger.LogInfo((object)("Sacrifice restored " + num2.ToString("0.##") + " HP; reserve remaining " + healthComponent.regainableHealth.ToString("0.##"))); } } } private static Exception EndHit(Exception __exception, HitContext __state) { if (__state != null) { currentHit = __state.Previous; } return __exception; } } internal static class RecoveryRules { internal static float Amount(float fullHealth, float health, float reserve, float percent, bool healingBlocked, bool eclipsePenalty) { if (healingBlocked || !Finite(fullHealth) || !Finite(health) || !Finite(reserve) || !Finite(percent) || fullHealth <= 0f || health <= 0f || reserve <= 0f) { return 0f; } float num = fullHealth * Math.Max(0f, Math.Min(100f, percent)) / 100f; if (eclipsePenalty) { num *= 0.5f; } return Math.Max(0f, Math.Min(num, Math.Min(reserve, fullHealth - health))); } private static bool Finite(float value) { if (!float.IsNaN(value)) { return !float.IsInfinity(value); } return false; } } internal sealed class TooltipPresentation : IDisposable { private static TooltipPresentation active; private readonly ConfigEntry<float> heal; private readonly AnimaExtension anima; private readonly ManualLogSource logger; private readonly Harmony patches = new Harmony("local.acolyte.sacrificeheal.presentation"); private Type rowType; private FieldInfo bodyField; private FieldInfo colorField; private Sprite icon; private Texture2D texture; private bool rowErrorLogged; internal TooltipPresentation(ConfigEntry<float> heal, AnimaExtension anima, ManualLogSource logger) { //IL_0006: Unknown result type (might be due to invalid IL or missing references) //IL_0010: Expected O, but got Unknown //IL_01b4: Unknown result type (might be due to invalid IL or missing references) //IL_01b9: Unknown result type (might be due to invalid IL or missing references) //IL_01c8: Expected O, but got Unknown //IL_01e1: Unknown result type (might be due to invalid IL or missing references) //IL_01ee: Expected O, but got Unknown this.heal = heal; this.anima = anima; this.logger = logger; try { LoadIcon(); Type typeFromHandle = typeof(CharacterSelectController); Type nestedType = typeFromHandle.GetNestedType("BodyInfo", BindingFlags.NonPublic); rowType = typeFromHandle.GetNestedType("StripDisplayData", BindingFlags.NonPublic); bodyField = nestedType?.GetField("bodyPrefabBodyComponent"); colorField = nestedType?.GetField("bodyColor"); if (rowType == null || bodyField == null || colorField == null) { throw new MissingMemberException("Character selection display format changed."); } string[] array = new string[7] { "enabled", "primaryColor", "icon", "titleString", "descriptionString", "keywordString", "actionName" }; foreach (string text in array) { if (rowType.GetField(text) == null) { throw new MissingFieldException(rowType.FullName, text); } } MethodInfo methodInfo = AccessTools.Method(typeof(Language), "GetLocalizedStringByToken", new Type[1] { typeof(string) }, (Type[])null); MethodInfo methodInfo2 = AccessTools.Method(typeFromHandle, "BuildSkillStripDisplayData", (Type[])null, (Type[])null); if (methodInfo == null || methodInfo2 == null) { throw new MissingMethodException("Tooltip method changed."); } active = this; patches.Patch((MethodBase)methodInfo, (HarmonyMethod)null, new HarmonyMethod(typeof(TooltipPresentation), "Describe", (Type[])null) { priority = 0 }, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); patches.Patch((MethodBase)methodInfo2, (HarmonyMethod)null, new HarmonyMethod(typeof(TooltipPresentation), "AddCard", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); logger.LogInfo((object)"Ability descriptions and Lingering Anima information card loaded."); } catch { Dispose(); throw; } } private void LoadIcon() { //IL_0030: Unknown result type (might be due to invalid IL or missing references) //IL_003a: Expected O, but got Unknown //IL_00aa: Unknown result type (might be due to invalid IL or missing references) //IL_00b9: Unknown result type (might be due to invalid IL or missing references) using Stream stream = Assembly.GetExecutingAssembly().GetManifestResourceStream("AcolyteSacrificeHeal.Anima.jpg"); using MemoryStream memoryStream = new MemoryStream(); if (stream == null) { throw new InvalidOperationException("Missing Anima icon."); } stream.CopyTo(memoryStream); texture = new Texture2D(2, 2, (TextureFormat)4, false); ((Object)texture).name = "Acolyte_LingeringAnima"; ((Texture)texture).wrapMode = (TextureWrapMode)1; ((Texture)texture).filterMode = (FilterMode)1; if (!ImageConversion.LoadImage(texture, memoryStream.ToArray(), true)) { throw new InvalidOperationException("Could not decode Anima icon."); } icon = Sprite.Create(texture, new Rect(0f, 0f, (float)((Texture)texture).width, (float)((Texture)texture).height), new Vector2(0.5f, 0.5f), 100f); ((Object)icon).name = ((Object)texture).name; } private static void Describe(string __0, ref string __result) { TooltipPresentation tooltipPresentation = active; if (tooltipPresentation != null) { __result = TooltipText.Describe(__0, __result, tooltipPresentation.heal.Value, tooltipPresentation.anima != null && tooltipPresentation.anima.IsEnabled, tooltipPresentation.anima?.SecondsPerKill ?? 0f, tooltipPresentation.anima?.MaxBonusSeconds ?? 0f); } } private static void AddCard(object[] __args) { TooltipPresentation tooltipPresentation = active; if (tooltipPresentation == null || tooltipPresentation.anima == null) { return; } try { string text = TooltipText.Anima(tooltipPresentation.anima.IsEnabled, tooltipPresentation.anima.SecondsPerKill, tooltipPresentation.anima.MaxBonusSeconds); if (text.Length == 0 || __args == null || __args.Length != 3) { return; } object? value = tooltipPresentation.bodyField.GetValue(__args[1]); CharacterBody val = (CharacterBody)((value is CharacterBody) ? value : null); IList list = __args[2] as IList; if (!Object.op_Implicit((Object)(object)val) || val.baseNameToken != "ACOLYTE_BODY_NAME" || list == null) { return; } foreach (object item in list) { if ((string)tooltipPresentation.rowType.GetField("titleString").GetValue(item) == "Lingering Anima") { return; } } object obj = Activator.CreateInstance(tooltipPresentation.rowType); tooltipPresentation.Set(obj, "enabled", true); tooltipPresentation.Set(obj, "primaryColor", tooltipPresentation.colorField.GetValue(__args[1])); tooltipPresentation.Set(obj, "icon", tooltipPresentation.icon); tooltipPresentation.Set(obj, "titleString", "Lingering Anima"); tooltipPresentation.Set(obj, "descriptionString", "<style=cIsUtility>Passive.</style> " + text); tooltipPresentation.Set(obj, "keywordString", "<style=cKeywordName>Lingering Anima</style><style=cSub>Only enemy kills credited to you count. Extracted souls and minion kills do not count. The bonus resets each transformation and cannot refill during it. Does not affect Anima Echo.</style>"); tooltipPresentation.Set(obj, "actionName", ""); list.Add(obj); } catch (Exception ex) { if (!tooltipPresentation.rowErrorLogged) { tooltipPresentation.rowErrorLogged = true; tooltipPresentation.logger.LogWarning((object)("Could not display Anima information card: " + ex)); } } } private void Set(object row, string name, object value) { rowType.GetField(name).SetValue(row, value); } public void Dispose() { patches.UnpatchSelf(); if (active == this) { active = null; } if (Object.op_Implicit((Object)(object)icon)) { Object.Destroy((Object)(object)icon); } if (Object.op_Implicit((Object)(object)texture)) { Object.Destroy((Object)(object)texture); } } } internal static class TooltipText { internal const string AnimaTitle = "Lingering Anima"; internal const string AnimaDetails = "Only enemy kills credited to you count. Extracted souls and minion kills do not count. The bonus resets each transformation and cannot refill during it. Does not affect Anima Echo."; private static string Number(float value) { return value.ToString("0.###", CultureInfo.InvariantCulture); } private static bool Positive(float value) { if (value > 0f && !float.IsNaN(value)) { return !float.IsInfinity(value); } return false; } internal static string Healing(float percent) { if (!Positive(percent)) { return ""; } return "Successful hits also restore up to <style=cIsHealing>" + Number(percent) + "% of maximum health</style> from your <style=cIsHealing>recoverable health</style>. Requires an existing reserve."; } internal static string Anima(bool enabled, float perKill, float cap) { if (!enabled || !Positive(perKill) || !Positive(cap)) { return ""; } return "While in <style=cIsDamage>Anima Harvester</style> form, your enemy kills extend the transformation by <style=cIsUtility>" + Number(perKill) + "s</style>, up to <style=cIsUtility>" + Number(cap) + " extra seconds per transformation</style>."; } internal static string Append(string original, string extra) { if (string.IsNullOrEmpty(extra) || (original != null && original.Contains(extra))) { return original; } if (!string.IsNullOrEmpty(original)) { return original + "\n\n" + extra; } return extra; } internal static string Describe(string token, string original, float percent, bool enabled, float perKill, float cap) { switch (token) { case "ACOLYTE_SPECIAL_DESCRIPTION": return Append(original, Healing(percent)); case "ACOLYTE_SPECIAL_TRANSFORM_DESCRIPTION": return Append(original, Anima(enabled, perKill, cap)); case "KEYWORD_LITANY": { string text = Anima(enabled, perKill, cap); if (text.Length != 0) { return Append(original, "<style=cKeywordName>Lingering Anima</style><style=cSub>" + text + "\nOnly enemy kills credited to you count. Extracted souls and minion kills do not count. The bonus resets each transformation and cannot refill during it. Does not affect Anima Echo.</style>"); } break; } } return original; } } }