From 734d20cbaf5d0c3a373a7f59148b105d433830a7 Mon Sep 17 00:00:00 2001 From: Kamron Batman <3953314+kamronbatman@users.noreply.github.com> Date: Fri, 19 May 2023 16:32:51 -0700 Subject: [PATCH] fix: Overhauls antimacro system (#1403) ### Summary - [X] Moves AntiMacro to it's own system. - [X] Removes antimacro from PlayerMobile. - [X] Adds `LastExpiration` to antimacro to easily clear out all antimacro tracking when a player logs out, or during world save. - [X] Optimizes the code somewhat. --- Projects/UOContent/Mobiles/PlayerMobile.cs | 72 ----- Projects/UOContent/Skills/AntiMacroSystem.cs | 283 ++++++++++++++++++ .../UOContent/{Misc => Skills}/SkillCheck.cs | 124 +------- 3 files changed, 286 insertions(+), 193 deletions(-) create mode 100644 Projects/UOContent/Skills/AntiMacroSystem.cs rename Projects/UOContent/{Misc => Skills}/SkillCheck.cs (74%) diff --git a/Projects/UOContent/Mobiles/PlayerMobile.cs b/Projects/UOContent/Mobiles/PlayerMobile.cs index cc42e627c..f1a23eb6c 100644 --- a/Projects/UOContent/Mobiles/PlayerMobile.cs +++ b/Projects/UOContent/Mobiles/PlayerMobile.cs @@ -141,8 +141,6 @@ namespace Server.Mobiles new(268, 624, 15) }; - private readonly Dictionary> m_AntiMacroTable; - private Dictionary m_AcquiredRecipes; private List m_AllFollowers; @@ -206,7 +204,6 @@ namespace Server.Mobiles VisibilityList = new List(); PermaFlags = new List(); - m_AntiMacroTable = new Dictionary>(); RecentlyReported = new List(); BOBFilter = new BOBFilter(); @@ -224,7 +221,6 @@ namespace Server.Mobiles public PlayerMobile(Serial s) : base(s) { VisibilityList = new List(); - m_AntiMacroTable = new Dictionary>(); } [CommandProperty(AccessLevel.GameMaster)] @@ -2872,36 +2868,6 @@ namespace Server.Mobiles return base.IsHarmfulCriminal(target); } - public bool AntiMacroCheck(Skill skill, object obj) - { - if (obj == null || m_AntiMacroTable == null || AccessLevel != AccessLevel.Player) - { - return true; - } - - if (!m_AntiMacroTable.TryGetValue(skill, out var tbl)) - { - m_AntiMacroTable[skill] = tbl = new Dictionary(); - } - - if (tbl.TryGetValue(obj, out var count)) - { - if (count.TimeStamp + SkillCheck.AntiMacro.Expire <= Core.Now) - { - count.Count = 1; - return true; - } - - ++count.Count; - return count.Count <= SkillCheck.AntiMacro.Allowance; - } - - tbl[obj] = count = new CountAndTimeStamp(); - count.Count = 1; - - return true; - } - private void RevertHair() { SetHairMods(-1, -1); @@ -3220,27 +3186,6 @@ namespace Server.Mobiles public override void Serialize(IGenericWriter writer) { - var toRemove = new List(); - - // cleanup our anti-macro table - foreach (var t in m_AntiMacroTable.Values) - { - toRemove.Clear(); - - foreach (var (k, v) in t) - { - if (v.TimeStamp + SkillCheck.AntiMacro.Expire <= Core.Now) - { - toRemove.Add(k); - } - } - - foreach (var key in toRemove) - { - t.Remove(key); - } - } - base.Serialize(writer); writer.Write(29); // version @@ -4588,23 +4533,6 @@ namespace Server.Mobiles } } - private class CountAndTimeStamp - { - private int m_Count; - - public DateTime TimeStamp { get; private set; } - - public int Count - { - get => m_Count; - set - { - m_Count = value; - TimeStamp = Core.Now; - } - } - } - private class MountBlock { private TimerExecutionToken _timerToken; diff --git a/Projects/UOContent/Skills/AntiMacroSystem.cs b/Projects/UOContent/Skills/AntiMacroSystem.cs new file mode 100644 index 000000000..9dace455b --- /dev/null +++ b/Projects/UOContent/Skills/AntiMacroSystem.cs @@ -0,0 +1,283 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Text.Json.Serialization; +using Server.Collections; +using Server.Json; +using Server.Mobiles; + +namespace Server.Misc; + +public static class AntiMacroSystem +{ + // *** NOTE ***: Modifying these values will not change an already created antimacro.json file! + private static readonly bool[] _antiMacroSkillDefaults = + { + false, // Alchemy = 0, + true, // Anatomy = 1, + true, // AnimalLore = 2, + true, // ItemID = 3, + true, // ArmsLore = 4, + false, // Parry = 5, + true, // Begging = 6, + false, // Blacksmith = 7, + false, // Fletching = 8, + true, // Peacemaking = 9, + true, // Camping = 10, + false, // Carpentry = 11, + false, // Cartography = 12, + false, // Cooking = 13, + true, // DetectHidden = 14, + true, // Discordance = 15, + true, // EvalInt = 16, + true, // Healing = 17, + true, // Fishing = 18, + true, // Forensics = 19, + true, // Herding = 20, + true, // Hiding = 21, + true, // Provocation = 22, + false, // Inscribe = 23, + true, // Lockpicking = 24, + true, // Magery = 25, + true, // MagicResist = 26, + false, // Tactics = 27, + true, // Snooping = 28, + true, // Musicianship = 29, + true, // Poisoning = 30, + false, // Archery = 31, + true, // SpiritSpeak = 32, + true, // Stealing = 33, + false, // Tailoring = 34, + true, // AnimalTaming = 35, + true, // TasteID = 36, + false, // Tinkering = 37, + true, // Tracking = 38, + true, // Veterinary = 39, + false, // Swords = 40, + false, // Macing = 41, + false, // Fencing = 42, + false, // Wrestling = 43, + true, // Lumberjacking = 44, + true, // Mining = 45, + true, // Meditation = 46, + true, // Stealth = 47, + true, // RemoveTrap = 48, + true, // Necromancy = 49, + false, // Focus = 50, + true, // Chivalry = 51 + true, // Bushido = 52 + true, // Ninjitsu = 53 + true, // Spellweaving + true, // Mysticism = 55 + true, // Imbuing = 56 + false, // Throwing = 57 + }; + + private static Dictionary _antiMacroTable; + private static Dictionary _logoutCleanup; + + private const string _antiMacroPath = "Configuration/antimacro.json"; + public static AntiMacroSettings Settings { get; private set; } + + public static void Configure() + { + var path = Path.Combine(Core.BaseDirectory, _antiMacroPath); + + if (File.Exists(path)) + { + Settings = JsonConfig.Deserialize(path); + } + else + { + Settings = new AntiMacroSettings + { + Enabled = false, + Allowance = 3, + LocationSize = 5, + Expire = TimeSpan.FromMinutes(5.0), + SkillsThatUseAntiMacro = new BitArray(_antiMacroSkillDefaults) + }; + + JsonConfig.Serialize(Path.Join(Core.BaseDirectory, _antiMacroPath), Settings); + } + } + + public static void Initialize() + { + EventSink.WorldSave += OnWorldSave; + EventSink.Logout += OnLogout; + EventSink.Login += OnLogin; + } + + private static void OnWorldSave() + { + if (_antiMacroTable == null) + { + return; + } + + var now = Core.Now; + + using var toRemove = PooledRefQueue.Create(); + foreach (var (m, antiMacro) in _antiMacroTable) + { + if (antiMacro._lastExpiration <= now) + { + toRemove.Enqueue(m); + } + else + { + antiMacro.CleanExpired(); + } + } + + while (toRemove.Count > 0) + { + _antiMacroTable.Remove(toRemove.Dequeue()); + } + } + + private static void OnLogin(Mobile m) + { + // Stop the clear out timer + if (_logoutCleanup?.Remove(m, out var timer) == true) + { + timer.Stop(); + } + } + + private static void OnLogout(Mobile m) + { + if (_antiMacroTable?.TryGetValue(m, out var antiMacro) != true) + { + return; + } + + if (antiMacro._lastExpiration < Core.Now) + { + _antiMacroTable.Remove(m); + return; + } + + _logoutCleanup ??= new Dictionary(); + if (_logoutCleanup.TryGetValue(m, out var timer)) + { + timer.Stop(); + } + else + { + _logoutCleanup[m] = timer = Timer.DelayCall(Settings.Expire, CleanupPlayer, m); + } + + timer.Start(); + } + + public static void CleanupPlayer(Mobile pm) + { + if (_antiMacroTable?.Remove(pm, out var antiMacro) == true) + { + // Hint to GC that we don't want this + antiMacro._antiMacroTracking.Clear(); + antiMacro._antiMacroTracking = null; + } + + if (_logoutCleanup?.Remove(pm, out var timer) == true) + { + timer.Stop(); + } + } + + public static bool UseAntiMacro(int skillId) => + skillId >= 0 && skillId < Settings.SkillsThatUseAntiMacro.Length && Settings.SkillsThatUseAntiMacro[skillId]; + + public static bool AntiMacroCheck(PlayerMobile pm, Skill skill, object obj) + { + if (!Settings.Enabled || obj == null || pm.AccessLevel != AccessLevel.Player || !UseAntiMacro(skill.Info.SkillID)) + { + return true; + } + + _antiMacroTable ??= new Dictionary(); + + // Hot path so use optimized code + ref PlayerAntiMacro antiMacro = ref CollectionsMarshal.GetValueRefOrNullRef(_antiMacroTable, pm); + if (Unsafe.IsNullRef(ref antiMacro)) + { + antiMacro = new PlayerAntiMacro(); + } + + return antiMacro.AntiMacroCheck(skill, obj); + } + + public record AntiMacroSettings + { + // How many times may we use the same location/target for gain + public int Allowance { get; init; } + + // The size of each location, make this smaller so players dont have to move as far + public int LocationSize { get; init; } + + public bool Enabled { get; init; } + + // How long do we remember targets/locations? + public TimeSpan Expire { get; init; } + + [JsonConverter(typeof(BitArrayEnumIndexConverter))] + public BitArray SkillsThatUseAntiMacro { get; init; } + } + + private class PlayerAntiMacro + { + // This can get quite large. If a player is logged in for a while, this can be promoted to Gen 2 and + // become a memory leak. + public Dictionary<(Skill, object), CountAndTimeStamp> _antiMacroTracking = new(); + public DateTime _lastExpiration; + + public bool AntiMacroCheck(Skill skill, object obj) + { + var now = Core.Now; + + // Potential hot path, so use optimized code + ref CountAndTimeStamp _countTimeStamp = + ref CollectionsMarshal.GetValueRefOrAddDefault(_antiMacroTracking, (skill, obj), out bool exists); + + _countTimeStamp._count++; + + if (!exists || _countTimeStamp._expiration <= now || _countTimeStamp._count < Settings.Allowance) + { + _countTimeStamp._expiration = _lastExpiration = now + Settings.Expire; + return true; + } + + return false; + } + + public void CleanExpired() + { + var now = Core.Now; + + using var toRemove = PooledRefQueue<(Skill, object)>.Create(); + + foreach (var (key, countAndTimeStamp) in _antiMacroTracking) + { + if (countAndTimeStamp._count <= 0 || countAndTimeStamp._expiration <= now) + { + toRemove.Enqueue(key); + } + } + + while (toRemove.Count > 0) + { + _antiMacroTracking.Remove(toRemove.Dequeue()); + } + } + } + + private struct CountAndTimeStamp + { + public int _count; + public DateTime _expiration; + } +} diff --git a/Projects/UOContent/Misc/SkillCheck.cs b/Projects/UOContent/Skills/SkillCheck.cs similarity index 74% rename from Projects/UOContent/Misc/SkillCheck.cs rename to Projects/UOContent/Skills/SkillCheck.cs index 5105e53f2..a92b151c9 100644 --- a/Projects/UOContent/Misc/SkillCheck.cs +++ b/Projects/UOContent/Skills/SkillCheck.cs @@ -1,9 +1,5 @@ using System; -using System.IO; -using System.Text.Json.Serialization; -using Server.Collections; using Server.Factions; -using Server.Json; using Server.Mobiles; namespace Server.Misc; @@ -21,98 +17,9 @@ public static class SkillCheck // Publish 16 changed max stats from 100 to 125 private static int StatMax = Core.LBR ? 125 : 100; - // *** NOTE ***: Modifying these values will not change an already created antimacro.json file! - private static readonly bool[] _skillThatUseAntiMacro = - { - false, // Alchemy = 0, - true, // Anatomy = 1, - true, // AnimalLore = 2, - true, // ItemID = 3, - true, // ArmsLore = 4, - false, // Parry = 5, - true, // Begging = 6, - false, // Blacksmith = 7, - false, // Fletching = 8, - true, // Peacemaking = 9, - true, // Camping = 10, - false, // Carpentry = 11, - false, // Cartography = 12, - false, // Cooking = 13, - true, // DetectHidden = 14, - true, // Discordance = 15, - true, // EvalInt = 16, - true, // Healing = 17, - true, // Fishing = 18, - true, // Forensics = 19, - true, // Herding = 20, - true, // Hiding = 21, - true, // Provocation = 22, - false, // Inscribe = 23, - true, // Lockpicking = 24, - true, // Magery = 25, - true, // MagicResist = 26, - false, // Tactics = 27, - true, // Snooping = 28, - true, // Musicianship = 29, - true, // Poisoning = 30, - false, // Archery = 31, - true, // SpiritSpeak = 32, - true, // Stealing = 33, - false, // Tailoring = 34, - true, // AnimalTaming = 35, - true, // TasteID = 36, - false, // Tinkering = 37, - true, // Tracking = 38, - true, // Veterinary = 39, - false, // Swords = 40, - false, // Macing = 41, - false, // Fencing = 42, - false, // Wrestling = 43, - true, // Lumberjacking = 44, - true, // Mining = 45, - true, // Meditation = 46, - true, // Stealth = 47, - true, // RemoveTrap = 48, - true, // Necromancy = 49, - false, // Focus = 50, - true, // Chivalry = 51 - true, // Bushido = 52 - true, // Ninjitsu = 53 - true, // Spellweaving - true, // Mysticism = 55 - true, // Imbuing = 56 - false, // Throwing = 57 - }; - private static readonly TimeSpan m_StatGainDelay = TimeSpan.FromMinutes(Core.ML ? 0.05 : 15); private static readonly TimeSpan m_PetStatGainDelay = TimeSpan.FromMinutes(5.0); - private const string _antiMacroPath = "Configuration/antimacro.json"; - public static AntiMacroSettings AntiMacro { get; private set; } - - public static void Configure() - { - var path = Path.Combine(Core.BaseDirectory, _antiMacroPath); - - if (File.Exists(path)) - { - AntiMacro = JsonConfig.Deserialize(path); - } - else - { - AntiMacro = new AntiMacroSettings - { - Enabled = false, - Allowance = 3, - LocationSize = 5, - Expire = TimeSpan.FromMinutes(5.0), - SkillsThatUseAntiMacro = new BitArray(_skillThatUseAntiMacro) - }; - - JsonConfig.Serialize(Path.Join(Core.BaseDirectory, _antiMacroPath), AntiMacro); - } - } - public static void Initialize() { Mobile.SkillCheckLocationHandler = Mobile_SkillCheckLocation; @@ -145,7 +52,7 @@ public static class SkillCheck var chance = (value - minSkill) / (maxSkill - minSkill); - var size = AntiMacro.LocationSize; + var size = AntiMacroSystem.Settings.LocationSize; var loc = new Point2D(from.Location.X / size, from.Location.Y / size); return CheckSkill(from, skill, loc, chance); } @@ -169,7 +76,7 @@ public static class SkillCheck return true; // No challenge } - var size = AntiMacro.LocationSize; + var size = AntiMacroSystem.Settings.LocationSize; var loc = new Point2D(from.Location.X / size, from.Location.Y / size); return CheckSkill(from, skill, loc, chance); } @@ -279,12 +186,7 @@ public static class SkillCheck return false; } - if (AntiMacro.Enabled && from is PlayerMobile mobile && AntiMacro.UseAntiMacro(skill.Info.SkillID)) - { - return mobile.AntiMacroCheck(skill, obj); - } - - return true; + return from is not PlayerMobile mobile || AntiMacroSystem.AntiMacroCheck(mobile, skill, obj); } public static void Gain(Mobile from, Skill skill) @@ -518,24 +420,4 @@ public static class SkillCheck IncreaseStat(from, stat, atrophy); } - - public record AntiMacroSettings - { - // How many times may we use the same location/target for gain - public int Allowance { get; init; } - - // The size of each location, make this smaller so players dont have to move as far - public int LocationSize { get; init; } - - public bool Enabled { get; init; } - - // How long do we remember targets/locations? - public TimeSpan Expire { get; init; } - - [JsonConverter(typeof(BitArrayEnumIndexConverter))] - public BitArray SkillsThatUseAntiMacro { get; init; } - - public bool UseAntiMacro(int skillId) => - skillId < SkillsThatUseAntiMacro.Length && SkillsThatUseAntiMacro[skillId]; - } }