namespace Aeshnidae.Enlightenment; /// /// Stops one player enlightening twice from a single set of requirements. /// /// The design brief asked for an NPC specifically because a command had been /// abusable, so it is worth being precise about what that buys. ACE has no /// enlightenment command - the only route in is EmoteType.Enlightenment (9001), /// which only the Font's emote chain raises - so the NPC route is already the only /// route, and this mod adds no command that grants anything. /// /// What the NPC does not fix by itself is re-entry: an emote chain that reaches /// two InqYesNo confirmations can run the payload twice. In practice the level /// reset closes that window - the second run re-checks requirements and finds a /// level 1 character - but that is a consequence of ResetLevel being on, not a /// guarantee. Turn ResetLevel off and the only thing standing between a player and /// a title farm is this class. So it is explicit rather than incidental: /// /// - Running: held for the duration of one ritual, so a re-entrant call during /// the chain is refused outright. /// - Cooldown: held for ReentryGuardSeconds afterwards, covering two /// confirmations answered a tick apart. /// /// Keyed on character guid rather than account, and dropped on logout, so it never /// grows without bound. /// public static class Guard { private static readonly ConcurrentDictionary Completed = new(); private static readonly ConcurrentDictionary Running = new(); /// /// Claims the right to run a ritual for this player. False means one is already /// running - the caller must not proceed. /// public static bool TryEnter(Player player) => Running.TryAdd(player.Guid.Full, 0); /// Releases the claim and starts the cooldown. Always call from a finally. public static void Exit(Player player, bool completed) { Running.TryRemove(player.Guid.Full, out _); if (completed) Completed[player.Guid.Full] = DateTime.UtcNow; } public static bool IsCoolingDown(Player player, out double secondsRemaining) { secondsRemaining = 0; var seconds = Mod.Settings.ReentryGuardSeconds; if (seconds <= 0 || !Completed.TryGetValue(player.Guid.Full, out var last)) return false; var elapsed = (DateTime.UtcNow - last).TotalSeconds; if (elapsed >= seconds) { Completed.TryRemove(player.Guid.Full, out _); return false; } secondsRemaining = Math.Ceiling(seconds - elapsed); return true; } public static void Forget(Player player) { Completed.TryRemove(player.Guid.Full, out _); Running.TryRemove(player.Guid.Full, out _); } /// Drops everything, for mod unload. public static void Clear() { Completed.Clear(); Running.Clear(); } }