Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

new getRound function etc... #80

Open
wants to merge 20 commits into
base: main
Choose a base branch
from
Open
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
18 changes: 5 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,28 +27,20 @@ Feel free to make a pull request if you have any improvements or create a issue
<a name="dependenices"/>

## Requirements & Dependencies
- Tesseract v5.0+
- Python 3.10+

```
keyboard==0.13.5
mouse==0.7.1
mss==6.1.0
numpy==1.22.3
opencv_python==4.5.5.64
pytesseract==0.3.9
mss==9.0.1
numpy==1.25.1
opencv_python==4.8.0.74
Pymem==1.12.0
pywin32==306
```
<a name="installation"/>

## Installation of dependencies:
The script relies on tesseract (tested with v5.3.0) which can be installed using this [this](https://github.com/UB-Mannheim/tesseract/wiki) guide.
(*If by any chance the tesseract installation directory is different from the directory specified in Bot.py you need to manually change that in the script. Otherwise the bot will not work!*)

default path (all users tesseract installation):
```py
pytesseract.pytesseract.tesseract_cmd = r'C:\Program Files\Tesseract-OCR\tesseract.exe'
```

The python library requirments can be installed using `python -m pip install -r requirements.txt` or by running `Install_Requirements.bat`

<a name="running"/>
Expand Down
2 changes: 1 addition & 1 deletion Run.bat
Original file line number Diff line number Diff line change
@@ -1 +1 @@
python src/main.py --gameplan_path "gameplans/Dark_Castle_Hard_Standard" --restart
python src/main.py --gameplan_path "gameplans/Dark_Castle_Hard_Chimps"
2 changes: 1 addition & 1 deletion gameplans/Dark_Castle_Hard_Chimps/notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,6 @@

...and 1 insta-monkey!

# Credits to : Tanttinen#4001
# Credits to : mirko93s

*Note: The strat that is being used has RNG because of the Alchemist, so the results may differ to you from what I got*
9 changes: 5 additions & 4 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
keyboard==0.13.5
mouse==0.7.1
mss==6.1.0
numpy==1.22.3
opencv_python==4.5.5.64
pytesseract==0.3.9
mss==9.0.1
numpy==1.25.1
opencv_python==4.8.0.74
Pymem==1.12.0
pywin32==306
190 changes: 77 additions & 113 deletions src/Bot.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,13 @@
from pathlib import Path

# Local imports
import ocr
import recognition
import simulatedinput
import monitor
import gameplan
from logger import logger as log
import pymem
from winreg import *

# Definently fix this
class Bot():
Expand All @@ -37,8 +38,6 @@ def __init__(self,
# Something to do with how python handles copying objects
self._game_plan_copy = copy.deepcopy(self.game_plan)
####

self.round_area = None

self.DEBUG = debug_mode
self.RESTART = restart_mode
Expand Down Expand Up @@ -308,7 +307,6 @@ def set_static_target(self, tower_position, target_pos):
def remove_tower(self, position):
simulatedinput.click(position)
simulatedinput.send_key("backspace")
simulatedinput.send_key("esc")

def execute_instruction(self, instruction):
"""Handles instructions from the gameplan"""
Expand Down Expand Up @@ -401,33 +399,17 @@ def check_for_collection_crates(self):
# Can this be done better?
if self.checkFor("diamond_case"):
log.debug("easter collection detected")

simulatedinput.click("EASTER_COLLECTION") #DUE TO EASTER EVENT:
time.sleep(1)
simulatedinput.click("LEFT_INSTA") # unlock insta
time.sleep(1)
simulatedinput.click("LEFT_INSTA") # collect insta
time.sleep(1)
simulatedinput.click("RIGHT_INSTA") # unlock r insta
time.sleep(1)
simulatedinput.click("RIGHT_INSTA") # collect r insta
time.sleep(1)
simulatedinput.click("F_LEFT_INSTA")
time.sleep(1)
simulatedinput.click("F_LEFT_INSTA")
time.sleep(1)
simulatedinput.click("MID_INSTA") # unlock insta
time.sleep(1)
simulatedinput.click("MID_INSTA") # collect insta
#c lick collect button
simulatedinput.click("EVENT_COLLECTION", timeout=1.5)
# collect instas
simulatedinput.click("LEFT_INSTA", amount=2, timeout=1.5)
simulatedinput.click("RIGHT_INSTA", amount=2, timeout=1.5)
simulatedinput.click("F_LEFT_INSTA", amount=2, timeout=1.5)
simulatedinput.click("MID_INSTA", amount=2, timeout=1.5)
simulatedinput.click("F_RIGHT_INSTA", amount=2, timeout=1.5)
# click continue and exit to main menu
time.sleep(1)
simulatedinput.click("F_RIGHT_INSTA")
time.sleep(1)
simulatedinput.click("F_RIGHT_INSTA")
time.sleep(1)

time.sleep(1)
simulatedinput.click("EASTER_CONTINUE")

simulatedinput.click("EVENT_CONTINUE",timeout=1)
simulatedinput.send_key("esc")

# select hero if not selected
Expand Down Expand Up @@ -524,94 +506,23 @@ def wait_for_loading(self):
still_loading = self.checkFor("loading_screen")

log.debug("Out of loading screen, continuing..")

def getRoundArea(self):
# Init round area dict with width and height of the round area
round_area = {
"width": 200,
"height": 42
}

# Search for round text, returns (1484,13) on 1080p
area = self.checkFor("round", return_cords=True, center_on_found=False)
log.debug("this should be only printed once, getting round area")
log.debug(f"Round area found at {area}, applying offsetts")

if area:
log.info("Found round area!")

# set round area to the found area + offset
x, y, roundwidth, roundheight = area

# Fiddled offset, do not tuch
# Offset from ROUND text to round number
xOffset = roundwidth + 10
yOffset = int(roundheight * 3) - 40

round_area["top"] = y + yOffset
round_area["left"] = x - xOffset

return round_area

# If it cant find anything
log.warning("Could not find round area, setting default values")
default_round_area_scaled = monitor.scaling([0.7083333333333333, 0.0277777777777778]) # Use default values, (1360,30) on 1080p

# left = x, top = y
round_area["left"] = default_round_area_scaled[0]
round_area["top"] = default_round_area_scaled[1]
return round_area



def getRound(self):
# If round area is not located yet
if self.round_area is None:
self.round_area = self.getRoundArea()

# Setting up screen capture area
# The screen part to capture
screenshot_dimensions = {
'top': self.round_area["top"],
'left': self.round_area["left"],
'width': self.round_area["width"],
'height': self.round_area["height"] + 50
}

# Take Screenshot
with mss.mss() as screenshotter:
screenshot = screenshotter.grab(screenshot_dimensions)
found_text, _ocrImage = ocr.getTextFromImage(screenshot)

if self.DEBUG:
from cv2 import imwrite, IMWRITE_PNG_COMPRESSION
def get_valid_filename(s):
s = str(s).strip().replace(' ', '_')
return re.sub(r'(?u)[^-\w.]', '', s)
imwrite(f"./DEBUG/OCR_DONE_FOUND_{get_valid_filename(found_text)}_{str(time.time())}.png", _ocrImage, [IMWRITE_PNG_COMPRESSION, 0])

# Get only the first number/group so we don't need to replace anything in the string
if re.search(r"(\d+/\d+)", found_text):
found_text = re.search(r"(\d+)", found_text)
return int(found_text.group(0))
else:
# If the found text does not match the regex requirements, Debug and save image
log.warning("Found text '{}' does not match regex requirements".format(found_text))

try:
file_path = Path(__file__).resolve().parent.parent/ "DEBUG"
if not file_path.exists():
Path.mkdir(file_path)
add = 0x03096928
offsets = [0xB8, 0x0, 0xC0, 0x120, 0x20, 0x20]

with open(file_path/f"GETROUND_IMAGE_{str(time.time())}.png", "wb") as output_file:
output_file.write(mss.tools.to_png(screenshot.rgb, screenshot.size))

log.warning("Saved screenshot of what was found")
pm = pymem.Pymem('BloonsTD6.exe')
gameModule = pymem.process.module_from_name(pm.process_handle, "GameAssembly.dll").lpBaseOfDll
address = pm.read_longlong(gameModule+add)

except Exception as e:
log.error(e)
log.warning("Could not save screenshot of what was found")
for offset in offsets:
try:
address = pm.read_longlong(address + offset)
except Exception as e:
print(e)

return None
return address + offsets[-1] -32

def waitForRound(self, round) -> None:
"""Wait for a specific round to start, use this to wait until to execute a gameplan instruction"""
Expand Down Expand Up @@ -660,6 +571,59 @@ def checkFor(self,
return_cords,
center_on_found
)

def findStore(self):
# check if game is installed on Steam
reg = ConnectRegistry(None,HKEY_CURRENT_USER)
game_on_Steam = False
try:
key = OpenKey(reg, r"SOFTWARE\Valve\Steam\Apps\960090")
if key:
try:
isInstalled = QueryValueEx(key, "Installed")[0]
# Other useful values are "Running" and "Updating"
if isInstalled == 1:
game_on_Steam = True
print("Detected Steam installation.")
except WindowsError:
# game is not installed but in the Steam library ???
pass
CloseKey(key)
except WindowsError:
# game is not installed on Steam
pass
CloseKey(reg)

# if game is not installed on Steam check Epic Games
reg = ConnectRegistry(None,HKEY_LOCAL_MACHINE)
game_on_EpicGames = False
try:
key = OpenKey(reg, r"SOFTWARE\WOW6432Node\Epic Games\EpicGamesLauncher")
if key:
try:
epicgamesLauncherPath = QueryValueEx(key, "AppDataPath")[0]
if epicgamesLauncherPath:
path = re.search(r".+Epic\\", epicgamesLauncherPath).group(0) + "UnrealEngineLauncher\LauncherInstalled.dat"
try:
with open(path) as f:
data = json.load(f)
for x in data["InstallationList"]:
if x["NamespaceId"] == "6a8dfa6e441e4f2f9048a98776c6077d":
game_on_EpicGames = True
print("Detected Epic Games installation.")
except:
# game is not installed on Epic Games
pass
except WindowsError:
# couldn't find Epic Games launcher path
pass
CloseKey(key)
except WindowsError:
# Epic Games launcher is not installed
pass
CloseKey(reg)

return(game_on_Steam,game_on_EpicGames)

if __name__ == "__main__":
import time
Expand Down
Binary file added src/assets/admiral_brickell_3.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/assets/captain_churchill_3.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified src/assets/instamonkey.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/assets/pat_fusty_3.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/assets/sauda_3.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/assets/startup.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
48 changes: 44 additions & 4 deletions src/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,11 @@
from Failsafe import FailSafe
import mouse
import simulatedinput
import monitor
from logger import logger as log
import os
import win32gui
import subprocess

def main(arg_parser):

Expand Down Expand Up @@ -60,15 +63,52 @@ def no_gameplan_exception():
print("Gamemode:", bot.settings["GAMEMODE"].replace("_", " ").title())
print("="*25)

print("Waiting for Home screen. Please switch to the Bloons TD 6 window.")
print("Finding game process.")

# Wait for btd6 home screen
# set mouse starting position to the bottom right corner
simulatedinput.move_mouse((monitor.width,monitor.height))

waiting_for_game = True
hwnd = win32gui.FindWindow(None, 'BloonsTD6')

# game is not open
if not hwnd:
print("Game process not found. Finding game installations.")

Steam, EpicGames = bot.findStore()

if Steam:
print("Starting the game through Steam. Please wait...")
subprocess.run("start steam://run/960090", shell=True, check=True)

elif EpicGames:
print("Starting the game through Epic Games. Please wait...")
subprocess.run("start com.epicgames.launcher://apps/6a8dfa6e441e4f2f9048a98776c6077d%3A49c4bf5c6fd24259b87d0bcc96b6009f%3A7786b355a13b47a6b3915335117cd0b2?action=launch", shell=True, check=True)

else:
# We can exit the program here if we don't want cracked users to use the bot
# sys.exit(1)
print("Please start the game manually.")

while waiting_for_game:
time.sleep(0.2)
hwnd = win32gui.FindWindow(None, 'BloonsTD6')
# game process found, focus its window
if hwnd:
waiting_for_game = False
win32gui.SetForegroundWindow(hwnd)
print("Game found. Switching to game window.")

# Wait for btd6 home screen or startup screen
waiting_for_home = False

log.info("Waiting for home screen..")
while waiting_for_home is False:
time.sleep(0.2) # add a short timeout to avoid spamming the cpu
waiting_for_home = bot.checkFor("home_menu")
waiting_for_startup, waiting_for_home = bot.checkFor(["startup","home_menu"], return_raw=True)
if waiting_for_startup:
log.info("Startup screen detected")
simulatedinput.click("STARTUP")
log.info("Home screen detected")

print("Starting bot..\nIf you want to stop the bot, move your mouse to the upper left corner of your screen or press ctrl+c in the terminal")
Expand Down Expand Up @@ -109,7 +149,7 @@ def no_gameplan_exception():

parser.add_argument('-p', '--path', '--gameplan_path', type=str, help='Path to the gameplan directory', required=True)
parser.add_argument('-d', '--debug', action='store_true', help='Enable debug mode')
parser.add_argument('-r', '--restart', action='store_true', help='automatically restarts the game when finished, instead of going to home screen')
parser.add_argument('-r', '--restart', action='store_true', help='automatically restarts the game when finished, instead of going to home screen \(games don\'t count towards event progression if you don\'t go back to home)')
parser.add_argument('-s', '--sandbox', action='store_true', help='Try put gameplan in sandbox mode without waiting for specific rounds')

# Start the bot on a seperate thread
Expand Down
Loading