namespace Aeshnidae.AllegianceBounty; public static class Commands { [CommandHandler("bounty", AccessLevel.Player, CommandHandlerFlag.RequiresWorld, 0, "Your allegiance's bounty: the quests completed in the last day and the kill-experience bonus they add up to.", "/bounty the active entries, the total, the size band, the settings\n" + "/bounty quests the quest list and what each is worth")] public static void HandleBounty(Session session, params string[] parameters) { var player = session.Player; if (player is null) return; var s = Mod.Settings; if (parameters.Length > 0 && parameters[0].StartsWith("quest", StringComparison.OrdinalIgnoreCase)) { var sb = new StringBuilder(); sb.AppendLine($"Bounty quests ({QuestList.Detectable} of {QuestList.Entries.Count} detectable):"); foreach (var group in QuestList.Entries.Where(e => e.Flags.Count > 0).GroupBy(e => e.Kind).OrderBy(g => g.Key)) sb.AppendLine($" {group.Key} (+{group.First().EffectivePercent(s):0.##}%): {string.Join(", ", group.Select(e => e.Name).OrderBy(n => n))}"); Reply(session, sb.ToString()); return; } if (!s.Enabled) { Reply(session, "The bounty is switched off."); return; } var monarch = Bounty.MonarchOf(player); var head = new StringBuilder(); head.AppendLine($"Allegiance bounty - every listed quest a member completes adds to the kill experience of the whole allegiance for {s.HoursActive:0.#} hours: " + $"+{s.QuestPercent:0.##}% per quest, +{s.KillTaskPercent:0.##}% per kill task, once per quest per allegiance per day" + (s.MaxPercent > 0 ? $", capped at {s.MaxPercent:0.##}%" : ", no cap") + $"; the bigger the allegiance the smaller each one counts ({string.Join(", ", s.SizeBands.Select(b => b.UpToMembers > 0 ? $"to {b.UpToMembers}: x{b.Multiplier:0.##}" : $"above: x{b.Multiplier:0.##}"))})."); if (monarch is null) { head.AppendLine(" you : not sworn to an allegiance - swear to one and its bounty applies to your kills too."); head.Append(PersonalLine(player)); Reply(session, head.ToString()); return; } var summary = Bounty.Summary(player); var entries = Bounty.EntriesOf(monarch.Value); var now = DateTime.UtcNow; head.AppendLine($" your allegiance: {summary.Members} member{(summary.Members == 1 ? "" : "s")} ({summary.Band}: x{summary.Multiplier:0.##}), {entries.Count} quest{(entries.Count == 1 ? "" : "s")} active"); head.AppendLine($" bounty : +{summary.Raw:0.##}% raw x{summary.Multiplier:0.##} = +{summary.Effective:0.##}% on every member's kill experience" + (s.ApplyToFellowshipShares ? " (fellowship shares of kills too)" : "") + (s.ApplyToQuestExperience ? " and quest experience" : "")); foreach (var e in entries) { var left = e.ExpiresAt - now; var when = left.TotalHours >= 1 ? $"{(int)left.TotalHours}h {left.Minutes:00}m" : $"{Math.Max(0, left.Minutes)}m"; head.AppendLine($" +{e.Percent:0.##}% {e.QuestName} - {e.CompletedBy}, {when} left"); } if (entries.Count == 0) head.AppendLine(" nothing active - complete a listed quest (/bounty quests) and the allegiance's kills are up for a day."); head.Append(PersonalLine(player)); Reply(session, head.ToString()); } /// "Today: N distinct quest stamps, +X% (cap 20%), resets at 17:00". private static string PersonalLine(Player player) { var s = Mod.Settings; if (!s.PersonalEnabled) return " today : personal bounty off"; var stamps = Personal.StampsToday(player); var pct = Personal.PercentOf(stamps); var cap = s.PersonalMaxPercent > 0 ? $" (cap {s.PersonalMaxPercent:0.##}%)" : " (no cap)"; return $" today : {stamps} distinct quest stamp{(stamps == 1 ? "" : "s")}, +{pct:0.##}%{cap}, resets at {Personal.NextReset():HH:mm} - " + $"+{s.PersonalPercentPerStamp:0.##}% per {(s.PersonalAnyQuest ? "quest flag" : "listed quest")} you stamp, each once a day, on your own kills, on top of the allegiance's"; } [CommandHandler("bounty-reload", AccessLevel.Admin, CommandHandlerFlag.None, 0, "Re-read Settings.json and Quests.json for Aeshnidae.AllegianceBounty without restarting it. Active entries are kept.", "/bounty-reload")] public static void HandleReload(Session session, params string[] parameters) { try { Mod.ReloadFiles(); Reply(session, $"{Mod.Name}: settings and quest list re-read - {QuestList.Entries.Count} quests, {QuestList.Detectable} detectable, " + $"{Bounty.ActiveEntries} entries active across {Bounty.ActiveAllegiances} allegiances."); } catch (Exception ex) { Reply(session, $"{Mod.Name}: reload failed: {ex.Message}"); } } [CommandHandler("bounty-grant", AccessLevel.Admin, CommandHandlerFlag.None, 2, "Record a quest completion for a player's allegiance as if they had just turned it in. Testing.", "/bounty-grant the quest name may be a prefix; use quotes or just type it")] public static void HandleGrant(Session session, params string[] parameters) { if (parameters.Length < 2) { Reply(session, "/bounty-grant "); return; } // The player name is the first word (or the first quoted run); the rest is the quest. var (playerName, questName) = SplitNameAndQuest(parameters); var target = PlayerManager.GetOnlinePlayer(playerName); if (target is null) { Reply(session, $"'{playerName}' is not online."); return; } var quest = QuestList.ByName(questName); if (quest is null) { Reply(session, $"No listed quest matches '{questName}'. /bounty quests lists them."); return; } var monarch = Bounty.MonarchOf(target); if (monarch is null) { Reply(session, $"{target.Name} is not sworn to an allegiance; a bounty needs one."); return; } if (!BountyDb.Ready) { Reply(session, "The bounty table is not open; nothing recorded."); return; } var flag = quest.Flags.FirstOrDefault() ?? "(granted)"; var added = Bounty.Gain(target, monarch.Value, quest, flag, "was granted"); var summary = Bounty.Summary(target); Reply(session, added ? $"Recorded '{quest.Name}' (+{quest.EffectivePercent(Mod.Settings):0.##}%) for {target.Name}'s allegiance: now +{summary.Raw:0.##}% raw x{summary.Multiplier:0.##} ({summary.Members} members) = +{summary.Effective:0.##}%, {summary.Count} active." : $"{target.Name}'s allegiance already has '{quest.Name}' active; nothing added (+{summary.Effective:0.##}% stands)."); ModManager.Log($"[{Mod.Name}] {session?.Player?.Name ?? "console"} granted '{quest.Name}' to {target.Name}'s allegiance: {(added ? "added" : "already active")}"); } private static (string player, string quest) SplitNameAndQuest(string[] parameters) { var joined = string.Join(" ", parameters); if (joined.StartsWith('"')) { var end = joined.IndexOf('"', 1); if (end > 0) return (joined[1..end], joined[(end + 1)..].Trim().Trim('"')); } return (parameters[0], string.Join(" ", parameters.Skip(1)).Trim('"')); } /// The client renders an embedded newline as a music note: one SendMessage per line. 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.Player.SendMessage(text); } } }