Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -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)
Expand Down
19 changes: 19 additions & 0 deletions ToonTown Rewritten Bot/Models/GameControls.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,25 @@ public static VirtualKeyCode Remap(VirtualKeyCode key)
};
}

/// <summary>
/// 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.
/// </summary>
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)}";
}

/// <summary>
/// Applies the saved bindings from <see cref="UserPreferences"/> to the runtime fields.
/// Unparseable or empty values fall back to the TTR default for that control.
Expand Down
2 changes: 1 addition & 1 deletion ToonTown Rewritten Bot/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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());
}
Expand Down
4 changes: 2 additions & 2 deletions ToonTown Rewritten Bot/Properties/AssemblyInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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")]
104 changes: 51 additions & 53 deletions ToonTown Rewritten Bot/Services/CoordinatesManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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.");
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
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
Expand Down Expand Up @@ -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<List<GolfActionCommand>>(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<GolfActionCommand>();
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)
Expand All @@ -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++)
{
Expand Down Expand Up @@ -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
{
Expand All @@ -154,6 +174,10 @@ public async Task PerformGolfActions(CancellationToken cancellationToken)
ReportStatus("Cancelled");
throw;
}
finally
{
CoreFunctionality.UseBackgroundInput = previousBackgroundMode;
}
}
}
}
}
24 changes: 11 additions & 13 deletions ToonTown Rewritten Bot/Services/GolfService.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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}");
}
}

/// <summary>
Expand Down Expand Up @@ -182,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);
Expand All @@ -196,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
Expand Down Expand Up @@ -377,14 +375,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<GolfActionCommand>();
}

string json = File.ReadAllText(filePath);
return JsonConvert.DeserializeObject<GolfActionCommand[]>(json);
return result.File?.Actions?.ToArray() ?? Array.Empty<GolfActionCommand>();
}

public static string GetCustomGolfActionFilePath(string fileName)
Expand Down
8 changes: 4 additions & 4 deletions ToonTown Rewritten Bot/Templates/UIElementCoordinates.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -315,4 +315,4 @@
"CachedCenter": null,
"LastFoundTime": null
}
}
}
4 changes: 2 additions & 2 deletions ToonTown Rewritten Bot/ToonTown Rewritten Bot.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
<UpdateRequired>false</UpdateRequired>
<MapFileExtensions>true</MapFileExtensions>
<ApplicationRevision>0</ApplicationRevision>
<ApplicationVersion>2.6.0.0</ApplicationVersion>
<ApplicationVersion>2.7.0.0</ApplicationVersion>
<UseApplicationTrust>false</UseApplicationTrust>
<PublishWizardCompleted>true</PublishWizardCompleted>
<BootstrapperEnabled>true</BootstrapperEnabled>
Expand Down Expand Up @@ -138,4 +138,4 @@
<ItemGroup>
<None Include="Templates\CustomFishingTemplates\**\*" CopyToOutputDirectory="PreserveNewest" LinkBase="Templates\CustomFishingTemplates" />
</ItemGroup>
</Project>
</Project>
Loading