├── .gitignore ├── LICENSE ├── README.md ├── Tesseract-OCR.zip ├── __main__.py ├── awthemes-10.4.0.zip ├── build.cmd ├── fcmacros.py ├── fcmacros.spec ├── images ├── fc_icon.ico ├── icon.png └── screenshot.png ├── keymaps.py ├── locations.py ├── make_dist.cmd ├── ocr.py └── setup.cmd /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | 131 | # The Tesseract unzipped install 132 | /Tesseract-OCR/ 133 | /awthemes-10.4.0/ 134 | 135 | # Testing/Debugging files 136 | /debugimages/ 137 | /test/ 138 | /test_route.csv 139 | 140 | # Pycharm files 141 | .idea/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Seth Osher 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # fc-macros 2 | Elite Dangerous Fleet Carrier Macros 3 | There are 3 primary macros: 4 | * Ctrl+F11: Refill your ship with Tritium and donate it to the carrier. 5 | * Ctrl+F9: Schedule the next jump on the route 6 | * Alt+F9: Auto jump the remaining route 7 | * Ctrl+F10: Stop auto jumping 8 | * Ctrl+F5: Empty your ship's cargo to the carrier. 9 | 10 | Route files are in CSV format from the spansh router. Only the first column matres and it must be titled 11 | "System Name". https://www.spansh.co.uk/fleet-carrier 12 | 13 | ![screenshot](images/screenshot.png) 14 | 15 | ------ 16 | 17 | Currently, this plugin uses default keybindings and mouse for input. The mouse is used to navigate the galmap. 18 | Because it uses the keyboard and mouse, it is unlikely to work with a joystick or HOTAS enabled. 19 | 20 | Note: See _keymaps.py_ to edit the keyboard mappings. (Only in source code version.) 21 | 22 | The latest version no longer uses image matching, instead of uses known location on the screen 23 | and text recognition. It may now work with custom HUD colors. It is also hopefully insensitive 24 | to HUD Brightness options. It is also tested on both windowed and full screen modes, but only at 1008p full screen. 25 | 26 | The tool is written attempting for safety in the UI. 27 | If the macro cannot find the expected images or text it will abort for safety. 28 | But I cannot guarantee it won't misbehave in some odd way. It is just sending keys and mouse 29 | actions so if E:D loses focus while running it will start sending them to whatever application does have the focus. 30 | 31 | If you use the refuel option, make sure your ship has enough room for a full jumps worth of Tritium, 32 | as much as 150 Tons if your carrier is fully loaded. This option will load the carrier and the ship before 33 | setting up a jump to minimize the carrier mass. 34 | 35 | 36 | --------------- 37 | 38 | # Installation 39 | 40 | Option 1: 41 | * Download a release zip file 42 | * Extract 43 | * Run fcmacros.exe 44 | 45 | Option 2: 46 | * Download the source 47 | * pip install keyboard pyautogui pillow opencv-python usersettings pywin32 pytesseract 48 | * Or run "setup.cmd" 49 | * python main.py 50 | 51 | 52 | -------------- 53 | # 3rd Party Dependencies 54 | 55 | These are all included in the installation or are part of the pip install in setup.cmd. 56 | 57 | The latest edition uses teseract for the text recognition. 58 | A portable copy is provided in a zip file and is auto extracted at first runtime. 59 | More about the teseract project can be found [here](https://github.com/tesseract-ocr/tesseract/blob/main/doc/tesseract.1.asc). 60 | The provided copy is the [University of Manheim 64bit build.](https://github.com/UB-Mannheim/tesseract/wiki) 61 | 62 | There is heavy use of [OpenCV](https://opencv.org/). 63 | 64 | The latest version uses much less of [pyautogui](https://pyautogui.readthedocs.io/en/latest/) 65 | But much homage is owed to this tool. 66 | 67 | The UI theme is [awthemes awdark](https://sourceforge.net/projects/tcl-awthemes/) by Brad Lanam. (Thank you.) 68 | 69 | -------------- 70 | 71 | # Troubleshooting 72 | 73 | Summary of actions are written to "fcmacros.log" in the working directory. 74 | If you are experiencing issues, enable debug logging. The log file will contain 75 | details of every step the macro is taking - what keypresses it is making, 76 | the images or text it is searching for on the screen etc. Hopefully there will be enough 77 | information to troubleshoot. For now it saves many debug_*.png images in the debugimages/ folder. 78 | These are to also help troubleshoot problems. You might see something like debug_TRITIUM_threshold.png. 79 | If it doesn't contain the word "TRITIUM", that's a hint that it's not finding the text properly. 80 | 81 | Likely causes: 82 | * The image matching is screen resolution specific. If you're not running E:D at 1080p the image locations are probably wrong. 83 | It should be possible to make them scale, but some experimentation is needed with different resolutions, 84 | especially those that are at different aspect ratios. 85 | * Custom HUD colours may be a problem, though hopefully not in the latest version. If for some reason 86 | your colour palette is very bright or very dim, it might make it difficult to find what button is selected as the 87 | image is forced to a very high contrast. 88 | * As with the above, you might try using the default UI brightness. 89 | * Non-standard keybindings can also be a problem. 90 | * Try running in windowed mode if you are in full-screen mode. Alt-Enter to switch modes. This also should be 91 | working properly on the latest version in both modes, but its worth a try. 92 | * Look at some of the suggestions in the thread on issue report number 2: https://github.com/pilotso11/fc-macros/issues/2 93 | though these are less relevent with the latest releases. 94 | 95 | If you have the source code version you can edit the keybindings in "keymaps.py". 96 | 97 | # Work in progress 98 | 99 | The latest version has abandoned the pyautogui "find this image" technique and instead 100 | uses known offsets and text recognition. This should be less trouble free. 101 | 102 | More work is needed to support non-1080p resolutions. 103 | 104 | ----------------- 105 | 106 | Copyright (c) 2022 Seth Osher. All Rights Reseved. 107 | Released under the MIT license. 108 | 109 | ------------------ 110 | Tags: Elite Dangerous Fleet Carrier autopilot auto jumper macros 111 | -------------------------------------------------------------------------------- /Tesseract-OCR.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pilotso11/fc-macros/ca73379e8de67b1092187f7c32f460929945e64f/Tesseract-OCR.zip -------------------------------------------------------------------------------- /__main__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2022 Seth Osher 2 | # Main launch for fcmacros running via pyinstaller 3 | from fcmacros import run_main 4 | 5 | 6 | def main(): 7 | run_main() 8 | 9 | 10 | if __name__ == '__main__': 11 | main() 12 | -------------------------------------------------------------------------------- /awthemes-10.4.0.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pilotso11/fc-macros/ca73379e8de67b1092187f7c32f460929945e64f/awthemes-10.4.0.zip -------------------------------------------------------------------------------- /build.cmd: -------------------------------------------------------------------------------- 1 | pyinstaller.exe fcmacros.spec 2 | -------------------------------------------------------------------------------- /fcmacros.py: -------------------------------------------------------------------------------- 1 | # fcmacros.py 2 | # Elite Dangerous Fleet Carrier Macros - autopilot/auto jumper 3 | # Copyright (c) 2022 Seth Osher 4 | # 5 | import tkinter 6 | from tkinter import * 7 | from tkinter import ttk 8 | from tkinter import filedialog as fd 9 | import keyboard as kb 10 | import pyautogui 11 | from time import sleep 12 | import glob 13 | import getpass 14 | import os 15 | import usersettings 16 | import webbrowser 17 | import ocr 18 | from keymaps import * 19 | from locations import * 20 | import logging 21 | import logging.handlers 22 | import sys 23 | import win32gui 24 | import zipfile 25 | 26 | VERSION = "0.2.3" 27 | BUNDLED = False 28 | LOGFILE = "fcmacros.log" 29 | 30 | # Global state 31 | current_system = '' 32 | jumping = False 33 | cool_down = False 34 | auto_jump = False 35 | jump_one = False 36 | ship = '' 37 | route = [] 38 | next_waypoint = '' 39 | route_file = "" 40 | is_odyssey = False 41 | screen_shape = [1080, 1920, 3] 42 | fullscreen = False 43 | carrier_services_loc = None 44 | 45 | # initialize settings 46 | settings = usersettings.Settings('com.ed.fcmacros') 47 | settings.add_setting("route_file", str, default="") 48 | settings.add_setting("refuel", int, default=1) 49 | settings.add_setting("debug", int, default=0) 50 | settings.add_setting("grayscale", int, default=0) 51 | settings.add_setting("confidence", int, default=75) 52 | 53 | logger = logging.getLogger() 54 | 55 | 56 | def log_setup(): 57 | global BUNDLED 58 | # Logging setup 59 | logger.setLevel(logging.DEBUG) 60 | handler = logging.handlers.RotatingFileHandler( 61 | LOGFILE, maxBytes=(1048576*5), backupCount=10 62 | ) 63 | formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s', datefmt='%Y-%m-%d %H:%M:%S') 64 | handler.setFormatter(formatter) 65 | logger.addHandler(handler) 66 | 67 | if getattr(sys, 'frozen', False) and hasattr(sys, '_MEIPASS'): 68 | BUNDLED = True 69 | else: 70 | console_handler = logging.StreamHandler() 71 | console_handler.setFormatter(formatter) 72 | logger.addHandler(console_handler) 73 | 74 | logging.info(f"Starting fcmacros.py version: {VERSION}") 75 | if BUNDLED: logging.info('Running in a PyInstaller bundle') 76 | else: logging.info('Running as a console application') 77 | 78 | 79 | def check_for_themes(): 80 | logging.debug("Check for awthemes") 81 | r = glob.glob("./awthemes-10.4.0/awthemes.tcl") 82 | if len(r) == 0: 83 | logging.info("Unzipping awthemes") 84 | with zipfile.ZipFile("awthemes-10.4.0.zip", "r") as zipf: 85 | zipf.extractall(".") 86 | 87 | 88 | def set_debug_level(): 89 | if settings.debug: 90 | logger.setLevel(logging.DEBUG) 91 | else: 92 | logger.setLevel(logging.INFO) 93 | 94 | 95 | def check_settings(): 96 | global route_file 97 | if route_file == '' and settings.route_file > '': 98 | route_file = settings.route_file 99 | route_label.config(text=route_file) 100 | load_route(route_file) 101 | elif route_file != settings.route_file: 102 | settings.route_file = route_file 103 | 104 | settings.refuel = do_refuel.get() 105 | settings.debug = DEBUG.get() 106 | set_debug_level() 107 | settings.grayscale = GRAYSCALE.get() 108 | try: 109 | settings.confidence = int(CONFIDENCE.get()) 110 | except ValueError: 111 | pass 112 | 113 | settings.save_settings() 114 | 115 | root.after(1000, check_settings) 116 | 117 | 118 | # Find most recent E:D log file 119 | def get_latest_log_file(): 120 | list_of_files = glob.glob( 121 | f"C:\\Users\\{getpass.getuser()}\\Saved Games\\Frontier Developments\\Elite Dangerous\\Journal.*") 122 | return max(list_of_files, key=os.path.getmtime) 123 | 124 | 125 | def get_current_focus(): 126 | global screen_shape, fullscreen 127 | logging.debug("Checking window focus before running macro") 128 | win = win32gui.GetForegroundWindow() 129 | title = win32gui.GetWindowText(win) 130 | if title != "Elite - Dangerous (CLIENT)": 131 | logging.warning(f"Current window is '{title}' and not 'Elite - Dangerous' abort macro") 132 | set_status("Elite - Dangerous does not have the focus, aborting") 133 | return False 134 | logging.debug("Elite - Dangerous has the focus, macro proceeding") 135 | screen_shape = ocr.get_screen_width() 136 | fullscreen = ocr.is_fullscreen() 137 | if fullscreen: adjust_for_fullscreen() 138 | 139 | return True 140 | 141 | 142 | # Load and parse the route.csv 143 | def load_route(route_file_name): 144 | global route, route_file 145 | route = [] # Clear prior route 146 | if len(route_file_name) < 4: return 147 | 148 | with open(route_file_name, 'r') as f: 149 | cnt = 0 150 | while True: 151 | try: 152 | line = f.readline() 153 | parts = line.strip().split(",") 154 | if len(line) == 0: 155 | update_route_position() 156 | set_status("Loaded route with {} systems".format(len(route))) 157 | return # Done with file 158 | if cnt == 0: 159 | if parts[0].strip('"') != 'System Name': 160 | set_status("Invalid route file, first column must be System Name") 161 | route_file = "" 162 | return 163 | else: 164 | system = parts[0].strip('"') 165 | route.append(system) 166 | cnt += 1 167 | except (RuntimeError, UnicodeDecodeError) as err: 168 | logging.warning(f"Exception loading route file {route_file_name}: {err}") 169 | set_status("Invalid route file, first column must be System Name") 170 | route_file = "" 171 | 172 | 173 | # Press down key for down_time and wait after for delay 174 | def press(key, delay=0.5, down_time=0.2): 175 | if key == '\b': 176 | out = "Press: backspace" 177 | elif key == '\t': 178 | out = "Press: tab" 179 | elif key == '\r': 180 | out = "Press: return" 181 | elif key == '\n': 182 | out = "Press: newline" 183 | elif key == ' ': 184 | out = "Press: space" 185 | else: 186 | out = "Press: " + key 187 | logging.debug(out) 188 | kb.press(key) 189 | sleep(down_time) 190 | kb.release(key) 191 | sleep(delay) 192 | 193 | 194 | # Hold image list 195 | images_dict = {} 196 | 197 | 198 | def get_matching_images(image): 199 | if image in images_dict: 200 | image_set = images_dict[image] 201 | else: 202 | image_set = glob.glob("images/" + image + "-*.png") 203 | logging.info(f"Found { len(image_set)} images for {image}: {image_set}") 204 | images_dict[image] = image_set 205 | 206 | return image_set 207 | 208 | 209 | # look for the image on screen , if found, return True 210 | # if not, press the key specified, true this up to max_trues times, 211 | # if max_tries is exceeded return False 212 | # confidence is the confidence interval on the match 213 | def deprecated_press_and_find(key, image, max_tries=10, do_log=True): 214 | image_set = get_matching_images(image) 215 | grayscale = settings.grayscale == 1 216 | confidence = settings.confidence / 100.0 217 | return deprecated_press_and_find_set(key, image_set, max_tries, confidence, grayscale, do_log) 218 | 219 | 220 | # look for words on screen , if found, return True 221 | # if not, press the key specified, true this up to max_trues times, 222 | # if max_tries is exceeded return False 223 | def press_and_find_text(key, words, region=None, max_tries=10): 224 | while max_tries >= 0: 225 | max_tries -= 1 226 | found, where = ocr.is_text_on_screen(words, region=region) 227 | if found: 228 | return True 229 | press(key) 230 | return False 231 | 232 | 233 | # As above, but for a list of a list of words - used to validate the right HUD is selected 234 | def press_and_find_text_list(key, words_list, region=None, max_tries=10): 235 | while max_tries >= 0: 236 | max_tries -= 1 237 | found, where = ocr.is_text_on_screen_list(words_list, region=region) 238 | if found: 239 | return True 240 | press(key) 241 | return False 242 | 243 | 244 | # Press a key until the indicated region is highlighted 245 | def press_until_selected_region(key, region, debug_text, max_count=10): 246 | logging.info(f"Selecting region {debug_text} at {region}") 247 | while max_count >= 0: 248 | max_count -= 1 249 | if ocr.get_average_color_bw(region, debug_text=debug_text) > ENABLED_THRESHOLD: return True 250 | press(key) 251 | return False 252 | 253 | 254 | # Search for any of a set of text on the screen 255 | # If not found, press key 256 | # Repeat 257 | def press_until_text_found(key, word_list, max_count=10, pause=0.5): 258 | while max_count > 0: 259 | b, res = ocr.is_text_on_screen_list(word_list) 260 | if b: 261 | logging.debug(f"Found Words {res}") 262 | return True 263 | press(key) 264 | max_count -= 1 265 | sleep(pause) 266 | return False 267 | 268 | 269 | # Return to the center HUD 270 | def return_hud_to_start(): 271 | if not press_until_text_found(ED_BACK, [["LAUNCH"], ["AUTO"], ["DISEMBARK"], ["CARRIER", "SERVICES"]], max_count=10): 272 | set_status("Unable to find main HUD") 273 | return False 274 | if ocr.get_average_color_bw(get_carrier_services_location(), debug_text="CARRIER SERVICES") > ENABLED_THRESHOLD: 275 | return True 276 | press(ED_UI_UP) 277 | press(ED_UI_UP) 278 | press(ED_UI_UP) 279 | return press_until_selected_region(ED_UI_DOWN, get_carrier_services_location(), debug_text="CARRIER SERVICES", max_count=5) 280 | 281 | 282 | # Locate an image(set) on the screen, return its found position 283 | def deprecated_locate_on_screen(image, do_log=True): 284 | image_set = get_matching_images(image) 285 | 286 | grayscale = settings.grayscale == 1 287 | confidence = settings.confidence / 100.0 288 | return locate_on_screen_set(image_set, confidence, grayscale, do_log) 289 | 290 | 291 | def locate_on_screen_set(images=None, confidence=0.75, grayscale=False, do_log=True): 292 | if images is None: 293 | images = [] 294 | for i in images: 295 | pos = pyautogui.locateOnScreen(i, confidence=confidence, grayscale=grayscale) 296 | if pos is not None: 297 | if do_log: logging.debug(f"Found {i}") 298 | return pos 299 | return None 300 | 301 | 302 | def deprecated_press_and_find_set(key=ED_UI_LEFT, images=None, max_tries=10, confidence=0.75, grayscale=False, do_log=True): 303 | if images is None: 304 | images = [] 305 | cnt = 0 306 | if do_log: logging.debug(f"Looking for one of {images}") 307 | while True: 308 | for i in images: 309 | if pyautogui.locateOnScreen(i, confidence=confidence, grayscale=grayscale) is not None: 310 | if do_log: logging.debug(f"Found {i}") 311 | return True 312 | press(key) 313 | cnt += 1 314 | if cnt > max_tries: 315 | logging.debug(f"No image found after {max_tries} attempts") 316 | set_status('Macro failed') 317 | return False 318 | 319 | 320 | def mouse_move_to_region(region, x_offset=0, y_offset=0): 321 | x = region[0] + region[2]//2 + x_offset 322 | y = region[1] + region[3]//2 + y_offset 323 | pyautogui.moveTo(x, y) 324 | 325 | 326 | def mouse_click_at_region(region, pause=0.25, click_duration=0.25, x_offset=0, y_offset=0): 327 | x = region[0] + region[2]//2 + x_offset 328 | y = region[1] + region[3]//2 + y_offset 329 | mouse_click_at(x, y, pause=pause, click_duration=click_duration) 330 | 331 | 332 | def mouse_click_at(x, y, pause=0.25, click_duration=0.25): 333 | pyautogui.moveTo(x, y) 334 | sleep(pause) 335 | pyautogui.mouseDown() 336 | sleep(click_duration) 337 | pyautogui.mouseUp() 338 | 339 | 340 | def get_carrier_services_location(): 341 | global carrier_services_loc 342 | if carrier_services_loc is not None: 343 | return carrier_services_loc 344 | 345 | if ship in carrier_services_locations.keys(): 346 | loc = carrier_services_locations[ship] 347 | carrier_services_loc = (loc[0]-10, loc[1]-3, loc[2]+20, loc[3]+6) 348 | logging.info(f'Found know carrier services location for {ship} at {carrier_services_loc}') 349 | return carrier_services_loc 350 | 351 | carrier_services_loc = ocr.get_carrier_services_loc() 352 | if carrier_services_loc is None: 353 | set_status("Unable to find CARRIER SERVICES") 354 | return None 355 | 356 | new_loc = (carrier_services_loc[0], carrier_services_loc[1], 357 | carrier_services_loc[2]-carrier_services_loc[0], 358 | carrier_services_loc[3] - carrier_services_loc[1]) 359 | carrier_services_loc = new_loc 360 | logging.info(f"Found CARRIER SERVICES at {new_loc}") 361 | 362 | return carrier_services_loc 363 | 364 | 365 | # Move back to the carrier main screen after a jump 366 | def reset_to_main_hud_after_jump(): 367 | sleep(5) 368 | logging.debug("Select right HUD (workaround)") 369 | if not press_and_find_text_list(ED_RIGHT_WINDOW, [["MODULES"], ["STATUS"], ["FIRE", "GROUPS"]]): 370 | set_status("Unable to verify right HUD - cant find MODULES, STATUS or FIRE GROUPS") 371 | return False 372 | sleep(1) 373 | logging.debug("Back to center HUD") 374 | if not return_hud_to_start(): return False 375 | if ocr.get_average_color_bw(get_carrier_services_location(), debug_text="CARRIER SERVICES") <= ENABLED_THRESHOLD: 376 | set_status("Unable to find selected CARRIER SERVICES") 377 | return False 378 | 379 | 380 | # Callback to schedule a jump 381 | def schedule_jump(): 382 | find_system_and_jump() 383 | 384 | 385 | # Macro to schedule a jump 386 | def find_system_and_jump(*args): 387 | global jumping, jump_one 388 | 389 | # Check E:D still has the focus 390 | if not get_current_focus(): return False 391 | 392 | # Get next waypoint and check I'm still enabled for auto jump 393 | if len(next_waypoint) < 3: 394 | set_status("No next waypoint") 395 | return False 396 | 397 | if not (auto_jump or jump_one): 398 | logging.warning("Jump requested but no jump is enabled") 399 | set_status("Jump is disabled, aborting") 400 | return False 401 | 402 | if do_refuel.get() == 1: 403 | new_args = [donate_tritium, load_tritium, find_system_and_jump_1] 404 | for a in args: new_args.append(a) 405 | load_tritium(*new_args) 406 | else: 407 | root.after(100, find_system_and_jump_1, *args) 408 | 409 | 410 | def find_system_and_jump_1(*args): 411 | global jumping, jump_one 412 | if jump_one: jump_one = False 413 | set_status('Finding next waypoint') 414 | 415 | # Back to main menu 416 | if not return_hud_to_start(): return False 417 | press(ED_UI_SELECT) 418 | 419 | if not press_until_selected_region(ED_UI_DOWN, TRITIUM_DEPOT_POS, ED_UI_UP): return False 420 | if not press_until_selected_region(ED_UI_RIGHT, CARRIER_MANAGEMENT_POS, ED_UI_DOWN): return False 421 | press(ED_UI_SELECT) 422 | sleep(5) # Wait for galmap, sometimes slow 423 | if not press_until_selected_region(ED_UI_DOWN, NAVIGATION_ICON, debug_text="NAVIGATION ICON", max_count=5): 424 | set_status("Unable to select NAVIGATION ICON") 425 | return False 426 | press(ED_UI_SELECT) 427 | sleep(0.5) 428 | if not press_until_selected_region(ED_UI_DOWN, GALMAP_IMAGE, debug_text="OPEN GALMAP", max_count=5): 429 | set_status("Unable to SELECT OPEN GALMAP") 430 | return False 431 | press(ED_UI_SELECT) 432 | root.after(3000, find_system_and_jump_2, *args) 433 | 434 | 435 | def find_system_and_jump_2(*args): 436 | global jumping, jump_one 437 | mouse_click_at_region(GALMAP_SEARCH) 438 | kb.write(next_waypoint) 439 | sleep(1) 440 | mouse_click_at_region(GALMAP_SEARCH, y_offset=GALMAP_SEARCH[3]) 441 | sleep(2) 442 | mouse_move_to_region(SET_CARRIER_DESTINATION_POS) 443 | sleep(0.25) 444 | # Check if set carrier destination is selectable 445 | if ocr.get_average_color_bw(SET_CARRIER_DESTINATION_POS) <= ENABLED_THRESHOLD: 446 | set_status("Destination is invalid") 447 | return 448 | mouse_click_at_region(SET_CARRIER_DESTINATION_POS) 449 | set_status("Jump set for {}".format(next_waypoint)) 450 | jumping = True 451 | 452 | if not return_hud_to_start(): return False 453 | press(ED_UI_SELECT) 454 | call_next_args(100, *args) 455 | 456 | 457 | # Callback to refuel the ship, the carrier and then the ship 458 | # to optimise carrier mass and save up to 1 ton of fuel per jump 459 | def refuel(): 460 | if not get_current_focus(): return 461 | set_status('Refueling') 462 | if not load_tritium(donate_tritium): return 463 | 464 | 465 | # Refill ship with tritium 466 | def load_tritium(*args): 467 | # Check E:D still has the focus 468 | if not get_current_focus(): return False 469 | 470 | set_status('Filling ship with tritium') 471 | if not return_hud_to_start(): return False 472 | press(ED_RIGHT_WINDOW, 0.5) 473 | root.after(100, load_tritium_1, *args) 474 | 475 | 476 | def load_tritium_1(*args): 477 | if not press_until_selected_region(ED_MENU_RIGHT, INVENTORY_POS, debug_text="INVENTORY", max_count=15): 478 | set_status("Unable to select INVENTORY") 479 | return False 480 | if not press_until_selected_region(ED_UI_RIGHT, TRANSFER_POS, debug_text="TRANSFER", max_count=5): 481 | press(ED_UI_UP) 482 | sleep(0.5) 483 | press(ED_UI_UP) 484 | if not press_until_selected_region(ED_UI_RIGHT, TRANSFER_POS, debug_text="TRANSFER"): 485 | set_status("Unable to select TRANSFER") 486 | return False 487 | press(ED_UI_SELECT) 488 | root.after(100, load_tritium_2, *args) 489 | 490 | 491 | # Find the select words on the screen (bounding box) 492 | # Press key until the words are selectable 493 | # If the Words are not found, press the alt_key_if_words_not_found 494 | # Retry both operations up to max_Cnt 495 | def press_until_selected_text(key, words, alt_key_if_words_not_found, max_cnt=10): 496 | cnt = 0 497 | while True: 498 | cnt += 1 499 | if cnt >= max_cnt: 500 | logging.warning(f"Unable to find words for selection: {words}") 501 | return False 502 | b, loc = ocr.is_text_on_screen(words, debug=True, show=False, save=words[0]) 503 | if not b: 504 | press(alt_key_if_words_not_found) 505 | else: 506 | break 507 | region = [loc[0]-20, loc[1], loc[2]-loc[0]+40, loc[3]-loc[1]] 508 | cnt = 0 509 | while cnt < max_cnt: 510 | if ocr.get_average_color_bw(region) > ENABLED_THRESHOLD: 511 | break 512 | press(key) 513 | cnt += 1 514 | if cnt >= max_cnt: 515 | logging.debug(f"Unable to find selected text: {words} at: {region}") 516 | return False 517 | return True 518 | 519 | 520 | def load_tritium_2(*args): 521 | if not press_until_selected_text(ED_UI_UP, ["TRITIUM"], ED_UI_DOWN): return False 522 | 523 | # hold down the ED_UI_LEFT key until max capacity is reached 524 | kb.press(ED_UI_LEFT) 525 | logging.debug(f"Press and hold {ED_UI_LEFT}") 526 | while True: 527 | res, loc = ocr.is_text_on_screen(["MAX", "CAPACITY"], region=MAX_CAPACITY_POS, debug=True, save='max_capacity', show=False) 528 | if res: break 529 | sleep(0.3) 530 | logging.debug(f"max_capacity found") 531 | logging.debug(f"Release {ED_UI_LEFT}") 532 | kb.release(ED_UI_LEFT) 533 | root.after(100, load_tritium_3, *args) 534 | 535 | 536 | def load_tritium_3(*args): 537 | if not press_until_selected_region(ED_UI_DOWN, TRANSFER_CANCEL_POS, ED_UI_UP): return False 538 | press(ED_UI_RIGHT) 539 | press(ED_UI_SELECT) 540 | set_status('tritium loaded') 541 | if not return_hud_to_start(): return False 542 | call_next_args(100, *args) 543 | 544 | 545 | def call_next_args(delay_ms=100, *args): 546 | if len(args) > 0: 547 | if len(args) > 1: 548 | new_args = args[1:] 549 | root.after(delay_ms, args[0], *new_args) 550 | else: 551 | root.after(delay_ms, args[0]) 552 | 553 | 554 | # Donate tritium 555 | def donate_tritium(*args): 556 | set_status('Donating tritium') 557 | if not return_hud_to_start(): return False 558 | press(ED_UI_SELECT) 559 | 560 | if not press_until_selected_region(ED_UI_DOWN, TRITIUM_DEPOT_POS, ED_UI_UP): return False 561 | press(ED_UI_SELECT) 562 | sleep(0.5) 563 | if not press_until_selected_region(ED_UI_UP, DONATE_TRITIUM_POS, debug_text="DONATE TRITIUM"): return False 564 | press(ED_UI_SELECT) 565 | if not press_until_selected_region(ED_UI_UP, CONFIRM_DEPOSIT_POS, debug_text="CONFIRM DEPOSIT"): return False 566 | press(ED_UI_SELECT) 567 | set_status('tritium donated') 568 | if not press_until_selected_region(ED_UI_DOWN, TRITIUM_EXIT, debug_text="EXIT DOOR"): return False 569 | press(ED_UI_SELECT) 570 | 571 | return_hud_to_start() 572 | 573 | call_next_args(100, *args) 574 | 575 | 576 | def empty_cargo(): 577 | # Check focus is E:D 578 | if not get_current_focus(): return False 579 | 580 | set_status('Emptying your cargo hold') 581 | if not return_hud_to_start(): return False 582 | 583 | press(ED_RIGHT_WINDOW, 0.5) 584 | if not press_until_selected_region(ED_MENU_RIGHT, INVENTORY_POS, max_count=15, debug_text="INVENTORY"): return False 585 | press(ED_UI_RIGHT) 586 | press(ED_UI_RIGHT) 587 | press(ED_UI_UP) 588 | press(ED_UI_UP) 589 | if not press_until_selected_region(ED_UI_RIGHT, TRANSFER_POS, debug_text="TRANSFER"): return False 590 | press(ED_UI_SELECT) 591 | if not press_until_selected_text(ED_UI_UP, ["TO", "CARRIER"], ED_UI_DOWN): return False 592 | press(ED_UI_SELECT) 593 | sleep(0.5) 594 | press(ED_UI_SELECT) 595 | set_status('Cargo hold emptied') 596 | 597 | return_hud_to_start() 598 | 599 | 600 | # Select route file dialog 601 | def select_route_file(): 602 | global route_file 603 | route_file = fd.askopenfilename() 604 | route_label.config(text=route_file) 605 | load_route(route_file) 606 | settings.route_file = route_file 607 | settings.save_settings() 608 | 609 | 610 | # Update UI with route details and find next waypoint 611 | def update_route_position(): 612 | global next_waypoint, jumping, auto_jump 613 | if len(route) == 0: return 614 | 615 | next_waypoint = "" # Next waypoint requires positive validation 616 | 617 | route_len_label.config(text="{} via {} waypoints".format(route[-1], len(route))) 618 | n = 0 619 | if current_system == route[-1]: 620 | route_pos_label.config(text="Destination reached") 621 | progress.set(100) 622 | jumping = False 623 | auto_jump = False 624 | auto_jump_label.config(text="Route Complete") 625 | 626 | return 627 | 628 | for r in route: 629 | if r == current_system: 630 | next_waypoint = route[n + 1] 631 | route_pos_label.config(text="At waypoint {}, next is {}".format(n, next_waypoint)) 632 | progress.set(100 * n // len(route)) 633 | return 634 | n += 1 635 | route_pos_label.config(text="First waypoint is {}".format(route[0])) 636 | next_waypoint = route[0] 637 | 638 | 639 | def dark_style2(tk_root): 640 | _style = ttk.Style(tk_root) 641 | tk_root.tk.call('source', 'awthemes-10.4.0/awdark.tcl') 642 | _style.theme_use('awdark') 643 | return _style 644 | 645 | 646 | ui_started = False 647 | root: Tk 648 | do_refuel: tkinter.IntVar 649 | DEBUG: tkinter.IntVar 650 | GRAYSCALE: tkinter.IntVar 651 | CONFIDENCE: tkinter.IntVar 652 | progress: tkinter.IntVar 653 | system_label: ttk.Label 654 | route_label: ttk.Label 655 | route_pos_label: ttk.Label 656 | route_jump_label: ttk.Label 657 | route_len_label: ttk.Label 658 | auto_jump_label: ttk.Label 659 | status: ttk.Label 660 | log = "" 661 | if log > "": 662 | ed_log = open(log, 'r') 663 | else: 664 | ed_log = None 665 | 666 | 667 | def on_debug_change(*args): 668 | if DEBUG is not None and DEBUG.get() == 1: 669 | logging.info("Debug enabled") 670 | else: 671 | logging.info("Debug disabled") 672 | check_settings() 673 | 674 | 675 | # Setup UI 676 | def setup_ui(): 677 | global root, ui_started, do_refuel, DEBUG, GRAYSCALE, CONFIDENCE, progress, system_label, route_label, route_jump_label, route_len_label, route_pos_label, status, auto_jump_label, log, ed_log 678 | 679 | root = Tk() 680 | root.title("Fleet Carrier Macros") 681 | root.iconbitmap('images/fc_icon.ico') 682 | dark_style2(root) 683 | 684 | do_refuel = tkinter.IntVar() 685 | DEBUG = tkinter.IntVar() 686 | GRAYSCALE = tkinter.IntVar() 687 | CONFIDENCE = tkinter.StringVar() 688 | CONFIDENCE.set("75") 689 | 690 | DEBUG.trace_add("write", on_debug_change) 691 | GRAYSCALE.trace_add("write", on_debug_change) 692 | CONFIDENCE.trace_add("write", on_debug_change) 693 | 694 | progress = tkinter.IntVar() 695 | 696 | frame = ttk.Frame(root, padding=10) 697 | frame.grid() 698 | row = 0 699 | 700 | ttk.Label(frame, text="Refuel Carrier:").grid(column=0, row=row, sticky="e") 701 | ttk.Label(frame, text="Ctrl+F11").grid(column=1, row=row, sticky="w") 702 | 703 | ttk.Label(frame, text="Empty cargo hold:").grid(column=2, row=row, sticky="e") 704 | ttk.Label(frame, text="Ctrl+F5").grid(column=3, row=row, sticky="w") 705 | row += 1 706 | 707 | ttk.Label(frame, text="Enable autojump:").grid(column=0, row=row, sticky="e") 708 | ttk.Label(frame, text="Alt+F9").grid(column=1, row=row, sticky="w") 709 | 710 | ttk.Label(frame, text="Cancel autojump:").grid(column=2, row=row, sticky="e") 711 | ttk.Label(frame, text="Ctrl+F10").grid(column=3, row=row, sticky="w") 712 | row += 1 713 | 714 | ttk.Label(frame, text="Engage next jump on route:").grid(column=0, row=row, sticky="e") 715 | ttk.Label(frame, text="Ctrl+F9").grid(column=1, row=row, sticky="w") 716 | row += 1 717 | 718 | ttk.Separator(frame, orient="horizontal").grid(column=0, row=row, columnspan=5, sticky="ew") 719 | row += 1 720 | 721 | ttk.Label(frame, text="Current System:").grid(column=0, row=row, sticky="e") 722 | system_label = ttk.Label(frame, text="Unknown") 723 | system_label.grid(column=1, row=row, sticky="w") 724 | row += 1 725 | 726 | ttk.Button(frame, text="Select Route", command=select_route_file).grid(column=0, row=row, sticky="e") 727 | route_label = ttk.Label(frame, text="Unknown") 728 | route_label.grid(column=1, row=row, sticky="w", columnspan=5) 729 | row += 1 730 | 731 | ttk.Label(frame, text="Refuel on jump?").grid(column=0, row=row, sticky="e") 732 | ttk.Checkbutton(frame, variable=do_refuel).grid(column=1, row=row, sticky="w") 733 | row += 1 734 | 735 | ttk.Label(frame, text="Destination:").grid(column=0, row=row, sticky="e") 736 | route_len_label = ttk.Label(frame, text="Unknown") 737 | route_len_label.grid(column=1, row=row, sticky="w", columnspan=3) 738 | row += 1 739 | 740 | ttk.Label(frame, text="Progress:").grid(column=0, row=row, sticky="e") 741 | route_pos_label = ttk.Label(frame, text="Unknown") 742 | route_pos_label.grid(column=1, row=row, sticky="w", columnspan=3) 743 | row += 1 744 | ttk.Progressbar(frame, variable=progress, length=200).grid(column=1, row=row, columnspan=2, sticky="ew") 745 | row += 1 746 | 747 | ttk.Separator(frame, orient="horizontal").grid(column=0, row=row, columnspan=5, sticky="ew") 748 | row += 1 749 | 750 | status = ttk.Label(frame, text="") 751 | status.grid(column=0, row=row, columnspan=5, sticky="w") 752 | row += 1 753 | 754 | ttk.Label(frame, text="Debug Log?").grid(column=0, row=row, sticky="e") 755 | ttk.Checkbutton(frame, variable=DEBUG).grid(column=1, row=row, sticky="w") 756 | # ttk.Label(frame, text="Image matching confidence?").grid(column=2, row=row, sticky="e") 757 | # ttk.Checkbutton(frame, variable=GRAYSCALE).grid(column=3, row=row, sticky="w") 758 | # ttk.Spinbox(frame, from_=10, to=90, textvariable=CONFIDENCE).grid(column=3, row=row, sticky="e") 759 | 760 | row += 1 761 | ttk.Label(frame, text="Version " + VERSION).grid(column=0, row=row, sticky="w") 762 | auto_jump_label = ttk.Label(frame, text="") 763 | auto_jump_label.grid(column=2, row=row, sticky="ew", columnspan=2) 764 | 765 | row += 1 766 | link1 = ttk.Label(frame, text="FCMacros Homepage (github)", cursor="hand2", foreground="lightblue") 767 | link1.grid(column=0, row=row, columnspan=4, sticky="w") 768 | link1.bind("", lambda e: callback("https://github.com/pilotso11/fc-macros")) 769 | ttk.Button(frame, text="Quit", command=root.destroy).grid(column=4, row=row) 770 | 771 | settings.load_settings() 772 | do_refuel.set(settings.refuel) 773 | DEBUG.set(settings.debug) 774 | set_debug_level() 775 | GRAYSCALE.set(settings.grayscale) 776 | CONFIDENCE.set(settings.confidence) 777 | 778 | log = get_latest_log_file() 779 | logging.info(f"Opening E:D log: {log}") 780 | ed_log = open(log, 'r') 781 | 782 | return root 783 | 784 | 785 | def callback(url): 786 | webbrowser.open_new(url) 787 | 788 | 789 | def cool_down_complete(): 790 | global cool_down 791 | cool_down = False 792 | set_status("Carrier cool down complete") 793 | if auto_jump: 794 | schedule_jump() 795 | 796 | 797 | def get_value_from_log_line(line, key): 798 | parts = line.strip('{} ').split(',') 799 | for p in parts: 800 | keys = p.split(':') 801 | k = keys[0].strip('" ') 802 | if k == key: 803 | return keys[1].strip('" ') 804 | return '' 805 | 806 | 807 | def check_newer_log(): 808 | global ed_log, log 809 | newest = get_latest_log_file() 810 | if log != newest: 811 | ed_log.close() 812 | log = newest 813 | logging.info(f"Opening newer E:D log: {log}") 814 | ed_log = open(log, 'r', encoding='utf-8') 815 | root.after(10000, check_newer_log) 816 | 817 | 818 | def process_log(): 819 | global current_system, jumping, cool_down, is_odyssey, ship, carrier_services_loc 820 | try: 821 | lines = ed_log.readlines() 822 | if len(lines) > 0: 823 | logging.debug(f"Got {len(lines)} lines from E:D log") 824 | new_system = '' 825 | new_ship = '' 826 | for line in lines: 827 | line = line.strip() 828 | # logging.debug(line) 829 | if line.count('StarSystem') > 0: 830 | new_system = get_value_from_log_line(line, 'StarSystem') 831 | if line.count('Odyssey') > 0: 832 | ody = get_value_from_log_line(line, 'Odyssey') 833 | if ody == "true": 834 | is_odyssey = True 835 | logging.info("We're in Odyssey") 836 | if line.count('CarrierJumpRequest') > 0: 837 | dest = get_value_from_log_line(line, 'SystemName') 838 | if dest == next_waypoint: 839 | set_status("Jump to {} confirmed".format(dest)) 840 | if line.count('event":"Loadout') > 0: 841 | new_ship = get_value_from_log_line(line, 'Ship') 842 | 843 | # Process last ship 844 | if '' < new_ship != ship: 845 | ship = new_ship 846 | carrier_services_loc = None 847 | logging.info(f"Current ship is '{ship}'") 848 | 849 | # Process last system 850 | if '' < new_system != current_system: 851 | current_system = new_system 852 | if jumping: 853 | jumping = False 854 | cool_down = True 855 | root.after(1000 * 210, cool_down_complete) 856 | reset_to_main_hud_after_jump() 857 | # and into the CARRIER SERVICES 858 | press(ED_UI_SELECT) 859 | 860 | set_status("Current system: " + current_system) 861 | system_label.config(text=current_system) 862 | update_route_position() 863 | root.update_idletasks() 864 | 865 | except (UnicodeDecodeError, ) as err: 866 | logging.warning(f"Exception processing log file: {err}") 867 | 868 | root.after(1000, process_log) 869 | 870 | 871 | # Set a status message 872 | def set_status(msg=''): 873 | status.config(text=msg) 874 | logging.info(msg) 875 | root.update_idletasks() 876 | 877 | 878 | # Check for our macro keys being pressed 879 | def check_keys(): 880 | global auto_jump, jump_one 881 | if kb.is_pressed('ctrl+f11'): 882 | set_status("Refuel requested") 883 | root.after(100, refuel) 884 | root.after(1000, check_keys) 885 | return 886 | if kb.is_pressed('ctrl+f5'): 887 | set_status("Cargo unload requested") 888 | root.after(100, empty_cargo) 889 | root.after(1000, check_keys) 890 | return 891 | if kb.is_pressed('alt+f9'): 892 | set_status("Enable auto jump") 893 | auto_jump = True 894 | auto_jump_label.config(text="Auto Jump Enabled") 895 | root.after(100, schedule_jump) 896 | root.after(1000, check_keys) 897 | return 898 | if kb.is_pressed('ctrl+f9'): 899 | set_status("Scheduling next jump") 900 | jump_one = True 901 | root.after(100, schedule_jump) 902 | root.after(1000, check_keys) 903 | return 904 | if kb.is_pressed('ctrl+f10'): 905 | set_status("Auto jump disabled") 906 | auto_jump = False 907 | auto_jump_label.config(text="") 908 | root.after(1000, check_keys) 909 | return 910 | if kb.is_pressed('Ctrl+F3'): 911 | ocr.capture_debug() 912 | root.after(5000, check_keys) 913 | return 914 | root.after(20, check_keys) 915 | 916 | 917 | # Run the UI 918 | def run_ui(): 919 | ui_root = setup_ui() 920 | check_newer_log() # Start checking for log rotation 921 | process_log() # start log processing 922 | check_settings() # start monitoring settings 923 | check_keys() # start key monitoring 924 | ui_root.mainloop() # run 925 | 926 | 927 | def run_main(): 928 | global ui_started, screen_shape 929 | if ui_started: return 930 | ui_started = True 931 | log_setup() 932 | check_for_themes() 933 | ocr.check_for_tesseract_exe() 934 | run_ui() 935 | 936 | 937 | if __name__ == '__main__': 938 | run_main() 939 | -------------------------------------------------------------------------------- /fcmacros.spec: -------------------------------------------------------------------------------- 1 | # -*- mode: python ; coding: utf-8 -*- 2 | 3 | 4 | block_cipher = None 5 | 6 | added_files = [ 7 | ( 'images/*', 'images' ), 8 | ( 'README.md', '.' ), 9 | ( 'LICENSE', '.' ), 10 | ( 'awthemes-10.4.0.zip', '.' ), 11 | ( 'Tesseract-OCR.zip', '.' ), 12 | ] 13 | 14 | a = Analysis(['fcmacros.py'], 15 | pathex=[], 16 | binaries=[], 17 | datas=added_files, 18 | hiddenimports=["PIL","cv2","pywin32","pytesseract"], 19 | hookspath=[], 20 | hooksconfig={}, 21 | runtime_hooks=[], 22 | excludes=[], 23 | win_no_prefer_redirects=False, 24 | win_private_assemblies=False, 25 | cipher=block_cipher, 26 | noarchive=False) 27 | pyz = PYZ(a.pure, a.zipped_data, 28 | cipher=block_cipher) 29 | 30 | exe = EXE(pyz, 31 | a.scripts, 32 | [], 33 | exclude_binaries=True, 34 | name='fcmacros', 35 | debug=False, 36 | bootloader_ignore_signals=False, 37 | strip=False, 38 | upx=True, 39 | console=False, 40 | icon='images/fc_icon.ico', 41 | disable_windowed_traceback=False, 42 | target_arch=None, 43 | codesign_identity=None, 44 | entitlements_file=None ) 45 | coll = COLLECT(exe, 46 | a.binaries, 47 | a.zipfiles, 48 | a.datas, 49 | strip=False, 50 | upx=True, 51 | upx_exclude=[], 52 | name='fcmacros') 53 | -------------------------------------------------------------------------------- /images/fc_icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pilotso11/fc-macros/ca73379e8de67b1092187f7c32f460929945e64f/images/fc_icon.ico -------------------------------------------------------------------------------- /images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pilotso11/fc-macros/ca73379e8de67b1092187f7c32f460929945e64f/images/icon.png -------------------------------------------------------------------------------- /images/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pilotso11/fc-macros/ca73379e8de67b1092187f7c32f460929945e64f/images/screenshot.png -------------------------------------------------------------------------------- /keymaps.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2022 Seth Osher 2 | # These are the default keybindings 3 | # Change these if you have non standard settings 4 | # See also https://github.com/boppreh/keyboard/blob/master/keyboard/_canonical_names.py 5 | ED_BACK = '\b' 6 | ED_RIGHT_WINDOW = '4' 7 | ED_LEFT_WINDOW = '1' 8 | ED_UI_UP = 'w' 9 | ED_UI_LEFT = 'a' 10 | ED_UI_RIGHT = 'd' 11 | ED_UI_DOWN = 's' 12 | ED_UI_SELECT = 'space' 13 | ED_MENU_LEFT = 'q' 14 | ED_MENU_RIGHT = 'e' 15 | -------------------------------------------------------------------------------- /locations.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2022 Seth Osher 2 | # Offset to adjust for menubar missing on fullscreen 3 | FULLSCREEN_OFFSET = -10 4 | WINDOW_TEST = [635, 0, 100, 10] # Location to test for window border 5 | 6 | # Color depth threshold for selected 7 | ENABLED_THRESHOLD = 128 8 | 9 | # Known locations 10 | # for screen captures 11 | MAX_CAPACITY_POS = [670, 350, 130, 30] 12 | NAVIGATION_ICON = [80, 240, 40, 40] 13 | NAVIGATION_IMAGE = [80, 240, 200, 40] 14 | GALMAP_IMAGE = [250, 875, 230, 50] 15 | GALMAP_SEARCH = [695, 125, 230, 25] 16 | TRITIUM_EXIT = [585, 930, 45, 45] 17 | INVENTORY_POS = [1135, 265, 150, 30] 18 | TRANSFER_POS = [1475, 320, 125, 35] 19 | SET_CARRIER_DESTINATION_POS = [1420, 400, 330, 60] 20 | GO_TO_LOCATION_POS = [1204, 124, 25, 25] 21 | DONATE_TRITIUM_POS = [870, 860, 190, 35] 22 | CONFIRM_DEPOSIT_POS = [850, 830, 225, 25] 23 | CARRIER_MANAGEMENT_POS = [880, 805, 230, 30] 24 | TRITIUM_DEPOT_POS = [250, 805, 160, 30] 25 | TRANSFER_CANCEL_POS = [624, 771, 75, 18] 26 | 27 | is_adjusted_for_fullscreen = False 28 | 29 | 30 | def adjust_for_fullscreen(): 31 | global is_adjusted_for_fullscreen 32 | if is_adjusted_for_fullscreen: return 33 | is_adjusted_for_fullscreen = True 34 | MAX_CAPACITY_POS[1] += FULLSCREEN_OFFSET 35 | NAVIGATION_ICON[1] += FULLSCREEN_OFFSET 36 | NAVIGATION_IMAGE[1] += FULLSCREEN_OFFSET 37 | GALMAP_IMAGE[1] += FULLSCREEN_OFFSET 38 | GALMAP_SEARCH[1] += FULLSCREEN_OFFSET 39 | TRITIUM_EXIT[1] += FULLSCREEN_OFFSET 40 | INVENTORY_POS[1] += FULLSCREEN_OFFSET 41 | TRANSFER_POS[1] += FULLSCREEN_OFFSET 42 | SET_CARRIER_DESTINATION_POS[1] += FULLSCREEN_OFFSET 43 | GO_TO_LOCATION_POS[1] += FULLSCREEN_OFFSET 44 | DONATE_TRITIUM_POS[1] += FULLSCREEN_OFFSET 45 | CONFIRM_DEPOSIT_POS[1] += FULLSCREEN_OFFSET 46 | CARRIER_MANAGEMENT_POS[1] += FULLSCREEN_OFFSET 47 | TRITIUM_DEPOT_POS[1] += FULLSCREEN_OFFSET 48 | TRANSFER_CANCEL_POS[1] += FULLSCREEN_OFFSET 49 | 50 | 51 | carrier_services_locations = { 52 | 'python': (884, 840, 962-884+76, 12), 53 | 'diamondbackxl': (882, 863, 961-882+80, 12), # DBX 54 | 'dolphin': (885, 832, 964-885+78, 12), 55 | 'asp': (883, 865, 962-883+78, 11), 56 | 'empire_trader': (881, 789, 958-881+78, 11), # Imperial Clipper 57 | 'orca': (894, 848, 971-894+76, 11), 58 | 'type9': (878, 843, 961-878+82, 12), 59 | 'belugaliner': (890, 748, 961-890+71, 10), 60 | 'anaconda': (881, 852, 960-881+78, 13), 61 | 'type9_military':(879, 843, 961-879+83, 12), # Type10 62 | 'cutter': (870, 906, 961-870+91, 15), 63 | 'federation_corvette': (882, 853, 961-882+80, 12), 64 | 'empire_eagle': (865, 835, 962-865+96, 14), 65 | 'ferdelance': (867, 815, 960-867+93, 14), 66 | 'mamba': (862, 874, 962-862+99, 16), 67 | 'krait_mkii': (883, 854, 961-883+79, 11), 68 | 'federation_dropship_mkii': (882, 853, 962-882+79, 11), # Federal assault ship 69 | 'federation_dropship': (882, 853, 962-882+79, 11), # Federal drop ship 70 | 'cobramkiii': (879, 869, 962-879+82, 12), 71 | 'empire_courier':(882, 853, 159, 11), 72 | 'diamondback': (882, 863, 962-882+79, 12) # DBS 73 | } 74 | 75 | ####### Notes from screencaps 76 | # Python 77 | # python 78 | # 2022-03-15 20:09:54 - DEBUG - 'CARRIER' conf=96.440369 at 884,840 - 69,12 79 | # 2022-03-15 20:09:54 - DEBUG - 'SERVICES' conf=96.094719 at 962,840 - 76,12 80 | # 81 | # DBX 82 | # diamondbackxl 83 | # 2022-03-15 20:13:21 - DEBUG - 'CARRIER' conf=96.430115 at 882,863 - 71,12 84 | # 2022-03-15 20:13:21 - DEBUG - 'SERVICES' conf=96.853378 at 961,863 - 80,12 85 | # 86 | # Dolphin 87 | # dolphin 88 | # 2022-03-15 20:16:37 - DEBUG - 'CARRIER' conf=91.582306 at 885,832 - 71,12 89 | # 2022-03-15 20:16:37 - DEBUG - 'SERVICES' conf=95.854897 at 964,832 - 78,12 90 | # 91 | # Asp Explorer 92 | # asp 93 | # 2022-03-15 20:17:53 - DEBUG - 'CARRIER' conf=95.824303 at 883,865 - 70,11 94 | # 2022-03-15 20:17:53 - DEBUG - 'SERV' conf=96.637978 at 962,865 - 38,11 95 | # 2022-03-15 20:18:22 - DEBUG - 'CARRIER' conf=96.353050 at 883,865 - 70,11 96 | # 2022-03-15 20:18:22 - DEBUG - 'SERVICES' conf=96.363914 at 962,865 - 78,11 97 | # 98 | # Imperial Clipper 99 | # empire_trader 100 | # 2022-03-15 20:19:43 - DEBUG - 'CARRIER' conf=96.089005 at 881,789 - 69,11 101 | # 2022-03-15 20:19:43 - DEBUG - 'SERVICES' conf=92.866127 at 958,789 - 78,11 102 | # 103 | # Orca 104 | # orca 105 | # 2022-03-15 20:20:48 - DEBUG - 'CARRIER' conf=82.640289 at 894,848 - 69,11 106 | # 2022-03-15 20:20:48 - DEBUG - 'SERVICES' conf=84.101578 at 971,848 - 76,11 107 | # 108 | # Type-9 109 | # type9 110 | # 2022-03-15 20:22:04 - DEBUG - 'CARRIER' conf=81.305870 at 878,843 - 74,13 111 | # 2022-03-15 20:22:04 - DEBUG - 'SERVICES' conf=95.352760 at 961,843 - 82,12 112 | # 113 | # Beluga 114 | # belugaliner 115 | # 2022-03-15 20:26:27 - DEBUG - 'CARRIER' conf=96.448929 at 890,748 - 64,10 116 | # 2022-03-15 20:26:27 - DEBUG - 'SERVICES' conf=96.694786 at 961,748 - 71,10 117 | # 118 | # Anaconda 119 | # anaconda 120 | # 2022-03-15 20:27:39 - DEBUG - 'TARRIER' conf=71.630692 at 879,852 - 72,12 121 | # 2022-03-15 20:27:39 - DEBUG - 'SERVICES' conf=96.245331 at 960,852 - 78, 122 | # 2022-03-15 20:28:27 - DEBUG - 'CARRIER' conf=96.550133 at 881,852 - 70,13 123 | # 2022-03-15 20:28:27 - DEBUG - 'SERVICES' conf=96.760590 at 960,852 - 78,13 124 | # 125 | # Type 10 126 | # type9_military 127 | # 2022-03-15 20:29:47 - DEBUG - 'CARRIER' conf=95.627495 at 879,843 - 74,12 128 | # 2022-03-15 20:29:47 - DEBUG - 'SERVICES' conf=96.276894 at 961,843 - 83,12 129 | # 130 | # Imperial Cutter 131 | # cutter 132 | # 2022-03-15 20:30:58 - DEBUG - 'CARRIER' conf=84.562981 at 870,906 - 82,15 133 | # 2022-03-15 20:30:58 - DEBUG - 'SERVICES' conf=95.720673 at 962,906 - 91,15 134 | # 135 | # Federal Corvette 136 | # federation_corvette 137 | # 2022-03-15 20:31:58 - DEBUG - 'CARRIER' conf=96.335411 at 882,853 - 71,12 138 | # 2022-03-15 20:31:58 - DEBUG - 'SERVICES' conf=95.933846 at 961,853 - 80,12 139 | # 140 | # Imperial Eagle 141 | # empire_eagle 142 | # 2022-03-15 20:33:45 - DEBUG - 'CARRIER' conf=95.286850 at 865,835 - 86,14 143 | # 2022-03-15 20:33:45 - DEBUG - 'SERVICES' conf=96.157745 at 962,835 - 96,14 144 | # 145 | # Fer De Lance 146 | # ferdelance 147 | # 2022-03-15 20:44:38 - DEBUG - 'CARRIER' conf=88.678116 at 867,815 - 84,14 148 | # 2022-03-15 20:44:38 - DEBUG - 'SERVICES' conf=96.354332 at 960,815 - 93,14 149 | # 150 | # Mamba 151 | # mamba 152 | # 2022-03-15 20:38:32 - DEBUG - 'CARRIER' conf=96.125740 at 862,874 - 89,15 153 | # 2022-03-15 20:38:32 - DEBUG - 'SERVICES' conf=96.317787 at 962,873 - 99,16 154 | # 155 | # Krait Mk II 156 | # krait_mkii 157 | # 2022-03-15 20:43:31 - DEBUG - 'CARRIER' conf=96.511787 at 883,854 - 70,11 158 | # 2022-03-15 20:43:31 - DEBUG - 'SERVICES' conf=96.342117 at 961,854 - 79,11 159 | # 160 | # Federal Assault Ship 161 | # federation_dropship_mkii 162 | # 2022-03-15 20:37:15 - DEBUG - 'CARRIER' conf=96.134789 at 882,853 - 71,11 163 | # 2022-03-15 20:37:15 - DEBUG - 'SERVICES' conf=95.974770 at 962,853 - 79,11 164 | # 165 | # Cobra Mk III 166 | # cobramkiii 167 | # 2022-03-15 20:36:14 - DEBUG - 'CARRIER' conf=95.945961 at 879,863 - 74,12 168 | # 2022-03-15 20:36:14 - DEBUG - 'SERVICES' conf=96.082794 at 962,863 - 82,12 169 | # 170 | # Imperial Courier 171 | # empire_courier 172 | # 2022-03-15 20:35:19 - DEBUG - 'CARRIEFCSERVICES' conf=69.902817 at 882,853 - 159,11 173 | # 174 | # DBS 175 | # diamondback 176 | # 2022-03-15 20:34:29 - DEBUG - 'CARRIER' conf=96.507072 at 882,863 - 71,12 177 | # 2022-03-15 20:34:29 - DEBUG - 'SERVICES' conf=96.700348 at 962,863 - 79,12 178 | 179 | 180 | -------------------------------------------------------------------------------- /make_dist.cmd: -------------------------------------------------------------------------------- 1 | pyinstaller fcmacros.spec --noconfirm 2 | 3 | -------------------------------------------------------------------------------- /ocr.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2022 Seth Osher 2 | # OCR and Screen Image detection for fcmacros 3 | import os 4 | 5 | import cv2 6 | import pyautogui 7 | import pytesseract 8 | import numpy as np 9 | import fcmacros 10 | from time import sleep 11 | from keymaps import * 12 | from locations import * 13 | import logging 14 | import glob 15 | import zipfile 16 | 17 | # Teserect installer for windows 18 | # https://github.com/UB-Mannheim/tesseract/wiki 19 | pytesseract.pytesseract.tesseract_cmd = "./Tesseract-OCR/tesseract.exe" 20 | custom_config = r'--oem 3 --psm 12' 21 | 22 | 23 | # Get screenshot as CV image 24 | def get_cv_screenshot(region=None): 25 | if region is None: 26 | pil_image = pyautogui.screenshot() 27 | else: 28 | pil_image = pyautogui.screenshot(region=region) 29 | image = np.array(pil_image) 30 | image = image[:, :, ::-1].copy() 31 | return image 32 | 33 | 34 | # Take a screenshot and save it , with region x, y, w, h, return CV image 35 | def get_and_save_cv_screenshot_region(file, region): 36 | pil_image = pyautogui.screenshot(file, region=region) 37 | image = np.array(pil_image) 38 | image = image[:, :, ::-1].copy() 39 | return image 40 | 41 | 42 | # Take a screenshot and save it , with region, x, y, x2, y2, return CV image 43 | def get_and_save_cv_screenshot(file, x, y, x2, y2): 44 | pil_image = pyautogui.screenshot(file, region=(x - 5, y - 5, x2 - x + 10, y2 - y + 10)) 45 | image = np.array(pil_image) 46 | image = image[:, :, ::-1].copy() 47 | return image 48 | 49 | 50 | def get_carrier_services_loc(): 51 | cnt = 0 52 | while not fcmacros.get_current_focus(): 53 | cnt += 1 54 | fcmacros.set_status(f'Looking for E:D {cnt}') 55 | if cnt > 5: 56 | fcmacros.set_status(f'E:D Does not have focus, aborting') 57 | return None 58 | sleep(2) 59 | fcmacros.press(ED_BACK) 60 | fcmacros.press(ED_BACK) 61 | cnt = 0 62 | while True: 63 | image = get_cv_screenshot() 64 | gray_image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) 65 | results = recognise(gray_image) 66 | mid_al = find_words(["LAUNCH"], results) 67 | mid_cs = find_words(["CARRIER", "SERVICES"], results) 68 | if mid_cs is None and mid_al is not None: 69 | logging.debug(f"Found LAUNCH at {mid_al}") 70 | fcmacros.press(ED_UI_DOWN, delay=0.5) 71 | else: 72 | if mid_cs is not None: 73 | logging.info(f"Found CARRIER SERVICES at {mid_cs}") 74 | return mid_cs 75 | else: 76 | fcmacros.press(ED_UI_UP, delay=0.5) 77 | cnt += 1 78 | if cnt > 5: 79 | logging.warning("Unable to find CARRIER SERVICES after 5 loops, aborting.") 80 | return None 81 | 82 | 83 | # Screencap CARRIER SERVICES selected 84 | def capture_carrier_services(debug=False): 85 | cnt = 0 86 | while not fcmacros.get_current_focus(): 87 | cnt += 1 88 | fcmacros.set_status(f'Looking for E:D {cnt}') 89 | if cnt > 5: 90 | fcmacros.set_status(f'E:D Does not have focus, aborting') 91 | return False 92 | sleep(2) 93 | 94 | fcmacros.press(ED_BACK) 95 | fcmacros.press(ED_BACK) 96 | while True: 97 | image = get_cv_screenshot() 98 | gray_image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) 99 | results = recognise(gray_image) 100 | mid_al = find_words(["LAUNCH"], results) 101 | mid_cs = find_words(["CARRIER", "SERVICES"], results) 102 | if mid_cs is None and mid_al is not None: 103 | logging.debug(f"Found LAUNCH at {mid_al}") 104 | fcmacros.press(ED_UI_DOWN, delay=0.5) 105 | else: 106 | if mid_cs is not None: 107 | logging.debug(f"Found CARRIER SERVICES at {mid_cs}") 108 | fcmacros.press(ED_UI_UP, delay=0.25) 109 | fcmacros.press(ED_UI_UP, delay=0.25) 110 | fcmacros.press(ED_UI_UP, delay=0.25) 111 | fcmacros.press(ED_UI_UP, delay=0.25) 112 | fcmacros.press(ED_UI_DOWN, delay=0.25) 113 | x, y, x2, y2 = mid_cs[0], mid_cs[1], mid_cs[2], mid_cs[3] 114 | part = get_and_save_cv_screenshot('images/carrier_services99.png', x, y, x2, y2) 115 | fcmacros.set_status("Saved carrier_services99.png") 116 | if debug: 117 | cv2.imshow('screencap', part) 118 | cv2.waitKey(0) 119 | fcmacros.root.after(100, after_carrier_services) 120 | return True 121 | else: 122 | fcmacros.press(ED_UI_UP, delay=0.5) 123 | 124 | 125 | # Capture, regocnise, show and save the current screen 126 | def test(): 127 | image = get_cv_screenshot() 128 | # cv2.imshow('screen', image) 129 | # cv2.waitKey(0) 130 | gray_image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) 131 | # gray_image = cv2.bitwise_not(gray_image) 132 | # gray_image = cv2.threshold(gray_image, 0, 255, cv2.THRESH_BINARY | cv2.THRESH_OTSU)[1] 133 | # gray_image = cv2.adaptiveThreshold(gray_image, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY_INV, 5, 2) 134 | # edges = cv2.Canny(image, 50, 150) 135 | 136 | # threshold_image = cv2.threshold(gray_image, 50, 200, cv2.THRESH_BINARY | cv2.THRESH_OTSU)[1] 137 | # cv2.imshow('original', image) 138 | # cv2.imshow('gray image', gray_image) 139 | # cv2.waitKey(0) 140 | # cv2.destroyAllWindows() 141 | 142 | recognise(gray_image, confidence=51, debug=True, show=True, save=True) 143 | 144 | 145 | # Recognize text in image 146 | # If debug is true then: 147 | # Text recognized with details will be logged 148 | # Show will create bounding boxes on an image copy and show it 149 | # Save will save the image 150 | def recognise(image, debug=False, show=False, save=False, confidence=80, save_suffix=""): 151 | details = pytesseract.image_to_data(image, output_type=pytesseract.Output.DICT, config=custom_config, lang='eng') 152 | 153 | if debug: 154 | # print(details['text']) 155 | total_boxes = len(details['text']) 156 | image = cv2.cvtColor(image, cv2.COLOR_GRAY2BGR) 157 | # add rectangles on threshold image 158 | for sequence_number in range(total_boxes): 159 | if float(details['conf'][sequence_number]) > confidence and len(details['text'][sequence_number]) > 1: 160 | (x, y, w, h) = ( 161 | details['left'][sequence_number], details['top'][sequence_number], 162 | details['width'][sequence_number], 163 | details['height'][sequence_number]) 164 | image = cv2.rectangle(image, (x, y), (x + w, y + h), (0, 255, 0), 2) 165 | logging.debug( 166 | f"'{details['text'][sequence_number]}' conf={details['conf'][sequence_number]} at {x},{y} - {w},{h}") 167 | if show: 168 | cv2.imshow('captured text', image) 169 | cv2.waitKey(0) 170 | cv2.destroyAllWindows() 171 | if save: 172 | cv2.imwrite(f'debugimages/debug_{save_suffix}.png', image) 173 | 174 | return details 175 | 176 | 177 | # Find min distance of two boxes (H or V, it doesn't look for a diagonal) 178 | def get_box_dist(x1, y1, w1, h1, x2, y2): 179 | d = 1000 180 | d = min(d, abs(x1 - x2)) 181 | d = min(d, abs(y1 - y2)) 182 | d = min(d, abs(x1 + w1 - x2)) 183 | d = min(d, abs(y1 + h1 - y2)) 184 | return d 185 | 186 | 187 | # Find bounding area for a set of locations (close words) 188 | def get_box_from_set(locs): 189 | max_x = 0 190 | min_x = 10000 191 | max_y = 0 192 | min_y = 10000 193 | for loc in locs: 194 | x, y, w, h = loc[0], loc[1], loc[2], loc[3] 195 | max_x = max(max_x, x + w) 196 | min_x = min(min_x, x) 197 | max_y = max(max_y, y + h) 198 | min_y = min(min_y, y) 199 | 200 | return min_x, min_y, max_x, max_y 201 | 202 | 203 | # Search for words near each other in the output 204 | def find_words(words, results, confidence=90, max_dist=50): 205 | for i in range(len(results['text'])): 206 | conf = float(results['conf'][i]) 207 | text = results['text'][i] 208 | (x, y, w, h) = (results['left'][i], results['top'][i], results['width'][i], results['height'][i]) 209 | if conf > confidence and text == words[0]: 210 | logging.debug(f"found {words[0]} at {x},{y} {w}x{h}") 211 | loc = [(x, y, w, h)] 212 | if len(words) == 1: # just one word 213 | return get_box_from_set(loc) 214 | 215 | for word in words[1:]: 216 | logging.debug(f"Looking for {word}") 217 | for j in range(len(results['text'])): 218 | conf = float(results['conf'][j]) 219 | text = results['text'][j] 220 | (x2, y2, w2, h2) = ( 221 | results['left'][j], results['top'][j], results['width'][j], results['height'][j]) 222 | if conf > confidence and text[:len(word)] == word: 223 | if get_box_dist(x, y, w, h, x2, y2) < max_dist: # w2, h2 not used 224 | logging.debug(f"found {word} at {x2},{y2} {w2}x{h2}") 225 | loc.append((x2, y2, w2, h2)) 226 | if len(loc) == len(words): 227 | logging.debug("Found all words") 228 | return get_box_from_set(loc) 229 | return None 230 | 231 | 232 | def check_for_tesseract_exe(): 233 | r = glob.glob("./Tesseract-OCR/tesseract.exe") 234 | if len(r) == 0: 235 | with zipfile.ZipFile("Tesseract-OCR.zip", "r") as zipf: 236 | zipf.extractall(".") 237 | 238 | 239 | if __name__ == '__main__': 240 | check_for_tesseract_exe() 241 | test() 242 | 243 | 244 | def get_screen_width(): 245 | image = get_cv_screenshot() 246 | logging.info(f"Screen shape: {image.shape}") 247 | return image.shape 248 | 249 | 250 | def is_text_on_screen_list(words_list, region=None, debug=False, save="", show=False): 251 | image = get_cv_screenshot(region) 252 | gray_image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) 253 | 254 | results = recognise(gray_image, confidence=51, debug=debug, save=len(save) > 0, show=show, save_suffix=save) 255 | for words in words_list: 256 | res = find_words(words, results, confidence=51) 257 | if res is not None: 258 | logging.debug(f"Found {words} at {res}") 259 | return True, res 260 | 261 | return False, None 262 | 263 | 264 | def is_text_on_screen(words, region=None, debug=False, save="", show=False): 265 | image = get_cv_screenshot(region) 266 | gray_image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) 267 | 268 | results = recognise(gray_image, confidence=51, debug=debug, save=len(save) > 0, show=show, save_suffix=save) 269 | res = find_words(words, results, confidence=51) 270 | if res is None: return False, None 271 | return True, res 272 | 273 | 274 | def get_average_color(region, debug_text=""): 275 | image = get_cv_screenshot(region) 276 | avg_color_per_row = np.average(image, axis=0) 277 | avg_color = np.average(avg_color_per_row, axis=0) 278 | logging.debug(f"Average color for {debug_text} at {region} is {avg_color}") 279 | return avg_color 280 | 281 | 282 | def get_average_color_gray(region, debug_text=""): 283 | image = get_cv_screenshot(region) 284 | gray_image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) 285 | avg_color_per_row = np.average(gray_image, axis=0) 286 | avg_color = np.average(avg_color_per_row, axis=0) 287 | logging.debug(f"Average color for {debug_text} at {region} is {avg_color}") 288 | return avg_color 289 | 290 | 291 | def get_average_color_bw(region, debug_text=""): 292 | image = get_cv_screenshot(region) 293 | cv2.imwrite(f'debugimages/debug_{debug_text}_color.png', image) 294 | gray_image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) 295 | cv2.imwrite(f'debugimages/debug_{debug_text}_gray.png', gray_image) 296 | gray_image = cv2.threshold(gray_image, 0, 255, cv2.THRESH_BINARY | cv2.THRESH_OTSU)[1] 297 | cv2.imwrite(f'debugimages/debug_{debug_text}_thresh.png', gray_image) 298 | avg_color_per_row = np.average(gray_image, axis=0) 299 | avg_color = np.average(avg_color_per_row, axis=0) 300 | logging.debug(f"Average color for {debug_text} at {region} is {avg_color}") 301 | return avg_color 302 | 303 | 304 | # Capture the screen and ORR it, show the results 305 | def capture_debug(): 306 | check_for_tesseract_exe() 307 | test() 308 | 309 | 310 | # Capture all needed images 311 | # Work in progresss .... 312 | def capture_all_images(): 313 | check_for_tesseract_exe() 314 | # capture_carrier_services() 315 | 316 | 317 | def after_carrier_services(*args): 318 | fcmacros.set_status("Saved images .... ") 319 | 320 | 321 | def is_fullscreen(): 322 | f = glob.glob("debugimages") 323 | if len(f) == 0: 324 | os.mkdir("debugimages") 325 | logging.info("Created debugimages folder") 326 | 327 | val = get_average_color_bw(WINDOW_TEST) <= ENABLED_THRESHOLD 328 | logging.info(f"Fullscreen={val}") 329 | return val 330 | -------------------------------------------------------------------------------- /setup.cmd: -------------------------------------------------------------------------------- 1 | pip install keyboard pyautogui pillow opencv-python usersettings pywin32 pytesseract 2 | --------------------------------------------------------------------------------