namespace Aeshnidae.ResonanceAuras; /// /// Where the ranks become real. /// /// Seven of the auras are a rank count added to a Player property that ACE's own /// formulas already read - the same properties the retail augmentation gems and /// luminance auras write. The patch is a postfix on the typed getter, so the /// stored property is never touched: the gem device and the luminance vendor check /// their caps through GetProperty (raw) and Aeshnidae.Enlightenment checks its /// prerequisites the same way, so our ranks neither block a retail purchase nor /// count towards enlightenment. Enlightenment's reset writes 0 through the typed /// setter, which removes the stored property and leaves ours alone. /// /// The two with no retail counterpart - components and ammunition - are patched at /// the moment ACE spends the thing. /// /// Every patch guards itself: these sit on hot paths (a vital tick, every spell) /// and ACE catches nothing on our behalf. /// [HarmonyPatch] public static class Patches { // ------------------------------------------------------------------ cache [HarmonyPostfix] [HarmonyPatch(typeof(Player), nameof(Player.PlayerEnterWorld))] public static void PostPlayerEnterWorld(Player __instance) { try { Auras.LoadInto(__instance); Auras.TellClientOnLogin(__instance); } catch (Exception ex) { ModManager.Log($"[{Mod.Name}] failed to load auras on login: {ex}", ModManager.LogLevel.Error); } } // ------------------------------------------------- the property-backed seven private static int Ranks(Player player, string key) { try { return Auras.RanksOf(player, key); } catch (Exception ex) { ModManager.Log($"[{Mod.Name}] {key} lookup failed: {ex.Message}", ModManager.LogLevel.Error); return 0; } } /// Lasting Enchantments: EnchantmentManager multiplies duration by 1 + 0.2 x this. [HarmonyPostfix] [HarmonyPatch(typeof(Player), nameof(Player.AugmentationIncreasedSpellDuration), MethodType.Getter)] public static void PostSpellDuration(Player __instance, ref int __result) => __result += Ranks(__instance, Aura.SpellDuration); /// Frugal Mana: Player_Tick scales item mana burn by GetNegativeRatingMod(5 x this). [HarmonyPostfix] [HarmonyPatch(typeof(Player), nameof(Player.LumAugItemManaUsage), MethodType.Getter)] public static void PostItemManaUsage(Player __instance, ref int __result) => __result += Ranks(__instance, Aura.ItemMana); /// Charmed Hands: RecipeManager adds 0.05 x this to the imbue success chance. [HarmonyPostfix] [HarmonyPatch(typeof(Player), nameof(Player.AugmentationBonusImbueChance), MethodType.Getter)] public static void PostImbueChance(Player __instance, ref int __result) => __result += Ranks(__instance, Aura.Imbue); /// Generous Stones: ManaStone scales the ration by GetPositiveRatingMod(5 x this). [HarmonyPostfix] [HarmonyPatch(typeof(Player), nameof(Player.LumAugItemManaGain), MethodType.Getter)] public static void PostItemManaGain(Player __instance, ref int __result) => __result += Ranks(__instance, Aura.ManaStone); /// Keen Salvager: Player_Crafting multiplies value by 1 + 0.25 x this and units through CalcNumUnits. [HarmonyPostfix] [HarmonyPatch(typeof(Player), nameof(Player.AugmentationBonusSalvage), MethodType.Getter)] public static void PostSalvage(Player __instance, ref int __result) => __result += Ranks(__instance, Aura.Salvage); /// Strong Back: capacity is 150 x strength + 30 x strength x this (see PostEncumbranceCapacity for the cap). [HarmonyPostfix] [HarmonyPatch(typeof(Player), nameof(Player.AugmentationIncreasedCarryingCapacity), MethodType.Getter)] public static void PostCarryingCapacity(Player __instance, ref int __result) => __result += Ranks(__instance, Aura.Carry); /// Deep Rest: Creature_Vitals adds this to the regen multiplier while lying down. [HarmonyPostfix] [HarmonyPatch(typeof(Player), nameof(Player.AugmentationFasterRegen), MethodType.Getter)] public static void PostFasterRegen(Player __instance, ref int __result) => __result += Ranks(__instance, Aura.Regen); /// /// The burden formula used for movement, jumping and the burden penalty caps the /// augmentation bonus at 150 (five gems' worth), which would make Strong Back /// worthless to anyone who owns the five retail gems. The pack-space check in /// Player.GetEncumbranceCapacity has no such cap. Lift it by our maximum, so the /// two agree: only a count above five can get here, and only our aura makes one. /// [HarmonyPostfix] [HarmonyPatch(typeof(ACE.Server.Physics.Common.EncumbranceSystem), nameof(ACE.Server.Physics.Common.EncumbranceSystem.EncumbranceCapacity))] public static void PostEncumbranceCapacity(int strength, int numAugs, ref int __result) { try { if (strength <= 0 || numAugs <= 5) return; var ours = Aura.ByKey(Aura.Carry)?.MaxRanks ?? 0; var bonusBurden = Math.Min(30 * numAugs, 150 + 30 * ours); __result = 150 * strength + strength * bonusBurden; } catch (Exception ex) { ModManager.Log($"[{Mod.Name}] capacity cap failed: {ex.Message}", ModManager.LogLevel.Error); } } // ------------------------------------------------------- components and ammo /// /// Thrifty Caster. Spell.TryBurnComponents has already rolled which components /// burn; each one gets a second roll to survive. Runs before Player.TryBurnComponents /// takes them from the pack and names them in chat, so what the player is told /// burned is what burned. /// [HarmonyPostfix] [HarmonyPatch(typeof(Spell), nameof(Spell.TryBurnComponents))] public static void PostTryBurnComponents(Player player, ref List __result) { try { if (__result is null || __result.Count == 0) return; var chance = Ranks(player, Aura.Components) * Mod.Settings.ComponentSaveChancePerRank; if (chance <= 0) return; for (var i = __result.Count - 1; i >= 0; i--) if (ThreadSafeRandom.Next(0.0f, 1.0f) < chance) __result.RemoveAt(i); } catch (Exception ex) { ModManager.Log($"[{Mod.Name}] component save failed: {ex.Message}", ModManager.LogLevel.Error); } } /// /// Steady Quiver. Player.UpdateAmmoAfterLaunch hides the held ammo and then /// spends one; on a save it is only hidden, and the reload animation that /// follows parents it back into the hand as it always does. The last one in the /// stack is never saved: Player.LaunchProjectile has already decided from the /// stack size that the player is out of ammunition, and a saved arrow the /// client believes is gone would be worse than a spent one. /// [HarmonyPrefix] [HarmonyPatch(typeof(Player), nameof(Player.UpdateAmmoAfterLaunch))] public static bool PreUpdateAmmoAfterLaunch(Player __instance, WorldObject ammo) { try { if (ammo is null || ammo.UnlimitedUse || ammo.StackSize is null || ammo.StackSize <= 1) return true; var chance = Ranks(__instance, Aura.Ammo) * Mod.Settings.AmmoSaveChancePerRank; if (chance <= 0 || ThreadSafeRandom.Next(0.0f, 1.0f) >= chance) return true; // Saved: do the part of the original that is not the spending. __instance.EnqueueBroadcast(new GameMessagePickupEvent(ammo)); return false; } catch (Exception ex) { ModManager.Log($"[{Mod.Name}] ammo save failed: {ex.Message}", ModManager.LogLevel.Error); return true; } } }