From 2d381dad91ee0c8a3daa91e3273429a41e5c897a Mon Sep 17 00:00:00 2001 From: Bohica <53943479+Bohicatv@users.noreply.github.com> Date: Thu, 13 Nov 2025 06:03:16 -0800 Subject: [PATCH] feat: Adds anti-botting system for resource harvesting --- Projects/UOContent/Engines/Harvest/Fishing.cs | 11 ++ .../Engines/Harvest/Lumberjacking.cs | 11 ++ Projects/UOContent/Engines/Harvest/Mining.cs | 11 ++ .../Systems/AntiBotSystem/AntiBotGump.cs | 62 ++++++++ .../Systems/AntiBotSystem/AntiBotSystem.cs | 132 ++++++++++++++++++ 5 files changed, 227 insertions(+) create mode 100644 Projects/UOContent/Systems/AntiBotSystem/AntiBotGump.cs create mode 100644 Projects/UOContent/Systems/AntiBotSystem/AntiBotSystem.cs diff --git a/Projects/UOContent/Engines/Harvest/Fishing.cs b/Projects/UOContent/Engines/Harvest/Fishing.cs index df46fe9fc..99de3d11d 100644 --- a/Projects/UOContent/Engines/Harvest/Fishing.cs +++ b/Projects/UOContent/Engines/Harvest/Fishing.cs @@ -492,6 +492,17 @@ namespace Server.Engines.Harvest return false; } + if (Utility.Random(100) < 1) + { + if (!AntiBotSystem.CheckPlayer(from, () => + { + from.Target = new HarvestTarget(tool, this); + })) + { + return false; // antibot challenge sent + } + } + from.SendLocalizedMessage(500974); // What water do you want to fish in? return true; } diff --git a/Projects/UOContent/Engines/Harvest/Lumberjacking.cs b/Projects/UOContent/Engines/Harvest/Lumberjacking.cs index 9640f68d8..95f4c0fa1 100644 --- a/Projects/UOContent/Engines/Harvest/Lumberjacking.cs +++ b/Projects/UOContent/Engines/Harvest/Lumberjacking.cs @@ -185,6 +185,17 @@ namespace Server.Engines.Harvest { from.RevealingAction(); } + + if (Utility.Random(100) < 1) + { + if (!AntiBotSystem.CheckPlayer(from, () => + { + from.Target = new HarvestTarget(tool, this); + })) + { + // antibot challenge sent + } + } } public static void Initialize() diff --git a/Projects/UOContent/Engines/Harvest/Mining.cs b/Projects/UOContent/Engines/Harvest/Mining.cs index dd7f5c747..60c734343 100644 --- a/Projects/UOContent/Engines/Harvest/Mining.cs +++ b/Projects/UOContent/Engines/Harvest/Mining.cs @@ -444,6 +444,17 @@ namespace Server.Engines.Harvest return false; } + if (Utility.Random(100) < 1) + { + if (!AntiBotSystem.CheckPlayer(from, () => + { + from.Target = new HarvestTarget(tool, this); + })) + { + // antibot challenge sent + } + } + from.SendLocalizedMessage(503033); // Where do you wish to dig? return true; } diff --git a/Projects/UOContent/Systems/AntiBotSystem/AntiBotGump.cs b/Projects/UOContent/Systems/AntiBotSystem/AntiBotGump.cs new file mode 100644 index 000000000..cfe607894 --- /dev/null +++ b/Projects/UOContent/Systems/AntiBotSystem/AntiBotGump.cs @@ -0,0 +1,62 @@ +using Server.Gumps; +using Server.Network; + +namespace Server.Engines.AntiBot +{ + public class AntiBotGump : Gump + { + public readonly Mobile _mobile; + public readonly int _code; + + public AntiBotGump(Mobile mobile, int code) : base(150, 150) + { + _mobile = mobile; + _code = code; + + Closable = false; + Disposable = false; + Draggable = true; + Resizable = false; + + AddPage(0); + AddBackground(0, 0, 350, 220, 9270); + + AddHtml(20, 20, 310, 25, "
Anti-Bot Verification
", false, false); + AddHtml(20, 50, 310, 40, $"
Please enter the following number:
", false, false); + AddHtml(20, 55, 310, 40, $"

{code}
", false, false); + AddHtml(20, 100, 310, 50, $"You have 2 minutes to input the correct number.
Incorrect numbers, cancellations, or timeouts will disconnect you from the server.", false, false); + + AddBackground(20, 160, 200, 25, 3000); + AddTextEntry(25, 165, 190, 20, 0, 0, ""); + + AddButton(230, 160, 4005, 4007, 1, GumpButtonType.Reply, 0); // Submit + AddButton(290, 160, 4017, 4019, 0, GumpButtonType.Reply, 0); // Cancel + + AddHtml(230, 190, 40, 20, "
Submit
", false, false); + AddHtml(290, 190, 40, 20, "
Cancel
", false, false); + } + + public override void OnResponse(NetState sender, in RelayInfo info) + { + var from = sender?.Mobile; + if (from == null) + { + return; + } + + bool cancelled = info.ButtonID == 0; + int enteredCode = 0; + + if (!cancelled) + { + var textEntry = info.GetTextEntry(0); + if (textEntry != null && !int.TryParse(textEntry.Trim(), out enteredCode)) + { + enteredCode = -1; // Invalid input - will cause disconnect + } + } + + AntiBotSystem.ProcessResponse(from, enteredCode, cancelled); + } + } +} diff --git a/Projects/UOContent/Systems/AntiBotSystem/AntiBotSystem.cs b/Projects/UOContent/Systems/AntiBotSystem/AntiBotSystem.cs new file mode 100644 index 000000000..b36af735a --- /dev/null +++ b/Projects/UOContent/Systems/AntiBotSystem/AntiBotSystem.cs @@ -0,0 +1,132 @@ +using System; +using System.Collections.Generic; +using Server.Gumps; +using Server.Mobiles; + +namespace Server.Engines.AntiBot +{ + public static class AntiBotSystem + { + private class AntiBotChallenge + { + public int Code { get; set; } + public DateTime ChallengeExpiry { get; set; } + public Action SuccessCallback { get; set; } + public Timer TimeoutTimer { get; set; } + } + + private static readonly Dictionary _activeChallenges = new(); + + public static bool Enabled { get; set; } = true; + public static int MaxAttempts { get; set; } = 1; + public static TimeSpan ChallengeTimeout { get; set; } = TimeSpan.FromMinutes(2); + + public static bool CheckPlayer(Mobile from, Action onSuccess) + { + if (!Enabled || from is not PlayerMobile) + { + return true; + } + + lock (_activeChallenges) + { + CleanupExpiredChallenges(); + + if (_activeChallenges.TryGetValue(from, out var existing)) + { + from.CloseGump(); + from.SendGump(new AntiBotGump(from, existing.Code)); + return false; + } + + var challenge = new AntiBotChallenge + { + Code = Utility.RandomMinMax(1000, 9999), + ChallengeExpiry = Core.Now.Add(ChallengeTimeout), + SuccessCallback = onSuccess + }; + + challenge.TimeoutTimer = Timer.DelayCall(ChallengeTimeout, () => + { + lock (_activeChallenges) + { + if (_activeChallenges.ContainsKey(from)) + { + _activeChallenges.Remove(from); + from.SendMessage("Anti-Bot: Verification timed out. Disconnecting..."); + from.NetState?.Disconnect("Anti-Bot: Verification failed by timing out."); + } + } + }); + + _activeChallenges[from] = challenge; + from.CloseGump(); + from.SendGump(new AntiBotGump(from, challenge.Code)); + return false; + } + } + + internal static void ProcessResponse(Mobile from, int enteredCode, bool cancelled) + { + lock (_activeChallenges) + { + if (!_activeChallenges.TryGetValue(from, out var challenge)) + { + return; + } + + challenge.TimeoutTimer?.Stop(); + _activeChallenges.Remove(from); + + if (cancelled) + { + from.SendMessage("Anti-Bot: Verification cancelled. Disconnecting..."); + from.NetState?.Disconnect("Anti-Bot: Verification failed by cancellation."); + return; + } + + if (enteredCode == challenge.Code) + { + from.SendMessage("Anti-Bot: Verification successful!!!"); + } + else + { + from.SendMessage("Anti-Bot: Incorrect number. Disconnecting..."); + from.NetState?.Disconnect("Anti-Bot: Verification failed by incorrect number."); + } + } + } + + private static void CleanupExpiredChallenges() + { + var now = Core.Now; + var toRemove = new List(); + + foreach (var kvp in _activeChallenges) + { + if (kvp.Value.ChallengeExpiry < now) + { + kvp.Value.TimeoutTimer?.Stop(); + toRemove.Add(kvp.Key); + } + } + + foreach (var mobile in toRemove) + { + _activeChallenges.Remove(mobile); + } + } + + public static void CancelChallenge(Mobile from) + { + lock (_activeChallenges) + { + if (_activeChallenges.TryGetValue(from, out var challenge)) + { + challenge.TimeoutTimer?.Stop(); + _activeChallenges.Remove(from); + } + } + } + } +} \ No newline at end of file