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