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