From 5bdd609694ad29627181776efa629f77b52dc548 Mon Sep 17 00:00:00 2001 From: primetime43 <12754111+primetime43@users.noreply.github.com> Date: Fri, 31 Jul 2026 16:50:55 -0400 Subject: [PATCH 1/7] Refactor DurationFormatter parsing and formatting #100 Replace manual character-by-character numeric extraction with Regex-based parsing for robustness. FormatSeconds now uses up to 3 decimal places instead of conditionally rounding. ParseToMilliseconds uses word-boundary regex for ms/s unit detection, validates range/NaN/Infinity, and uses MidpointRounding.AwayFromZero. CustomFishingActions now correctly converts the display value back to milliseconds when populating the time input. --- .../Utilities/DurationFormatter.cs | 81 ++++++------------- .../Views/CustomFishingActions.cs | 8 +- 2 files changed, 26 insertions(+), 63 deletions(-) diff --git a/ToonTown Rewritten Bot/Utilities/DurationFormatter.cs b/ToonTown Rewritten Bot/Utilities/DurationFormatter.cs index aa039c5e..f6b5817c 100644 --- a/ToonTown Rewritten Bot/Utilities/DurationFormatter.cs +++ b/ToonTown Rewritten Bot/Utilities/DurationFormatter.cs @@ -1,6 +1,6 @@ using System; -using System.Linq; -using System.Text; +using System.Globalization; +using System.Text.RegularExpressions; namespace ToonTown_Rewritten_Bot.Utilities { @@ -17,17 +17,7 @@ public static class DurationFormatter public static string FormatSeconds(long milliseconds) { double seconds = milliseconds / 1000.0; - - if (seconds < 10) - { - // Show one decimal place for shorter durations - return $"{seconds:0.#}s"; - } - else - { - // Show as whole seconds for longer durations - return $"{Math.Round(seconds)}s"; - } + return seconds.ToString("0.###", CultureInfo.InvariantCulture) + "s"; } /// @@ -65,58 +55,33 @@ public static int ParseToMilliseconds(string input) if (string.IsNullOrWhiteSpace(input)) return 0; - // First, try to extract just digits (handles "TIME (500 milliseconds)" format) - string digitsOnly = new string(input.Where(char.IsDigit).ToArray()); - if (int.TryParse(digitsOnly, out int msValue)) - { - // If the input contains "s" or "sec" but NOT "ms" or "milliseconds", - // treat the number as seconds - string lower = input.ToLowerInvariant(); - bool hasSeconds = lower.Contains("sec") || (lower.Contains("s") && !lower.Contains("ms") && !lower.Contains("millisec")); - - if (hasSeconds) - { - // Number is in seconds, convert to milliseconds - // But first check if there's a decimal point we might have missed - return msValue * 1000; - } - - return msValue; - } + Match numericMatch = Regex.Match(input, @"[+-]?(?:\d+(?:[.,]\d+)?|[.,]\d+)"); + if (!numericMatch.Success) + return 0; - // Try parsing as a decimal for formats like "0.5s" - // Extract the numeric part including decimal point - var numericPart = new StringBuilder(); - bool foundDecimal = false; - foreach (char c in input) + string numericValue = numericMatch.Value.Replace(',', '.'); + if (!double.TryParse(numericValue, + NumberStyles.AllowLeadingSign | NumberStyles.AllowDecimalPoint, + CultureInfo.InvariantCulture, + out double value)) { - if (char.IsDigit(c)) - { - numericPart.Append(c); - } - else if (c == '.' && !foundDecimal) - { - numericPart.Append(c); - foundDecimal = true; - } + return 0; } - if (double.TryParse(numericPart.ToString(), out double value)) + string lower = input.ToLowerInvariant(); + bool hasMilliseconds = Regex.IsMatch(lower, + @"(? int.MaxValue || + double.IsNaN(milliseconds) || double.IsInfinity(milliseconds)) { - string lower = input.ToLowerInvariant(); - if (lower.Contains("s") && !lower.Contains("ms")) - { - // Seconds - convert to milliseconds - return (int)(value * 1000); - } - else - { - // Assume milliseconds - return (int)value; - } + return 0; } - return 0; + return (int)Math.Round(milliseconds, MidpointRounding.AwayFromZero); } /// diff --git a/ToonTown Rewritten Bot/Views/CustomFishingActions.cs b/ToonTown Rewritten Bot/Views/CustomFishingActions.cs index 0e2c10e8..29e4c2ed 100644 --- a/ToonTown Rewritten Bot/Views/CustomFishingActions.cs +++ b/ToonTown Rewritten Bot/Views/CustomFishingActions.cs @@ -279,11 +279,9 @@ private void actionItemsListBox_SelectedIndexChanged(object sender, EventArgs e) // Select "TIME" in comboBox1 if available comboBox1.SelectedItem = "TIME"; - // Extract the numeric value (time in seconds) - string timeValue = new String(selectedItem.Where(char.IsDigit).ToArray()); - - // Set the extracted time into textBox1 - actionTimeTxtBox.Text = timeValue; + // The editor accepts milliseconds, so convert the formatted display + // value back to milliseconds before populating the input. + actionTimeTxtBox.Text = ExtractMillisecondsFromDisplay(selectedItem).ToString(); } else { From c098f6120139d5228b1c22729759e78a54ce88e9 Mon Sep 17 00:00:00 2001 From: primetime43 <12754111+primetime43@users.noreply.github.com> Date: Fri, 31 Jul 2026 17:31:16 -0400 Subject: [PATCH 2/7] Improve golf key handling and UX #99 - Replace hardcoded CTRL swing key references with the user-configured Jump key, shown dynamically in all golf dialogs and the instructions label - Refresh golf instructions label when game controls are changed in Settings - Switch key simulation from raw InputSimulator calls to CoreFunctionality.SendKeyDown/Up so remapped keys are used correctly - Temporarily disable background input mode during golf actions (foreground SendInput required) - Wrap async key holds in try/finally to guarantee key release on cancellation - Add pre-flight validation before starting golf: checks for TTR window, empty/invalid action files, and missing SWING POWER action - Route golf action file loading through CustomGolfActionFileManager in both GolfService and CustomActionsGolf - Remove swallowed exception catch in GolfService.StartGolfAsync so errors surface properly --- .../CustomGolfActions/CustomActionsGolf.cs | 68 +++++++++++------ .../Services/GolfService.cs | 14 ++-- .../Views/CustomGolfWizardForm.cs | 15 +++- ToonTown Rewritten Bot/Views/MainForm.Golf.cs | 75 ++++++++++++++++++- .../Views/MainForm.Settings.cs | 1 + ToonTown Rewritten Bot/Views/MainForm.cs | 1 + ToonTown Rewritten Bot/Views/MainForm.resx | 10 +-- 7 files changed, 141 insertions(+), 43 deletions(-) diff --git a/ToonTown Rewritten Bot/Services/CustomGolfActions/CustomActionsGolf.cs b/ToonTown Rewritten Bot/Services/CustomGolfActions/CustomActionsGolf.cs index 5c604abf..653d5628 100644 --- a/ToonTown Rewritten Bot/Services/CustomGolfActions/CustomActionsGolf.cs +++ b/ToonTown Rewritten Bot/Services/CustomGolfActions/CustomActionsGolf.cs @@ -1,4 +1,3 @@ -using Newtonsoft.Json; using System; using System.Collections.Generic; using System.IO; @@ -6,6 +5,7 @@ using System.Threading; using WindowsInput; using ToonTown_Rewritten_Bot.Models; +using ToonTown_Rewritten_Bot.Utilities; using System.Windows.Forms; namespace ToonTown_Rewritten_Bot.Services.CustomGolfActions @@ -50,19 +50,30 @@ public CustomActionsGolf(string filePath) private void LoadActionsFromJson(string filePath) { - if (File.Exists(filePath)) + var result = CustomGolfActionFileManager.Load(filePath); + if (!result.Success) { - string json = File.ReadAllText(filePath); - actions = JsonConvert.DeserializeObject>(json); + throw new InvalidDataException( + $"Could not load golf action file: {result.ErrorMessage}"); } - } - private async Task PrepareToHitBall() - { - var up = GameControls.Remap(VirtualKeyCode.UP); - InputSimulator.SimulateKeyDown(up); - await Task.Delay(50); - InputSimulator.SimulateKeyUp(up); + actions = result.File?.Actions ?? new List(); + if (actions.Count == 0) + { + throw new InvalidDataException("The selected golf action file contains no actions."); + } + + if (actions.Exists(action => action.Duration <= 0)) + { + throw new InvalidDataException( + "Every golf action must have a duration greater than zero."); + } + + if (!actions.Exists(action => action.Action == "SWING POWER")) + { + throw new InvalidDataException( + "The selected golf action file has no SWING POWER action, so it cannot shoot."); + } } private void ReportProgress(int currentStep, string currentAction, string nextAction, int durationMs) @@ -84,17 +95,21 @@ private void ReportStatus(string status) public async Task PerformGolfActions(CancellationToken cancellationToken) { - CoreFunctionality.FocusTTRWindow(); - ReportStatus("Starting"); - await Task.Delay(1000, cancellationToken); - GolfActionKeys keys = new GolfActionKeys(); + // Background mode is a fishing preference. Golf relies on foreground SendInput, + // so temporarily disable it to ensure FocusTTRWindow and key delivery agree. + bool previousBackgroundMode = CoreFunctionality.UseBackgroundInput; + CoreFunctionality.UseBackgroundInput = false; try { + CoreFunctionality.FocusTTRWindow(); + ReportStatus("Starting"); + await Task.Delay(1000, cancellationToken); + GolfActionKeys keys = new GolfActionKeys(); + ReportStatus("Running"); int executedStep = 0; - int totalExecutableActions = TotalActions; for (int i = 0; i < actions.Count; i++) { @@ -134,11 +149,16 @@ public async Task PerformGolfActions(CancellationToken cancellationToken) // Process other actions that should correspond to actual key presses if (keys.ActionKeyMap.TryGetValue(actionCommand.Action, out VirtualKeyCode keyCode)) { - // Translate the default control key into whatever the user has bound in TTR. - keyCode = GameControls.Remap(keyCode); - InputSimulator.SimulateKeyDown(keyCode); - await Task.Delay(actionCommand.Duration, cancellationToken); - InputSimulator.SimulateKeyUp(keyCode); + CoreFunctionality.SendKeyDown(keyCode); + try + { + await Task.Delay(actionCommand.Duration, cancellationToken); + } + finally + { + // Always release held keys, including when the user cancels mid-swing. + CoreFunctionality.SendKeyUp(keyCode); + } } else { @@ -154,6 +174,10 @@ public async Task PerformGolfActions(CancellationToken cancellationToken) ReportStatus("Cancelled"); throw; } + finally + { + CoreFunctionality.UseBackgroundInput = previousBackgroundMode; + } } } -} \ No newline at end of file +} diff --git a/ToonTown Rewritten Bot/Services/GolfService.cs b/ToonTown Rewritten Bot/Services/GolfService.cs index 71ef4003..9272a376 100644 --- a/ToonTown Rewritten Bot/Services/GolfService.cs +++ b/ToonTown Rewritten Bot/Services/GolfService.cs @@ -1,7 +1,7 @@ -using Newtonsoft.Json; using System; using System.Diagnostics; using System.IO; +using System.Linq; using System.Threading; using System.Threading.Tasks; using System.Windows.Forms; @@ -82,10 +82,6 @@ public static async Task StartCustomGolfAction(string filePath, CancellationToke Debug.WriteLine("Golf operation was canceled by the user."); throw; } - catch (Exception ex) - { - MessageBox.Show($"An error occurred: {ex.Message}"); - } } /// @@ -377,14 +373,14 @@ private static void HideOverlayInternal() public static GolfActionCommand[] GetCustomGolfActions(string filePath) { - if (!File.Exists(filePath)) + var result = CustomGolfActionFileManager.Load(filePath); + if (!result.Success) { - Debug.WriteLine("File does not exist: " + filePath); + Debug.WriteLine($"Could not load golf action file: {result.ErrorMessage}"); return Array.Empty(); } - string json = File.ReadAllText(filePath); - return JsonConvert.DeserializeObject(json); + return result.File?.Actions?.ToArray() ?? Array.Empty(); } public static string GetCustomGolfActionFilePath(string fileName) diff --git a/ToonTown Rewritten Bot/Views/CustomGolfWizardForm.cs b/ToonTown Rewritten Bot/Views/CustomGolfWizardForm.cs index 4ae08c57..ba8ef833 100644 --- a/ToonTown Rewritten Bot/Views/CustomGolfWizardForm.cs +++ b/ToonTown Rewritten Bot/Views/CustomGolfWizardForm.cs @@ -467,6 +467,8 @@ private async void BtnTest_Click(object sender, EventArgs e) lblTestStatus.Text = "Testing sequence... (Press ESC to cancel)"; btnTest.Enabled = false; + bool previousBackgroundMode = CoreFunctionality.UseBackgroundInput; + CoreFunctionality.UseBackgroundInput = false; try { @@ -488,9 +490,15 @@ private async void BtnTest_Click(object sender, EventArgs e) } else if (golfKeys.ActionKeyMap.TryGetValue(action.Action, out var keyCode)) { - WindowsInput.InputSimulator.SimulateKeyDown(keyCode); - await Task.Delay(action.Duration, _testCts.Token); - WindowsInput.InputSimulator.SimulateKeyUp(keyCode); + CoreFunctionality.SendKeyDown(keyCode); + try + { + await Task.Delay(action.Duration, _testCts.Token); + } + finally + { + CoreFunctionality.SendKeyUp(keyCode); + } } } @@ -506,6 +514,7 @@ private async void BtnTest_Click(object sender, EventArgs e) } finally { + CoreFunctionality.UseBackgroundInput = previousBackgroundMode; btnTest.Enabled = true; _testCts?.Dispose(); _testCts = null; diff --git a/ToonTown Rewritten Bot/Views/MainForm.Golf.cs b/ToonTown Rewritten Bot/Views/MainForm.Golf.cs index b6a897a2..7b362fce 100644 --- a/ToonTown Rewritten Bot/Views/MainForm.Golf.cs +++ b/ToonTown Rewritten Bot/Views/MainForm.Golf.cs @@ -10,6 +10,22 @@ namespace ToonTown_Rewritten_Bot { public partial class MainForm { + private string ConfiguredGolfSwingKey => + Models.GameControls.GetDisplayName(Models.GameControls.Jump); + + private void UpdateGolfInstructionsLabel() + { + golfInstructionsLabel.Text = + $"Golf uses your configured Jump key: {ConfiguredGolfSwingKey}.\n" + + "It must match TTR (Options & Codes → Controls).\n\n" + + "1. Select a golf course\n" + + "2. Check the required tee position\n" + + "3. Move to that position in TTR\n" + + "4. Click Start Golf\n" + + "5. Keep TTR focused until the shot finishes\n\n" + + "Change the key under Settings → Configure Game Controls."; + } + private void createCustomGolfActionsBtn_Click(object sender, EventArgs e) { using (var form = new CustomGolfActions()) @@ -42,8 +58,44 @@ private async void startGolfBtn_Click(object sender, EventArgs e) // Get the full path to the selected golf action file. string filePath = GolfService.GetCustomGolfActionFilePath(selectedFileName); + if (CoreFunctionality.FindToontownWindow() == IntPtr.Zero) + { + MessageBox.Show( + "The Toontown Rewritten window was not found.\n\n" + + "Start TTR and log in before starting golf.", + "TTR Not Found", + MessageBoxButtons.OK, + MessageBoxIcon.Warning); + return; + } + + var actions = GolfService.GetCustomGolfActions(filePath); + if (actions.Length == 0) + { + MessageBox.Show( + "The selected golf action file is empty, missing, or invalid.\n\n" + + "Choose another course or recreate the action file.", + "Invalid Golf Actions", + MessageBoxButtons.OK, + MessageBoxIcon.Error); + return; + } + + var swingAction = Array.Find(actions, + action => action.Action == "SWING POWER" && action.Duration > 0); + if (swingAction == null) + { + MessageBox.Show( + "The selected action file has no valid SWING POWER action, so the bot cannot shoot.\n\n" + + "Edit or recreate the action file and add a Swing Power step.", + "No Swing Action", + MessageBoxButtons.OK, + MessageBoxIcon.Error); + return; + } + // Get shot summary to show helpful instructions - var summary = GolfService.GetShotSummaryFromFile(filePath); + var summary = GolfService.GetShotSummary(actions); // Build instruction message string positionInstruction = summary.RequiresPositionChange @@ -54,11 +106,14 @@ private async void startGolfBtn_Click(object sender, EventArgs e) $"Course: {selectedFileName}\n\n" + $"━━━ YOU DO (before clicking OK) ━━━\n" + $"1. {positionInstruction}\n" + - $"2. Make sure your swing key is set to CTRL\n\n" + + $"2. Bot swing key: {ConfiguredGolfSwingKey}\n" + + $"3. Make sure TTR's Jump control uses the same key\n" + + $" (Bot Settings → Configure Game Controls)\n\n" + $"━━━ BOT WILL DO ━━━\n" + $"• Aim: {summary.Aim}\n" + $"• Power: ~{summary.Power}%\n\n" + - $"After clicking OK, switch to TTR within {summary.DelaySeconds} seconds.\n\n" + + $"After clicking OK, TTR will be focused automatically.\n" + + $"The shot starts after {summary.DelaySeconds} seconds—keep TTR focused.\n\n" + "Ready to start?", "Golf Setup - " + selectedFileName, MessageBoxButtons.OKCancel, @@ -109,6 +164,17 @@ private async void startAutoGolfBtn_Click(object sender, EventArgs e) return; } + if (CoreFunctionality.FindToontownWindow() == IntPtr.Zero) + { + MessageBox.Show( + "The Toontown Rewritten window was not found.\n\n" + + "Start TTR and log in before starting auto golf.", + "TTR Not Found", + MessageBoxButtons.OK, + MessageBoxIcon.Warning); + return; + } + // Remind user about auto-golf setup var result = MessageBox.Show( "━━━ AUTO GOLF MODE ━━━\n\n" + @@ -118,7 +184,8 @@ private async void startAutoGolfBtn_Click(object sender, EventArgs e) "3. Aim and swing with the right power\n" + "4. Repeat for all 3 holes\n\n" + "━━━ YOU MUST DO ━━━\n" + - "• Set swing key to CTRL (default)\n" + + $"• TTR's Jump/swing key must match: {ConfiguredGolfSwingKey}\n" + + " (Bot Settings → Configure Game Controls)\n" + "• Keep TTR visible on screen\n" + "• ⚠️ Move to LEFT or RIGHT tee when instructed!\n" + " (Watch the overlay for position instructions)\n\n" + diff --git a/ToonTown Rewritten Bot/Views/MainForm.Settings.cs b/ToonTown Rewritten Bot/Views/MainForm.Settings.cs index 122d8de1..bd5c4358 100644 --- a/ToonTown Rewritten Bot/Views/MainForm.Settings.cs +++ b/ToonTown Rewritten Bot/Views/MainForm.Settings.cs @@ -87,6 +87,7 @@ private void btnGameControls_Click(object sender, EventArgs e) if (form.ShowDialog(this) == DialogResult.OK) { RefreshPreferencesDisplay(); + UpdateGolfInstructionsLabel(); } } } diff --git a/ToonTown Rewritten Bot/Views/MainForm.cs b/ToonTown Rewritten Bot/Views/MainForm.cs index ed05aaf1..4cc17ada 100644 --- a/ToonTown Rewritten Bot/Views/MainForm.cs +++ b/ToonTown Rewritten Bot/Views/MainForm.cs @@ -174,6 +174,7 @@ private void LoadUserPreferences() // Game control bindings — apply saved key bindings to the input layer. Models.GameControls.LoadFrom(prefs); + UpdateGolfInstructionsLabel(); // Global stop/pause hotkeys — apply saved bindings, then reflect them in the UI hints. Models.Hotkeys.LoadFrom(prefs); diff --git a/ToonTown Rewritten Bot/Views/MainForm.resx b/ToonTown Rewritten Bot/Views/MainForm.resx index 55c4d842..65abf9e6 100644 --- a/ToonTown Rewritten Bot/Views/MainForm.resx +++ b/ToonTown Rewritten Bot/Views/MainForm.resx @@ -3257,7 +3257,7 @@ Use 'Edit Selected' to manually modify existing files. - IMPORTANT: Your golf swing key in TTR must be set to CTRL (default). + IMPORTANT: Your configured Jump key must match TTR. 1. Select a golf course from the dropdown @@ -3265,12 +3265,12 @@ Use 'Edit Selected' to manually modify existing files. 3. In TTR, move to the correct tee position if required -4. Click 'Start Golf' and switch to TTR within 15 seconds +4. Click 'Start Golf' -5. Bot waits 15 sec (for position lock), then aims and swings! +5. Keep TTR focused until the bot aims and swings Note: Some courses require LEFT or RIGHT tee position. -The preview tells you where to stand - you must move there yourself. +Change keys under Settings → Configure Game Controls. @@ -5195,4 +5195,4 @@ Use this if the bot is clicking the wrong locations even after recapturing templ ////+AAA///////////////////////4AAA= - \ No newline at end of file + From e78c6d48f77fb02b052857d3cee10eb4f27f20b2 Mon Sep 17 00:00:00 2001 From: primetime43 <12754111+primetime43@users.noreply.github.com> Date: Fri, 31 Jul 2026 17:59:22 -0400 Subject: [PATCH 3/7] Improve golf turn timer detection reliability Refactor IsReadyToSwing to search only the top-right region instead of the full window, reducing false positives from orange course graphics. Add multi-variant template support, require two consecutive detections before confirming a turn, and restrict color fallback to timer-only region when no template is saved. Improve PromptForTurnTimerTemplate to support recapture with a Yes/No dialog and return whether a template was saved. Propagate detector status changes to the main status label in GolfService. --- .../Services/GolfService.cs | 10 +- .../Utilities/GolfCourseDetector.cs | 215 ++++++++++++++---- 2 files changed, 175 insertions(+), 50 deletions(-) diff --git a/ToonTown Rewritten Bot/Services/GolfService.cs b/ToonTown Rewritten Bot/Services/GolfService.cs index 9272a376..b81947eb 100644 --- a/ToonTown Rewritten Bot/Services/GolfService.cs +++ b/ToonTown Rewritten Bot/Services/GolfService.cs @@ -178,10 +178,15 @@ public static async Task StartContinuousAutoGolfAsync(CancellationToken cancella { await detector.InitializeAsync(); - // Subscribe to detector status changes for overlay updates + int holesPlayed = 0; + string lastPlayedCourse = null; + + // Keep both the overlay and the main status label informative while the detector + // is waiting, confirming the timer, or guiding a recapture. detector.StatusChanged += (status) => { UpdateOverlayStatus(status); + RaiseStatusChanged(status, null, holesPlayed, holesPerRound); }; RaiseStatusChanged("Auto-golf started - waiting for course...", null, 0, holesPerRound); @@ -192,9 +197,6 @@ public static async Task StartContinuousAutoGolfAsync(CancellationToken cancella UpdateOverlayStatus("Auto-golf active"); } - int holesPlayed = 0; - string lastPlayedCourse = null; - while (!cancellationToken.IsCancellationRequested && holesPlayed < holesPerRound) { // Wait for course detection diff --git a/ToonTown Rewritten Bot/Utilities/GolfCourseDetector.cs b/ToonTown Rewritten Bot/Utilities/GolfCourseDetector.cs index 3815775e..a79d3ad7 100644 --- a/ToonTown Rewritten Bot/Utilities/GolfCourseDetector.cs +++ b/ToonTown Rewritten Bot/Utilities/GolfCourseDetector.cs @@ -20,6 +20,7 @@ public class GolfCourseDetector : IDisposable private TextRecognition _ocr; private bool _disposed = false; private string _pencilButtonTemplatePath; + private bool _turnTimerTemplateMismatch; /// /// Event raised when detection status changes. @@ -836,6 +837,11 @@ private async Task PromptForManualCourseSelectionAsync() } private const string TurnTimerTemplateName = "Golf_Turn_Timer"; + private const double TurnTimerTemplateThreshold = 0.80; + private const int RequiredConsecutiveTurnDetections = 2; + private const int MissingTemplatePromptDelayMs = 15000; + private const int ExistingTemplatePromptDelayMs = 30000; + private const int WaitingStatusIntervalMs = 10000; /// /// Checks if the game is ready for the player to swing. @@ -843,32 +849,74 @@ private async Task PromptForManualCourseSelectionAsync() /// public bool IsReadyToSwing() { + _turnTimerTemplateMismatch = false; + try { using (var screenshot = (Bitmap)ImageRecognition.GetWindowScreenshot()) { if (screenshot == null) return false; - // Method 1: Template matching for the turn timer (most reliable if captured) - if (UIElementManager.Instance.HasTemplate(TurnTimerTemplateName)) + Rectangle timerRegion = GetTurnTimerSearchRegion(screenshot.Size); + + // Search only where the timer can appear. Full-window matching was both slow and + // susceptible to finding a similar orange graphic elsewhere on the course. + using (var timerImage = screenshot.Clone(timerRegion, screenshot.PixelFormat)) { - string templatePath = UIElementManager.Instance.GetTemplatePath(TurnTimerTemplateName); - using (var template = new Bitmap(templatePath)) + bool checkedUsableTemplate = false; + + // Try every captured variant so users can save timer appearances from + // different resolutions or countdown frames. + foreach (string templatePath in UIElementManager.Instance.GetAllTemplatePaths(TurnTimerTemplateName)) { - var result = ImageTemplateMatcher.FindTemplate(screenshot, template, 0.7); - if (result.Found) + try + { + using (var template = new Bitmap(templatePath)) + { + if (template.Width > timerImage.Width || template.Height > timerImage.Height) + { + Debug.WriteLine($"[GolfDetector] Skipping oversized timer template: {Path.GetFileName(templatePath)}"); + continue; + } + + checkedUsableTemplate = true; + var result = ImageTemplateMatcher.FindTemplate( + timerImage, + template, + TurnTimerTemplateThreshold); + if (result.Found) + { + Debug.WriteLine( + $"[GolfDetector] Timer template found in expected region " + + $"({Path.GetFileName(templatePath)}, confidence: {result.Confidence:P0})"); + return true; + } + } + } + catch (Exception ex) { - Debug.WriteLine($"[GolfDetector] Ready to swing - timer template found (confidence: {result.Confidence:P0})"); - return true; + // A corrupt or obsolete variant should not disable the color fallback + // or prevent another valid variant from being checked. + Debug.WriteLine($"[GolfDetector] Could not check timer template '{templatePath}': {ex.Message}"); } } - } - // Method 2: Detect the orange countdown timer by color in the top-right corner - if (DetectTurnTimerByColor(screenshot)) - { - Debug.WriteLine("[GolfDetector] Ready to swing - timer detected by color"); - return true; + bool colorDetected = DetectTurnTimerByColor(timerImage); + + // Once the user has a usable template, treat it as authoritative. Falling + // through to the looser color heuristic after a template miss caused random + // starts on orange course graphics. If color suggests the real timer is now + // visible, use that moment to offer recapture instead of starting the shot. + if (checkedUsableTemplate && colorDetected) + { + _turnTimerTemplateMismatch = true; + Debug.WriteLine("[GolfDetector] Orange timer candidate found, but saved templates did not match"); + } + else if (colorDetected) + { + Debug.WriteLine("[GolfDetector] Timer detected by color in expected region"); + return true; + } } return false; @@ -881,20 +929,29 @@ public bool IsReadyToSwing() } } + private static Rectangle GetTurnTimerSearchRegion(Size screenshotSize) + { + // The countdown clock is near the upper-right corner. Keep enough padding for + // different window sizes while excluding most course graphics from consideration. + int width = Math.Max(1, (int)Math.Ceiling(screenshotSize.Width * 0.25)); + int height = Math.Max(1, (int)Math.Ceiling(screenshotSize.Height * 0.22)); + return new Rectangle(screenshotSize.Width - width, 0, width, height); + } + /// /// Detects the orange countdown timer in the top-right corner. /// The timer is an orange/yellow circular clock that appears when it's your turn. /// - private bool DetectTurnTimerByColor(Bitmap screenshot) + private bool DetectTurnTimerByColor(Bitmap timerImage) { try { - // The timer is in the top-right corner of the screen - // Sample the area where the orange clock appears - int timerCenterX = screenshot.Width - (int)(screenshot.Width * 0.05); // ~5% from right edge - int timerCenterY = (int)(screenshot.Height * 0.07); // ~7% from top + // Coordinates are relative to the cropped top-right search region. The clock's + // expected full-window position (~95% across, ~7% down) lands near here. + int timerCenterX = (int)(timerImage.Width * 0.80); + int timerCenterY = (int)(timerImage.Height * 0.32); - int searchRadius = Math.Min(screenshot.Width, screenshot.Height) / 15; + int searchRadius = Math.Max(4, (int)(timerImage.Height * 0.30)); int orangePixelCount = 0; int totalSamples = 0; @@ -906,10 +963,10 @@ private bool DetectTurnTimerByColor(Bitmap screenshot) int x = timerCenterX + xOffset; int y = timerCenterY + yOffset; - if (x < 0 || x >= screenshot.Width || y < 0 || y >= screenshot.Height) + if (x < 0 || x >= timerImage.Width || y < 0 || y >= timerImage.Height) continue; - Color pixel = screenshot.GetPixel(x, y); + Color pixel = timerImage.GetPixel(x, y); totalSamples++; // Orange color: high R, medium-high G, low B @@ -984,23 +1041,72 @@ public bool IsTeeSelectionScreen() /// public async Task WaitUntilReadyToSwingAsync(CancellationToken cancellationToken, int scanIntervalMs = 500) { - int attempts = 0; - const int promptAfterAttempts = 30; // Prompt for template after ~15 seconds of not detecting + if (scanIntervalMs <= 0) + { + throw new ArgumentOutOfRangeException(nameof(scanIntervalMs), "Scan interval must be greater than zero."); + } + + int consecutiveDetections = 0; + int nextStatusAtMs = WaitingStatusIntervalMs; + bool recoveryOffered = false; + var waitTimer = Stopwatch.StartNew(); + + ReportStatus("Waiting for your turn - watching for the orange timer..."); while (!cancellationToken.IsCancellationRequested) { if (IsReadyToSwing()) { - return; + consecutiveDetections++; + if (consecutiveDetections >= RequiredConsecutiveTurnDetections) + { + ReportStatus("Turn confirmed - starting shot..."); + return; + } + + ReportStatus("Turn timer found - confirming..."); + } + else + { + // A single-frame result can be a transient course graphic. Require the timer + // to survive the next scan before allowing the macro to start. + if (consecutiveDetections > 0) + { + ReportStatus("Timer signal was brief - still waiting for your turn..."); + } + consecutiveDetections = 0; } - attempts++; + int elapsedMs = (int)Math.Min(int.MaxValue, waitTimer.ElapsedMilliseconds); + + if (elapsedMs >= nextStatusAtMs) + { + int elapsedSeconds = elapsedMs / 1000; + ReportStatus($"Still waiting for your turn ({elapsedSeconds}s) - looking for the orange timer..."); + nextStatusAtMs += WaitingStatusIntervalMs; + } - // If we've been waiting a while and don't have a timer template, offer to capture one - if (attempts == promptAfterAttempts && !UIElementManager.Instance.HasTemplate(TurnTimerTemplateName)) + bool hasTemplate = UIElementManager.Instance.HasTemplate(TurnTimerTemplateName); + int recoveryDelayMs = hasTemplate ? ExistingTemplatePromptDelayMs : MissingTemplatePromptDelayMs; + bool templateMismatchNowVisible = hasTemplate && _turnTimerTemplateMismatch; + if (!recoveryOffered && (templateMismatchNowVisible || elapsedMs >= recoveryDelayMs)) { - ReportStatus("Having trouble detecting turn - capture template?"); - PromptForTurnTimerTemplate(); + recoveryOffered = true; + ReportStatus(templateMismatchNowVisible + ? "Orange timer found, but the saved template is outdated - recapture help opened" + : hasTemplate + ? "Turn timer not detected - recapture help opened" + : "Turn timer template needed - capture help opened"); + + bool captured = PromptForTurnTimerTemplate( + allowRecapture: hasTemplate, + timerCandidateVisible: templateMismatchNowVisible); + ReportStatus(captured + ? "Timer template saved - resuming turn detection..." + : "Continuing to wait for the orange turn timer..."); + + // Do not count a frame from before a modal capture dialog toward confirmation. + consecutiveDetections = 0; } await Task.Delay(scanIntervalMs, cancellationToken); @@ -1010,35 +1116,52 @@ public async Task WaitUntilReadyToSwingAsync(CancellationToken cancellationToken /// /// Prompts the user to capture the turn timer template. /// - private void PromptForTurnTimerTemplate() + private bool PromptForTurnTimerTemplate(bool allowRecapture, bool timerCandidateVisible) { - if (UIElementManager.Instance.HasTemplate(TurnTimerTemplateName)) + bool hasTemplate = UIElementManager.Instance.HasTemplate(TurnTimerTemplateName); + if (hasTemplate && !allowRecapture) { - return; + return false; } // Need to invoke on UI thread if (Application.OpenForms.Count > 0) { var mainForm = Application.OpenForms[0]; - if (mainForm.InvokeRequired) + Func showCapture = () => { - mainForm.Invoke(new Action(() => + if (hasTemplate) { - TemplateCaptureForm.CaptureTemplate( - TurnTimerTemplateName, - "Capture the orange countdown timer (clock) in the top-right corner.\n" + - "This appears when it's your turn to swing."); - })); - } - else - { - TemplateCaptureForm.CaptureTemplate( + var choice = MessageBox.Show( + (timerCandidateVisible + ? "Auto Golf can see the orange timer, but your saved template did not match.\n\n" + : "Auto Golf has not detected your turn timer for 30 seconds.\n\n") + + "If the orange countdown clock is visible now, choose Yes and capture only the clock.\n" + + "If another player is taking a turn, choose No and Auto Golf will keep waiting.", + "Turn Timer Not Detected", + MessageBoxButtons.YesNo, + MessageBoxIcon.Information); + if (choice != DialogResult.Yes) + { + return false; + } + } + + return TemplateCaptureForm.CaptureTemplate( TurnTimerTemplateName, - "Capture the orange countdown timer (clock) in the top-right corner.\n" + - "This appears when it's your turn to swing."); + "Wait until the orange countdown clock is visible, then capture only the clock.\n" + + "Avoid including the course background or other UI so turn detection stays reliable."); + }; + + if (mainForm.InvokeRequired) + { + return (bool)mainForm.Invoke(showCapture); } + + return showCapture(); } + + return false; } /// From 445da53b343bc4a12c13fe770667571cd02034c7 Mon Sep 17 00:00:00 2001 From: primetime43 <12754111+primetime43@users.noreply.github.com> Date: Fri, 31 Jul 2026 18:10:39 -0400 Subject: [PATCH 4/7] Fix stale manual UI coordinates #84 Keep UIElementManager's manual fallback data in sync with the legacy coordinate file so Reset State actually clears stale positions. Manual element coordinates are now treated as absolute screen points, invalid manual values outside the game window are discarded and re-searched, and failures now direct the user to recapture the template instead of silently falling back. --- .../Services/CoordinatesManager.cs | 104 +++++++++--------- .../Templates/UIElementCoordinates.json | 8 +- .../Utilities/UIElementManager.cs | 32 ++++++ ToonTown Rewritten Bot/Views/MainForm.Dev.cs | 6 +- 4 files changed, 90 insertions(+), 60 deletions(-) diff --git a/ToonTown Rewritten Bot/Services/CoordinatesManager.cs b/ToonTown Rewritten Bot/Services/CoordinatesManager.cs index fd151c5c..cd883a5a 100644 --- a/ToonTown Rewritten Bot/Services/CoordinatesManager.cs +++ b/ToonTown Rewritten Bot/Services/CoordinatesManager.cs @@ -121,12 +121,61 @@ private static CoordinateActions FindCoordinateByKey(string key) Logger.Debug("Coordinates", $"Manual coords available for '{elementName}': ({manualCoords.x}, {manualCoords.y})"); UIElementManager.Instance.SetManualCoordinates(elementName, new Point(manualCoords.x, manualCoords.y)); } + else + { + // Keep UIElementManager's separate fallback store synchronized with the legacy + // coordinate file. Reset State previously zeroed this file but left a stale manual + // value (notably Red Fishing Button at 960,765) active in UIElementManager. + UIElementManager.Instance.ClearManualCoordinates(elementName); + } // Try to find using image recognition (will prompt for template capture if needed) var (location, source) = await UIElementManager.Instance.GetElementLocationWithSourceAsync(elementName, description); if (location.HasValue) { + if (source == UIElementSource.Manual) + { + // Manual updates are captured from the OS cursor, so they are already absolute + // screen coordinates. Adding the window offset again breaks moved/multi-monitor + // game windows. + var windowRect = CoreFunctionality.GetGameWindowRect(); + if (windowRect.IsEmpty) + { + throw new InvalidOperationException("Could not use manual coordinates because the Toontown window was not found."); + } + + if (!windowRect.Contains(location.Value)) + { + Logger.Warning( + "Coordinates", + $"Ignoring manual coordinates for '{elementName}' at ({location.Value.X}, {location.Value.Y}); " + + $"they are outside the game window {windowRect}."); + + // Remove the invalid value from both stores, then force a real search. If it + // still fails, UIElementManager will guide the user through recapture. + UpdateCoordinateByKey(keyAsString, 0, 0); + UIElementManager.Instance.ClearManualCoordinates(elementName); + (location, source) = await UIElementManager.Instance.GetElementLocationWithSourceAsync( + elementName, + description, + forceSearch: true); + } + else + { + Logger.Info( + "Coordinates", + $"'{elementName}': screen ({location.Value.X}, {location.Value.Y}) [source=Manual]"); + return (location.Value.X, location.Value.Y); + } + } + + if (!location.HasValue) + { + throw new InvalidOperationException( + $"Could not locate '{elementName}'. Recapture its template before retrying."); + } + // Convert window-relative coordinates to screen coordinates var windowOffset = CoreFunctionality.GetGameWindowOffset(); int screenX = location.Value.X + windowOffset.X; @@ -137,60 +186,9 @@ private static CoordinateActions FindCoordinateByKey(string key) return (screenX, screenY); } - // If still not found, fall back to manual coordinates from the old system - // Manual coordinates are already screen coordinates - if (manualCoords.x != 0 || manualCoords.y != 0) - { - Logger.Info("Coordinates", $"'{elementName}': screen ({manualCoords.x}, {manualCoords.y}) [source=ManualFallback]"); - return manualCoords; - } - - // Offer the user a chance to recapture the template before giving up - // Bring bot window to front so the dialog is visible over TTR - DialogResult recaptureChoice = DialogResult.No; - if (Application.OpenForms.Count > 0 && Application.OpenForms[0].InvokeRequired) - { - Application.OpenForms[0].Invoke(new Action(() => - { - CoreFunctionality.BringBotWindowToFront(); - recaptureChoice = MessageBox.Show( - $"Could not find '{elementName}' on screen. Would you like to capture the template for this element?", - "Element Not Found", - MessageBoxButtons.YesNo, - MessageBoxIcon.Question); - })); - } - else - { - CoreFunctionality.BringBotWindowToFront(); - recaptureChoice = MessageBox.Show( - $"Could not find '{elementName}' on screen. Would you like to capture the template for this element?", - "Element Not Found", - MessageBoxButtons.YesNo, - MessageBoxIcon.Question); - } - - if (recaptureChoice == DialogResult.Yes) - { - Logger.Info("Coordinates", $"User chose to recapture template for '{elementName}'"); - bool captured = UIElementManager.Instance.PromptForTemplateCapture(elementName, description ?? $"Please select the '{elementName}' on screen"); - if (captured) - { - // Retry with the newly captured template - var retryLocation = await UIElementManager.Instance.GetElementLocationAsync(elementName, description, forceSearch: true); - if (retryLocation.HasValue) - { - var windowOffset = CoreFunctionality.GetGameWindowOffset(); - int screenX = retryLocation.Value.X + windowOffset.X; - int screenY = retryLocation.Value.Y + windowOffset.Y; - Logger.Info("Coordinates", $"'{elementName}': screen ({screenX}, {screenY}) [source=Recapture]"); - return (screenX, screenY); - } - } - } - Logger.Error("Coordinates", $"Could not find '{elementName}' via image recognition or manual coordinates"); - throw new Exception($"Could not find element '{elementName}' via image recognition or manual coordinates."); + throw new InvalidOperationException( + $"Could not locate '{elementName}'. Recapture its template from the prompt or Dev tab before retrying."); } /// diff --git a/ToonTown Rewritten Bot/Templates/UIElementCoordinates.json b/ToonTown Rewritten Bot/Templates/UIElementCoordinates.json index a685a64d..57e527b5 100644 --- a/ToonTown Rewritten Bot/Templates/UIElementCoordinates.json +++ b/ToonTown Rewritten Bot/Templates/UIElementCoordinates.json @@ -65,9 +65,9 @@ "Templates\\Red_Fishing_Button.png" ], "TemplatePath": "Templates\\Red_Fishing_Button.png", - "ManualCoordinates": "960, 765", - "CachedCenter": "966, 782", - "LastFoundTime": "2026-03-03T15:35:57.4550924-05:00" + "ManualCoordinates": null, + "CachedCenter": null, + "LastFoundTime": null }, "Green SpeedChat Button": { "Name": "Green SpeedChat Button", @@ -315,4 +315,4 @@ "CachedCenter": null, "LastFoundTime": null } -} \ No newline at end of file +} diff --git a/ToonTown Rewritten Bot/Utilities/UIElementManager.cs b/ToonTown Rewritten Bot/Utilities/UIElementManager.cs index 3988a341..77cb2f69 100644 --- a/ToonTown Rewritten Bot/Utilities/UIElementManager.cs +++ b/ToonTown Rewritten Bot/Utilities/UIElementManager.cs @@ -518,6 +518,23 @@ public void SetManualCoordinates(string elementName, Point coordinates) } } + /// + /// Clears a manual fallback for an element. Also clears any location cached while that + /// fallback was active so the next lookup performs a real template search. + /// + public void ClearManualCoordinates(string elementName) + { + var element = GetOrCreateElement(elementName); + if (element.ManualCoordinates.HasValue) + { + element.ManualCoordinates = null; + element.CachedCenter = null; + element.LastFoundTime = null; + Logger.Info("TemplateMatch", $"Manual coordinates cleared for '{elementName}' (cache cleared)"); + SaveElementData(); + } + } + /// /// Gets manual fallback coordinates for an element. /// @@ -556,6 +573,21 @@ public void ClearAllCache() SaveElementData(); } + /// + /// Clears all manual fallbacks and cached locations. Used by Reset State so both the + /// legacy coordinate file and image-recognition state are actually reset together. + /// + public void ClearAllPositionData() + { + foreach (var element in _elements.Values) + { + element.ManualCoordinates = null; + element.CachedCenter = null; + element.LastFoundTime = null; + } + SaveElementData(); + } + /// /// Gets all registered element names. /// diff --git a/ToonTown Rewritten Bot/Views/MainForm.Dev.cs b/ToonTown Rewritten Bot/Views/MainForm.Dev.cs index 395ec603..f5fe04d5 100644 --- a/ToonTown Rewritten Bot/Views/MainForm.Dev.cs +++ b/ToonTown Rewritten Bot/Views/MainForm.Dev.cs @@ -16,9 +16,9 @@ public partial class MainForm private void devResetCoordinatesBtn_Click(object sender, EventArgs e) { CoordinatesManager.CreateFreshCoordinatesFile(); - // Also clear the image-recognition cache — otherwise stale cached centers - // from previous runs keep getting returned and "reset" feels broken. - UIElementManager.Instance.ClearAllCache(); + // Clear both image-recognition caches and its separate manual fallback store. + // Previously Reset State left stale manual coordinates behind there. + UIElementManager.Instance.ClearAllPositionData(); MessageBox.Show("Manual coordinates reset and image-recognition cache cleared.", "Reset State", MessageBoxButtons.OK, MessageBoxIcon.Information); } From 82bda4ed9dfb7443118dae67dbd080097a4179f2 Mon Sep 17 00:00:00 2001 From: primetime43 <12754111+primetime43@users.noreply.github.com> Date: Fri, 31 Jul 2026 18:30:21 -0400 Subject: [PATCH 5/7] Handle black PrintWindow captures gracefully #54 Introduces a dedicated `WindowCaptureException` and hardens screenshot capture when `PrintWindow` returns unusable black frames. `ImageRecognition` now retries `PrintWindow` with alternate flags, detects effectively black captures via grid sampling, falls back to visible screen capture when needed, and emits a one-time warning when background capture is unavailable. It also adds explicit minimized-window and invalid-bounds failure messages. Fish and golf detectors were updated to rethrow `WindowCaptureException` instead of swallowing it in generic handlers, so capture failures surface correctly. --- .../Utilities/FishBubbleDetector.cs | 4 + .../Utilities/GolfCourseDetector.cs | 32 +++ .../Utilities/ImageRecognition.cs | 200 +++++++++++++++--- 3 files changed, 207 insertions(+), 29 deletions(-) diff --git a/ToonTown Rewritten Bot/Utilities/FishBubbleDetector.cs b/ToonTown Rewritten Bot/Utilities/FishBubbleDetector.cs index 5137adfa..c9a1308b 100644 --- a/ToonTown Rewritten Bot/Utilities/FishBubbleDetector.cs +++ b/ToonTown Rewritten Bot/Utilities/FishBubbleDetector.cs @@ -585,6 +585,10 @@ public async Task DetectFishAndCalculateCastAsync(CancellationTok return ScanForBubbleByColor(screenshot, sStartX, sStartY, sEndX, sEndY, windowOffset, cancellationToken); } } + catch (WindowCaptureException) + { + throw; + } catch (Exception ex) { Logger.Warning("FishDetect", $"Scan error: {ex.Message}"); diff --git a/ToonTown Rewritten Bot/Utilities/GolfCourseDetector.cs b/ToonTown Rewritten Bot/Utilities/GolfCourseDetector.cs index a79d3ad7..515ab7d6 100644 --- a/ToonTown Rewritten Bot/Utilities/GolfCourseDetector.cs +++ b/ToonTown Rewritten Bot/Utilities/GolfCourseDetector.cs @@ -161,6 +161,10 @@ public string DetectCourse() } } } + catch (WindowCaptureException) + { + throw; + } catch (Exception ex) { System.Diagnostics.Debug.WriteLine($"[GolfDetector] Error detecting course: {ex.Message}"); @@ -268,6 +272,10 @@ public bool IsScoreboardOpen() return isOpen; } } + catch (WindowCaptureException) + { + throw; + } catch (Exception ex) { Debug.WriteLine($"[GolfDetector] Error in IsScoreboardOpen: {ex.Message}"); @@ -392,6 +400,10 @@ public async Task CloseScoreboardAsync() return; } } + catch (WindowCaptureException) + { + throw; + } catch (Exception ex) { Debug.WriteLine($"[GolfDetector] Error closing scoreboard: {ex.Message}"); @@ -583,6 +595,10 @@ private string PromptForPencilButtonTemplate() } } } + catch (WindowCaptureException) + { + throw; + } catch (Exception ex) { Debug.WriteLine($"[GolfDetector] Error finding pencil button: {ex.Message}"); @@ -623,6 +639,10 @@ public async Task ClickPencilButtonAsync() Debug.WriteLine($"[GolfDetector] Clicked pencil button at ({clickX}, {clickY})"); return true; } + catch (WindowCaptureException) + { + throw; + } catch (Exception ex) { Debug.WriteLine($"[GolfDetector] Error clicking pencil button: {ex.Message}"); @@ -665,6 +685,10 @@ public async Task DetectCourseViaScoreboardAsync() return course; } + catch (WindowCaptureException) + { + throw; + } catch (Exception ex) { ReportStatus($"Error: {ex.Message}"); @@ -922,6 +946,10 @@ public bool IsReadyToSwing() return false; } } + catch (WindowCaptureException) + { + throw; + } catch (Exception ex) { Debug.WriteLine($"[GolfDetector] Error checking ready state: {ex.Message}"); @@ -1030,6 +1058,10 @@ public bool IsTeeSelectionScreen() lowerText.Contains("left") && lowerText.Contains("right"); } } + catch (WindowCaptureException) + { + throw; + } catch { return false; diff --git a/ToonTown Rewritten Bot/Utilities/ImageRecognition.cs b/ToonTown Rewritten Bot/Utilities/ImageRecognition.cs index aa4536ed..f6953d1e 100644 --- a/ToonTown Rewritten Bot/Utilities/ImageRecognition.cs +++ b/ToonTown Rewritten Bot/Utilities/ImageRecognition.cs @@ -11,8 +11,18 @@ namespace ToonTown_Rewritten_Bot.Utilities { + internal sealed class WindowCaptureException : InvalidOperationException + { + public WindowCaptureException(string message) : base(message) + { + } + } + class ImageRecognition { + private static volatile bool _printWindowUnavailable; + private static int _screenFallbackWarningLogged; + /// /// Captures a screenshot of the game window. /// @@ -25,68 +35,196 @@ public static Image GetWindowScreenshot(bool captureBackground = true) nint windowHandle = NativeMethods.FindWindow(null, windowName); if (windowHandle == nint.Zero) { - throw new ArgumentException("Window not found."); + throw new WindowCaptureException("The Toontown Rewritten window was not found."); } // Get the window's position and size NativeMethods.Rect windowRect = new NativeMethods.Rect(); - NativeMethods.GetWindowRect(windowHandle, ref windowRect); + if (!NativeMethods.GetWindowRect(windowHandle, ref windowRect)) + { + throw new WindowCaptureException("Could not read the Toontown window bounds for screen capture."); + } if (captureBackground) { - // Use PrintWindow to capture even when window is behind other windows + // Some GPU/driver combinations report PrintWindow success but return black pixels. + // Once that is detected, avoid repeatedly calling the broken path for this session. + if (_printWindowUnavailable) + { + return CaptureVisibleWindow(windowHandle, windowRect); + } + return CaptureWindowWithPrintWindow(windowHandle, windowRect.Width, windowRect.Height); } else { - // Traditional screen capture (only works when window is visible) - Bitmap screenshot = new Bitmap(windowRect.Width, windowRect.Height); - using (Graphics graphics = Graphics.FromImage(screenshot)) - { - graphics.CopyFromScreen(windowRect.Left, windowRect.Top, 0, 0, screenshot.Size); - } - return screenshot; + return CaptureVisibleWindow(windowHandle, windowRect); } } /// /// Captures a window using PrintWindow API, which works even when the window is obscured. + /// Retries with the legacy flag and falls back to visible screen capture when Windows + /// reports success but returns a black frame. /// private static Bitmap CaptureWindowWithPrintWindow(nint windowHandle, int width, int height) + { + Bitmap bitmap = TryCaptureWithPrintWindow( + windowHandle, + width, + height, + NativeMethods.PW_RENDERFULLCONTENT); + if (bitmap != null) + { + return bitmap; + } + + // Some drivers support PrintWindow but not PW_RENDERFULLCONTENT. + bitmap = TryCaptureWithPrintWindow(windowHandle, width, height, 0); + if (bitmap != null) + { + return bitmap; + } + + _printWindowUnavailable = true; + if (Interlocked.Exchange(ref _screenFallbackWarningLogged, 1) == 0) + { + Logger.Warning( + "Capture", + "PrintWindow returned unusable black frames. Using visible screen capture for this session; " + + "keep the Toontown window visible and unobscured."); + } + + NativeMethods.Rect windowRect = new NativeMethods.Rect(); + if (!NativeMethods.GetWindowRect(windowHandle, ref windowRect)) + { + throw new WindowCaptureException("Could not read the Toontown window bounds for screen capture."); + } + + return CaptureVisibleWindow(windowHandle, windowRect); + } + + private static Bitmap TryCaptureWithPrintWindow( + nint windowHandle, + int width, + int height, + uint flags) { Bitmap bitmap = new Bitmap(width, height, PixelFormat.Format32bppArgb); + bool success; using (Graphics graphics = Graphics.FromImage(bitmap)) { IntPtr hdc = graphics.GetHdc(); try { - // PW_RENDERFULLCONTENT (0x2) works better with DWM/hardware-accelerated windows - bool success = NativeMethods.PrintWindow(windowHandle, hdc, NativeMethods.PW_RENDERFULLCONTENT); - - if (!success) - { - // Fallback: try without the flag - success = NativeMethods.PrintWindow(windowHandle, hdc, 0); - } - - if (!success) - { - // If PrintWindow fails completely, fall back to screen capture - graphics.ReleaseHdc(hdc); - NativeMethods.Rect windowRect = new NativeMethods.Rect(); - NativeMethods.GetWindowRect(windowHandle, ref windowRect); - graphics.CopyFromScreen(windowRect.Left, windowRect.Top, 0, 0, bitmap.Size); - return bitmap; - } + success = NativeMethods.PrintWindow(windowHandle, hdc, flags); } finally { + // Release exactly once. The previous fallback path released here and inside the + // failure branch, leaving Graphics in an invalid state on some machines. graphics.ReleaseHdc(hdc); } } - return bitmap; + if (success && !IsBitmapEffectivelyBlack(bitmap)) + { + return bitmap; + } + + Debug.WriteLine(success + ? $"PrintWindow returned a black frame (flags=0x{flags:X})" + : $"PrintWindow failed (flags=0x{flags:X})"); + bitmap.Dispose(); + return null; + } + + private static Bitmap CaptureVisibleWindow(nint windowHandle, NativeMethods.Rect windowRect) + { + if (windowRect.Width <= 0 || windowRect.Height <= 0) + { + throw new WindowCaptureException("The Toontown window has invalid capture dimensions."); + } + + if (NativeMethods.IsIconic(windowHandle)) + { + throw new WindowCaptureException( + "Toontown is minimized and this graphics driver does not support background capture. " + + "Restore the game window and try again."); + } + + Bitmap screenshot = new Bitmap(windowRect.Width, windowRect.Height, PixelFormat.Format32bppArgb); + try + { + using (Graphics graphics = Graphics.FromImage(screenshot)) + { + graphics.CopyFromScreen(windowRect.Left, windowRect.Top, 0, 0, screenshot.Size); + } + + if (IsBitmapEffectivelyBlack(screenshot)) + { + throw new WindowCaptureException( + "Both background and visible Toontown capture returned a black frame. Keep the game " + + "visible and unobscured, then try disabling Hardware-accelerated GPU scheduling or " + + "updating/rolling back the graphics driver."); + } + + return screenshot; + } + catch + { + screenshot.Dispose(); + throw; + } + } + + /// + /// Detects the effectively empty frames returned by PrintWindow on affected Intel/Windows + /// configurations. A grid is used instead of a single pixel so dark UI borders do not + /// trigger the fallback, while a frame with only a few non-black artifacts still does. + /// + private static bool IsBitmapEffectivelyBlack(Bitmap bitmap) + { + if (bitmap == null || bitmap.Width <= 0 || bitmap.Height <= 0) + { + return true; + } + + const int columns = 9; + const int rows = 7; + const int blackChannelThreshold = 12; + const double requiredBlackRatio = 0.95; + + int marginX = bitmap.Width / 10; + int marginY = bitmap.Height / 10; + int sampleWidth = Math.Max(1, bitmap.Width - (marginX * 2)); + int sampleHeight = Math.Max(1, bitmap.Height - (marginY * 2)); + int blackSamples = 0; + int totalSamples = 0; + + for (int row = 0; row < rows; row++) + { + int y = marginY + (sampleHeight - 1) * row / (rows - 1); + y = Math.Min(y, bitmap.Height - 1); + + for (int column = 0; column < columns; column++) + { + int x = marginX + (sampleWidth - 1) * column / (columns - 1); + x = Math.Min(x, bitmap.Width - 1); + + Color pixel = bitmap.GetPixel(x, y); + if (pixel.R <= blackChannelThreshold && + pixel.G <= blackChannelThreshold && + pixel.B <= blackChannelThreshold) + { + blackSamples++; + } + totalSamples++; + } + } + + return totalSamples == 0 || (double)blackSamples / totalSamples >= requiredBlackRatio; } /// @@ -193,6 +331,10 @@ public struct Rect [return: MarshalAs(UnmanagedType.Bool)] public static extern bool IsWindowVisible(IntPtr hWnd); + [DllImport("user32.dll")] + [return: MarshalAs(UnmanagedType.Bool)] + public static extern bool IsIconic(IntPtr hWnd); + // PrintWindow flags public const uint PW_RENDERFULLCONTENT = 0x00000002; // Works better with DWM/hardware acceleration } From 040dba51e01be331caaf8695d5e59890be730233 Mon Sep 17 00:00:00 2001 From: primetime43 <12754111+primetime43@users.noreply.github.com> Date: Fri, 31 Jul 2026 18:45:28 -0400 Subject: [PATCH 6/7] Improve keyboard hook, DPI, and UX robustness - Switch DPI mode to PerMonitorV2 for sharper rendering on high-DPI displays - GlobalKeyboardHook.Start() now returns bool and exposes LastErrorCode; callers show actionable error dialogs on failure - Custom fishing recorders show warnings when nothing was recorded and restore the window before showing dialogs - Key-to-action mapping delegates to GameControls.GetMovementAction() so custom bindings are respected instead of hardcoding arrow keys - Add GetMovementBindingSummary() helper and update recorder UI labels to show the active bindings - Update Random Variance tooltip to clarify it does not provide ban protection - Add README warning about automation policy risks - Fix 'black frame' log messages to say 'blank frame'; add uniform-frame detection to PrintWindow fallback logic --- README.md | 5 ++ ToonTown Rewritten Bot/Models/GameControls.cs | 19 ++++++ ToonTown Rewritten Bot/Program.cs | 2 +- .../Utilities/GlobalKeyboardHook.cs | 25 +++++--- .../Utilities/ImageRecognition.cs | 64 +++++++++++++++++-- .../Views/CustomFishingActions.cs | 54 +++++++++++----- .../Views/CustomFishingWizardForm.cs | 55 +++++++++++----- .../Views/MainForm.Designer.cs | 2 +- ToonTown Rewritten Bot/Views/MainForm.cs | 11 +++- 9 files changed, 190 insertions(+), 47 deletions(-) diff --git a/README.md b/README.md index 355941b7..1d0335f9 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,10 @@ # Toontown-Rewritten-Bot +> [!WARNING] +> Toontown Rewritten prohibits third-party programs that automate gameplay. Using this bot can +> result in penalties, including account or Toon stat action. Randomized timing or cursor movement +> does not make automation undetectable or safe. Use is entirely at your own risk. + Table of Contents - [Toontown-Rewritten-Bot](#toontown-rewritten-bot) diff --git a/ToonTown Rewritten Bot/Models/GameControls.cs b/ToonTown Rewritten Bot/Models/GameControls.cs index 7f7c5df0..8a05cfd8 100644 --- a/ToonTown Rewritten Bot/Models/GameControls.cs +++ b/ToonTown Rewritten Bot/Models/GameControls.cs @@ -44,6 +44,25 @@ public static VirtualKeyCode Remap(VirtualKeyCode key) }; } + /// + /// Maps a raw Windows virtual-key code from the global recorder to a movement action. + /// This keeps custom-action recording aligned with the bindings configured in the app. + /// + public static string GetMovementAction(int virtualKeyCode) + { + if (virtualKeyCode == (int)Forward) return "WALK FORWARDS"; + if (virtualKeyCode == (int)Reverse) return "WALK BACKWARDS"; + if (virtualKeyCode == (int)Left) return "TURN LEFT"; + if (virtualKeyCode == (int)Right) return "TURN RIGHT"; + return null; + } + + public static string GetMovementBindingSummary() + { + return $"{GetDisplayName(Forward)}, {GetDisplayName(Reverse)}, " + + $"{GetDisplayName(Left)}, {GetDisplayName(Right)}"; + } + /// /// Applies the saved bindings from to the runtime fields. /// Unparseable or empty values fall back to the TTR default for that control. diff --git a/ToonTown Rewritten Bot/Program.cs b/ToonTown Rewritten Bot/Program.cs index 74c838b5..b0c89edf 100644 --- a/ToonTown Rewritten Bot/Program.cs +++ b/ToonTown Rewritten Bot/Program.cs @@ -14,8 +14,8 @@ static class Program [STAThread] static void Main() { + Application.SetHighDpiMode(HighDpiMode.PerMonitorV2); Application.EnableVisualStyles(); - Application.SetHighDpiMode(HighDpiMode.SystemAware); Application.SetCompatibleTextRenderingDefault(false); Application.Run(new MainForm()); } diff --git a/ToonTown Rewritten Bot/Utilities/GlobalKeyboardHook.cs b/ToonTown Rewritten Bot/Utilities/GlobalKeyboardHook.cs index 763b5f86..3171c4a0 100644 --- a/ToonTown Rewritten Bot/Utilities/GlobalKeyboardHook.cs +++ b/ToonTown Rewritten Bot/Utilities/GlobalKeyboardHook.cs @@ -22,6 +22,9 @@ public class GlobalKeyboardHook : IDisposable private LowLevelKeyboardProc _proc; private bool _disposed = false; + public bool IsRunning => _hookId != IntPtr.Zero; + public int LastErrorCode { get; private set; } + /// /// When true, the next handled key press will be suppressed (not passed to the game). /// Set by the event handler to consume the key. @@ -64,10 +67,15 @@ public GlobalKeyboardHook() /// /// Starts listening for global keyboard events. /// - public void Start() + public bool Start() { if (_hookId != IntPtr.Zero) - return; // Already hooked + return true; // Already hooked + + if (_disposed) + { + throw new ObjectDisposedException(nameof(GlobalKeyboardHook)); + } using (Process curProcess = Process.GetCurrentProcess()) using (ProcessModule curModule = curProcess.MainModule) @@ -77,13 +85,14 @@ public void Start() if (_hookId == IntPtr.Zero) { - int errorCode = Marshal.GetLastWin32Error(); - Logger.Error("Input", $"Failed to install keyboard hook (error {errorCode}). F12/Esc hotkeys will not work."); - } - else - { - Logger.Debug("Input", "Global keyboard hook installed successfully"); + LastErrorCode = Marshal.GetLastWin32Error(); + Logger.Error("Input", $"Failed to install global keyboard hook (Windows error {LastErrorCode})."); + return false; } + + LastErrorCode = 0; + Logger.Debug("Input", "Global keyboard hook installed successfully"); + return true; } /// diff --git a/ToonTown Rewritten Bot/Utilities/ImageRecognition.cs b/ToonTown Rewritten Bot/Utilities/ImageRecognition.cs index f6953d1e..8a6c2602 100644 --- a/ToonTown Rewritten Bot/Utilities/ImageRecognition.cs +++ b/ToonTown Rewritten Bot/Utilities/ImageRecognition.cs @@ -47,7 +47,7 @@ public static Image GetWindowScreenshot(bool captureBackground = true) if (captureBackground) { - // Some GPU/driver combinations report PrintWindow success but return black pixels. + // Some GPU/driver combinations report PrintWindow success but return blank pixels. // Once that is detected, avoid repeatedly calling the broken path for this session. if (_printWindowUnavailable) { @@ -65,7 +65,7 @@ public static Image GetWindowScreenshot(bool captureBackground = true) /// /// Captures a window using PrintWindow API, which works even when the window is obscured. /// Retries with the legacy flag and falls back to visible screen capture when Windows - /// reports success but returns a black frame. + /// reports success but returns a black or uniform blank frame. /// private static Bitmap CaptureWindowWithPrintWindow(nint windowHandle, int width, int height) { @@ -91,7 +91,7 @@ private static Bitmap CaptureWindowWithPrintWindow(nint windowHandle, int width, { Logger.Warning( "Capture", - "PrintWindow returned unusable black frames. Using visible screen capture for this session; " + + "PrintWindow returned unusable blank frames. Using visible screen capture for this session; " + "keep the Toontown window visible and unobscured."); } @@ -128,13 +128,14 @@ private static Bitmap TryCaptureWithPrintWindow( } } - if (success && !IsBitmapEffectivelyBlack(bitmap)) + string unusableReason = success ? GetPrintWindowFrameFailureReason(bitmap) : null; + if (success && unusableReason == null) { return bitmap; } Debug.WriteLine(success - ? $"PrintWindow returned a black frame (flags=0x{flags:X})" + ? $"PrintWindow returned an unusable {unusableReason} (flags=0x{flags:X})" : $"PrintWindow failed (flags=0x{flags:X})"); bitmap.Dispose(); return null; @@ -227,6 +228,59 @@ private static bool IsBitmapEffectivelyBlack(Bitmap bitmap) return totalSamples == 0 || (double)blackSamples / totalSamples >= requiredBlackRatio; } + /// + /// Identifies frames where PrintWindow rendered the non-client title bar but filled the + /// game client area with one flat gray/white color. The interior-only grid deliberately + /// excludes the title bar and borders that can otherwise make a blank frame look valid. + /// + private static string GetPrintWindowFrameFailureReason(Bitmap bitmap) + { + if (IsBitmapEffectivelyBlack(bitmap)) + { + return "black frame"; + } + + const int columns = 9; + const int rows = 7; + const int uniformChannelRange = 8; + + int marginX = bitmap.Width / 10; + int marginY = bitmap.Height / 10; + int sampleWidth = Math.Max(1, bitmap.Width - (marginX * 2)); + int sampleHeight = Math.Max(1, bitmap.Height - (marginY * 2)); + int minR = 255; + int minG = 255; + int minB = 255; + int maxR = 0; + int maxG = 0; + int maxB = 0; + + for (int row = 0; row < rows; row++) + { + int y = marginY + (sampleHeight - 1) * row / (rows - 1); + y = Math.Min(y, bitmap.Height - 1); + + for (int column = 0; column < columns; column++) + { + int x = marginX + (sampleWidth - 1) * column / (columns - 1); + x = Math.Min(x, bitmap.Width - 1); + + Color pixel = bitmap.GetPixel(x, y); + minR = Math.Min(minR, pixel.R); + minG = Math.Min(minG, pixel.G); + minB = Math.Min(minB, pixel.B); + maxR = Math.Max(maxR, pixel.R); + maxG = Math.Max(maxG, pixel.G); + maxB = Math.Max(maxB, pixel.B); + } + } + + bool isUniform = maxR - minR <= uniformChannelRange && + maxG - minG <= uniformChannelRange && + maxB - minB <= uniformChannelRange; + return isUniform ? "uniform/blank frame" : null; + } + /// /// Gets the game window handle. /// diff --git a/ToonTown Rewritten Bot/Views/CustomFishingActions.cs b/ToonTown Rewritten Bot/Views/CustomFishingActions.cs index 29e4c2ed..646ef3e3 100644 --- a/ToonTown Rewritten Bot/Views/CustomFishingActions.cs +++ b/ToonTown Rewritten Bot/Views/CustomFishingActions.cs @@ -43,6 +43,7 @@ public CustomFishingActions() _keyboardHook = new GlobalKeyboardHook(); _keyboardHook.KeyPressed += OnGlobalKeyPressed; _keyboardHook.KeyReleased += OnGlobalKeyReleased; + lblRecorderHelp.Text = $"Start recording, then walk in TTR using {GameControls.GetMovementBindingSummary()}. Add Sell at the bucket, then stop."; UpdatePathPreview(); } @@ -355,6 +356,22 @@ private void btnStartRecording_Click(object sender, EventArgs e) return; } + if (!_keyboardHook.Start()) + { + this.WindowState = FormWindowState.Normal; + this.BringToFront(); + lblRecordingStatus.Text = "Status: Could not start keyboard recording"; + lblRecordingStatus.ForeColor = Color.Red; + MessageBox.Show( + this, + $"The keyboard recorder could not start (Windows error {_keyboardHook.LastErrorCode}).\n\n" + + "Restart the bot and make sure the bot and Toontown are running at the same administrator level.", + "Recording Unavailable", + MessageBoxButtons.OK, + MessageBoxIcon.Error); + return; + } + // Clear previous recording _recordedKeys.Clear(); _currentKeyHeld = null; @@ -364,14 +381,13 @@ private void btnStartRecording_Click(object sender, EventArgs e) btnStartRecording.Enabled = false; btnStopRecording.Enabled = true; btnAddSellFish.Enabled = true; - lblRecordingStatus.Text = "Status: Recording... Press arrow keys in TTR"; + lblRecordingStatus.Text = $"Status: Recording... Use {GameControls.GetMovementBindingSummary()}"; lblRecordingStatus.ForeColor = Color.Green; lblLivePreview.Text = ""; // Start recording _isRecording = true; _recordingStopwatch.Restart(); - _keyboardHook.Start(); } private void btnStopRecording_Click(object sender, EventArgs e) @@ -384,13 +400,28 @@ private void btnStopRecording_Click(object sender, EventArgs e) // Convert recorded keys to action items ConvertRecordedKeysToActionItems(); - // Update UI - lblRecordingStatus.Text = $"Status: Stopped - {_recordedKeys.Count} actions recorded"; - lblRecordingStatus.ForeColor = Color.Blue; - - // Restore window + // Restore before showing any feedback so warnings cannot appear behind a minimized owner. this.WindowState = FormWindowState.Normal; this.BringToFront(); + + // Update UI + if (_recordedKeys.Count == 0) + { + lblRecordingStatus.Text = "Status: No actions recorded"; + lblRecordingStatus.ForeColor = Color.Red; + MessageBox.Show( + this, + $"No movement keys were recorded. Use the configured keys ({GameControls.GetMovementBindingSummary()}) and make sure the bot and Toontown run at the same administrator level.", + "Nothing Recorded", + MessageBoxButtons.OK, + MessageBoxIcon.Warning); + } + else + { + lblRecordingStatus.Text = $"Status: Stopped - {_recordedKeys.Count} actions recorded"; + lblRecordingStatus.ForeColor = Color.Blue; + } + } private void btnAddSellFish_Click(object sender, EventArgs e) @@ -495,14 +526,7 @@ private void FinalizeCurrentKey() private string GetActionFromKey(Keys key) { - return key switch - { - Keys.Up => "WALK FORWARDS", - Keys.Down => "WALK BACKWARDS", - Keys.Left => "TURN LEFT", - Keys.Right => "TURN RIGHT", - _ => null - }; + return GameControls.GetMovementAction((int)(key & Keys.KeyCode)); } private void StopRecordingCleanup() diff --git a/ToonTown Rewritten Bot/Views/CustomFishingWizardForm.cs b/ToonTown Rewritten Bot/Views/CustomFishingWizardForm.cs index ca330360..241c7ff3 100644 --- a/ToonTown Rewritten Bot/Views/CustomFishingWizardForm.cs +++ b/ToonTown Rewritten Bot/Views/CustomFishingWizardForm.cs @@ -332,7 +332,7 @@ private void UpdateUI() { case 1: lblStepTitle.Text = "Step 1: Record Walk to Fisherman"; - lblStepDescription.Text = "Stand on your fishing dock in TTR. Click 'Start Recording', then walk to the fisherman using arrow keys. Stop when you reach the sell bucket."; + lblStepDescription.Text = $"Stand on your fishing dock in TTR. Click 'Start Recording', then walk to the fisherman using your configured keys ({GameControls.GetMovementBindingSummary()})."; groupBoxContent.Text = "Record Path to Fisherman"; btnStartRecording.Visible = true; btnStopRecording.Visible = true; @@ -354,7 +354,7 @@ private void UpdateUI() case 3: lblStepTitle.Text = "Step 3: Record Walk Back to Dock"; - lblStepDescription.Text = "After selling, you need to walk back to the dock. Click 'Start Recording', then walk back to your fishing dock."; + lblStepDescription.Text = $"After selling, click 'Start Recording' and walk back using your configured keys ({GameControls.GetMovementBindingSummary()})."; groupBoxContent.Text = "Record Path Back to Dock"; btnStartRecording.Visible = true; btnStopRecording.Visible = true; @@ -455,6 +455,22 @@ private void BtnStartRecording_Click(object sender, EventArgs e) return; } + if (!_keyboardHook.Start()) + { + this.WindowState = FormWindowState.Normal; + this.BringToFront(); + lblRecordingStatus.Text = "Status: Could not start keyboard recording"; + lblRecordingStatus.ForeColor = Color.Red; + MessageBox.Show( + this, + $"The keyboard recorder could not start (Windows error {_keyboardHook.LastErrorCode}).\n\n" + + "Restart the bot and make sure the bot and Toontown are running at the same administrator level.", + "Recording Unavailable", + MessageBoxButtons.OK, + MessageBoxIcon.Error); + return; + } + // Clear previous recording _recordedKeys.Clear(); _currentKeyHeld = null; @@ -463,13 +479,12 @@ private void BtnStartRecording_Click(object sender, EventArgs e) // Update UI btnStartRecording.Enabled = false; btnStopRecording.Enabled = true; - lblRecordingStatus.Text = "Status: Recording... Press arrow keys in TTR"; + lblRecordingStatus.Text = $"Status: Recording... Use {GameControls.GetMovementBindingSummary()}"; lblRecordingStatus.ForeColor = Color.Green; // Start recording _isRecording = true; _recordingStopwatch.Restart(); - _keyboardHook.Start(); } private void BtnStopRecording_Click(object sender, EventArgs e) @@ -491,12 +506,27 @@ private void BtnStopRecording_Click(object sender, EventArgs e) UpdatePathPreviewLabel(actions); - // Update UI - lblRecordingStatus.Text = $"Status: Recorded {_recordedKeys.Count} movements"; - lblRecordingStatus.ForeColor = Color.Blue; - this.WindowState = FormWindowState.Normal; this.BringToFront(); + + // Update UI + if (_recordedKeys.Count == 0) + { + lblRecordingStatus.Text = "Status: No movements recorded"; + lblRecordingStatus.ForeColor = Color.Red; + MessageBox.Show( + this, + $"No movement keys were recorded. Use the configured keys ({GameControls.GetMovementBindingSummary()}) and make sure the bot and Toontown run at the same administrator level.", + "Nothing Recorded", + MessageBoxButtons.OK, + MessageBoxIcon.Warning); + } + else + { + lblRecordingStatus.Text = $"Status: Recorded {_recordedKeys.Count} movements"; + lblRecordingStatus.ForeColor = Color.Blue; + } + } private void OnGlobalKeyPressed(object sender, Keys key) @@ -561,14 +591,7 @@ private void FinalizeCurrentKey() private string GetActionFromKey(Keys key) { - return key switch - { - Keys.Up => "WALK FORWARDS", - Keys.Down => "WALK BACKWARDS", - Keys.Left => "TURN LEFT", - Keys.Right => "TURN RIGHT", - _ => null - }; + return GameControls.GetMovementAction((int)(key & Keys.KeyCode)); } private void StopRecordingCleanup() diff --git a/ToonTown Rewritten Bot/Views/MainForm.Designer.cs b/ToonTown Rewritten Bot/Views/MainForm.Designer.cs index ecb62234..77382a68 100644 --- a/ToonTown Rewritten Bot/Views/MainForm.Designer.cs +++ b/ToonTown Rewritten Bot/Views/MainForm.Designer.cs @@ -520,7 +520,7 @@ private void InitializeComponent() randomFishingCheckBox.Size = new System.Drawing.Size(135, 20); randomFishingCheckBox.TabIndex = 6; randomFishingCheckBox.Text = "Random Variance"; - toolTip1.SetToolTip(randomFishingCheckBox, "Adds randomness so you won't cast your line at the same spot every time."); + toolTip1.SetToolTip(randomFishingCheckBox, "Varies the cast position only. This does not provide ban protection or make automation safe."); randomFishingCheckBox.UseVisualStyleBackColor = true; randomFishingCheckBox.CheckedChanged += randomFishing_CheckedChanged; // diff --git a/ToonTown Rewritten Bot/Views/MainForm.cs b/ToonTown Rewritten Bot/Views/MainForm.cs index 4cc17ada..2df1406e 100644 --- a/ToonTown Rewritten Bot/Views/MainForm.cs +++ b/ToonTown Rewritten Bot/Views/MainForm.cs @@ -45,7 +45,16 @@ public MainForm() // Enable global keyboard hook (works even when game has focus) _globalKeyboardHook = new GlobalKeyboardHook(); _globalKeyboardHook.KeyPressed += GlobalKeyboardHook_KeyPressed; - _globalKeyboardHook.Start(); + if (!_globalKeyboardHook.Start()) + { + MessageBox.Show( + $"Global keyboard shortcuts could not start (Windows error {_globalKeyboardHook.LastErrorCode}).\n\n" + + "F11/F12/Esc shortcuts and custom path recording may not work. Restart the bot and make " + + "sure the bot and Toontown are running at the same administrator level.", + "Keyboard Shortcuts Unavailable", + MessageBoxButtons.OK, + MessageBoxIcon.Warning); + } // Clean up hook when form closes this.FormClosing += MainForm_FormClosing; From f6103fe934163a42e3421dab21f0287b7c6126fe Mon Sep 17 00:00:00 2001 From: primetime43 <12754111+primetime43@users.noreply.github.com> Date: Fri, 31 Jul 2026 19:08:40 -0400 Subject: [PATCH 7/7] Update to version 2.7.0 Update to version 2.7.0 --- ToonTown Rewritten Bot/Properties/AssemblyInfo.cs | 4 ++-- ToonTown Rewritten Bot/ToonTown Rewritten Bot.csproj | 4 ++-- ToonTown Rewritten Bot/Utilities/GlobalSettings.cs | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/ToonTown Rewritten Bot/Properties/AssemblyInfo.cs b/ToonTown Rewritten Bot/Properties/AssemblyInfo.cs index cefdd305..3505de2b 100644 --- a/ToonTown Rewritten Bot/Properties/AssemblyInfo.cs +++ b/ToonTown Rewritten Bot/Properties/AssemblyInfo.cs @@ -32,5 +32,5 @@ // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("2.6.0.0")] -[assembly: AssemblyFileVersion("2.6.0.0")] +[assembly: AssemblyVersion("2.7.0.0")] +[assembly: AssemblyFileVersion("2.7.0.0")] diff --git a/ToonTown Rewritten Bot/ToonTown Rewritten Bot.csproj b/ToonTown Rewritten Bot/ToonTown Rewritten Bot.csproj index 62e23106..116aa4a8 100644 --- a/ToonTown Rewritten Bot/ToonTown Rewritten Bot.csproj +++ b/ToonTown Rewritten Bot/ToonTown Rewritten Bot.csproj @@ -14,7 +14,7 @@ false true 0 - 2.6.0.0 + 2.7.0.0 false true true @@ -138,4 +138,4 @@ - \ No newline at end of file + diff --git a/ToonTown Rewritten Bot/Utilities/GlobalSettings.cs b/ToonTown Rewritten Bot/Utilities/GlobalSettings.cs index 66de36eb..8b4fcd82 100644 --- a/ToonTown Rewritten Bot/Utilities/GlobalSettings.cs +++ b/ToonTown Rewritten Bot/Utilities/GlobalSettings.cs @@ -4,7 +4,7 @@ public class GlobalSettings { public static class ApplicationInfo { - public static string Version { get; } = "2.6.0"; + public static string Version { get; } = "2.7.0"; public static string Author { get; } = "primetime43"; } }