diff --git a/.gitignore b/.gitignore index 7022fc9b2..13a182b47 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ /Distribution/ModernUO.* /Distribution/Assemblies /Distribution/bsdtar +/Distribution/Configuration/antimacro.json /Distribution/Configuration/assistants.json /Distribution/Configuration/modernuo.json /Distribution/Configuration/email-settings.json diff --git a/Projects/Server/Json/Converters/BitArrayEnumIndexConverter.cs b/Projects/Server/Json/Converters/BitArrayEnumIndexConverter.cs new file mode 100644 index 000000000..0f5d5f89e --- /dev/null +++ b/Projects/Server/Json/Converters/BitArrayEnumIndexConverter.cs @@ -0,0 +1,88 @@ +/************************************************************************* + * ModernUO * + * Copyright 2019-2023 - ModernUO Development Team * + * Email: hi@modernuo.com * + * File: BitArrayEnumIndexConverter.cs * + * * + * This program is free software: you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation, either version 3 of the License, or * + * (at your option) any later version. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program. If not, see . * + *************************************************************************/ + +using System; +using System.Text.Json; +using System.Text.Json.Serialization; +using Server.Collections; + +namespace Server.Json; + +public class BitArrayEnumIndexConverter : JsonConverter where T : struct, Enum +{ + // ReSharper disable once StaticMemberInGenericType + private static int _maxValue = int.MinValue; + + public override BitArray Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (_maxValue < 0) + { + // Get the max value and cache it. + var values = Enum.GetValues(); + for (var i = 0; i < values.Length; i++) + { + var v = (int)(object)values[i]; + if (v > _maxValue) + { + _maxValue = v; + } + } + } + + // If the value is very large, we will have a major problem. + // We always assume zero-offset + var bitArray = new BitArray(_maxValue + 1); + + while (true) + { + reader.Read(); + if (reader.TokenType == JsonTokenType.EndObject) + { + break; + } + + if (reader.TokenType != JsonTokenType.PropertyName) + { + throw new JsonException("Invalid Json structure for Flag object"); + } + + var key = reader.GetString(); + + reader.Read(); + + if (!reader.GetBoolean() || !Enum.TryParse(key, out var val)) + { + continue; + } + + bitArray[(int)(object)val] = true; + } + + return bitArray; + } + + public override void Write(Utf8JsonWriter writer, BitArray bitArray, JsonSerializerOptions options) + { + writer.WriteStartObject(); + + foreach (var flagName in Enum.GetNames(typeof(T))) + { + var value = (int)(object)Enum.Parse(flagName, false); + writer.WriteBoolean(flagName, bitArray[value]); + } + + writer.WriteEndObject(); + } +} diff --git a/Projects/Server/Json/Converters/FlagsConverter.cs b/Projects/Server/Json/Converters/FlagsConverter.cs index 0eea803b9..7b2d1c738 100644 --- a/Projects/Server/Json/Converters/FlagsConverter.cs +++ b/Projects/Server/Json/Converters/FlagsConverter.cs @@ -130,20 +130,4 @@ public class FlagsConverter : JsonConverter where T : struct, Enum TypeCode.UInt64 => (ulong)value, _ => throw new InvalidOperationException() }; - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static ulong GetUnderlyingTypeLength(TypeCode typeCode) => - typeCode switch - { - TypeCode.Byte => 8, - TypeCode.SByte => 8, - TypeCode.Int16 => 16, - TypeCode.UInt16 => 16, - TypeCode.Char => 16, - TypeCode.Int32 => 32, - TypeCode.UInt32 => 32, - TypeCode.Int64 => 64, - TypeCode.UInt64 => 64, - _ => 64 - }; -} \ No newline at end of file +} diff --git a/Projects/Server/Utilities/Utility.cs b/Projects/Server/Utilities/Utility.cs index 9effa3b73..a7d5210b9 100644 --- a/Projects/Server/Utilities/Utility.cs +++ b/Projects/Server/Utilities/Utility.cs @@ -1703,4 +1703,20 @@ public static class Utility return center; } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int GetUnderlyingNumericBitLength(this TypeCode typeCode) => + typeCode switch + { + TypeCode.Byte => 8, + TypeCode.SByte => 8, + TypeCode.Int16 => 16, + TypeCode.UInt16 => 16, + TypeCode.Char => 16, + TypeCode.Int32 => 32, + TypeCode.UInt32 => 32, + TypeCode.Int64 => 64, + TypeCode.UInt64 => 64, + _ => 64 + }; } diff --git a/Projects/UOContent/Assistants/AssistantConfiguration.cs b/Projects/UOContent/Assistants/AssistantConfiguration.cs index 1c29e4bb0..c4d3de460 100644 --- a/Projects/UOContent/Assistants/AssistantConfiguration.cs +++ b/Projects/UOContent/Assistants/AssistantConfiguration.cs @@ -38,7 +38,7 @@ public static class AssistantConfiguration WarningMessage = _defaultWarningMessage }; - Save(path); + Save(); } } @@ -62,10 +62,9 @@ public static class AssistantConfiguration Save(); } - private static void Save(string path = null) + private static void Save() { - path ??= Path.Join(Core.BaseDirectory, _path); - JsonConfig.Serialize(path, Settings); + JsonConfig.Serialize(Path.Join(Core.BaseDirectory, _path), Settings); } } diff --git a/Projects/UOContent/Misc/SkillCheck.cs b/Projects/UOContent/Misc/SkillCheck.cs index 60b7cb120..5105e53f2 100644 --- a/Projects/UOContent/Misc/SkillCheck.cs +++ b/Projects/UOContent/Misc/SkillCheck.cs @@ -1,7 +1,10 @@ using System; +using System.IO; +using System.Text.Json.Serialization; +using Server.Collections; using Server.Factions; +using Server.Json; using Server.Mobiles; -using Server.Regions; namespace Server.Misc; @@ -18,18 +21,9 @@ public static class SkillCheck // Publish 16 changed max stats from 100 to 125 private static int StatMax = Core.LBR ? 125 : 100; - public const int Allowance = 3; // How many times may we use the same location/target for gain - - private const int - LocationSize = 5; // The size of eeach location, make this smaller so players dont have to move as far - - private static readonly bool AntiMacroCode = !Core.ML; // Change this to false to disable anti-macro code - - public static TimeSpan AntiMacroExpire = TimeSpan.FromMinutes(5.0); // How long do we remember targets/locations? - - private static readonly bool[] UseAntiMacro = + // *** NOTE ***: Modifying these values will not change an already created antimacro.json file! + private static readonly bool[] _skillThatUseAntiMacro = { - // true if this skill uses the anti-macro code, false if it does not false, // Alchemy = 0, true, // Anatomy = 1, true, // AnimalLore = 2, @@ -93,6 +87,32 @@ public static class SkillCheck 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; @@ -125,7 +145,8 @@ public static class SkillCheck var chance = (value - minSkill) / (maxSkill - minSkill); - var loc = new Point2D(from.Location.X / LocationSize, from.Location.Y / LocationSize); + var size = AntiMacro.LocationSize; + var loc = new Point2D(from.Location.X / size, from.Location.Y / size); return CheckSkill(from, skill, loc, chance); } @@ -148,7 +169,8 @@ public static class SkillCheck return true; // No challenge } - var loc = new Point2D(from.Location.X / LocationSize, from.Location.Y / LocationSize); + var size = AntiMacro.LocationSize; + var loc = new Point2D(from.Location.X / size, from.Location.Y / size); return CheckSkill(from, skill, loc, chance); } @@ -257,7 +279,7 @@ public static class SkillCheck return false; } - if (AntiMacroCode && from is PlayerMobile mobile && UseAntiMacro[skill.Info.SkillID]) + if (AntiMacro.Enabled && from is PlayerMobile mobile && AntiMacro.UseAntiMacro(skill.Info.SkillID)) { return mobile.AntiMacroCheck(skill, obj); } @@ -496,4 +518,24 @@ 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]; + } } diff --git a/Projects/UOContent/Mobiles/PlayerMobile.cs b/Projects/UOContent/Mobiles/PlayerMobile.cs index 5c44a75d2..874b7f5f5 100644 --- a/Projects/UOContent/Mobiles/PlayerMobile.cs +++ b/Projects/UOContent/Mobiles/PlayerMobile.cs @@ -2886,14 +2886,14 @@ namespace Server.Mobiles if (tbl.TryGetValue(obj, out var count)) { - if (count.TimeStamp + SkillCheck.AntiMacroExpire <= Core.Now) + if (count.TimeStamp + SkillCheck.AntiMacro.Expire <= Core.Now) { count.Count = 1; return true; } ++count.Count; - return count.Count <= SkillCheck.Allowance; + return count.Count <= SkillCheck.AntiMacro.Allowance; } tbl[obj] = count = new CountAndTimeStamp(); @@ -3229,7 +3229,7 @@ namespace Server.Mobiles foreach (var (k, v) in t) { - if (v.TimeStamp + SkillCheck.AntiMacroExpire <= Core.Now) + if (v.TimeStamp + SkillCheck.AntiMacro.Expire <= Core.Now) { toRemove.Add(k); }