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;
+ });
+ }
+ }
+ }
+}