Skip to content

Instantly share code, notes, and snippets.

@ryunp
Last active October 25, 2022 00:21
Show Gist options
  • Save ryunp/00c4a39cce3beec9b13b59cdea8b5a15 to your computer and use it in GitHub Desktop.
Save ryunp/00c4a39cce3beec9b13b59cdea8b5a15 to your computer and use it in GitHub Desktop.
[WreckFest] Automated driving for high returns custom game spam
; 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