namespace Aeshnidae.ResonanceAuras; /// /// /aura. Not /a - the client keeps that one (Allegiance chat) and never sends it. /// public static class Commands { [CommandHandler("aura", AccessLevel.Player, CommandHandlerFlag.RequiresWorld, -1, "Resonance auras: small, permanent conveniences bought with Resonance from your account bank.", "/aura every aura, your rank, the next rank's price\n" + "/aura what it does and what you have\n" + "/aura buy [n] buy n ranks, default 1\n" + "/aura check what the formulas see")] public static void HandleAura(Session session, params string[] parameters) { var player = session.Player; if (player is null) return; if (parameters.Length == 0) { ShowAll(session); return; } var verb = parameters[0].ToLowerInvariant(); switch (verb) { case "buy": Buy(session, parameters.Skip(1).ToArray()); return; case "check": Check(session); return; default: Show(session, string.Join(" ", parameters)); return; } } [CommandHandler("aura-reload", AccessLevel.Admin, CommandHandlerFlag.None, 0, "Restart the Aeshnidae.ResonanceAuras mod, re-reading Settings.json.", "/aura-reload")] public static void HandleReload(Session session, params string[] parameters) { var container = Mod.Container; if (container is null) { Reply(session, $"{Mod.Name} is not loaded."); return; } container.Restart(); Reply(session, $"{Mod.Name} restarted."); } private static void ShowAll(Session session) { var player = session.Player!; var balance = player.Account is null ? 0 : AuraDb.ResonanceBalance(player.Account.AccountId); var sb = new StringBuilder(); sb.AppendLine($"Resonance auras - rank n costs {Mod.Settings.RankPriceBase:N0} x n. Banked Resonance: {balance:N0}"); foreach (var aura in Aura.All) { if (aura.MaxRanks == 0) continue; var ranks = Auras.RanksOf(player, aura.Key); var next = ranks < aura.MaxRanks ? $"next {aura.PriceOfRank(ranks + 1):N0}" : "max"; var afford = ranks < aura.MaxRanks && aura.PriceOfRank(ranks + 1) <= balance ? " *" : ""; sb.AppendLine($" {aura.Name}: {ranks}/{aura.MaxRanks}, {next}{afford}"); } sb.AppendLine("* you can afford the next rank. /aura for what one does; /aura buy to buy."); Reply(session, sb.ToString().TrimEnd()); } private static void Show(Session session, string name) { var player = session.Player!; if (!Aura.TryParse(name, out var aura, out var problem)) { Reply(session, problem); return; } var ranks = Auras.RanksOf(player, aura.Key); var balance = player.Account is null ? 0 : AuraDb.ResonanceBalance(player.Account.AccountId); var sb = new StringBuilder(); sb.AppendLine($"{aura.Name} - each rank: {aura.PerRank}. On retail: {aura.Retail}."); sb.AppendLine($" you have : rank {ranks} of {aura.MaxRanks}"); if (aura.MaxRanks == 0) { sb.Append(" not for sale on this server."); } else if (ranks >= aura.MaxRanks) { sb.Append(" at the maximum."); } else { var remaining = aura.MaxRanks - ranks; sb.AppendLine($" next rank : {aura.PriceOfRank(ranks + 1):N0} Resonance"); if (remaining > 1) sb.AppendLine($" to the max : {aura.PriceOfRanks(ranks, remaining):N0} ({remaining} ranks)"); sb.AppendLine($" banked : {balance:N0}"); sb.Append($" /aura buy {aura.Name.ToLowerInvariant()} to buy the next rank"); } Reply(session, sb.ToString()); } private static void Buy(Session session, string[] parameters) { var player = session.Player!; if (parameters.Length == 0) { Reply(session, "/aura buy [ranks]"); return; } var count = 1; var nameParts = parameters; if (parameters.Length > 1 && int.TryParse(parameters[^1], out var parsed)) { count = parsed; nameParts = parameters[..^1]; } if (!Aura.TryParse(string.Join(" ", nameParts), out var aura, out var problem)) { Reply(session, problem); return; } Auras.Buy(player, aura, count, out var message); Reply(session, message); } /// /// /aura check: what ACE's own formulas see, beside what they should see. /// /// The property patches are postfixes on getters. A caller the JIT had already /// inlined the getter into would read the stored value and never see the ranks - /// silently. Player.GetEncumbranceCapacity and GetBurdenMod are real, hot callers /// of the carrying getter, so calling them here and comparing is a true test of /// whether the patch is reaching old code; the other six getters are the same /// shape and are read by the same kind of caller. /// private static void Check(Session session) { var player = session.Player!; var sb = new StringBuilder(); sb.AppendLine("Resonance auras - stored (retail) + ours = what the getter returns:"); void Row(string label, PropertyInt property, string key, int viaGetter) { var stored = player.GetProperty(property) ?? 0; var ours = Auras.RanksOf(player, key); var ok = viaGetter == stored + ours ? "ok" : "NOT SEEN"; sb.AppendLine($" {label,-22} {stored} + {ours} = {viaGetter} {ok}"); } Row("spell duration", PropertyInt.AugmentationIncreasedSpellDuration, Aura.SpellDuration, player.AugmentationIncreasedSpellDuration); Row("item mana usage", PropertyInt.LumAugItemManaUsage, Aura.ItemMana, player.LumAugItemManaUsage); Row("imbue chance", PropertyInt.AugmentationBonusImbueChance, Aura.Imbue, player.AugmentationBonusImbueChance); Row("item mana gain", PropertyInt.LumAugItemManaGain, Aura.ManaStone, player.LumAugItemManaGain); Row("salvage", PropertyInt.AugmentationBonusSalvage, Aura.Salvage, player.AugmentationBonusSalvage); Row("carrying", PropertyInt.AugmentationIncreasedCarryingCapacity, Aura.Carry, player.AugmentationIncreasedCarryingCapacity); Row("faster regen", PropertyInt.AugmentationFasterRegen, Aura.Regen, player.AugmentationFasterRegen); // The real test: ACE's own callers, compiled long before this mod loaded. var strength = (int)player.Strength.Current; var storedCarry = player.GetProperty(PropertyInt.AugmentationIncreasedCarryingCapacity) ?? 0; var expectedAugs = storedCarry + Auras.RanksOf(player, Aura.Carry); var expectedCapacity = 150 * strength + expectedAugs * 30 * strength; var actualCapacity = player.GetEncumbranceCapacity(); var expectedBurdenCapacity = 150 * strength + strength * Math.Min(30 * expectedAugs, 150 + 30 * (Aura.ByKey(Aura.Carry)?.MaxRanks ?? 0)); var actualBurdenCapacity = ACE.Server.Physics.Common.EncumbranceSystem.EncumbranceCapacity(strength, player.AugmentationIncreasedCarryingCapacity); sb.AppendLine($"ACE's own callers (strength {strength}):"); sb.AppendLine($" pack capacity {actualCapacity:N0} expected {expectedCapacity:N0} {(actualCapacity == expectedCapacity ? "ok - the getter patch reaches old code" : "NOT SEEN - the getter was inlined into this caller")}"); sb.AppendLine($" burden capacity {actualBurdenCapacity:N0} expected {expectedBurdenCapacity:N0} {(actualBurdenCapacity == expectedBurdenCapacity ? "ok" : "NOT SEEN")}"); sb.Append($" burden now {player.GetBurdenMod():F2} (1.00 is unburdened)"); Reply(session, sb.ToString()); } /// /// Ranks without the Resonance, for testing. Its own command rather than a verb /// of /aura so that it carries an admin access level, which is what puts it in /// Aeshnidae.AdminAudit's trail like every other privileged command. /// [CommandHandler("aura-grant", AccessLevel.Admin, CommandHandlerFlag.RequiresWorld, 3, "Give (or, with a negative count, take) aura ranks without paying. Testing.", "/aura-grant ")] public static void HandleGrant(Session session, params string[] parameters) { if (parameters.Length < 3 || !int.TryParse(parameters[^1], out var delta)) { Reply(session, "/aura-grant (negative takes ranks away)"); return; } var targetName = parameters[0]; var target = PlayerManager.GetOnlinePlayer(targetName); if (target is null) { Reply(session, $"{targetName} is not online - a grant needs them in the world so the ranks take effect."); return; } if (!Aura.TryParse(string.Join(" ", parameters[1..^1]), out var aura, out var problem)) { Reply(session, problem); return; } var result = Auras.Grant(target, aura, delta); Reply(session, result); ModManager.Log($"[{Mod.Name}] {session.Player?.Name} granted: {result}"); if (target != session.Player) target.SendMessage($"An admin set your {aura.Name} aura: {result[(result.IndexOf(':') + 1)..].Trim()}"); } /// /// One chat message per line, with any carriage return stripped - the client /// renders a stray CR as a musical note. The house pattern. /// private static void Reply(Session? session, string message) { if (session?.Player is null) { ModManager.Log(message); return; } foreach (var line in (message ?? "").Split('\n')) { var text = line.TrimEnd('\r'); if (!string.IsNullOrWhiteSpace(text)) session.Network.EnqueueSend(new GameMessageSystemChat(text, ChatMessageType.Broadcast)); } } }