diff --git a/R2API/RecalculateStatsAPI.cs b/R2API/RecalculateStatsAPI.cs new file mode 100644 index 00000000..f745bb88 --- /dev/null +++ b/R2API/RecalculateStatsAPI.cs @@ -0,0 +1,344 @@ +using Mono.Cecil.Cil; +using MonoMod.Cil; +using R2API.Utils; +using RoR2; +using System; + +namespace R2API { + + /// + /// API for computing bonuses granted by factors inside RecalculateStats. + /// + [R2APISubmodule] + public static class RecalculateStatsAPI { + + /// + /// Return true if the submodule is loaded. + /// + public static bool Loaded { + get => _loaded; + internal set => _loaded = value; + } + + private static bool _loaded; + + [R2APISubmoduleInit(Stage = InitStage.SetHooks)] + internal static void SetHooks() { + IL.RoR2.CharacterBody.RecalculateStats += HookRecalculateStats; + } + + [R2APISubmoduleInit(Stage = InitStage.UnsetHooks)] + internal static void UnsetHooks() { + IL.RoR2.CharacterBody.RecalculateStats -= HookRecalculateStats; + } + + /// + /// A collection of modifiers for various stats. It will be passed down the event chain of GetStatCoefficients; add to the contained values to modify stats. + /// + public class StatHookEventArgs : EventArgs { + + /// Added to the direct multiplier to base health. MAX_HEALTH ~ (BASE_HEALTH + baseHealthAdd) * (HEALTH_MULT + healthMultAdd). + public float healthMultAdd = 0f; + + /// Added to base health. MAX_HEALTH ~ (BASE_HEALTH + baseHealthAdd) * (HEALTH_MULT + healthMultAdd). + public float baseHealthAdd = 0f; + + /// Added to base shield. MAX_SHIELD ~ BASE_SHIELD + baseShieldAdd. + public float baseShieldAdd = 0f; + + /// Added to the direct multiplier to base health regen. HEALTH_REGEN ~ (BASE_REGEN + baseRegenAdd) * (REGEN_MULT + regenMultAdd). + public float regenMultAdd = 0f; + + /// Added to base health regen. HEALTH_REGEN ~ (BASE_REGEN + baseRegenAdd) * (REGEN_MULT + regenMultAdd). + public float baseRegenAdd = 0f; + + /// Added to base move speed. MOVE_SPEED ~ (BASE_MOVE_SPEED + baseMoveSpeedAdd) * (MOVE_SPEED_MULT + moveSpeedMultAdd) + public float baseMoveSpeedAdd = 0f; + + /// Added to the direct multiplier to move speed. MOVE_SPEED ~ (BASE_MOVE_SPEED + baseMoveSpeedAdd) * (MOVE_SPEED_MULT + moveSpeedMultAdd) + public float moveSpeedMultAdd = 0f; + + /// Added to the direct multiplier to jump power. JUMP_POWER ~ BASE_JUMP_POWER * (JUMP_POWER_MULT + jumpPowerMultAdd) + public float jumpPowerMultAdd = 0f; + + /// Added to the direct multiplier to base damage. DAMAGE ~ (BASE_DAMAGE + baseDamageAdd) * (DAMAGE_MULT + damageMultAdd). + public float damageMultAdd = 0f; + + /// Added to base damage. DAMAGE ~ (BASE_DAMAGE + baseDamageAdd) * (DAMAGE_MULT + damageMultAdd). + public float baseDamageAdd = 0f; + + /// Added to attack speed. ATTACK_SPEED ~ (BASE_ATTACK_SPEED + baseAttackSpeedAdd) * (ATTACK_SPEED_MULT + attackSpeedMultAdd). + public float baseAttackSpeedAdd = 0f; + + /// Added to the direct multiplier to attack speed. ATTACK_SPEED ~ (BASE_ATTACK_SPEED + baseAttackSpeedAdd) * (ATTACK_SPEED_MULT + attackSpeedMultAdd). + public float attackSpeedMultAdd = 0f; + + /// Added to crit chance. CRIT_CHANCE ~ BASE_CRIT_CHANCE + critAdd. + public float critAdd = 0f; + + /// Added to armor. ARMOR ~ BASE_ARMOR + armorAdd. + public float armorAdd = 0f; + } + + /// + /// Used as the delegate type for the GetStatCoefficients event. + /// + /// The CharacterBody which RecalculateStats is being called for. + /// An instance of StatHookEventArgs, passed to each subscriber to this event in turn for modification. + public delegate void StatHookEventHandler(CharacterBody sender, StatHookEventArgs args); + + /// + /// Subscribe to this event to modify one of the stat hooks which StatHookEventArgs covers. Fired during CharacterBody.RecalculateStats. + /// + public static event StatHookEventHandler GetStatCoefficients; + + private static void HookRecalculateStats(ILContext il) { + ILCursor c = new ILCursor(il); + + StatHookEventArgs statMods = null; + c.Emit(OpCodes.Ldarg_0); + c.EmitDelegate>((cb) => { + statMods = new StatHookEventArgs(); + GetStatCoefficients?.Invoke(cb, statMods); + }); + + ModifyHealthStat(c, statMods); + ModifyShieldStat(c, statMods); + ModifyHealthRegenStat(c, statMods); + ModifyMovementSpeedStat(c, statMods); + ModifyJumpStat(c, statMods); + ModifyDamageStat(c, statMods); + ModifyAttackSpeedStat(c, statMods); + ModifyCritStat(c, statMods); + ModifyArmorStat(c, statMods); + } + + private static void ModifyArmorStat(ILCursor c, StatHookEventArgs statMods) { + c.Index = 0; + + bool ILFound = c.TryGotoNext( + x => x.MatchLdfld("baseArmor") + ) && c.TryGotoNext( + x => x.MatchCallOrCallvirt("get_armor") + ) && c.TryGotoNext(MoveType.After, + x => x.MatchCallOrCallvirt("get_armor") + ); + + if (ILFound) { + c.EmitDelegate>((oldArmor) => { + return oldArmor + statMods.armorAdd; + }); + } + } + + private static void ModifyAttackSpeedStat(ILCursor c, StatHookEventArgs statMods) { + c.Index = 0; + + int locBaseAttackSpeedIndex = -1; + int locAttackSpeedMultIndex = -1; + bool ILFound = c.TryGotoNext( + x => x.MatchLdfld("baseAttackSpeed"), + x => x.MatchLdarg(0), + x => x.MatchLdfld("levelAttackSpeed") + ) && c.TryGotoNext( + x => x.MatchStloc(out locBaseAttackSpeedIndex) + ) && c.TryGotoNext( + x => x.MatchLdloc(locBaseAttackSpeedIndex), + x => x.MatchLdloc(out locAttackSpeedMultIndex), + x => x.MatchMul(), + x => x.MatchStloc(locBaseAttackSpeedIndex) + ); + + if (ILFound) { + c.GotoPrev(x => x.MatchLdfld("baseAttackSpeed")); + c.GotoNext(x => x.MatchStloc(locBaseAttackSpeedIndex)); + c.EmitDelegate>((origSpeed) => { + return origSpeed + statMods.baseAttackSpeedAdd; + }); + c.GotoNext(x => x.MatchStloc(locAttackSpeedMultIndex)); + c.EmitDelegate>((origSpeedMult) => { + return origSpeedMult + statMods.attackSpeedMultAdd; + }); + } + } + + private static void ModifyCritStat(ILCursor c, StatHookEventArgs statMods) { + c.Index = 0; + + int locOrigCrit = -1; + bool ILFound = c.TryGotoNext( + x => x.MatchLdarg(0), + x => x.MatchLdloc(out locOrigCrit), + x => x.MatchCallOrCallvirt("set_crit")); + + if (ILFound) { + c.Emit(OpCodes.Ldloc, locOrigCrit); + c.EmitDelegate>((origCrit) => { + return origCrit + statMods.critAdd; + }); + c.Emit(OpCodes.Stloc, locOrigCrit); + } + } + + private static void ModifyDamageStat(ILCursor c, StatHookEventArgs statMods) { + c.Index = 0; + + int locBaseDamageIndex = -1; + int locDamageMultIndex = -1; + bool ILFound = c.TryGotoNext( + x => x.MatchLdfld("baseDamage"), + x => x.MatchLdarg(0), + x => x.MatchLdfld("levelDamage") + ) && c.TryGotoNext( + x => x.MatchStloc(out locBaseDamageIndex) + ) && c.TryGotoNext( + x => x.MatchLdloc(locBaseDamageIndex), + x => x.MatchLdloc(out locDamageMultIndex), + x => x.MatchMul(), + x => x.MatchStloc(locBaseDamageIndex) + ); + + if (ILFound) { + c.GotoPrev(x => x.MatchLdfld("baseDamage")); + c.GotoNext(x => x.MatchStloc(locBaseDamageIndex)); + c.EmitDelegate>((origDamage) => { + return origDamage + statMods.baseDamageAdd; + }); + c.GotoNext(x => x.MatchStloc(locDamageMultIndex)); + c.EmitDelegate>((origDamageMult) => { + return origDamageMult + statMods.damageMultAdd; + }); + } + } + + private static void ModifyJumpStat(ILCursor c, StatHookEventArgs statMods) { + c.Index = 0; + + bool ILFound = c.TryGotoNext(MoveType.After, + x => x.MatchLdfld("baseJumpPower"), + x => x.MatchLdarg(0), + x => x.MatchLdfld("levelJumpPower"), + x => x.MatchLdloc(out _), + x => x.MatchMul(), + x => x.MatchAdd()); + + if (ILFound) { + c.EmitDelegate>((origJumpPower) => { + return origJumpPower * (1 + statMods.jumpPowerMultAdd); + }); + } + } + + private static void ModifyHealthStat(ILCursor c, StatHookEventArgs statMods) { + c.Index = 0; + + int locBaseHealthIndex = -1; + int locHealthMultIndex = -1; + bool ILFound = c.TryGotoNext( + x => x.MatchLdfld("baseMaxHealth"), + x => x.MatchLdarg(0), + x => x.MatchLdfld("levelMaxHealth") + ) && c.TryGotoNext( + x => x.MatchStloc(out locBaseHealthIndex) + ) && c.TryGotoNext( + x => x.MatchLdloc(locBaseHealthIndex), + x => x.MatchLdloc(out locHealthMultIndex), + x => x.MatchMul(), + x => x.MatchStloc(locBaseHealthIndex) + ); + + if (ILFound) { + c.GotoPrev(x => x.MatchLdfld("baseMaxHealth")); + c.GotoNext(x => x.MatchStloc(locBaseHealthIndex)); + c.EmitDelegate>((origMaxHealth) => { + return origMaxHealth + statMods.baseHealthAdd; + }); + c.GotoNext(x => x.MatchStloc(locHealthMultIndex)); + c.EmitDelegate>((origHealthMult) => { + return origHealthMult + statMods.healthMultAdd; + }); + } + } + + private static void ModifyShieldStat(ILCursor c, StatHookEventArgs statMods) { + c.Index = 0; + + int locBaseShieldIndex = -1; + bool ILFound = c.TryGotoNext( + x => x.MatchLdfld("baseMaxShield"), + x => x.MatchLdarg(0), + x => x.MatchLdfld("levelMaxShield") + ) && c.TryGotoNext( + x => x.MatchStloc(out locBaseShieldIndex) + ); + + if (ILFound) { + c.EmitDelegate>((origBaseShield) => { + return origBaseShield + statMods.baseShieldAdd; + }); + } + } + + private static void ModifyHealthRegenStat(ILCursor c, StatHookEventArgs statMods) { + c.Index = 0; + + int locRegenMultIndex = -1; + int locFinalRegenIndex = -1; + bool ILFound = c.TryGotoNext( + x => x.MatchLdloc(out locFinalRegenIndex), + x => x.MatchCallOrCallvirt("set_regen") + ) && c.TryGotoPrev( + x => x.MatchAdd(), + x => x.MatchLdloc(out locRegenMultIndex), + x => x.MatchMul(), + x => x.MatchStloc(out locFinalRegenIndex) + ); + + if (ILFound) { + c.GotoNext(x => x.MatchLdloc(out locRegenMultIndex)); + c.EmitDelegate>(() => { + return statMods.baseRegenAdd; + }); + c.Emit(OpCodes.Add); + c.GotoNext(x => x.MatchMul()); + c.EmitDelegate>((origRegenMult) => { + return origRegenMult + statMods.regenMultAdd; + }); + } + } + + private static void ModifyMovementSpeedStat(ILCursor c, StatHookEventArgs statMods) { + c.Index = 0; + + int locBaseSpeedIndex = -1; + int locSpeedMultIndex = -1; + int locSpeedDivIndex = -1; + bool ILFound = c.TryGotoNext( + x => x.MatchLdfld("baseMoveSpeed"), + x => x.MatchLdarg(0), + x => x.MatchLdfld("levelMoveSpeed") + ) && c.TryGotoNext( + x => x.MatchStloc(out locBaseSpeedIndex) + ) && c.TryGotoNext( + x => x.MatchLdloc(locBaseSpeedIndex), + x => x.MatchLdloc(out locSpeedMultIndex), + x => x.MatchLdloc(out locSpeedDivIndex), + x => x.MatchDiv(), + x => x.MatchMul(), + x => x.MatchStloc(locBaseSpeedIndex) + ); + + if (ILFound) { + c.GotoPrev(x => x.MatchLdfld("levelMoveSpeed")); + c.GotoNext(x => x.MatchStloc(locBaseSpeedIndex)); + c.EmitDelegate>((origBaseMoveSpeed) => { + return origBaseMoveSpeed + statMods.baseMoveSpeedAdd; + }); + c.GotoNext(x => x.MatchStloc(locSpeedMultIndex)); + c.EmitDelegate>((origMoveSpeedMult) => { + return origMoveSpeedMult + statMods.moveSpeedMultAdd; + }); + } + } + } +}