Last active
October 25, 2022 00:21
-
-
Save ryunp/00c4a39cce3beec9b13b59cdea8b5a15 to your computer and use it in GitHub Desktop.
[WreckFest] Automated driving for high returns custom game spam
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
; Author: Ryan Paul | |
; Date: 9/3/22 | |
; Target_Game: Wreckfest | |
; Description: Automated driving for high returns custom game spam | |
#NoEnv | |
SendMode Input | |
SetWorkingDir %A_ScriptDir% | |
SetTitleMatchMode, 1 | |
;;;;;;;;;;;; | |
; App Configuration (safe to modify) | |
;;;;;;;;;;;; | |
; Hotkey to toggle AutoPlay | |
ToggleAutoPlayKey := "\" | |
; Game configuration | |
global GameWinTitle := "Wreckfest" | |
global GameKeybinds := {"MainMenuOpen": "Esc" | |
, "MenuNavDown": "Down" | |
, "MenuClose": "Enter" | |
, "CarAccelerate": "Up" | |
, "CarReset": "r"} | |
; Race reset timeout (ms) in case of stuck/immobile car | |
global RaceResetThresholdMs = 40000 | |
; Frequency (ms) of clock ticks for automated actions | |
global AutomationTickFreqMs := 500 | |
; Duration (ms) range of key press [ .bind(MIN, MAX) ] | |
global KeyDurationFn := Func("RandomInt").Bind(30, 70) | |
; Sleep duration (ms) range after key presses [ .bind(MIN, MAX) ] | |
global KeyDelayFn := Func("RandomInt").Bind(50, 75) | |
;;;;;;;;;;;; | |
; Business Logic (highly coupled; careful) | |
;;;;;;;;;;;; | |
Hotkey, ~%ToggleAutoPlayKey%, ToggleAutoPlay | |
global AutoPlayEnabled := False | |
global CarAccelerating := False | |
global AutomationTickFn := Func("AutomationTick") | |
global RaceSessionEpoc := 0 | |
global SpeedGaugeLastSeen := 0 | |
global RaceTimesLog := [] | |
global RaceStats := {"AvgDnfRaceTime": 0, "AvgTotalRaceTime": 0, "DnfCount": 0, "RestartCount": 0} | |
GetAverageRaceTime(DnfLogs) | |
ToggleAutoPlay() | |
{ | |
if (AutoPlayEnabled := !AutoPlayEnabled) | |
EnableAutoPlay() | |
else | |
DisableAutoPlay() | |
} | |
EnableAutoPlay() | |
{ | |
if (IsGameWindowActive()) | |
SetTimer, %AutomationTickFn%, %AutomationTickFreqMs% | |
} | |
DisableAutoPlay() | |
{ | |
GameDecelerateCar() | |
SetTimer, %AutomationTickFn%, off | |
ToolTip() | |
} | |
AutomationTick() | |
{ | |
if (IsGameWindowActive()) | |
{ | |
; Handle two exclusive game mode states: Race, Unknown | |
; 1) Active race session | |
if (IsGameSpeedGaugeVisible()) | |
{ | |
; Handle three exclusive race session states: New, Active, Expired | |
; 1) New race session | |
if (RaceSessionEpoc == 0) | |
{ | |
RaceSessionEpoc := A_TickCount | |
GameAccelerateCar() | |
} | |
; 2) Active race session | |
else if (RaceSessionDuration() < RaceResetThresholdMs) | |
{ | |
SendKeystroke(GameKeybinds.CarReset, KeyDurationFn.Call()) | |
} | |
; 3) Expired race session | |
else | |
{ | |
; Restart race session in case car is stuck/immobile | |
RaceSessionEpoc := 0 | |
raceLogEntry := {"Duration": RaceResetThresholdMs, "WasRestarted": True} | |
AddRaceLogEntry(raceLogEntry) | |
GameRestartRaceSession() | |
} | |
; Record last known timestamp of race session | |
SpeedGaugeLastSeen := A_TickCount | |
DisplayRaceStats() | |
} | |
; 2) Unknown: Race screen obstructed | |
else | |
{ | |
; Handle two exclusive states: InMenu, Overlay | |
; 1) In main menu | |
if (RaceSessionEpoc == 0) | |
{ | |
SendKeystroke(GameKeybinds.MenuClose, KeyDurationFn.Call(), KeyDelayFn.Call()) | |
} | |
; 2) Either Overlay or exiting race session; record state change for | |
; dependent business logic, no state specific actions required | |
else | |
{ | |
; Race session Overlay only takes 1 - 1.5 seconds to fade, | |
; anything longer means menu open or race session completed | |
speedGaugeHiddenDuration := A_TickCount - SpeedGaugeLastSeen | |
if (speedGaugeHiddenDuration > 2000) | |
{ | |
; Assume race has completed, record stats | |
; Note: User could have manually opened game menu | |
if (RaceSessionEpoc > 0) | |
{ | |
raceLogEntry := {"Duration": A_TickCount - RaceSessionEpoc, "WasRestarted": False} | |
AddRaceLogEntry(raceLogEntry) | |
} | |
RaceSessionEpoc := 0 | |
GameDecelerateCar() | |
} | |
DisplayRaceStats() | |
} | |
} | |
} | |
else | |
{ | |
GameDecelerateCar() | |
RaceSessionEpoc := 0 | |
ToolTip() | |
} | |
} | |
AddRaceLogEntry(entry) | |
{ | |
RaceTimesLog.Push(entry) | |
CalculateRaceStats() | |
} | |
CalculateRaceStats() | |
{ | |
DnfLogs := ArrayFilter(RaceTimesLog, Func("RaceEntryWasNotRestarted")) | |
RaceStats.DnfCount := DnfLogs.Length() | |
RaceStats.RestartCount := RaceTimesLog.Length() - DnfLogs.Length() | |
RaceStats.AvgDnfRaceTime := GetAverageRaceTime(DnfLogs) | |
RaceStats.AvgTotalRaceTime := GetAverageRaceTime(RaceTimesLog) | |
} | |
RaceEntryWasNotRestarted(raceLog) | |
{ | |
return !raceLog.WasRestarted | |
} | |
DisplayRaceStats() | |
{ | |
msRemaining := RaceResetThresholdMs - RaceSessionDuration() | |
textContent := [] | |
textContent.Push(Format("DNFs: {}", RaceStats.DnfCount)) | |
textContent.Push(Format("Restarts: {}", RaceStats.RestartCount)) | |
textContent.Push(Format("Avg DNF Time: {:0.2f}s", RaceStats.AvgDnfRaceTime)) | |
textContent.Push(Format("Total Avg Time: {:0.2f}s", RaceStats.AvgTotalRaceTime)) | |
textContent.Push(Format("Restart Timer: {}/{}s", Round(msRemaining / 1000), Round(RaceResetThresholdMs / 1000))) | |
textContent.Push(Format("Gas Pedal: {}", CarAccelerating ? "True" : "False")) | |
ToolTip(join("`n", textContent*), 0, 0) | |
} | |
GetAverageRaceTime(raceLogs) | |
{ | |
if (raceLogs.Length() == 0) | |
return 0 | |
sum = 0 | |
for idx, logEntry in raceLogs | |
sum += logEntry.Duration / 1000 | |
return sum / raceLogs.Length() | |
} | |
GameAccelerateCar() | |
{ | |
if (GetKeyState(GameKeybinds.CarAccelerate) == 0) | |
{ | |
Send % "{" GameKeybinds.CarAccelerate " DOWN}" | |
CarAccelerating := true | |
} | |
} | |
GameDecelerateCar() | |
{ | |
if (GetKeyState(GameKeybinds.CarAccelerate) == 1) | |
{ | |
Send % "{" GameKeybinds.CarAccelerate " UP}" | |
CarAccelerating := false | |
} | |
} | |
GameRestartRaceSession() | |
{ | |
SendKeystroke(GameKeybinds.MainMenuOpen, KeyDurationFn.Call(), KeyDelayFn.Call()) | |
SendKeystroke(GameKeybinds.MenuNavDown, KeyDurationFn.Call(), KeyDelayFn.Call()) | |
SendKeystroke(GameKeybinds.MenuClose, KeyDurationFn.Call(), KeyDelayFn.Call()) | |
SendKeystroke(GameKeybinds.MenuClose, KeyDurationFn.Call()) | |
} | |
IsGameWindowActive() | |
{ | |
return WinActive(GameWinTitle) | |
} | |
IsGameSpeedGaugeVisible() | |
{ | |
screenCenterY := A_ScreenHeight / 2 | |
; X Axis | |
gaugeXLeft := Round(A_ScreenWidth * 0.825) | |
gaugeXCenter := Round(A_ScreenWidth * 0.882) | |
gaugeXRight := Round(A_ScreenWidth * 0.939) | |
; Y Axis | |
screenCenterYToGaugeTop := A_ScreenWidth * 0.145 | |
screenCenterYToGaugeBottom := A_ScreenWidth * 0.213 | |
gaugeYTop := Round(screenCenterY + screenCenterYToGaugeTop) | |
gaugeYBottom := Round(screenCenterY + screenCenterYToGaugeBottom) | |
; Known pixel colors (using aspect ratios for positioning, should scale) | |
knownPixelList := [{"ExpectedColor": RgbToHex(255,255,255), "x": gaugeXLeft, "y": gaugeYBottom} | |
, {"ExpectedColor": RgbToHex(255,255,255), "x": gaugeXCenter, "y": gaugeYTop} | |
, {"ExpectedColor": RgbToHex(208,075,039), "x": gaugeXRight, "y": gaugeYBottom}] | |
; Test against screen | |
matchedPixels := ArrayFilter(knownPixelList, Func("TestScreenPixelColor")) | |
; Success requires ratio of ~2/3 or higher | |
return matchedPixels.Length() >= Ceil(knownPixelList.Length() * 0.66) | |
} | |
TestScreenPixelColor(pInfo) | |
{ | |
return GetPixelColor(pInfo.x, pInfo.y) == pInfo.ExpectedColor | |
} | |
GetPixelColor(x, y) | |
{ | |
PixelGetColor, hex, %x%, %y%, RGB | |
return hex | |
} | |
RaceSessionDuration() | |
{ | |
if (RaceSessionEpoc == 0) | |
return 0 | |
return A_TickCount - RaceSessionEpoc | |
} | |
;;;;;;;;;;;; | |
; General Utility | |
;;;;;;;;;;;; | |
ToolTip(text := "", x := "", y := "", whichtooltip := 1, timeout := "") | |
{ | |
ToolTip, % Text, %x%, %y%, %whichtooltip% | |
If (timeout) { | |
clearToolTipFn := Func("ToolTip").Bind(,,, whichtooltip) | |
SetTimer, %clearToolTipFn%, % -timeout | |
} | |
} | |
RandomInt(min, max) | |
{ | |
Random, out, %min%, %max% | |
return out | |
} | |
SendKeystroke(key, duration := -2, delay := -2) | |
{ | |
if (duration == -2) | |
duration := A_KeyDuration | |
if (delay == -2) | |
delay := A_KeyDelay | |
Send % "{" key " DOWN}" | |
Sleep, %duration% | |
Send % "{" key " UP}" | |
Sleep, %delay% | |
} | |
RgbToHex(r, g, b) | |
{ | |
return Format("0x{:X}{:X}{:X}", r, g, b) | |
} | |
Join(sep, head, args*) | |
{ | |
for _, arg in args | |
head .= sep . arg | |
return head | |
} | |
ArrayFilter(array, fnObj) | |
{ | |
results := [] | |
for idx, item in array | |
if (fnObj.Call(item) == True) | |
results.Push(item) | |
return results | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment