using System; using System.Collections; using System.Collections.Generic; using System.IO; using System.Runtime.InteropServices; using System.Text.Json.Serialization; using ModernUO.CodeGeneratedEvents; 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; } 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()); } } [OnEvent(nameof(PlayerMobile.PlayerLoginEvent))] public static void OnLogin(PlayerMobile pm) { // Stop the clear out timer if (_logoutCleanup?.Remove(pm, 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 var antiMacro = ref CollectionsMarshal.GetValueRefOrAddDefault(_antiMacroTable, pm, out var exists); if (!exists) { 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 var _countTimeStamp = ref CollectionsMarshal.GetValueRefOrAddDefault(_antiMacroTracking, (skill, obj), out var 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; } }